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) {
do {
try ensureTunnel()

View file

@ -22,12 +22,17 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
var debugProxy: OpaquePointer?
var remoteServer: OpaquePointer?
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.debugProxy = debugProxy
self.remoteServer = remoteServer
self.semaphore = semaphore
self.resumeBundleID = resumeBundleID
}
func runScript(path: URL, scriptName: String? = nil) throws {
@ -58,7 +63,13 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
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
@ -79,7 +90,12 @@ final class RunJSViewModel: ObservableObject, Identifiable, @unchecked Sendable
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.")

View file

@ -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?