Skip to content

Commit e3a43a1

Browse files
committed
hmm
1 parent 3783419 commit e3a43a1

File tree

7 files changed

+393
-32
lines changed

7 files changed

+393
-32
lines changed

StikJIT/Info.plist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@
2424
<string>_stikdebug._tcp</string>
2525
<string>_stikdebug._udp</string>
2626
</array>
27+
<key>BGTaskSchedulerPermittedIdentifiers</key>
28+
<array>
29+
<string>$(PRODUCT_BUNDLE_IDENTIFIER).continuedProcessingTask.script</string>
30+
</array>
2731
<key>UIBackgroundModes</key>
2832
<array>
2933
<string>audio</string>
34+
<string>processing</string>
3035
</array>
3136
<key>UIFileSharingEnabled</key>
3237
<true/>

StikJIT/JSSupport/RunJSView.swift

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ class RunJSViewModel: ObservableObject {
1616
@Published var logs: [String] = []
1717
@Published var scriptName: String = "Script"
1818
@Published var executionInterrupted = false
19+
var onFinish: (() -> Void)?
1920
var pid: Int
2021
var debugProxy: OpaquePointer?
2122
var remoteServer: OpaquePointer?
2223
var semaphore: dispatch_semaphore_t?
24+
private var progressTimer: DispatchSourceTimer?
25+
private var reportedProgress: Double = 0
26+
private var didFinish = false
2327

2428
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: dispatch_semaphore_t?) {
2529
self.pid = pid
@@ -35,6 +39,7 @@ class RunJSViewModel: ObservableObject {
3539
func runScript(data: Data, name: String? = nil) throws {
3640
let scriptContent = String(data: data, encoding: .utf8)
3741
scriptName = name ?? "Script"
42+
startContinuedProcessing(withTitle: scriptName)
3843

3944
let getPidFunction: @convention(block) () -> Int = {
4045
return self.pid
@@ -88,10 +93,27 @@ class RunJSViewModel: ObservableObject {
8893
if let exception = self.context?.exception {
8994
self.logs.append(exception.debugDescription)
9095
}
96+
let success = self.context?.exception == nil && !self.executionInterrupted
97+
self.notifyFinished(success: success)
98+
self.stopContinuedProcessing(success: success)
9199
self.logs.append("Script Execution Completed")
92-
self.logs.append("You are safe to close the PIP Window.")
100+
let usesContinuedProcessing = ContinuedProcessingManager.shared.isSupported
101+
&& UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing)
102+
if usesContinuedProcessing {
103+
self.logs.append("Background processing finished. You can dismiss this view.")
104+
} else if UserDefaults.standard.bool(forKey: "enablePiP") {
105+
self.logs.append("You are safe to close the PiP window.")
106+
} else {
107+
self.logs.append("You can dismiss this view.")
108+
}
93109
}
94110
}
111+
112+
func notifyFinished(success _: Bool) {
113+
guard !didFinish else { return }
114+
didFinish = true
115+
onFinish?()
116+
}
95117

96118
private func captureScreenshot(named preferredName: String?) -> String {
97119
if executionInterrupted {
@@ -201,6 +223,38 @@ class RunJSViewModel: ObservableObject {
201223
guard let context else { return }
202224
context.exception = JSValue(object: message, in: context)
203225
}
226+
227+
private func startContinuedProcessing(withTitle title: String) {
228+
guard ContinuedProcessingManager.shared.isSupported,
229+
UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) else { return }
230+
stopProgressTimer()
231+
reportedProgress = 0.05
232+
ContinuedProcessingManager.shared.begin(title: title, subtitle: "Script execution in progress")
233+
ContinuedProcessingManager.shared.updateProgress(reportedProgress)
234+
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
235+
timer.schedule(deadline: .now() + 5, repeating: 5)
236+
timer.setEventHandler { [weak self] in
237+
guard let self else { return }
238+
self.reportedProgress = min(0.9, self.reportedProgress + 0.1)
239+
ContinuedProcessingManager.shared.updateProgress(self.reportedProgress)
240+
if self.reportedProgress >= 0.9 {
241+
self.stopProgressTimer()
242+
}
243+
}
244+
timer.resume()
245+
progressTimer = timer
246+
}
247+
248+
private func stopContinuedProcessing(success: Bool) {
249+
stopProgressTimer()
250+
ContinuedProcessingManager.shared.updateProgress(1.0)
251+
ContinuedProcessingManager.shared.finish(success: success)
252+
}
253+
254+
private func stopProgressTimer() {
255+
progressTimer?.cancel()
256+
progressTimer = nil
257+
}
204258
}
205259

206260
struct RunJSViewPiP: View {

StikJIT/StikJITApp.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ private func registerAdvancedOptionsDefault() {
1515
let os = ProcessInfo.processInfo.operatingSystemVersion
1616
// Enable advanced options by default on iOS 19/26 and above
1717
let enabled = os.majorVersion >= 19
18-
UserDefaults.standard.register(defaults: ["enableAdvancedOptions": enabled])
19-
UserDefaults.standard.register(defaults: ["enablePiP": enabled])
20-
UserDefaults.standard.register(defaults: [UserDefaults.Keys.txmOverride: false])
21-
UserDefaults.standard.register(defaults: ["autoEnableLocalDevVPN": false])
22-
UserDefaults.standard.register(defaults: ["showLocalDevVPNPrompt": true])
18+
let defaults = UserDefaults.standard
19+
defaults.register(defaults: ["enableAdvancedOptions": enabled])
20+
defaults.register(defaults: ["enablePiP": enabled])
21+
defaults.register(defaults: [UserDefaults.Keys.enableContinuedProcessing: false])
22+
defaults.register(defaults: [UserDefaults.Keys.txmOverride: false])
23+
defaults.register(defaults: ["autoEnableLocalDevVPN": false])
24+
defaults.register(defaults: ["showLocalDevVPNPrompt": true])
2325
}
2426

2527
// MARK: - Welcome Sheet
@@ -300,6 +302,10 @@ struct HeartbeatApp: App {
300302

301303
init() {
302304
registerAdvancedOptionsDefault()
305+
if ContinuedProcessingManager.shared.isSupported,
306+
UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) {
307+
ContinuedProcessingManager.shared.configureIfNeeded()
308+
}
303309
newVerCheck()
304310
let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))!
305311
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
//
2+
// ContinuedProcessingManager.swift
3+
// StikJIT
4+
//
5+
// Created by se2crid on 18/12/25.
6+
//
7+
8+
import BackgroundTasks
9+
import Foundation
10+
11+
final class ContinuedProcessingManager {
12+
static let shared = ContinuedProcessingManager()
13+
private let handler: ContinuedProcessingHandling
14+
15+
private init() {
16+
if #available(iOS 26.0, *) {
17+
handler = ModernContinuedProcessingHandler()
18+
} else {
19+
handler = NoopContinuedProcessingHandler()
20+
}
21+
}
22+
23+
var isSupported: Bool { handler.isSupported }
24+
25+
func configureIfNeeded() {
26+
handler.configureIfNeeded()
27+
}
28+
29+
func cancelPendingTasks() {
30+
handler.cancelPendingTasks()
31+
}
32+
33+
func begin(title: String, subtitle: String) {
34+
handler.begin(title: title, subtitle: subtitle)
35+
}
36+
37+
func updateProgress(_ fraction: Double) {
38+
handler.updateProgress(fraction)
39+
}
40+
41+
func finish(success: Bool) {
42+
handler.finish(success: success)
43+
}
44+
}
45+
46+
private protocol ContinuedProcessingHandling: AnyObject {
47+
var isSupported: Bool { get }
48+
func configureIfNeeded()
49+
func cancelPendingTasks()
50+
func begin(title: String, subtitle: String)
51+
func updateProgress(_ fraction: Double)
52+
func finish(success: Bool)
53+
}
54+
55+
private final class NoopContinuedProcessingHandler: ContinuedProcessingHandling {
56+
var isSupported: Bool { false }
57+
func configureIfNeeded() {}
58+
func cancelPendingTasks() {}
59+
func begin(title: String, subtitle: String) {}
60+
func updateProgress(_ fraction: Double) {}
61+
func finish(success: Bool) {}
62+
}
63+
64+
@available(iOS 26.0, *)
65+
private final class ModernContinuedProcessingHandler: ContinuedProcessingHandling {
66+
private let scheduler = BGTaskScheduler.shared
67+
private let taskIdentifier: String
68+
private var didRegister = false
69+
private var activeTask: BGContinuedProcessingTask?
70+
private let queue = DispatchQueue(
71+
label: "com.stikdebug.continuedProcessing",
72+
qos: .utility)
73+
private var pendingMetadata: (title: String, subtitle: String)?
74+
75+
init() {
76+
let bundleID = Bundle.main.bundleIdentifier ?? "com.stik.sj"
77+
taskIdentifier = "\(bundleID).continuedProcessingTask.script"
78+
}
79+
80+
var isSupported: Bool { true }
81+
82+
func configureIfNeeded() {
83+
guard !didRegister else { return }
84+
scheduler.register(forTaskWithIdentifier: taskIdentifier, using: nil) { [weak self] task in
85+
guard let continuedTask = task as? BGContinuedProcessingTask else {
86+
task.setTaskCompleted(success: false)
87+
return
88+
}
89+
self?.handle(task: continuedTask)
90+
}
91+
didRegister = true
92+
}
93+
94+
func begin(title: String, subtitle: String) {
95+
guard UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) else {
96+
return
97+
}
98+
configureIfNeeded()
99+
var reserved = false
100+
queue.sync {
101+
if activeTask == nil && pendingMetadata == nil {
102+
pendingMetadata = (title: title, subtitle: subtitle)
103+
reserved = true
104+
}
105+
}
106+
guard reserved else { return }
107+
// Clear any stale request that might block new submissions.
108+
scheduler.cancel(taskRequestWithIdentifier: taskIdentifier)
109+
let request = BGContinuedProcessingTaskRequest(
110+
identifier: taskIdentifier,
111+
title: title,
112+
subtitle: subtitle)
113+
request.strategy = .queue
114+
do {
115+
try scheduler.submit(request)
116+
LogManager.shared.addInfoLog("Requested continued processing: \(title)")
117+
} catch {
118+
LogManager.shared.addWarningLog(
119+
"Unable to request continued processing: \(error.localizedDescription)")
120+
queue.async { [weak self] in
121+
self?.pendingMetadata = nil
122+
}
123+
}
124+
}
125+
126+
func cancelPendingTasks() {
127+
queue.async { [weak self] in
128+
guard let self else { return }
129+
if let task = activeTask {
130+
task.setTaskCompleted(success: false)
131+
activeTask = nil
132+
}
133+
pendingMetadata = nil
134+
scheduler.cancel(taskRequestWithIdentifier: taskIdentifier)
135+
}
136+
}
137+
138+
func updateProgress(_ fraction: Double) {
139+
queue.async { [weak self] in
140+
guard let task = self?.activeTask else { return }
141+
let clamped = max(0.0, min(1.0, fraction))
142+
task.progress.totalUnitCount = max(task.progress.totalUnitCount, 100)
143+
task.progress.completedUnitCount = Int64(Double(task.progress.totalUnitCount) * clamped)
144+
}
145+
}
146+
147+
func finish(success: Bool) {
148+
queue.async { [weak self] in
149+
guard let self else { return }
150+
if let task = self.activeTask {
151+
task.progress.completedUnitCount = task.progress.totalUnitCount
152+
task.setTaskCompleted(success: success)
153+
self.activeTask = nil
154+
} else if pendingMetadata != nil {
155+
scheduler.cancel(taskRequestWithIdentifier: taskIdentifier)
156+
}
157+
pendingMetadata = nil
158+
}
159+
}
160+
161+
private func handle(task: BGContinuedProcessingTask) {
162+
queue.async { [weak self] in
163+
guard let self else { return }
164+
activeTask = task
165+
if let metadata = pendingMetadata {
166+
task.updateTitle(metadata.title, subtitle: metadata.subtitle)
167+
}
168+
if task.progress.totalUnitCount == 0 {
169+
task.progress.totalUnitCount = 100
170+
}
171+
task.progress.completedUnitCount = 1
172+
task.expirationHandler = { [weak self] in
173+
self?.handleExpiration()
174+
}
175+
}
176+
}
177+
178+
private func handleExpiration() {
179+
LogManager.shared.addWarningLog("Continued processing expired early")
180+
finish(success: false)
181+
}
182+
}

