diff --git a/Configuration/UTMQemuConfigurationQEMU.swift b/Configuration/UTMQemuConfigurationQEMU.swift index 14c6d2619..8c64e9069 100644 --- a/Configuration/UTMQemuConfigurationQEMU.swift +++ b/Configuration/UTMQemuConfigurationQEMU.swift @@ -66,6 +66,8 @@ struct UTMQemuConfigurationQEMU: Codable { /// Set to true to request guest tools install. Not saved. var isGuestToolsInstallRequested: Bool = false + var unattended: Bool = false + /// Set to true to request UEFI variable reset. Not saved. var isUefiVariableResetRequested: Bool = false diff --git a/Platform/Shared/VMContextMenuModifier.swift b/Platform/Shared/VMContextMenuModifier.swift index 066f5dfd1..3e6666e41 100644 --- a/Platform/Shared/VMContextMenuModifier.swift +++ b/Platform/Shared/VMContextMenuModifier.swift @@ -179,7 +179,8 @@ struct VMContextMenuModifier: ViewModifier { .onChange(of: (vm.config as? UTMQemuConfiguration)?.qemu.isGuestToolsInstallRequested) { newValue in if newValue == true { data.busyWorkAsync { - try await data.mountSupportTools(for: vm.wrapped!) + let unattend = await (vm.config as? UTMQemuConfiguration)?.qemu.unattended ?? false + try await data.mountSupportTools(for: vm.wrapped!, unattendless: unattend) } } } diff --git a/Platform/Shared/VMWizardOSWindowsView.swift b/Platform/Shared/VMWizardOSWindowsView.swift index 2136123e9..9dee322fa 100644 --- a/Platform/Shared/VMWizardOSWindowsView.swift +++ b/Platform/Shared/VMWizardOSWindowsView.swift @@ -60,6 +60,7 @@ struct VMWizardOSWindowsView: View { } label: { Label("Fetch latest Windows installer…", systemImage: "link") }.buttonStyle(.link) + Toggle("Unattended Installation", isOn: $wizardState.windowsUnattendedInstall) } #endif Link(destination: URL(string: "https://docs.getutm.app/guides/windows/")!) { diff --git a/Platform/Shared/VMWizardState.swift b/Platform/Shared/VMWizardState.swift index 48d9dbfc2..f8a34baeb 100644 --- a/Platform/Shared/VMWizardState.swift +++ b/Platform/Shared/VMWizardState.swift @@ -30,6 +30,7 @@ enum VMWizardPage: Int, Identifiable { case macOSBoot case linuxBoot case windowsBoot + case windowsUnattendConfig case otherBoot case hardware case drives @@ -136,6 +137,10 @@ enum VMBootDevice: Int, Identifiable { @Published var name: String? @Published var isOpenSettingsAfterCreation: Bool = false @Published var useNvmeAsDiskInterface = false + @Published var windowsUnattendedInstall = false + @Published var unattendLanguage = "en-US" + @Published var unattendUsername = "user" + @Published var unattendPassword = "" /// SwiftUI BUG: on macOS 12, when VoiceOver is enabled and isBusy changes the disable state of a button being clicked, var isNeverDisabledWorkaround: Bool { @@ -239,6 +244,12 @@ enum VMBootDevice: Int, Identifiable { alertMessage = AlertMessage(NSLocalizedString("Please select a boot image.", comment: "VMWizardState")) return } + if windowsUnattendedInstall { + nextPage = .windowsUnattendConfig + } else { + nextPage = .hardware + } + case .windowsUnattendConfig: nextPage = .hardware case .hardware: guard systemMemoryMib > 0 else { @@ -512,9 +523,19 @@ enum VMBootDevice: Int, Identifiable { diskImage.imageType = .disk diskImage.interface = mainDriveInterface config.drives.append(diskImage) - if operatingSystem == .Windows && isGuestToolsInstallRequested { - let toolsDiskDrive = UTMQemuConfigurationDrive(forArchitecture: systemArchitecture, target: systemTarget, isExternal: true) - config.drives.append(toolsDiskDrive) + if operatingSystem == .Windows { + if isGuestToolsInstallRequested { + let toolsDiskDrive = UTMQemuConfigurationDrive(forArchitecture: systemArchitecture, target: systemTarget, isExternal: true) + config.drives.append(toolsDiskDrive) + } + if windowsUnattendedInstall { + var unattendDrive = UTMQemuConfigurationDrive(forArchitecture: systemArchitecture, target: systemTarget, isExternal: false) + unattendDrive.isRawImage = true + unattendDrive.imageURL = try createAutounattendIso() + unattendDrive.interface = .usb + unattendDrive.imageType = .cd + config.drives.append(unattendDrive) + } } } if legacyHardware { @@ -524,6 +545,520 @@ enum VMBootDevice: Int, Identifiable { return config } + func createUnattendXml() -> String { + return """ + + + + + + false + + + false + + + * + + + * + + + + + true + + + true + + + 1 + + + 1 + + + + + true + + + true + + + * + + UTM + UTM Virtual Machine + + UTM + https://mac.getutm.app/support/ + + UTM + + + * + + UTM + UTM Virtual Machine + + UTM + https://mac.getutm.app/support/ + + UTM + + + 0 + + + 0 + + + + + + \(unattendLanguage) + + \(unattendLanguage) + \(unattendLanguage) + \(unattendLanguage) + \(unattendLanguage) + + + + \(unattendLanguage) + + e\(unattendLanguage) + \(unattendLanguage) + \(unattendLanguage) + \(unattendLanguage) + + + + false + + + + 1 + reg add HKLM\\System\\Setup\\LabConfig /v BypassCPUCheck /t REG_DWORD /d 0x00000001 /f + + + 2 + reg add HKLM\\System\\Setup\\LabConfig /v BypassRAMCheck /t REG_DWORD /d 0x00000001 /f + + + 3 + reg add HKLM\\System\\Setup\\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 0x00000001 /f + + + 4 + reg add HKLM\\System\\Setup\\LabConfig /v BypassTPMCheck /t REG_DWORD /d 0x00000001 /f + + + + + + W269N-WFGWX-YVC9B-4J6C9-T83GX + + true + + + + + + 1 + 100 + EFI + + + 2 + 16 + MSR + + + 3 + Primary + true + + + + + FAT32 + + 1 + 1 + + + NTFS + 2 + 3 + + + 0 + true + + OnError + + + + + 0 + 3 + + + + + + + false + + + + 1 + reg add HKLM\\System\\Setup\\LabConfig /v BypassCPUCheck /t REG_DWORD /d 0x00000001 /f + + + 2 + reg add HKLM\\System\\Setup\\LabConfig /v BypassRAMCheck /t REG_DWORD /d 0x00000001 /f + + + 3 + reg add HKLM\\System\\Setup\\LabConfig /v BypassSecureBootCheck /t REG_DWORD /d 0x00000001 /f + + + 4 + reg add HKLM\\System\\Setup\\LabConfig /v BypassTPMCheck /t REG_DWORD /d 0x00000001 /f + + + + + + W269N-WFGWX-YVC9B-4J6C9-T83GX + + true + + + + + + 1 + 100 + EFI + + + 2 + 16 + MSR + + + 3 + Primary + true + + + + + FAT32 + + 1 + 1 + + + NTFS + 2 + 3 + + + 0 + true + + OnError + + + + + 0 + 3 + + + + + + + + + E:\\Drivers\\qemufwcfg\\w10\\amd64 + + + E:\\Drivers\\vioscsi\\w10\\amd64 + + + E:\\Drivers\\viostor\\w10\\amd64 + + + E:\\Drivers\\vioserial\\w10\\amd64 + + + E:\\Drivers\\qxldod\\w10\\amd64 + + + E:\\Drivers\\viogpu\\w10\\amd64 + + + E:\\Drivers\\viorng\\w10\\amd64 + + + E:\\Drivers\\NetKVM\\w10\\amd64 + + + E:\\Drivers\\Balloon\\w10\\amd64 + + + + + + + E:\\Drivers\\vioscsi\\w10\\ARM64 + + + E:\\Drivers\\viostor\\w10\\ARM64 + + + E:\\Drivers\\vioserial\\w10\\ARM64 + + + E:\\Drivers\\viogpu\\w10\\ARM64 + + + E:\\Drivers\\NetKVM\\w10\\ARM64 + + + E:\\Drivers\\Balloon\\w10\\ARM64 + + + + + + + \(unattendLanguage) + \(unattendLanguage) + \(unattendLanguage) + \(unattendLanguage) + + + \(unattendLanguage) + \(unattendLanguage) + \(unattendLanguage) + \(unattendLanguage) + + + + + + \(unattendUsername) + \(unattendUsername) + Administrators + + \(unattendPassword) + true</PlainText> + </Password> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <AutoLogon> + <Enabled>true</Enabled> + <Username>\(unattendUsername)</Username> + <Password> + <PlainText>true</PlainText> + <Value>\(unattendPassword)</Value> + </Password> + <LogonCount>1</LogonCount> + </AutoLogon> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <ProtectYourPC>3</ProtectYourPC> + <VMModeOptimizations> + <SkipWinREInitialization>true</SkipWinREInitialization> + </VMModeOptimizations> + </OOBE> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <CommandLine>Cmd /c POWERCFG -H OFF</CommandLine> + <Description>Disable Hibernation</Description> + <Order>1</Order> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>reg add "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon" /v AutoLogonCount /t REG_DWORD /d 0 /f</CommandLine> + <Description>Disable Autologon</Description> + <Order>2</Order> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>powershell "$name = 'utm-guest-tools-0.229.exe'; foreach ($drive in Get-PSDrive -PSProvider FileSystem) { $path = Join-Path $drive.Root $name; if (Test-Path $path) { &amp; $path; break } }"</CommandLine> + <Description>Install SPICE tools</Description> + <Order>3</Order> + </SynchronousCommand> + </FirstLogonCommands> + </component> + <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="arm64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"> + <UserAccounts> + <LocalAccounts> + <LocalAccount wcm:action="add"> + <DisplayName>alice</DisplayName> + <Name>\(unattendUsername)</Name> + <Group>Administrators</Group> + <Password> + <Value>\(unattendPassword)</Value> + <PlainText>true</PlainText> + </Password> + </LocalAccount> + </LocalAccounts> + </UserAccounts> + <AutoLogon> + <Enabled>true</Enabled> + <Username>\(unattendUsername)</Username> + <Password> + <PlainText>true</PlainText> + <Value>\(unattendPassword)</Value> + </Password> + <LogonCount>1</LogonCount> + </AutoLogon> + <OOBE> + <HideEULAPage>true</HideEULAPage> + <HideOnlineAccountScreens>true</HideOnlineAccountScreens> + <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE> + <ProtectYourPC>3</ProtectYourPC> + <VMModeOptimizations> + <SkipWinREInitialization>true</SkipWinREInitialization> + </VMModeOptimizations> + </OOBE> + <FirstLogonCommands> + <SynchronousCommand wcm:action="add"> + <CommandLine>Cmd /c POWERCFG -H OFF</CommandLine> + <Description>Disable Hibernation</Description> + <Order>1</Order> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>reg add "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon" /v AutoLogonCount /t REG_DWORD /d 0 /f</CommandLine> + <Description>Disable Autologon</Description> + <Order>2</Order> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <CommandLine>powershell "$name = 'utm-guest-tools-0.229.exe'; foreach ($drive in Get-PSDrive -PSProvider FileSystem) { $path = Join-Path $drive.Root $name; if (Test-Path $path) { &amp; $path; break } }"</CommandLine> + <Description>Install SPICE tools</Description> + <Order>3</Order> + </SynchronousCommand> + </FirstLogonCommands> + </component> + </settings> +</unattend> +""" + } + + func createAutounattendIso() throws -> URL { + let fileManager = FileManager.default + let url = fileManager.temporaryDirectory.appendingPathComponent("autounattend.iso") + let xml = createUnattendXml().data(using: .utf8)! + // Data layout + // Sectors 0-15 - empty + // Sector 16 - Primary Volume Descriptor + // Sector 17 - Descriptor Set Terminator + // Sector 18 - LE Path Table + // Sector 19 - BE Path Table + // Sector 20 - Root Directory + // Sector 21 - File Data + let sector = 2048 + var iso = Data(count: sector * 21) + let xpad = 100 + iso.withCursor { data in + var data = data + let descriptorId = "CD001" + data.advance(by: 16 * sector) + data.write(u8: 1) // Primary Volume Descriptor + data.write(ascii: descriptorId) // id + data.write(u8: 1) // version + data.write(u8: 0) // unused + data.write(ascii: "", padTo: 32) // System Identifier + data.write(ascii: "AUTOUNATTEND", padTo: 32) // Volume Identifier + data.advance(by: 8) + var xmlSect = xml.count / sector + if xml.count % sector != 0 { + xmlSect += 1 + } + data.write(i32bi: Int32(20 + xmlSect + xpad)) // Volume Space Size + data.advance(by: 32) + data.write(i16bi: 1) // Volume Set Size + data.write(i16bi: 1) // Volume Sequence Number + data.write(i16bi: Int16(sector)) // Logical Block Size + data.write(i32bi: 10) // Path table size + data.write(i32: 18) // Type-L Path Table + data.write(i32: 0) // Type-L Optional Path Table + data.write(i32: Int32(19).bigEndian) // Type-M Path Table + data.write(i32: 0) // Type-M Optional Path Table + // Root Directory Record + data.writeDirRecord(name: Data(repeating: 0, count: 1), location: 20, size: Int32(sector), directory: true) + + data.write(ascii: "", padTo: 128) // Volume Set Identifier + data.write(ascii: "", padTo: 128) // Publisher Identifier + data.write(ascii: "", padTo: 128) // Data Preparer Identifier + data.write(ascii: "", padTo: 128) // Application Identifier + data.write(ascii: "", padTo: 37) // Copyright File Identifier + data.write(ascii: "", padTo: 37) // Abstract File Identifier + data.write(ascii: "", padTo: 37) // Bibliographic File Identifier + data.write(decDate: NSDate.now as Date) // Creation Date + data.write(decDate: NSDate.now as Date) // Modification Date + data.write(decDate: NSDate.now as Date + 315360000) // Expiration Date + data.write(decDate: NSDate.now as Date) // Effective Date + data.write(u8: 1) // File Structure Version + data.write(u8: 0) // Padding + data.advance(by: 512) // Application Use + data.advance(by: 653) // Reserved + + data.write(u8: 255) // Descriptor Set Terminator + data.write(ascii: descriptorId) // id + data.write(u8: 1) // Version + data.advance(by: sector - 7) // Reserved + + // L - Path Table + data.write(u8: 1) // Directory Identifier Length + data.write(u8: 0) // XA Length + data.write(i32: 20) // Location + data.write(i16: 1) // Parent directory number + data.write(u8: 0) // Filename + data.write(u8: 0) // Padding + data.advance(by: sector - 10) + + // M - Path Table + data.write(u8: 1) // Directory Identifier Length + data.write(u8: 0) // XA Length + data.write(i32: Int32(20).bigEndian) // Location + data.write(i16: Int16(1).bigEndian) // Parent directory number + data.write(u8: 0) // Filename + data.write(u8: 0) // Padding + data.advance(by: sector - 10) + + // Root directory + data.writeDirRecord(name: Data(repeating: 0, count: 1), location: 20, size: Int32(sector), directory: true) + data.writeDirRecord(name: Data(repeating: 1, count: 1), location: 20, size: Int32(sector), directory: true) + data.writeDirRecord(name: "AUTOUNATTEND.XML;1".data(using: .ascii)!, location: 21, size: Int32(xml.count), directory: false) + } + iso.append(xml) + if xml.count % sector != 0 { + iso.append(Data(repeating: 0, count: 2048 - xml.count % sector)) + } + iso.append(Data(repeating: 0, count: sector * xpad)) + fileManager.createFile(atPath: url.path, contents: iso) + return url + } + func generateConfig() throws -> any UTMConfiguration { if useVirtualization && useAppleVirtualization { #if os(macOS) @@ -552,6 +1087,126 @@ enum VMBootDevice: Int, Identifiable { } } +struct DataCursor { + var ptr: UnsafeMutableRawBufferPointer + var cursor: Int + mutating func advance(by: Int) { + self.cursor += by + } + mutating func write(u8: UInt8) { + ptr[cursor] = u8 + self.cursor += 1 + } + mutating func write(i32: Int32) { + ptr.storeBytes(of: i32, toByteOffset: self.cursor, as: Int32.self) + self.cursor += 4 + } + mutating func write(i32bi i: Int32) { + write(i32: i) + write(i32: i.bigEndian) + } + mutating func write(i16: Int16) { + ptr.storeBytes(of: i16, toByteOffset: self.cursor, as: Int16.self) + self.cursor += 2 + } + mutating func write(i16bi i: Int16) { + write(i16: i) + write(i16: i.bigEndian) + } + mutating func write(ascii: String) { + write(ascii: ascii, padTo: ascii.utf8.count) + } + mutating func write(ascii: String, padTo: Int) { + ascii.withCString { bytes in + for i in 0..<ascii.utf8.count { + write(u8: UInt8(bytes[i])) + } + } + let padding = max(0, padTo - ascii.utf8.count) + for _ in 0..<padding { + write(u8: 0x20) + } + } + mutating func write(dirDate date: Date) { + let cal = Calendar(identifier: .gregorian) + let tz = TimeZone.current + let components = cal.dateComponents(in: tz, from: date) + write(u8: UInt8(components.year! - 1900)) + write(u8: UInt8(components.month!)) + write(u8: UInt8(components.day!)) + write(u8: UInt8(components.hour!)) + write(u8: UInt8(components.minute!)) + write(u8: UInt8(components.second!)) + write(u8: UInt8(0)) + } + mutating func write(digit: UInt8) { + write(u8: digit + 48) + } + mutating func write(decDate date: Date) { + let cal = Calendar(identifier: .gregorian) + let tz = TimeZone.current + let components = cal.dateComponents(in: tz, from: date) + let year = components.year! + write(digit: UInt8(year / 1000)) + write(digit: UInt8(year % 1000 / 100)) + write(digit: UInt8(year % 100 / 10)) + write(digit: UInt8(year % 10)) + let month = UInt8(components.month!) + write(digit: month / 10) + write(digit: month % 10) + let day = UInt8(components.day!) + write(digit: day / 10) + write(digit: day % 10) + let hour = UInt8(components.hour!) + write(digit: hour / 10) + write(digit: hour % 10) + let minute = UInt8(components.minute!) + write(digit: minute / 10) + write(digit: minute % 10) + let second = UInt8(components.second!) + write(digit: second / 10) + write(digit: second % 10) + let ms = UInt8(0) + write(digit: ms / 10) + write(digit: ms % 10) + write(u8: UInt8(0)) + } + mutating func write(data: Data) { + for i in 0..<data.count { + write(u8: data[i]) + } + } + mutating func writeDirRecord(name: Data, location: Int32, size: Int32, directory: Bool) { + write(u8: UInt8(33 + name.count + 1 - name.count % 2)) // Length + write(u8: 0) // Extended Attribute Length + write(i32bi: location) // Location of extent + write(i32bi: size) // Data Size + write(dirDate: NSDate.now as Date) // Recording Date + var flags = 0 + if directory { + flags = 2 + } + write(u8: UInt8(flags)) // Flags + write(u8: 0) // File unit size + write(u8: 0) // Interleave gap size + write(i16bi: 1) // Volume Sequence Number + write(u8: UInt8(name.count)) // File name length + write(data: name) // File name + if (name.count % 2 == 0) { + write(u8: 0) // Padding + } + } +} + +extension Data { + mutating func withCursor<ResultType>(_ body: (DataCursor) throws -> ResultType) rethrows -> ResultType { + try self.withUnsafeMutableBytes { ptr in + let dc = DataCursor(ptr: ptr, cursor: 0) + return try body(dc) + } + } +} + // MARK: - Warnings for common mistakes extension VMWizardState { diff --git a/Platform/Shared/VMWizardWindowsUnattendView.swift b/Platform/Shared/VMWizardWindowsUnattendView.swift new file mode 100644 index 000000000..367bc3443 --- /dev/null +++ b/Platform/Shared/VMWizardWindowsUnattendView.swift @@ -0,0 +1,48 @@ +// +// Copyright © 2021 osy. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import SwiftUI + +struct VMWizardWindowsUnattendView: View { + @ObservedObject var wizardState: VMWizardState + var body: some View { + VMWizardContent("Unattended Installation") { + Section { + Form { + TextField("Language", text: $wizardState.unattendLanguage) + .keyboardType(.asciiCapable) + .lineLimit(1) + TextField("Username", text: $wizardState.unattendUsername) + .keyboardType(.asciiCapable) + .lineLimit(1) + SecureField("Password", text: $wizardState.unattendPassword) + .keyboardType(.asciiCapable) + .lineLimit(1) + } + } header: { + Text("Configuration") + } + } + } +} + +struct VMWizardWindowsUnattendView_Previews: PreviewProvider { + @StateObject static var wizardState = VMWizardState() + + static var previews: some View { + VMWizardWindowsUnattendView(wizardState: wizardState) + } +} diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index 5af4ccca2..bfeeb5a7b 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -737,11 +737,11 @@ struct AlertMessage: Identifiable { } } - func mountSupportTools(for vm: any UTMVirtualMachine) async throws { + func mountSupportTools(for vm: any UTMVirtualMachine, unattendless: Bool) async throws { guard let vm = vm as? any UTMSpiceVirtualMachine else { throw UTMDataError.unsupportedBackend } - let task = UTMDownloadSupportToolsTask(for: vm) + let task = UTMDownloadSupportToolsTask(for: vm, unattendless: unattendless) if await task.hasExistingSupportTools { vm.config.qemu.isGuestToolsInstallRequested = false _ = try await task.mountTools() diff --git a/Platform/UTMDownloadSupportToolsTask.swift b/Platform/UTMDownloadSupportToolsTask.swift index c7dabfecb..1848265fd 100644 --- a/Platform/UTMDownloadSupportToolsTask.swift +++ b/Platform/UTMDownloadSupportToolsTask.swift @@ -19,15 +19,21 @@ import Foundation /// Downloads support tools ISO class UTMDownloadSupportToolsTask: UTMDownloadTask { private let vm: any UTMSpiceVirtualMachine + private let unattend: Bool private static let supportToolsDownloadUrl = URL(string: "https://getutm.app/downloads/utm-guest-tools-latest.iso")! + private static let supportToolsDownloadUrlUa = URL(string: "https://getutm.app/downloads/unattendless.iso")! private var toolsUrl: URL { fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("GuestSupportTools") } private var supportToolsLocalUrl: URL { - toolsUrl.appendingPathComponent(Self.supportToolsDownloadUrl.lastPathComponent) + var url = Self.supportToolsDownloadUrl + if unattend { + url = Self.supportToolsDownloadUrlUa + } + return toolsUrl.appendingPathComponent(url.lastPathComponent) } @Setting("LastDownloadedGuestTools") @@ -42,10 +48,15 @@ class UTMDownloadSupportToolsTask: UTMDownloadTask { } } - init(for vm: any UTMSpiceVirtualMachine) { + init(for vm: any UTMSpiceVirtualMachine, unattendless: Bool) { self.vm = vm let name = NSLocalizedString("Windows Guest Support Tools", comment: "UTMDownloadSupportToolsTask") - super.init(for: Self.supportToolsDownloadUrl, named: name) + self.unattend = unattendless + var url = Self.supportToolsDownloadUrl + if unattend { + url = Self.supportToolsDownloadUrlUa + } + super.init(for: url, named: name) } override func processCompletedDownload(at location: URL, response: URLResponse?) async throws -> any UTMVirtualMachine { diff --git a/Platform/iOS/VMWizardView.swift b/Platform/iOS/VMWizardView.swift index 266c28eeb..58e8e40d9 100644 --- a/Platform/iOS/VMWizardView.swift +++ b/Platform/iOS/VMWizardView.swift @@ -173,6 +173,8 @@ fileprivate struct WizardViewWrapper: View { VMWizardOSView(wizardState: wizardState) case .macOSBoot: EmptyView() + case .windowsUnattendConfig: + VMWizardWindowsUnattendView(wizardState: wizardState) case .linuxBoot: VMWizardOSLinuxView(wizardState: wizardState) case .windowsBoot: diff --git a/Platform/macOS/VMWizardView.swift b/Platform/macOS/VMWizardView.swift index 380d3f733..d17b50bcf 100644 --- a/Platform/macOS/VMWizardView.swift +++ b/Platform/macOS/VMWizardView.swift @@ -60,6 +60,9 @@ struct VMWizardView: View { case .windowsBoot: VMWizardOSWindowsView(wizardState: wizardState) .transition(wizardState.slide) + case .windowsUnattendConfig: + VMWizardWindowsUnattendView(wizardState: wizardState) + .transition(wizardState.slide) case .hardware: VMWizardHardwareView(wizardState: wizardState) .transition(wizardState.slide) @@ -115,6 +118,7 @@ struct VMWizardView: View { _ = try await data.create(config: qemuConfig) await MainActor.run { qemuConfig.qemu.isGuestToolsInstallRequested = wizardState.isGuestToolsInstallRequested + qemuConfig.qemu.unattended = wizardState.windowsUnattendedInstall } } else if let appleConfig = config as? UTMAppleConfiguration { _ = try await data.create(config: appleConfig) diff --git a/Remote/UTMRemoteServer.swift b/Remote/UTMRemoteServer.swift index 7a50dede6..0a75e37ed 100644 --- a/Remote/UTMRemoteServer.swift +++ b/Remote/UTMRemoteServer.swift @@ -812,7 +812,7 @@ extension UTMRemoteServer { private func _mountGuestToolsOnVirtualMachine(parameters: M.MountGuestToolsOnVirtualMachine.Request) async throws -> M.MountGuestToolsOnVirtualMachine.Reply { let vm = try await findVM(withId: parameters.id) if let wrapped = await vm.wrapped { - try await data.mountSupportTools(for: wrapped) + try await data.mountSupportTools(for: wrapped, unattendless: false) } return .init() } diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index 4603c8241..cb46d6402 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -7,6 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 003B61A42C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003B61A32C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift */; }; + 003B61A52C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003B61A32C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift */; }; + 003B61A62C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003B61A32C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift */; }; + 003B61A72C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 003B61A32C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift */; }; 2C33B3A92566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */; }; 2C33B3AA2566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */; }; 2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C6D9E02256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift */; }; @@ -1584,6 +1588,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 003B61A32C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardWindowsUnattendView.swift; sourceTree = "<group>"; }; 037DAA1C2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/VMDisplayWindow.strings; sourceTree = "<group>"; }; 037DAA1D2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 037DAA1E2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; }; @@ -2972,6 +2977,7 @@ 83034C0626AB630F006B4BAF /* UTMPendingVMView.swift */, 84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */, 84909A9027CADAE0005605F1 /* UTMUnavailableVMView.swift */, + 003B61A32C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift */, ); path = Shared; sourceTree = "<group>"; @@ -3605,6 +3611,7 @@ CEF0307426A2B40B00667B63 /* VMWizardHardwareView.swift in Sources */, 841E997528AA1191003C6CB6 /* UTMRegistry.swift in Sources */, 8401868F288A50B90050AC51 /* VMDisplayViewControllerDelegate.swift in Sources */, + 003B61A42C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift in Sources */, CE2D92D724AD46670059923A /* UTMLogging.m in Sources */, 848D99A8285DB5550055C215 /* VMConfigConstantPicker.swift in Sources */, 8453DCB4278CE5410037A0DA /* UTMQemuImage.swift in Sources */, @@ -3787,6 +3794,7 @@ CE0B6CF524AD568400FE012D /* UTMLegacyQemuConfiguration+Miscellaneous.m in Sources */, CE25125129C806AF000790AB /* UTMScriptingDeleteCommand.swift in Sources */, CE0B6CFB24AD568400FE012D /* UTMLegacyQemuConfiguration+Networking.m in Sources */, + 003B61A72C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift in Sources */, 84C584E5268F8C65000FCABF /* VMAppleSettingsView.swift in Sources */, CE9B15442B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */, 84F746BB276FF70700A20C87 /* VMDisplayQemuDisplayController.swift in Sources */, @@ -4021,6 +4029,7 @@ CEA45F00263519B5002FA97D /* VMCardView.swift in Sources */, CEA45F01263519B5002FA97D /* UTMLegacyQemuConfiguration+Sharing.m in Sources */, 841619B328431DA5000034B2 /* UTMQemuConfigurationQEMU.swift in Sources */, + 003B61A52C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift in Sources */, CEF0305C26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */, CEA45F08263519B5002FA97D /* ActivityView.swift in Sources */, 848F71E9277A2A4E006A0240 /* UTMSerialPort.swift in Sources */, @@ -4138,6 +4147,7 @@ CEF7F5E82AEEDCC400E34952 /* VMDisplayViewControllerDelegate.swift in Sources */, CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */, CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */, + 003B61A62C03EC5B0013BA74 /* VMWizardWindowsUnattendView.swift in Sources */, CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */, CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */, CEF7F5F02AEEDCC400E34952 /* NumberTextField.swift in Sources */,