Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions StikJIT/JSSupport/IDeviceJSBridgeDebugProxy.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// IDeviceJSBridgeDebugProxy.m
// StikJIT
//
// Created by s s on 2025/4/25.
//
@import Foundation;
@import JavaScriptCore;
#import "JSSupport.h"
#import "../idevice/JITEnableContext.h"
#import "../idevice/idevice.h"
#include "../idevice/jit.h"

NSString* handleJSContextSendDebugCommand(JSContext* context, NSString* commandStr, DebugProxyHandle* debugProxy) {
DebugserverCommandHandle* command = 0;

command = debugserver_command_new([commandStr UTF8String], NULL, 0);

char* attach_response = 0;
IdeviceFfiError* err = debug_proxy_send_command2(debugProxy, command, &attach_response);
debugserver_command_free(command);
if (err) {
context.exception = [JSValue valueWithObject:[NSString stringWithFormat:@"error code %d, msg %s", err->code, err->message] inContext:context];
idevice_error_free(err);
return nil;
}
NSString* commandResponse = nil;
if(attach_response) {
commandResponse = @(attach_response);
}
idevice_string_free(attach_response);
return commandResponse;
}
11 changes: 11 additions & 0 deletions StikJIT/JSSupport/JSSupport.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// JSSupport.h
// StikJIT
//
// Created by s s on 2025/4/24.
//
@import WebKit;
@import JavaScriptCore;
#include "../idevice/jit.h"

NSString* handleJSContextSendDebugCommand(JSContext* context, NSString* commandStr, DebugProxyHandle* debugProxy);
81 changes: 81 additions & 0 deletions StikJIT/JSSupport/RunJSView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// RunJSView.swift
// StikJIT
//
// Created by s s on 2025/4/24.
//

import SwiftUI
import JavaScriptCore


class RunJSViewModel : ObservableObject {
var context: JSContext?
@Published var logs: [String] = []
@Published var scriptName : String = "Script"
var pid: Int
var debugProxy: OpaquePointer?
var semaphore: dispatch_semaphore_t?

init(pid: Int, debugProxy: OpaquePointer?, semaphore: dispatch_semaphore_t?) {
self.pid = pid
self.debugProxy = debugProxy
self.semaphore = semaphore
}

func runScript(path: URL) throws {
let scriptContent = try String(contentsOf: path, encoding: .utf8)
scriptName = path.lastPathComponent

let getPidFunction: @convention(block) () -> Int = {
return self.pid
}

let sendCommandFunction: @convention(block) (String?) -> String? = { commandStr in
guard let commandStr else {
self.context?.exception = JSValue(object: "command should not be nil", in: self.context!)
return nil
}

return handleJSContextSendDebugCommand(self.context, commandStr, self.debugProxy) ?? ""
}

let logFunction: @convention(block) (String) -> Void = { logStr in
DispatchQueue.main.async {
self.logs.append(logStr)
}
}

context = JSContext()
context?.setObject(getPidFunction, forKeyedSubscript: "get_pid" as NSString)
context?.setObject(sendCommandFunction, forKeyedSubscript: "send_command" as NSString)
context?.setObject(logFunction, forKeyedSubscript: "log" as NSString)

context?.evaluateScript(scriptContent)
if let semaphore {
semaphore.signal()
}
DispatchQueue.main.async {
if let exception = self.context?.exception {
self.logs.append(exception.debugDescription)
}
self.logs.append("Script Execution Completed.")
}
}

}

struct RunJSView : View {
@ObservedObject var model : RunJSViewModel
@State var webViewShow = false

var body: some View {
List {
ForEach(model.logs, id: \.self) { logStr in
Text(logStr)
}
}
.navigationTitle("Running \(model.scriptName)")
}

}
50 changes: 50 additions & 0 deletions StikJIT/JSSupport/ScriptEditorView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//
// ScriptEditorView.swift
// StikDebug
//
// Created by s s on 2025/7/4.
//

import SwiftUI

