mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
[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:
151
Shared/Extensions/Process+Conveniences.swift
Normal file
151
Shared/Extensions/Process+Conveniences.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user