Skip to content

Commit bf4ecd4

Browse files
committed
Merge remote-tracking branch 'origin/main' into test
2 parents 3357447 + f5f3d3e commit bf4ecd4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+3750
-2101
lines changed

.github/workflows/build_ipa.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ on:
99

1010
permissions:
1111
contents: write
12-
12+
1313
jobs:
1414
build:
1515
name: Build Debug IPA
1616
runs-on: macos-latest
17+
env:
18+
UPLOAD_IPA: ${{ vars.UPLOAD_IPA || 'false' }}
1719

1820
steps:
1921
- name: Checkout
@@ -49,6 +51,7 @@ jobs:
4951
rm -rf Payload StikDebug.app
5052
5153
- name: Upload Debug IPA (artifact)
54+
if: env.UPLOAD_IPA == 'true'
5255
uses: actions/upload-artifact@v4
5356
with:
5457
name: StikDebug-Debug.ipa

DebugWidget/DebugWidgetBundle.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import SwiftUI
1111
@main
1212
struct StikDebugWidgetBundle: WidgetBundle {
1313
var body: some Widget {
14-
// Both widgets enabled: Favorites uses enable-jit URL scheme (with continued processing handled in-app),
14+
// Both widgets enabled: Favorites uses enable-jit URL scheme (with PiP/script handled in-app),
1515
// System Apps uses launch-app URL scheme for non-debug launch behavior.
1616
FavoritesWidget()
1717
SystemAppsWidget()

StikDebug.xcodeproj/project.pbxproj

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
17C744F02E20BED000834F17 /* Pipify in Frameworks */ = {isa = PBXBuildFile; productRef = 17C744EF2E20BED000834F17 /* Pipify */; };
1011
68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68D1FA3F2E847E4A0028A0EA /* StoreKit.framework */; };
1112
68D569BE2E1B415700A5BA36 /* CodeEditorView in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BD2E1B415700A5BA36 /* CodeEditorView */; };
1213
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */ = {isa = PBXBuildFile; productRef = 68D569BF2E1B415700A5BA36 /* LanguageSupport */; };
@@ -153,6 +154,7 @@
153154
isa = PBXFrameworksBuildPhase;
154155
buildActionMask = 2147483647;
155156
files = (
157+
17C744F02E20BED000834F17 /* Pipify in Frameworks */,
156158
DCBA85862E3897BD00E88C06 /* StikImporter in Frameworks */,
157159
68D569C02E1B415700A5BA36 /* LanguageSupport in Frameworks */,
158160
68D1FA402E847E4A0028A0EA /* StoreKit.framework in Frameworks */,
@@ -270,6 +272,7 @@
270272
packageProductDependencies = (
271273
68D569BD2E1B415700A5BA36 /* CodeEditorView */,
272274
68D569BF2E1B415700A5BA36 /* LanguageSupport */,
275+
17C744EF2E20BED000834F17 /* Pipify */,
273276
DCBA85852E3897BD00E88C06 /* StikImporter */,
274277
68E714E52E6AA2B00025610F /* ZIPFoundation */,
275278
);
@@ -382,11 +385,13 @@
382385
en,
383386
Base,
384387
es,
388+
it,
385389
);
386390
mainGroup = DC6F1D2E2D94EADD0071B2B6;
387391
minimizedProjectReferenceProxies = 1;
388392
packageReferences = (
389393
68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */,
394+
17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */,
390395
DCBA85842E3897BD00E88C06 /* XCRemoteSwiftPackageReference "StikImporter" */,
391396
68E714E42E6AA2B00025610F /* XCRemoteSwiftPackageReference "ZIPFoundation" */,
392397
);
@@ -699,7 +704,9 @@
699704
);
700705
GENERATE_INFOPLIST_FILE = YES;
701706
INFOPLIST_FILE = StikJIT/Info.plist;
707+
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
702708
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
709+
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.";
703710
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
704711
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
705712
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -755,7 +762,9 @@
755762
);
756763
GENERATE_INFOPLIST_FILE = YES;
757764
INFOPLIST_FILE = StikJIT/Info.plist;
765+
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
758766
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
767+
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.";
759768
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
760769
"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
761770
"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
@@ -997,6 +1006,14 @@
9971006
/* End XCConfigurationList section */
9981007