struct ScriptEditorView: View {
let scriptURL: URL
@State private var scriptContent: String = ""
@Environment(\.dismiss) private var dismiss

var body: some View {
VStack {
TextEditor(text: $scriptContent)
.padding()
.border(Color.gray, width: 1)
.navigationTitle(scriptURL.lastPathComponent)
.navigationBarTitleDisplayMode(.inline)
.font(.system(.footnote, design: .monospaced))

HStack {
Button("Cancel") {
dismiss()
}
.buttonStyle(.bordered)

Spacer()

Button("Save") {
saveScript()
dismiss()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
.onAppear(perform: loadScript)
}

private func loadScript() {
scriptContent = (try? String(contentsOf: scriptURL)) ?? ""
}

private func saveScript() {
try? scriptContent.write(to: scriptURL, atomically: true, encoding: .utf8)
}
}
139 changes: 139 additions & 0 deletions StikJIT/JSSupport/ScriptListView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//
// ScriptListView.swift
// StikDebug
//
// Created by s s on 2025/7/4.
//

import SwiftUI

struct ScriptListView: View {
@State private var scripts: [URL] = []
@State private var selectedScript: URL?
@State private var navigateToEditor = false
@State private var showNewFileAlert = false
@State private var newFileName = ""
@AppStorage("DefaultScriptName") var defaultScriptName = "attachDetach.js"

var body: some View {
NavigationStack {
List {
Section {
ForEach(scripts, id: \.self) { script in
NavigationLink {
ScriptEditorView(scriptURL: script)
} label: {
HStack {
Text(script.lastPathComponent)
.font(.headline)

if(defaultScriptName == script.lastPathComponent) {
Spacer()
Image(systemName: "star.fill")
}
}
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
deleteScript(script)
} label: {
Label("Delete", systemImage: "trash")
}

Button {
saveDefaultScript(script)
} label: {
Label("Set Default", systemImage: "star")
}
.tint(.blue)
}
}
} footer: {
Text("Swipe left to set a script as the default script. Enable script execution after connecting in settings.")
}
}
.navigationTitle("JavaScript Files")
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: {
showNewFileAlert = true
}) {
Label("New Script", systemImage: "plus")
}
}
}
.onAppear(perform: loadScripts)
.alert("New Script", isPresented: $showNewFileAlert, actions: {
TextField("Filename", text: $newFileName)
Button("Create", action: createNewScript)
Button("Cancel", role: .cancel) {}
})

}
}

private func scriptsDirectory() -> URL {
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("scripts")

var isDir : ObjCBool = false
var isDirExist = FileManager.default.fileExists(atPath: dir.path(), isDirectory: &isDir)

do {
if isDirExist && !isDir.boolValue {
try FileManager.default.removeItem(at: dir)
isDirExist = false
}

if !isDirExist {
try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
try FileManager.default.copyItem(at: Bundle.main.url(forResource: "attachDetach", withExtension: "js")!, to: dir.appendingPathComponent("attachDetach.js"))
}
} catch {
showAlert(title: "Unable to Create Scripts Folder", message: error.localizedDescription, showOk: true)
}
return dir
}

private func loadScripts() {
let dir = scriptsDirectory()
scripts = (try? FileManager.default.contentsOfDirectory(at: dir, includingPropertiesForKeys: nil))?
.filter { $0.pathExtension == "js" } ?? []

}

private func saveDefaultScript(_ url: URL) {
defaultScriptName = url.lastPathComponent
}

private func createNewScript() {
guard !newFileName.isEmpty else { return }
var filename = newFileName
if !filename.hasSuffix(".js") {
filename += ".js"
}

let newURL = scriptsDirectory().appendingPathComponent(filename)

guard !FileManager.default.fileExists(atPath: newURL.path) else {
showAlert(title: "Failed to Create New Script", message: "A script with the same name already exists.", showOk: true)
return
}

do {
try "".write(to: newURL, atomically: true, encoding: .utf8)
newFileName = ""
loadScripts()
} catch {
print("Error creating file: \(error)")
}
}

private func deleteScript(_ url: URL) {
try? FileManager.default.removeItem(at: url)
if url.lastPathComponent == defaultScriptName {
UserDefaults.standard.removeObject(forKey: "DefaultScriptName")
}
loadScripts()
}
}
18 changes: 18 additions & 0 deletions StikJIT/JSSupport/attachDetach.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
function attach() {
log("Welcome to use StikDebug's srcipting feature! Here you can use JavaScript to customize your debug experience!")
log("This script is a demo script that attaches to the connected app and detaches from it.")

// get pid of the connected app
let pid = get_pid();
log(`pid = ${pid}`)

// attach
let attachResponse = send_command(`vAttach;${pid.toString(16)}`)
log(`attach_response = ${attachResponse}`)

// detach
let detachResponse = send_command(`D`)
log(`detachResponse = ${detachResponse}`)
}

attach()
1 change: 1 addition & 0 deletions StikJIT/StikJIT-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
#include "idevice/JITEnableContext.h"
#include "idevice/idevice.h"
#include "idevice/heartbeat.h"
#include "JSSupport/JSSupport.h"
Loading