diff --git a/Sources/tart/Commands/Run.swift b/Sources/tart/Commands/Run.swift index 79ae5766..306f605d 100644 --- a/Sources/tart/Commands/Run.swift +++ b/Sources/tart/Commands/Run.swift @@ -12,30 +12,35 @@ var vm: VM? struct IPNotFound: Error { } -extension VZDiskImageSynchronizationMode : LosslessStringConvertible { - public init?(_ description: String) { +@available(macOS 14, *) +extension VZDiskSynchronizationMode { + public init(_ description: String) throws { switch description { case "none": self = .none - case "fsync": - self = .fsync case "full": self = .full + case "": + self = .full default: - return nil + throw RuntimeError.VMConfigurationError("unsupported disk synchronization mode: \"\(description)\"") } } +} - public var description: String { - switch self { - case .none: - return "none" - case .fsync: - return "fsync" - case .full: - return "full" - @unknown default: - return "unknown" +extension VZDiskImageSynchronizationMode { + public init(_ description: String) throws { + switch description { + case "none": + self = .none + case "fsync": + self = .fsync + case "full": + self = .full + case "": + self = .full + default: + throw RuntimeError.VMConfigurationError("unsupported disk image synchronization mode: \"\(description)\"") } } } @@ -168,13 +173,19 @@ struct Run: AsyncParsableCommand { @Flag(help: ArgumentHelp("Restrict network access to the host-only network")) var netHost: Bool = false - @Option(help: ArgumentHelp("Set the root disk synchronization mode (Linux images only). Possible values: none, fsync, full.", + @Option(help: ArgumentHelp("Set the root disk options (e.g. --root-disk-opts=\"ro\" or --root-disk-opts=\"sync=none\")", discussion: """ - 'full' synchronizes data with the drive and ensures that it is written to permanent storage. - 'fsync' synchronizes data with the drive but doesn't guarantee that it is written to permanent storage. - 'none' doesn't synchronize data with the drive. - """)) - var sync: String = "full" + Options are comma-separated and are as follows: + + * ro — attach the root disk in read-only mode instead of the default read-write (e.g. --root-disk-opts="ro") + + * sync=none — disable data synchronization with the permanent storage to increase performance at the cost of a higher chance of data loss (e.g. --root-disk-opts="sync=none") + + * sync=fsync — enable data synchronization with the permanent storage, but don't ensure that it was actually written (e.g. --root-disk-opts="sync=fsync") + + * sync=full — enable data synchronization with the permanent storage and ensure that it was actually written (e.g. --root-disk-opts="sync=full") + """, valueName: "options")) + var rootDiskOpts: String = "" #if arch(arm64) @Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\".")) @@ -231,10 +242,6 @@ struct Run: AsyncParsableCommand { throw ValidationError("Seems you have a disk targeting x86 architecture (hence amd64 in the name). Please use an 'arm64' version of the disk.") } } - - if(VZDiskImageSynchronizationMode(sync) == nil) { - throw ValidationError("Invalid disk synchronization mode: \(sync)") - } } @MainActor @@ -279,6 +286,9 @@ struct Run: AsyncParsableCommand { serialPorts.append(createSerialPortConfiguration(tty_read!, tty_write!)) } + // Parse root disk options + let diskOptions = DiskOptions(rootDiskOpts) + vm = try VM( vmDir: vmDir, network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(), @@ -288,7 +298,7 @@ struct Run: AsyncParsableCommand { suspendable: suspendable, audio: !noAudio, clipboard: !noClipboard, - sync: VZDiskImageSynchronizationMode(sync) ?? .full + sync: VZDiskImageSynchronizationMode(diskOptions.syncModeRaw) ) let vncImpl: VNC? = try { @@ -741,13 +751,11 @@ struct AdditionalDisk { throw UnsupportedOSError("attaching Network Block Devices", "are") } - let syncMode = try parseSyncMode(syncModeRaw) - let nbdAttachment = try VZNetworkBlockDeviceStorageDeviceAttachment( url: diskURL!, timeout: 30, isForcedReadOnly: diskReadOnly, - synchronizationMode: syncMode + synchronizationMode: try VZDiskSynchronizationMode(syncModeRaw) ) return VZVirtioBlockDeviceConfiguration(attachment: nbdAttachment) @@ -775,7 +783,7 @@ struct AdditionalDisk { } let blockAttachment = try VZDiskBlockDeviceStorageDeviceAttachment(fileHandle: FileHandle(fileDescriptor: fd, closeOnDealloc: true), - readOnly: diskReadOnly, synchronizationMode: try parseSyncMode(syncModeRaw)) + readOnly: diskReadOnly, synchronizationMode: try VZDiskSynchronizationMode(syncModeRaw)) return VZVirtioBlockDeviceConfiguration(attachment: blockAttachment) } @@ -809,7 +817,7 @@ struct AdditionalDisk { url: diskFileURL, readOnly: diskReadOnly, cachingMode: .automatic, - synchronizationMode: try Self.parseImageSyncMode(syncModeRaw) + synchronizationMode: try VZDiskImageSynchronizationMode(syncModeRaw) ) return VZVirtioBlockDeviceConfiguration(attachment: diskImageAttachment) @@ -817,58 +825,36 @@ struct AdditionalDisk { static func parseOptions(_ parseFrom: String) -> (String, Bool, String) { var arguments = parseFrom.split(separator: ":") - let options = arguments.last!.split(separator: ",") - var readOnly: Bool = false - var syncModeRaw: String = "" + let options = DiskOptions(String(arguments.last!)) + if options.foundAtLeastOneOption { + arguments.removeLast() + } - var foundAtLeastOneOption: Bool = false + return (arguments.joined(separator: ":"), options.readOnly, options.syncModeRaw) + } +} + +struct DiskOptions { + var readOnly: Bool = false + var syncModeRaw: String = "" + var foundAtLeastOneOption: Bool = false + + init(_ parseFrom: String) { + let options = parseFrom.split(separator: ",") for option in options { switch true { case option == "ro": - readOnly = true - foundAtLeastOneOption = true + self.readOnly = true + self.foundAtLeastOneOption = true case option.hasPrefix("sync="): - syncModeRaw = String(option.dropFirst("sync=".count)) - foundAtLeastOneOption = true + self.syncModeRaw = String(option.dropFirst("sync=".count)) + self.foundAtLeastOneOption = true default: continue } } - - if foundAtLeastOneOption { - arguments.removeLast() - } - - return (arguments.joined(separator: ":"), readOnly, syncModeRaw) - } - - @available(macOS 14, *) - static func parseSyncMode(_ parseFrom: String) throws -> VZDiskSynchronizationMode { - switch parseFrom { - case "none": - return .none - case "full": - return .full - case "": - return .full - default: - throw RuntimeError.VMConfigurationError("unsupported disk synchronization mode: \"\(parseFrom)\"") - } - } - - static func parseImageSyncMode(_ parseFrom: String) throws -> VZDiskImageSynchronizationMode { - switch parseFrom { - case "none": - return .none - case "full": - return .full - case "": - return .full - default: - throw RuntimeError.VMConfigurationError("unsupported disk image synchronization mode: \"\(parseFrom)\"") - } } } diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index 2db5e71a..cf62fb16 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -354,7 +354,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject { let attachment: VZDiskImageStorageDeviceAttachment = vmConfig.os == .linux ? // Use "cached" caching mode for virtio drive to prevent fs corruption on linux try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false, cachingMode: .cached, synchronizationMode: sync) : - try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false) + try VZDiskImageStorageDeviceAttachment(url: diskURL, readOnly: false, cachingMode: .automatic, synchronizationMode: sync) var device: VZStorageDeviceConfiguration if #available(macOS 14, *), vmConfig.os == .linux {