StikJIT/Utilities/Extensions.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ extension UserDefaults {
2424
enum Keys {
2525
/// Forces the app to treat the current device as TXM-capable so scripts always run.
2626
static let txmOverride = "overrideTXMForScripts"
27+
/// Controls whether BGContinuedProcessingTask should be used to keep scripts alive in the background.
28+
static let enableContinuedProcessing = "enableContinuedProcessing"
2729
/// Tracks whether an external device profile is currently active.
2830
static let usingExternalDevice = "UsingExternalDevice"
2931
}
@@ -33,6 +35,8 @@ enum LocalDevVPN {
3335
static let scheme = "localdevvpn"
3436
static let callbackScheme = "stikjit"
3537
static var didRequestEnableThisSession = false
38+
static var didShowMissingWarning = false
39+
static let appStoreURL = "https://apps.apple.com/app/localdevvpn/id6755608044"
3640

3741
static func isInstalled() -> Bool {
3842
guard let url = URL(string: "\(scheme)://") else { return false }
@@ -47,6 +51,11 @@ enum LocalDevVPN {
4751
open(action: "disable")
4852
}
4953

54+
static func openAppStore() {
55+
guard let url = URL(string: appStoreURL) else { return }
56+
UIApplication.shared.open(url, options: [:], completionHandler: nil)
57+
}
58+
5059
private static func open(action: String) {
5160
var components = URLComponents()
5261
components.scheme = scheme

0 commit comments

Comments
 (0)