Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/build_ipa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ on:

permissions:
contents: write

jobs:
build:
name: Build Debug IPA
runs-on: macos-latest
env:
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'false' }}

steps:
- name: Checkout
Expand Down Expand Up @@ -49,14 +51,15 @@ jobs:
rm -rf Payload StikDebug.app

- name: Upload Debug IPA (artifact)
if: env.UPLOAD_IPA == 'true'
uses: actions/upload-artifact@v4
with:
name: StikDebug-Debug.ipa
path: StikDebug.ipa
retention-days: 90

- name: Create or Update GitHub Release (GitHub-Alpha)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
if: env.UPLOAD_IPA == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: softprops/action-gh-release@v2
with:
tag_name: GitHub-Alpha
Expand Down
2 changes: 1 addition & 1 deletion DebugWidget/DebugWidgetBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import SwiftUI
@main
struct StikDebugWidgetBundle: WidgetBundle {
var body: some Widget {
// Both widgets enabled: Favorites uses enable-jit URL scheme (with continued processing handled in-app),
// Both widgets enabled: Favorites uses enable-jit URL scheme (with PiP/script handled in-app),
// System Apps uses launch-app URL scheme for non-debug launch behavior.
FavoritesWidget()
SystemAppsWidget()
Expand Down
22 changes: 22 additions & 0 deletions StikDebug.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
17C744F02E20BED000834F17 /* Pipify in Frameworks */ = {isa = PBXBuildFile; productRef = 17C744EF2E20BED000834F17 /* Pipify */; };
68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68D1FA3F2E847E4A0028A0EA /* StoreKit.framework */; };
68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BD2E1B415700A5BA36 /* CodeEditorView */; };
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BF2E1B415700A5BA36 /* LanguageSupport */; };
Expand Down Expand Up @@ -153,6 +154,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
17C744F02E20BED000834F17 /* Pipify in Frameworks */,
DCBA85862E3897BD00E88C06 /* StikImporter in Frameworks */,
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */,
68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */,
Expand Down Expand Up @@ -270,6 +272,7 @@
packageProductDependencies = (
68D569BD2E1B415700A5BA36 /* CodeEditorView */,
68D569BF2E1B415700A5BA36 /* LanguageSupport */,
17C744EF2E20BED000834F17 /* Pipify */,
DCBA85852E3897BD00E88C06 /* StikImporter */,
68E714E52E6AA2B00025610F /* ZIPFoundation */,
);
Expand Down Expand Up @@ -382,11 +385,13 @@
en,
Base,
es,
it,
);
mainGroup = DC6F1D2E2D94EADD0071B2B6;
minimizedProjectReferenceProxies = 1;
packageReferences = (
68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */,
17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */,
DCBA85842E3897BD00E88C06 /* XCRemoteSwiftPackageReference "StikImporter" */,
68E714E42E6AA2B00025610F /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
);
Expand Down Expand Up @@ -699,7 +704,9 @@
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = StikJIT/Info.plist;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "StikDebug needs access to devices on your local network so it can connect to the targets you add to the Device Library.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand Down Expand Up @@ -755,7 +762,9 @@
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = StikJIT/Info.plist;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "StikDebug needs access to devices on your local network so it can connect to the targets you add to the Device Library.";
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
Expand Down Expand Up @@ -997,6 +1006,14 @@
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/hugeBlack/swiftui-pipify";
requirement = {
branch = main;
kind = branch;
};
Comment on lines +1009 to +1015
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The swiftui-pipify Swift Package is referenced using a mutable branch (branch = main) instead of an immutable version tag or commit hash, which creates a supply chain risk: any compromise or force-push on that branch could silently change the code that gets built into your app. An attacker who gains control of the GitHub repository or the dependency’s distribution path could inject malicious code that runs with your app’s privileges (including access to network and any in-app secrets). Pin this dependency to a specific, vetted version or commit SHA and update it only through intentional dependency bumps to limit the impact of a compromised upstream.

