From d846445448ea8b54a6518d902a3b0aae0fee15f6 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Thu, 7 Sep 2023 18:00:53 -0500 Subject: [PATCH] [AltJIT] Adds AltJIT CLI tool to enable JIT on devices running iOS 17+ Commands: altjit enable [app/pid] --udid [udid] * Enables JIT for given app/process altjit mount --udid [udid] * Mounts personalized developer disk --- AltJIT/AltJIT-Bridging-Header.h | 9 + AltJIT/AltJIT.swift | 18 + AltJIT/Commands/EnableJIT.swift | 452 ++++++++++++++++++ AltJIT/Commands/MountDisk.swift | 75 +++ AltJIT/Extensions/Logger+AltJIT.swift | 16 + AltJIT/Extensions/Task+Timeout.swift | 57 +++ AltJIT/Extensions/URL+Tools.swift | 15 + AltJIT/Types/ALTErrorKeys.h | 14 + AltJIT/Types/ALTErrorKeys.m | 13 + AltJIT/Types/PythonCommand.swift | 58 +++ .../Types/RemoteServiceDiscoveryTunnel.swift | 41 ++ AltStore.xcodeproj/project.pbxproj | 288 ++++++++++- .../xcshareddata/xcschemes/AltJIT.xcscheme | 101 ++++ .../xcshareddata/swiftpm/Package.resolved | 9 + Shared/Categories/NSError+ALTServerError.m | 15 +- Shared/Errors/ALTLocalizedError.swift | 3 + Shared/Errors/JITError.swift | 52 ++ Shared/Errors/ProcessError.swift | 88 ++++ Shared/Extensions/Process+Conveniences.swift | 151 ++++++ Shared/Types/AppProcess.swift | 35 ++ 20 files changed, 1503 insertions(+), 7 deletions(-) create mode 100644 AltJIT/AltJIT-Bridging-Header.h create mode 100644 AltJIT/AltJIT.swift create mode 100644 AltJIT/Commands/EnableJIT.swift create mode 100644 AltJIT/Commands/MountDisk.swift create mode 100644 AltJIT/Extensions/Logger+AltJIT.swift create mode 100644 AltJIT/Extensions/Task+Timeout.swift create mode 100644 AltJIT/Extensions/URL+Tools.swift create mode 100644 AltJIT/Types/ALTErrorKeys.h create mode 100644 AltJIT/Types/ALTErrorKeys.m create mode 100644 AltJIT/Types/PythonCommand.swift create mode 100644 AltJIT/Types/RemoteServiceDiscoveryTunnel.swift create mode 100644 AltStore.xcodeproj/xcshareddata/xcschemes/AltJIT.xcscheme create mode 100644 Shared/Errors/JITError.swift create mode 100644 Shared/Errors/ProcessError.swift create mode 100644 Shared/Extensions/Process+Conveniences.swift create mode 100644 Shared/Types/AppProcess.swift diff --git a/AltJIT/AltJIT-Bridging-Header.h b/AltJIT/AltJIT-Bridging-Header.h new file mode 100644 index 00000000..807b3139 --- /dev/null +++ b/AltJIT/AltJIT-Bridging-Header.h @@ -0,0 +1,9 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + +#import "ALTErrorKeys.h" + +// Shared +#import "ALTWrappedError.h" +#import "NSError+ALTServerError.h" diff --git a/AltJIT/AltJIT.swift b/AltJIT/AltJIT.swift new file mode 100644 index 00000000..cc73f33b --- /dev/null +++ b/AltJIT/AltJIT.swift @@ -0,0 +1,18 @@ +// +// AltJIT.swift +// AltJIT +// +// Created by Riley Testut on 8/29/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import OSLog +import ArgumentParser + +@main +struct AltJIT: AsyncParsableCommand +{ + static let configuration = CommandConfiguration(commandName: "altjit", + abstract: "Enable JIT for sideloaded apps.", + subcommands: [EnableJIT.self, MountDisk.self]) +} diff --git a/AltJIT/Commands/EnableJIT.swift b/AltJIT/Commands/EnableJIT.swift new file mode 100644 index 00000000..2a50017a --- /dev/null +++ b/AltJIT/Commands/EnableJIT.swift @@ -0,0 +1,452 @@ +// +// EnableJIT.swift +// AltPackage +// +// Created by Riley Testut on 8/29/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation +import OSLog +import RegexBuilder + +import ArgumentParser + +struct EnableJIT: PythonCommand +{ + static let configuration = CommandConfiguration(commandName: "enable", abstract: "Enable JIT for a specific app on your device.") + + @Argument(help: "The name or PID of the app to enable JIT for.", transform: AppProcess.init) + var process: AppProcess + + @Option(help: "Your iOS device's UDID.") + var udid: String + + // PythonCommand + var pythonPath: String? + + mutating func run() async throws + { + // Use local variables to fix "escaping autoclosure captures mutating self parameter" compiler error. + let process = self.process + let udid = self.udid + + do + { + do + { + Logger.main.info("Enabling JIT for \(process, privacy: .private(mask: .hash)) on device \(udid, privacy: .private(mask: .hash))...") + + try await self.prepare() + + let rsdTunnel = try await self.startRSDTunnel() + defer { rsdTunnel.process.terminate() } + print("Connected to device \(self.udid)!", rsdTunnel) + + let port = try await self.startDebugServer(rsdTunnel: rsdTunnel) + print("Started debugserver on port \(port).") + + print("Attaching debugger...") + let lldb = try await self.attachDebugger(ipAddress: rsdTunnel.ipAddress, port: port) + defer { lldb.terminate() } + print("Attached debugger to \(process).") + + try await self.detachDebugger(lldb) + print("Detached debugger from \(process).") + + print("✅ Successfully enabled JIT for \(process) on device \(udid)!") + } + catch let error as ProcessError + { + if let output = error.output + { + print(output) + } + + throw error + } + } + catch + { + print("❌ Unable to enable JIT for \(process) on device \(udid).") + print(error.localizedDescription) + + Logger.main.error("Failed to enable JIT for \(process, privacy: .private(mask: .hash)) on device \(udid, privacy: .private(mask: .hash)). \(error, privacy: .public)") + + throw ExitCode.failure + } + } +} + +private extension EnableJIT +{ + func startRSDTunnel() async throws -> RemoteServiceDiscoveryTunnel + { + do + { + Logger.main.info("Starting RSD tunnel...") + + let process = try Process.launch(.python3, arguments: ["-u", "-m", "pymobiledevice3", "remote", "start-quic-tunnel", "--udid", self.udid], environment: self.processEnvironment) + + do + { + let rsdTunnel = try await withTimeout(seconds: 20) { + let regex = Regex { + "--rsd" + + OneOrMore(.whitespace) + + Capture { + OneOrMore(.anyGraphemeCluster) + } + + OneOrMore(.whitespace) + + TryCapture { + OneOrMore(.digit) + } transform: { match in + Int(match) + } + } + + for try await line in process.outputLines + { + if let match = line.firstMatch(of: regex) + { + let rsdTunnel = RemoteServiceDiscoveryTunnel(ipAddress: String(match.1), port: match.2, process: process) + return rsdTunnel + } + } + + throw ProcessError.unexpectedOutput(executableURL: .python3, output: process.output) + } + + // MUST close standardOutput in order to stream output later. + process.stopOutput() + + return rsdTunnel + } + catch is TimedOutError + { + process.terminate() + + let error = ProcessError.timedOut(executableURL: .python3, output: process.output) + throw error + } + catch + { + process.terminate() + throw error + } + } + catch let error as NSError + { + let localizedFailure = NSLocalizedString("Could not connect to device \(self.udid).", comment: "") + throw error.withLocalizedFailure(localizedFailure) + } + } + + func startDebugServer(rsdTunnel: RemoteServiceDiscoveryTunnel) async throws -> Int + { + do + { + Logger.main.info("Starting debugserver...") + + return try await withTimeout(seconds: 10) { + let arguments = ["-u", "-m", "pymobiledevice3", "developer", "debugserver", "start-server"] + rsdTunnel.commandArguments + + let output = try await Process.launchAndWait(.python3, arguments: arguments, environment: self.processEnvironment) + + let port = Reference(Int.self) + let regex = Regex { + "connect://" + + OneOrMore(.anyGraphemeCluster, .eager) + + ":" + + TryCapture(as: port) { + OneOrMore(.digit) + } transform: { match in + Int(match) + } + } + + if let match = output.firstMatch(of: regex) + { + return match[port] + } + + throw ProcessError.unexpectedOutput(executableURL: .python3, output: output) + } + } + catch let error as NSError + { + let localizedFailure = NSLocalizedString("Could not start debugserver on device \(self.udid).", comment: "") + throw error.withLocalizedFailure(localizedFailure) + } + } + + func attachDebugger(ipAddress: String, port: Int) async throws -> Process + { + do + { + Logger.main.info("Attaching debugger...") + + let processID: Int + + switch self.process + { + case .pid(let pid): processID = pid + case .name(let name): + guard let pid = try await self.getPID(for: name) else { throw JITError.processNotRunning(self.process) } + processID = pid + } + + let process = try Process.launch(.lldb, environment: self.processEnvironment) + + do + { + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + + // // Throw error if program terminates. + // taskGroup.addTask { + // try await withCheckedThrowingContinuation { continuation in + // process.terminationHandler = { process in + // Task { + // // Should NEVER be called unless an error occurs. + // continuation.resume(throwing: ProcessError.terminated(executableURL: .lldb, exitCode: process.terminationStatus, output: process.output)) + // } + // } + // } + // } + + taskGroup.addTask { + do + { + try await self.sendDebuggerCommand("platform select remote-ios", to: process, timeout: 5) { + ChoiceOf { + "SDK Roots:" + "unable to locate SDK" + } + } + + let ipAddress = "[\(ipAddress)]" + let connectCommand = "process connect connect://\(ipAddress):\(port)" + try await self.sendDebuggerCommand(connectCommand, to: process, timeout: 10) + + try await self.sendDebuggerCommand("settings set target.memory-module-load-level minimal", to: process, timeout: 5) + + let attachCommand = "attach -p \(processID)" + let failureMessage = "attach failed" + let output = try await self.sendDebuggerCommand(attachCommand, to: process, timeout: 120) { + + ChoiceOf { + failureMessage + + Regex { + "Process " + OneOrMore(.digit) + " stopped" + } + } + } + + if output.contains(failureMessage) + { + throw ProcessError.failed(executableURL: .lldb, exitCode: -1, output: process.output) + } + } + catch is TimedOutError + { + let error = ProcessError.timedOut(executableURL: .lldb, output: process.output) + throw error + } + } + + // Wait until first child task returns + _ = try await taskGroup.next()! + + // Cancel remaining tasks + taskGroup.cancelAll() + } + + return process + } + catch + { + process.terminate() + throw error + } + } + catch let error as NSError + { + let localizedFailure = String(format: NSLocalizedString("Could not attach debugger to %@.", comment: ""), self.process.description) + throw error.withLocalizedFailure(localizedFailure) + } + } + + func detachDebugger(_ process: Process) async throws + { + do + { + Logger.main.info("Detaching debugger...") + + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + +// // Throw error if program terminates. +// taskGroup.addTask { +// try await withCheckedThrowingContinuation { continuation in +// process.terminationHandler = { process in +// if process.terminationStatus == 0 +// { +// continuation.resume() +// } +// else +// { +// continuation.resume(throwing: ProcessError.terminated(executableURL: .lldb, exitCode: process.terminationStatus, output: process.output)) +// } +// } +// } +// } + + taskGroup.addTask { + do + { + try await self.sendDebuggerCommand("c", to: process, timeout: 10) { + "Process " + OneOrMore(.digit) + " resuming" + } + + try await self.sendDebuggerCommand("detach", to: process, timeout: 10) { + "Process " + OneOrMore(.digit) + " detached" + } + } + catch is TimedOutError + { + let error = ProcessError.timedOut(executableURL: .lldb, output: process.output) + throw error + } + } + + // Wait until first child task returns + _ = try await taskGroup.next()! + + // Cancel remaining tasks + taskGroup.cancelAll() + } + } + catch let error as NSError + { + let localizedFailure = NSLocalizedString("Could not detach debugger from \(self.process).", comment: "") + throw error.withLocalizedFailure(localizedFailure) + } + } +} + +private extension EnableJIT +{ + func getPID(for name: String) async throws -> Int? + { + Logger.main.info("Retrieving PID for \(name, privacy: .private(mask: .hash))...") + + let arguments = ["-m", "pymobiledevice3", "processes", "pgrep", name, "--udid", self.udid] + let output = try await Process.launchAndWait(.python3, arguments: arguments, environment: self.processEnvironment) + + let regex = Regex { + "INFO" + + OneOrMore(.whitespace) + + TryCapture { + OneOrMore(.digit) + } transform: { match in + Int(match) + } + + OneOrMore(.whitespace) + + name + } + + if let match = output.firstMatch(of: regex) + { + Logger.main.info("\(name, privacy: .private(mask: .hash)) PID is \(match.1)") + return match.1 + } + + return nil + } + + @discardableResult + func sendDebuggerCommand(_ command: String, to process: Process, timeout: TimeInterval, + @RegexComponentBuilder regex: @escaping () -> (some RegexComponent)? = { Optional>.none }) async throws -> String + { + guard let inputPipe = process.standardInput as? Pipe else { preconditionFailure("`process` must have a Pipe as its standardInput") } + defer { + inputPipe.fileHandleForWriting.writeabilityHandler = nil + } + + let initialOutput = process.output + + let data = (command + "\n").data(using: .utf8)! // Will always succeed. + Logger.main.info("Sending lldb command: \(command, privacy: .public)") + + let output = try await withTimeout(seconds: timeout) { + // Wait until process is ready to receive input. + try await withCheckedThrowingContinuation { continuation in + inputPipe.fileHandleForWriting.writeabilityHandler = { fileHandle in + inputPipe.fileHandleForWriting.writeabilityHandler = nil + + let result = Result { try fileHandle.write(contentsOf: data) } + continuation.resume(with: result) + } + } + + // Wait until we receive at least one line of output. + for try await _ in process.outputLines + { + break + } + + // Keep waiting until output doesn't change. + // If regex is provided, we keep waiting until a match is found. + var previousOutput = process.output + while true + { + try await Task.sleep(for: .seconds(0.2)) + + let output = process.output + if output == previousOutput + { + guard let regex = regex() else { + // No regex, so break as soon as output stops changing. + break + } + + if output.contains(regex) + { + // Found a match, so exit while loop. + break + } + else + { + // Output hasn't changed, but regex does not match (yet). + continue + } + } + + previousOutput = output + } + + return previousOutput + } + + // Subtract initialOutput from output to get just this command's output. + let commandOutput = output.replacingOccurrences(of: initialOutput, with: "") + return commandOutput + } +} diff --git a/AltJIT/Commands/MountDisk.swift b/AltJIT/Commands/MountDisk.swift new file mode 100644 index 00000000..802feabb --- /dev/null +++ b/AltJIT/Commands/MountDisk.swift @@ -0,0 +1,75 @@ +// +// MountDisk.swift +// AltPackage +// +// Created by Riley Testut on 8/31/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation +import OSLog + +import ArgumentParser + +typealias MountError = MountErrorCode.Error +enum MountErrorCode: Int, ALTErrorEnum +{ + case alreadyMounted + + var errorFailureReason: String { + switch self + { + case .alreadyMounted: return NSLocalizedString("A personalized Developer Disk is already mounted.", comment: "") + } + } +} + +struct MountDisk: PythonCommand +{ + static let configuration = CommandConfiguration(commandName: "mount", abstract: "Mount a personalized developer disk image onto an iOS device.") + + @Option(help: "The iOS device's UDID.") + var udid: String + + // PythonCommand + var pythonPath: String? + + mutating func run() async throws + { + do + { + print("Mounting personalized developer disk...") + + try await self.prepare() + + let output = try await Process.launchAndWait(.python3, arguments: ["-m", "pymobiledevice3", "mounter", "auto-mount", "--udid", self.udid]) + if !output.contains("DeveloperDiskImage") + { + throw ProcessError.unexpectedOutput(executableURL: .python3, output: output) + } + + if output.contains("already mounted") + { + throw MountError(.alreadyMounted) + } + + print("✅ Successfully mounted personalized Developer Disk!") + } + catch let error as MountError where error.code == .alreadyMounted + { + // Prepend ⚠️ since this is not really an error. + let localizedDescription = "⚠️ " + error.localizedDescription + print(localizedDescription) + + throw ExitCode.success + } + catch + { + // Output failure message first before error. + print("❌ Unable to mount personalized Developer Disk.") + print(error.localizedDescription) + + throw ExitCode.failure + } + } +} diff --git a/AltJIT/Extensions/Logger+AltJIT.swift b/AltJIT/Extensions/Logger+AltJIT.swift new file mode 100644 index 00000000..b8453141 --- /dev/null +++ b/AltJIT/Extensions/Logger+AltJIT.swift @@ -0,0 +1,16 @@ +// +// Logger+AltJIT.swift +// AltJIT +// +// Created by Riley Testut on 8/29/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import OSLog + +public extension Logger +{ + static let altjitSubsystem = Bundle.main.bundleIdentifier! + + static let main = Logger(subsystem: altjitSubsystem, category: "AltJIT") +} diff --git a/AltJIT/Extensions/Task+Timeout.swift b/AltJIT/Extensions/Task+Timeout.swift new file mode 100644 index 00000000..33f448f3 --- /dev/null +++ b/AltJIT/Extensions/Task+Timeout.swift @@ -0,0 +1,57 @@ +// +// Task+Timeout.swift +// AltPackage +// +// Created by Riley Testut on 8/31/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// +// Based heavily on https://forums.swift.org/t/running-an-async-task-with-a-timeout/49733/13 +// + +import Foundation + +struct TimedOutError: LocalizedError +{ + var duration: TimeInterval + + public var errorDescription: String? { + //TODO: Change pluralization for 1 second. + let errorDescription = String(format: NSLocalizedString("The task timed out after %@ seconds.", comment: ""), self.duration.formatted()) + return errorDescription + } +} + +/// +/// Execute an operation in the current task subject to a timeout. +/// +/// - Parameters: +/// - seconds: The duration in seconds `operation` is allowed to run before timing out. +/// - operation: The async operation to perform. +/// - Returns: Returns the result of `operation` if it completed in time. +/// - Throws: Throws ``TimedOutError`` if the timeout expires before `operation` completes. +/// If `operation` throws an error before the timeout expires, that error is propagated to the caller. +func withTimeout(seconds: TimeInterval, file: StaticString = #file, line: Int = #line, operation: @escaping @Sendable () async throws -> R) async throws -> R +{ + return try await withThrowingTaskGroup(of: R.self) { group in + let deadline = Date(timeIntervalSinceNow: seconds) + + // Start actual work. + group.addTask { + return try await operation() + } + // Start timeout child task. + group.addTask { + let interval = deadline.timeIntervalSinceNow + if interval > 0 { + try await Task.sleep(for: .seconds(interval)) + } + try Task.checkCancellation() + // We’ve reached the timeout. + throw TimedOutError(duration: seconds) + } + // First finished child task wins, cancel the other task. + let result = try await group.next()! + group.cancelAll() + return result + } +} diff --git a/AltJIT/Extensions/URL+Tools.swift b/AltJIT/Extensions/URL+Tools.swift new file mode 100644 index 00000000..364be414 --- /dev/null +++ b/AltJIT/Extensions/URL+Tools.swift @@ -0,0 +1,15 @@ +// +// URL+Tools.swift +// AltJIT +// +// Created by Riley Testut on 9/3/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +extension URL +{ + static let python3 = URL(fileURLWithPath: "/usr/bin/python3") + static let lldb = URL(fileURLWithPath: "/usr/bin/lldb") +} diff --git a/AltJIT/Types/ALTErrorKeys.h b/AltJIT/Types/ALTErrorKeys.h new file mode 100644 index 00000000..a169eaaf --- /dev/null +++ b/AltJIT/Types/ALTErrorKeys.h @@ -0,0 +1,14 @@ +// +// ALTErrorKeys.h +// AltJIT +// +// Created by Riley Testut on 9/1/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +@import Foundation; + +// Can't import AltSign (for reasons), so re-declare these constants here instead. +extern NSErrorUserInfoKey const ALTSourceFileErrorKey; +extern NSErrorUserInfoKey const ALTSourceLineErrorKey; +extern NSErrorUserInfoKey const ALTAppNameErrorKey; diff --git a/AltJIT/Types/ALTErrorKeys.m b/AltJIT/Types/ALTErrorKeys.m new file mode 100644 index 00000000..48040989 --- /dev/null +++ b/AltJIT/Types/ALTErrorKeys.m @@ -0,0 +1,13 @@ +// +// ALTErrorKeys.m +// AltJIT +// +// Created by Riley Testut on 9/1/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +#import "ALTErrorKeys.h" + +NSErrorUserInfoKey const ALTSourceFileErrorKey = @"ALTSourceFile"; +NSErrorUserInfoKey const ALTSourceLineErrorKey = @"ALTSourceLine"; +NSErrorUserInfoKey const ALTAppNameErrorKey = @"appName"; diff --git a/AltJIT/Types/PythonCommand.swift b/AltJIT/Types/PythonCommand.swift new file mode 100644 index 00000000..0f493e71 --- /dev/null +++ b/AltJIT/Types/PythonCommand.swift @@ -0,0 +1,58 @@ +// +// PythonCommand.swift +// AltJIT +// +// Created by Riley Testut on 9/6/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import ArgumentParser + +protocol PythonCommand: AsyncParsableCommand +{ + var pythonPath: String? { get set } +} + +extension PythonCommand +{ + var processEnvironment: [String: String] { + var environment = ProcessInfo.processInfo.environment + + if let pythonPath + { + environment["PYTHONPATH"] = pythonPath + } + + return environment + } + + mutating func prepare() async throws + { + let pythonPath = try await self.readPythonPath() + self.pythonPath = pythonPath.path(percentEncoded: false) + } +} + +private extension PythonCommand +{ + func readPythonPath() async throws -> URL + { + let processOutput: String + + do + { + processOutput = try await Process.launchAndWait(.python3, arguments: ["-m", "site", "--user-site"]) + } + catch let error as ProcessError where error.exitCode == 2 + { + // Ignore exit code 2. + guard let output = error.output else { throw error } + processOutput = output + } + + let sanitizedOutput = processOutput.trimmingCharacters(in: .whitespacesAndNewlines) + + let pythonURL = URL(filePath: sanitizedOutput) + return pythonURL + } +} diff --git a/AltJIT/Types/RemoteServiceDiscoveryTunnel.swift b/AltJIT/Types/RemoteServiceDiscoveryTunnel.swift new file mode 100644 index 00000000..545b91db --- /dev/null +++ b/AltJIT/Types/RemoteServiceDiscoveryTunnel.swift @@ -0,0 +1,41 @@ +// +// RemoteServiceDiscoveryTunnel.swift +// AltJIT +// +// Created by Riley Testut on 9/3/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +final class RemoteServiceDiscoveryTunnel +{ + let ipAddress: String + let port: Int + + let process: Process + + var commandArguments: [String] { + ["--rsd", self.ipAddress, String(self.port)] + } + + init(ipAddress: String, port: Int, process: Process) + { + self.ipAddress = ipAddress + self.port = port + + self.process = process + } + + deinit + { + self.process.terminate() + } +} + +extension RemoteServiceDiscoveryTunnel: CustomStringConvertible +{ + var description: String { + "\(self.ipAddress) \(self.port)" + } +} diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 82b8d1b6..861232c4 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -356,10 +356,13 @@ D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; }; D51AD28029356B8000967AAA /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; }; D52C08EE28AEC37A006C4AE5 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */; }; + D52DD35E2AAA89A600A7F2B6 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D52DD35D2AAA89A600A7F2B6 /* AltSign-Dynamic */; }; + D52DD35F2AAA89A700A7F2B6 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D52DD35D2AAA89A600A7F2B6 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52EF2BD2A0594550096C377 /* AppDetailCollectionViewController.swift */; }; D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8B62727841800A9B5DD /* libAppleArchive.tbd */; settings = {ATTRIBUTES = (Weak, ); }; }; D533E8BC2727BBEE00A9B5DD /* libfragmentzip.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */; }; D533E8BE2727BBF800A9B5DD /* libcurl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D533E8BD2727BBF800A9B5DD /* libcurl.a */; }; + D537C85B2AA9507A009A1E08 /* libcorecrypto.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D537C85A2AA95066009A1E08 /* libcorecrypto.tbd */; platformFilters = (macos, ); }; D53D84022A2158FC00543C3B /* Permissions.plist in Resources */ = {isa = PBXBuildFile; fileRef = D53D84012A2158FC00543C3B /* Permissions.plist */; }; D54058B92A1D6269008CCC58 /* AppPermissionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54058B82A1D6269008CCC58 /* AppPermissionProtocol.swift */; }; D54058BB2A1D8FE3008CCC58 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */; }; @@ -394,7 +397,15 @@ D5935AED29C39DE300C157EF /* SourceDetailsComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5935AEC29C39DE300C157EF /* SourceDetailsComponents.swift */; }; D5935AEF29C3B23600C157EF /* Sources.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5935AEE29C3B23600C157EF /* Sources.storyboard */; }; D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D593F1932717749A006E82DE /* PatchAppOperation.swift */; }; + D59A6B7B2AA91B8E00F61259 /* PythonCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7A2AA91B8E00F61259 /* PythonCommand.swift */; }; + D59A6B7F2AA9226C00F61259 /* AppProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7D2AA9226C00F61259 /* AppProcess.swift */; }; + D59A6B822AA92D1C00F61259 /* Process+Conveniences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B802AA92D1C00F61259 /* Process+Conveniences.swift */; }; + D59A6B842AA932F700F61259 /* Logger+AltServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B832AA932F700F61259 /* Logger+AltServer.swift */; }; D5A0537329B91DB400997551 /* SourceDetailContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A0537229B91DB400997551 /* SourceDetailContentViewController.swift */; }; + D5A1D2E42AA50EB60066CACC /* JITError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2E32AA50EB60066CACC /* JITError.swift */; }; + D5A1D2E92AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2E82AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift */; }; + D5A1D2EB2AA513410066CACC /* URL+Tools.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2EA2AA513410066CACC /* URL+Tools.swift */; }; + D5A1D2EC2AA51D490066CACC /* ProcessError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1B2AA284ED00EF863D /* ProcessError.swift */; }; D5A2193429B14F94002229FC /* DeprecatedAPIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */; }; D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; }; D5CA0C4B280E141900469595 /* ManagedPatron.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CA0C4A280E141900469595 /* ManagedPatron.swift */; }; @@ -416,6 +427,18 @@ D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; }; D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; }; D5FB7A0E2AA25A4E00EF863D /* Previews.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */; }; + D5FB7A212AA284ED00EF863D /* EnableJIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */; }; + D5FB7A242AA284ED00EF863D /* Logger+AltJIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1D2AA284ED00EF863D /* Logger+AltJIT.swift */; }; + D5FB7A252AA284ED00EF863D /* AltJIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1E2AA284ED00EF863D /* AltJIT.swift */; }; + D5FB7A262AA284ED00EF863D /* MountDisk.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1F2AA284ED00EF863D /* MountDisk.swift */; }; + D5FB7A272AA284ED00EF863D /* Task+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A202AA284ED00EF863D /* Task+Timeout.swift */; }; + D5FB7A2A2AA2854100EF863D /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; }; + D5FB7A2B2AA2854400EF863D /* UserInfoValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5189C002A01BC6800F44625 /* UserInfoValue.swift */; }; + D5FB7A2E2AA2859400EF863D /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = D5FB7A2D2AA2859400EF863D /* ArgumentParser */; }; + D5FB7A312AA28A2900EF863D /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; }; + D5FB7A322AA28A4000EF863D /* ALTWrappedError.m in Sources */ = {isa = PBXBuildFile; fileRef = D51AD27D29356B7B00967AAA /* ALTWrappedError.m */; }; + D5FB7A392AA28D8300EF863D /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; }; + D5FB7A472AA293D000EF863D /* ALTErrorKeys.m in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A462AA293D000EF863D /* ALTErrorKeys.m */; }; D5FD4EC52A952EAD0097BEE8 /* AltWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */; }; D5FD4EC92A9530C00097BEE8 /* AppSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FD4EC82A9530C00097BEE8 /* AppSnapshot.swift */; }; D5FD4ECB2A9532960097BEE8 /* DatabaseManager+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */; }; @@ -457,6 +480,13 @@ remoteGlobalIDString = BFF7C903257844C900E55F36; remoteInfo = AltXPC; }; + D537C8562AA94D4A009A1E08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFD247622284B9A500981D42 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D5FB7A122AA284BE00EF863D; + remoteInfo = AltJIT; + }; D586D39C28EF58B0000E101F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFD247622284B9A500981D42 /* Project object */; @@ -523,6 +553,17 @@ name = "Embed XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; + D52DD3602AAA89A700A7F2B6 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + D52DD35F2AAA89A700A7F2B6 /* AltSign-Dynamic in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; D561B2EC28EF5A4F006752E4 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -534,6 +575,15 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + D5FB7A112AA284BE00EF863D /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = /usr/share/man/man1/; + dstSubfolderSpec = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 1; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -897,6 +947,7 @@ D533E8B82727B61400A9B5DD /* fragmentzip.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = fragmentzip.h; sourceTree = ""; }; D533E8BB2727BBEE00A9B5DD /* libfragmentzip.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libfragmentzip.a; path = Dependencies/fragmentzip/libfragmentzip.a; sourceTree = SOURCE_ROOT; }; D533E8BD2727BBF800A9B5DD /* libcurl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libcurl.a; path = Dependencies/libcurl/libcurl.a; sourceTree = SOURCE_ROOT; }; + D537C85A2AA95066009A1E08 /* libcorecrypto.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcorecrypto.tbd; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.0.sdk/usr/lib/system/libcorecrypto.tbd; sourceTree = DEVELOPER_DIR; }; D53D84012A2158FC00543C3B /* Permissions.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Permissions.plist; sourceTree = ""; }; D54058B82A1D6269008CCC58 /* AppPermissionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionProtocol.swift; sourceTree = ""; }; D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = ""; }; @@ -932,7 +983,13 @@ D5935AEC29C39DE300C157EF /* SourceDetailsComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailsComponents.swift; sourceTree = ""; }; D5935AEE29C3B23600C157EF /* Sources.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Sources.storyboard; sourceTree = ""; }; D593F1932717749A006E82DE /* PatchAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchAppOperation.swift; sourceTree = ""; }; + D59A6B7A2AA91B8E00F61259 /* PythonCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PythonCommand.swift; sourceTree = ""; }; + D59A6B7D2AA9226C00F61259 /* AppProcess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProcess.swift; sourceTree = ""; }; + D59A6B802AA92D1C00F61259 /* Process+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Process+Conveniences.swift"; sourceTree = ""; }; D5A0537229B91DB400997551 /* SourceDetailContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDetailContentViewController.swift; sourceTree = ""; }; + D5A1D2E32AA50EB60066CACC /* JITError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JITError.swift; sourceTree = ""; }; + D5A1D2E82AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteServiceDiscoveryTunnel.swift; sourceTree = ""; }; + D5A1D2EA2AA513410066CACC /* URL+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Tools.swift"; sourceTree = ""; }; D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = ""; }; D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = ""; }; D5CA0C4A280E141900469595 /* ManagedPatron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedPatron.swift; sourceTree = ""; }; @@ -953,6 +1010,16 @@ D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = ""; }; D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = ""; }; D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Previews.xcassets; sourceTree = ""; }; + D5FB7A132AA284BE00EF863D /* altjit */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = altjit; sourceTree = BUILT_PRODUCTS_DIR; }; + D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnableJIT.swift; path = AltJIT/Commands/EnableJIT.swift; sourceTree = SOURCE_ROOT; }; + D5FB7A1B2AA284ED00EF863D /* ProcessError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProcessError.swift; path = Shared/Errors/ProcessError.swift; sourceTree = SOURCE_ROOT; }; + D5FB7A1D2AA284ED00EF863D /* Logger+AltJIT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Logger+AltJIT.swift"; path = "AltJIT/Extensions/Logger+AltJIT.swift"; sourceTree = SOURCE_ROOT; }; + D5FB7A1E2AA284ED00EF863D /* AltJIT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AltJIT.swift; path = AltJIT/AltJIT.swift; sourceTree = SOURCE_ROOT; }; + D5FB7A1F2AA284ED00EF863D /* MountDisk.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MountDisk.swift; path = AltJIT/Commands/MountDisk.swift; sourceTree = SOURCE_ROOT; }; + D5FB7A202AA284ED00EF863D /* Task+Timeout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Task+Timeout.swift"; path = "AltJIT/Extensions/Task+Timeout.swift"; sourceTree = SOURCE_ROOT; }; + D5FB7A382AA28AC700EF863D /* AltJIT-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "AltJIT-Bridging-Header.h"; path = "AltJIT/AltJIT-Bridging-Header.h"; sourceTree = SOURCE_ROOT; }; + D5FB7A452AA293D000EF863D /* ALTErrorKeys.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTErrorKeys.h; sourceTree = ""; }; + D5FB7A462AA293D000EF863D /* ALTErrorKeys.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTErrorKeys.m; sourceTree = ""; }; D5FD4EC42A952EAD0097BEE8 /* AltWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltWidgetBundle.swift; sourceTree = ""; }; D5FD4EC82A9530C00097BEE8 /* AppSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSnapshot.swift; sourceTree = ""; }; D5FD4ECA2A9532960097BEE8 /* DatabaseManager+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseManager+Async.swift"; sourceTree = ""; }; @@ -1051,6 +1118,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D5FB7A102AA284BE00EF863D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D52DD35E2AAA89A600A7F2B6 /* AltSign-Dynamic in Frameworks */, + D537C85B2AA9507A009A1E08 /* libcorecrypto.tbd in Frameworks */, + D5FB7A2E2AA2859400EF863D /* ArgumentParser in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -1118,6 +1195,7 @@ D5DB145728F9DC0300A8F606 /* Errors */, BFF767C32489A6800097E58C /* Extensions */, BFF767C42489A6980097E58C /* Categories */, + D59A6B7C2AA9225C00F61259 /* Types */, ); path = Shared; sourceTree = ""; @@ -1659,6 +1737,7 @@ BF18BFE824857D7900DD5981 /* AltDaemon */, BF98916C250AABF3002ACF50 /* AltWidget */, BFF7C905257844C900E55F36 /* AltXPC */, + D5FB7A142AA284BE00EF863D /* AltJIT */, D586D39928EF58B0000E101F /* AltTests */, BFD247852284BB3300981D42 /* Frameworks */, BFD2476B2284B9A500981D42 /* Products */, @@ -1679,6 +1758,7 @@ BF989167250AABF3002ACF50 /* AltWidgetExtension.appex */, BFF7C904257844C900E55F36 /* AltXPC.xpc */, D586D39828EF58B0000E101F /* AltTests.xctest */, + D5FB7A132AA284BE00EF863D /* altjit */, ); name = Products; sourceTree = ""; @@ -1719,6 +1799,7 @@ BFD247852284BB3300981D42 /* Frameworks */ = { isa = PBXGroup; children = ( + D537C85A2AA95066009A1E08 /* libcorecrypto.tbd */, D533E8B62727841800A9B5DD /* libAppleArchive.tbd */, BF088D322501A4FF008082D9 /* OpenSSL.xcframework */, BF580497246A3D19008AE704 /* UIKit.framework */, @@ -1905,6 +1986,7 @@ BFF435D7255CBDAB00DD724F /* ALTApplication+AltStoreApp.swift */, BF6C336124197D700034FD24 /* NSError+AltStore.swift */, D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */, + D59A6B802AA92D1C00F61259 /* Process+Conveniences.swift */, ); path = Extensions; sourceTree = ""; @@ -1988,6 +2070,17 @@ path = Model; sourceTree = ""; }; + D522F9562AA509E9003E57D1 /* Types */ = { + isa = PBXGroup; + children = ( + D5FB7A452AA293D000EF863D /* ALTErrorKeys.h */, + D5FB7A462AA293D000EF863D /* ALTErrorKeys.m */, + D5A1D2E82AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift */, + D59A6B7A2AA91B8E00F61259 /* PythonCommand.swift */, + ); + path = Types; + sourceTree = ""; + }; D54058B72A1D6251008CCC58 /* Previews */ = { isa = PBXGroup; children = ( @@ -2042,6 +2135,14 @@ path = "Error Log"; sourceTree = ""; }; + D59A6B7C2AA9225C00F61259 /* Types */ = { + isa = PBXGroup; + children = ( + D59A6B7D2AA9226C00F61259 /* AppProcess.swift */, + ); + path = Types; + sourceTree = ""; + }; D5DB145728F9DC0300A8F606 /* Errors */ = { isa = PBXGroup; children = ( @@ -2049,10 +2150,43 @@ D5189C002A01BC6800F44625 /* UserInfoValue.swift */, D51AD27C29356B7B00967AAA /* ALTWrappedError.h */, D51AD27D29356B7B00967AAA /* ALTWrappedError.m */, + D5FB7A1B2AA284ED00EF863D /* ProcessError.swift */, + D5A1D2E32AA50EB60066CACC /* JITError.swift */, ); path = Errors; sourceTree = ""; }; + D5FB7A142AA284BE00EF863D /* AltJIT */ = { + isa = PBXGroup; + children = ( + D5FB7A1E2AA284ED00EF863D /* AltJIT.swift */, + D5FB7A382AA28AC700EF863D /* AltJIT-Bridging-Header.h */, + D5FB7A282AA2851200EF863D /* Commands */, + D522F9562AA509E9003E57D1 /* Types */, + D5FB7A292AA2851C00EF863D /* Extensions */, + ); + path = AltJIT; + sourceTree = ""; + }; + D5FB7A282AA2851200EF863D /* Commands */ = { + isa = PBXGroup; + children = ( + D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */, + D5FB7A1F2AA284ED00EF863D /* MountDisk.swift */, + ); + path = Commands; + sourceTree = ""; + }; + D5FB7A292AA2851C00EF863D /* Extensions */ = { + isa = PBXGroup; + children = ( + D5FB7A1D2AA284ED00EF863D /* Logger+AltJIT.swift */, + D5A1D2EA2AA513410066CACC /* URL+Tools.swift */, + D5FB7A202AA284ED00EF863D /* Task+Timeout.swift */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -2321,13 +2455,35 @@ productReference = D586D39828EF58B0000E101F /* AltTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + D5FB7A122AA284BE00EF863D /* AltJIT */ = { + isa = PBXNativeTarget; + buildConfigurationList = D5FB7A172AA284BE00EF863D /* Build configuration list for PBXNativeTarget "AltJIT" */; + buildPhases = ( + D5FB7A0F2AA284BE00EF863D /* Sources */, + D5FB7A102AA284BE00EF863D /* Frameworks */, + D5FB7A112AA284BE00EF863D /* CopyFiles */, + D52DD3602AAA89A700A7F2B6 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = AltJIT; + packageProductDependencies = ( + D5FB7A2D2AA2859400EF863D /* ArgumentParser */, + D52DD35D2AAA89A600A7F2B6 /* AltSign-Dynamic */, + ); + productName = AltJIT; + productReference = D5FB7A132AA284BE00EF863D /* altjit */; + productType = "com.apple.product-type.tool"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ BFD247622284B9A500981D42 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1400; + LastSwiftUpdateCheck = 1500; LastUpgradeCheck = 1410; ORGANIZATIONNAME = "Riley Testut"; TargetAttributes = { @@ -2384,6 +2540,9 @@ CreatedOnToolsVersion = 14.0.1; TestTargetID = BFD247692284B9A500981D42; }; + D5FB7A122AA284BE00EF863D = { + CreatedOnToolsVersion = 15.0; + }; }; }; buildConfigurationList = BFD247652284B9A500981D42 /* Build configuration list for PBXProject "AltStore" */; @@ -2397,6 +2556,7 @@ mainGroup = BFD247612284B9A500981D42; packageReferences = ( D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */, + D5FB7A2C2AA2859400EF863D /* XCRemoteSwiftPackageReference "swift-argument-parser" */, ); productRefGroup = BFD2476B2284B9A500981D42 /* Products */; projectDirPath = ""; @@ -2411,6 +2571,7 @@ BF66EE7D2501AE50007EE018 /* AltStoreCore */, BF989166250AABF3002ACF50 /* AltWidgetExtension */, BFF7C903257844C900E55F36 /* AltXPC */, + D5FB7A122AA284BE00EF863D /* AltJIT */, D586D39728EF58B0000E101F /* AltTests */, ); }; @@ -3004,6 +3165,31 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D5FB7A0F2AA284BE00EF863D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + D5A1D2EB2AA513410066CACC /* URL+Tools.swift in Sources */, + D5FB7A212AA284ED00EF863D /* EnableJIT.swift in Sources */, + D5FB7A312AA28A2900EF863D /* NSError+AltStore.swift in Sources */, + D59A6B7B2AA91B8E00F61259 /* PythonCommand.swift in Sources */, + D5A1D2EC2AA51D490066CACC /* ProcessError.swift in Sources */, + D5FB7A262AA284ED00EF863D /* MountDisk.swift in Sources */, + D5FB7A392AA28D8300EF863D /* NSError+ALTServerError.m in Sources */, + D5FB7A472AA293D000EF863D /* ALTErrorKeys.m in Sources */, + D59A6B7F2AA9226C00F61259 /* AppProcess.swift in Sources */, + D5FB7A272AA284ED00EF863D /* Task+Timeout.swift in Sources */, + D59A6B822AA92D1C00F61259 /* Process+Conveniences.swift in Sources */, + D5FB7A2A2AA2854100EF863D /* ALTLocalizedError.swift in Sources */, + D5A1D2E92AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift in Sources */, + D5FB7A2B2AA2854400EF863D /* UserInfoValue.swift in Sources */, + D5FB7A242AA284ED00EF863D /* Logger+AltJIT.swift in Sources */, + D5A1D2E42AA50EB60066CACC /* JITError.swift in Sources */, + D5FB7A252AA284ED00EF863D /* AltJIT.swift in Sources */, + D5FB7A322AA28A4000EF863D /* ALTWrappedError.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -3869,6 +4055,80 @@ }; name = Release; }; + D5FB7A182AA284BE00EF863D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CREATE_INFOPLIST_SECTION_IN_BINARY = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 6XVY5G3U44; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + "ALTJIT=1", + ); + GENERATE_INFOPLIST_FILE = YES; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)/usr/lib/system", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltJIT; + PRODUCT_NAME = altjit; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = NO; + SWIFT_OBJC_BRIDGING_HEADER = "AltJIT/AltJIT-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + D5FB7A192AA284BE00EF863D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CREATE_INFOPLIST_SECTION_IN_BINARY = YES; + DEVELOPMENT_TEAM = 6XVY5G3U44; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "ALTJIT=1", + ); + GENERATE_INFOPLIST_FILE = YES; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(SDKROOT)/usr/lib/system", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 13.0; + OTHER_SWIFT_FLAGS = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltJIT; + PRODUCT_NAME = altjit; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SKIP_INSTALL = NO; + SWIFT_OBJC_BRIDGING_HEADER = "AltJIT/AltJIT-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -3971,6 +4231,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D5FB7A172AA284BE00EF863D /* Build configuration list for PBXNativeTarget "AltJIT" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D5FB7A182AA284BE00EF863D /* Debug */, + D5FB7A192AA284BE00EF863D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -3982,6 +4251,14 @@ minimumVersion = 4.1.0; }; }; + D5FB7A2C2AA2859400EF863D /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-argument-parser.git"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 1.2.3; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3997,6 +4274,10 @@ isa = XCSwiftPackageProductDependency; productName = "AltSign-Static"; }; + D52DD35D2AAA89A600A7F2B6 /* AltSign-Dynamic */ = { + isa = XCSwiftPackageProductDependency; + productName = "AltSign-Dynamic"; + }; D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */ = { isa = XCSwiftPackageProductDependency; productName = "AltSign-Dynamic"; @@ -4006,6 +4287,11 @@ package = D58D5F2C26DFE68E00E55E38 /* XCRemoteSwiftPackageReference "LaunchAtLogin" */; productName = LaunchAtLogin; }; + D5FB7A2D2AA2859400EF863D /* ArgumentParser */ = { + isa = XCSwiftPackageProductDependency; + package = D5FB7A2C2AA2859400EF863D /* XCRemoteSwiftPackageReference "swift-argument-parser" */; + productName = ArgumentParser; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/AltStore.xcodeproj/xcshareddata/xcschemes/AltJIT.xcscheme b/AltStore.xcodeproj/xcshareddata/xcschemes/AltJIT.xcscheme new file mode 100644 index 00000000..440b8636 --- /dev/null +++ b/AltStore.xcodeproj/xcshareddata/xcschemes/AltJIT.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AltStore.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AltStore.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2c4a1843..9ecc50ea 100644 --- a/AltStore.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AltStore.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,6 +9,15 @@ "revision": "e8171b3e38a2816f579f58f3dac1522aa39efe41", "version": "4.2.0" } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser.git", + "state": { + "branch": null, + "revision": "8f4d2753f0e4778c76d5f05ad16c74f707390531", + "version": "1.2.3" + } } ] }, diff --git a/Shared/Categories/NSError+ALTServerError.m b/Shared/Categories/NSError+ALTServerError.m index d107eaab..3394b6ad 100644 --- a/Shared/Categories/NSError+ALTServerError.m +++ b/Shared/Categories/NSError+ALTServerError.m @@ -8,13 +8,16 @@ #import "NSError+ALTServerError.h" -#if TARGET_OS_OSX -#import "AltServer-Swift.h" -#else -#import -#endif - +#if ALTJIT +#import "AltJIT-Swift.h" @import AltSign; +#elif TARGET_OS_OSX +#import "AltServer-Swift.h" +@import AltSign; +#elif !TARGET_OS_OSX +#import +@import AltSign; +#endif NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError"; NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError"; diff --git a/Shared/Errors/ALTLocalizedError.swift b/Shared/Errors/ALTLocalizedError.swift index 57bfe60b..09a154dd 100644 --- a/Shared/Errors/ALTLocalizedError.swift +++ b/Shared/Errors/ALTLocalizedError.swift @@ -7,7 +7,10 @@ // import Foundation + +#if !ALTJIT import AltSign +#endif public let ALTLocalizedTitleErrorKey = "ALTLocalizedTitle" public let ALTLocalizedDescriptionKey = "ALTLocalizedDescription" diff --git a/Shared/Errors/JITError.swift b/Shared/Errors/JITError.swift new file mode 100644 index 00000000..2b2a0e0f --- /dev/null +++ b/Shared/Errors/JITError.swift @@ -0,0 +1,52 @@ +// +// JITError.swift +// AltJIT +// +// Created by Riley Testut on 9/3/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +extension JITError +{ + enum Code: Int, ALTErrorCode + { + typealias Error = JITError + + case processNotRunning + } + + static func processNotRunning(_ process: AppProcess, file: StaticString = #file, line: Int = #line) -> JITError { + JITError(code: .processNotRunning, process: process, sourceFile: file, sourceLine: UInt(line)) + } +} + +struct JITError: ALTLocalizedError +{ + let code: Code + + var errorTitle: String? + var errorFailure: String? + + @UserInfoValue var process: AppProcess? + + var sourceFile: StaticString? + var sourceLine: UInt? + + var errorFailureReason: String { + switch self.code + { + case .processNotRunning: + let targetName = self.process?.description ?? NSLocalizedString("The target app", comment: "") + return String(format: NSLocalizedString("%@ is not running.", comment: ""), targetName) + } + } + + var recoverySuggestion: String? { + switch self.code + { + case .processNotRunning: return NSLocalizedString("Make sure the app is running in the foreground on your device then try again.", comment: "") + } + } +} diff --git a/Shared/Errors/ProcessError.swift b/Shared/Errors/ProcessError.swift new file mode 100644 index 00000000..f5e05f4c --- /dev/null +++ b/Shared/Errors/ProcessError.swift @@ -0,0 +1,88 @@ +// +// ProcessError.swift +// AltPackage +// +// Created by Riley Testut on 9/1/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +extension ProcessError +{ + enum Code: Int, ALTErrorCode + { + typealias Error = ProcessError + + case failed + case timedOut + case unexpectedOutput + case terminated + } + + static func failed(executableURL: URL, exitCode: Int32, output: String?, file: StaticString = #file, line: Int = #line) -> ProcessError { + ProcessError(code: .failed, executableURL: executableURL, exitCode: exitCode, output: output, sourceFile: file, sourceLine: UInt(line)) + } + + static func timedOut(executableURL: URL, exitCode: Int32? = nil, output: String? = nil, file: StaticString = #file, line: Int = #line) -> ProcessError { + ProcessError(code: .timedOut, executableURL: executableURL, exitCode: exitCode, output: output, sourceFile: file, sourceLine: UInt(line)) + } + + static func unexpectedOutput(executableURL: URL, output: String, exitCode: Int32? = nil, file: StaticString = #file, line: Int = #line) -> ProcessError { + ProcessError(code: .unexpectedOutput, executableURL: executableURL, exitCode: exitCode, output: output, sourceFile: file, sourceLine: UInt(line)) + } + + static func terminated(executableURL: URL, exitCode: Int32, output: String, file: StaticString = #file, line: Int = #line) -> ProcessError { + ProcessError(code: .terminated, executableURL: executableURL, exitCode: exitCode, output: output, sourceFile: file, sourceLine: UInt(line)) + } +} + +struct ProcessError: ALTLocalizedError +{ + let code: Code + + var errorTitle: String? + var errorFailure: String? + + @UserInfoValue var executableURL: URL? + @UserInfoValue var exitCode: Int32? + @UserInfoValue var output: String? + + var sourceFile: StaticString? + var sourceLine: UInt? + + var errorFailureReason: String { + switch self.code + { + case .failed: + guard let exitCode else { return String(format: NSLocalizedString("%@ failed.", comment: ""), self.processName) } + + let baseMessage = String(format: NSLocalizedString("%@ failed with code %@.", comment: ""), self.processName, NSNumber(value: exitCode)) + guard let lastLine = self.lastOutputLine else { return baseMessage } + + let failureReason = baseMessage + " " + lastLine + return failureReason + + case .timedOut: return String(format: NSLocalizedString("%@ timed out.", comment: ""), self.processName) + case .terminated: return String(format: NSLocalizedString("%@ unexpectedly quit.", comment: ""), self.processName) + case .unexpectedOutput: + let baseMessage = String(format: NSLocalizedString("%@ returned unexpected output.", comment: ""), self.processName) + guard let lastLine = self.lastOutputLine else { return baseMessage } + + let failureReason = baseMessage + " " + lastLine + return failureReason + } + } + + private var processName: String { + guard let executableName = self.executableURL?.lastPathComponent else { return NSLocalizedString("The process", comment: "") } + return String(format: NSLocalizedString("The process '%@'", comment: ""), executableName) + } + + private var lastOutputLine: String? { + guard let output else { return nil } + + let lastLine = output.components(separatedBy: .newlines).last(where: { !$0.isEmpty }) + return lastLine + } +} diff --git a/Shared/Extensions/Process+Conveniences.swift b/Shared/Extensions/Process+Conveniences.swift new file mode 100644 index 00000000..c116340f --- /dev/null +++ b/Shared/Extensions/Process+Conveniences.swift @@ -0,0 +1,151 @@ +// +// Process+Conveniences.swift +// AltStore +// +// Created by Riley Testut on 9/6/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation +import OSLog +import Combine + +@available(macOS 12, *) +extension Process +{ + // Based loosely off of https://developer.apple.com/forums/thread/690310 + class func launch(_ toolURL: URL, arguments: [String] = [], environment: [String: String] = ProcessInfo.processInfo.environment) throws -> Process + { + let inputPipe = Pipe() + let outputPipe = Pipe() + + let process = Process() + process.executableURL = toolURL + process.arguments = arguments + process.environment = environment + process.standardInput = inputPipe + process.standardOutput = outputPipe + process.standardError = outputPipe + + func posixErr(_ error: Int32) -> Error { NSError(domain: NSPOSIXErrorDomain, code: Int(error), userInfo: nil) } + + // If you write to a pipe whose remote end has closed, the OS raises a + // `SIGPIPE` signal whose default disposition is to terminate your + // process. Helpful! `F_SETNOSIGPIPE` disables that feature, causing + // the write to fail with `EPIPE` instead. + + let fcntlResult = fcntl(inputPipe.fileHandleForWriting.fileDescriptor, F_SETNOSIGPIPE, 1) + guard fcntlResult >= 0 else { throw posixErr(errno) } + + // Actually run the process. + try process.run() + + let outputTask = Task { + do + { + let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: toolURL.lastPathComponent) + + // Automatically cancels when fileHandle closes. + for try await line in outputPipe.fileHandleForReading.bytes.lines + { + process.output += line + "\n" + process.outputPublisher.send(line) + + logger.notice("\(line, privacy: .public)") + } + + try Task.checkCancellation() + process.outputPublisher.send(completion: .finished) + } + catch let error as CancellationError + { + process.outputPublisher.send(completion: .failure(error)) + } + catch + { + Logger.main.error("Failed to read process output. \(error.localizedDescription, privacy: .public)") + + try Task.checkCancellation() + process.outputPublisher.send(completion: .failure(error)) + } + } + + process.terminationHandler = { process in + Logger.main.notice("Process \(toolURL, privacy: .public) terminated with exit code \(process.terminationStatus).") + + outputTask.cancel() + process.outputPublisher.send(completion: .finished) + } + + return process + } + + class func launchAndWait(_ toolURL: URL, arguments: [String] = [], environment: [String: String] = ProcessInfo.processInfo.environment) async throws -> String + { + let process = try self.launch(toolURL, arguments: arguments, environment: environment) + + await withCheckedContinuation { (continuation: CheckedContinuation) in + let previousHandler = process.terminationHandler + process.terminationHandler = { process in + previousHandler?(process) + continuation.resume() + } + } + + guard process.terminationStatus == 0 else { + throw ProcessError.failed(executableURL: toolURL, exitCode: process.terminationStatus, output: process.output) + } + + return process.output + } +} + +@available(macOS 12, *) +extension Process +{ + private static var outputKey: Int = 0 + private static var publisherKey: Int = 0 + + fileprivate(set) var output: String { + get { + let output = objc_getAssociatedObject(self, &Process.outputKey) as? String ?? "" + return output + } + set { + objc_setAssociatedObject(self, &Process.outputKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC) + } + } + + // Should be type-erased, but oh well. + var outputLines: AsyncThrowingPublisher> { + return self.outputPublisher + .buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest) + .values + } + + private var outputPublisher: PassthroughSubject { + if let publisher = objc_getAssociatedObject(self, &Process.publisherKey) as? PassthroughSubject + { + return publisher + } + + let publisher = PassthroughSubject() + objc_setAssociatedObject(self, &Process.publisherKey, publisher, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return publisher + } + + // We must manually close outputPipe in order for us to read a second Process' standardOutput via async-await 🤷‍♂️ + func stopOutput() + { + guard let outputPipe = self.standardOutput as? Pipe else { return } + + do + { + try outputPipe.fileHandleForReading.close() + } + catch + { + Logger.main.error("Failed to close \(self.executableURL?.lastPathComponent ?? "process", privacy: .public)'s standardOutput. \(error.localizedDescription, privacy: .public)") + } + } +} diff --git a/Shared/Types/AppProcess.swift b/Shared/Types/AppProcess.swift new file mode 100644 index 00000000..13daeffb --- /dev/null +++ b/Shared/Types/AppProcess.swift @@ -0,0 +1,35 @@ +// +// AppProcess.swift +// AltStore +// +// Created by Riley Testut on 9/6/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import Foundation + +enum AppProcess: CustomStringConvertible +{ + case name(String) + case pid(Int) + + var description: String { + switch self + { + case .name(let name): return name + case .pid(let pid): return "Process \(pid)" + } + } + + init(_ value: String) + { + if let pid = Int(value) + { + self = .pid(pid) + } + else + { + self = .name(value) + } + } +}