[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
This commit is contained in:
Riley Testut
2023-09-07 18:00:53 -05:00
parent 1a42aaeae8
commit d846445448
20 changed files with 1503 additions and 7 deletions

View File

@@ -8,13 +8,16 @@
#import "NSError+ALTServerError.h"
#if TARGET_OS_OSX
#import "AltServer-Swift.h"
#else
#import <AltStoreCore/AltStoreCore-Swift.h>
#endif
#if ALTJIT
#import "AltJIT-Swift.h"
@import AltSign;
#elif TARGET_OS_OSX
#import "AltServer-Swift.h"
@import AltSign;
#elif !TARGET_OS_OSX
#import <AltStoreCore/AltStoreCore-Swift.h>
@import AltSign;
#endif
NSErrorDomain const AltServerErrorDomain = @"AltServer.ServerError";
NSErrorDomain const AltServerInstallationErrorDomain = @"Apple.InstallationError";

View File

@@ -7,7 +7,10 @@
//
import Foundation
#if !ALTJIT
import AltSign
#endif
public let ALTLocalizedTitleErrorKey = "ALTLocalizedTitle"
public let ALTLocalizedDescriptionKey = "ALTLocalizedDescription"

View File

@@ -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: "")
}
}
}

View File

@@ -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
}
}

View File

@@ -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<Void, Never>) 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<some Publisher<String, Error>> {
return self.outputPublisher
.buffer(size: 100, prefetch: .byRequest, whenFull: .dropOldest)
.values
}
private var outputPublisher: PassthroughSubject<String, Error> {
if let publisher = objc_getAssociatedObject(self, &Process.publisherKey) as? PassthroughSubject<String, Error>
{
return publisher
}
let publisher = PassthroughSubject<String, Error>()
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)")
}
}
}

View File

@@ -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)
}
}
}