diff --git a/StikDebug/Device/JITEnableContext.swift b/StikDebug/Device/JITEnableContext.swift index 656004b..bb2d670 100644 --- a/StikDebug/Device/JITEnableContext.swift +++ b/StikDebug/Device/JITEnableContext.swift @@ -642,6 +642,31 @@ final class JITEnableContext { } } + func relaunchApp(_ bundleID: String, logger: LogFunc? = nil) -> Bool { + do { + let pid = try withConnectedRemoteServer { remoteServer in + try withProcessControl(remoteServer: remoteServer) { processControl in + var pid: UInt64 = 0 + let ffiError = bundleID.withCString { bundleID in + process_control_launch_app(processControl, bundleID, nil, 0, nil, 0, false, false, &pid) + } + + if let ffiError { + throw error(from: ffiError, fallback: "Failed to return to app") + } + + return pid + } + } + + emitLog("Returned to \(bundleID) (PID \(pid))", logger: logger) + return true + } catch { + emitLog("Failed to return to \(bundleID): \(error.localizedDescription)", logger: logger) + return false + } + } + func startSyslogRelay(handler: @escaping SyslogLineHandler, onError: @escaping SyslogErrorHandler) { do { try ensureTunnel() diff --git a/StikDebug/JSSupport/RunJSView.swift b/StikDebug/JSSupport/RunJSView.swift index 0d5ef1e..674bc32 100644 --- a/StikDebug/JSSupport/RunJSView.swift +++ b/StikDebug/JSSupport/RunJSView.swift @@ -22,12 +22,17 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable var debugProxy: OpaquePointer? var remoteServer: OpaquePointer? var semaphore: DispatchSemaphore? - - init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: DispatchSemaphore?) { + let resumeBundleID: String? + + private let resumeLock = NSLock() + private var hasResumedApp = false + + init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: DispatchSemaphore?, resumeBundleID: String? = nil) { self.pid = pid self.debugProxy = debugProxy self.remoteServer = remoteServer self.semaphore = semaphore + self.resumeBundleID = resumeBundleID } func runScript(path: URL, scriptName: String? = nil) throws { @@ -57,8 +62,14 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable self.context?.exception = JSValue(object: "Script execution is interrupted by StikDebug.", in: self.context!) return "" } - - return handleJSContextSendDebugCommand(self.context, commandStr, self.debugProxy) ?? "" + + let response = handleJSContextSendDebugCommand(self.context, commandStr, self.debugProxy) ?? "" + + if commandStr.hasPrefix("vAttach"), response.hasPrefix("T") { + self.resumeTargetApp() + } + + return response } let logFunction: @convention(block) (String) -> Void = { logStr in @@ -78,8 +89,13 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable let hasTXMFunction: @convention(block) () -> Bool = { return ProcessInfo.processInfo.hasTXM } - + + let resumeAppFunction: @convention(block) () -> String = { + return self.resumeTargetApp() + } + context = JSContext() + context?.setObject(resumeAppFunction, forKeyedSubscript: "resume_app" as NSString) context?.setObject(hasTXMFunction, forKeyedSubscript: "hasTXM" as NSString) context?.setObject(getPidFunction, forKeyedSubscript: "get_pid" as NSString) context?.setObject(sendCommandFunction, forKeyedSubscript: "send_command" as NSString) @@ -101,6 +117,35 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable } } + @discardableResult + private func resumeTargetApp() -> String { + resumeLock.lock() + if hasResumedApp { + resumeLock.unlock() + return "OK" + } + guard let resumeBundleID else { + resumeLock.unlock() + return "no app to return to for this session" + } + hasResumedApp = true + resumeLock.unlock() + + appendLog("Returning to \(resumeBundleID)...") + let success = JITEnableContext.shared.relaunchApp(resumeBundleID) + guard success else { + appendLog("Failed to return to \(resumeBundleID) — continuing in the foreground.") + return "failed to return to \(resumeBundleID)" + } + return "OK" + } + + private func appendLog(_ message: String) { + DispatchQueue.main.async { + self.logs.append(message) + } + } + private func captureScreenshot(named preferredName: String?) -> String { if executionInterrupted { raiseException("Script execution is interrupted by StikDebug.") diff --git a/StikDebug/Views/HomeView.swift b/StikDebug/Views/HomeView.swift index a647853..d9a592b 100644 --- a/StikDebug/Views/HomeView.swift +++ b/StikDebug/Views/HomeView.swift @@ -259,12 +259,13 @@ struct HomeView: View { .accessibilityLabel(feedback.message) } - private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback { + private func getJsCallback(_ script: Data, name: String? = nil, resumeBundleID: String? = nil) -> DebugAppCallback { return { pid, debugProxyHandle, remoteServerHandle, semaphore in let model = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, remoteServer: remoteServerHandle, - semaphore: semaphore) + semaphore: semaphore, + resumeBundleID: resumeBundleID) DispatchQueue.main.async { scriptRunModel = model @@ -338,9 +339,11 @@ struct HomeView: View { scriptData = preferred.data } + let resumeBundleID = pid == nil ? nil : bundleID + var callback: DebugAppCallback? = nil if ProcessInfo.processInfo.hasTXM, let sd = scriptData { - callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script") + callback = getJsCallback(sd, name: scriptName ?? bundleID ?? "Script", resumeBundleID: resumeBundleID) } var lastDebugMessage: String?