diff --git a/Package.resolved b/Package.resolved index 182f49cc..0685be6a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,14 @@ { "pins" : [ + { + "identity" : "dynamic", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mhdhejazi/Dynamic", + "state" : { + "branch" : "master", + "revision" : "772883073d044bc754d401cabb6574624eb3778f" + } + }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 33c531a0..84bb053a 100644 --- a/Package.swift +++ b/Package.swift @@ -13,10 +13,12 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.2"), .package(url: "https://github.com/pointfreeco/swift-parsing", from: "0.9.2"), + .package(url: "https://github.com/mhdhejazi/Dynamic", branch: "master"), ], targets: [ .executableTarget(name: "tart", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Dynamic", package: "Dynamic"), .product(name: "Parsing", package: "swift-parsing"), ]), .testTarget(name: "TartTests", dependencies: ["tart"]) diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index c440b231..ddd7ba35 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -13,6 +13,8 @@ struct Run: AsyncParsableCommand { @Flag var noGraphics: Bool = false + @Flag var recovery: Bool = false + @MainActor func run() async throws { let vmDir = try VMStorageLocal().open(name) @@ -21,7 +23,7 @@ struct Run: AsyncParsableCommand { await withThrowingTaskGroup(of: Void.self) { group in group.addTask { do { - try await vm!.run() + try await vm!.run(recovery) Foundation.exit(0) } catch { diff --git a/Sources/tart/VM+Recovery.swift b/Sources/tart/VM+Recovery.swift new file mode 100644 index 00000000..999676a4 --- /dev/null +++ b/Sources/tart/VM+Recovery.swift @@ -0,0 +1,37 @@ +import Foundation +import Virtualization +import Dynamic + +// Kudos to @saagarjha's VirtualApple for finding about _VZVirtualMachineStartOptions + +extension VZVirtualMachine { + func start(_ recovery: Bool) async throws { + if !recovery { + // just use the regular API + return try await withCheckedThrowingContinuation { continuation in + DispatchQueue.main.async { + self.start(completionHandler: { result in + continuation.resume(with: result) + }) + } + } + } + + // use some private stuff only for recovery + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DispatchQueue.main.async { + let handler: @convention(block) (_ result: Any?) -> Void = { result in + if let error = result as? Error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } + } + // dynamic magic + let options = Dynamic._VZVirtualMachineStartOptions() + options.bootMacOSRecovery = recovery + Dynamic(self)._start(withOptions: options, completionHandler: handler) + } + } + } +} diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index 1a63a123..b4630ba6 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -133,14 +133,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { } } - func run() async throws { - try await withCheckedThrowingContinuation { continuation in - DispatchQueue.main.async { - self.virtualMachine.start(completionHandler: { result in - continuation.resume(with: result) - }) - } - } + func run(_ recovery: Bool) async throws { + try await virtualMachine.start(recovery) await withTaskCancellationHandler(operation: { sema.wait()