Return to the target app after attaching for JIT

This commit is contained in:
CelloSerenity 2026-07-25 13:17:25 -06:00
parent c5b23e2282
commit 5e0e48d4e6
3 changed files with 81 additions and 8 deletions

View file

@ -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) { func startSyslogRelay(handler: @escaping SyslogLineHandler, onError: @escaping SyslogErrorHandler) {
do { do {
try ensureTunnel() try ensureTunnel()

View file

@ -22,12 +22,17 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
var debugProxy: OpaquePointer? var debugProxy: OpaquePointer?
var remoteServer: OpaquePointer? var remoteServer: OpaquePointer?
var semaphore: DispatchSemaphore? var semaphore: DispatchSemaphore?
let resumeBundleID: String?
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: DispatchSemaphore?) { private let resumeLock = NSLock()
private var hasResumedApp = false
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: DispatchSemaphore?, resumeBundleID: String? = nil) {
self.pid = pid self.pid = pid
self.debugProxy = debugProxy self.debugProxy = debugProxy
self.remoteServer = remoteServer self.remoteServer = remoteServer
self.semaphore = semaphore self.semaphore = semaphore
self.resumeBundleID = resumeBundleID
} }
func runScript(path: URL, scriptName: String? = nil) throws { func runScript(path: URL, scriptName: String? = nil) throws {
@ -58,7 +63,13 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
return "" 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 let logFunction: @convention(block) (String) -> Void = { logStr in
@ -79,7 +90,12 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
return ProcessInfo.processInfo.hasTXM return ProcessInfo.processInfo.hasTXM
} }
let resumeAppFunction: @convention(block) () -> String = {
return self.resumeTargetApp()
}
context = JSContext() context = JSContext()
context?.setObject(resumeAppFunction, forKeyedSubscript: "resume_app" as NSString)
context?.setObject(hasTXMFunction, forKeyedSubscript: "hasTXM" as NSString) context?.setObject(hasTXMFunction, forKeyedSubscript: "hasTXM" as NSString)
context?.setObject(getPidFunction, forKeyedSubscript: "get_pid" as NSString) context?.setObject(getPidFunction, forKeyedSubscript: "get_pid" as NSString)
context?.setObject(sendCommandFunction, forKeyedSubscript: "send_command" 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 { private func captureScreenshot(named preferredName: String?) -> String {
if executionInterrupted { if executionInterrupted {
raiseException("Script execution is interrupted by StikDebug.") raiseException("Script execution is interrupted by StikDebug.")

View file

@ -259,12 +259,13 @@ struct HomeView: View {
.accessibilityLabel(feedback.message) .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 return { pid, debugProxyHandle, remoteServerHandle, semaphore in
let model = RunJSViewModel(pid: Int(pid), let model = RunJSViewModel(pid: Int(pid),
debugProxy: debugProxyHandle, debugProxy: debugProxyHandle,
remoteServer: remoteServerHandle, remoteServer: remoteServerHandle,
semaphore: semaphore) semaphore: semaphore,
resumeBundleID: resumeBundleID)
DispatchQueue.main.async { DispatchQueue.main.async {
scriptRunModel = model scriptRunModel = model
@ -338,9 +339,11 @@ struct HomeView: View {
scriptData = preferred.data scriptData = preferred.data
} }
let resumeBundleID = pid == nil ? nil : bundleID
var callback: DebugAppCallback? = nil var callback: DebugAppCallback? = nil
if ProcessInfo.processInfo.hasTXM, let sd = scriptData { 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? var lastDebugMessage: String?