mirror of
https://github.com/trycua/computer.git
synced 2026-01-04 20:40:15 -06:00
Merge pull request #37 from aktech/recovery-mode-option
Add recovery mode option
This commit is contained in:
13
README.md
13
README.md
@@ -65,12 +65,13 @@ Command Options:
|
||||
--ipsw <path> Path to IPSW file or 'latest' for macOS VMs
|
||||
|
||||
run:
|
||||
--no-display Do not start the VNC client app
|
||||
--shared-dir <dir> Share directory with VM (format: path[:ro|rw])
|
||||
--mount <path> For Linux VMs only, attach a read-only disk image
|
||||
--registry <url> Container registry URL (default: ghcr.io)
|
||||
--organization <org> Organization to pull from (default: trycua)
|
||||
--vnc-port <port> Port to use for the VNC server (default: 0 for auto-assign)
|
||||
--no-display Do not start the VNC client app
|
||||
--shared-dir <dir> Share directory with VM (format: path[:ro|rw])
|
||||
--mount <path> For Linux VMs only, attach a read-only disk image
|
||||
--registry <url> Container registry URL (default: ghcr.io)
|
||||
--organization <org> Organization to pull from (default: trycua)
|
||||
--vnc-port <port> Port to use for the VNC server (default: 0 for auto-assign)
|
||||
--recovery-mode <boolean> For MacOS VMs only, start VM in recovery mode (default: false)
|
||||
|
||||
set:
|
||||
--cpu <cores> New number of CPU cores (e.g., 4)
|
||||
|
||||
@@ -43,7 +43,8 @@ curl --connect-timeout 6000 \
|
||||
"hostPath": "~/Projects",
|
||||
"readOnly": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"recoveryMode": false
|
||||
}' \
|
||||
http://localhost:3000/lume/vms/lume_vm/run
|
||||
```
|
||||
|
||||
@@ -27,7 +27,10 @@ struct Run: AsyncParsableCommand {
|
||||
|
||||
@Option(name: [.customLong("vnc-port")], help: "Port to use for the VNC server. Defaults to 0 (auto-assign)")
|
||||
var vncPort: Int = 0
|
||||
|
||||
|
||||
@Option(help: "For MacOS VMs only, boot into the VM in recovery mode")
|
||||
var recoveryMode: Bool = false
|
||||
|
||||
private var parsedSharedDirectories: [SharedDirectory] {
|
||||
get throws {
|
||||
try sharedDirectories.map { dirString -> SharedDirectory in
|
||||
@@ -79,7 +82,8 @@ struct Run: AsyncParsableCommand {
|
||||
mount: mount,
|
||||
registry: registry,
|
||||
organization: organization,
|
||||
vncPort: vncPort
|
||||
vncPort: vncPort,
|
||||
recoveryMode: recoveryMode
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,8 @@ final class LumeController {
|
||||
mount: Path? = nil,
|
||||
registry: String = "ghcr.io",
|
||||
organization: String = "trycua",
|
||||
vncPort: Int = 0
|
||||
vncPort: Int = 0,
|
||||
recoveryMode: Bool = false
|
||||
) async throws {
|
||||
let normalizedName = normalizeVMName(name: name)
|
||||
Logger.info(
|
||||
@@ -267,6 +268,7 @@ final class LumeController {
|
||||
"shared_directories": "\(sharedDirectories.map( { $0.string } ).joined(separator: ", "))",
|
||||
"mount": mount?.path ?? "none",
|
||||
"vnc_port": "\(vncPort)",
|
||||
"recovery_mode": "\(recoveryMode)",
|
||||
])
|
||||
|
||||
do {
|
||||
@@ -286,7 +288,7 @@ final class LumeController {
|
||||
|
||||
let vm = try get(name: normalizedName)
|
||||
SharedVM.shared.setVM(name: normalizedName, vm: vm)
|
||||
try await vm.run(noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount, vncPort: vncPort)
|
||||
try await vm.run(noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount, vncPort: vncPort, recoveryMode: recoveryMode)
|
||||
Logger.info("VM started successfully", metadata: ["name": normalizedName])
|
||||
} catch {
|
||||
SharedVM.shared.removeVM(name: normalizedName)
|
||||
|
||||
@@ -161,7 +161,7 @@ extension Server {
|
||||
}
|
||||
|
||||
func handleRunVM(name: String, body: Data?) async throws -> HTTPResponse {
|
||||
let request = body.flatMap { try? JSONDecoder().decode(RunVMRequest.self, from: $0) } ?? RunVMRequest(noDisplay: nil, sharedDirectories: nil)
|
||||
let request = body.flatMap { try? JSONDecoder().decode(RunVMRequest.self, from: $0) } ?? RunVMRequest(noDisplay: nil, sharedDirectories: nil, recoveryMode: nil)
|
||||
|
||||
do {
|
||||
let dirs = try request.parse()
|
||||
@@ -170,7 +170,8 @@ extension Server {
|
||||
startVM(
|
||||
name: name,
|
||||
noDisplay: request.noDisplay ?? false,
|
||||
sharedDirectories: dirs
|
||||
sharedDirectories: dirs,
|
||||
recoveryMode: request.recoveryMode ?? false
|
||||
)
|
||||
|
||||
// Return response immediately
|
||||
@@ -299,7 +300,8 @@ extension Server {
|
||||
nonisolated private func startVM(
|
||||
name: String,
|
||||
noDisplay: Bool,
|
||||
sharedDirectories: [SharedDirectory] = []
|
||||
sharedDirectories: [SharedDirectory] = [],
|
||||
recoveryMode: Bool = false
|
||||
) {
|
||||
Task.detached { @MainActor @Sendable in
|
||||
Logger.info("Starting VM in background", metadata: ["name": name])
|
||||
@@ -308,7 +310,8 @@ extension Server {
|
||||
try await vmController.runVM(
|
||||
name: name,
|
||||
noDisplay: noDisplay,
|
||||
sharedDirectories: sharedDirectories
|
||||
sharedDirectories: sharedDirectories,
|
||||
recoveryMode: recoveryMode
|
||||
)
|
||||
Logger.info("VM started successfully in background", metadata: ["name": name])
|
||||
} catch {
|
||||
|
||||
@@ -5,6 +5,7 @@ import Virtualization
|
||||
struct RunVMRequest: Codable {
|
||||
let noDisplay: Bool?
|
||||
let sharedDirectories: [SharedDirectoryRequest]?
|
||||
let recoveryMode: Bool?
|
||||
|
||||
struct SharedDirectoryRequest: Codable {
|
||||
let hostPath: String
|
||||
|
||||
@@ -88,7 +88,7 @@ class VM {
|
||||
|
||||
// MARK: - VM Lifecycle Management
|
||||
|
||||
func run(noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0) async throws {
|
||||
func run(noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0, recoveryMode: Bool = false) async throws {
|
||||
guard vmDirContext.initialized else {
|
||||
throw VMError.notInitialized(vmDirContext.name)
|
||||
}
|
||||
@@ -112,7 +112,8 @@ class VM {
|
||||
"sharedDirectories": sharedDirectories.map(
|
||||
{ $0.string }
|
||||
).joined(separator: ", "),
|
||||
"vncPort": "\(vncPort)"
|
||||
"vncPort": "\(vncPort)",
|
||||
"recoveryMode": "\(recoveryMode)"
|
||||
])
|
||||
|
||||
// Create and configure the VM
|
||||
@@ -122,7 +123,8 @@ class VM {
|
||||
memorySize: memorySize,
|
||||
display: vmDirContext.config.display.string,
|
||||
sharedDirectories: sharedDirectories,
|
||||
mount: mount
|
||||
mount: mount,
|
||||
recoveryMode: recoveryMode
|
||||
)
|
||||
virtualizationService = try virtualizationServiceFactory(config)
|
||||
|
||||
@@ -368,7 +370,8 @@ class VM {
|
||||
memorySize: UInt64,
|
||||
display: String,
|
||||
sharedDirectories: [SharedDirectory] = [],
|
||||
mount: Path? = nil
|
||||
mount: Path? = nil,
|
||||
recoveryMode: Bool = false
|
||||
) throws -> VMVirtualizationServiceContext {
|
||||
return VMVirtualizationServiceContext(
|
||||
cpuCount: cpuCount,
|
||||
@@ -380,7 +383,8 @@ class VM {
|
||||
machineIdentifier: vmDirContext.config.machineIdentifier,
|
||||
macAddress: vmDirContext.config.macAddress!,
|
||||
diskPath: vmDirContext.diskPath,
|
||||
nvramPath: vmDirContext.nvramPath
|
||||
nvramPath: vmDirContext.nvramPath,
|
||||
recoveryMode: recoveryMode
|
||||
)
|
||||
}
|
||||
|
||||
@@ -400,4 +404,4 @@ class VM {
|
||||
func finalize(to name: String, home: Home) throws {
|
||||
try vmDirContext.finalize(to: name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ struct VMVirtualizationServiceContext {
|
||||
let macAddress: String
|
||||
let diskPath: Path
|
||||
let nvramPath: Path
|
||||
let recoveryMode: Bool
|
||||
}
|
||||
|
||||
/// Protocol defining the interface for virtualization operations
|
||||
@@ -30,30 +31,48 @@ protocol VMVirtualizationService {
|
||||
@MainActor
|
||||
class BaseVirtualizationService: VMVirtualizationService {
|
||||
let virtualMachine: VZVirtualMachine
|
||||
let recoveryMode: Bool // Store whether we should start in recovery mode
|
||||
|
||||
var state: VZVirtualMachine.State {
|
||||
virtualMachine.state
|
||||
}
|
||||
|
||||
init(virtualMachine: VZVirtualMachine) {
|
||||
init(virtualMachine: VZVirtualMachine, recoveryMode: Bool = false) {
|
||||
self.virtualMachine = virtualMachine
|
||||
self.recoveryMode = recoveryMode
|
||||
}
|
||||
|
||||
func start() async throws {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
Task { @MainActor in
|
||||
virtualMachine.start { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
if #available(macOS 13, *) {
|
||||
let startOptions = VZMacOSVirtualMachineStartOptions()
|
||||
startOptions.startUpFromMacOSRecovery = recoveryMode
|
||||
if recoveryMode {
|
||||
Logger.info("Starting VM in recovery mode")
|
||||
}
|
||||
virtualMachine.start(options: startOptions) { error in
|
||||
if let error = error {
|
||||
continuation.resume(throwing: error)
|
||||
} else {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Logger.info("Starting VM in normal mode")
|
||||
virtualMachine.start { result in
|
||||
switch result {
|
||||
case .success:
|
||||
continuation.resume()
|
||||
case .failure(let error):
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func stop() async throws {
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
virtualMachine.stop { error in
|
||||
@@ -210,7 +229,7 @@ final class DarwinVirtualizationService: BaseVirtualizationService {
|
||||
|
||||
init(configuration: VMVirtualizationServiceContext) throws {
|
||||
let vzConfig = try Self.createConfiguration(configuration)
|
||||
super.init(virtualMachine: VZVirtualMachine(configuration: vzConfig))
|
||||
super.init(virtualMachine: VZVirtualMachine(configuration: vzConfig), recoveryMode: configuration.recoveryMode)
|
||||
}
|
||||
|
||||
func installMacOS(imagePath: Path, progressHandler: (@Sendable (Double) -> Void)?) async throws {
|
||||
@@ -326,4 +345,4 @@ final class LinuxVirtualizationService: BaseVirtualizationService {
|
||||
let vzConfig = try Self.createConfiguration(configuration)
|
||||
super.init(virtualMachine: VZVirtualMachine(configuration: vzConfig))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,13 @@ class MockVM: VM {
|
||||
try vmDirContext.saveConfig()
|
||||
}
|
||||
|
||||
override func run(noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0) async throws {
|
||||
override func run(noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0, recoveryMode: Bool = false) async throws {
|
||||
mockIsRunning = true
|
||||
try await super.run(noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount, vncPort: vncPort)
|
||||
try await super.run(noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount, vncPort: vncPort, recoveryMode: recoveryMode)
|
||||
}
|
||||
|
||||
override func stop() async throws {
|
||||
mockIsRunning = false
|
||||
try await super.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func testVMRunAndStop() async throws {
|
||||
|
||||
// Test running VM
|
||||
let runTask = Task {
|
||||
try await vm.run(noDisplay: false, sharedDirectories: [], mount: nil, vncPort: 0)
|
||||
try await vm.run(noDisplay: false, sharedDirectories: [], mount: nil, vncPort: 0, recoveryMode: false)
|
||||
}
|
||||
|
||||
// Give the VM time to start
|
||||
|
||||
Reference in New Issue
Block a user