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
10 changes: 7 additions & 3 deletions StikJIT/JSSupport/RunJSView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,13 @@ class RunJSViewModel: ObservableObject {
self.semaphore = semaphore
}

func runScript(path: URL) throws {
let scriptContent = try String(contentsOf: path, encoding: .utf8)
scriptName = path.lastPathComponent
func runScript(path: URL, scriptName: String? = nil) throws {
try runScript(data: Data(contentsOf: path), name: scriptName)
}

func runScript(data: Data, name: String? = nil) throws {
let scriptContent = String(data: data, encoding: .utf8)
scriptName = name ?? "Script"

let getPidFunction: @convention(block) () -> Int = {
return self.pid
Expand Down
7 changes: 7 additions & 0 deletions StikJIT/Utilities/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//
// Created by s s on 2025/7/9.
//
import UniformTypeIdentifiers

extension FileManager {
func filePath(atPath path: String, withLength length: Int) -> String? {
Expand All @@ -17,3 +18,9 @@ public extension ProcessInfo {
{ if let boot = FileManager.default.filePath(atPath: "/System/Volumes/Preboot", withLength: 36), let file = FileManager.default.filePath(atPath: "\(boot)/boot", withLength: 96) { return access("\(file)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 } else { return (FileManager.default.filePath(atPath: "/private/preboot", withLength: 96).map { access("\($0)/usr/standalone/firmware/FUD/Ap,TrustedExecutionMonitor.img4", F_OK) == 0 }) ?? false } }()
}
}

extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
}
}
185 changes: 110 additions & 75 deletions StikJIT/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import SwiftUI
import UniformTypeIdentifiers
import Pipify

extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
}
struct JITEnableConfiguration {
var bundleID: String? = nil
var pid : Int? = nil
var scriptData: Data? = nil
var scriptName : String? = nil
}

struct HomeView: View {
Expand All @@ -37,13 +38,13 @@ struct HomeView: View {
@State private var pidStr = ""

@State private var viewDidAppeared = false
@State private var pendingBundleIdToEnableJIT : String? = nil
@State private var pendingPIDToEnableJIT : Int? = nil
@State private var pendingJITEnableConfiguration : JITEnableConfiguration? = nil
@AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false

@AppStorage("useDefaultScript") private var useDefaultScript = false
@AppStorage("enablePiP") private var enablePiP = true
@State var scriptViewShow = false
@State var pipRequired = false
@AppStorage("DefaultScriptName") var selectedScript = "attachDetach.js"
@State var jsModel: RunJSViewModel?

Expand Down Expand Up @@ -301,12 +302,12 @@ struct HomeView: View {
bundleID = selectedBundle
isShowingInstalledApps = false
HapticFeedbackHelper.trigger()
startJITInBackground(with: selectedBundle)
startJITInBackground(bundleID: selectedBundle)
}
}
.pipify(isPresented: Binding(
get: { useDefaultScript && enablePiP && isProcessing },
set: { newValue in isProcessing = newValue }
get: { pipRequired && enablePiP },
set: { newValue in pipRequired = newValue }
)) {
RunJSViewPiP(model: $jsModel)
}
Expand All @@ -318,7 +319,6 @@ struct HomeView: View {
ToolbarItem(placement: .topBarTrailing) {
Button("Done") {
scriptViewShow = false
isProcessing = false
}
}
}
Expand Down Expand Up @@ -348,7 +348,7 @@ struct HomeView: View {
showAlert(title: "", message: "Invalid PID".localized, showOk: true, completion: { _ in })
return
}
startJITInBackground(with: pid)
startJITInBackground(pid: pid)

},
actionCancel: {_ in
Expand All @@ -361,31 +361,37 @@ struct HomeView: View {
return
}

var config = JITEnableConfiguration()
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)

if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) {
config.pid = pid
}
if let bundleId = components?.queryItems?.first(where: { $0.name == "bundle-id" })?.value {
if viewDidAppeared {
startJITInBackground(with: bundleId)
} else {
pendingBundleIdToEnableJIT = bundleId
}
} else if let pidStr = components?.queryItems?.first(where: { $0.name == "pid" })?.value, let pid = Int(pidStr) {
if viewDidAppeared {
startJITInBackground(with: pid)
} else {
pendingPIDToEnableJIT = pid
config.bundleID = bundleId
}
if let scriptBase64URL = components?.queryItems?.first(where: { $0.name == "script-data" })?.value?.removingPercentEncoding {
let base64 = base64URLToBase64(scriptBase64URL)
if let scriptData = Data(base64Encoded: base64) {
config.scriptData = scriptData
}
}
if let scriptName = components?.queryItems?.first(where: { $0.name == "script-name" })?.value {
config.scriptName = scriptName
}

if viewDidAppeared {
startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true)
} else {
pendingJITEnableConfiguration = config
}

}
.onAppear() {
viewDidAppeared = true
if let pendingBundleIdToEnableJIT {
startJITInBackground(with: pendingBundleIdToEnableJIT)
self.pendingBundleIdToEnableJIT = nil
}
if let pendingPIDToEnableJIT {
startJITInBackground(with: pendingPIDToEnableJIT)
self.pendingPIDToEnableJIT = nil
if let config = pendingJITEnableConfiguration {
startJITInBackground(bundleID: config.bundleID, pid: config.pid, scriptData: config.scriptData, scriptName: config.scriptName, triggeredByURLScheme: true)
self.pendingJITEnableConfiguration = nil
}
}
}
Expand All @@ -410,84 +416,113 @@ struct HomeView: View {
// but we'll keep it empty to avoid breaking anything
}

private func getJsCallback(for scriptName: String? = nil) -> DebugAppCallback? {
let name = scriptName ?? selectedScript
let selectedScriptURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("scripts").appendingPathComponent(name)

if !FileManager.default.fileExists(atPath: selectedScriptURL.path) {
return nil
}

private func getJsCallback(_ script: Data, name: String? = nil) -> DebugAppCallback {
return { pid, debugProxyHandle, semaphore in
jsModel = RunJSViewModel(pid: Int(pid), debugProxy: debugProxyHandle, semaphore: semaphore)
scriptViewShow = true

DispatchQueue.global(qos: .background).async {
do {
try jsModel?.runScript(path: selectedScriptURL)
isProcessing = false
try jsModel?.runScript(data: script, name: name)
} catch {
showAlert(title: "Error Occurred While Executing the Default Script.".localized, message: error.localizedDescription, showOk: true)
}
}
}
}

private func startJITInBackground(with bundleID: String) {
// launch app following this order: pid > bundleID
// load script following this order: scriptData > script file from script name > saved script for bundleID > default script
// if advanced mode is disabled the whole script loading will be skipped. If use default script is disabled default script will not be loaded
private func startJITInBackground(bundleID: String? = nil, pid : Int? = nil, scriptData: Data? = nil, scriptName : String? = nil, triggeredByURLScheme: Bool = false) {
isProcessing = true

// Add log message
LogManager.shared.addInfoLog("Starting Debug for \(bundleID)")
LogManager.shared.addInfoLog("Starting Debug for \(bundleID ?? String(pid ?? 0))")

DispatchQueue.global(qos: .background).async {
var callback: DebugAppCallback? = nil
if enableAdvancedOptions {
let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String]
if let script = mapping?[bundleID] {
callback = getJsCallback(for: script)
} else if useDefaultScript {
callback = getJsCallback()
var scriptData = scriptData
var scriptName = scriptName
if enableAdvancedOptions && scriptData == nil {
if scriptName == nil, let bundleID, let mapping = UserDefaults.standard.dictionary(forKey: "BundleScriptMap") as? [String: String] {
scriptName = mapping[bundleID]
}

if useDefaultScript && scriptName == nil {
scriptName = selectedScript
}

if scriptData == nil, let scriptName {
let selectedScriptURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent("scripts").appendingPathComponent(scriptName)

if FileManager.default.fileExists(atPath: selectedScriptURL.path) {
do {
scriptData = try Data(contentsOf: selectedScriptURL)
} catch {
print("failed to load data from script \(error)")
}

}
}
} else {
scriptData = nil
}
let success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: { message in


var callback: DebugAppCallback? = nil

if let scriptData {
callback = getJsCallback(scriptData, name: scriptName ?? bundleID ?? "Script")
if triggeredByURLScheme {
usleep(500000)
}

pipRequired = true
}

let logger: LogFunc = { message in

if let message = message {
// Log messages from the JIT process
LogManager.shared.addInfoLog(message)
}
}, jsCallback: callback)
}
var success : Bool
if let pid {
success = JITEnableContext.shared.debugApp(withPID: Int32(pid), logger: logger, jsCallback: callback)
} else if let bundleID {
success = JITEnableContext.shared.debugApp(withBundleID: bundleID, logger: logger, jsCallback: callback)
} else {
DispatchQueue.main.async {
showAlert(title: "Failed to Debug App".localized, message: "Either bundle ID or PID should be specified.".localized, showOk: true)
}
success = false
}

DispatchQueue.main.async {
LogManager.shared.addInfoLog("Debug process completed for \(bundleID)")
isProcessing = false
if success {
DispatchQueue.main.async {
LogManager.shared.addInfoLog("Debug process completed for \(bundleID ?? String(pid ?? 0))")
}
}
isProcessing = false
pipRequired = false
}
}

private func startJITInBackground(with pid: Int) {
isProcessing = true

// Add log message
LogManager.shared.addInfoLog("Starting JIT for pid \(pid)")

DispatchQueue.global(qos: .background).async {
func base64URLToBase64(_ base64url: String) -> String {
var base64 = base64url
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")

let jsCallback: DebugAppCallback? = (enableAdvancedOptions && useDefaultScript) ? getJsCallback() : nil
let success = JITEnableContext.shared.debugApp(withPID: Int32(pid), logger: { message in

if let message = message {
// Log messages from the JIT process
LogManager.shared.addInfoLog(message)
}
}, jsCallback: jsCallback)

DispatchQueue.main.async {
LogManager.shared.addInfoLog("JIT process completed for \(pid)")
showAlert(title: "Success".localized, message: String(format: "JIT has been enabled for pid %d.".localized, pid), showOk: true, messageType: .success)
isProcessing = false
}
// Pad with "=" to make length a multiple of 4
let paddingLength = 4 - (base64.count % 4)
if paddingLength < 4 {
base64 += String(repeating: "=", count: paddingLength)
}

return base64
}

}

class InstalledAppsViewModel: ObservableObject {
Expand Down