Copilot uses AI. Check for mistakes.
};
68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mchakravarty/CodeEditorView";
Expand Down Expand Up @@ -1024,6 +1041,11 @@
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
17C744EF2E20BED000834F17 /* Pipify */ = {
isa = XCSwiftPackageProductDependency;
package = 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */;
productName = Pipify;
};
68D569BD2E1B415700A5BA36 /* CodeEditorView */ = {
isa = XCSwiftPackageProductDependency;
package = 68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
12 changes: 3 additions & 9 deletions StikJIT/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,11 @@
</array>
</dict>
</array>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<key>NSBonjourServices</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).continuedProcessingTask.script</string>
<string>_stikdebug._tcp</string>
<string>_stikdebug._udp</string>
</array>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
<string>processing</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UIFileSharingEnabled</key>
<true/>
</dict>
Expand Down
165 changes: 134 additions & 31 deletions StikJIT/JSSupport/RunJSView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,23 @@
import SwiftUI
import JavaScriptCore

typealias RemoteServerHandle = OpaquePointer
typealias ScreenshotClientHandle = OpaquePointer

class RunJSViewModel: ObservableObject {
var context: JSContext?
@Published var logs: [String] = []
@Published var scriptName: String = "Script"
@Published var executionInterrupted = false
var pid: Int
var debugProxy: OpaquePointer?
var remoteServer: OpaquePointer?
var semaphore: dispatch_semaphore_t?
private var progressTimer: DispatchSourceTimer?
private var reportedProgress: Double = 0

init(pid: Int, debugProxy: OpaquePointer?, semaphore: dispatch_semaphore_t?) {
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: dispatch_semaphore_t?) {
self.pid = pid
self.debugProxy = debugProxy
self.remoteServer = remoteServer
self.semaphore = semaphore
}

Expand All @@ -32,7 +35,6 @@ class RunJSViewModel: ObservableObject {
func runScript(data: Data, name: String? = nil) throws {
let scriptContent = String(data: data, encoding: .utf8)
scriptName = name ?? "Script"
startContinuedProcessing(withTitle: scriptName)

let getPidFunction: @convention(block) () -> Int = {
return self.pid
Expand Down Expand Up @@ -61,6 +63,10 @@ class RunJSViewModel: ObservableObject {
return handleJITPageWrite(self.context, startAddr, regionSize, self.debugProxy) ?? ""
}

let takeScreenshotFunction: @convention(block) (String?) -> String? = { fileName in
return self.captureScreenshot(named: fileName)
}

let hasTXMFunction: @convention(block) () -> Bool = {
return ProcessInfo.processInfo.hasTXM
}
Expand All @@ -70,6 +76,7 @@ class RunJSViewModel: ObservableObject {
context?.setObject(getPidFunction, forKeyedSubscript: "get_pid" as NSString)
context?.setObject(sendCommandFunction, forKeyedSubscript: "send_command" as NSString)
context?.setObject(prepareMemoryRegionFunction, forKeyedSubscript: "prepare_memory_region" as NSString)
context?.setObject(takeScreenshotFunction, forKeyedSubscript: "take_screenshot" as NSString)
context?.setObject(logFunction, forKeyedSubscript: "log" as NSString)

context?.evaluateScript(scriptContent)
Expand All @@ -81,43 +88,139 @@ class RunJSViewModel: ObservableObject {
if let exception = self.context?.exception {
self.logs.append(exception.debugDescription)
}
let success = self.context?.exception == nil && !self.executionInterrupted
self.stopContinuedProcessing(success: success)
self.logs.append("Script Execution Completed")
self.logs.append("Background processing finished. You can dismiss this view.")
self.logs.append("You are safe to close the PIP Window.")
}
}

private func captureScreenshot(named preferredName: String?) -> String {
if executionInterrupted {
raiseException("Script execution is interrupted by StikDebug.")
return ""
}
guard let remoteServer else {
raiseException("Screenshot capture is unavailable in the current session.")
return ""
}

var screenshotClient: ScreenshotClientHandle?
let creationError = screenshot_client_new(remoteServer, &screenshotClient)
if let creationError {
let message = describeIdeviceError(creationError)
idevice_error_free(creationError)
raiseException("Failed to create screenshot client: \(message)")
return ""
}
guard let screenshotClient else {
raiseException("Failed to allocate screenshot client.")
return ""
}
defer { screenshot_client_free(screenshotClient) }

var buffer: UnsafeMutablePointer<UInt8>?
var length: UInt = 0
let captureError = screenshot_client_take_screenshot(screenshotClient, &buffer, &length)
if let captureError {
let message = describeIdeviceError(captureError)
idevice_error_free(captureError)
raiseException("Failed to take screenshot: \(message)")
return ""
}
guard let buffer else {
raiseException("Device returned empty screenshot data.")
return ""
}
defer { idevice_data_free(buffer, length) }

let data = Data(bytes: buffer, count: Int(length))
do {
let fileURL = try screenshotFileURL(preferredName: preferredName)
try data.write(to: fileURL, options: .atomic)
return fileURL.path
} catch {
raiseException("Failed to save screenshot: \(error.localizedDescription)")
return ""
}
}

private func screenshotFileURL(preferredName: String?) throws -> URL {
let directory = URL.documentsDirectory.appendingPathComponent("screenshots", isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
let fileManager = FileManager.default
let initialName = sanitizedScreenshotName(from: preferredName)
var targetURL = directory.appendingPathComponent(initialName)
guard fileManager.fileExists(atPath: targetURL.path) else {
return targetURL
}

let baseName = targetURL.deletingPathExtension().lastPathComponent
let ext = targetURL.pathExtension.isEmpty ? "png" : targetURL.pathExtension
var counter = 1
repeat {
let candidate = "\(baseName)-\(counter).\(ext)"
targetURL = directory.appendingPathComponent(candidate)
counter += 1
} while fileManager.fileExists(atPath: targetURL.path)
return targetURL
}

private func startContinuedProcessing(withTitle title: String) {
guard ContinuedProcessingManager.shared.isSupported,
UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) else { return }
stopProgressTimer()
reportedProgress = 0.05
ContinuedProcessingManager.shared.begin(title: title, subtitle: "Script execution in progress")
ContinuedProcessingManager.shared.updateProgress(reportedProgress)
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
timer.schedule(deadline: .now() + 5, repeating: 5)
timer.setEventHandler { [weak self] in
guard let self else { return }
self.reportedProgress = min(0.9, self.reportedProgress + 0.1)
ContinuedProcessingManager.shared.updateProgress(self.reportedProgress)
if self.reportedProgress >= 0.9 {
self.stopProgressTimer()
private func sanitizedScreenshotName(from preferredName: String?) -> String {
let defaultName = "screenshot-\(Int(Date().timeIntervalSince1970))"
guard var candidate = preferredName?.trimmingCharacters(in: .whitespacesAndNewlines),
!candidate.isEmpty else {
return "\(defaultName).png"
}

let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_."))
var sanitized = ""
sanitized.reserveCapacity(candidate.count)
for scalar in candidate.unicodeScalars {
if allowed.contains(scalar) {
sanitized.append(Character(scalar))
} else {
sanitized.append("_")
}
}
timer.resume()
progressTimer = timer
if sanitized.isEmpty {
sanitized = defaultName
}
if !sanitized.lowercased().hasSuffix(".png") {
sanitized += ".png"
}
return sanitized
}

private func describeIdeviceError(_ error: UnsafeMutablePointer<IdeviceFfiError>) -> String {
if let messagePointer = error.pointee.message {
return "[\(error.pointee.code)] \(String(cString: messagePointer))"
}
return "[\(error.pointee.code)] Unknown error"
}

private func stopContinuedProcessing(success: Bool) {
stopProgressTimer()
ContinuedProcessingManager.shared.updateProgress(1.0)
ContinuedProcessingManager.shared.finish(success: success)
private func raiseException(_ message: String) {
guard let context else { return }
context.exception = JSValue(object: message, in: context)
}
}

private func stopProgressTimer() {
progressTimer?.cancel()
progressTimer = nil
struct RunJSViewPiP: View {
@Binding var model: RunJSViewModel?
@State private var logs: [String] = []
private let timer = Timer.publish(every: 0.034, on: .main, in: .common).autoconnect()

var body: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(logs.suffix(6).indices, id: \.self) { index in
Text(logs.suffix(6)[index])
.font(.system(size: 12))
.foregroundStyle(.white)
}
}
.padding()
.onReceive(timer) { _ in
logs = model?.logs ?? []
}
.frame(width: 300, height: 150)
}
}

Expand Down
Loading