9991008
/* Begin XCRemoteSwiftPackageReference section */
1009+
17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */ = {
1010+
isa = XCRemoteSwiftPackageReference;
1011+
repositoryURL = "https://github.com/hugeBlack/swiftui-pipify";
1012+
requirement = {
1013+
branch = main;
1014+
kind = branch;
1015+
};
1016+
};
10001017
68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */ = {
10011018
isa = XCRemoteSwiftPackageReference;
10021019
repositoryURL = "https://github.com/mchakravarty/CodeEditorView";
@@ -1024,6 +1041,11 @@
10241041
/* End XCRemoteSwiftPackageReference section */
10251042

10261043
/* Begin XCSwiftPackageProductDependency section */
1044+
17C744EF2E20BED000834F17 /* Pipify */ = {
1045+
isa = XCSwiftPackageProductDependency;
1046+
package = 17C744EE2E20BED000834F17 /* XCRemoteSwiftPackageReference "swiftui-pipify" */;
1047+
productName = Pipify;
1048+
};
10271049
68D569BD2E1B415700A5BA36 /* CodeEditorView */ = {
10281050
isa = XCSwiftPackageProductDependency;
10291051
package = 68D569BC2E1B415700A5BA36 /* XCRemoteSwiftPackageReference "CodeEditorView" */;

StikDebug.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
File renamed without changes.

StikJIT/Info.plist

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,11 @@
1515
</array>
1616
</dict>
1717
</array>
18-
<key>BGTaskSchedulerPermittedIdentifiers</key>
18+
<key>NSBonjourServices</key>
1919
<array>
20-
<string>$(PRODUCT_BUNDLE_IDENTIFIER).continuedProcessingTask.script</string>
20+
<string>_stikdebug._tcp</string>
21+
<string>_stikdebug._udp</string>
2122
</array>
22-
<key>UIBackgroundModes</key>
23-
<array>
24-
<string>audio</string>
25-
<string>processing</string>
26-
</array>
27-
<key>ITSAppUsesNonExemptEncryption</key>
28-
<false/>
2923
<key>UIFileSharingEnabled</key>
3024
<true/>
3125
</dict>

StikJIT/JSSupport/RunJSView.swift

Lines changed: 134 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,23 @@
88
import SwiftUI
99
import JavaScriptCore
1010

11+
typealias RemoteServerHandle = OpaquePointer
12+
typealias ScreenshotClientHandle = OpaquePointer
13+
1114
class RunJSViewModel: ObservableObject {
1215
var context: JSContext?
1316
@Published var logs: [String] = []
1417
@Published var scriptName: String = "Script"
1518
@Published var executionInterrupted = false
1619
var pid: Int
1720
var debugProxy: OpaquePointer?
21+
var remoteServer: OpaquePointer?
1822
var semaphore: dispatch_semaphore_t?
19-
private var progressTimer: DispatchSourceTimer?
20-
private var reportedProgress: Double = 0
2123

22-
init(pid: Int, debugProxy: OpaquePointer?, semaphore: dispatch_semaphore_t?) {
24+
init(pid: Int, debugProxy: OpaquePointer?, remoteServer: OpaquePointer?, semaphore: dispatch_semaphore_t?) {
2325
self.pid = pid
2426
self.debugProxy = debugProxy
27+
self.remoteServer = remoteServer
2528
self.semaphore = semaphore
2629
}
2730

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

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

66+
let takeScreenshotFunction: @convention(block) (String?) -> String? = { fileName in
67+
return self.captureScreenshot(named: fileName)
68+
}
69+
6470
let hasTXMFunction: @convention(block) () -> Bool = {
6571
return ProcessInfo.processInfo.hasTXM
6672
}
@@ -70,6 +76,7 @@ class RunJSViewModel: ObservableObject {
7076
context?.setObject(getPidFunction, forKeyedSubscript: "get_pid" as NSString)
7177
context?.setObject(sendCommandFunction, forKeyedSubscript: "send_command" as NSString)
7278
context?.setObject(prepareMemoryRegionFunction, forKeyedSubscript: "prepare_memory_region" as NSString)
79+
context?.setObject(takeScreenshotFunction, forKeyedSubscript: "take_screenshot" as NSString)
7380
context?.setObject(logFunction, forKeyedSubscript: "log" as NSString)
7481

7582
context?.evaluateScript(scriptContent)
@@ -81,43 +88,139 @@ class RunJSViewModel: ObservableObject {
8188
if let exception = self.context?.exception {
8289
self.logs.append(exception.debugDescription)
8390
}
84-
let success = self.context?.exception == nil && !self.executionInterrupted
85-
self.stopContinuedProcessing(success: success)
8691
self.logs.append("Script Execution Completed")
87-
self.logs.append("Background processing finished. You can dismiss this view.")
92+
self.logs.append("You are safe to close the PIP Window.")
93+
}
94+
}
95+
96+
private func captureScreenshot(named preferredName: String?) -> String {
97+
if executionInterrupted {
98+
raiseException("Script execution is interrupted by StikDebug.")
99+
return ""
100+
}
101+
guard let remoteServer else {
102+
raiseException("Screenshot capture is unavailable in the current session.")
103+
return ""
104+
}
105+
106+
var screenshotClient: ScreenshotClientHandle?
107+
let creationError = screenshot_client_new(remoteServer, &screenshotClient)
108+
if let creationError {
109+
let message = describeIdeviceError(creationError)
110+
idevice_error_free(creationError)
111+
raiseException("Failed to create screenshot client: \(message)")
112+
return ""
113+
}
114+
guard let screenshotClient else {
115+
raiseException("Failed to allocate screenshot client.")
116+
return ""
117+
}
118+
defer { screenshot_client_free(screenshotClient) }
119+
120+
var buffer: UnsafeMutablePointer<UInt8>?
121+
var length: UInt = 0
122+
let captureError = screenshot_client_take_screenshot(screenshotClient, &buffer, &length)
123+
if let captureError {
124+
let message = describeIdeviceError(captureError)
125+
idevice_error_free(captureError)
126+
raiseException("Failed to take screenshot: \(message)")
127+
return ""
128+
}
129+
guard let buffer else {
130+
raiseException("Device returned empty screenshot data.")
131+
return ""
132+
}
133+
defer { idevice_data_free(buffer, length) }
134+
135+
let data = Data(bytes: buffer, count: Int(length))
136+
do {
137+
let fileURL = try screenshotFileURL(preferredName: preferredName)
138+
try data.write(to: fileURL, options: .atomic)
139+
return fileURL.path
140+
} catch {
141+
raiseException("Failed to save screenshot: \(error.localizedDescription)")
142+
return ""
143+
}
144+
}
145+
146+
private func screenshotFileURL(preferredName: String?) throws -> URL {
147+
let directory = URL.documentsDirectory.appendingPathComponent("screenshots", isDirectory: true)
148+
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
149+
let fileManager = FileManager.default
150+
let initialName = sanitizedScreenshotName(from: preferredName)
151+
var targetURL = directory.appendingPathComponent(initialName)
152+
guard fileManager.fileExists(atPath: targetURL.path) else {
153+
return targetURL
88154
}
155+
156+
let baseName = targetURL.deletingPathExtension().lastPathComponent
157+
let ext = targetURL.pathExtension.isEmpty ? "png" : targetURL.pathExtension
158+
var counter = 1
159+
repeat {
160+
let candidate = "\(baseName)-\(counter).\(ext)"
161+
targetURL = directory.appendingPathComponent(candidate)
162+
counter += 1
163+
} while fileManager.fileExists(atPath: targetURL.path)
164+
return targetURL
89165
}
90166

91-
private func startContinuedProcessing(withTitle title: String) {
92-
guard ContinuedProcessingManager.shared.isSupported,
93-
UserDefaults.standard.bool(forKey: UserDefaults.Keys.enableContinuedProcessing) else { return }
94-
stopProgressTimer()
95-
reportedProgress = 0.05
96-
ContinuedProcessingManager.shared.begin(title: title, subtitle: "Script execution in progress")
97-
ContinuedProcessingManager.shared.updateProgress(reportedProgress)
98-
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.global(qos: .background))
99-
timer.schedule(deadline: .now() + 5, repeating: 5)
100-
timer.setEventHandler { [weak self] in
101-
guard let self else { return }
102-
self.reportedProgress = min(0.9, self.reportedProgress + 0.1)
103-
ContinuedProcessingManager.shared.updateProgress(self.reportedProgress)
104-
if self.reportedProgress >= 0.9 {
105-
self.stopProgressTimer()
167+
private func sanitizedScreenshotName(from preferredName: String?) -> String {
168+
let defaultName = "screenshot-\(Int(Date().timeIntervalSince1970))"
169+
guard var candidate = preferredName?.trimmingCharacters(in: .whitespacesAndNewlines),
170+
!candidate.isEmpty else {
171+
return "\(defaultName).png"
172+
}
173+
174+
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_."))
175+
var sanitized = ""
176+
sanitized.reserveCapacity(candidate.count)
177+
for scalar in candidate.unicodeScalars {
178+
if allowed.contains(scalar) {
179+
sanitized.append(Character(scalar))
180+
} else {
181+
sanitized.append("_")
106182
}
107183
}
108-
timer.resume()
109-
progressTimer = timer
184+
if sanitized.isEmpty {
185+
sanitized = defaultName
186+
}
187+
if !sanitized.lowercased().hasSuffix(".png") {
188+
sanitized += ".png"
189+
}
190+
return sanitized
191+
}
192+
193+
private func describeIdeviceError(_ error: UnsafeMutablePointer<IdeviceFfiError>) -> String {
194+
if let messagePointer = error.pointee.message {
195+
return "[\(error.pointee.code)] \(String(cString: messagePointer))"
196+
}
197+
return "[\(error.pointee.code)] Unknown error"
110198
}
111199

112-
private func stopContinuedProcessing(success: Bool) {
113-
stopProgressTimer()
114-
ContinuedProcessingManager.shared.updateProgress(1.0)
115-
ContinuedProcessingManager.shared.finish(success: success)
200+
private func raiseException(_ message: String) {
201+
guard let context else { return }
202+
context.exception = JSValue(object: message, in: context)
116203
}
204+
}
117205

118-
private func stopProgressTimer() {
119-
progressTimer?.cancel()
120-
progressTimer = nil
206+
struct RunJSViewPiP: View {
207+
@Binding var model: RunJSViewModel?
208+
@State private var logs: [String] = []
209+
private let timer = Timer.publish(every: 0.034, on: .main, in: .common).autoconnect()
210+
211+
var body: some View {
212+
VStack(alignment: .leading, spacing: 4) {
213+
ForEach(logs.suffix(6).indices, id: \.self) { index in
214+
Text(logs.suffix(6)[index])
215+
.font(.system(size: 12))
216+
.foregroundStyle(.white)
217+
}
218+
}
219+
.padding()
220+
.onReceive(timer) { _ in
221+
logs = model?.logs ?? []
222+
}
223+
.frame(width: 300, height: 150)
121224
}
122225
}
123226

0 commit comments

Comments
 (0)