diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 210e1377..4124c579 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ on: default: 'false' env: - BUILD_XCODE_PATH: /Applications/Xcode_15.1.app + BUILD_XCODE_PATH: /Applications/Xcode_15.2.app RUNNER_IMAGE: macos-13 jobs: @@ -53,7 +53,7 @@ jobs: strategy: matrix: arch: [arm64] - platform: [ios, ios_simulator, ios-tci, macos, visionos, visionos_simulator, visionos-tci] + platform: [ios, ios_simulator, ios-tci, ios_simulator-tci, macos, visionos, visionos_simulator, visionos-tci, visionos_simulator-tci] include: # x86_64 supported only for macOS and simulators - arch: x86_64 @@ -91,7 +91,7 @@ jobs: if: steps.cache-sysroot.outputs.cache-hit != 'true' || github.event.inputs.rebuild_sysroot == 'true' run: ./scripts/build_dependencies.sh -p ${{ matrix.platform }} -a ${{ matrix.arch }} env: - NCPU: ${{ matrix.platform == 'ios-tci' && '2' || '0' }} # limit 2 CPU for TCI build due to memory issues, 0 = unlimited for other builds + NCPU: ${{ endsWith(matrix.platform, '-tci') && '4' || '0' }} # limit 4 CPU for TCI build due to memory issues, 0 = unlimited for other builds - name: Compress Sysroot if: steps.cache-sysroot.outputs.cache-hit != 'true' || github.event_name == 'release' || github.event.inputs.test_release == 'true' run: tar -acf sysroot.tgz sysroot* @@ -152,14 +152,16 @@ jobs: needs: [configuration, build-sysroot] strategy: matrix: - arch: [arm64] - platform: [ios, ios_simulator, ios-tci, macos, visionos, visionos_simulator, visionos-tci] - include: - # x86_64 supported only for macOS and simulators - - arch: x86_64 - platform: macos - - arch: x86_64 - platform: ios_simulator + configuration: [ + {arch: "arm64", sdk: "iphoneos", platform: "ios", scheme: "iOS"}, + {arch: "arm64", sdk: "iphoneos", platform: "ios-tci", scheme: "iOS-SE"}, + {arch: "arm64", sdk: "iphoneos", platform: "ios-tci", scheme: "iOS-Remote"}, + {arch: "arm64", sdk: "xros", platform: "visionos", scheme: "iOS"}, + {arch: "arm64", sdk: "xros", platform: "visionos-tci", scheme: "iOS-SE"}, + {arch: "arm64", sdk: "xros", platform: "visionos-tci", scheme: "iOS-Remote"}, + {arch: "arm64", sdk: "macosx", platform: "macos", scheme: "macOS"}, + {arch: "x86_64", sdk: "macosx", platform: "macos", scheme: "macOS"}, + ] steps: - name: Checkout uses: actions/checkout@v3 @@ -169,8 +171,8 @@ jobs: id: cache-sysroot uses: osy/actions-cache@v3 with: - path: sysroot-${{ matrix.platform }}-${{ matrix.arch }} - key: ${{ matrix.platform }}-${{ matrix.arch }}-${{ hashFiles('scripts/build_dependencies.sh') }}-${{ hashFiles('patches/**') }} + path: sysroot-${{ matrix.configuration.platform }}-${{ matrix.configuration.arch }} + key: ${{ matrix.configuration.platform }}-${{ matrix.configuration.arch }}-${{ hashFiles('scripts/build_dependencies.sh') }}-${{ hashFiles('patches/**') }} - name: Check Cache if: steps.cache-sysroot.outputs.cache-hit != 'true' uses: actions/github-script@v6 @@ -182,12 +184,12 @@ jobs: [[ "$(xcode-select -p)" == "${{ env.BUILD_XCODE_PATH }}"* ]] || sudo xcode-select -s "${{ env.BUILD_XCODE_PATH }}" - name: Build UTM run: | - ./scripts/build_utm.sh -p ${{ matrix.platform }} -a ${{ matrix.arch }} -o UTM + ./scripts/build_utm.sh -k ${{ matrix.configuration.sdk }} -s ${{ matrix.configuration.scheme }} -a ${{ matrix.configuration.arch }} -o UTM tar -acf UTM.xcarchive.tgz UTM.xcarchive - name: Upload UTM uses: actions/upload-artifact@v3 with: - name: UTM-${{ matrix.platform }}-${{ matrix.arch }} + name: UTM-${{ matrix.configuration.scheme }}-${{ matrix.configuration.platform }}-${{ matrix.configuration.arch }} path: UTM.xcarchive.tgz build-universal: name: Build UTM (Universal Mac) @@ -215,7 +217,7 @@ jobs: [[ "$(xcode-select -p)" == "${{ env.BUILD_XCODE_PATH }}"* ]] || sudo xcode-select -s "${{ env.BUILD_XCODE_PATH }}" - name: Build UTM run: | - ./scripts/build_utm.sh -t "$SIGNING_TEAM_ID" -p macos -a "arm64 x86_64" -o UTM + ./scripts/build_utm.sh -t "$SIGNING_TEAM_ID" -k macosx -s macOS -a "arm64 x86_64" -o UTM tar -acf UTM.xcarchive.tgz UTM.xcarchive env: SIGNING_TEAM_ID: ${{ vars.SIGNING_TEAM_ID }} @@ -231,12 +233,14 @@ jobs: strategy: matrix: configuration: [ - {platform: "ios", mode: "ipa", name: "UTM.ipa", path: "UTM.ipa"}, - {platform: "ios-tci", mode: "ipa-se", name: "UTM-SE.ipa", path: "UTM SE.ipa"}, - {platform: "ios", mode: "ipa-hv", name: "UTM-HV.ipa", path: "UTM.ipa"}, - {platform: "ios", mode: "deb", name: "UTM.deb", path: "UTM.deb"}, - {platform: "visionos", mode: "ipa", name: "UTM-visionOS.ipa", path: "UTM.ipa"}, - {platform: "visionos-tci", mode: "ipa-se", name: "UTM-SE-visionOS.ipa", path: "UTM SE.ipa"} + {platform: "ios", scheme: "iOS", mode: "ipa", name: "UTM.ipa", path: "UTM.ipa"}, + {platform: "ios-tci", scheme: "iOS-SE", mode: "ipa-se", name: "UTM-SE.ipa", path: "UTM SE.ipa"}, + {platform: "ios", scheme: "iOS", mode: "ipa-hv", name: "UTM-HV.ipa", path: "UTM.ipa"}, + {platform: "ios", scheme: "iOS", mode: "deb", name: "UTM.deb", path: "UTM.deb"}, + {platform: "visionos", scheme: "iOS", mode: "ipa", name: "UTM-visionOS.ipa", path: "UTM.ipa"}, + {platform: "visionos-tci", scheme: "iOS-SE", mode: "ipa-se", name: "UTM-SE-visionOS.ipa", path: "UTM SE.ipa"}, + {platform: "ios-tci", scheme: "iOS-Remote", mode: "ipa-remote", name: "UTM-Remote.ipa", path: "UTM Remote.ipa"}, + {platform: "visionos-tci", scheme: "iOS-Remote", mode: "ipa-remote", name: "UTM-Remote-visionOS.ipa", path: "UTM Remote.ipa"}, ] if: github.event_name == 'release' || github.event.inputs.test_release == 'true' steps: @@ -245,7 +249,7 @@ jobs: - name: Download Artifact uses: actions/download-artifact@v3 with: - name: UTM-${{ matrix.configuration.platform }}-arm64 + name: UTM-${{ matrix.configuration.scheme }}-${{ matrix.configuration.platform }}-arm64 - name: Install ldid + dpkg run: brew install ldid dpkg - name: Fakesign IPA diff --git a/Configuration/QEMUConstant.swift b/Configuration/QEMUConstant.swift index 75a6d9ae..be78c41e 100644 --- a/Configuration/QEMUConstant.swift +++ b/Configuration/QEMUConstant.swift @@ -424,20 +424,20 @@ extension QEMUArchitecture { default: return true } } - + var hasHypervisorSupport: Bool { - guard jb_has_hypervisor() else { + guard UTMCapabilities.current.contains(.hasHypervisorSupport) else { + return false + } + if UTMCapabilities.current.contains(.isAarch64) { + return self == .aarch64 + } else if UTMCapabilities.current.contains(.isX86_64) { + return self == .x86_64 + } else { return false } - #if arch(arm64) - return self == .aarch64 - #elseif arch(x86_64) - return self == .x86_64 - #else - return false - #endif } - + /// TSO is supported on jailbroken iOS devices with Hypervisor support var hasTSOSupport: Bool { #if os(iOS) || os(visionOS) diff --git a/Configuration/UTMConfiguration.swift b/Configuration/UTMConfiguration.swift index 2d458fba..abfe3470 100644 --- a/Configuration/UTMConfiguration.swift +++ b/Configuration/UTMConfiguration.swift @@ -120,7 +120,7 @@ extension UTMConfiguration { #endif // is it a legacy QEMU config? let dict = try NSDictionary(contentsOf: configURL, error: ()) as! [AnyHashable : Any] - let name = UTMQemuVirtualMachine.virtualMachineName(for: packageURL) + let name = ConcreteVirtualMachine.virtualMachineName(for: packageURL) let legacy = UTMLegacyQemuConfiguration(dictionary: dict, name: name, path: packageURL) return UTMQemuConfiguration(migrating: legacy) } else if stub.backend == .qemu { diff --git a/Configuration/UTMConfigurationDrive.swift b/Configuration/UTMConfigurationDrive.swift index 61103ac9..57122602 100644 --- a/Configuration/UTMConfigurationDrive.swift +++ b/Configuration/UTMConfigurationDrive.swift @@ -15,7 +15,6 @@ // import Foundation -import QEMUKitInternal /// Settings for single disk device protocol UTMConfigurationDrive: Codable, Hashable, Identifiable { @@ -101,13 +100,17 @@ extension UTMConfigurationDrive { try handle.close() }.value } - + private func createQcow2Image(at newURL: URL, size sizeMib: Int) async throws { + #if WITH_REMOTE + fatalError("Not implemented") + #else try await Task.detached { if !QEMUGenerateDefaultQcow2File(newURL as CFURL, sizeMib) { throw UTMConfigurationError.cannotCreateDiskImage } }.value + #endif } #if os(macOS) diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index 2f445bec..201b9cd6 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/Configuration/UTMQemuConfiguration+Arguments.swift @@ -61,6 +61,26 @@ import Virtualization // for getting network interfaces socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("swtpm") } + /// Used only if in remote sever mode. + var monitorPipeURL: URL { + socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qmp") + } + + /// Used only if in remote sever mode. + var guestAgentPipeURL: URL { + socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("qga") + } + + /// Used only if in remote sever mode. + var spiceTlsKeyUrl: URL { + socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("pem") + } + + /// Used only if in remote sever mode. + var spiceTlsCertUrl: URL { + socketURL.appendingPathComponent(information.uuid.uuidString).appendingPathExtension("crt") + } + /// Combined generated and user specified arguments. @QEMUArgumentBuilder var allArguments: [QEMUArgument] { generatedArguments @@ -109,16 +129,48 @@ import Virtualization // for getting network interfaces @QEMUArgumentBuilder private var spiceArguments: [QEMUArgument] { f("-spice") - "unix=on" - "addr=\(spiceSocketURL.lastPathComponent)" - "disable-ticketing=on" - "image-compression=off" - "playback-compression=off" - "streaming-video=off" - "gl=\(isGLOn ? "on" : "off")" + if let port = qemu.spiceServerPort { + if qemu.isSpiceServerTlsEnabled { + "tls-port=\(port)" + "tls-channel=default" + "x509-key-file=" + spiceTlsKeyUrl + "x509-cert-file=" + spiceTlsCertUrl + "x509-cacert-file=" + spiceTlsCertUrl + } else { + "port=\(port)" + } + } else { + "unix=on" + "addr=\(spiceSocketURL.lastPathComponent)" + } + if let _ = qemu.spiceServerPassword { + "password-secret=secspice0" + } else { + "disable-ticketing=on" + } + if !isRemoteSpice { + "image-compression=off" + "playback-compression=off" + "streaming-video=off" + } else { + "streaming-video=filter" + } + "gl=\(isGLSupported && !isRemoteSpice ? "on" : "off")" f() f("-chardev") - f("spiceport,id=org.qemu.monitor.qmp,name=org.qemu.monitor.qmp.0") + if isRemoteSpice { + "pipe" + "path=" + monitorPipeURL + } else { + "spiceport" + "name=org.qemu.monitor.qmp.0" + } + "id=org.qemu.monitor.qmp" + f() f("-mon") f("chardev=org.qemu.monitor.qmp,mode=control") if !isSparc { // disable -vga and other default devices @@ -128,8 +180,28 @@ import Virtualization // for getting network interfaces f("-vga") f("none") } + if let password = qemu.spiceServerPassword { + // assume anyone who can read this is in our trust domain + f("-object") + f("secret,id=secspice0,data=\(password)") + } } - + + private func filterDisplayIfRemote(_ display: any QEMUDisplayDevice) -> any QEMUDisplayDevice { + if isRemoteSpice { + let rawValue = display.rawValue + if rawValue.hasSuffix("-gl") { + return AnyQEMUConstant(rawValue: String(rawValue.dropLast(3)))! + } else if rawValue.contains("-gl-") { + return AnyQEMUConstant(rawValue: String(rawValue.replacingOccurrences(of: "-gl-", with: "-")))! + } else { + return display + } + } else { + return display + } + } + @QEMUArgumentBuilder private var displayArguments: [QEMUArgument] { if displays.isEmpty { f("-nographic") @@ -143,7 +215,7 @@ import Virtualization // for getting network interfaces } else { for display in displays { f("-device") - display.hardware + filterDisplayIfRemote(display.hardware) if let vgaRamSize = displays[0].vgaRamMib { "vgamem_mb=\(vgaRamSize)" } @@ -152,7 +224,7 @@ import Virtualization // for getting network interfaces } } - private var isGLOn: Bool { + private var isGLSupported: Bool { displays.contains { display in display.hardware.rawValue.contains("-gl-") || display.hardware.rawValue.hasSuffix("-gl") } @@ -161,7 +233,11 @@ import Virtualization // for getting network interfaces private var isSparc: Bool { system.architecture == .sparc || system.architecture == .sparc64 } - + + private var isRemoteSpice: Bool { + qemu.spiceServerPort != nil + } + @QEMUArgumentBuilder private var serialArguments: [QEMUArgument] { for i in serials.indices { f("-chardev") @@ -318,9 +394,9 @@ import Virtualization // for getting network interfaces } let tbSize = system.jitCacheSize > 0 ? system.jitCacheSize : system.memorySize / 4 "tb-size=\(tbSize)" - #if !WITH_QEMU_TCI + #if WITH_JIT // use mirror mapping when we don't have JIT entitlements - if !jb_has_jit_entitlement() { + if !UTMCapabilities.current.contains(.hasJitEntitlements) { "split-wx=on" } #endif @@ -433,6 +509,10 @@ import Virtualization // for getting network interfaces #if os(iOS) || os(visionOS) return false #else + // only support SPICE audio if we are running remotely + if isRemoteSpice { + return false + } // force CoreAudio backend for mac99 which only supports 44100 Hz // pcspk doesn't work with SPICE audio if sound.contains(where: { $0.hardware.rawValue == "screamer" || $0.hardware.rawValue == "pcspk" }) { @@ -671,7 +751,7 @@ import Virtualization // for getting network interfaces f("usb-mouse,bus=usb-bus.0") f("-device") f("usb-kbd,bus=usb-bus.0") - #if !WITH_QEMU_TCI + #if WITH_USB let maxDevices = input.maximumUsbShare let buses = (maxDevices + 2) / 3 if input.usbBusSupport == .usb3_0 { @@ -859,7 +939,16 @@ import Virtualization // for getting network interfaces f("-device") f("virtserialport,chardev=org.qemu.guest_agent,name=org.qemu.guest_agent.0") f("-chardev") - f("spiceport,id=org.qemu.guest_agent,name=org.qemu.guest_agent.0") + if isRemoteSpice { + "pipe" + "path=" + guestAgentPipeURL + } else { + "spiceport" + "name=org.qemu.guest_agent.0" + } + "id=org.qemu.guest_agent" + f() } if isSpiceAgentUsed { f("-device") diff --git a/Configuration/UTMQemuConfigurationQEMU.swift b/Configuration/UTMQemuConfigurationQEMU.swift index a04c4ada..14c6d261 100644 --- a/Configuration/UTMQemuConfigurationQEMU.swift +++ b/Configuration/UTMQemuConfigurationQEMU.swift @@ -69,6 +69,15 @@ struct UTMQemuConfigurationQEMU: Codable { /// Set to true to request UEFI variable reset. Not saved. var isUefiVariableResetRequested: Bool = false + /// Set to open a port for remote SPICE session. Not saved. + var spiceServerPort: UInt16? + + /// If true, all SPICE channels will be over TLS. Not saved. + var isSpiceServerTlsEnabled: Bool = false + + /// Set to a password shared with the client. Not saved. + var spiceServerPassword: String? + enum CodingKeys: String, CodingKey { case hasDebugLog = "DebugLog" case hasUefiBoot = "UEFIBoot" diff --git a/Platform/Main.swift b/Platform/Main.swift index 6e024b43..23c448a4 100644 --- a/Platform/Main.swift +++ b/Platform/Main.swift @@ -34,7 +34,7 @@ class Main { static var jitAvailable = true static func main() { - #if (os(iOS) || os(visionOS)) && !WITH_QEMU_TCI + #if (os(iOS) || os(visionOS)) && WITH_JIT // check if we have jailbreak if jb_spawn_ptrace_child(CommandLine.argc, CommandLine.unsafeArgv) { logger.info("JIT: ptrace() child spawn trick") diff --git a/Platform/Shared/BigButtonStyle.swift b/Platform/Shared/BigButtonStyle.swift index 744cfc23..377948b1 100644 --- a/Platform/Shared/BigButtonStyle.swift +++ b/Platform/Shared/BigButtonStyle.swift @@ -17,12 +17,12 @@ import SwiftUI struct BigButtonStyle: ButtonStyle { - let width: CGFloat - let height: CGFloat - + let width: CGFloat? + let height: CGFloat? + fileprivate struct BigButtonView: View { - let width: CGFloat - let height: CGFloat + let width: CGFloat? + let height: CGFloat? let configuration: BigButtonStyle.Configuration @Environment(\.isEnabled) private var isEnabled: Bool diff --git a/Platform/Shared/ContentView.swift b/Platform/Shared/ContentView.swift index ce971971..62c20ca6 100644 --- a/Platform/Shared/ContentView.swift +++ b/Platform/Shared/ContentView.swift @@ -20,8 +20,11 @@ import UniformTypeIdentifiers import IQKeyboardManagerSwift #endif -#if WITH_QEMU_TCI +// on visionOS, there is no text to show more than UTM +#if WITH_QEMU_TCI && !os(visionOS) let productName = "UTM SE" +#elseif WITH_REMOTE && !os(visionOS) +let productName = "UTM Remote" #else let productName = "UTM" #endif @@ -33,7 +36,8 @@ struct ContentView: View { @State private var newPopupPresented = false @State private var openSheetPresented = false @Environment(\.openURL) var openURL - + @AppStorage("ServerAutostart") private var isServerAutostart: Bool = false + var body: some View { VMNavigationListView() .overlay(data.showSettingsModal ? AnyView(EmptyView()) : AnyView(BusyOverlay())) @@ -67,6 +71,11 @@ struct ContentView: View { .onAppear { Task { await data.listRefresh() + #if os(macOS) + if isServerAutostart { + await data.remoteServer.start() + } + #endif } Task { await releaseHelper.fetchReleaseNotes() @@ -78,7 +87,7 @@ struct ContentView: View { #if !os(visionOS) IQKeyboardManager.shared.enable = true #endif - #if !WITH_QEMU_TCI + #if WITH_JIT if !Main.jitAvailable { data.busyWorkAsync { let jitStreamerAttach = UserDefaults.standard.bool(forKey: "JitStreamerAttach") @@ -95,7 +104,7 @@ struct ContentView: View { #endif // ignore error when we are running on a HV only build - if !jb_has_hypervisor() { + if !UTMCapabilities.current.contains(.hasHypervisorSupport) { throw NSLocalizedString("Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached. See https://getutm.app/install/ for more details.", comment: "ContentView") } } @@ -163,7 +172,7 @@ struct ContentView: View { case "pause": if let vm = findVM(), vm.state == .started { let shouldSaveOnPause: Bool - if let vm = vm.wrapped as? UTMQemuVirtualMachine { + if let vm = vm.wrapped as? (any UTMSpiceVirtualMachine) { shouldSaveOnPause = !vm.isRunningAsDisposible } else { shouldSaveOnPause = true diff --git a/Platform/Shared/MacDeviceLabel.swift b/Platform/Shared/MacDeviceLabel.swift new file mode 100644 index 00000000..48f97dc0 --- /dev/null +++ b/Platform/Shared/MacDeviceLabel.swift @@ -0,0 +1,111 @@ +// +// Copyright © 2024 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 +import UniformTypeIdentifiers + +struct MacDeviceLabel: View where Title : StringProtocol { + let title: Title + let device: MacDevice + + init(_ title: Title, device macDevice: MacDevice) { + self.title = title + self.device = macDevice + } + + var body: some View { + Label(title, systemImage: device.symbolName) + } +} + +// credits: https://adamdemasi.com/2023/04/15/mac-device-icon-by-device-class.html + +private extension UTTagClass { + static let deviceModelCode = UTTagClass(rawValue: "com.apple.device-model-code") +} + +private extension UTType { + static let macBook = UTType("com.apple.mac.laptop") + static let macBookWithNotch = UTType("com.apple.mac.notched-laptop") + static let macMini = UTType("com.apple.macmini") + static let macStudio = UTType("com.apple.macstudio") + static let iMac = UTType("com.apple.imac") + static let macPro = UTType("com.apple.macpro") + static let macPro2013 = UTType("com.apple.macpro-cylinder") + static let macPro2019 = UTType("com.apple.macpro-2019") +} + +struct MacDevice { + let model: String + let symbolName: String + + #if os(macOS) + static let current: Self = { + let key = "hw.model" + var size = size_t() + sysctlbyname(key, nil, &size, nil, 0) + let value = malloc(size) + defer { + value?.deallocate() + } + sysctlbyname(key, value, &size, nil, 0) + guard let cChar = value?.bindMemory(to: CChar.self, capacity: size) else { + return Self(model: "Unknown") + } + return Self(model: String(cString: cChar)) + }() + #endif + + init(model: String?) { + self.model = model ?? "Unknown" + self.symbolName = Self.symbolName(from: self.model) + } + + private static func checkModel(_ model: String, conformsTo type: UTType?) -> Bool { + guard let type else { + return false + } + return UTType(tag: model, tagClass: .deviceModelCode, conformingTo: nil)?.conforms(to: type) ?? false + } + + private static func symbolName(from model: String) -> String { + if checkModel(model, conformsTo: .macBookWithNotch), + #available(macOS 14, iOS 17, macCatalyst 17, tvOS 17, watchOS 10, *) { + // macbook.gen2 was added with SF Symbols 5.0 (macOS Sonoma, 2023), but MacBooks with a notch + // were released in 2021! + return "macbook.gen2" + } else if checkModel(model, conformsTo: .macBook) { + return "laptopcomputer" + } else if checkModel(model, conformsTo: .macMini) { + return "macmini" + } else if checkModel(model, conformsTo: .macStudio) { + return "macstudio" + } else if checkModel(model, conformsTo: .iMac) { + return "desktopcomputer" + } else if checkModel(model, conformsTo: .macPro2019) { + return "macpro.gen3" + } else if checkModel(model, conformsTo: .macPro2013) { + return "macpro.gen2" + } else if checkModel(model, conformsTo: .macPro) { + return "macpro" + } + return "display" + } +} + +#Preview { + MacDeviceLabel("MacBook", device: MacDevice(model: "Mac14,6")) +} diff --git a/Platform/Shared/NumberTextField.swift b/Platform/Shared/NumberTextField.swift index dadfcb5d..67f1c4db 100644 --- a/Platform/Shared/NumberTextField.swift +++ b/Platform/Shared/NumberTextField.swift @@ -107,7 +107,16 @@ struct NumberTextField: View { self.onEditingChanged = onEditingChanged self.promptKey = prompt } - + + init(_ titleKey: LocalizedStringKey, number: Binding<Int?>, prompt: LocalizedStringKey = "0", onEditingChanged: @escaping (Bool) -> Void = { _ in }) { + let nsnumber = Binding<NSNumber?> { + return number.wrappedValue as NSNumber? + } set: { newValue in + number.wrappedValue = newValue?.intValue + } + self.init(titleKey, number: nsnumber, prompt: prompt, onEditingChanged: onEditingChanged) + } + init(_ titleKey: LocalizedStringKey, number: Binding<Int>, prompt: LocalizedStringKey = "0", onEditingChanged: @escaping (Bool) -> Void = { _ in }) { let nsnumber = Binding<NSNumber?> { return number.wrappedValue as NSNumber diff --git a/Platform/Shared/UTMUnavailableVMView.swift b/Platform/Shared/UTMUnavailableVMView.swift index 3a978822..8e687345 100644 --- a/Platform/Shared/UTMUnavailableVMView.swift +++ b/Platform/Shared/UTMUnavailableVMView.swift @@ -25,7 +25,13 @@ struct UTMUnavailableVMView: View { subtitle: vm.detailsSubtitleLabel, progress: nil, imageOverlaySystemName: "questionmark.circle.fill", - popover: { WrappedVMDetailsView(path: vm.pathUrl.path, onRemove: remove) }, + popover: { + #if WITH_REMOTE + UnsupportedVMDetailsView(vm: vm) + #else + WrappedVMDetailsView(path: vm.pathUrl.path, onRemove: remove) + #endif + }, onRemove: remove) } @@ -71,6 +77,26 @@ fileprivate struct WrappedVMDetailsView: View { } } +#if WITH_REMOTE +fileprivate struct UnsupportedVMDetailsView: View { + @ObservedObject var vm: VMData + + var body: some View { + VStack(alignment: .center) { + if let remotevm = vm as? VMRemoteData, let reason = remotevm.unavailableReason { + Text(reason) + .lineLimit(nil) + } else { + Text("This VM is unavailable.") + } + } + #if os(macOS) + .frame(width: 230) + #endif + } +} +#endif + struct UTMUnavailableVMView_Previews: PreviewProvider { static var previews: some View { UTMUnavailableVMView(vm: VMData(from: UTMRegistryEntry.empty)) diff --git a/Platform/Shared/VMCommands.swift b/Platform/Shared/VMCommands.swift index 54cc7597..fe0ce91a 100644 --- a/Platform/Shared/VMCommands.swift +++ b/Platform/Shared/VMCommands.swift @@ -21,6 +21,7 @@ struct VMCommands: Commands { @CommandsBuilder var body: some Commands { + #if !WITH_REMOTE // FIXME: implement remote feature CommandGroup(replacing: .newItem) { Button(action: { NotificationCenter.default.post(name: NSNotification.NewVirtualMachine, object: nil) }, label: { Text("New…") @@ -29,6 +30,7 @@ struct VMCommands: Commands { Text("Open…") }).keyboardShortcut(KeyEquivalent("o")) } + #endif SidebarCommands() ToolbarCommands() CommandGroup(replacing: .help) { diff --git a/Platform/Shared/VMConfigInputView.swift b/Platform/Shared/VMConfigInputView.swift index f244be0f..71cfbf77 100644 --- a/Platform/Shared/VMConfigInputView.swift +++ b/Platform/Shared/VMConfigInputView.swift @@ -26,7 +26,7 @@ struct VMConfigInputView: View { VMConfigConstantPicker("USB Support", selection: $config.usbBusSupport) } - #if !WITH_QEMU_TCI + #if WITH_USB if config.usbBusSupport != .disabled { Section(header: Text("USB Sharing")) { if !jb_has_usb_entitlement() { diff --git a/Platform/Shared/VMConfigSystemView.swift b/Platform/Shared/VMConfigSystemView.swift index 1fcf9e48..a2c1ce88 100644 --- a/Platform/Shared/VMConfigSystemView.swift +++ b/Platform/Shared/VMConfigSystemView.swift @@ -101,7 +101,7 @@ struct VMConfigSystemView: View { } #endif let actualJitSizeMib = jitSizeMib == 0 ? memorySizeMib / 4 : jitSizeMib - let jitMirrorMultiplier = jb_has_jit_entitlement() ? 1 : 2; + let jitMirrorMultiplier = UTMCapabilities.current.contains(.hasJitEntitlements) ? 1 : 2; let estMemoryUsage = UInt64(memorySizeMib + jitMirrorMultiplier*actualJitSizeMib + baseUsageMib) * bytesInMib if Double(estMemoryUsage) > Double(totalDeviceMemory) * warningThreshold { warningMessage = WarningMessage.overallocatedRam(totalMib: totalDeviceMemory / bytesInMib, estimatedMib: estMemoryUsage / bytesInMib) @@ -177,7 +177,7 @@ private struct HardwareOptions: View { } } .onChange(of: config.architecture) { newValue in - isArchitectureSupported = UTMQemuVirtualMachine.isSupported(systemArchitecture: newValue) + isArchitectureSupported = ConcreteVirtualMachine.isSupported(systemArchitecture: newValue) if newValue != architecture { architecture = newValue } diff --git a/Platform/Shared/VMContextMenuModifier.swift b/Platform/Shared/VMContextMenuModifier.swift index 6c4859dd..066f5dfd 100644 --- a/Platform/Shared/VMContextMenuModifier.swift +++ b/Platform/Shared/VMContextMenuModifier.swift @@ -61,6 +61,7 @@ struct VMContextMenuModifier: ViewModifier { }.help("Reveal where the VM is stored.") Divider() #endif + #if !WITH_REMOTE // FIXME: implement remote feature Button { data.close(vm: vm) // close window data.edit(vm: vm) @@ -68,6 +69,7 @@ struct VMContextMenuModifier: ViewModifier { Label("Edit", systemImage: "slider.horizontal.3") }.disabled(vm.hasSuspendState || !vm.isModifyAllowed) .help("Modify settings for this VM.") + #endif if vm.hasSuspendState || !vm.isStopped { Button { confirmAction = .confirmStopVM @@ -99,7 +101,7 @@ struct VMContextMenuModifier: ViewModifier { } #endif - if let _ = vm.wrapped as? UTMQemuVirtualMachine { + if let _ = vm.config as? UTMQemuConfiguration { Button { data.run(vm: vm, options: .bootDisposibleMode) } label: { @@ -120,6 +122,7 @@ struct VMContextMenuModifier: ViewModifier { Divider() } + #if !WITH_REMOTE // FIXME: implement remote feature Button { shareItem = .utmCopy(vm) showSharePopup.toggle() @@ -164,6 +167,7 @@ struct VMContextMenuModifier: ViewModifier { }.disabled(!vm.isModifyAllowed) .help("Delete this VM and all its data.") } + #endif } .modifier(VMShareItemModifier(isPresented: $showSharePopup, shareItem: shareItem)) .modifier(VMConfirmActionModifier(vm: vm, confirmAction: $confirmAction) { @@ -175,7 +179,7 @@ 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 as! UTMQemuVirtualMachine) + try await data.mountSupportTools(for: vm.wrapped!) } } } diff --git a/Platform/Shared/VMDetailsView.swift b/Platform/Shared/VMDetailsView.swift index 62fe46af..b1cbe8d5 100644 --- a/Platform/Shared/VMDetailsView.swift +++ b/Platform/Shared/VMDetailsView.swift @@ -29,9 +29,10 @@ struct VMDetailsView: View { #else private let regularScreenSizeClass: Bool = true #endif - + + @State private var size: Int64 = 0 + private var sizeLabel: String { - let size = data.computeSize(for: vm) return ByteCountFormatter.string(fromByteCount: size, countStyle: .binary) } @@ -70,8 +71,8 @@ struct VMDetailsView: View { .padding([.leading, .trailing, .bottom]) } #else - let qemuVM = vm.wrapped as! UTMQemuVirtualMachine - VMRemovableDrivesView(vm: vm, config: qemuVM.config) + let qemuConfig = vm.config as! UTMQemuConfiguration + VMRemovableDrivesView(vm: vm, config: qemuConfig) .padding([.leading, .trailing, .bottom]) #endif } else { @@ -89,8 +90,8 @@ struct VMDetailsView: View { VMRemovableDrivesView(vm: vm, config: qemuVM.config) } #else - let qemuVM = vm.wrapped as! UTMQemuVirtualMachine - VMRemovableDrivesView(vm: vm, config: qemuVM.config) + let qemuConfig = vm.config as! UTMQemuConfiguration + VMRemovableDrivesView(vm: vm, config: qemuConfig) #endif }.padding([.leading, .trailing, .bottom]) } @@ -109,6 +110,16 @@ struct VMDetailsView: View { } #endif } + .onAppear { + Task { + size = await data.computeSize(for: vm) + #if WITH_REMOTE + if let vm = vm.wrapped as? UTMRemoteSpiceVirtualMachine { + await vm.loadScreenshotFromServer() + } + #endif + } + } } } } @@ -151,7 +162,7 @@ struct Screenshot: View { .blendMode(.hardLight) #if os(visionOS) .overlay { - if vm.isStopped { + if vm.isStopped || vm.isTakeoverAllowed { Image(systemName: "play.circle.fill") .resizable() .frame(width: 100, height: 100) @@ -164,7 +175,7 @@ struct Screenshot: View { #endif if vm.isBusy { Spinner(size: .large) - } else if vm.isStopped { + } else if vm.isStopped || vm.isTakeoverAllowed { #if !os(visionOS) Button(action: { data.run(vm: vm) }, label: { Label("Run", systemImage: "play.circle.fill") diff --git a/Platform/Shared/VMNavigationListView.swift b/Platform/Shared/VMNavigationListView.swift index eaeea400..5fee68f3 100644 --- a/Platform/Shared/VMNavigationListView.swift +++ b/Platform/Shared/VMNavigationListView.swift @@ -66,8 +66,10 @@ struct VMNavigationListView: View { } } }.onMove(perform: move) + #if !WITH_REMOTE // FIXME: implement remote feature .onDelete(perform: delete) - + #endif + if data.pendingVMs.count > 0 { Section(header: Text("Pending")) { ForEach(data.pendingVMs, id: \.name) { vm in @@ -119,10 +121,12 @@ private struct VMListModifier: ViewModifier { newButton } #else + #if !WITH_REMOTE // FIXME: implement remote feature ToolbarItem(placement: .navigationBarLeading) { newButton } - #if !os(visionOS) + #endif + #if !os(visionOS) && !WITH_REMOTE ToolbarItem(placement: .navigationBarTrailing) { Button("Settings") { settingsPresented.toggle() @@ -140,7 +144,9 @@ private struct VMListModifier: ViewModifier { if data.showNewVMSheet { VMWizardView() } else if settingsPresented { + #if !WITH_REMOTE UTMSettingsView() + #endif } } .onChange(of: data.showNewVMSheet) { newValue in diff --git a/Platform/Shared/VMPlaceholderView.swift b/Platform/Shared/VMPlaceholderView.swift index c906b375..307e965f 100644 --- a/Platform/Shared/VMPlaceholderView.swift +++ b/Platform/Shared/VMPlaceholderView.swift @@ -17,39 +17,100 @@ import SwiftUI struct VMPlaceholderView: View { - @EnvironmentObject private var data: UTMData - @Environment(\.openURL) private var openURL - + var body: some View { + if #available(iOS 16, macOS 13, *) { + VMPlaceholderViewNew() + } else { + VMPlaceholderViewOld() + } + } +} + +fileprivate struct VMPlaceholderViewOld: View { var body: some View { VStack { + Title() HStack { - Text("Welcome to UTM").font(.title) + FirstRow() } HStack { - TileButton(Label(String.create, systemImage: "plus.circle")) { - data.newVM() - } - TileButton(Label(String.browse, systemImage: "arrow.down.circle")) { - openURL(URL(string: "https://mac.getutm.app/gallery/")!) - } + SecondRow() } - HStack { - TileButton(Label(String.guide, systemImage: "book.circle")) { - openURL(URL(string: "https://docs.getutm.app/basics/basics/")!) + } + } +} + +@available(iOS 16, macOS 13, *) +fileprivate struct VMPlaceholderViewNew: View { + @Environment(\.openWindow) private var openWindow + + var body: some View { + VStack { + Title() + Grid { + GridRow { + FirstRow() } - TileButton(Label(String.support, systemImage: "questionmark.circle")) { - openURL(URL(string: "https://docs.getutm.app/")!) + GridRow { + SecondRow() } + #if os(macOS) + GridRow { + Button { + openWindow(id: "server") + } label: { + Label(String.server, systemImage: "server.rack") + }.buttonStyle(BigButtonStyle(width: nil, height: 50)) + .gridCellColumns(2) + .gridCellUnsizedAxes(.horizontal) + } + #endif } } } } +fileprivate struct Title: View { + var body: some View { + HStack { + Text("Welcome to UTM").font(.title) + } + } +} + +fileprivate struct FirstRow: View { + @EnvironmentObject private var data: UTMData + @Environment(\.openURL) private var openURL + + var body: some View { + TileButton(Label(String.create, systemImage: "plus.circle")) { + data.newVM() + } + TileButton(Label(String.browse, systemImage: "arrow.down.circle")) { + openURL(URL(string: "https://mac.getutm.app/gallery/")!) + } + } +} + +fileprivate struct SecondRow: View { + @Environment(\.openURL) private var openURL + + var body: some View { + TileButton(Label(String.guide, systemImage: "book.circle")) { + openURL(URL(string: "https://docs.getutm.app/basics/basics/")!) + } + TileButton(Label(String.support, systemImage: "questionmark.circle")) { + openURL(URL(string: "https://docs.getutm.app/")!) + } + } +} + fileprivate extension String { static let create = NSLocalizedString("Create a New Virtual Machine", comment: "Welcome view") static let browse = NSLocalizedString("Browse UTM Gallery", comment: "Welcome view") static let guide = NSLocalizedString("User Guide", comment: "Welcome view") static let support = NSLocalizedString("Support", comment: "Welcome view") + static let server = NSLocalizedString("Server", comment: "Server view") } private struct TileButton: View { diff --git a/Platform/Shared/VMRemovableDrivesView.swift b/Platform/Shared/VMRemovableDrivesView.swift index 73387f9f..0682533b 100644 --- a/Platform/Shared/VMRemovableDrivesView.swift +++ b/Platform/Shared/VMRemovableDrivesView.swift @@ -26,8 +26,8 @@ struct VMRemovableDrivesView: View { @State private var workaroundFileImporterBug: Bool = false @State private var currentDrive: UTMQemuConfigurationDrive? - private var qemuVM: UTMQemuVirtualMachine! { - vm.wrapped as? UTMQemuVirtualMachine + private var qemuVM: (any UTMSpiceVirtualMachine)! { + vm.wrapped as? any UTMSpiceVirtualMachine } var fileManager: FileManager { @@ -78,6 +78,7 @@ struct VMRemovableDrivesView: View { } ForEach(config.drives.filter { $0.isExternal }) { drive in HStack { + #if !WITH_REMOTE // FIXME: implement remote feature // Drive menu Menu { // Browse button @@ -118,6 +119,9 @@ struct VMRemovableDrivesView: View { } label: { DriveLabel(drive: drive, isInserted: qemuVM.externalImageURL(for: drive) != nil) }.disabled(vm.hasSuspendState) + #else + DriveLabel(drive: drive, isInserted: qemuVM.externalImageURL(for: drive) != nil) + #endif Spacer() // Disk image path, or (empty) Text(pathFor(drive)) diff --git a/Platform/Shared/VMToolbarModifier.swift b/Platform/Shared/VMToolbarModifier.swift index 28e49c85..d8c34502 100644 --- a/Platform/Shared/VMToolbarModifier.swift +++ b/Platform/Shared/VMToolbarModifier.swift @@ -51,6 +51,7 @@ struct VMToolbarModifier: ViewModifier { UTMPreferenceButtonToolbarContent() #endif ToolbarItemGroup(placement: buttonPlacement) { + #if !WITH_REMOTE // FIXME: implement remote feature if vm.isShortcut { DestructiveButton { confirmAction = .confirmDeleteShortcut @@ -112,6 +113,7 @@ struct VMToolbarModifier: ViewModifier { Spacer() } #endif + #endif if vm.hasSuspendState || !vm.isStopped { Button { confirmAction = .confirmStopVM @@ -129,6 +131,7 @@ struct VMToolbarModifier: ViewModifier { }.help("Run selected VM") .padding(.leading, padding) } + #if !WITH_REMOTE // FIXME: implement remote feature #if !os(macOS) if bottom { Spacer() @@ -143,6 +146,7 @@ struct VMToolbarModifier: ViewModifier { }.help("Edit selected VM") .disabled(vm.hasSuspendState || !vm.isModifyAllowed) .padding(.leading, padding) + #endif } } .modifier(VMShareItemModifier(isPresented: $showSharePopup, shareItem: shareItem)) diff --git a/Platform/Shared/VMWizardStartView.swift b/Platform/Shared/VMWizardStartView.swift index 785e4c3b..3c9a4774 100644 --- a/Platform/Shared/VMWizardStartView.swift +++ b/Platform/Shared/VMWizardStartView.swift @@ -26,12 +26,12 @@ struct VMWizardStartView: View { #if os(macOS) VZVirtualMachine.isSupported && !processIsTranslated() #else - jb_has_hypervisor() + UTMCapabilities.current.contains(.hasHypervisorSupport) #endif } var isEmulationSupported: Bool { - #if WITH_QEMU_TCI + #if !WITH_JIT true #else Main.jitAvailable diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index 77d4b81c..0a69c3cf 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -21,9 +21,19 @@ import AppKit import UIKit import SwiftUI #endif -#if canImport(AltKit) && !WITH_QEMU_TCI +#if canImport(AltKit) && WITH_JIT import AltKit #endif +#if WITH_SERVER +import Combine +#endif + +#if WITH_REMOTE +import CocoaSpiceNoUsb +typealias ConcreteVirtualMachine = UTMRemoteSpiceVirtualMachine +#else +typealias ConcreteVirtualMachine = UTMQemuVirtualMachine +#endif struct AlertMessage: Identifiable { var message: String @@ -88,7 +98,18 @@ struct AlertMessage: Identifiable { nonisolated private var documentsURL: URL { UTMData.defaultStorageUrl } - + + #if WITH_SERVER + /// Remote access server + private(set) var remoteServer: UTMRemoteServer! + + /// Listeners for remote access + private var remoteChangeListeners: [VMData: Set<AnyCancellable>] = [:] + + /// Listener for list changes + private var listChangedListener: AnyCancellable? + #endif + /// Queue to run `busyWork` tasks private var busyQueue: DispatchQueue @@ -100,6 +121,10 @@ struct AlertMessage: Identifiable { self.virtualMachines = [] self.pendingVMs = [] self.selectedVM = nil + #if WITH_SERVER + self.remoteServer = UTMRemoteServer(data: self) + beginObservingChanges() + #endif listLoadFromDefaults() } @@ -133,7 +158,7 @@ struct AlertMessage: Identifiable { guard try file.resourceValues(forKeys: [.isDirectoryKey]).isDirectory ?? false else { continue } - guard UTMQemuVirtualMachine.isVirtualMachine(url: file) else { + guard ConcreteVirtualMachine.isVirtualMachine(url: file) else { continue } await Task.yield() @@ -168,7 +193,7 @@ struct AlertMessage: Identifiable { } /// Load VM list (and order) from persistent storage - private func listLoadFromDefaults() { + fileprivate func listLoadFromDefaults() { let defaults = UserDefaults.standard guard defaults.object(forKey: "VMList") == nil else { listLegacyLoadFromDefaults() @@ -186,7 +211,7 @@ struct AlertMessage: Identifiable { guard let list = defaults.stringArray(forKey: "VMEntryList") else { return } - virtualMachines = list.uniqued().compactMap { uuidString in + let virtualMachines: [VMData] = list.uniqued().compactMap { uuidString in guard let entry = UTMRegistry.shared.entry(for: uuidString) else { return nil } @@ -198,6 +223,7 @@ struct AlertMessage: Identifiable { } return vm } + listReplace(with: virtualMachines) } /// Load VM list (and order) from persistent storage (legacy) @@ -205,7 +231,7 @@ struct AlertMessage: Identifiable { let defaults = UserDefaults.standard // legacy path list if let files = defaults.array(forKey: "VMList") as? [String] { - virtualMachines = files.uniqued().compactMap({ file in + let virtualMachines = files.uniqued().compactMap({ file in let url = documentsURL.appendingPathComponent(file, isDirectory: true) if let vm = try? VMData(url: url) { return vm @@ -213,10 +239,11 @@ struct AlertMessage: Identifiable { return nil } }) + listReplace(with: virtualMachines) } // bookmark list if let list = defaults.array(forKey: "VMList") { - virtualMachines = list.compactMap { item in + let virtualMachines = list.compactMap { item in let vm: VMData? if let bookmark = item as? Data { vm = VMData(bookmark: bookmark) @@ -228,6 +255,7 @@ struct AlertMessage: Identifiable { try? vm?.load() return vm } + listReplace(with: virtualMachines) } } @@ -238,8 +266,15 @@ struct AlertMessage: Identifiable { defaults.set(wrappedVMs, forKey: "VMEntryList") } - private func listReplace(with vms: [VMData]) { + /// Replace current VM list with a new list + /// - Parameter vms: List to replace with + fileprivate func listReplace(with vms: [VMData]) { + virtualMachines.forEach({ endObservingChanges(for: $0) }) virtualMachines = vms + vms.forEach({ beginObservingChanges(for: $0) }) + if let vm = selectedVM, !vms.contains(where: { $0 == vm }) { + selectedVM = nil + } } /// Add VM to list @@ -254,6 +289,7 @@ struct AlertMessage: Identifiable { } else { virtualMachines.append(vm) } + beginObservingChanges(for: vm) } /// Select VM in list @@ -267,6 +303,7 @@ struct AlertMessage: Identifiable { /// - Returns: Index of item removed or nil if already removed @discardableResult public func listRemove(vm: VMData) -> Int? { let index = virtualMachines.firstIndex(of: vm) + endObservingChanges(for: vm) if let index = index { virtualMachines.remove(at: index) } @@ -316,7 +353,7 @@ struct AlertMessage: Identifiable { let nameForId = { (i: Int) in i <= 1 ? base : "\(base) \(i)" } for i in 1..<1000 { let name = nameForId(i) - let file = UTMQemuVirtualMachine.virtualMachinePath(for: name, in: documentsURL) + let file = ConcreteVirtualMachine.virtualMachinePath(for: name, in: documentsURL) if !fileManager.fileExists(atPath: file.path) { return name } @@ -383,6 +420,13 @@ struct AlertMessage: Identifiable { func save(vm: VMData) async throws { do { try await vm.save() + #if WITH_SERVER + if let qemuConfig = vm.config as? UTMQemuConfiguration { + await remoteServer.broadcast { remote in + try await remote.qemuConfigurationHasChanged(id: vm.id, configuration: qemuConfig) + } + } + #endif } catch { // refresh the VM object as it is now stale let origError = error @@ -450,8 +494,8 @@ struct AlertMessage: Identifiable { /// - Returns: The new VM @discardableResult func clone(vm: VMData) async throws -> VMData { let newName: String = newDefaultVMName(base: vm.detailsTitleLabel) - let newPath = UTMQemuVirtualMachine.virtualMachinePath(for: newName, in: documentsURL) - + let newPath = ConcreteVirtualMachine.virtualMachinePath(for: newName, in: documentsURL) + try await copyItemWithCopyfile(at: vm.pathUrl, to: newPath) guard let newVM = try? VMData(url: newPath) else { throw UTMDataError.cloneFailed @@ -532,7 +576,7 @@ struct AlertMessage: Identifiable { /// Calculate total size of VM and data /// - Parameter vm: VM to calculate size /// - Returns: Size in bytes - func computeSize(for vm: VMData) -> Int64 { + func computeSize(for vm: VMData) async -> Int64 { let path = vm.pathUrl guard let enumerator = fileManager.enumerator(at: path, includingPropertiesForKeys: [.totalFileAllocatedSizeKey]) else { logger.error("failed to create enumerator for \(path)") @@ -616,7 +660,7 @@ struct AlertMessage: Identifiable { listSelect(vm: vm) } - func copyItemWithCopyfile(at srcURL: URL, to dstURL: URL) async throws { + private func copyItemWithCopyfile(at srcURL: URL, to dstURL: URL) async throws { try await Task.detached(priority: .userInitiated) { let status = copyfile(srcURL.path, dstURL.path, nil, copyfile_flags_t(COPYFILE_ALL | COPYFILE_RECURSIVE | COPYFILE_CLONE | COPYFILE_DATA_SPARSE)) if status < 0 { @@ -677,7 +721,10 @@ struct AlertMessage: Identifiable { } } - func mountSupportTools(for vm: UTMQemuVirtualMachine) async throws { + func mountSupportTools(for vm: any UTMVirtualMachine) async throws { + guard let vm = vm as? any UTMSpiceVirtualMachine else { + throw UTMDataError.unsupportedBackend + } let task = UTMDownloadSupportToolsTask(for: vm) if await task.hasExistingSupportTools { vm.config.qemu.isGuestToolsInstallRequested = false @@ -756,7 +803,60 @@ struct AlertMessage: Identifiable { } vm.changeUuid(to: UUID(), name: nil, copyingEntry: vm.registryEntry) } - + + // MARK: - Change listener + + private func beginObservingChanges() { + #if WITH_SERVER + listChangedListener = $virtualMachines.sink { vms in + Task { + await self.remoteServer.broadcast { remote in + try await remote.listHasChanged(ids: vms.map({ $0.id })) + } + } + } + #endif + } + + private func beginObservingChanges(for vm: VMData) { + #if WITH_SERVER + var observers = Set<AnyCancellable>() + let registryEntry = vm.registryEntry + observers.insert(vm.objectWillChange.sink { [self] _ in + // reset observers when registry changes + if vm.registryEntry != registryEntry { + endObservingChanges(for: vm) + beginObservingChanges(for: vm) + } + }) + observers.insert(vm.$state.sink { state in + Task { + let isTakeoverAllowed = self.vmWindows[vm] is VMRemoteSessionState && (state == .started || state == .paused) + await self.remoteServer.broadcast { remote in + try await remote.virtualMachine(id: vm.id, didTransitionToState: state, isTakeoverAllowed: isTakeoverAllowed) + } + } + }) + if let registryEntry = registryEntry { + observers.insert(registryEntry.externalDrivePublisher.sink { drives in + let mountedDrives = drives.mapValues({ $0.path }) + Task { + await self.remoteServer.broadcast { remote in + try await remote.mountedDrivesHasChanged(id: vm.id, mountedDrives: mountedDrives) + } + } + }) + } + remoteChangeListeners[vm] = observers + #endif + } + + private func endObservingChanges(for vm: VMData) { + #if WITH_SERVER + remoteChangeListeners.removeValue(forKey: vm) + #endif + } + // MARK: - Other utility functions /// In some regions, iOS will prompt the user for network access @@ -790,16 +890,20 @@ struct AlertMessage: Identifiable { /// Execute a task with spinning progress indicator (Swift concurrency version) /// - Parameter work: Function to execute - func busyWorkAsync(_ work: @escaping @Sendable () async throws -> Void) { + @discardableResult + func busyWorkAsync<T>(_ work: @escaping @Sendable () async throws -> T) -> Task<T, any Error> { Task.detached(priority: .userInitiated) { await self.setBusyIndicator(true) do { - try await work() + let result = try await work() + await self.setBusyIndicator(false) + return result } catch { logger.error("\(error)") await self.showErrorAlert(message: error.localizedDescription) + await self.setBusyIndicator(false) + throw error } - await self.setBusyIndicator(false) } } @@ -824,7 +928,7 @@ struct AlertMessage: Identifiable { /// - vm: VM to send mouse/tablet coordinates to /// - components: Data (see UTM Wiki for details) func automationSendMouse(to vm: VMData, urlComponents components: URLComponents) { - guard let qemuVm = vm.wrapped as? UTMQemuVirtualMachine else { return } // FIXME: implement for Apple VM + guard let qemuVm = vm.wrapped as? any UTMSpiceVirtualMachine else { return } // FIXME: implement for Apple VM guard !qemuVm.config.displays.isEmpty else { return } guard let queryItems = components.queryItems else { return } /// Parse targeted position @@ -868,7 +972,7 @@ struct AlertMessage: Identifiable { // MARK: - AltKit -#if canImport(AltKit) && !WITH_QEMU_TCI +#if canImport(AltKit) && WITH_JIT /// Detect if we are installed from AltStore and can use AltJIT var isAltServerCompatible: Bool { guard let _ = Bundle.main.infoDictionary?["ALTServerID"] else { @@ -968,6 +1072,8 @@ struct AlertMessage: Identifiable { // MARK: - Errors enum UTMDataError: Error { case virtualMachineAlreadyExists + case virtualMachineUnavailable + case unsupportedBackend case cloneFailed case shortcutCreationFailed case importFailed @@ -977,6 +1083,8 @@ enum UTMDataError: Error { case jitStreamerDecodeFailed case jitStreamerAttachFailed case jitStreamerUrlInvalid(String) + case notImplemented + case reconnectFailed } extension UTMDataError: LocalizedError { @@ -984,6 +1092,10 @@ extension UTMDataError: LocalizedError { switch self { case .virtualMachineAlreadyExists: return NSLocalizedString("An existing virtual machine already exists with this name.", comment: "UTMData") + case .virtualMachineUnavailable: + return NSLocalizedString("This virtual machine is currently unavailable, make sure it is not open in another session.", comment: "UTMData") + case .unsupportedBackend: + return NSLocalizedString("Operation not supported by the backend.", comment: "UTMData") case .cloneFailed: return NSLocalizedString("Failed to clone VM.", comment: "UTMData") case .shortcutCreationFailed: @@ -1002,6 +1114,239 @@ extension UTMDataError: LocalizedError { return NSLocalizedString("Failed to attach to JitStreamer.", comment: "UTMData") case .jitStreamerUrlInvalid(let urlString): return String.localizedStringWithFormat(NSLocalizedString("Invalid JitStreamer attach URL:\n%@", comment: "UTMData"), urlString) + case .notImplemented: + return NSLocalizedString("This functionality is not yet implemented.", comment: "UTMData") + case .reconnectFailed: + return NSLocalizedString("Failed to reconnect to the server.", comment: "UTMData") } } } + +// MARK: - Remote Client + +/// Declare host capabilities to any remote client +struct UTMCapabilities: OptionSet, Codable { + let rawValue: UInt + + /// If set, no trick is needed to get JIT working as the process is entitled. + static let hasJitEntitlements = Self(rawValue: 1 << 0) + + /// If set, virtualization is supported by this host. + static let hasHypervisorSupport = Self(rawValue: 1 << 1) + + /// If set, host is aarch64 + static let isAarch64 = Self(rawValue: 1 << 2) + + /// If set, host is x86_64 + static let isX86_64 = Self(rawValue: 1 << 3) + + static fileprivate(set) var current: Self = { + var current = Self() + #if WITH_JIT + if jb_has_jit_entitlement() { + current.insert(.hasJitEntitlements) + } + if jb_has_hypervisor() { + current.insert(.hasHypervisorSupport) + } + #endif + #if arch(arm64) + current.insert(.isAarch64) + #endif + #if arch(x86_64) + current.insert(.isX86_64) + #endif + return current + }() +} + +#if WITH_REMOTE +private let kReconnectTimeoutSeconds: UInt64 = 5 + +@MainActor +class UTMRemoteData: UTMData { + /// Remote access client + private(set) var remoteClient: UTMRemoteClient! + + override init() { + super.init() + self.remoteClient = UTMRemoteClient(data: self) + } + + override func listLoadFromDefaults() { + // do nothing since we do not load from VMList + } + + override func listRefresh() async { + busyWorkAsync { + try await self.listRefreshFromRemote() + } + } + + func reconnect(to server: UTMRemoteClient.State.SavedServer) async throws { + var reconnectTask: Task<UTMRemoteClient.Remote, any Error>? + let timeoutTask = Task { + try await Task.sleep(nanoseconds: kReconnectTimeoutSeconds * NSEC_PER_SEC) + reconnectTask?.cancel() + } + reconnectTask = busyWorkAsync { [self] in + do { + try await remoteClient.connect(server) + } catch is CancellationError { + throw UTMDataError.reconnectFailed + } + timeoutTask.cancel() + try await listRefreshFromRemote() + return await remoteClient.server + } + // make all active sessions wait on the reconnect + for session in VMSessionState.allActiveSessions.values { + let vm = session.vm as! UTMRemoteSpiceVirtualMachine + Task { + do { + try await vm.reconnectServer { + try await reconnectTask!.value + } + } catch { + session.stop() + } + } + } + _ = try await reconnectTask!.value + } + + private func listRefreshFromRemote() async throws { + if let capabilities = await self.remoteClient.server.capabilities { + UTMCapabilities.current = capabilities + } + let ids = try await remoteClient.server.listVirtualMachines() + let items = try await remoteClient.server.getVirtualMachineInformation(for: ids) + let openSessionVms = VMSessionState.allActiveSessions.values.map({ $0.vm }) + let vms = items.map { item in + let wrapped = openSessionVms.first(where: { $0.id == item.id }) as? UTMRemoteSpiceVirtualMachine + return VMRemoteData(fromRemoteItem: item, existingWrapped: wrapped) + } + await loadVirtualMachines(vms) + } + + private func loadVirtualMachines(_ vms: [VMData]) async { + listReplace(with: vms) + for vm in vms { + let remoteVM = vm as! VMRemoteData + if remoteVM.isLoaded { + continue + } + do { + try await remoteVM.load(withRemoteServer: remoteClient.server) + } catch { + remoteVM.unavailableReason = error.localizedDescription + } + await Task.yield() + } + } + + func remoteListHasChanged(ids: [UUID]) async { + var existing = virtualMachines.reduce(into: [:]) { partialResult, vm in + partialResult[vm.id] = vm + } + let new = ids.compactMap { id in + if existing[id] == nil { + return id + } else { + return nil + } + } + if !new.isEmpty, let newItems = try? await remoteClient.server.getVirtualMachineInformation(for: new) { + newItems.map({ VMRemoteData(fromRemoteItem: $0) }).forEach { vm in + existing[vm.id] = vm + } + } + let vms = ids.compactMap({ existing[$0] }) + await loadVirtualMachines(vms) + } + + func remoteQemuConfigurationHasChanged(id: UUID, configuration: UTMQemuConfiguration) async { + guard let vm = virtualMachines.first(where: { $0.id == id }) as? VMRemoteData else { + return + } + await vm.reloadConfiguration(withRemoteServer: remoteClient.server, config: configuration) + } + + func remoteMountedDrivesHasChanged(id: UUID, mountedDrives: [String: String]) async { + guard let vm = virtualMachines.first(where: { $0.id == id }) as? VMRemoteData else { + return + } + vm.updateMountedDrives(mountedDrives) + } + + func remoteVirtualMachineDidTransition(id: UUID, state: UTMVirtualMachineState, isTakeoverAllowed: Bool) async { + guard let vm = virtualMachines.first(where: { $0.id == id }) else { + return + } + let remoteVM = vm as! VMRemoteData + let wrapped = remoteVM.wrapped as! UTMRemoteSpiceVirtualMachine + remoteVM.isTakeoverAllowed = isTakeoverAllowed + await wrapped.updateRemoteState(state) + } + + func remoteVirtualMachineDidError(id: UUID, message: String) async { + if let session = VMSessionState.allActiveSessions.values.first(where: { $0.vm.id == id }) { + session.nonfatalError = message + } + } + + override func listMove(fromOffsets: IndexSet, toOffset: Int) { + let ids = fromOffsets.map({ virtualMachines[$0].id }) + Task { + try await remoteClient.server.reorderVirtualMachines(fromIds: ids, toOffset: toOffset) + } + super.listMove(fromOffsets: fromOffsets, toOffset: toOffset) + } + + override func save(vm: VMData) async throws { + throw UTMDataError.notImplemented + } + + override func discardChanges(for vm: VMData) throws { + throw UTMDataError.notImplemented + } + + override func create<Config: UTMConfiguration>(config: Config) async throws -> VMData { + throw UTMDataError.notImplemented + } + + @discardableResult + override func delete(vm: VMData, alsoRegistry: Bool) async throws -> Int? { + throw UTMDataError.notImplemented + } + + @discardableResult + override func clone(vm: VMData) async throws -> VMData { + throw UTMDataError.notImplemented + } + + override func export(vm: VMData, to url: URL) async throws { + throw UTMDataError.notImplemented + } + + override func move(vm: VMData, to url: URL) async throws { + throw UTMDataError.notImplemented + } + + override func template(vm: VMData) async throws { + throw UTMDataError.notImplemented + } + + override func computeSize(for vm: VMData) async -> Int64 { + (try? await remoteClient.server.getPackageSize(for: vm.id)) ?? 0 + } + + override func importUTM(from url: URL, asShortcut: Bool) async throws { + throw UTMDataError.notImplemented + } + + override func mountSupportTools(for vm: any UTMVirtualMachine) async throws { + try await remoteClient.server.mountGuestToolsOnVirtualMachine(id: vm.id) + } +} +#endif diff --git a/Platform/UTMDownloadSupportToolsTask.swift b/Platform/UTMDownloadSupportToolsTask.swift index c08b34a8..c7dabfec 100644 --- a/Platform/UTMDownloadSupportToolsTask.swift +++ b/Platform/UTMDownloadSupportToolsTask.swift @@ -18,8 +18,8 @@ import Foundation /// Downloads support tools ISO class UTMDownloadSupportToolsTask: UTMDownloadTask { - private let vm: UTMQemuVirtualMachine - + private let vm: any UTMSpiceVirtualMachine + private static let supportToolsDownloadUrl = URL(string: "https://getutm.app/downloads/utm-guest-tools-latest.iso")! private var toolsUrl: URL { @@ -42,7 +42,7 @@ class UTMDownloadSupportToolsTask: UTMDownloadTask { } } - init(for vm: UTMQemuVirtualMachine) { + init(for vm: any UTMSpiceVirtualMachine) { self.vm = vm let name = NSLocalizedString("Windows Guest Support Tools", comment: "UTMDownloadSupportToolsTask") super.init(for: Self.supportToolsDownloadUrl, named: name) diff --git a/Platform/UTMReleaseHelper.swift b/Platform/UTMReleaseHelper.swift index cffddf8a..4848aa64 100644 --- a/Platform/UTMReleaseHelper.swift +++ b/Platform/UTMReleaseHelper.swift @@ -99,6 +99,10 @@ class UTMReleaseHelper: ObservableObject { if platform == "iOS SE" { currentSection.body.append(description) } + #elseif WITH_REMOTE + if platform == "iOS Remote" { + currentSection.body.append(description) + } #endif #if os(visionOS) if platform.hasPrefix("visionOS") { diff --git a/Platform/VMData.swift b/Platform/VMData.swift index a5a901d0..f96826e2 100644 --- a/Platform/VMData.swift +++ b/Platform/VMData.swift @@ -20,7 +20,7 @@ import SwiftUI /// Model wrapping a single UTMVirtualMachine for use in views @MainActor class VMData: ObservableObject { /// Underlying virtual machine - private(set) var wrapped: (any UTMVirtualMachine)? { + fileprivate(set) var wrapped: (any UTMVirtualMachine)? { willSet { objectWillChange.send() } @@ -53,8 +53,8 @@ import SwiftUI } /// Registry entry before loading - private var registryEntryWrapped: UTMRegistryEntry? - + fileprivate var registryEntryWrapped: UTMRegistryEntry? + /// Set when we use a temporary UUID because we loaded a legacy entry private var uuidUnknown: Bool = false @@ -67,14 +67,22 @@ import SwiftUI @Published var state: UTMVirtualMachineState = .stopped /// Copy from wrapped VM - @Published var screenshot: PlatformImage? - + @Published var screenshot: UTMVirtualMachineScreenshot? + + /// If true, it is possible to hijack the session. + @Published var isTakeoverAllowed: Bool = false + /// Allows changes in the config, registry, and VM to be reflected private var observers: [AnyCancellable] = [] + /// True if the .utm is loaded outside of the default storage + var isShortcut: Bool { + isShortcut(pathUrl) + } + /// No default init - private init() { - + fileprivate init() { + } /// Create a VM from an existing object @@ -129,9 +137,11 @@ import SwiftUI /// - Parameter config: Configuration to create new VM convenience init<Config: UTMConfiguration>(creatingFromConfig config: Config, destinationUrl: URL) throws { self.init() + #if !WITH_REMOTE if let qemuConfig = config as? UTMQemuConfiguration { wrapped = try UTMQemuVirtualMachine(newForConfiguration: qemuConfig, destinationUrl: destinationUrl) } + #endif #if os(macOS) if let appleConfig = config as? UTMAppleConfiguration { wrapped = try UTMAppleVirtualMachine(newForConfiguration: appleConfig, destinationUrl: destinationUrl) @@ -160,9 +170,11 @@ import SwiftUI } var loaded: (any UTMVirtualMachine)? let config = try UTMQemuConfiguration.load(from: url) + #if !WITH_REMOTE if let qemuConfig = config as? UTMQemuConfiguration { loaded = try UTMQemuVirtualMachine(packageUrl: url, configuration: qemuConfig, isShortcut: isShortcut(url)) } + #endif #if os(macOS) if let appleConfig = config as? UTMAppleConfiguration { loaded = try UTMAppleVirtualMachine(packageUrl: url, configuration: appleConfig, isShortcut: isShortcut(url)) @@ -195,7 +207,7 @@ import SwiftUI } /// Listen to changes in the underlying object and propogate upwards - private func subscribeToChildren() { + fileprivate func subscribeToChildren() { var s: [AnyCancellable] = [] if let wrapped = wrapped { wrapped.onConfigurationChange = { [weak self] in @@ -205,10 +217,12 @@ import SwiftUI } } - wrapped.onStateChange = { [weak self] in + wrapped.onStateChange = { [weak self, weak wrapped] in Task { @MainActor in - self?.state = wrapped.state - self?.screenshot = wrapped.screenshot + if let wrapped = wrapped { + self?.state = wrapped.state + self?.screenshot = wrapped.screenshot + } } } } @@ -281,11 +295,6 @@ extension VMData: Hashable { // MARK: - VM State extension VMData { - /// True if the .utm is loaded outside of the default storage - var isShortcut: Bool { - isShortcut(pathUrl) - } - func isShortcut(_ url: URL) -> Bool { let defaultStorageUrl = UTMData.defaultStorageUrl.standardizedFileURL let parentUrl = url.deletingLastPathComponent().standardizedFileURL @@ -422,6 +431,98 @@ extension VMData { /// If non-null, is the most recent screenshot image of the running VM var screenshotImage: PlatformImage? { - wrapped?.screenshot + wrapped?.screenshot?.image } } + +#if WITH_REMOTE +@MainActor +class VMRemoteData: VMData { + private var backend: UTMBackend + private var _isShortcut: Bool + override var isShortcut: Bool { + _isShortcut + } + private var initialState: UTMVirtualMachineState + private var existingWrapped: UTMRemoteSpiceVirtualMachine? + + /// Set by caller when VM is unavailable and there is a reason for it. + @Published var unavailableReason: String? + + init(fromRemoteItem item: UTMRemoteMessageServer.VirtualMachineInformation, existingWrapped: UTMRemoteSpiceVirtualMachine? = nil) { + self.backend = item.backend + self._isShortcut = item.isShortcut + self.initialState = item.state + self.existingWrapped = existingWrapped + super.init() + self.isTakeoverAllowed = item.isTakeoverAllowed + self.registryEntryWrapped = UTMRegistry.shared.entry(uuid: item.id, name: item.name, path: item.path) + self.registryEntryWrapped!.isSuspended = item.isSuspended + self.registryEntryWrapped!.externalDrives = item.mountedDrives.mapValues({ UTMRegistryEntry.File(dummyFromPath: $0) }) + } + + override func load() throws { + throw VMRemoteDataError.notImplemented + } + + func load(withRemoteServer server: UTMRemoteClient.Remote) async throws { + guard backend == .qemu else { + throw VMRemoteDataError.backendNotSupported + } + let entry = registryEntryWrapped! + let config = try await server.getQEMUConfiguration(for: entry.uuid) + await loadCustomIcon(withRemoteServer: server, id: entry.uuid, config: config) + let vm: UTMRemoteSpiceVirtualMachine + if let existingWrapped = existingWrapped { + vm = existingWrapped + wrapped = vm + self.existingWrapped = nil + await reloadConfiguration(withRemoteServer: server, config: config) + vm.updateRegistry(entry) + } else { + vm = UTMRemoteSpiceVirtualMachine(forRemoteServer: server, remotePath: entry.package.path, entry: entry, config: config) + wrapped = vm + } + vm.updateConfigFromRegistry() + subscribeToChildren() + await vm.updateRemoteState(initialState) + } + + func reloadConfiguration(withRemoteServer server: UTMRemoteClient.Remote, config: UTMQemuConfiguration) async { + let spiceVM = wrapped as! UTMRemoteSpiceVirtualMachine + await loadCustomIcon(withRemoteServer: server, id: spiceVM.id, config: config) + spiceVM.reload(usingConfiguration: config) + } + + private func loadCustomIcon(withRemoteServer server: UTMRemoteClient.Remote, id: UUID, config: UTMQemuConfiguration) async { + if config.information.isIconCustom, let iconUrl = config.information.iconURL { + if let iconUrl = try? await server.getPackageFile(for: id, relativePathComponents: [UTMQemuConfiguration.dataDirectoryName, iconUrl.lastPathComponent]) { + config.information.iconURL = iconUrl + } + } + } + + func updateMountedDrives(_ mountedDrives: [String: String]) { + guard let registryEntry = registryEntry else { + return + } + registryEntry.externalDrives = mountedDrives.mapValues({ UTMRegistryEntry.File(dummyFromPath: $0) }) + } +} + +enum VMRemoteDataError: Error { + case notImplemented + case backendNotSupported +} + +extension VMRemoteDataError: LocalizedError { + var errorDescription: String? { + switch self { + case .notImplemented: + return NSLocalizedString("This function is not implemented.", comment: "VMData") + case .backendNotSupported: + return NSLocalizedString("This VM is configured for a backend that does not support remote clients.", comment: "VMData") + } + } +} +#endif diff --git a/Platform/iOS/Display/VMDisplayMetalViewController+Pointer.m b/Platform/iOS/Display/VMDisplayMetalViewController+Pointer.m index 5abd169f..153a5840 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController+Pointer.m +++ b/Platform/iOS/Display/VMDisplayMetalViewController+Pointer.m @@ -129,7 +129,11 @@ NS_AVAILABLE_IOS(13.4) - (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region { // Hide cursor while hovering in VM view if (interaction.view == self.mtkView && self.hasTouchpadPointer) { +#if TARGET_OS_VISION + return nil; // FIXME: hidden pointer seems to jump around due to following gaze +#else return [UIPointerStyle hiddenPointerStyle]; +#endif } return nil; } @@ -153,11 +157,13 @@ NS_AVAILABLE_IOS(13.4) - (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction regionForRequest:(UIPointerRegionRequest *)request defaultRegion:(UIPointerRegion *)defaultRegion { +#if !TARGET_OS_VISION if (@available(iOS 14.0, *)) { if (self.prefersPointerLocked) { return nil; } } +#endif // Requesting region for the VM display? if (interaction.view == self.mtkView && self.hasTouchpadPointer) { // Then we need to find out if the pointer is in the actual display area or outside diff --git a/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m b/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m index 5e9148dd..bf0b94de 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m +++ b/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m @@ -181,11 +181,15 @@ const CGFloat kScrollResistance = 10.0f; } - (VMMouseType)indirectMouseType { +#if TARGET_OS_VISION + return VMMouseTypeAbsolute; +#else if (@available(iOS 14.0, *)) { return VMMouseTypeRelative; } else { return VMMouseTypeAbsolute; // legacy iOS 13.4 mouse handling requires absolute } +#endif } #pragma mark - Converting view points to VM display points @@ -635,7 +639,7 @@ static CGRect CGRectClipToBounds(CGRect rect1, CGRect rect2) { VMMouseType type = [self touchTypeToMouseType:touch.type]; #if TARGET_OS_VISION if ([self isTouchGazeGesture:touch]) { - type = self.indirectMouseType; + type = VMMouseTypeRelative; } #endif if ([self switchMouseType:type]) { diff --git a/Platform/iOS/Display/VMDisplayMetalViewController.h b/Platform/iOS/Display/VMDisplayMetalViewController.h index 6a1a3566..8bf301bd 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController.h +++ b/Platform/iOS/Display/VMDisplayMetalViewController.h @@ -16,7 +16,7 @@ #import <UIKit/UIKit.h> #import "VMDisplayViewController.h" -#if defined(WITH_QEMU_TCI) +#if !defined(WITH_USB) @import CocoaSpiceNoUsb; #else @import CocoaSpice; @@ -42,6 +42,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) NSMutableArray<UIKeyCommand *> *mutableKeyCommands; +@property (nonatomic) BOOL isDynamicResolutionSupported; + - (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil NS_UNAVAILABLE; - (instancetype)initWithDisplay:(CSDisplay *)display input:(nullable CSInput *)input NS_DESIGNATED_INITIALIZER; diff --git a/Platform/iOS/Display/VMDisplayMetalViewController.m b/Platform/iOS/Display/VMDisplayMetalViewController.m index 02a1ffe8..4a3e4cce 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController.m +++ b/Platform/iOS/Display/VMDisplayMetalViewController.m @@ -29,11 +29,15 @@ #import "UTM-Swift.h" @import CocoaSpiceRenderer; +static const NSInteger kResizeDebounceSecs = 1; +static const NSInteger kResizeTimeoutSecs = 5; + @interface VMDisplayMetalViewController () @property (nonatomic, nullable) CSMetalRenderer *renderer; -@property (nonatomic) CGFloat windowScaling; -@property (nonatomic) CGPoint windowOrigin; +@property (nonatomic, nullable) id debounceResize; +@property (nonatomic, nullable) id cancelResize; +@property (nonatomic) BOOL ignoreNextResize; @end @@ -43,9 +47,6 @@ if (self = [super initWithNibName:nil bundle:nil]) { self.vmDisplay = display; self.vmInput = input; - self.windowScaling = 1.0; - self.windowOrigin = CGPointZero; - [self addObserver:self forKeyPath:@"vmDisplay.displaySize" options:NSKeyValueObservingOptionNew context:nil]; } return self; } @@ -120,19 +121,25 @@ - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; self.prefersHomeIndicatorAutoHidden = YES; +#if !TARGET_OS_VISION [self startGCMouse]; +#endif [self.vmDisplay addRenderer:self.renderer]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; +#if !TARGET_OS_VISION [self stopGCMouse]; +#endif [self.vmDisplay removeRenderer:self.renderer]; + [self removeObserver:self forKeyPath:@"vmDisplay.displaySize"]; } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; self.delegate.displayViewSize = [self convertSizeToNative:self.view.bounds.size]; + [self addObserver:self forKeyPath:@"vmDisplay.displaySize" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial) context:nil]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator { @@ -140,10 +147,12 @@ [coordinator animateAlongsideTransition:nil completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { self.delegate.displayViewSize = [self convertSizeToNative:size]; [self.delegate display:self.vmDisplay didResizeTo:self.vmDisplay.displaySize]; + if (self.delegate.qemuDisplayIsDynamicResolution && self.isDynamicResolutionSupported) { + if (!CGSizeEqualToSize(size, self.vmDisplay.displaySize)) { + [self requestResolutionChangeToSize:size]; + } + } }]; - if (self.delegate.qemuDisplayIsDynamicResolution) { - [self displayResize:size]; - } } - (void)enterSuspendedWithIsBusy:(BOOL)busy { @@ -161,8 +170,8 @@ [super enterLive]; self.prefersPointerLocked = YES; self.view.window.isIndirectPointerTouchIgnored = YES; - if (self.delegate.qemuDisplayIsDynamicResolution) { - [self displayResize:self.view.bounds.size]; + if (self.delegate.qemuDisplayIsDynamicResolution && self.isDynamicResolutionSupported) { + [self requestResolutionChangeToSize:self.view.bounds.size]; } if (self.delegate.qemuHasClipboardSharing) { [[UTMPasteboard generalPasteboard] requestPollingModeForObject:self]; @@ -200,11 +209,21 @@ return size; } -- (void)displayResize:(CGSize)size { - UTMLog(@"resizing to (%f, %f)", size.width, size.height); - size = [self convertSizeToNative:size]; - CGRect bounds = CGRectMake(0, 0, size.width, size.height); - [self.vmDisplay requestResolution:bounds]; +- (void)requestResolutionChangeToSize:(CGSize)size { + self.debounceResize = [self debounce:kResizeDebounceSecs context:self.debounceResize action:^{ + UTMLog(@"DISPLAY: requesting resolution (%f, %f)", size.width, size.height); + CGSize newSize = [self convertSizeToNative:size]; + CGRect bounds = CGRectMake(0, 0, newSize.width, newSize.height); + self.debounceResize = nil; +#if defined(TARGET_OS_VISION) && TARGET_OS_VISION + self.cancelResize = [self debounce:kResizeTimeoutSecs context:self.cancelResize action:^{ + self.cancelResize = nil; + UTMLog(@"DISPLAY: requesting resolution cancelled"); + [self resizeWindowToDisplaySize]; + }]; +#endif + [self.vmDisplay requestResolution:bounds]; + }]; } - (void)setVmDisplay:(CSDisplay *)display { @@ -217,8 +236,6 @@ - (void)setDisplayScaling:(CGFloat)scaling origin:(CGPoint)origin { self.vmDisplay.viewportOrigin = origin; - self.windowScaling = scaling; - self.windowOrigin = origin; if (!self.delegate.qemuDisplayIsNativeResolution) { scaling = CGPointToPixel(scaling); } @@ -229,25 +246,67 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { if ([keyPath isEqualToString:@"vmDisplay.displaySize"]) { -#if defined(TARGET_OS_VISION) && TARGET_OS_VISION - dispatch_async(dispatch_get_main_queue(), ^{ - CGSize minSize = self.vmDisplay.displaySize; - if (self.delegate.qemuDisplayIsNativeResolution) { - minSize.width = CGPixelToPoint(minSize.width); - minSize.height = CGPixelToPoint(minSize.height); - } - CGSize displaySize = CGSizeMake(minSize.width * self.windowScaling, minSize.height * self.windowScaling); - CGSize maxSize = CGSizeMake(UIProposedSceneSizeNoPreference, UIProposedSceneSizeNoPreference); - UIWindowSceneGeometryPreferencesVision *geoPref = [[UIWindowSceneGeometryPreferencesVision alloc] initWithSize:displaySize]; - geoPref.minimumSize = minSize; - geoPref.maximumSize = maxSize; - geoPref.resizingRestrictions = UIWindowSceneResizingRestrictionsUniform; - [self.view.window.windowScene requestGeometryUpdateWithPreferences:geoPref errorHandler:nil]; - }); -#else - [self.delegate display:self.vmDisplay didResizeTo:self.vmDisplay.displaySize]; -#endif + UTMLog(@"DISPLAY: vmDisplay.displaySize changed"); + if (self.cancelResize) { + [self debounce:0 context:self.cancelResize action:^{}]; + self.cancelResize = nil; + } + self.debounceResize = [self debounce:kResizeDebounceSecs context:self.debounceResize action:^{ + [self resizeWindowToDisplaySize]; + }]; } } +- (void)setIsDynamicResolutionSupported:(BOOL)isDynamicResolutionSupported { + if (_isDynamicResolutionSupported != isDynamicResolutionSupported) { + _isDynamicResolutionSupported = isDynamicResolutionSupported; + UTMLog(@"DISPLAY: isDynamicResolutionSupported = %d", isDynamicResolutionSupported); + if (self.delegate.qemuDisplayIsDynamicResolution) { + if (isDynamicResolutionSupported) { + [self requestResolutionChangeToSize:self.view.bounds.size]; + } else { + [self resizeWindowToDisplaySize]; + } + } + } +} + +- (void)resizeWindowToDisplaySize { + CGSize displaySize = self.vmDisplay.displaySize; + UTMLog(@"DISPLAY: request window resize to (%f, %f)", displaySize.width, displaySize.height); +#if defined(TARGET_OS_VISION) && TARGET_OS_VISION + CGSize minSize = displaySize; + if (self.delegate.qemuDisplayIsNativeResolution) { + minSize.width = CGPixelToPoint(minSize.width); + minSize.height = CGPixelToPoint(minSize.height); + } + CGSize maxSize = CGSizeMake(UIProposedSceneSizeNoPreference, UIProposedSceneSizeNoPreference); + UIWindowSceneGeometryPreferencesVision *geoPref = [[UIWindowSceneGeometryPreferencesVision alloc] initWithSize:minSize]; + if (self.delegate.qemuDisplayIsDynamicResolution && self.isDynamicResolutionSupported) { + geoPref.minimumSize = CGSizeMake(800, 600); + geoPref.maximumSize = maxSize; + geoPref.resizingRestrictions = UIWindowSceneResizingRestrictionsFreeform; + } else { + geoPref.minimumSize = minSize; + geoPref.maximumSize = maxSize; + geoPref.resizingRestrictions = UIWindowSceneResizingRestrictionsUniform; + } + dispatch_async(dispatch_get_main_queue(), ^{ + CGSize currentViewSize = self.view.bounds.size; + UTMLog(@"DISPLAY: old view size = (%f, %f)", currentViewSize.width, currentViewSize.height); + if (CGSizeEqualToSize(minSize, currentViewSize)) { + // since `-viewWillTransitionToSize:withTransitionCoordinator:` is not called + self.delegate.displayViewSize = [self convertSizeToNative:currentViewSize]; + [self.delegate display:self.vmDisplay didResizeTo:displaySize]; + } + [self.view.window.windowScene requestGeometryUpdateWithPreferences:geoPref errorHandler:nil]; + }); +#else + if (CGSizeEqualToSize(displaySize, CGSizeZero)) { + return; + } + [self.delegate display:self.vmDisplay didResizeTo:displaySize]; +#endif +} + @end diff --git a/Platform/iOS/Display/VMDisplayViewController.swift b/Platform/iOS/Display/VMDisplayViewController.swift index 9991faa8..3c368a9a 100644 --- a/Platform/iOS/Display/VMDisplayViewController.swift +++ b/Platform/iOS/Display/VMDisplayViewController.swift @@ -55,7 +55,7 @@ public extension VMDisplayViewController { parent.setChildViewControllerForPointerLock(self) UIPress.pressResponderOverride = self } - #if !os(visionOS) + #if !os(visionOS) && !WITH_REMOTE if runInBackground { logger.info("Start location tracking to enable running in background") UTMLocationManager.sharedInstance().startUpdatingLocation() @@ -75,24 +75,6 @@ public extension VMDisplayViewController { func enterLive() { UIApplication.shared.isIdleTimerDisabled = disableIdleTimer } - - private func suspend() { - // dummy function for selector - } - - func terminateApplication() { - DispatchQueue.main.async { [self] in - // animate to home screen - let app = UIApplication.shared - app.performSelector(onMainThread: #selector(suspend), with: nil, waitUntilDone: true) - - // wait 2 seconds while app is going background - Thread.sleep(forTimeInterval: 2) - - // exit app when app is in background - exit(0); - } - } } // MARK: Toolbar hiding @@ -134,4 +116,15 @@ public extension VMDisplayViewController { func integerForSetting(_ key: String) -> Int { return UserDefaults.standard.integer(forKey: key) } + + @discardableResult + func debounce(_ delaySeconds: Int, context: Any? = nil, action: @escaping () -> Void) -> Any { + if context != nil { + let previous = context as! DispatchWorkItem + previous.cancel() + } + let item = DispatchWorkItem(block: action) + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(delaySeconds), execute: item) + return item + } } diff --git a/Platform/iOS/Display/VMKeyboardView.m b/Platform/iOS/Display/VMKeyboardView.m index 07b15046..7cbc0afc 100644 --- a/Platform/iOS/Display/VMKeyboardView.m +++ b/Platform/iOS/Display/VMKeyboardView.m @@ -370,7 +370,7 @@ static int indexForExtChar(const ext_key_mapping_t *table, size_t table_len, cha - (void)insertUTF8Sequence:(const char *)ctext { unsigned long ctext_len = strlen(ctext); - UTMLog(@"ctext length=%lu\n", ctext_len); + //UTMLog(@"ctext length=%lu\n", ctext_len); unsigned char tc = ctext[0]; int keycode = 0; @@ -393,7 +393,7 @@ static int indexForExtChar(const ext_key_mapping_t *table, size_t table_len, cha switch (ctext_len) { case 1: - UTMLog(@"char=%d\n", tc); + //UTMLog(@"char=%d\n", tc); index = indexForChar(_map, _map_len, tc); if (index != -1) { keycode = _map[index].key; @@ -401,8 +401,8 @@ static int indexForExtChar(const ext_key_mapping_t *table, size_t table_len, cha } break; case 2: - UTMLog(@"char=%d\n", tc); - UTMLog(@"ext1=%d\n", (unsigned char) ctext[1]); + //UTMLog(@"char=%d\n", tc); + //UTMLog(@"ext1=%d\n", (unsigned char) ctext[1]); index = indexForExtChar(_ext_map, _ext_map_len, tc, ctext[1], 0); if (index != -1) { keycode = _ext_map[index].key; @@ -412,9 +412,9 @@ static int indexForExtChar(const ext_key_mapping_t *table, size_t table_len, cha } break; case 3: - UTMLog(@"char=%d\n", tc); - UTMLog(@"ext1=%d\n", (unsigned char) ctext[1]); - UTMLog(@"ext2=%d\n", (unsigned char) ctext[2]); + //UTMLog(@"char=%d\n", tc); + //UTMLog(@"ext1=%d\n", (unsigned char) ctext[1]); + //UTMLog(@"ext2=%d\n", (unsigned char) ctext[2]); index = indexForExtChar(_ext_map, _ext_map_len, tc, ctext[1], ctext[2]); if (index != -1) { keycode = _ext_map[index].key; diff --git a/Platform/iOS/Info-Remote.plist b/Platform/iOS/Info-Remote.plist new file mode 100644 index 00000000..98f527f5 --- /dev/null +++ b/Platform/iOS/Info-Remote.plist @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> + <key>CFBundleShortVersionString</key> + <string>$(MARKETING_VERSION)</string> + <key>CFBundleVersion</key> + <string>$(CURRENT_PROJECT_VERSION)</string> + <key>ITSAppUsesNonExemptEncryption</key> + <false/> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>NSAppTransportSecurity</key> + <dict> + <key>NSAllowsArbitraryLoads</key> + <true/> + </dict> + <key>NSBonjourServices</key> + <array> + <string>_utm_server._tcp</string> + </array> + <key>NSLocalNetworkUsageDescription</key> + <string>UTM uses the local network to find and connect to UTM Remote servers.</string> + <key>NSMicrophoneUsageDescription</key> + <string>Permission is required for any virtual machine to record from the microphone.</string> + <key>UIApplicationSupportsIndirectInputEvents</key> + <true/> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>arm64</string> + </array> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>UISupportedInterfaceOrientations~ipad</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> + <key>UIViewControllerBasedStatusBarAppearance</key> + <true/> + <key>UIApplicationSceneManifest</key> + <dict> + <key>UIApplicationSupportsMultipleScenes</key> + <true/> + <key>UISceneConfigurations</key> + <dict> + <key>UIWindowSceneSessionRoleExternalDisplay</key> + <array> + <dict> + <key>UISceneDelegateClassName</key> + <string>$(PRODUCT_MODULE_NAME).UTMExternalSceneDelegate</string> + <key>UISceneConfigurationName</key> + <string>External</string> + </dict> + </array> + </dict> + </dict> +</dict> +</plist> diff --git a/Platform/iOS/RemoteContentView.swift b/Platform/iOS/RemoteContentView.swift new file mode 100644 index 00000000..05a092ed --- /dev/null +++ b/Platform/iOS/RemoteContentView.swift @@ -0,0 +1,32 @@ +// +// Copyright © 2024 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 RemoteContentView: View { + @ObservedObject var remoteClientState: UTMRemoteClient.State + @EnvironmentObject private var data: UTMRemoteData + + var body: some View { + if remoteClientState.isConnected { + ContentView() + .environmentObject(data as UTMData) + } else { + UTMRemoteConnectView(remoteClientState: remoteClientState) + .transition(.move(edge: .leading)) + } + } +} diff --git a/Platform/iOS/Settings.bundle/Root.plist b/Platform/iOS/Settings.bundle/Root.plist index d282f587..75619e06 100644 --- a/Platform/iOS/Settings.bundle/Root.plist +++ b/Platform/iOS/Settings.bundle/Root.plist @@ -21,6 +21,12 @@ <string>RunInBackground</string> <key>DefaultValue</key> <false/> + <key>ExcludeTargets</key> + <array> + <string>iOS-Remote</string> + </array> + <key>Platform</key> + <string>iOS</string> </dict> <dict> <key>Type</key> @@ -31,6 +37,8 @@ <string>AutosaveBackground</string> <key>DefaultValue</key> <true/> + <key>Platform</key> + <string>iOS</string> </dict> <dict> <key>Type</key> @@ -83,6 +91,11 @@ <string>NoUsbPrompt</string> <key>DefaultValue</key> <false/> + <key>ExcludeTargets</key> + <array> + <string>iOS-Remote</string> + <string>iOS-SE</string> + </array> </dict> <dict> <key>Type</key> @@ -99,6 +112,10 @@ <string>PSGroupSpecifier</string> <key>Title</key> <string>Graphics</string> + <key>ExcludeTargets</key> + <array> + <string>iOS-Remote</string> + </array> </dict> <dict> <key>Type</key> @@ -121,6 +138,10 @@ <integer>1</integer> <integer>2</integer> </array> + <key>ExcludeTargets</key> + <array> + <string>iOS-Remote</string> + </array> </dict> <dict> <key>Type</key> @@ -155,6 +176,10 @@ <integer>105</integer> <integer>120</integer> </array> + <key>ExcludeTargets</key> + <array> + <string>iOS-Remote</string> + </array> </dict> <dict> <key>Type</key> @@ -2789,6 +2814,11 @@ <string>PSGroupSpecifier</string> <key>Title</key> <string>JitStreamer</string> + <key>ExcludeTargets</key> + <array> + <string>iOS-Remote</string> + <string>iOS-SE</string> + </array> </dict> <dict> <key>Type</key> @@ -2799,6 +2829,11 @@ <string>JitStreamerAttach</string> <key>DefaultValue</key> <false/> + <key>ExcludeTargets</key> + <array> + <string>iOS-Remote</string> + <string>iOS-SE</string> + </array> </dict> <dict> <key>Type</key> @@ -2809,6 +2844,11 @@ <string>JitStreamerAddress</string> <key>DefaultValue</key> <string>69.69.0.1</string> + <key>ExcludeTargets</key> + <array> + <string>iOS-Remote</string> + <string>iOS-SE</string> + </array> </dict> <dict> <key>Type</key> diff --git a/Platform/iOS/UTMDataExtension.swift b/Platform/iOS/UTMDataExtension.swift index 6d43fec0..3c109a48 100644 --- a/Platform/iOS/UTMDataExtension.swift +++ b/Platform/iOS/UTMDataExtension.swift @@ -19,15 +19,23 @@ import SwiftUI extension UTMData { func run(vm: VMData, options: UTMVirtualMachineStartOptions = []) { + #if WITH_SOLO_VM guard VMSessionState.allActiveSessions.count == 0 else { logger.error("Session already started") return } + #endif guard let wrapped = vm.wrapped else { return } - let session = VMSessionState(for: wrapped as! UTMQemuVirtualMachine) - session.start() + if let session = VMSessionState.allActiveSessions.values.first(where: { $0.vm.id == wrapped.id }) { + session.showWindow() + } else if vm.isStopped || vm.isTakeoverAllowed { + let session = VMSessionState(for: wrapped as! (any UTMSpiceVirtualMachine)) + session.start(options: options) + } else { + showErrorAlert(message: NSLocalizedString("This virtual machine is already running. In order to run it from this device, you must stop it first.", comment: "UTMDataExtension")) + } } func stop(vm: VMData) { @@ -37,6 +45,7 @@ extension UTMData { if wrapped.registryEntry.isSuspended { wrapped.requestVmDeleteState() } + wrapped.requestVmStop() } func close(vm: VMData) { diff --git a/Platform/iOS/UTMRemoteConnectView.swift b/Platform/iOS/UTMRemoteConnectView.swift new file mode 100644 index 00000000..6dc9ed6c --- /dev/null +++ b/Platform/iOS/UTMRemoteConnectView.swift @@ -0,0 +1,300 @@ +// +// Copyright © 2023 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 + +private let kTimeoutSeconds: UInt64 = 15 + +struct UTMRemoteConnectView: View { + @ObservedObject var remoteClientState: UTMRemoteClient.State + @Environment(\.openURL) private var openURL + @EnvironmentObject private var data: UTMRemoteData + @State private var selectedServer: UTMRemoteClient.State.SavedServer? + @State private var isAutoConnect: Bool = false + + private var remoteClient: UTMRemoteClient { + data.remoteClient + } + + var body: some View { + VStack { + HStack { + ProgressView().progressViewStyle(.circular) + Spacer() + Text("Select a UTM Server") + .font(.headline) + Spacer() + Button { + openURL(URL(string: "https://docs.getutm.app/remote/")!) + } label: { + Label("Help", systemImage: "questionmark.circle") + .labelStyle(.iconOnly) + .font(.title2) + } + Button { + selectedServer = .init() + } label: { + Label("New Connection", systemImage: "plus") + .labelStyle(.iconOnly) + .font(.title2) + } + }.padding() + List { + if remoteClientState.savedServers.count > 0 { + Section(header: Text("Saved")) { + ForEach(remoteClientState.savedServers) { server in + Button { + isAutoConnect = true + selectedServer = server + } label: { + MacDeviceLabel(server.name.isEmpty ? server.hostname : server.name, device: .init(model: server.model)) + }.disabled(!server.isAvailable) + .contextMenu { + Button { + isAutoConnect = false + selectedServer = server + } label: { + Label("Edit…", systemImage: "slider.horizontal.3") + } + DestructiveButton("Delete") { + remoteClientState.delete(server: server) + Task { + await remoteClient.refresh() + } + } + } + }.onDelete { indexSet in + remoteClientState.savedServers.remove(atOffsets: indexSet) + Task { + await remoteClient.refresh() + } + } + } + } + Section(header: Text("Discovered"), footer: helpText) { + ForEach(remoteClientState.foundServers) { server in + Button { + isAutoConnect = true + selectedServer = UTMRemoteClient.State.SavedServer(from: server) + } label: { + MacDeviceLabel(server.name, device: .init(model: server.model)) + } + } + } + }.listStyle(.insetGrouped) + }.alert(item: $remoteClientState.alertMessage) { item in + Alert(title: Text(item.message)) + } + .sheet(item: $selectedServer) { server in + ServerConnectView(remoteClientState: remoteClientState, server: server, isAutoConnect: $isAutoConnect) + } + .onAppear { + Task { + await remoteClient.startScanning() + } + } + .onDisappear { + Task { + await remoteClient.stopScanning() + } + } + } + + @ViewBuilder + private var helpText: some View { + if remoteClientState.foundServers.isEmpty { + Text("Make sure the latest version of UTM is running on your Mac and UTM Server is enabled. You can download UTM from the Mac App Store.") + } + } +} + +private struct ServerConnectView: View { + @ObservedObject var remoteClientState: UTMRemoteClient.State + @State var server: UTMRemoteClient.State.SavedServer + @Binding var isAutoConnect: Bool + + @EnvironmentObject private var data: UTMRemoteData + @Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode> + + @State private var connectionTask: Task<Void, Error>? + private var isConnecting: Bool { + connectionTask != nil + } + @State private var isPasswordRequired: Bool = false + @State private var isTrustButton: Bool = false + + private var remoteClient: UTMRemoteClient { + data.remoteClient + } + + var body: some View { + NavigationView { + Form { + Section { + if #available(iOS 15, *) { + TextField("", text: $server.name, prompt: Text("Name (optional)")) + } else { + DefaultTextField("", text: $server.name, prompt: "Name (optional)") + } + } header: { + Text("Name") + } + Section { + if server.endpoint != nil { + Text(server.hostname) + } else { + if #available(iOS 15, *) { + TextField("", text: $server.hostname, prompt: Text("Hostname or IP address")) + .keyboardType(.asciiCapable) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + TextField("", value: $server.port, format: .number.grouping(.never), prompt: Text("Port")) + .keyboardType(.decimalPad) + } else { + DefaultTextField("", text: $server.hostname, prompt: "Hostname or IP address") + .keyboardType(.asciiCapable) + .autocorrectionDisabled() + NumberTextField("", number: $server.port, prompt: "Port") + } + } + } header: { + Text("Host") + } + let fingerprint = (server.fingerprint ^ remoteClient.fingerprint).hexString() + if !fingerprint.isEmpty { + Section { + if #available(iOS 16.4, *) { + Text(fingerprint).monospaced() + } else { + Text(fingerprint) + } + } header: { + Text("Fingerprint") + } + } + if isPasswordRequired { + Section { + if #available(iOS 15, *) { + FocusedPasswordView(password: $server.password.bound) + } else { + SecureField("Password", text: $server.password.bound) + } + Toggle("Save Password", isOn: $server.shouldSavePassword) + } header: { + Text("Password") + } + } + }.disabled(isConnecting) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button { + presentationMode.wrappedValue.dismiss() + } label: { + Text("Close") + }.disabled(isConnecting) + } + ToolbarItem(placement: .topBarTrailing) { + HStack { + if isConnecting { + ProgressView().progressViewStyle(.circular) + Button { + connectionTask?.cancel() + } label: { + Text("Cancel") + } + } else { + Button { + connect() + } label: { + if isTrustButton { + Text("Trust") + } else { + Text("Connect") + } + }.disabled(server.hostname.isEmpty || !server.isAvailable) + } + } + } + } + } + .onAppear { + // if we have an existing password, assume it should be saved + if server.password?.isEmpty == false { + server.shouldSavePassword = true + } + if isAutoConnect { + connect() + } + } + .alert(item: $remoteClientState.alertMessage) { item in + Alert(title: Text(item.message)) + } + } + + private func connect() { + guard connectionTask == nil else { + return + } + connectionTask = Task { + let timeoutTask = Task { + try await Task.sleep(nanoseconds: kTimeoutSeconds * NSEC_PER_SEC) + connectionTask?.cancel() + remoteClientState.showErrorAlert(NSLocalizedString("Timed out trying to connect.", comment: "UTMRemoteConnectView")) + } + do { + try await remoteClient.connect(server) + } catch { + if case UTMRemoteClient.ConnectionError.passwordRequired = error { + withAnimation { + isPasswordRequired = true + isTrustButton = false + } + } else if case UTMRemoteClient.ConnectionError.fingerprintUntrusted(let fingerprint) = error, server.fingerprint.isEmpty { + withAnimation { + server.fingerprint = fingerprint + isTrustButton = true + } + remoteClientState.showErrorAlert(error.localizedDescription) + } else if error is CancellationError { + // ignore it + } else { + remoteClientState.showErrorAlert(error.localizedDescription) + } + } + timeoutTask.cancel() + connectionTask = nil + } + } +} + +@available(iOS 15, *) +private struct FocusedPasswordView: View { + @Binding var password: String + + @FocusState private var isFocused: Bool + + var body: some View { + SecureField("Password", text: $password) + .focused($isFocused) + .onAppear { + isFocused = true + } + } +} + +#Preview { + UTMRemoteConnectView(remoteClientState: .init()) +} diff --git a/Platform/iOS/UTMSettingsView.swift b/Platform/iOS/UTMSettingsView.swift index c65d0fa8..11621fec 100644 --- a/Platform/iOS/UTMSettingsView.swift +++ b/Platform/iOS/UTMSettingsView.swift @@ -19,12 +19,20 @@ import SwiftUI struct UTMSettingsView: View { @Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode> + private var hasContainer: Bool { + #if WITH_JIT + jb_has_container() + #else + true + #endif + } + var body: some View { NavigationView { IASKAppSettings() .navigationTitle("Settings") .navigationBarTitleDisplayMode(.inline) - .appSettingsShowPrivacyLink(jb_has_container()) + .appSettingsShowPrivacyLink(hasContainer) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Close") { diff --git a/Platform/iOS/UTMSingleWindowView.swift b/Platform/iOS/UTMSingleWindowView.swift index a369942b..419bcfa8 100644 --- a/Platform/iOS/UTMSingleWindowView.swift +++ b/Platform/iOS/UTMSingleWindowView.swift @@ -19,8 +19,12 @@ import SwiftUI @MainActor struct UTMSingleWindowView: View { let isInteractive: Bool - + + #if WITH_REMOTE + @State private var data: UTMRemoteData = UTMRemoteData() + #else @State private var data: UTMData = UTMData() + #endif @State private var session: VMSessionState? @State private var identifier: VMSessionState.WindowID? @@ -36,7 +40,11 @@ struct UTMSingleWindowView: View { if let session = session { VMWindowView(id: identifier!, isInteractive: isInteractive).environmentObject(session) } else if isInteractive { + #if WITH_REMOTE + RemoteContentView(remoteClientState: data.remoteClient.state).environmentObject(data) + #else ContentView().environmentObject(data) + #endif } else { VStack { Text("Waiting for VM to connect to display...") diff --git a/Platform/iOS/VMDisplayHostedView.swift b/Platform/iOS/VMDisplayHostedView.swift index c471816a..c8d7435c 100644 --- a/Platform/iOS/VMDisplayHostedView.swift +++ b/Platform/iOS/VMDisplayHostedView.swift @@ -19,7 +19,7 @@ import SwiftUI struct VMDisplayHostedView: UIViewControllerRepresentable { internal class Coordinator: VMDisplayViewControllerDelegate { - let vm: UTMQemuVirtualMachine + let vm: any UTMSpiceVirtualMachine let device: VMWindowState.Device @Binding var state: VMWindowState var vmStateCancellable: AnyCancellable? @@ -37,19 +37,19 @@ struct VMDisplayHostedView: UIViewControllerRepresentable { } @MainActor var qemuDisplayUpscaler: MTLSamplerMinMagFilter { - vmConfig.displays[state.device!.configIndex].upscalingFilter.metalSamplerMinMagFilter + vmConfig.displays[device.configIndex].upscalingFilter.metalSamplerMinMagFilter } @MainActor var qemuDisplayDownscaler: MTLSamplerMinMagFilter { - vmConfig.displays[state.device!.configIndex].downscalingFilter.metalSamplerMinMagFilter + vmConfig.displays[device.configIndex].downscalingFilter.metalSamplerMinMagFilter } @MainActor var qemuDisplayIsDynamicResolution: Bool { - vmConfig.displays[state.device!.configIndex].isDynamicResolution + vmConfig.displays[device.configIndex].isDynamicResolution } @MainActor var qemuDisplayIsNativeResolution: Bool { - vmConfig.displays[state.device!.configIndex].isNativeResolution + vmConfig.displays[device.configIndex].isNativeResolution } @MainActor var qemuHasClipboardSharing: Bool { @@ -57,7 +57,7 @@ struct VMDisplayHostedView: UIViewControllerRepresentable { } @MainActor var qemuConsoleResizeCommand: String? { - vmConfig.serials[state.device!.configIndex].terminal?.resizeCommand + vmConfig.serials[device.configIndex].terminal?.resizeCommand } var isViewportChanged: Bool { @@ -100,7 +100,7 @@ struct VMDisplayHostedView: UIViewControllerRepresentable { } } - init(with vm: UTMQemuVirtualMachine, device: VMWindowState.Device, state: Binding<VMWindowState>) { + init(with vm: any UTMSpiceVirtualMachine, device: VMWindowState.Device, state: Binding<VMWindowState>) { self.vm = vm self.device = device self._state = state @@ -131,7 +131,7 @@ struct VMDisplayHostedView: UIViewControllerRepresentable { } } - let vm: UTMQemuVirtualMachine + let vm: any UTMSpiceVirtualMachine let device: VMWindowState.Device @Binding var state: VMWindowState @@ -168,7 +168,12 @@ struct VMDisplayHostedView: UIViewControllerRepresentable { if let vc = uiViewController as? VMDisplayMetalViewController { vc.vmInput = session.primaryInput } - if state.isKeyboardShown != state.isKeyboardRequested { + #if os(visionOS) + let useSystemOsk = !(uiViewController is VMDisplayMetalViewController) + #else + let useSystemOsk = true + #endif + if useSystemOsk && state.isKeyboardShown != state.isKeyboardRequested { DispatchQueue.main.async { if state.isKeyboardRequested { uiViewController.showKeyboard() @@ -190,6 +195,7 @@ struct VMDisplayHostedView: UIViewControllerRepresentable { } // some obscure SwiftUI error means we cannot refer to Coordinator's state binding vc.setDisplayScaling(state.displayScale, origin: state.displayOrigin) + vc.isDynamicResolutionSupported = state.isDynamicResolutionSupported } case .serial(let serial, _): if let vc = uiViewController as? VMDisplayTerminalViewController { diff --git a/Platform/iOS/VMSessionState.swift b/Platform/iOS/VMSessionState.swift index 2c7ac20f..0805b635 100644 --- a/Platform/iOS/VMSessionState.swift +++ b/Platform/iOS/VMSessionState.swift @@ -37,21 +37,21 @@ import SwiftUI let id: ID = ID() - let vm: UTMQemuVirtualMachine - + let vm: any UTMSpiceVirtualMachine + var qemuConfig: UTMQemuConfiguration { vm.config } @Published var vmState: UTMVirtualMachineState = .stopped - @Published var fatalError: String? - @Published var nonfatalError: String? - + + @Published var fatalError: String? + @Published var primaryInput: CSInput? - #if !WITH_QEMU_TCI + #if WITH_USB private var primaryUsbManager: CSUSBManager? private var usbManagerQueue = DispatchQueue(label: "USB Manager Queue", qos: .utility) @@ -78,10 +78,12 @@ import SwiftUI @Published var externalWindowBinding: Binding<VMWindowState>? @Published var hasShownMemoryWarning: Bool = false - + + @Published var isDynamicResolutionSupported: Bool = false + private var hasAutosave: Bool = false - init(for vm: UTMQemuVirtualMachine) { + init(for vm: any UTMSpiceVirtualMachine) { self.vm = vm super.init() vm.delegate = self @@ -148,7 +150,7 @@ extension VMSessionState: UTMVirtualMachineDelegate { Task { @MainActor in vmState = state if state == .stopped { - #if !WITH_QEMU_TCI + #if WITH_USB clearDevices() #endif } @@ -157,7 +159,7 @@ extension VMSessionState: UTMVirtualMachineDelegate { nonisolated func virtualMachine(_ vm: any UTMVirtualMachine, didErrorWithMessage message: String) { Task { @MainActor in - fatalError = message + nonfatalError = message } } @@ -281,7 +283,7 @@ extension VMSessionState: UTMSpiceIODelegate { } } - #if !WITH_QEMU_TCI + #if WITH_USB nonisolated func spiceDidChangeUsbManager(_ usbManager: CSUSBManager?) { Task { @MainActor in primaryUsbManager?.delegate = nil @@ -291,9 +293,21 @@ extension VMSessionState: UTMSpiceIODelegate { } } #endif + + nonisolated func spiceDynamicResolutionSupportDidChange(_ supported: Bool) { + Task { @MainActor in + isDynamicResolutionSupported = supported + } + } + + nonisolated func spiceDidDisconnect() { + Task { @MainActor in + fatalError = NSLocalizedString("Connection to the server was lost.", comment: "VMSessionState") + } + } } -#if !WITH_QEMU_TCI +#if WITH_USB extension VMSessionState: CSUSBManagerDelegate { nonisolated func spiceUsbManager(_ usbManager: CSUSBManager, deviceError error: String, for device: CSUSBDevice) { Task { @MainActor in @@ -419,10 +433,18 @@ extension VMSessionState { logger.warning("Error starting audio session: \(error.localizedDescription)") } Self.allActiveSessions[id] = self - NotificationCenter.default.post(name: .vmSessionCreated, object: nil, userInfo: ["Session": self]) - vm.requestVmStart(options: options) + showWindow() + if vm.state == .paused { + vm.requestVmResume() + } else { + vm.requestVmStart(options: options) + } } - + + func showWindow() { + NotificationCenter.default.post(name: .vmSessionCreated, object: nil, userInfo: ["Session": self]) + } + @objc private func suspend() { // dummy function for selector } @@ -436,7 +458,9 @@ extension VMSessionState { } // tell other screens to shut down Self.allActiveSessions.removeValue(forKey: id) - NotificationCenter.default.post(name: .vmSessionEnded, object: nil, userInfo: ["Session": self]) + closeWindows() + + #if WITH_SOLO_VM // animate to home screen let app = UIApplication.shared app.performSelector(onMainThread: #selector(suspend), with: nil, waitUntilDone: true) @@ -446,12 +470,17 @@ extension VMSessionState { // exit app when app is in background exit(0) + #endif } - - func powerDown() { + + func closeWindows() { + NotificationCenter.default.post(name: .vmSessionEnded, object: nil, userInfo: ["Session": self]) + } + + func powerDown(isKill: Bool = false) { Task { try? await vm.deleteSnapshot(name: nil) - try await vm.stop(usingMethod: .force) + try await vm.stop(usingMethod: isKill ? .kill : .force) self.stop() } } @@ -482,6 +511,7 @@ extension VMSessionState { } func didEnterBackground() { + #if !os(visionOS) logger.info("Entering background") let shouldAutosaveBackground = UserDefaults.standard.bool(forKey: "AutosaveBackground") if shouldAutosaveBackground && vmState == .started { @@ -494,7 +524,7 @@ extension VMSessionState { } Task { do { - try await vm.saveSnapshot() + try await vm.saveSnapshot(name: nil) self.hasAutosave = true logger.info("Save snapshot complete") } catch { @@ -504,14 +534,17 @@ extension VMSessionState { task = .invalid } } + #endif } func didEnterForeground() { + #if !os(visionOS) logger.info("Entering foreground!") if (hasAutosave && vmState == .started) { logger.info("Deleting snapshot") vm.requestVmDeleteState() } + #endif } } diff --git a/Platform/iOS/VMToolbarDriveMenuView.swift b/Platform/iOS/VMToolbarDriveMenuView.swift index 6e649db5..c622da75 100644 --- a/Platform/iOS/VMToolbarDriveMenuView.swift +++ b/Platform/iOS/VMToolbarDriveMenuView.swift @@ -52,6 +52,7 @@ struct VMToolbarDriveMenuView: View { } ForEach(config.drives) { drive in if drive.isExternal { + #if !WITH_REMOTE // FIXME: implement remote feature Menu { Button { selectedDrive = drive @@ -68,6 +69,12 @@ struct VMToolbarDriveMenuView: View { } label: { MenuLabel(label(for: drive), systemImage: session.vm.externalImageURL(for: drive) == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill") } + #else + Button { + } label: { + MenuLabel(label(for: drive), systemImage: session.vm.externalImageURL(for: drive) == nil ? "opticaldiscdrive" : "opticaldiscdrive.fill") + }.disabled(true) + #endif } else if drive.imageType == .disk || drive.imageType == .cd { Button { } label: { diff --git a/Platform/iOS/VMToolbarView.swift b/Platform/iOS/VMToolbarView.swift index adaff2e1..5a9d2bfc 100644 --- a/Platform/iOS/VMToolbarView.swift +++ b/Platform/iOS/VMToolbarView.swift @@ -82,13 +82,17 @@ struct VMToolbarView: View { GeometryReader { geometry in Group { Button { - if session.vm.state == .started { + if state.isRunning { state.alert = .powerDown } else { state.alert = .terminateApp } } label: { - Label(state.isRunning ? "Power Off" : "Quit", systemImage: state.isRunning ? "power" : "xmark") + if state.isRunning { + Label("Power Off", systemImage: "power") + } else { + Label("Force Kill", systemImage: "xmark") + } }.offset(offset(for: 8)) Button { session.pauseResume() @@ -110,7 +114,7 @@ struct VMToolbarView: View { } label: { Label("Zoom", systemImage: state.isViewportChanged ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right") }.offset(offset(for: 5)) - #if !WITH_QEMU_TCI + #if WITH_USB if session.vm.hasUsbRedirection { VMToolbarUSBMenuView() .offset(offset(for: 4)) diff --git a/Platform/iOS/VMWindowState.swift b/Platform/iOS/VMWindowState.swift index c4db71d2..c9452fc8 100644 --- a/Platform/iOS/VMWindowState.swift +++ b/Platform/iOS/VMWindowState.swift @@ -71,6 +71,8 @@ struct VMWindowState: Identifiable { var isRunning: Bool = false var alert: Alert? + + var isDynamicResolutionSupported: Bool = false } // MARK: - VM action alerts @@ -82,7 +84,7 @@ extension VMWindowState { case .powerDown: return 0 case .terminateApp: return 1 case .restart: return 2 - #if !WITH_QEMU_TCI + #if WITH_USB case .deviceConnected(_): return 3 #endif case .nonfatalError(_): return 4 @@ -94,7 +96,7 @@ extension VMWindowState { case powerDown case terminateApp case restart - #if !WITH_QEMU_TCI + #if WITH_USB case deviceConnected(CSUSBDevice) #endif case nonfatalError(String) diff --git a/Platform/iOS/VMWindowView.swift b/Platform/iOS/VMWindowView.swift index 2a55b676..ce4e1b82 100644 --- a/Platform/iOS/VMWindowView.swift +++ b/Platform/iOS/VMWindowView.swift @@ -16,6 +16,9 @@ import SwiftUI import SwiftUIVisualEffects +#if os(visionOS) +import VisionKeyboardKit +#endif struct VMWindowView: View { let id: VMSessionState.WindowID @@ -24,7 +27,10 @@ struct VMWindowView: View { @State private var state: VMWindowState @EnvironmentObject private var session: VMSessionState @Environment(\.scenePhase) private var scenePhase - + #if os(visionOS) + @Environment(\.dismissWindow) private var dismissWindow + #endif + private let keyboardDidShowNotification = NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification) private let keyboardDidHideNotification = NotificationCenter.default.publisher(for: UIResponder.keyboardDidHideNotification) private let didReceiveMemoryWarningNotification = NotificationCenter.default.publisher(for: UIApplication.didReceiveMemoryWarningNotification) @@ -108,13 +114,13 @@ struct VMWindowView: View { }, secondaryButton: .cancel(Text("No"))) case .terminateApp: return Alert(title: Text("Are you sure you want to exit UTM?"), primaryButton: .destructive(Text("Yes")) { - session.stop() + session.powerDown(isKill: true) }, secondaryButton: .cancel(Text("No"))) case .restart: return Alert(title: Text("Are you sure you want to reset this VM? Any unsaved changes will be lost."), primaryButton: .destructive(Text("Yes")) { session.reset() }, secondaryButton: .cancel(Text("No"))) - #if !WITH_QEMU_TCI + #if WITH_USB case .deviceConnected(let device): return Alert(title: Text("Would you like to connect '\(device.name ?? device.description)' to this virtual machine?"), primaryButton: .default(Text("Yes")) { session.mostRecentConnectedDevice = nil @@ -127,6 +133,8 @@ struct VMWindowView: View { return Alert(title: Text(message), dismissButton: .cancel(Text("OK")) { if case .fatalError(_) = type { session.stop() + } else if session.vmState == .stopped { + session.stop() } else { session.nonfatalError = nil } @@ -151,7 +159,7 @@ struct VMWindowView: View { state.saveWindow(to: session.vm.registryEntry, device: oldDevice) state.restoreWindow(from: session.vm.registryEntry, device: newDevice) } - #if !WITH_QEMU_TCI + #if WITH_USB .onChange(of: session.mostRecentConnectedDevice) { newValue in if session.activeWindow == state.id, let device = newValue { state.alert = .deviceConnected(device) @@ -171,6 +179,9 @@ struct VMWindowView: View { .onChange(of: session.vmState) { [oldValue = session.vmState] newValue in vmStateUpdated(from: oldValue, to: newValue) } + .onChange(of: session.isDynamicResolutionSupported) { newValue in + state.isDynamicResolutionSupported = newValue + } .onReceive(keyboardDidShowNotification) { _ in state.isKeyboardShown = true state.isKeyboardRequested = true @@ -202,12 +213,30 @@ struct VMWindowView: View { if !isInteractive { session.externalWindowBinding = $state } + state.isDynamicResolutionSupported = session.isDynamicResolutionSupported + // in case an alert appeared before we created the view + if session.activeWindow == state.id { + #if WITH_USB + if let device = session.mostRecentConnectedDevice { + state.alert = .deviceConnected(device) + } + #endif + if let nonfatalError = session.nonfatalError { + state.alert = .nonfatalError(nonfatalError) + } + if let fatalError = session.fatalError { + state.alert = .fatalError(fatalError) + } + } } .onDisappear { session.removeWindow(state.id) if !isInteractive { session.externalWindowBinding = nil } + #if os(visionOS) + dismissWindow(keyboardFor: state.id) + #endif } } @@ -221,9 +250,12 @@ struct VMWindowView: View { state.isBusy = false state.isRunning = false } + // do not close if we have a popup open DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { - if session.vmState == .stopped && session.fatalError == nil { - session.stop() + if session.nonfatalError == nil && session.fatalError == nil { + if session.vmState == .stopped { + session.stop() + } } } case .pausing, .stopping, .starting, .resuming, .saving, .restoring: diff --git a/Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift b/Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift index c1ac6645..45d5b533 100644 --- a/Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift +++ b/Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift @@ -44,7 +44,7 @@ class VMDisplayAppleTerminalWindowController: VMDisplayAppleWindowController, VM private var isSizeChangeIgnored: Bool = true @Setting("OptionAsMetaKey") var isOptionAsMetaKey: Bool = false - convenience init(primaryForIndex index: Int, vm: UTMAppleVirtualMachine, onClose: ((Notification) -> Void)?) { + convenience init(primaryForIndex index: Int, vm: UTMAppleVirtualMachine, onClose: (() -> Void)?) { self.init(vm: vm, onClose: onClose) self.index = index } diff --git a/Platform/macOS/Display/VMDisplayAppleWindowController.swift b/Platform/macOS/Display/VMDisplayAppleWindowController.swift index acbacd5e..8b2c3bcd 100644 --- a/Platform/macOS/Display/VMDisplayAppleWindowController.swift +++ b/Platform/macOS/Display/VMDisplayAppleWindowController.swift @@ -257,9 +257,9 @@ extension VMDisplayAppleWindowController { } extension VMDisplayAppleWindowController: UTMScreenshotProvider { - var screenshot: PlatformImage? { + var screenshot: UTMVirtualMachineScreenshot? { if let image = mainView?.image() { - return image + return UTMVirtualMachineScreenshot(wrapping: image) } else { return nil } diff --git a/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift b/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift index eaec9a91..124b8180 100644 --- a/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift +++ b/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift @@ -149,7 +149,7 @@ class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController { override func enterSuspended(isBusy busy: Bool) { if !busy { metalView.isHidden = true - screenshotView.image = vm.screenshot + screenshotView.image = vm.screenshot?.image screenshotView.isHidden = false } if vm.state == .stopped { diff --git a/Platform/macOS/Display/VMDisplayWindowController.swift b/Platform/macOS/Display/VMDisplayWindowController.swift index eb40ce4c..7686b008 100644 --- a/Platform/macOS/Display/VMDisplayWindowController.swift +++ b/Platform/macOS/Display/VMDisplayWindowController.swift @@ -38,7 +38,7 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate { var shouldAutoStartVM: Bool = true var vm: (any UTMVirtualMachine)! - var onClose: ((Notification) -> Void)? + var onClose: (() -> Void)? private(set) var secondaryWindows: [VMDisplayWindowController] = [] private(set) weak var primaryWindow: VMDisplayWindowController? private var preventIdleSleepAssertion: IOPMAssertionID? @@ -60,7 +60,7 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate { self } - convenience init(vm: any UTMVirtualMachine, onClose: ((Notification) -> Void)?) { + convenience init(vm: any UTMVirtualMachine, onClose: (() -> Void)?) { self.init(window: nil) self.vm = vm self.onClose = onClose @@ -236,7 +236,7 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate { func registerSecondaryWindow(_ secondaryWindow: VMDisplayWindowController, at index: Int? = nil) { secondaryWindows.insert(secondaryWindow, at: index ?? secondaryWindows.endIndex) - secondaryWindow.onClose = { [weak self] _ in + secondaryWindow.onClose = { [weak self] in self?.secondaryWindows.removeAll(where: { $0 == secondaryWindow }) } secondaryWindow.primaryWindow = self @@ -367,7 +367,7 @@ extension VMDisplayWindowController: NSWindowDelegate { IOPMAssertionRelease(preventIdleSleepAssertion) } isFinalizing = true - onClose?(notification) + onClose?() } func windowDidBecomeKey(_ notification: Notification) { diff --git a/Platform/macOS/SettingsView.swift b/Platform/macOS/SettingsView.swift index 961ac1f3..4485a773 100644 --- a/Platform/macOS/SettingsView.swift +++ b/Platform/macOS/SettingsView.swift @@ -37,6 +37,10 @@ struct SettingsView: View { .tabItem { Label("Input", systemImage: "keyboard") } + ServerSettingsView().padding() + .tabItem { + Label("Server", systemImage: "server.rack") + } }.frame(minWidth: 600, minHeight: 350, alignment: .topLeading) } } @@ -181,6 +185,65 @@ struct InputSettingsView: View { } } +struct ServerSettingsView: View { + private let defaultPort = 21589 + + @AppStorage("ServerAutostart") var isServerAutostart: Bool = false + @AppStorage("ServerExternal") var isServerExternal: Bool = false + @AppStorage("ServerAutoblock") var isServerAutoblock: Bool = false + @AppStorage("ServerPort") var serverPort: Int = 0 + @AppStorage("ServerPasswordRequired") var isServerPasswordRequired: Bool = false + @AppStorage("ServerPassword") var serverPassword: String = "" + + // note it is okay to store the server password in plaintext in the settings plist because if the attacker is able to see the password, + // they can gain execution in UTM application context... which is the context needed to read the password. + + var body: some View { + Form { + Section(header: Text("Startup")) { + Toggle("Automatically start UTM server", isOn: $isServerAutostart) + } + Section(header: Text("Network")) { + Toggle("Reject unknown connections by default", isOn: $isServerAutoblock) + .help("If checked, you will not be prompted about any unknown connection and they will be rejected.") + Toggle("Allow access from external clients", isOn: $isServerExternal) + .help("By default, the server is only available on LAN but setting this will use UPnP/NAT-PMP to port forward to WAN.") + .onChange(of: isServerExternal) { newValue in + if newValue { + if serverPort == 0 { + serverPort = defaultPort + } + if !isServerPasswordRequired { + isServerPasswordRequired = true + } + } + } + NumberTextField("", number: $serverPort, prompt: "Any") + .frame(width: 80) + .multilineTextAlignment(.trailing) + .help("Specify a port number to listen on. This is required if external clients are permitted.") + .onChange(of: serverPort) { newValue in + if serverPort == 0 { + isServerExternal = false + } + } + } + Section(header: Text("Authentication")) { + Toggle("Require Password", isOn: $isServerPasswordRequired) + .disabled(isServerExternal) + .help("If enabled, clients must enter a password. This is required if you want to access the server externally.") + .onChange(of: isServerPasswordRequired) { newValue in + if newValue && serverPassword.count == 0 { + serverPassword = .random(length: 32) + } + } + TextField("Password", text: $serverPassword) + .disabled(!isServerPasswordRequired) + } + } + } +} + extension UserDefaults { @objc dynamic var KeepRunningAfterLastWindowClosed: Bool { false } @objc dynamic var ShowMenuIcon: Bool { false } diff --git a/Platform/macOS/UTMApp.swift b/Platform/macOS/UTMApp.swift index 3e70c479..5e22ff4c 100644 --- a/Platform/macOS/UTMApp.swift +++ b/Platform/macOS/UTMApp.swift @@ -58,6 +58,9 @@ struct UTMApp: App { SettingsView() } UTMMenuBarExtraScene(data: data) + Window("UTM Server", id: "server") { + UTMServerView().environmentObject(data.remoteServer.state) + } } // HACK: SwiftUI doesn't provide if-statement support in SceneBuilder diff --git a/Platform/macOS/UTMDataExtension.swift b/Platform/macOS/UTMDataExtension.swift index 6c2f6659..3acdb861 100644 --- a/Platform/macOS/UTMDataExtension.swift +++ b/Platform/macOS/UTMDataExtension.swift @@ -22,7 +22,7 @@ extension UTMData { func run(vm: VMData, options: UTMVirtualMachineStartOptions = [], startImmediately: Bool = true) { var window: Any? = vmWindows[vm] if window == nil { - let close = { (notification: Notification) -> Void in + let close = { self.vmWindows.removeValue(forKey: vm) window = nil } @@ -76,6 +76,37 @@ extension UTMData { } } + /// Start a remote session and return SPICE server port. + /// - Parameters: + /// - vm: VM to start + /// - options: Start options + /// - server: Remote server + /// - Returns: Port number to SPICE server + func startRemote(vm: VMData, options: UTMVirtualMachineStartOptions, forClient client: UTMRemoteServer.Remote) async throws -> UTMRemoteMessageServer.StartVirtualMachine.ServerInformation { + guard let wrapped = vm.wrapped as? UTMQemuVirtualMachine, type(of: wrapped).capabilities.supportsRemoteSession else { + throw UTMDataError.unsupportedBackend + } + if let existingSession = vmWindows[vm] as? VMRemoteSessionState, let spiceServerInfo = wrapped.spiceServerInfo { + if wrapped.state == .paused { + try await wrapped.resume() + } + existingSession.client = client + return spiceServerInfo + } + guard vmWindows[vm] == nil else { + throw UTMDataError.virtualMachineUnavailable + } + let session = VMRemoteSessionState(for: wrapped, client: client) { + self.vmWindows.removeValue(forKey: vm) + } + try await wrapped.start(options: options.union(.remoteSession)) + vmWindows[vm] = session + guard let spiceServerInfo = wrapped.spiceServerInfo else { + throw UTMDataError.unsupportedBackend + } + return spiceServerInfo + } + func stop(vm: VMData) { guard let wrapped = vm.wrapped else { return diff --git a/Platform/macOS/UTMServerView.swift b/Platform/macOS/UTMServerView.swift new file mode 100644 index 00000000..9b6bee3c --- /dev/null +++ b/Platform/macOS/UTMServerView.swift @@ -0,0 +1,173 @@ +// +// Copyright © 2023 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 + +@available(macOS 13, *) +struct UTMServerView: View { + @EnvironmentObject private var remoteServer: UTMRemoteServer.State + @State private var isDeletingAll: Bool = false + + var body: some View { + VStack(alignment: .leading) { + HStack { + Toggle("Enable UTM Server", isOn: Binding<Bool>(get: { + remoteServer.isServerActive + }, set: { value in + if value { + remoteServer.requestServerAction(.start) + } else { + remoteServer.requestServerAction(.stop) + } + })) + Spacer() + Button { + isDeletingAll = true + } label: { + Text("Reset Identity") + } + .alert("Confirmation", isPresented: $isDeletingAll) { + Button(role: .destructive) { + remoteServer.allClients.removeAll() + remoteServer.requestServerAction(.reset) + } label: { + Text("Reset Identity") + }.keyboardShortcut(.defaultAction) + } message: { + Text("Do you want to forget all clients and generate a new server identity? Any clients that previously paired with this server will be instructed to manually unpair with this server before they can connect again.") + } + }.padding([.top, .leading, .trailing]) + ServerOverview() + Divider() + HStack { + if let address = remoteServer.externalIPAddress, let port = remoteServer.externalPort { + Text("Server IP: \(address), Port: \(String(port))") + .textSelection(.enabled) + } + Spacer() + if remoteServer.isServerActive { + Image(systemName: "circle.fill") + .foregroundStyle(.green) + Text("Running") + } else { + Image(systemName: "circle.fill") + .foregroundStyle(.red) + Text("Stopped") + } + }.padding([.bottom, .leading, .trailing]) + }.disabled(remoteServer.isBusy) + } +} + +@available(macOS 13, *) +fileprivate struct ServerOverview: View { + @EnvironmentObject private var remoteServer: UTMRemoteServer.State + @State private var sortOrder = [KeyPathComparator(\UTMRemoteServer.State.Client.name)] + @State private var selectedFingerprints = Set<UTMRemoteServer.State.ClientFingerprint>() + @State private var isDeleting: Bool = false + + var body: some View { + Table(remoteServer.allClients, selection: $selectedFingerprints, sortOrder: $sortOrder) { + TableColumn("") { client in + if remoteServer.isConnected(client.fingerprint) { + Image(systemName: "circle.fill") + .foregroundStyle(.green) + } + }.width(16) + TableColumn("Name", value: \.name) + .width(ideal: 200) + TableColumn("Fingerprint") { client in + Text((client.fingerprint ^ remoteServer.serverFingerprint).hexString()) + }.width(ideal: 300) + TableColumn("Last Seen", value: \.lastSeen) { client in + Text(DateFormatter.localizedString(from: client.lastSeen, dateStyle: .short, timeStyle: .short)) + }.width(ideal: 150) + TableColumn("Status") { client in + if remoteServer.isConnected(client.fingerprint) { + Text("Connected") + } else if remoteServer.isBlocked(client.fingerprint) { + Text("Blocked") + } else if !remoteServer.isApproved(client.fingerprint) { + HStack { + Button { + remoteServer.approve(client.fingerprint) + } label: { + Text("Approve") + }.buttonStyle(.bordered) + Button { + remoteServer.block(client.fingerprint) + } label: { + Text("Block") + }.buttonStyle(.bordered) + } + } + }.width(ideal: 140) + } + .contextMenu(forSelectionType: UTMRemoteServer.State.ClientFingerprint.self) { items in + if items.count == 1 { + if remoteServer.isConnected(items.first!) { + Button { + remoteServer.disconnect(items.first!) + } label: { + Text("Disconnect") + } + } + if !remoteServer.isApproved(items.first!) { + Button { + remoteServer.approve(items.first!) + } label: { + Text("Approve") + } + } + if !remoteServer.isBlocked(items.first!) { + Button { + remoteServer.block(items.first!) + } label: { + Text("Block") + } + } + } + if items.count > 0 { + Button { + isDeleting = true + selectedFingerprints = items + } label: { + Text("Delete") + } + } + } + .onChange(of: sortOrder) { + remoteServer.allClients.sort(using: $0) + } + .onDeleteCommand { + isDeleting = true + } + .alert("Confirmation", isPresented: $isDeleting) { + Button(role: .destructive) { + remoteServer.allClients.removeAll(where: { selectedFingerprints.contains($0.fingerprint) }) + } label: { + Text("Delete") + }.keyboardShortcut(.defaultAction) + } message: { + Text("Do you want to forget the selected client(s)?") + } + } +} + +@available(macOS 13, *) +#Preview { + UTMServerView() +} diff --git a/Platform/macOS/VMHeadlessSessionState.swift b/Platform/macOS/VMHeadlessSessionState.swift index a6b9f020..c26b863a 100644 --- a/Platform/macOS/VMHeadlessSessionState.swift +++ b/Platform/macOS/VMHeadlessSessionState.swift @@ -18,20 +18,18 @@ import Foundation import IOKit.pwr_mgt /// Represents the UI state for a single headless VM session. -@MainActor class VMHeadlessSessionState: NSObject, ObservableObject { +@MainActor class VMHeadlessSessionState: NSObject, ObservableObject, UTMVirtualMachineDelegate { let vm: any UTMVirtualMachine - var onStop: ((Notification) -> Void)? + var onStop: (() -> Void)? @Published var vmState: UTMVirtualMachineState = .stopped - @Published var fatalError: String? - private var hasStarted: Bool = false private var preventIdleSleepAssertion: IOPMAssertionID? @Setting("PreventIdleSleep") private var isPreventIdleSleep: Bool = false - init(for vm: any UTMVirtualMachine, onStop: ((Notification) -> Void)?) { + init(for vm: any UTMVirtualMachine, onStop: (() -> Void)?) { self.vm = vm self.onStop = onStop super.init() @@ -42,9 +40,7 @@ import IOKit.pwr_mgt deinit { NSWorkspace.shared.notificationCenter.removeObserver(self, name: NSWorkspace.didWakeNotification, object: nil) } -} -extension VMHeadlessSessionState: UTMVirtualMachineDelegate { nonisolated func virtualMachine(_ vm: any UTMVirtualMachine, didTransitionToState state: UTMVirtualMachineState) { Task { @MainActor in vmState = state @@ -63,7 +59,6 @@ extension VMHeadlessSessionState: UTMVirtualMachineDelegate { nonisolated func virtualMachine(_ vm: any UTMVirtualMachine, didErrorWithMessage message: String) { Task { @MainActor in - fatalError = message NotificationCenter.default.post(name: .vmSessionError, object: nil, userInfo: ["Session": self, "Message": message]) if !hasStarted { // if we got an error and haven't started, then cleanup @@ -101,6 +96,7 @@ extension VMHeadlessSessionState { if let preventIdleSleepAssertion = preventIdleSleepAssertion { IOPMAssertionRelease(preventIdleSleepAssertion) } + onStop?() } } diff --git a/Platform/macOS/VMRemoteSessionState.swift b/Platform/macOS/VMRemoteSessionState.swift new file mode 100644 index 00000000..66a946c7 --- /dev/null +++ b/Platform/macOS/VMRemoteSessionState.swift @@ -0,0 +1,35 @@ +// +// Copyright © 2024 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 Foundation +import IOKit.pwr_mgt + +/// Represents the UI state for a single headless VM session. +class VMRemoteSessionState: VMHeadlessSessionState { + public weak var client: UTMRemoteServer.Remote? + + init(for vm: any UTMVirtualMachine, client: UTMRemoteServer.Remote, onStop: (() -> Void)?) { + self.client = client + super.init(for: vm, onStop: onStop) + } + + override func virtualMachine(_ vm: any UTMVirtualMachine, didErrorWithMessage message: String) { + Task { + try? await client?.virtualMachine(id: vm.id, didErrorWithMessage: message) + super.virtualMachine(vm, didErrorWithMessage: message) + } + } +} diff --git a/Platform/macOS/macOS-unsigned.entitlements b/Platform/macOS/macOS-unsigned.entitlements index 216c2ec7..64b7e649 100644 --- a/Platform/macOS/macOS-unsigned.entitlements +++ b/Platform/macOS/macOS-unsigned.entitlements @@ -4,6 +4,10 @@ <dict> <key>com.apple.security.app-sandbox</key> <true/> + <key>com.apple.security.application-groups</key> + <array> + <string>$(TeamIdentifierPrefix)$(PRODUCT_BUNDLE_PREFIX:default=com.utmapp).UTM</string> + </array> <key>com.apple.security.cs.disable-library-validation</key> <true/> <key>com.apple.security.device.audio-input</key> @@ -14,6 +18,8 @@ <true/> <key>com.apple.security.network.client</key> <true/> + <key>com.apple.security.network.server</key> + <true/> <key>com.apple.security.temporary-exception.sbpl</key> <array> <string>(allow network-outbound)</string> diff --git a/Platform/macOS/macOS.entitlements b/Platform/macOS/macOS.entitlements index 4b3bdf54..de00e8fc 100644 --- a/Platform/macOS/macOS.entitlements +++ b/Platform/macOS/macOS.entitlements @@ -16,6 +16,8 @@ <true/> <key>com.apple.security.network.client</key> <true/> + <key>com.apple.security.network.server</key> + <true/> <key>com.apple.security.virtualization</key> <true/> <key>com.apple.vm.device-access</key> diff --git a/Platform/visionOS/UTMApp.swift b/Platform/visionOS/UTMApp.swift index e3e1487f..c82496e3 100644 --- a/Platform/visionOS/UTMApp.swift +++ b/Platform/visionOS/UTMApp.swift @@ -15,23 +15,40 @@ // import SwiftUI +import VisionKeyboardKit @MainActor struct UTMApp: App { + #if WITH_REMOTE + @State private var data: UTMRemoteData = UTMRemoteData() + #else @State private var data: UTMData = UTMData() + #endif @Environment(\.openWindow) private var openWindow @Environment(\.dismissWindow) private var dismissWindow private let vmSessionCreatedNotification = NotificationCenter.default.publisher(for: .vmSessionCreated) private let vmSessionEndedNotification = NotificationCenter.default.publisher(for: .vmSessionEnded) + private var contentView: some View { + #if WITH_REMOTE + RemoteContentView(remoteClientState: data.remoteClient.state) + #else + ContentView() + #endif + } + var body: some Scene { WindowGroup(id: "home") { - ContentView() + contentView .environmentObject(data) .onReceive(vmSessionCreatedNotification) { output in let newSession = output.userInfo!["Session"] as! VMSessionState - openWindow(value: newSession.newWindow()) + if let window = newSession.windows.first { + openWindow(value: window) + } else { + openWindow(value: newSession.newWindow()) + } } .onReceive(vmSessionEndedNotification) { output in let endedSession = output.userInfo!["Session"] as! VMSessionState @@ -46,12 +63,17 @@ struct UTMApp: App { WindowGroup(for: VMSessionState.GlobalWindowID.self) { $globalID in if let globalID = globalID, let session = VMSessionState.allActiveSessions[globalID.sessionID] { VMWindowView(id: globalID.windowID).environmentObject(session) + .glassBackgroundEffect(in: .rect(cornerRadius: 15)) + #if WITH_SOLO_VM .onAppear { // currently we only support one session, so close the home window dismissWindow(id: "home") } + #endif } } + .windowStyle(.plain) .windowResizability(.contentMinSize) + KeyboardWindowGroup() } } diff --git a/Platform/visionOS/VMToolbarOrnamentModifier.swift b/Platform/visionOS/VMToolbarOrnamentModifier.swift index bc058a9d..88caf586 100644 --- a/Platform/visionOS/VMToolbarOrnamentModifier.swift +++ b/Platform/visionOS/VMToolbarOrnamentModifier.swift @@ -15,23 +15,35 @@ // import SwiftUI +import VisionKeyboardKit +#if !WITH_USB +import CocoaSpiceNoUsb +#else +import CocoaSpice +#endif struct VMToolbarOrnamentModifier: ViewModifier { @Binding var state: VMWindowState @EnvironmentObject private var session: VMSessionState @AppStorage("ToolbarIsCollapsed") private var isCollapsed: Bool = false + @Environment(\.openWindow) private var openWindow + @Environment(\.dismissWindow) private var dismissWindow func body(content: Content) -> some View { content.ornament(visibility: isCollapsed ? .hidden : .visible, attachmentAnchor: .scene(.top)) { HStack { Button { - if session.vm.state == .started { + if state.isRunning { state.alert = .powerDown } else { state.alert = .terminateApp } } label: { - Label(state.isRunning ? "Power Off" : "Quit", systemImage: state.isRunning ? "power" : "xmark") + if state.isRunning { + Label("Power Off", systemImage: "power") + } else { + Label("Force Kill", systemImage: "xmark") + } } .disabled(state.isBusy) Button { @@ -56,7 +68,7 @@ struct VMToolbarOrnamentModifier: ViewModifier { } .disabled(state.isBusy) } - #if !WITH_QEMU_TCI + #if WITH_USB if session.vm.hasUsbRedirection { VMToolbarUSBMenuView() .disabled(state.isBusy) @@ -67,11 +79,39 @@ struct VMToolbarOrnamentModifier: ViewModifier { VMToolbarDisplayMenuView(state: $state) .disabled(state.isBusy) Button { - state.isKeyboardRequested = true + if case .display(_, _) = state.device { + state.isKeyboardRequested = !state.isKeyboardShown + } else { + state.isKeyboardRequested = true + } } label: { Label("Keyboard", systemImage: "keyboard") } .disabled(state.isBusy) + .onChange(of: state.isKeyboardRequested) { _, newValue in + guard case .display(_, _) = state.device else { + return + } + if newValue { + openWindow(keyboardFor: state.id) + } else { + dismissWindow(keyboardFor: state.id) + } + } + .onReceive(KeyboardEvent.publisher(for: state.id)) { event in + switch event { + case .keyboardDidAppear: + state.isKeyboardShown = true + state.isKeyboardRequested = true + case .keyboardDidDisappear: + state.isKeyboardShown = false + state.isKeyboardRequested = false + case .keyUp(let keyCode, let modifier): + handleKeyEvent(keyCode, modifier: modifier, isKeyDown: false) + case .keyDown(let keyCode, let modifier): + handleKeyEvent(keyCode, modifier: modifier, isKeyDown: true) + } + } Divider() Button { isCollapsed = true @@ -90,6 +130,18 @@ struct VMToolbarOrnamentModifier: ViewModifier { .modifier(ToolbarOrnamentViewModifier()) } } + + private func handleKeyEvent(_ keyCode: KeyboardKeyCode, modifier: KeyboardModifier, isKeyDown: Bool) { + guard let primaryInput = session.primaryInput else { + logger.debug("ignoring key event because input channel is not ready") + return + } + var scanCode = keyCode.ps2Set1ScanMake(modifier).reduce(Int32(0), { ($0 << 8) | Int32($1) }) + if ((scanCode & 0xFF00) == 0xE000) { + scanCode = 0x100 | (scanCode & 0xFF); + } + primaryInput.send(isKeyDown ? .press : .release, code: scanCode) + } } // the following was suggested by Apple via Feedback to look close to .toolbar() with .bottomOrnament diff --git a/Remote/GenerateKey.c b/Remote/GenerateKey.c new file mode 100644 index 00000000..f7bad39a --- /dev/null +++ b/Remote/GenerateKey.c @@ -0,0 +1,276 @@ +// +// Copyright © 2023 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. +// + +#include "GenerateKey.h" +#include <stdio.h> +#include <openssl/bio.h> +#include <openssl/conf.h> +#include <openssl/err.h> +#include <openssl/objects.h> +#include <openssl/pem.h> +#include <openssl/pkcs12.h> +#include <openssl/x509v3.h> + +#define X509_ENTRY_MAX_LENGTH (1024) + +/* Add extension using V3 code: we can set the config file as NULL + * because we wont reference any other sections. + */ +static int add_ext(X509 *cert, int nid, char *value) { + X509_EXTENSION *ex; + X509V3_CTX ctx; + /* This sets the 'context' of the extensions. */ + /* No configuration database */ + X509V3_set_ctx_nodb(&ctx); + /* Issuer and subject certs: both the target since it is self signed, + * no request and no CRL + */ + X509V3_set_ctx(&ctx, cert, cert, NULL, NULL, 0); + ex = X509V3_EXT_conf_nid(NULL, &ctx, nid, value); + if (!ex) { + return 0; + } + + X509_add_ext(cert, ex, -1); + X509_EXTENSION_free(ex); + return 1; +} + +static int mkrsacert(X509 **x509p, EVP_PKEY **pkeyp, const char *commonName, const char *organizationName, long serial, int days, int isClient) { + X509 *x = NULL; + EVP_PKEY *pk = NULL; + BIGNUM *bne = NULL; + RSA *rsa = NULL; + X509_NAME *name = NULL; + + if ((pk = EVP_PKEY_new()) == NULL) { + goto err; + } + + if ((x = X509_new()) == NULL) { + goto err; + } + + bne = BN_new(); + if (!bne || !BN_set_word(bne, RSA_F4)){ + goto err; + } + + rsa = RSA_new(); + if (!rsa || !RSA_generate_key_ex(rsa, 4096, bne, NULL)) { + goto err; + } + BN_free(bne); + bne = NULL; + if (!EVP_PKEY_assign_RSA(pk, rsa)) { + goto err; + } + rsa = NULL; // EVP_PKEY_assign_RSA takes ownership + + X509_set_version(x, 2); + ASN1_INTEGER_set(X509_get_serialNumber(x), serial); + X509_gmtime_adj(X509_get_notBefore(x), 0); + X509_gmtime_adj(X509_get_notAfter(x), (long)60*60*24*days); + X509_set_pubkey(x, pk); + + name = X509_get_subject_name(x); + + /* This function creates and adds the entry, working out the + * correct string type and performing checks on its length. + * Normally we'd check the return value for errors... + */ + X509_NAME_add_entry_by_txt(name, SN_commonName, + MBSTRING_UTF8, (const unsigned char *)commonName, -1, -1, 0); + X509_NAME_add_entry_by_txt(name, SN_organizationName, + MBSTRING_UTF8, (const unsigned char *)organizationName, -1, -1, 0); + + /* Its self signed so set the issuer name to be the same as the + * subject. + */ + X509_set_issuer_name(x, name); + + /* Add various extensions: standard extensions */ + add_ext(x, NID_basic_constraints, "critical,CA:TRUE"); + add_ext(x, NID_key_usage, "critical,keyCertSign,cRLSign,keyEncipherment,digitalSignature"); + if (isClient) { + add_ext(x, NID_ext_key_usage, "clientAuth"); + } else { + add_ext(x, NID_ext_key_usage, "serverAuth"); + } + add_ext(x, NID_subject_key_identifier, "hash"); + + if (!X509_sign(x, pk, EVP_sha256())) { + goto err; + } + + *x509p = x; + *pkeyp = pk; + return 1; +err: + if (pk) { + EVP_PKEY_free(pk); + } + if (x) { + X509_free(x); + } + if (bne) { + BN_free(bne); + } + return 0; +} + +static _Nullable CFDataRef CreateP12FromKey(EVP_PKEY *pkey, X509 *cert) { + PKCS12 *p12; + BIO *mem; + char *ptr; + long length; + CFDataRef data; + + p12 = PKCS12_create("password", NULL, pkey, cert, NULL, NID_pbe_WithSHA1And3_Key_TripleDES_CBC, NID_pbe_WithSHA1And40BitRC2_CBC, PKCS12_DEFAULT_ITER, 1, 0); + if (!p12) { + ERR_print_errors_fp(stderr); + return NULL; + } + mem = BIO_new(BIO_s_mem()); + if (!mem || !i2d_PKCS12_bio(mem, p12)) { + ERR_print_errors_fp(stderr); + PKCS12_free(p12); + BIO_free(mem); + return NULL; + } + PKCS12_free(p12); + length = BIO_get_mem_data(mem, &ptr); + data = CFDataCreate(kCFAllocatorDefault, (void *)ptr, length); + BIO_free(mem); + return data; +} + +static _Nullable CFDataRef CreatePrivatePEMFromKey(EVP_PKEY *pkey) { + BIO *mem; + char *ptr; + long length; + CFDataRef data; + + mem = BIO_new(BIO_s_mem()); + if (!mem || !PEM_write_bio_PrivateKey(mem, pkey, NULL, NULL, 0, NULL, NULL)) { + ERR_print_errors_fp(stderr); + BIO_free(mem); + return NULL; + } + length = BIO_get_mem_data(mem, &ptr); + data = CFDataCreate(kCFAllocatorDefault, (void *)ptr, length); + BIO_free(mem); + return data; +} + +static _Nullable CFDataRef CreatePublicPEMFromCert(X509 *cert) { + BIO *mem; + char *ptr; + long length; + CFDataRef data; + + mem = BIO_new(BIO_s_mem()); + if (!mem || !PEM_write_bio_X509(mem, cert)) { + ERR_print_errors_fp(stderr); + BIO_free(mem); + return NULL; + } + length = BIO_get_mem_data(mem, &ptr); + data = CFDataCreate(kCFAllocatorDefault, (void *)ptr, length); + BIO_free(mem); + return data; +} + +static _Nullable CFDataRef CreatePublicKeyFromCert(X509 *cert) { + EVP_PKEY* pubkey; + BIO *mem; + char *ptr; + long length; + CFDataRef data; + + pubkey = X509_get_pubkey(cert); + if (!pubkey) { + ERR_print_errors_fp(stderr); + return NULL; + } + mem = BIO_new(BIO_s_mem()); + if (!mem || !i2d_PUBKEY_bio(mem, pubkey)) { + ERR_print_errors_fp(stderr); + EVP_PKEY_free(pubkey); + BIO_free(mem); + return NULL; + } + length = BIO_get_mem_data(mem, &ptr); + data = CFDataCreate(kCFAllocatorDefault, (void *)ptr, length); + BIO_free(mem); + EVP_PKEY_free(pubkey); + return data; +} + +_Nullable CFArrayRef GenerateRSACertificate(CFStringRef _Nonnull commonName, CFStringRef _Nonnull organizationName, CFNumberRef _Nullable serial, CFNumberRef _Nullable days, CFBooleanRef _Nonnull isClient) { + char _commonName[X509_ENTRY_MAX_LENGTH]; + char _organizationName[X509_ENTRY_MAX_LENGTH]; + long _serial = 0; + int _days = 365; + int _isClient = 0; + X509 *cert; + EVP_PKEY *pkey; + CFDataRef arr[4] = {NULL}; + CFArrayRef cfarr = NULL; + + if (!CFStringGetCString(commonName, _commonName, X509_ENTRY_MAX_LENGTH, kCFStringEncodingUTF8)) { + return NULL; + } + if (!CFStringGetCString(organizationName, _organizationName, X509_ENTRY_MAX_LENGTH, kCFStringEncodingUTF8)) { + return NULL; + } + if (serial) { + CFNumberGetValue(serial, kCFNumberLongType, &_serial); + } + if (days) { + CFNumberGetValue(days, kCFNumberIntType, &_days); + } + _isClient = CFBooleanGetValue(isClient); + + OpenSSL_add_all_algorithms(); + ERR_load_crypto_strings(); + if (!mkrsacert(&cert, &pkey, _commonName, _organizationName, _serial, _days, _isClient)) { + ERR_print_errors_fp(stderr); + return NULL; + } + arr[0] = CreateP12FromKey(pkey, cert); + arr[1] = CreatePrivatePEMFromKey(pkey); + arr[2] = CreatePublicPEMFromCert(cert); + arr[3] = CreatePublicKeyFromCert(cert); + if (arr[0] && arr[1] && arr[2] && arr[3]) { + cfarr = CFArrayCreate(kCFAllocatorDefault, (const void **)arr, 4, &kCFTypeArrayCallBacks); + } + if (arr[0]) { + CFRelease(arr[0]); + } + if (arr[1]) { + CFRelease(arr[1]); + } + if (arr[2]) { + CFRelease(arr[2]); + } + if (arr[3]) { + CFRelease(arr[3]); + } + EVP_PKEY_free(pkey); + X509_free(cert); + return cfarr; +} diff --git a/Remote/GenerateKey.h b/Remote/GenerateKey.h new file mode 100644 index 00000000..7e73d5f2 --- /dev/null +++ b/Remote/GenerateKey.h @@ -0,0 +1,33 @@ +// +// Copyright © 2023 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. +// + +#ifndef GenerateKey_h +#define GenerateKey_h + +#include <CoreFoundation/CoreFoundation.h> + +/// Generate a RSA-4096 key and return a PKCS#12 encoded data +/// +/// The password of the blob is `password`. Returns NULL on error. +/// - Parameters: +/// - commonName: CN field of the certificate, max length is 1024 bytes +/// - organizationName: O field of the certificate, max length is 1024 bytes +/// - serial: Serial number of the certificate +/// - days: Validity in days from today +/// - isClient: If 0 then a TLS Server certificate is generated, otherwise a TLS Client certificate is generated +_Nullable CFArrayRef GenerateRSACertificate(CFStringRef _Nonnull commonName, CFStringRef _Nonnull organizationName, CFNumberRef _Nullable serial, CFNumberRef _Nullable days, CFBooleanRef _Nonnull isClient); + +#endif /* GenerateKey_h */ diff --git a/Remote/UTMRemoteClient.swift b/Remote/UTMRemoteClient.swift new file mode 100644 index 00000000..f2cc3d67 --- /dev/null +++ b/Remote/UTMRemoteClient.swift @@ -0,0 +1,588 @@ +// +// Copyright © 2024 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 Foundation +import Network +import SwiftConnect + +let service = "_utm_server._tcp" + +actor UTMRemoteClient { + let state: State + private let keyManager = UTMRemoteKeyManager(forClient: true) + private let connectionQueue = DispatchQueue(label: "UTM Remote Client Connection") + private var local: Local + + private var scanTask: Task<Void, Error>? + + private(set) var server: Remote! + + nonisolated var fingerprint: [UInt8] { + keyManager.fingerprint ?? [] + } + + @MainActor + init(data: UTMRemoteData) { + self.state = State() + self.local = Local(data: data) + } + + private func withErrorAlert(_ body: () async throws -> Void) async { + do { + try await body() + } catch { + await state.showErrorAlert(error.localizedDescription) + } + } + + func startScanning() { + scanTask = Task { + await withErrorAlert { + for try await results in Connection.browse(forServiceType: service) { + await self.didFindResults(results) + } + } + } + } + + func stopScanning() { + scanTask?.cancel() + scanTask = nil + } + + func refresh() { + stopScanning() + startScanning() + } + + func didFindResults(_ results: Set<NWBrowser.Result>) async { + let servers = results.compactMap { result in + let model: String? + if case .bonjour(let txtRecord) = result.metadata, + case .string(let value) = txtRecord.getEntry(for: "Model") { + model = value + } else { + model = nil + } + switch result.endpoint { + case .service(let name, _, _, _): + return State.DiscoveredServer(hostname: result.endpoint.debugDescription, model: model, name: name, endpoint: result.endpoint) + default: + return nil + } + } + await state.updateFoundServers(servers) + } + + func connect(_ server: State.SavedServer) async throws { + var isSuccessful = false + let endpoint = server.endpoint ?? NWEndpoint.hostPort(host: .init(server.hostname), port: .init(integerLiteral: UInt16(server.port ?? 0))) + try await keyManager.load() + let connection = try await Connection(endpoint: endpoint, connectionQueue: connectionQueue, identity: keyManager.identity) { connection, error in + Task { + do { + try await self.local.data.reconnect(to: server) + } catch { + // reconnect failed + await self.state.setConnected(false) + await self.state.showErrorAlert(error.localizedDescription) + } + } + } + defer { + if !isSuccessful { + connection.close() + } + } + guard let host = connection.connection.currentPath?.remoteEndpoint?.hostname else { + throw ConnectionError.cannotDetermineHost + } + guard let fingerprint = connection.peerCertificateChain.first?.fingerprint() else { + throw ConnectionError.cannotFindFingerprint + } + if server.fingerprint.isEmpty { + throw ConnectionError.fingerprintUntrusted(fingerprint) + } else if server.fingerprint != fingerprint { + throw ConnectionError.fingerprintMismatch(fingerprint) + } + try Task.checkCancellation() + let peer = Peer(connection: connection, localInterface: local) + let remote = Remote(peer: peer, host: host) + let (isAuthenticated, device) = try await remote.handshake(password: server.password) + if !isAuthenticated { + if server.password == nil { + throw ConnectionError.passwordRequired + } else { + throw ConnectionError.passwordInvalid + } + } + self.server = remote + var server = server + await state.setConnected(true) + if !server.shouldSavePassword { + server.password = nil + } + if server.name.isEmpty { + server.name = server.hostname + } + server.lastSeen = Date() + server.model = device.model + await state.save(server: server) + isSuccessful = true + } +} + +extension UTMRemoteClient { + @MainActor + class State: ObservableObject { + typealias ServerFingerprint = [UInt8] + + struct DiscoveredServer: Identifiable { + let hostname: String + var model: String? + var name: String + var endpoint: NWEndpoint + + var id: String { + hostname + } + } + + struct SavedServer: Codable, Identifiable { + var fingerprint: ServerFingerprint + var hostname: String + var port: Int? + var model: String? + var name: String + var lastSeen: Date + var password: String? + var endpoint: NWEndpoint? + var shouldSavePassword: Bool = false + + private enum CodingKeys: String, CodingKey { + case fingerprint, hostname, port, model, name, lastSeen, password + } + + var id: ServerFingerprint { + fingerprint + } + + var isAvailable: Bool { + endpoint != nil || (port != nil && port != 0) + } + + init() { + self.hostname = "" + self.name = "" + self.lastSeen = Date() + self.fingerprint = [] + } + + init(from discovered: DiscoveredServer) { + self.hostname = discovered.hostname + self.model = discovered.model + self.name = discovered.name + self.lastSeen = Date() + self.endpoint = discovered.endpoint + self.fingerprint = [] + } + } + + struct AlertMessage: Identifiable { + let id = UUID() + let message: String + } + + @Published var savedServers: [SavedServer] { + didSet { + UserDefaults.standard.setValue(try! savedServers.propertyList(), forKey: "TrustedServers") + } + } + + @Published var foundServers: [DiscoveredServer] = [] + + @Published var isScanning: Bool = false + + @Published private(set) var isConnected: Bool = false + + @Published var alertMessage: AlertMessage? + + init() { + var _savedServers = Array<SavedServer>() + if let array = UserDefaults.standard.array(forKey: "TrustedServers") { + if let servers = try? Array<SavedServer>(fromPropertyList: array) { + _savedServers = servers + } + } + self.savedServers = _savedServers + } + + func showErrorAlert(_ message: String) { + alertMessage = AlertMessage(message: message) + } + + func updateFoundServers(_ servers: [DiscoveredServer]) { + for idx in savedServers.indices { + savedServers[idx].endpoint = nil + } + foundServers = servers.filter { server in + if let idx = savedServers.firstIndex(where: { $0.port == nil && $0.hostname == server.hostname }) { + savedServers[idx].endpoint = server.endpoint + return false + } else { + return true + } + } + } + + func save(server: SavedServer) { + if let idx = savedServers.firstIndex(where: { $0.fingerprint == server.fingerprint }) { + savedServers[idx] = server + } else { + savedServers.append(server) + } + } + + func delete(server: SavedServer) { + savedServers.removeAll(where: { $0.fingerprint == server.fingerprint }) + } + + fileprivate func setConnected(_ connected: Bool) { + isConnected = connected + } + } +} + +extension UTMRemoteClient { + class Local: LocalInterface { + typealias M = UTMRemoteMessageClient + + fileprivate let data: UTMRemoteData + + init(data: UTMRemoteData) { + self.data = data + } + + func handle(message: M, data: Data) async throws -> Data { + switch message { + case .clientHandshake: + return try await _handshake(parameters: .decode(data)).encode() + case .listHasChanged: + return try await _listHasChanged(parameters: .decode(data)).encode() + case .qemuConfigurationHasChanged: + return try await _qemuConfigurationHasChanged(parameters: .decode(data)).encode() + case .mountedDrivesHasChanged: + return try await _mountedDrivesHasChanged(parameters: .decode(data)).encode() + case .virtualMachineDidTransition: + return try await _virtualMachineDidTransition(parameters: .decode(data)).encode() + case .virtualMachineDidError: + return try await _virtualMachineDidError(parameters: .decode(data)).encode() + } + } + + private func _handshake(parameters: M.ClientHandshake.Request) async throws -> M.ClientHandshake.Reply { + return .init(version: UTMRemoteMessageClient.version, capabilities: .current) + } + + private func _listHasChanged(parameters: M.ListHasChanged.Request) async throws -> M.ListHasChanged.Reply { + await data.remoteListHasChanged(ids: parameters.ids) + return .init() + } + + private func _qemuConfigurationHasChanged(parameters: M.QEMUConfigurationHasChanged.Request) async throws -> M.QEMUConfigurationHasChanged.Reply { + await data.remoteQemuConfigurationHasChanged(id: parameters.id, configuration: parameters.configuration) + return .init() + } + + private func _mountedDrivesHasChanged(parameters: M.MountedDrivesHasChanged.Request) async throws -> M.MountedDrivesHasChanged.Reply { + await data.remoteMountedDrivesHasChanged(id: parameters.id, mountedDrives: parameters.mountedDrives) + return .init() + } + + private func _virtualMachineDidTransition(parameters: M.VirtualMachineDidTransition.Request) async throws -> M.VirtualMachineDidTransition.Reply { + await data.remoteVirtualMachineDidTransition(id: parameters.id, state: parameters.state, isTakeoverAllowed: parameters.isTakeoverAllowed) + return .init() + } + + private func _virtualMachineDidError(parameters: M.VirtualMachineDidError.Request) async throws -> M.VirtualMachineDidError.Reply { + await data.remoteVirtualMachineDidError(id: parameters.id, message: parameters.errorMessage) + return .init() + } + } +} + +extension UTMRemoteClient { + class Remote { + typealias M = UTMRemoteMessageServer + private let peer: Peer<UTMRemoteMessageClient> + let host: String + private(set) var capabilities: UTMCapabilities? + + init(peer: Peer<UTMRemoteMessageClient>, host: String) { + self.peer = peer + self.host = host + } + + func close() { + peer.close() + } + + func handshake(password: String?) async throws -> (isAuthenticated: Bool, device: MacDevice) { + let reply = try await _handshake(parameters: .init(version: UTMRemoteMessageServer.version, password: password)) + guard reply.version == UTMRemoteMessageServer.version else { + throw ClientError.versionMismatch + } + capabilities = reply.capabilities + return (isAuthenticated: reply.isAuthenticated, device: MacDevice(model: reply.model)) + } + + func listVirtualMachines() async throws -> [UUID] { + try await _listVirtualMachines(parameters: .init()).ids + } + + func reorderVirtualMachines(fromIds ids: [UUID], toOffset offset: Int) async throws { + try await _reorderVirtualMachines(parameters: .init(ids: ids, offset: offset)) + } + + func getVirtualMachineInformation(for ids: [UUID]) async throws -> [M.VirtualMachineInformation] { + try await _getVirtualMachineInformation(parameters: .init(ids: ids)).informations + } + + func getQEMUConfiguration(for id: UUID) async throws -> UTMQemuConfiguration { + try await _getQEMUConfiguration(parameters: .init(id: id)).configuration + } + + func getPackageSize(for id: UUID) async throws -> Int64 { + try await _getPackageSize(parameters: .init(id: id)).size + } + + func getPackageFile(for id: UUID, relativePathComponents: [String]) async throws -> URL { + let fm = FileManager.default + let packageUrl = try packageUrl(for: id) + let fileUrl = packageUrl.appendingPathComponent(relativePathComponents.joined(separator: "_")) + var lastModified: Date? + if fm.fileExists(atPath: fileUrl.path) { + lastModified = try? fm.attributesOfItem(atPath: fileUrl.path)[.modificationDate] as? Date + } + let reply = try await _getPackageFile(parameters: .init(id: id, relativePathComponents: relativePathComponents, lastModified: lastModified)) + if let data = reply.data { + fm.createFile(atPath: fileUrl.path, contents: data, attributes: [.modificationDate: reply.lastModified]) + } + return fileUrl + } + + func sendPackageFile(for id: UUID, relativePathComponents: [String], data: Data) async throws { + let fm = FileManager.default + let packageUrl = try packageUrl(for: id) + let fileUrl = packageUrl.appendingPathComponent(relativePathComponents.joined(separator: "_")) + guard fm.createFile(atPath: fileUrl.path, contents: data) else { + throw ConnectionError.failedToAccessFile + } + guard let lastModified = try fm.attributesOfItem(atPath: fileUrl.path)[.modificationDate] as? Date else { + throw ConnectionError.failedToAccessFile + } + try await _sendPackageFile(parameters: .init(id: id, relativePathComponents: relativePathComponents, lastModified: lastModified, data: data)) + } + + func deletePackageFile(for id: UUID, relativePathComponents: [String]) async throws { + let fm = FileManager.default + let packageUrl = try packageUrl(for: id) + let fileUrl = packageUrl.appendingPathComponent(relativePathComponents.joined(separator: "_")) + try fm.removeItem(at: fileUrl) + try await _deletePackageFile(parameters: .init(id: id, relativePathComponents: relativePathComponents)) + } + + func mountGuestToolsOnVirtualMachine(id: UUID) async throws { + try await _mountGuestToolsOnVirtualMachine(parameters: .init(id: id)) + } + + func startVirtualMachine(id: UUID, options: UTMVirtualMachineStartOptions) async throws -> UTMRemoteMessageServer.StartVirtualMachine.ServerInformation { + return try await _startVirtualMachine(parameters: .init(id: id, options: options)).serverInfo + } + + func stopVirtualMachine(id: UUID, method: UTMVirtualMachineStopMethod) async throws { + try await _stopVirtualMachine(parameters: .init(id: id, method: method)) + } + + func restartVirtualMachine(id: UUID) async throws { + try await _restartVirtualMachine(parameters: .init(id: id)) + } + + func pauseVirtualMachine(id: UUID) async throws { + try await _pauseVirtualMachine(parameters: .init(id: id)) + } + + func resumeVirtualMachine(id: UUID) async throws { + try await _resumeVirtualMachine(parameters: .init(id: id)) + } + + func saveSnapshotVirtualMachine(id: UUID, name: String?) async throws { + try await _saveSnapshotVirtualMachine(parameters: .init(id: id, name: name)) + } + + func deleteSnapshotVirtualMachine(id: UUID, name: String?) async throws { + try await _deleteSnapshotVirtualMachine(parameters: .init(id: id, name: name)) + } + + func restoreSnapshotVirtualMachine(id: UUID, name: String?) async throws { + try await _restoreSnapshotVirtualMachine(parameters: .init(id: id, name: name)) + } + + func changePointerTypeVirtualMachine(id: UUID, toTabletMode tablet: Bool) async throws { + try await _changePointerTypeVirtualMachine(parameters: .init(id: id, isTabletMode: tablet)) + } + + private func packageUrl(for id: UUID) throws -> URL { + let fm = FileManager.default + let cacheUrl = try fm.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + let packageUrl = cacheUrl.appendingPathComponent(id.uuidString) + if !fm.fileExists(atPath: packageUrl.path) { + try fm.createDirectory(at: packageUrl, withIntermediateDirectories: false) + } + return packageUrl + } + + private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply { + try await M.ServerHandshake.send(parameters, to: peer) + } + + private func _listVirtualMachines(parameters: M.ListVirtualMachines.Request) async throws -> M.ListVirtualMachines.Reply { + try await M.ListVirtualMachines.send(parameters, to: peer) + } + + @discardableResult + private func _reorderVirtualMachines(parameters: M.ReorderVirtualMachines.Request) async throws -> M.ReorderVirtualMachines.Reply { + try await M.ReorderVirtualMachines.send(parameters, to: peer) + } + + private func _getVirtualMachineInformation(parameters: M.GetVirtualMachineInformation.Request) async throws -> M.GetVirtualMachineInformation.Reply { + try await M.GetVirtualMachineInformation.send(parameters, to: peer) + } + + private func _getQEMUConfiguration(parameters: M.GetQEMUConfiguration.Request) async throws -> M.GetQEMUConfiguration.Reply { + try await M.GetQEMUConfiguration.send(parameters, to: peer) + } + + private func _getPackageSize(parameters: M.GetPackageSize.Request) async throws -> M.GetPackageSize.Reply { + try await M.GetPackageSize.send(parameters, to: peer) + } + + private func _getPackageFile(parameters: M.GetPackageFile.Request) async throws -> M.GetPackageFile.Reply { + try await M.GetPackageFile.send(parameters, to: peer) + } + + @discardableResult + private func _sendPackageFile(parameters: M.SendPackageFile.Request) async throws -> M.SendPackageFile.Reply { + try await M.SendPackageFile.send(parameters, to: peer) + } + + @discardableResult + private func _deletePackageFile(parameters: M.DeletePackageFile.Request) async throws -> M.DeletePackageFile.Reply { + try await M.DeletePackageFile.send(parameters, to: peer) + } + + @discardableResult + private func _mountGuestToolsOnVirtualMachine(parameters: M.MountGuestToolsOnVirtualMachine.Request) async throws -> M.MountGuestToolsOnVirtualMachine.Reply { + try await M.MountGuestToolsOnVirtualMachine.send(parameters, to: peer) + } + + private func _startVirtualMachine(parameters: M.StartVirtualMachine.Request) async throws -> M.StartVirtualMachine.Reply { + try await M.StartVirtualMachine.send(parameters, to: peer) + } + + @discardableResult + private func _stopVirtualMachine(parameters: M.StopVirtualMachine.Request) async throws -> M.StopVirtualMachine.Reply { + try await M.StopVirtualMachine.send(parameters, to: peer) + } + + @discardableResult + private func _restartVirtualMachine(parameters: M.RestartVirtualMachine.Request) async throws -> M.RestartVirtualMachine.Reply { + try await M.RestartVirtualMachine.send(parameters, to: peer) + } + + @discardableResult + private func _pauseVirtualMachine(parameters: M.PauseVirtualMachine.Request) async throws -> M.PauseVirtualMachine.Reply { + try await M.PauseVirtualMachine.send(parameters, to: peer) + } + + @discardableResult + private func _resumeVirtualMachine(parameters: M.ResumeVirtualMachine.Request) async throws -> M.ResumeVirtualMachine.Reply { + try await M.ResumeVirtualMachine.send(parameters, to: peer) + } + + @discardableResult + private func _saveSnapshotVirtualMachine(parameters: M.SaveSnapshotVirtualMachine.Request) async throws -> M.SaveSnapshotVirtualMachine.Reply { + try await M.SaveSnapshotVirtualMachine.send(parameters, to: peer) + } + + @discardableResult + private func _deleteSnapshotVirtualMachine(parameters: M.DeleteSnapshotVirtualMachine.Request) async throws -> M.DeleteSnapshotVirtualMachine.Reply { + try await M.DeleteSnapshotVirtualMachine.send(parameters, to: peer) + } + + @discardableResult + private func _restoreSnapshotVirtualMachine(parameters: M.RestoreSnapshotVirtualMachine.Request) async throws -> M.RestoreSnapshotVirtualMachine.Reply { + try await M.RestoreSnapshotVirtualMachine.send(parameters, to: peer) + } + + @discardableResult + private func _changePointerTypeVirtualMachine(parameters: M.ChangePointerTypeVirtualMachine.Request) async throws -> M.ChangePointerTypeVirtualMachine.Reply { + try await M.ChangePointerTypeVirtualMachine.send(parameters, to: peer) + } + } +} + +extension UTMRemoteClient { + enum ConnectionError: LocalizedError { + case cannotDetermineHost + case cannotFindFingerprint + case passwordRequired + case passwordInvalid + case fingerprintUntrusted(State.ServerFingerprint) + case fingerprintMismatch(State.ServerFingerprint) + case failedToAccessFile + + var errorDescription: String? { + switch self { + case .cannotDetermineHost: + return NSLocalizedString("Failed to determine host name.", comment: "UTMRemoteClient") + case .cannotFindFingerprint: + return NSLocalizedString("Failed to get host fingerprint.", comment: "UTMRemoteClient") + case .passwordRequired: + return NSLocalizedString("Password is required.", comment: "UTMRemoteClient") + case .passwordInvalid: + return NSLocalizedString("Password is incorrect.", comment: "UTMRemoteClient") + case .fingerprintUntrusted(_): + return NSLocalizedString("This host is not yet trusted. You should verify that the fingerprints match what is displayed on the host and then select Trust to continue.", comment: "UTMRemoteClient") + case .fingerprintMismatch(_): + return String.localizedStringWithFormat(NSLocalizedString("The host fingerprint does not match the saved value. This means that UTM Server was reset, a different host is using the same name, or an attacker is pretending to be the host. For your protection, you need to delete this saved host to continue.", comment: "UTMRemoteClient")) + case .failedToAccessFile: + return NSLocalizedString("Failed to access file.", comment: "UTMRemoteClient") + } + } + } + + enum ClientError: LocalizedError { + case versionMismatch + + var errorDescription: String? { + switch self { + case .versionMismatch: + return NSLocalizedString("The server interface version does not match the client.", comment: "UTMRemoteClient") + } + } + } +} diff --git a/Remote/UTMRemoteConnectInterface.h b/Remote/UTMRemoteConnectInterface.h new file mode 100644 index 00000000..814132ce --- /dev/null +++ b/Remote/UTMRemoteConnectInterface.h @@ -0,0 +1,39 @@ +// +// Copyright © 2024 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 <Foundation/Foundation.h> + +@protocol UTMRemoteConnectDelegate; + +NS_ASSUME_NONNULL_BEGIN + +@protocol UTMRemoteConnectInterface <NSObject> + +@property (nonatomic, weak) id<UTMRemoteConnectDelegate> connectDelegate; + +- (BOOL)connectWithError:(NSError * _Nullable *)error; +- (void)disconnect; + +@end + +@protocol UTMRemoteConnectDelegate <NSObject> + +- (void)remoteInterface:(id<UTMRemoteConnectInterface>)remoteInterface didErrorWithMessage:(NSString *)message; +- (void)remoteInterfaceDidConnect:(id<UTMRemoteConnectInterface>)remoteInterface; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Remote/UTMRemoteKeyManager.swift b/Remote/UTMRemoteKeyManager.swift new file mode 100644 index 00000000..4df5da5a --- /dev/null +++ b/Remote/UTMRemoteKeyManager.swift @@ -0,0 +1,196 @@ +// +// Copyright © 2023 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 Foundation +import Security +import CryptoKit +#if os(macOS) +import SystemConfiguration +#endif + +class UTMRemoteKeyManager { + let isClient: Bool + private(set) var isLoaded: Bool = false + private(set) var identity: SecIdentity! + private(set) var fingerprint: [UInt8]? + + init(forClient client: Bool) { + self.isClient = client + } + + private var certificateCommonNamePrefix: String { + "UTM Remote \(isClient ? "Client" : "Server")" + } + + private lazy var certificateCommonName: String = { + #if os(macOS) + let deviceName = SCDynamicStoreCopyComputerName(nil, nil) as? String ?? "macOS" + #else + let deviceName = UIDevice.current.name + #endif + return "\(certificateCommonNamePrefix) (\(deviceName))" + }() + + private func generateKey() throws -> SecIdentity { + let commonName = certificateCommonName as CFString + let organizationName = "UTM" as CFString + let serialNumber = Int.random(in: 1..<CLong.max) as CFNumber + let days = 3650 as CFNumber + guard let data = GenerateRSACertificate(commonName, organizationName, serialNumber, days, isClient as CFBoolean)?.takeUnretainedValue() as? [CFData] else { + throw UTMRemoteKeyManagerError.generateKeyFailure + } + let importOptions = [ kSecImportExportPassphrase as String: "password" ] as CFDictionary + var rawItems: CFArray? + try withSecurityThrow(SecPKCS12Import(data[0], importOptions, &rawItems)) + guard let items = (rawItems! as! [[String: Any]]).first else { + throw UTMRemoteKeyManagerError.parseKeyFailure + } + return items[kSecImportItemIdentity as String] as! SecIdentity + } + + private func importIdentity(_ identity: SecIdentity) throws { + let attributes = [ + kSecValueRef as String: identity, + ] as CFDictionary + try withSecurityThrow(SecItemAdd(attributes, nil)) + } + + private func loadIdentity() throws -> SecIdentity? { + var query = [ + kSecClass as String: kSecClassIdentity, + kSecReturnRef as String: true, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecMatchPolicy as String: SecPolicyCreateSSL(!isClient, nil), + ] as [String : Any] + #if os(macOS) + query[kSecMatchSubjectStartsWith as String] = certificateCommonNamePrefix + #endif + var copyResult: AnyObject? = nil + let result = SecItemCopyMatching(query as CFDictionary, ©Result) + if result == errSecItemNotFound { + return nil + } + try withSecurityThrow(result) + return (copyResult as! SecIdentity) + } + + private func deleteIdentity(_ identity: SecIdentity) throws { + let query = [ + kSecClass as String: kSecClassIdentity, + kSecMatchItemList as String: [identity], + ] as CFDictionary + try withSecurityThrow(SecItemDelete(query)) + } + + private func withSecurityThrow(_ block: @autoclosure () -> OSStatus) throws { + let err = block() + if err != errSecSuccess && err != errSecDuplicateItem { + throw NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil) + } + } +} + +extension UTMRemoteKeyManager { + func load() async throws { + guard !isLoaded else { + return + } + let identity = try await Task.detached { [self] in + if let identity = try loadIdentity() { + return identity + } else { + let identity = try generateKey() + try importIdentity(identity) + return identity + } + }.value + var certificate: SecCertificate? + try withSecurityThrow(SecIdentityCopyCertificate(identity, &certificate)) + self.identity = identity + self.fingerprint = certificate!.fingerprint() + self.isLoaded = true + } + + func reset() async throws { + try await Task.detached { [self] in + if let identity = try loadIdentity() { + try deleteIdentity(identity) + } + }.value + if isLoaded { + isLoaded = false + try await load() + } + } +} + +extension SecCertificate { + func fingerprint() -> [UInt8] { + let data = SecCertificateCopyData(self) + return SHA256.hash(data: data as Data).map({ $0 }) + } +} + +extension Array where Element == UInt8 { + func hexString() -> String { + self.map({ String(format: "%02X", $0) }).joined(separator: ":") + } + + init?(hexString: String) { + let cleanString = hexString.replacingOccurrences(of: ":", with: "") + guard cleanString.count % 2 == 0 else { + return nil + } + + var byteArray = [UInt8]() + var index = cleanString.startIndex + + while index < cleanString.endIndex { + let nextIndex = cleanString.index(index, offsetBy: 2) + if let byte = UInt8(cleanString[index..<nextIndex], radix: 16) { + byteArray.append(byte) + } else { + return nil // Invalid hex character + } + index = nextIndex + } + self = byteArray + } + + static func ^(lhs: Self, rhs: Self) -> Self { + let length = Swift.min(lhs.count, rhs.count) + return (0..<length).map({ lhs[$0] ^ rhs[$0] }) + } +} + +enum UTMRemoteKeyManagerError: Error { + case generateKeyFailure + case parseKeyFailure + case importKeyFailure +} + +extension UTMRemoteKeyManagerError: LocalizedError { + var errorDescription: String? { + switch self { + case .generateKeyFailure: + return NSLocalizedString("Failed to generate a key pair.", comment: "UTMRemoteKeyManager") + case .parseKeyFailure: + return NSLocalizedString("Failed to parse generated key pair.", comment: "UTMRemoteKeyManager") + case .importKeyFailure: + return NSLocalizedString("Failed to import generated key.", comment: "UTMRemoteKeyManager") + } + } +} diff --git a/Remote/UTMRemoteMessage.swift b/Remote/UTMRemoteMessage.swift new file mode 100644 index 00000000..9901d0be --- /dev/null +++ b/Remote/UTMRemoteMessage.swift @@ -0,0 +1,380 @@ +// +// Copyright © 2024 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 Foundation +import SwiftConnect + +enum UTMRemoteMessageServer: UInt8, MessageID { + static let version = 1 + case serverHandshake + case listVirtualMachines + case reorderVirtualMachines + case getVirtualMachineInformation + case getQEMUConfiguration + case getPackageSize + case getPackageFile + case sendPackageFile + case deletePackageFile + case mountGuestToolsOnVirtualMachine + case startVirtualMachine + case stopVirtualMachine + case restartVirtualMachine + case pauseVirtualMachine + case resumeVirtualMachine + case saveSnapshotVirtualMachine + case deleteSnapshotVirtualMachine + case restoreSnapshotVirtualMachine + case changePointerTypeVirtualMachine +} + + +enum UTMRemoteMessageClient: UInt8, MessageID { + static let version = 1 + case clientHandshake + case listHasChanged + case qemuConfigurationHasChanged + case mountedDrivesHasChanged + case virtualMachineDidTransition + case virtualMachineDidError +} + +extension UTMRemoteMessageServer { + struct ServerHandshake: Message { + static let id = UTMRemoteMessageServer.serverHandshake + + struct Request: Serializable, Codable { + let version: Int + let password: String? + } + + struct Reply: Serializable, Codable { + let version: Int + let isAuthenticated: Bool + let capabilities: UTMCapabilities + let model: String + } + } + + struct VirtualMachineInformation: Serializable, Codable { + let id: UUID + let name: String + let path: String + let isShortcut: Bool + let isSuspended: Bool + let isTakeoverAllowed: Bool + let backend: UTMBackend + let state: UTMVirtualMachineState + let mountedDrives: [String: String] + } + + struct ListVirtualMachines: Message { + static let id = UTMRemoteMessageServer.listVirtualMachines + + struct Request: Serializable, Codable {} + + struct Reply: Serializable, Codable { + let ids: [UUID] + } + } + + struct ReorderVirtualMachines: Message { + static let id = UTMRemoteMessageServer.reorderVirtualMachines + + struct Request: Serializable, Codable { + let ids: [UUID] + let offset: Int + } + + struct Reply: Serializable, Codable {} + } + + struct GetVirtualMachineInformation: Message { + static let id = UTMRemoteMessageServer.getVirtualMachineInformation + + struct Request: Serializable, Codable { + let ids: [UUID] + } + + struct Reply: Serializable, Codable { + let informations: [VirtualMachineInformation] + } + } + + struct GetQEMUConfiguration: Message { + static let id = UTMRemoteMessageServer.getQEMUConfiguration + + struct Request: Serializable, Codable { + let id: UUID + } + + struct Reply: Serializable, Codable { + let configuration: UTMQemuConfiguration + } + } + + struct GetPackageSize: Message { + static let id = UTMRemoteMessageServer.getPackageSize + + struct Request: Serializable, Codable { + let id: UUID + } + + struct Reply: Serializable, Codable { + let size: Int64 + } + } + + struct GetPackageFile: Message { + static let id = UTMRemoteMessageServer.getPackageFile + + struct Request: Serializable, Codable { + let id: UUID + let relativePathComponents: [String] + let lastModified: Date? + } + + struct Reply: Serializable, Codable { + let data: Data? + let lastModified: Date + } + } + + struct SendPackageFile: Message { + static let id = UTMRemoteMessageServer.sendPackageFile + + struct Request: Serializable, Codable { + let id: UUID + let relativePathComponents: [String] + let lastModified: Date + let data: Data + } + + struct Reply: Serializable, Codable {} + } + + struct DeletePackageFile: Message { + static let id = UTMRemoteMessageServer.deletePackageFile + + struct Request: Serializable, Codable { + let id: UUID + let relativePathComponents: [String] + } + + struct Reply: Serializable, Codable {} + } + + struct MountGuestToolsOnVirtualMachine: Message { + static let id = UTMRemoteMessageServer.mountGuestToolsOnVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + } + + struct Reply: Serializable, Codable {} + } + + struct StartVirtualMachine: Message { + static let id = UTMRemoteMessageServer.startVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + let options: UTMVirtualMachineStartOptions + } + + struct ServerInformation: Serializable, Codable { + let spicePortInternal: UInt16 + let spicePortExternal: UInt16? + let spiceHostExternal: String? + let spicePublicKey: Data + let spicePassword: String + } + + struct Reply: Serializable, Codable { + let serverInfo: ServerInformation + } + } + + struct StopVirtualMachine: Message { + static let id = UTMRemoteMessageServer.stopVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + let method: UTMVirtualMachineStopMethod + } + + struct Reply: Serializable, Codable {} + } + + struct RestartVirtualMachine: Message { + static let id = UTMRemoteMessageServer.restartVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + } + + struct Reply: Serializable, Codable {} + } + + struct PauseVirtualMachine: Message { + static let id = UTMRemoteMessageServer.pauseVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + } + + struct Reply: Serializable, Codable {} + } + + struct ResumeVirtualMachine: Message { + static let id = UTMRemoteMessageServer.resumeVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + } + + struct Reply: Serializable, Codable {} + } + + struct SaveSnapshotVirtualMachine: Message { + static let id = UTMRemoteMessageServer.saveSnapshotVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + let name: String? + } + + struct Reply: Serializable, Codable {} + } + + struct DeleteSnapshotVirtualMachine: Message { + static let id = UTMRemoteMessageServer.deleteSnapshotVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + let name: String? + } + + struct Reply: Serializable, Codable {} + } + + struct RestoreSnapshotVirtualMachine: Message { + static let id = UTMRemoteMessageServer.restoreSnapshotVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + let name: String? + } + + struct Reply: Serializable, Codable {} + } + + struct ChangePointerTypeVirtualMachine: Message { + static let id = UTMRemoteMessageServer.changePointerTypeVirtualMachine + + struct Request: Serializable, Codable { + let id: UUID + let isTabletMode: Bool + } + + struct Reply: Serializable, Codable {} + } +} + +extension Serializable where Self == UTMRemoteMessageServer.GetQEMUConfiguration.Reply { + static func decode(_ data: Data) throws -> Self { + let decoder = Decoder() + decoder.userInfo[.dataURL] = URL(fileURLWithPath: "/") + return try decoder.decode(Self.self, from: data) + } +} + +extension Serializable where Self == UTMRemoteMessageClient.QEMUConfigurationHasChanged.Request { + static func decode(_ data: Data) throws -> Self { + let decoder = Decoder() + decoder.userInfo[.dataURL] = URL(fileURLWithPath: "/") + return try decoder.decode(Self.self, from: data) + } +} + +extension UTMRemoteMessageClient { + struct ClientHandshake: Message { + static let id = UTMRemoteMessageClient.clientHandshake + + struct Request: Serializable, Codable { + let version: Int + } + + struct Reply: Serializable, Codable { + let version: Int + let capabilities: UTMCapabilities + } + } + + struct ListHasChanged: Message { + static let id = UTMRemoteMessageClient.listHasChanged + + struct Request: Serializable, Codable { + let ids: [UUID] + } + + struct Reply: Serializable, Codable {} + } + + struct QEMUConfigurationHasChanged: Message { + static let id = UTMRemoteMessageClient.qemuConfigurationHasChanged + + struct Request: Serializable, Codable { + let id: UUID + let configuration: UTMQemuConfiguration + } + + struct Reply: Serializable, Codable {} + } + + struct MountedDrivesHasChanged: Message { + static let id = UTMRemoteMessageClient.mountedDrivesHasChanged + + struct Request: Serializable, Codable { + let id: UUID + let mountedDrives: [String: String] + } + + struct Reply: Serializable, Codable {} + } + + struct VirtualMachineDidTransition: Message { + static let id = UTMRemoteMessageClient.virtualMachineDidTransition + + struct Request: Serializable, Codable { + let id: UUID + let state: UTMVirtualMachineState + let isTakeoverAllowed: Bool + } + + struct Reply: Serializable, Codable {} + } + + struct VirtualMachineDidError: Message { + static let id = UTMRemoteMessageClient.virtualMachineDidError + + struct Request: Serializable, Codable { + let id: UUID + let errorMessage: String + } + + struct Reply: Serializable, Codable {} + } +} diff --git a/Remote/UTMRemoteServer.swift b/Remote/UTMRemoteServer.swift new file mode 100644 index 00000000..ee39e3be --- /dev/null +++ b/Remote/UTMRemoteServer.swift @@ -0,0 +1,981 @@ +// +// Copyright © 2023 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 Foundation +import Combine +import Network +import SwiftConnect +import SwiftPortmap +import UserNotifications + +let service = "_utm_server._tcp" + +actor UTMRemoteServer { + fileprivate let data: UTMData + private let keyManager = UTMRemoteKeyManager(forClient: false) + private let center = UNUserNotificationCenter.current() + private let connectionQueue = DispatchQueue(label: "UTM Remote Server Connection") + let state: State + + private var cancellables = Set<AnyCancellable>() + private var notificationDelegate: NotificationDelegate? + private var listener: Task<Void, Error>? + private var pendingConnections: [State.ClientFingerprint: Connection] = [:] + private var establishedConnections: [State.ClientFingerprint: Remote] = [:] + private var natPort: SwiftPortmap.Port? + + private func _replaceCancellables(with set: Set<AnyCancellable>) { + cancellables = set + } + + @Setting("ServerAutostart") private var isServerAutostart: Bool = false + @Setting("ServerExternal") private var isServerExternal: Bool = false + @Setting("ServerAutoblock") private var isServerAutoblock: Bool = false + @Setting("ServerPort") private var serverPort: Int = 0 + @Setting("ServerPasswordRequired") private var isServerPasswordRequired: Bool = false + @Setting("ServerPassword") private var serverPassword: String = "" + + @MainActor + init(data: UTMData) { + let _state = State() + var _cancellables = Set<AnyCancellable>() + self.data = data + self.state = _state + + _cancellables.insert(_state.$approvedClients.sink { approved in + Task { + await self.approvedClientsHasChanged(approved) + } + }) + _cancellables.insert(_state.$blockedClients.sink { blocked in + Task { + await self.blockedClientsHasChanged(blocked) + } + }) + _cancellables.insert(_state.$connectedClients.sink { connected in + Task { + await self.connectedClientsHasChanged(connected) + } + }) + _cancellables.insert(_state.$serverAction.sink { action in + guard action != .none else { + return + } + Task { + switch action { + case .stop: + await self.stop() + break + case .start: + await self.start() + break + case .reset: + await self.resetServer() + break + default: + break + } + self.state.requestServerAction(.none) + } + }) + // this is a really ugly way to make sure that we keep a reference to the AnyCancellables even though + // we cannot access self._cancellables from init() due to it being associated with @MainActor. + // it should be fine because we only need to make sure the references are not dropped, we will never + // actually read from _cancellables + Task { + await self._replaceCancellables(with: _cancellables) + } + } + + private func withErrorNotification(_ body: () async throws -> Void) async { + do { + try await body() + } catch { + if case .silentError(let error) = error as? ServerError { + logger.error("Error message inhibited: \(error)") + } else { + await notifyError(error) + } + } + } + + private var metadata: NWTXTRecord { + NWTXTRecord(["Model": MacDevice.current.model]) + } + + func start() async { + do { + try await center.requestAuthorization(options: .alert) + } catch { + logger.error("Failed to authorize notifications.") + } + await withErrorNotification { + guard await !state.isServerActive else { + return + } + try await keyManager.load() + await state.setServerFingerprint(keyManager.fingerprint!) + registerNotifications() + listener = Task { + await withErrorNotification { + if isServerExternal && serverPort > 0 { + natPort = Port.TCP(internalPort: UInt16(serverPort)) + natPort!.mappingChangedHandler = { port in + Task { + let address = try? await port.externalIpv4Address + let port = try? await port.externalPort + await self.state.setExternalAddress(address, port: port) + } + } + await withErrorNotification { + guard try await natPort!.externalPort == serverPort else { + throw ServerError.natReservationMismatch(serverPort) + } + } + } + let port = serverPort > 0 ? NWEndpoint.Port(integerLiteral: UInt16(serverPort)) : .any + for try await connection in Connection.advertise(on: port, forServiceType: service, txtRecord: metadata, connectionQueue: connectionQueue, identity: keyManager.identity) { + let connection = try? await Connection(connection: connection, connectionQueue: connectionQueue) { connection, error in + Task { + guard let fingerprint = connection.fingerprint else { + return + } + if !(error is NWError) { + // connection errors are too noisy + await self.notifyError(error) + } + await self.state.disconnect(fingerprint) + } + } + if let connection = connection { + await newRemoteConnection(connection) + } + } + } + natPort = nil + await stop() + } + await state.setServerActive(true) + } + } + + func stop() async { + await state.disconnectAll() + unregisterNotifications() + if let listener = listener { + self.listener = nil + listener.cancel() + _ = await listener.result + } + await state.setExternalAddress() + await state.setServerActive(false) + } + + private func newRemoteConnection(_ connection: Connection) async { + let remoteAddress = connection.connection.endpoint.hostname ?? "\(connection.connection.endpoint)" + guard let fingerprint = connection.fingerprint else { + connection.close() + return + } + guard await !state.isBlocked(fingerprint) else { + connection.close() + return + } + await state.seen(fingerprint, name: remoteAddress) + if await state.isApproved(fingerprint) { + await notifyNewConnection(remoteAddress: remoteAddress, fingerprint: fingerprint) + await establishConnection(connection) + } else if isServerAutoblock { + await state.block(fingerprint) + connection.close() + } else { + pendingConnections[fingerprint] = connection + await notifyNewConnection(remoteAddress: remoteAddress, fingerprint: fingerprint, isUnknown: true) + } + } + + private func approvedClientsHasChanged(_ approvedClients: Set<State.Client>) async { + for approvedClient in approvedClients { + if let connection = pendingConnections.removeValue(forKey: approvedClient.fingerprint) { + await establishConnection(connection) + } + } + } + + private func blockedClientsHasChanged(_ blockedClients: Set<State.Client>) { + for blockedClient in blockedClients { + if let connection = pendingConnections.removeValue(forKey: blockedClient.fingerprint) { + connection.close() + } + } + } + + private func connectedClientsHasChanged(_ connectedClients: Set<State.ClientFingerprint>) { + for client in establishedConnections.keys { + if !connectedClients.contains(client) { + if let remote = establishedConnections.removeValue(forKey: client) { + remote.close() + Task { @MainActor in + await suspendSessions(for: remote) + } + } + } + } + } + + @MainActor + private func suspendSessions(for remote: Remote) async { + let sessions = data.vmWindows.compactMap { + if let session = $0.value as? VMRemoteSessionState { + return ($0.key, session) + } else { + return nil + } + } + await withTaskGroup(of: Void.self) { group in + for (vm, session) in sessions { + if session.client?.id == remote.id { + session.client = nil + } + group.addTask { + try? await vm.wrapped?.pause() + } + } + await group.waitForAll() + } + } + + private func establishConnection(_ connection: Connection) async { + guard let fingerprint = connection.fingerprint else { + connection.close() + return + } + await withErrorNotification { + let remote = Remote() + let local = Local(server: self, client: remote) + let peer = Peer(connection: connection, localInterface: local) + remote.peer = peer + do { + try await remote.handshake() + } catch { + if let error = error as? NWError, case .posix(let code) = error, code == .ECONNRESET { + // if the user canceled the connection, we don't do anything + throw ServerError.silentError(error) + } + peer.close() + throw error + } + establishedConnections.updateValue(remote, forKey: fingerprint) + await state.connect(fingerprint) + } + } + + private func resetServer() async { + await withErrorNotification { + try await keyManager.reset() + await state.setServerFingerprint(keyManager.fingerprint!) + } + } + + /// Send message to every connected remote client. + /// + /// If any are disconnected, we will gracefully handle the disconnect. + /// If `body` throws an error for any remote client (excluding NWError), then we ignore it. + /// - Parameter body: What to broadcast + func broadcast(_ body: @escaping (Remote) async throws -> Void) async { + enum BroadcastError: Error { + case connectionError(NWError, State.ClientFingerprint) + } + await withThrowingTaskGroup(of: Void.self) { group in + for (fingerprint, remote) in establishedConnections { + if Task.isCancelled { + break + } + group.addTask { + do { + try await body(remote) + } catch { + if let error = error as? NWError { + throw BroadcastError.connectionError(error, fingerprint) + } else { + throw error + } + } + } + } + while !group.isEmpty { + switch await group.nextResult() { + case .failure(let error): + if case BroadcastError.connectionError(_, let fingerprint) = error { + // disconnect any clients who failed to respond + await state.disconnect(fingerprint) + } else { + logger.error("client returned error on broadcast: \(error)") + } + default: + break + } + } + } + } +} + +extension UTMRemoteServer { + private class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate { + private let state: UTMRemoteServer.State + + init(state: UTMRemoteServer.State) { + self.state = state + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions { + .banner + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + Task { + let userInfo = response.notification.request.content.userInfo + guard let hexString = userInfo["FINGERPRINT"] as? String, let fingerprint = State.ClientFingerprint(hexString: hexString) else { + return + } + switch response.actionIdentifier { + case "ALLOW_ACTION": + await state.approve(fingerprint) + case "DENY_ACTION": + await state.block(fingerprint) + case "DISCONNECT_ACTION": + await state.disconnect(fingerprint) + default: + break + } + completionHandler() + } + } + } + + private func registerNotifications() { + let allowAction = UNNotificationAction(identifier: "ALLOW_ACTION", + title: NSString.localizedUserNotificationString(forKey: "Allow", arguments: nil), + options: []) + let denyAction = UNNotificationAction(identifier: "DENY_ACTION", + title: NSString.localizedUserNotificationString(forKey: "Deny", arguments: nil), + options: []) + let disconnectAction = UNNotificationAction(identifier: "DISCONNECT_ACTION", + title: NSString.localizedUserNotificationString(forKey: "Disconnect", arguments: nil), + options: []) + let unknownRemoteCategory = UNNotificationCategory(identifier: "UNKNOWN_REMOTE_CLIENT", + actions: [denyAction, allowAction], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: NSString.localizedUserNotificationString(forKey: "New unknown remote client connection.", arguments: nil), + options: .customDismissAction) + let trustedRemoteCategory = UNNotificationCategory(identifier: "TRUSTED_REMOTE_CLIENT", + actions: [disconnectAction], + intentIdentifiers: [], + hiddenPreviewsBodyPlaceholder: NSString.localizedUserNotificationString(forKey: "New trusted remote client connection.", arguments: nil), + options: []) + center.setNotificationCategories([unknownRemoteCategory, trustedRemoteCategory]) + notificationDelegate = NotificationDelegate(state: state) + center.delegate = notificationDelegate + } + + private func unregisterNotifications() { + center.setNotificationCategories([]) + notificationDelegate = nil + center.delegate = nil + } + + private func notifyNewConnection(remoteAddress: String, fingerprint: State.ClientFingerprint, isUnknown: Bool = false) async { + let settings = await center.notificationSettings() + let combinedFingerprint = (fingerprint ^ keyManager.fingerprint!).hexString() + guard settings.authorizationStatus == .authorized else { + logger.info("Notifications disabled, ignoring connection request from '\(remoteAddress)' with fingerprint '\(combinedFingerprint)'") + return + } + let content = UNMutableNotificationContent() + if isUnknown { + content.title = NSString.localizedUserNotificationString(forKey: "Unknown Remote Client", arguments: nil) + content.body = NSString.localizedUserNotificationString(forKey: "A client with fingerprint '%@' is attempting to connect.", arguments: [combinedFingerprint]) + content.categoryIdentifier = "UNKNOWN_REMOTE_CLIENT" + } else { + content.title = NSString.localizedUserNotificationString(forKey: "Remote Client Connected", arguments: nil) + content.body = NSString.localizedUserNotificationString(forKey: "Established connection from %@.", arguments: [remoteAddress]) + content.categoryIdentifier = "TRUSTED_REMOTE_CLIENT" + } + let clientFingerprint = fingerprint.hexString() + content.userInfo = ["FINGERPRINT": clientFingerprint] + let request = UNNotificationRequest(identifier: clientFingerprint, + content: content, + trigger: nil) + do { + try await center.add(request) + if !isUnknown { + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(15)) { + self.center.removeDeliveredNotifications(withIdentifiers: [clientFingerprint]) + } + } + } catch { + logger.error("Error sending remote connection request: \(error.localizedDescription)") + } + } + + fileprivate func notifyError(_ error: Error) async { + logger.error("UTM Remote Server error: '\(error)'") + let settings = await center.notificationSettings() + guard settings.authorizationStatus == .authorized else { + return + } + let content = UNMutableNotificationContent() + content.title = NSString.localizedUserNotificationString(forKey: "UTM Remote Server Error", arguments: nil) + content.body = error.localizedDescription + let request = UNNotificationRequest(identifier: UUID().uuidString, + content: content, + trigger: nil) + do { + try await center.add(request) + } catch { + logger.error("Error sending error notification: \(error.localizedDescription)") + } + } +} + +extension UTMRemoteServer { + @MainActor + class State: ObservableObject { + typealias ClientFingerprint = [UInt8] + typealias ServerFingerprint = [UInt8] + struct Client: Codable, Identifiable, Hashable { + let fingerprint: ClientFingerprint + var name: String + var lastSeen: Date + + var id: ClientFingerprint { + fingerprint + } + + func hash(into hasher: inout Hasher) { + hasher.combine(fingerprint) + } + + static func == (lhs: Client, rhs: Client) -> Bool { + lhs.hashValue == rhs.hashValue + } + } + + enum ServerAction { + case none + case stop + case start + case reset + } + + @Published var allClients: [Client] { + didSet { + let all = Set(allClients) + approvedClients.subtract(approvedClients.subtracting(all)) + blockedClients.subtract(blockedClients.subtracting(all)) + connectedClients.subtract(connectedClients.subtracting(all.map({ $0.fingerprint }))) + } + } + + @Published var approvedClients: Set<Client> { + didSet { + UserDefaults.standard.setValue(try! approvedClients.propertyList(), forKey: "TrustedClients") + } + } + + @Published var blockedClients: Set<Client> { + didSet { + UserDefaults.standard.setValue(try! blockedClients.propertyList(), forKey: "BlockedClients") + } + } + + @Published var connectedClients = Set<ClientFingerprint>() + + @Published var serverAction: ServerAction = .none + + var isBusy: Bool { + serverAction != .none + } + + @Published private(set) var isServerActive = false + + @Published private(set) var serverFingerprint: ServerFingerprint = [] { + didSet { + UserDefaults.standard.setValue(serverFingerprint.hexString(), forKey: "ServerFingerprint") + } + } + + @Published private(set) var externalIPAddress: String? + + @Published private(set) var externalPort: UInt16? + + init() { + var _approvedClients = Set<Client>() + if let array = UserDefaults.standard.array(forKey: "TrustedClients") { + if let clients = try? Set<Client>(fromPropertyList: array) { + _approvedClients = clients + } + } + self.approvedClients = _approvedClients + var _blockedClients = Set<Client>() + if let array = UserDefaults.standard.array(forKey: "BlockedClients") { + if let clients = try? Set<Client>(fromPropertyList: array) { + _blockedClients = clients + } + } + self.blockedClients = _blockedClients + self.allClients = Array(_approvedClients) + Array(_blockedClients) + if let value = UserDefaults.standard.string(forKey: "ServerFingerprint"), let serverFingerprint = ServerFingerprint(hexString: value) { + self.serverFingerprint = serverFingerprint + } + } + + func isConnected(_ fingerprint: ClientFingerprint) -> Bool { + connectedClients.contains(fingerprint) + } + + func isApproved(_ fingerprint: ClientFingerprint) -> Bool { + approvedClients.contains(where: { $0.fingerprint == fingerprint }) && !isBlocked(fingerprint) + } + + func isBlocked(_ fingerprint: ClientFingerprint) -> Bool { + blockedClients.contains(where: { $0.fingerprint == fingerprint }) + } + + fileprivate func setServerActive(_ isActive: Bool) { + isServerActive = isActive + } + + func requestServerAction(_ action: ServerAction) { + serverAction = action + } + + private func client(forFingerprint fingerprint: ClientFingerprint, name: String? = nil) -> (Int?, Client) { + if let idx = allClients.firstIndex(where: { $0.fingerprint == fingerprint }) { + if let name = name { + allClients[idx].name = name + } + return (idx, allClients[idx]) + } else { + return (nil, Client(fingerprint: fingerprint, name: name ?? "", lastSeen: Date())) + } + } + + func seen(_ fingerprint: ClientFingerprint, name: String? = nil) { + var (idx, client) = client(forFingerprint: fingerprint, name: name) + client.lastSeen = Date() + if let idx = idx { + allClients[idx] = client + } else { + allClients.append(client) + } + } + + fileprivate func connect(_ fingerprint: ClientFingerprint, name: String? = nil) { + connectedClients.insert(fingerprint) + } + + func disconnect(_ fingerprint: ClientFingerprint) { + connectedClients.remove(fingerprint) + } + + func disconnectAll() { + connectedClients.removeAll() + } + + func approve(_ fingerprint: ClientFingerprint) { + let (_, client) = client(forFingerprint: fingerprint) + approvedClients.insert(client) + blockedClients.remove(client) + } + + func block(_ fingerprint: ClientFingerprint) { + let (_, client) = client(forFingerprint: fingerprint) + approvedClients.remove(client) + blockedClients.insert(client) + } + + fileprivate func setServerFingerprint(_ fingerprint: ServerFingerprint) { + serverFingerprint = fingerprint + } + + fileprivate func setExternalAddress(_ address: String? = nil, port: UInt16? = nil) { + externalIPAddress = address + externalPort = port + } + } +} + +extension UTMRemoteServer { + class Local: LocalInterface { + typealias M = UTMRemoteMessageServer + + private let server: UTMRemoteServer + private let client: UTMRemoteServer.Remote + private var isAuthenticated: Bool = false + + private var data: UTMData { + server.data + } + + init(server: UTMRemoteServer, client: UTMRemoteServer.Remote) { + self.server = server + self.client = client + } + + func handle(message: M, data: Data) async throws -> Data { + guard isAuthenticated || message == .serverHandshake else { + throw ServerError.notAuthenticated + } + switch message { + case .serverHandshake: + return try await _handshake(parameters: .decode(data)).encode() + case .listVirtualMachines: + return try await _listVirtualMachines(parameters: .decode(data)).encode() + case .reorderVirtualMachines: + return try await _reorderVirtualMachines(parameters: .decode(data)).encode() + case .getVirtualMachineInformation: + return try await _getVirtualMachineInformation(parameters: .decode(data)).encode() + case .getQEMUConfiguration: + return try await _getQEMUConfiguration(parameters: .decode(data)).encode() + case .getPackageSize: + return try await _getPackageSize(parameters: .decode(data)).encode() + case .getPackageFile: + return try await _getPackageFile(parameters: .decode(data)).encode() + case .sendPackageFile: + return try await _sendPackageFile(parameters: .decode(data)).encode() + case .deletePackageFile: + return try await _deletePackageFile(parameters: .decode(data)).encode() + case .mountGuestToolsOnVirtualMachine: + return try await _mountGuestToolsOnVirtualMachine(parameters: .decode(data)).encode() + case .startVirtualMachine: + return try await _startVirtualMachine(parameters: .decode(data)).encode() + case .stopVirtualMachine: + return try await _stopVirtualMachine(parameters: .decode(data)).encode() + case .restartVirtualMachine: + return try await _restartVirtualMachine(parameters: .decode(data)).encode() + case .pauseVirtualMachine: + return try await _pauseVirtualMachine(parameters: .decode(data)).encode() + case .resumeVirtualMachine: + return try await _resumeVirtualMachine(parameters: .decode(data)).encode() + case .saveSnapshotVirtualMachine: + return try await _saveSnapshotVirtualMachine(parameters: .decode(data)).encode() + case .deleteSnapshotVirtualMachine: + return try await _deleteSnapshotVirtualMachine(parameters: .decode(data)).encode() + case .restoreSnapshotVirtualMachine: + return try await _restoreSnapshotVirtualMachine(parameters: .decode(data)).encode() + case .changePointerTypeVirtualMachine: + return try await _changePointerTypeVirtualMachine(parameters: .decode(data)).encode() + } + } + + @MainActor + private func findVM(withId id: UUID) throws -> VMData { + let vm = data.virtualMachines.first(where: { $0.id == id }) + if let vm = vm, let _ = vm.wrapped { + return vm + } else { + throw UTMRemoteServer.ServerError.notFound(id) + } + } + + @MainActor + private func packageFileHasChanged(for vm: VMData, relativePathComponents: [String]) throws { + if relativePathComponents.count == 1 && relativePathComponents[0] == kUTMBundleScreenshotFilename { + try vm.wrapped?.reloadScreenshotFromFile() + } + } + + private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply { + let serverPassword = await server.serverPassword + if await server.isServerPasswordRequired && !serverPassword.isEmpty { + if serverPassword == parameters.password { + isAuthenticated = true + } + } else { + isAuthenticated = true + } + return .init(version: UTMRemoteMessageServer.version, isAuthenticated: isAuthenticated, capabilities: .current, model: MacDevice.current.model) + } + + private func _listVirtualMachines(parameters: M.ListVirtualMachines.Request) async throws -> M.ListVirtualMachines.Reply { + let ids = await Task { @MainActor in + data.virtualMachines.map({ $0.id }) + }.value + return .init(ids: ids) + } + + private func _reorderVirtualMachines(parameters: M.ReorderVirtualMachines.Request) async throws -> M.ReorderVirtualMachines.Reply { + await Task { @MainActor in + let vms = data.virtualMachines + let source = parameters.ids.reduce(into: IndexSet(), { indexSet, id in + if let index = vms.firstIndex(where: { $0.id == id }) { + indexSet.insert(index) + } + }) + let destination = min(max(0, parameters.offset), vms.count) + data.listMove(fromOffsets: source, toOffset: destination) + return .init() + }.value + } + + private func _getVirtualMachineInformation(parameters: M.GetVirtualMachineInformation.Request) async throws -> M.GetVirtualMachineInformation.Reply { + let informations = try await Task { @MainActor in + try parameters.ids.map { id in + let vm = try findVM(withId: id) + let mountedDrives = vm.registryEntry?.externalDrives.mapValues({ $0.path }) ?? [:] + let isTakeoverAllowed = data.vmWindows[vm] is VMRemoteSessionState && (vm.state == .started || vm.state == .paused) + return M.VirtualMachineInformation(id: vm.id, + name: vm.detailsTitleLabel, + path: vm.pathUrl.path, + isShortcut: vm.isShortcut, + isSuspended: vm.registryEntry?.isSuspended ?? false, + isTakeoverAllowed: isTakeoverAllowed, + backend: vm.wrapped is UTMQemuVirtualMachine ? .qemu : .unknown, + state: vm.wrapped?.state ?? .stopped, + mountedDrives: mountedDrives) + } + }.value + return .init(informations: informations) + } + + private func _getQEMUConfiguration(parameters: M.GetQEMUConfiguration.Request) async throws -> M.GetQEMUConfiguration.Reply { + let vm = try await findVM(withId: parameters.id) + if let config = await vm.config as? UTMQemuConfiguration { + return .init(configuration: config) + } else { + throw ServerError.invalidBackend + } + } + + private func _getPackageSize(parameters: M.GetPackageSize.Request) async throws -> M.GetPackageSize.Reply { + let vm = try await findVM(withId: parameters.id) + let size = await data.computeSize(for: vm) + return .init(size: size) + } + + private func _getPackageFile(parameters: M.GetPackageFile.Request) async throws -> M.GetPackageFile.Reply { + let vm = try await findVM(withId: parameters.id) + let fm = FileManager.default + let pathUrl = await vm.pathUrl + let fileUrl = parameters.relativePathComponents.reduce(pathUrl, { $0.appendingPathComponent($1) }) + guard let lastModified = try fm.attributesOfItem(atPath: fileUrl.path)[.modificationDate] as? Date else { + throw ServerError.failedToAccessFile + } + if let requestLastModified = parameters.lastModified { + if lastModified.distance(to: requestLastModified).rounded(.towardZero) == 0 { + return .init(data: nil, lastModified: lastModified) + } + } + guard let data = fm.contents(atPath: fileUrl.path) else { + throw ServerError.failedToAccessFile + } + return .init(data: data, lastModified: lastModified) + } + + private func _sendPackageFile(parameters: M.SendPackageFile.Request) async throws -> M.SendPackageFile.Reply { + let vm = try await findVM(withId: parameters.id) + let fm = FileManager.default + let pathUrl = await vm.pathUrl + let fileUrl = parameters.relativePathComponents.reduce(pathUrl, { $0.appendingPathComponent($1) }) + try? fm.removeItem(at: fileUrl) + guard fm.createFile(atPath: fileUrl.path, contents: parameters.data, attributes: [.modificationDate: parameters.lastModified]) else { + throw ServerError.failedToAccessFile + } + try await packageFileHasChanged(for: vm, relativePathComponents: parameters.relativePathComponents) + return .init() + } + + private func _deletePackageFile(parameters: M.DeletePackageFile.Request) async throws -> M.DeletePackageFile.Reply { + let vm = try await findVM(withId: parameters.id) + let fm = FileManager.default + let pathUrl = await vm.pathUrl + let fileUrl = parameters.relativePathComponents.reduce(pathUrl, { $0.appendingPathComponent($1) }) + try fm.removeItem(at: fileUrl) + try await packageFileHasChanged(for: vm, relativePathComponents: parameters.relativePathComponents) + return .init() + } + + 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) + } + return .init() + } + + private func _startVirtualMachine(parameters: M.StartVirtualMachine.Request) async throws -> M.StartVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + let serverInfo = try await data.startRemote(vm: vm, options: parameters.options, forClient: client) + return .init(serverInfo: serverInfo) + } + + private func _stopVirtualMachine(parameters: M.StopVirtualMachine.Request) async throws -> M.StopVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + try await vm.wrapped!.stop(usingMethod: parameters.method) + return .init() + } + + private func _restartVirtualMachine(parameters: M.RestartVirtualMachine.Request) async throws -> M.RestartVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + try await vm.wrapped!.restart() + return .init() + } + + private func _pauseVirtualMachine(parameters: M.PauseVirtualMachine.Request) async throws -> M.PauseVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + try await vm.wrapped!.pause() + return .init() + } + + private func _resumeVirtualMachine(parameters: M.ResumeVirtualMachine.Request) async throws -> M.ResumeVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + try await vm.wrapped!.resume() + return .init() + } + + private func _saveSnapshotVirtualMachine(parameters: M.SaveSnapshotVirtualMachine.Request) async throws -> M.SaveSnapshotVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + try await vm.wrapped!.saveSnapshot(name: parameters.name) + return .init() + } + + private func _deleteSnapshotVirtualMachine(parameters: M.DeleteSnapshotVirtualMachine.Request) async throws -> M.DeleteSnapshotVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + try await vm.wrapped!.deleteSnapshot(name: parameters.name) + return .init() + } + + private func _restoreSnapshotVirtualMachine(parameters: M.RestoreSnapshotVirtualMachine.Request) async throws -> M.RestoreSnapshotVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + try await vm.wrapped!.restoreSnapshot(name: parameters.name) + return .init() + } + + private func _changePointerTypeVirtualMachine(parameters: M.ChangePointerTypeVirtualMachine.Request) async throws -> M.ChangePointerTypeVirtualMachine.Reply { + let vm = try await findVM(withId: parameters.id) + guard let wrapped = await vm.wrapped as? UTMQemuVirtualMachine else { + throw ServerError.invalidBackend + } + try await wrapped.changeInputTablet(parameters.isTabletMode) + return .init() + } + } +} + +extension UTMRemoteServer { + class Remote: Identifiable { + typealias M = UTMRemoteMessageClient + fileprivate(set) var peer: Peer<UTMRemoteMessageServer>! + let id = UUID() + + func close() { + peer.close() + } + + func handshake() async throws { + guard try await _handshake(parameters: .init(version: UTMRemoteMessageClient.version)).version == UTMRemoteMessageClient.version else { + throw ServerError.versionMismatch + } + } + + func listHasChanged(ids: [UUID]) async throws { + try await _listHasChanged(parameters: .init(ids: ids)) + } + + func qemuConfigurationHasChanged(id: UUID, configuration: UTMQemuConfiguration) async throws { + try await _qemuConfigurationHasChanged(parameters: .init(id: id, configuration: configuration)) + } + + func mountedDrivesHasChanged(id: UUID, mountedDrives: [String: String]) async throws { + try await _mountedDrivesHasChanged(parameters: .init(id: id, mountedDrives: mountedDrives)) + } + + func virtualMachine(id: UUID, didTransitionToState state: UTMVirtualMachineState, isTakeoverAllowed: Bool) async throws { + try await _virtualMachineDidTransition(parameters: .init(id: id, state: state, isTakeoverAllowed: isTakeoverAllowed)) + } + + func virtualMachine(id: UUID, didErrorWithMessage message: String) async throws { + try await _virtualMachineDidError(parameters: .init(id: id, errorMessage: message)) + } + + private func _handshake(parameters: M.ClientHandshake.Request) async throws -> M.ClientHandshake.Reply { + try await M.ClientHandshake.send(parameters, to: peer) + } + + @discardableResult + private func _listHasChanged(parameters: M.ListHasChanged.Request) async throws -> M.ListHasChanged.Reply { + try await M.ListHasChanged.send(parameters, to: peer) + } + + @discardableResult + private func _qemuConfigurationHasChanged(parameters: M.QEMUConfigurationHasChanged.Request) async throws -> M.QEMUConfigurationHasChanged.Reply { + try await M.QEMUConfigurationHasChanged.send(parameters, to: peer) + } + + @discardableResult + private func _mountedDrivesHasChanged(parameters: M.MountedDrivesHasChanged.Request) async throws -> M.MountedDrivesHasChanged.Reply { + try await M.MountedDrivesHasChanged.send(parameters, to: peer) + } + + @discardableResult + private func _virtualMachineDidTransition(parameters: M.VirtualMachineDidTransition.Request) async throws -> M.VirtualMachineDidTransition.Reply { + try await M.VirtualMachineDidTransition.send(parameters, to: peer) + } + + @discardableResult + private func _virtualMachineDidError(parameters: M.VirtualMachineDidError.Request) async throws -> M.VirtualMachineDidError.Reply { + try await M.VirtualMachineDidError.send(parameters, to: peer) + } + } +} + +extension UTMRemoteServer { + enum ServerError: LocalizedError { + case silentError(Error) + case natReservationMismatch(Int) + case notAuthenticated + case versionMismatch + case notFound(UUID) + case invalidBackend + case failedToAccessFile + + var errorDescription: String? { + switch self { + case .silentError(let error): + return error.localizedDescription + case .natReservationMismatch(let port): + return String.localizedStringWithFormat(NSLocalizedString("Cannot reserve port '%@' for external access from NAT. Make sure no other device on the network has reserved it.", comment: "UTMRemoteServer"), port) + case .notAuthenticated: + return NSLocalizedString("Not authenticated.", comment: "UTMRemoteServer") + case .versionMismatch: + return NSLocalizedString("The client interface version does not match the server.", comment: "UTMRemoteServer") + case .notFound(let id): + return String.localizedStringWithFormat(NSLocalizedString("Cannot find VM with ID: %@", comment: "UTMRemoteServer"), id.uuidString) + case .invalidBackend: + return NSLocalizedString("Invalid backend.", comment: "UTMRemoteServer") + case .failedToAccessFile: + return NSLocalizedString("Failed to access file.", comment: "UTMRemoteServer") + } + } + } +} + +extension Connection { + var fingerprint: [UInt8]? { + return peerCertificateChain.first?.fingerprint() + } +} diff --git a/Remote/UTMRemoteSpiceVirtualMachine.swift b/Remote/UTMRemoteSpiceVirtualMachine.swift new file mode 100644 index 00000000..c4736e4f --- /dev/null +++ b/Remote/UTMRemoteSpiceVirtualMachine.swift @@ -0,0 +1,424 @@ +// +// Copyright © 2024 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 Foundation + +final class UTMRemoteSpiceVirtualMachine: UTMSpiceVirtualMachine { + struct Capabilities: UTMVirtualMachineCapabilities { + var supportsProcessKill: Bool { + true + } + + var supportsSnapshots: Bool { + true + } + + var supportsScreenshots: Bool { + true + } + + var supportsDisposibleMode: Bool { + true + } + + var supportsRecoveryMode: Bool { + false + } + + var supportsRemoteSession: Bool { + false + } + } + + static let capabilities = Capabilities() + + private var server: UTMRemoteClient.Remote + + init(packageUrl: URL, configuration: UTMQemuConfiguration, isShortcut: Bool) throws { + throw UTMVirtualMachineError.notImplemented + } + + init(forRemoteServer server: UTMRemoteClient.Remote, remotePath: String, entry: UTMRegistryEntry, config: UTMQemuConfiguration) { + self.pathUrl = URL(fileURLWithPath: remotePath) + self.config = config + self.registryEntry = entry + self.server = server + _state = State(vm: self) + } + + private(set) var pathUrl: URL + + private(set) var isShortcut: Bool = false + + private(set) var isRunningAsDisposible: Bool = false + + weak var delegate: (UTMVirtualMachineDelegate)? + + var onConfigurationChange: (() -> Void)? + + var onStateChange: (() -> Void)? + + private(set) var config: UTMQemuConfiguration { + willSet { + onConfigurationChange?() + } + } + + private(set) var registryEntry: UTMRegistryEntry { + willSet { + onConfigurationChange?() + } + } + + private var _state: State! + + private(set) var state: UTMVirtualMachineState = .stopped { + willSet { + onStateChange?() + } + + didSet { + if state == .stopped { + virtualMachineDidStop() + } + delegate?.virtualMachine(self, didTransitionToState: state) + } + } + + var screenshot: UTMVirtualMachineScreenshot? { + willSet { + onStateChange?() + } + } + + private(set) var snapshotUnsupportedError: Error? + + weak var ioServiceDelegate: UTMSpiceIODelegate? { + didSet { + if let ioService = ioService { + ioService.delegate = ioServiceDelegate + } + } + } + + private(set) var ioService: UTMSpiceIO? { + didSet { + oldValue?.delegate = nil + ioService?.delegate = ioServiceDelegate + } + } + + var changeCursorRequestInProgress: Bool = false + + private weak var screenshotTimer: Timer? + + func reload(from packageUrl: URL?) throws { + throw UTMVirtualMachineError.notImplemented + } + + @MainActor + func reload(usingConfiguration config: UTMQemuConfiguration) { + self.config = config + updateConfigFromRegistry() + } + + @MainActor + func updateRegistry(_ entry: UTMRegistryEntry) { + self.registryEntry = entry + } + + func updateConfigFromRegistry() { + // not needed + } + + func changeUuid(to uuid: UUID, name: String?, copyingEntry entry: UTMRegistryEntry?) { + // not needed + } + + func reconnectServer(_ body: () async throws -> UTMRemoteClient.Remote) async throws { + try await _state.operation(during: .resuming) { + self.server = try await body() + } + } +} + +extension UTMRemoteSpiceVirtualMachine { + private class ConnectCoordinator: NSObject, UTMRemoteConnectDelegate { + var continuation: CheckedContinuation<Void, Error>? + + func remoteInterface(_ remoteInterface: UTMRemoteConnectInterface, didErrorWithMessage message: String) { + remoteInterface.connectDelegate = nil + continuation?.resume(throwing: VMError.spiceConnectError(message)) + continuation = nil + } + + func remoteInterfaceDidConnect(_ remoteInterface: UTMRemoteConnectInterface) { + remoteInterface.connectDelegate = nil + continuation?.resume() + continuation = nil + } + } +} + +extension UTMRemoteSpiceVirtualMachine { + private func connect(_ serverInfo: UTMRemoteMessageServer.StartVirtualMachine.ServerInformation, options: UTMSpiceIOOptions, remoteConnection: Bool) async throws -> UTMSpiceIO { + let ioService = UTMSpiceIO(host: remoteConnection ? serverInfo.spiceHostExternal! : server.host, + tlsPort: Int(remoteConnection ? serverInfo.spicePortExternal! : serverInfo.spicePortInternal), + serverPublicKey: serverInfo.spicePublicKey, + password: serverInfo.spicePassword, + options: options) + ioService.logHandler = { (line: String) -> Void in + guard !line.contains("spice_make_scancode") else { + return // do not log key presses for privacy reasons + } + NSLog("%@", line) // FIXME: log to file + } + try ioService.start() + let coordinator = ConnectCoordinator() + try await withCheckedThrowingContinuation { continuation in + coordinator.continuation = continuation + ioService.connectDelegate = coordinator + do { + try ioService.connect() + } catch { + ioService.connectDelegate = nil + continuation.resume(throwing: error) + } + } + return ioService + } + + func start(options: UTMVirtualMachineStartOptions) async throws { + try await _state.operation(before: [.stopped, .started, .paused], during: .starting, after: .started) { + let spiceServer = try await server.startVirtualMachine(id: id, options: options) + var options = UTMSpiceIOOptions() + if await !config.sound.isEmpty { + options.insert(.hasAudio) + } + if await config.sharing.hasClipboardSharing { + options.insert(.hasClipboardSharing) + } + if await config.sharing.isDirectoryShareReadOnly { + options.insert(.isShareReadOnly) + } + #if false // FIXME: verbose logging is broken on iOS + if hasDebugLog { + options.insert(.hasDebugLog) + } + #endif + do { + self.ioService = try await connect(spiceServer, options: options, remoteConnection: false) + } catch { + if spiceServer.spiceHostExternal != nil && spiceServer.spicePortExternal != nil { + // retry with external port + self.ioService = try await connect(spiceServer, options: options, remoteConnection: true) + } else { + throw error + } + } + if screenshotTimer == nil { + screenshotTimer = startScreenshotTimer() + } + } + } + + func stop(usingMethod method: UTMVirtualMachineStopMethod) async throws { + try await _state.operation(before: [.started, .paused], during: .stopping, after: .stopped) { + await saveScreenshot() + try await server.stopVirtualMachine(id: id, method: method) + } + } + + func restart() async throws { + try await _state.operation(before: [.started, .paused], during: .stopping, after: .started) { + try await server.restartVirtualMachine(id: id) + } + } + + func pause() async throws { + try await _state.operation(before: .started, during: .pausing, after: .paused) { + try await server.pauseVirtualMachine(id: id) + } + } + + func resume() async throws { + if ioService == nil { + return try await start(options: []) + } else { + try await _state.operation(before: .paused, during: .resuming, after: .started) { + try await server.resumeVirtualMachine(id: id) + } + } + } + + func saveSnapshot(name: String?) async throws { + try await _state.operation(before: [.started, .paused], during: .saving) { + await saveScreenshot() + try await server.saveSnapshotVirtualMachine(id: id, name: name) + } + } + + func deleteSnapshot(name: String?) async throws { + try await server.deleteSnapshotVirtualMachine(id: id, name: name) + } + + func restoreSnapshot(name: String?) async throws { + try await _state.operation(before: [.started, .paused], during: .saving) { + try await server.restoreSnapshotVirtualMachine(id: id, name: name) + } + } + + func loadScreenshotFromServer() async { + if let url = try? await server.getPackageFile(for: id, relativePathComponents: [kUTMBundleScreenshotFilename]) { + loadScreenshot(from: url) + } + } + + func loadScreenshot(from url: URL) { + screenshot = UTMVirtualMachineScreenshot(contentsOfURL: url) + } + + func saveScreenshot() async { + if let data = screenshot?.pngData { + try? await server.sendPackageFile(for: id, relativePathComponents: [kUTMBundleScreenshotFilename], data: data) + } + } + + private func virtualMachineDidStop() { + ioService = nil + } +} + +extension UTMRemoteSpiceVirtualMachine { + actor State { + private weak var vm: UTMRemoteSpiceVirtualMachine? + private var isInOperation: Bool = false + private(set) var state: UTMVirtualMachineState = .stopped { + didSet { + vm?.state = state + } + } + private var remoteState: UTMVirtualMachineState? + + init(vm: UTMRemoteSpiceVirtualMachine) { + self.vm = vm + } + + func operation(before: UTMVirtualMachineState, during: UTMVirtualMachineState, after: UTMVirtualMachineState? = nil, body: () async throws -> Void) async throws { + try await operation(before: [before], during: during, after: after, body: body) + } + + func operation(before: Set<UTMVirtualMachineState>? = nil, during: UTMVirtualMachineState, after: UTMVirtualMachineState? = nil, body: () async throws -> Void) async throws { + while isInOperation { + await Task.yield() + } + if let before = before { + guard before.contains(state) else { + throw VMError.operationInProgress + } + } + isInOperation = true + remoteState = nil + defer { + isInOperation = false + if let remoteState = remoteState { + state = remoteState + } + } + let previous = state + state = during + do { + try await body() + } catch { + state = previous + throw error + } + state = after ?? previous + } + + func updateRemoteState(_ state: UTMVirtualMachineState) { + self.remoteState = state + if !isInOperation && self.state != state { + self.state = state + } + } + } + + func updateRemoteState(_ state: UTMVirtualMachineState) async { + await _state.updateRemoteState(state) + } +} + +extension UTMRemoteSpiceVirtualMachine { + static func isSupported(systemArchitecture: QEMUArchitecture) -> Bool { + true // FIXME: somehow determine which architectures are supported + } +} + +extension UTMRemoteSpiceVirtualMachine { + func requestInputTablet(_ tablet: Bool) { + guard !changeCursorRequestInProgress else { + return + } + changeCursorRequestInProgress = true + Task { + defer { + changeCursorRequestInProgress = false + } + try await server.changePointerTypeVirtualMachine(id: id, toTabletMode: tablet) + ioService?.primaryInput?.requestMouseMode(!tablet) + } + } +} + +extension UTMRemoteSpiceVirtualMachine { + func eject(_ drive: UTMQemuConfigurationDrive) async throws { + // FIXME: implement remote feature + throw UTMVirtualMachineError.notImplemented + } + + func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws { + // FIXME: implement remote feature + throw UTMVirtualMachineError.notImplemented + } + +} + +extension UTMRemoteSpiceVirtualMachine { + func stopAccessingPath(_ path: String) async { + // not needed + } + + func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws { + throw UTMVirtualMachineError.notImplemented + } +} + +extension UTMRemoteSpiceVirtualMachine { + enum VMError: LocalizedError { + case spiceConnectError(String) + case operationInProgress + + var errorDescription: String? { + switch self { + case .spiceConnectError(let message): + return String.localizedStringWithFormat(NSLocalizedString("Failed to connect to SPICE: %@", comment: "UTMRemoteSpiceVirtualMachine"), message) + case .operationInProgress: + return NSLocalizedString("An operation is already in progress.", comment: "UTMRemoteSpiceVirtualMachine") + } + } + } +} diff --git a/Services/Swift-Bridging-Header.h b/Services/Swift-Bridging-Header.h index 8c5774c9..24b31c47 100644 --- a/Services/Swift-Bridging-Header.h +++ b/Services/Swift-Bridging-Header.h @@ -25,14 +25,21 @@ #include "UTMLegacyQemuConfiguration+Sharing.h" #include "UTMLegacyQemuConfiguration+System.h" #include "UTMLegacyQemuConfigurationPortForward.h" +#include "UTMLogging.h" +#if !defined(WITH_REMOTE) #include "UTMProcess.h" #include "UTMQemuSystem.h" #include "UTMJailbreak.h" -#include "UTMLogging.h" +#else +#include "UTMQemuSystemBackends.h" +#endif #include "UTMLegacyViewState.h" #include "UTMSpiceIO.h" +#include "GenerateKey.h" #if TARGET_OS_IPHONE +#if !defined(WITH_REMOTE) #include "UTMLocationManager.h" +#endif #include "VMDisplayViewController.h" //#if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION #include "VMDisplayMetalViewController.h" diff --git a/Services/UTMAppleVirtualMachine.swift b/Services/UTMAppleVirtualMachine.swift index 61f2596a..f4200865 100644 --- a/Services/UTMAppleVirtualMachine.swift +++ b/Services/UTMAppleVirtualMachine.swift @@ -40,6 +40,10 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { var supportsRecoveryMode: Bool { true } + + var supportsRemoteSession: Bool { + false + } } static let capabilities = Capabilities() @@ -85,7 +89,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { } } - private(set) var screenshot: PlatformImage? { + private(set) var screenshot: UTMVirtualMachineScreenshot? { willSet { onStateChange?() } @@ -474,7 +478,11 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { screenshot = screenshotDelegate?.screenshot return true } - + + func reloadScreenshotFromFile() { + screenshot = loadScreenshot() + } + @MainActor private func createAppleVM() throws { for i in config.serials.indices { let (fd, sfd, name) = try createPty() @@ -721,7 +729,7 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate { } protocol UTMScreenshotProvider: AnyObject { - var screenshot: PlatformImage? { get } + var screenshot: UTMVirtualMachineScreenshot? { get } } enum UTMAppleVirtualMachineError: Error { diff --git a/Services/UTMExtensions.swift b/Services/UTMExtensions.swift index 4e464941..bb4eabbf 100644 --- a/Services/UTMExtensions.swift +++ b/Services/UTMExtensions.swift @@ -16,6 +16,7 @@ import SwiftUI import UniformTypeIdentifiers +import Network extension Optional where Wrapped == String { var _bound: String? { @@ -383,4 +384,44 @@ extension String { } return Int(numeric) } + + static func random(length: Int) -> String { + let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + return String((0..<length).map{ _ in letters.randomElement()! }) + } +} + +extension Encodable { + func propertyList() throws -> Any { + let encoder = PropertyListEncoder() + encoder.outputFormat = .xml + let xml = try encoder.encode(self) + return try PropertyListSerialization.propertyList(from: xml, format: nil) + } +} + +extension Decodable { + init(fromPropertyList propertyList: Any) throws { + let data = try PropertyListSerialization.data(fromPropertyList: propertyList, format: .xml, options: 0) + let decoder = PropertyListDecoder() + self = try decoder.decode(Self.self, from: data) + } +} + +extension NWEndpoint { + var hostname: String? { + if case .hostPort(let host, _) = self { + switch host { + case .name(let hostname, _): + return hostname + case .ipv4(let address): + return "\(address)" + case .ipv6(let address): + return "\(address)" + @unknown default: + break + } + } + return nil + } } diff --git a/Services/UTMJailbreak.m b/Services/UTMJailbreak.m index 9c21e251..85003110 100644 --- a/Services/UTMJailbreak.m +++ b/Services/UTMJailbreak.m @@ -65,7 +65,7 @@ typedef struct memorystatus_memlimit_properties { int memorystatus_control(uint32_t command, int32_t pid, uint32_t flags, user_addr_t buffer, size_t buffersize); -#if !TARGET_OS_OSX && !defined(WITH_QEMU_TCI) +#if !TARGET_OS_OSX && defined(WITH_JIT) extern int csops(pid_t pid, unsigned int ops, void * useraddr, size_t usersize); extern boolean_t exc_server(mach_msg_header_t *, mach_msg_header_t *); extern int ptrace(int request, pid_t pid, caddr_t addr, int data); @@ -100,7 +100,7 @@ static bool jb_has_debugger_attached(void) { #endif bool jb_has_cs_disabled(void) { -#if TARGET_OS_OSX || defined(WITH_QEMU_TCI) +#if TARGET_OS_OSX || !defined(WITH_JIT) return false; #else int flags; @@ -236,7 +236,7 @@ static bool is_device_A12_or_newer(void) { bool jb_has_jit_entitlement(void) { #if TARGET_OS_OSX return true; -#elif defined(WITH_QEMU_TCI) +#elif !defined(WITH_JIT) return false; #else NSDictionary *entitlements = cached_app_entitlements(); @@ -330,7 +330,7 @@ bool jb_has_cs_execseg_allow_unsigned(void) { } bool jb_enable_ptrace_hack(void) { -#if TARGET_OS_OSX || defined(WITH_QEMU_TCI) +#if TARGET_OS_OSX || !defined(WITH_JIT) return false; #else bool debugged = jb_has_debugger_attached(); @@ -380,7 +380,7 @@ bool jb_increase_memlimit(void) { return ret1 == 0 && ret2 == 0; } -#if !TARGET_OS_OSX && !defined(WITH_QEMU_TCI) +#if !TARGET_OS_OSX && defined(WITH_JIT) extern const char *environ[]; static char *childArgv[] = {NULL, "debugme", NULL}; diff --git a/Services/UTMLogging.m b/Services/UTMLogging.m index 2d7812c5..7e15efa4 100644 --- a/Services/UTMLogging.m +++ b/Services/UTMLogging.m @@ -15,7 +15,9 @@ // #import "UTMLogging.h" +#if !defined(WITH_REMOTE) @import QEMUKitInternal; +#endif static UTMLogging *gLoggingInstance; @@ -42,7 +44,11 @@ void UTMLog(NSString *format, ...) { } - (void)writeLine:(NSString *)line { +#if defined(WITH_REMOTE) + NSLog(@"%@", line); +#else [QEMULogging.sharedInstance writeLine:line]; +#endif } @end diff --git a/Services/UTMPasteboard.swift b/Services/UTMPasteboard.swift index bea8de9d..7fb5a8be 100644 --- a/Services/UTMPasteboard.swift +++ b/Services/UTMPasteboard.swift @@ -26,7 +26,7 @@ typealias SystemPasteboardType = NSPasteboard.PasteboardType #else #error("Neither UIKit nor AppKit found!") #endif -#if WITH_QEMU_TCI +#if !WITH_USB import CocoaSpiceNoUsb #else import CocoaSpice diff --git a/Services/UTMPipeInterface.swift b/Services/UTMPipeInterface.swift new file mode 100644 index 00000000..8f1fc5c0 --- /dev/null +++ b/Services/UTMPipeInterface.swift @@ -0,0 +1,152 @@ +// +// Copyright © 2024 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 Foundation +import QEMUKit + +class UTMPipeInterface: NSObject, QEMUInterface { + weak var connectDelegate: QEMUInterfaceConnectDelegate? + + var monitorOutPipeURL: URL! + var monitorInPipeURL: URL! + var guestAgentOutPipeURL: URL! + var guestAgentInPipeURL: URL! + + private var pipeIOQueue = DispatchQueue(label: "UTMPipeInterface") + private var qemuMonitorPort: Port! + private var qemuGuestAgentPort: Port! + + func start() throws { + try initializePipe(at: monitorOutPipeURL) + try initializePipe(at: monitorInPipeURL) + try initializePipe(at: guestAgentOutPipeURL) + try initializePipe(at: guestAgentInPipeURL) + } + + func connect() throws { + pipeIOQueue.async { [self] in + do { + try openQemuPipes() + connectDelegate?.qemuInterface(self, didCreateMonitorPort: qemuMonitorPort) + connectDelegate?.qemuInterface(self, didCreateGuestAgentPort: qemuGuestAgentPort) + } catch { + connectDelegate?.qemuInterface(self, didErrorWithMessage: error.localizedDescription) + } + } + } + + func disconnect() { + cleanupPipes() + } +} + +extension UTMPipeInterface { + class Port: NSObject, QEMUPort { + let readPipe: FileHandle + + let writePipe: FileHandle + + var readDataHandler: readDataHandler_t? + + var errorHandler: errorHandler_t? + + var disconnectHandler: disconnectHandler_t? + + let isOpen: Bool = true + + init(readPipe: FileHandle, writePipe: FileHandle) { + self.readPipe = readPipe + self.writePipe = writePipe + super.init() + readPipe.readabilityHandler = { fileHandle in + self.readDataHandler?(fileHandle.availableData) + } + } + + func write(_ data: Data) { + writePipe.write(data) + } + } + + private var fileManager: FileManager { + FileManager.default + } + + private func initializePipe(at url: URL) throws { + if fileManager.fileExists(atPath: url.path) { + try fileManager.removeItem(at: url) + } + guard mkfifo(url.path, S_IRUSR | S_IWUSR) == 0 else { + throw ServerError.failedToCreatePipe(errno) + } + } + + private func openPipe(at url: URL, forReading isRead: Bool) throws -> FileHandle { + let fileHandle: FileHandle + if isRead { + fileHandle = try FileHandle(forReadingFrom: url) + } else { + fileHandle = try FileHandle(forWritingTo: url) + } + return fileHandle + } + + private func cleanupPipes() { + // unblock any un-opened pipes + _ = try? FileHandle(forUpdating: monitorOutPipeURL) + _ = try? FileHandle(forUpdating: monitorInPipeURL) + _ = try? FileHandle(forUpdating: guestAgentOutPipeURL) + _ = try? FileHandle(forUpdating: guestAgentInPipeURL) + pipeIOQueue.sync { + if let monitorOutPipeURL = monitorOutPipeURL { + try? fileManager.removeItem(at: monitorOutPipeURL) + } + if let monitorInPipeURL = monitorInPipeURL { + try? fileManager.removeItem(at: monitorInPipeURL) + } + if let guestAgentOutPipeURL = guestAgentOutPipeURL { + try? fileManager.removeItem(at: guestAgentOutPipeURL) + } + if let guestAgentInPipeURL = guestAgentInPipeURL { + try? fileManager.removeItem(at: guestAgentInPipeURL) + } + qemuMonitorPort = nil + qemuGuestAgentPort = nil + } + } + + private func openQemuPipes() throws { + let qmpReadPipe = try openPipe(at: monitorOutPipeURL, forReading: true) + let qmpWritePipe = try openPipe(at: monitorInPipeURL, forReading: false) + qemuMonitorPort = Port(readPipe: qmpReadPipe, writePipe: qmpWritePipe) + let qgaReadPipe = try openPipe(at: guestAgentOutPipeURL, forReading: true) + let qgaWritePipe = try openPipe(at: guestAgentInPipeURL, forReading: false) + qemuGuestAgentPort = Port(readPipe: qgaReadPipe, writePipe: qgaWritePipe) + } +} + +extension UTMPipeInterface { + enum ServerError: LocalizedError { + case failedToCreatePipe(Int32) + + var errorDescription: String? { + switch self { + case .failedToCreatePipe(_): + return NSLocalizedString("Failed to create pipe for communications.", comment: "UTMPipeInterface") + } + } + } +} diff --git a/Services/UTMQemuPort.swift b/Services/UTMQemuPort.swift index f9df727e..62023bbf 100644 --- a/Services/UTMQemuPort.swift +++ b/Services/UTMQemuPort.swift @@ -15,7 +15,7 @@ // import QEMUKitInternal -#if WITH_QEMU_TCI +#if !WITH_USB import CocoaSpiceNoUsb #else import CocoaSpice diff --git a/Services/UTMQemuSystem.h b/Services/UTMQemuSystem.h index f1fbfa3e..218ef172 100644 --- a/Services/UTMQemuSystem.h +++ b/Services/UTMQemuSystem.h @@ -15,24 +15,9 @@ // #import "UTMProcess.h" +#import "UTMQemuSystemBackends.h" @import QEMUKitInternal; -/// Specify the backend renderer for this VM -typedef NS_ENUM(NSInteger, UTMQEMURendererBackend) { - kQEMURendererBackendDefault = 0, - kQEMURendererBackendAngleGL = 1, - kQEMURendererBackendAngleMetal = 2, - kQEMURendererBackendMax = 3, -}; - -/// Specify the sound backend for this VM -typedef NS_ENUM(NSInteger, UTMQEMUSoundBackend) { - kQEMUSoundBackendDefault = 0, - kQEMUSoundBackendSPICE = 1, - kQEMUSoundBackendCoreAudio = 2, - kQEMUSoundBackendMax = 3, -}; - NS_ASSUME_NONNULL_BEGIN @interface UTMQemuSystem : UTMProcess <QEMULauncher> diff --git a/Services/UTMQemuSystemBackends.h b/Services/UTMQemuSystemBackends.h new file mode 100644 index 00000000..f8eb12d7 --- /dev/null +++ b/Services/UTMQemuSystemBackends.h @@ -0,0 +1,36 @@ +// +// Copyright © 2024 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. +// + +#ifndef UTMQemuSystemBackends_h +#define UTMQemuSystemBackends_h + +/// Specify the backend renderer for this VM +typedef NS_ENUM(NSInteger, UTMQEMURendererBackend) { + kQEMURendererBackendDefault = 0, + kQEMURendererBackendAngleGL = 1, + kQEMURendererBackendAngleMetal = 2, + kQEMURendererBackendMax = 3, +}; + +/// Specify the sound backend for this VM +typedef NS_ENUM(NSInteger, UTMQEMUSoundBackend) { + kQEMUSoundBackendDefault = 0, + kQEMUSoundBackendSPICE = 1, + kQEMUSoundBackendCoreAudio = 2, + kQEMUSoundBackendMax = 3, +}; + +#endif /* UTMQemuSystemBackends_h */ diff --git a/Services/UTMQemuVirtualMachine.swift b/Services/UTMQemuVirtualMachine.swift index c162e6a7..122acac9 100644 --- a/Services/UTMQemuVirtualMachine.swift +++ b/Services/UTMQemuVirtualMachine.swift @@ -16,13 +16,16 @@ import Foundation import QEMUKit +#if os(macOS) +import SwiftPortmap +#endif private var SpiceIoServiceGuestAgentContext = 0 private let kSuspendSnapshotName = "suspend" private let kProbeSuspendDelay = 1*NSEC_PER_SEC /// QEMU backend virtual machine -final class UTMQemuVirtualMachine: UTMVirtualMachine { +final class UTMQemuVirtualMachine: UTMSpiceVirtualMachine { struct Capabilities: UTMVirtualMachineCapabilities { var supportsProcessKill: Bool { true @@ -43,6 +46,10 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine { var supportsRecoveryMode: Bool { false } + + var supportsRemoteSession: Bool { + true + } } static let capabilities = Capabilities() @@ -88,7 +95,7 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine { } } - private(set) var screenshot: PlatformImage? { + var screenshot: UTMVirtualMachineScreenshot? { willSet { onStateChange?() } @@ -117,6 +124,9 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine { } } + /// Pipe interface (alternative to UTMSpiceIO) + private var pipeInterface: UTMPipeInterface? + private let qemuVM = QEMUVirtualMachine() private var system: UTMQemuSystem? { @@ -144,7 +154,13 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine { private var swtpm: UTMSWTPM? private var changeCursorRequestInProgress: Bool = false - + + #if WITH_SERVER + @Setting("ServerPort") private var serverPort: Int = 0 + private var spicePort: SwiftPortmap.Port? + private(set) var spiceServerInfo: UTMRemoteMessageServer.StartVirtualMachine.ServerInformation? + #endif + @MainActor required init(packageUrl: URL, configuration: UTMQemuConfiguration, isShortcut: Bool = false) throws { self.isScopedAccess = packageUrl.startAccessingSecurityScopedResource() // load configuration @@ -267,10 +283,24 @@ extension UTMQemuVirtualMachine { await qemuVM.setRedirectLog(url: nil) } let isRunningAsDisposible = options.contains(.bootDisposibleMode) + let isRemoteSession = options.contains(.remoteSession) + #if WITH_SERVER + let spicePassword = isRemoteSession ? String.random(length: 32) : nil + let spicePort = isRemoteSession ? try SwiftPortmap.Port.TCP(unusedPortStartingAt: UInt16(serverPort)) : nil + #else + if isRemoteSession { + throw UTMVirtualMachineError.notImplemented + } + #endif await MainActor.run { config.qemu.isDisposable = isRunningAsDisposible + #if WITH_SERVER + config.qemu.spiceServerPort = spicePort?.internalPort + config.qemu.spiceServerPassword = spicePassword + config.qemu.isSpiceServerTlsEnabled = true + #endif } - + // start TPM if await config.qemu.hasTPMDevice { let swtpm = UTMSWTPM() @@ -280,12 +310,12 @@ extension UTMQemuVirtualMachine { try await swtpm.start() self.swtpm = swtpm } - + let allArguments = await config.allArguments let arguments = allArguments.map({ $0.string }) let resources = allArguments.compactMap({ $0.fileUrls }).flatMap({ $0 }) let remoteBookmarks = await remoteBookmarks - + let system = await UTMQemuSystem(arguments: arguments, architecture: config.system.architecture.rawValue) system.resources = resources system.currentDirectoryUrl = await config.socketURL @@ -295,12 +325,12 @@ extension UTMQemuVirtualMachine { system.hasDebugLog = hasDebugLog #endif try Task.checkCancellation() - + if isShortcut { try await accessShortcut() try Task.checkCancellation() } - + var options = UTMSpiceIOOptions() if await !config.sound.isEmpty { options.insert(.hasAudio) @@ -317,14 +347,41 @@ extension UTMQemuVirtualMachine { } #endif let spiceSocketUrl = await config.spiceSocketURL - let ioService = UTMSpiceIO(socketUrl: spiceSocketUrl, options: options) - ioService.logHandler = { [weak system] (line: String) -> Void in - guard !line.contains("spice_make_scancode") else { - return // do not log key presses for privacy reasons + let interface: any QEMUInterface + let spicePublicKey: Data? + if isRemoteSession { + let pipeInterface = UTMPipeInterface() + await MainActor.run { + pipeInterface.monitorInPipeURL = config.monitorPipeURL.appendingPathExtension("in") + pipeInterface.monitorOutPipeURL = config.monitorPipeURL.appendingPathExtension("out") + pipeInterface.guestAgentInPipeURL = config.guestAgentPipeURL.appendingPathExtension("in") + pipeInterface.guestAgentOutPipeURL = config.guestAgentPipeURL.appendingPathExtension("out") } - system?.logging?.writeLine(line) + try pipeInterface.start() + interface = pipeInterface + // generate a TLS key for this session + guard let key = GenerateRSACertificate("UTM Remote SPICE Server" as CFString, + "UTM" as CFString, + Int.random(in: 1..<CLong.max) as CFNumber, + 1 as CFNumber, + false as CFBoolean)?.takeUnretainedValue() as? [Data] else { + throw UTMQemuVirtualMachineError.keyGenerationFailed + } + try await key[1].write(to: config.spiceTlsKeyUrl) + try await key[2].write(to: config.spiceTlsCertUrl) + spicePublicKey = key[3] + } else { + let ioService = UTMSpiceIO(socketUrl: spiceSocketUrl, options: options) + ioService.logHandler = { [weak system] (line: String) -> Void in + guard !line.contains("spice_make_scancode") else { + return // do not log key presses for privacy reasons + } + system?.logging?.writeLine(line) + } + try ioService.start() + interface = ioService + spicePublicKey = nil } - try ioService.start() try Task.checkCancellation() // create EFI variables for legacy config as well as handle UEFI resets @@ -333,7 +390,7 @@ extension UTMQemuVirtualMachine { // start QEMU await qemuVM.setDelegate(self) - try await qemuVM.start(launcher: system, interface: ioService) + try await qemuVM.start(launcher: system, interface: interface) let monitor = await monitor! try Task.checkCancellation() @@ -346,7 +403,11 @@ extension UTMQemuVirtualMachine { // set up SPICE sharing and removable drives try await self.restoreExternalDrives(withMounting: !isSuspended) - try await self.restoreSharedDirectory(for: ioService) + if let ioService = interface as? UTMSpiceIO { + try await self.restoreSharedDirectory(for: ioService) + } else { + // TODO: implement shared directory in remote interface + } try Task.checkCancellation() // continue VM boot @@ -358,11 +419,24 @@ extension UTMQemuVirtualMachine { } // save ioService and let it set the delegate - self.ioService = ioService + self.ioService = interface as? UTMSpiceIO + self.pipeInterface = interface as? UTMPipeInterface self.isRunningAsDisposible = isRunningAsDisposible // test out snapshots self.snapshotUnsupportedError = await determineSnapshotSupport() + + #if WITH_SERVER + // save server details + if let spicePort = spicePort, let spicePublicKey = spicePublicKey, let spicePassword = spicePassword { + self.spiceServerInfo = .init(spicePortInternal: spicePort.internalPort, + spicePortExternal: try? await spicePort.externalPort, + spiceHostExternal: try? await spicePort.externalIpv4Address, + spicePublicKey: spicePublicKey, + spicePassword: spicePassword) + self.spicePort = spicePort + } + #endif } func start(options: UTMVirtualMachineStartOptions = []) async throws { @@ -379,7 +453,7 @@ extension UTMQemuVirtualMachine { } try await startTask!.value state = .started - if screenshotTimer == nil { + if screenshotTimer == nil && !options.contains(.remoteSession) { screenshotTimer = startScreenshotTimer() } } catch { @@ -584,10 +658,16 @@ extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate { } func qemuVMDidStop(_ qemuVM: QEMUVirtualMachine) { + #if WITH_SERVER + spicePort = nil + spiceServerInfo = nil + #endif swtpm?.stop() swtpm = nil ioService = nil ioServiceDelegate = nil + pipeInterface?.disconnect() + pipeInterface = nil snapshotUnsupportedError = nil try? saveScreenshot() state = .stopped @@ -621,11 +701,27 @@ extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate { // MARK: - Input device switching extension UTMQemuVirtualMachine { - func requestInputTablet(_ tablet: Bool) { - guard !changeCursorRequestInProgress else { + func changeInputTablet(_ tablet: Bool) async throws { + defer { + changeCursorRequestInProgress = false + } + guard state == .started else { return } - guard let spiceIO = ioService else { + guard let monitor = await monitor else { + return + } + do { + let index = try await monitor.mouseIndex(forAbsolute: tablet) + try await monitor.mouseSelect(index) + ioService?.primaryInput?.requestMouseMode(!tablet) + } catch { + logger.error("Error changing mouse mode: \(error)") + } + } + + func requestInputTablet(_ tablet: Bool) { + guard !changeCursorRequestInProgress else { return } changeCursorRequestInProgress = true @@ -633,40 +729,11 @@ extension UTMQemuVirtualMachine { defer { changeCursorRequestInProgress = false } - guard state == .started else { - return - } - guard let monitor = await monitor else { - return - } - do { - let index = try await monitor.mouseIndex(forAbsolute: tablet) - try await monitor.mouseSelect(index) - spiceIO.primaryInput?.requestMouseMode(!tablet) - } catch { - logger.error("Error changing mouse mode: \(error)") - } + try await changeInputTablet(tablet) } } } -// MARK: - USB redirection -extension UTMQemuVirtualMachine { - var hasUsbRedirection: Bool { - return jb_has_usb_entitlement() - } -} - -// MARK: - Screenshot -extension UTMQemuVirtualMachine { - @MainActor @discardableResult - func takeScreenshot() async -> Bool { - let screenshot = await ioService?.screenshot() - self.screenshot = screenshot?.image - return true - } -} - // MARK: - Architecture supported extension UTMQemuVirtualMachine { /// Check if a QEMU target is supported @@ -695,7 +762,11 @@ extension UTMQemuVirtualMachine { // MARK: - External drives extension UTMQemuVirtualMachine { - func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool = false) async throws { + func eject(_ drive: UTMQemuConfigurationDrive) async throws { + try await eject(drive, isForced: false) + } + + private func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool) async throws { guard drive.isExternal else { return } @@ -707,8 +778,12 @@ extension UTMQemuVirtualMachine { } await registryEntry.removeExternalDrive(forId: drive.id) } - - func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL, isAccessOnly: Bool = false) async throws { + + func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws { + try await changeMedium(drive, to: url, isAccessOnly: false) + } + + private func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL, isAccessOnly: Bool) async throws { _ = url.startAccessingSecurityScopedResource() defer { url.stopAccessingSecurityScopedResource() @@ -719,7 +794,7 @@ extension UTMQemuVirtualMachine { await registryEntry.setExternalDrive(file, forId: drive.id) try await changeMedium(drive, with: tempBookmark, url: url, isSecurityScoped: false, isAccessOnly: isAccessOnly) } - + private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, url: URL?, isSecurityScoped: Bool, isAccessOnly: Bool) async throws { let system = await system ?? UTMProcess() let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped) @@ -731,8 +806,8 @@ extension UTMQemuVirtualMachine { try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) } } - - func restoreExternalDrives(withMounting isMounting: Bool) async throws { + + private func restoreExternalDrives(withMounting isMounting: Bool) async throws { guard await system != nil else { throw UTMQemuVirtualMachineError.invalidVmState } @@ -754,43 +829,14 @@ extension UTMQemuVirtualMachine { } } } - - @MainActor func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? { - registryEntry.externalDrives[drive.id]?.url - } } // MARK: - Shared directory extension UTMQemuVirtualMachine { - @MainActor var sharedDirectoryURL: URL? { - registryEntry.sharedDirectories.first?.url + func stopAccessingPath(_ path: String) async { + await system?.stopAccessingPath(path) } - - func clearSharedDirectory() async { - if let oldPath = await registryEntry.sharedDirectories.first?.path { - await system?.stopAccessingPath(oldPath) - } - await registryEntry.removeAllSharedDirectories() - } - - func changeSharedDirectory(to url: URL) async throws { - await clearSharedDirectory() - _ = url.startAccessingSecurityScopedResource() - defer { - url.stopAccessingSecurityScopedResource() - } - let file = try await UTMRegistryEntry.File(url: url, isReadOnly: config.sharing.isDirectoryShareReadOnly) - await registryEntry.setSingleSharedDirectory(file) - if await config.sharing.directoryShareMode == .webdav { - if let ioService = ioService { - ioService.changeSharedDirectory(url) - } - } else if await config.sharing.directoryShareMode == .virtfs { - let tempBookmark = try url.bookmarkData() - try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false) - } - } - + func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws { let system = await system ?? UTMProcess() let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped) @@ -799,61 +845,10 @@ extension UTMQemuVirtualMachine { } await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark) } - - func restoreSharedDirectory(for ioService: UTMSpiceIO) async throws { - guard let share = await registryEntry.sharedDirectories.first else { - return - } - if await config.sharing.directoryShareMode == .virtfs { - if let bookmark = share.remoteBookmark { - // a share bookmark was saved while QEMU was running - try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true) - } else { - // a share bookmark was saved while QEMU was NOT running - let url = try URL(resolvingPersistentBookmarkData: share.bookmark) - try await changeSharedDirectory(to: url) - } - } else if await config.sharing.directoryShareMode == .webdav { - ioService.changeSharedDirectory(share.url) - } - } } // MARK: - Registry syncing extension UTMQemuVirtualMachine { - @MainActor func updateRegistryFromConfig() async throws { - // save a copy to not collide with updateConfigFromRegistry() - let configShare = config.sharing.directoryShareUrl - let configDrives = config.drives - try await updateRegistryBasics() - for drive in configDrives { - if drive.isExternal, let url = drive.imageURL { - try await changeMedium(drive, to: url) - } else if drive.isExternal { - try await eject(drive) - } - } - if let url = configShare { - try await changeSharedDirectory(to: url) - } else { - await clearSharedDirectory() - } - // remove any unreferenced drives - registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in - configDrives.contains(where: { $0.id == element.key && $0.isExternal }) - }) - } - - @MainActor func updateConfigFromRegistry() { - config.sharing.directoryShareUrl = sharedDirectoryURL - for i in config.drives.indices { - let id = config.drives[i].id - if config.drives[i].isExternal { - config.drives[i].imageURL = registryEntry.externalDrives[id]?.url - } - } - } - @MainActor func changeUuid(to uuid: UUID, name: String? = nil, copyingEntry entry: UTMRegistryEntry? = nil) { config.information.uuid = uuid if let name = name { @@ -864,7 +859,7 @@ extension UTMQemuVirtualMachine { registryEntry.update(copying: entry) } } - + @MainActor var remoteBookmarks: [URL: Data] { var dict = [URL: Data]() for file in registryEntry.externalDrives.values { @@ -889,6 +884,7 @@ enum UTMQemuVirtualMachineError: Error { case accessShareFailed case invalidVmState case saveSnapshotFailed(Error) + case keyGenerationFailed } extension UTMQemuVirtualMachineError: LocalizedError { @@ -905,6 +901,8 @@ extension UTMQemuVirtualMachineError: LocalizedError { case .invalidVmState: return NSLocalizedString("The virtual machine is in an invalid state.", comment: "UTMQemuVirtualMachine") case .saveSnapshotFailed(let error): return String.localizedStringWithFormat(NSLocalizedString("Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@", comment: "UTMQemuVirtualMachine"), error.localizedDescription) + case .keyGenerationFailed: + return NSLocalizedString("Failed to generate TLS key for server.", comment: "UTMQemuVirtualMachine") } } } diff --git a/Services/UTMRegistry.swift b/Services/UTMRegistry.swift index 7007b388..1b5e6951 100644 --- a/Services/UTMRegistry.swift +++ b/Services/UTMRegistry.swift @@ -59,7 +59,7 @@ class UTMRegistry: NSObject { super.init() if let newEntries = try? serializedEntries.mapValues({ value in let dict = value as! [String: Any] - return try UTMRegistryEntry(from: dict) + return try UTMRegistryEntry(fromPropertyList: dict) }) { entries = newEntries } diff --git a/Services/UTMRegistryEntry.swift b/Services/UTMRegistryEntry.swift index 16a81c18..fa812b90 100644 --- a/Services/UTMRegistryEntry.swift +++ b/Services/UTMRegistryEntry.swift @@ -15,6 +15,7 @@ // import Foundation +import Combine @objc class UTMRegistryEntry: NSObject, Codable, ObservableObject { /// Empty registry entry used only as a workaround for object initialization @@ -61,7 +62,7 @@ import Foundation } else { package = nil } - _package = package ?? File(path: path) + _package = package ?? File(dummyFromPath: path) self.uuid = uuid _isSuspended = false _externalDrives = [:] @@ -109,11 +110,7 @@ import Foundation } func asDictionary() throws -> [String: Any] { - let encoder = PropertyListEncoder() - encoder.outputFormat = .xml - let xml = try encoder.encode(self) - let dict = try PropertyListSerialization.propertyList(from: xml, format: nil) - return dict as! [String: Any] + return try propertyList() as! [String: Any] } /// Update the UUID @@ -128,13 +125,6 @@ import Foundation protocol UTMRegistryEntryDecodable: Decodable {} extension UTMRegistryEntry: UTMRegistryEntryDecodable {} -extension UTMRegistryEntryDecodable { - init(from dictionary: [String: Any]) throws { - let data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0) - let decoder = PropertyListDecoder() - self = try decoder.decode(Self.self, from: data) - } -} // MARK: - Accessors @MainActor extension UTMRegistryEntry { @@ -177,7 +167,11 @@ extension UTMRegistryEntryDecodable { _externalDrives = newValue } } - + + var externalDrivePublisher: Published<[String: File]>.Publisher { + $_externalDrives + } + var sharedDirectories: [File] { get { _sharedDirectories @@ -308,7 +302,7 @@ extension UTMRegistryEntry { } for drive in viewState.allDrives() { if let bookmark = viewState.bookmark(forRemovableDrive: drive), let path = viewState.path(forRemovableDrive: drive) { - let file = File(path: path, remoteBookmark: bookmark) + let file = File(dummyFromPath: path, remoteBookmark: bookmark) _externalDrives[drive] = file } } @@ -393,7 +387,7 @@ extension UTMRegistryEntry { self.isValid = true } - fileprivate init(path: String, remoteBookmark: Data = Data()) { + init(dummyFromPath path: String, remoteBookmark: Data = Data()) { self.path = path self.bookmark = Data() self.isReadOnly = false diff --git a/Services/UTMSpiceIO.h b/Services/UTMSpiceIO.h index 2b3b8764..1e0f9dac 100644 --- a/Services/UTMSpiceIO.h +++ b/Services/UTMSpiceIO.h @@ -16,8 +16,12 @@ #import <Foundation/Foundation.h> #import "UTMSpiceIODelegate.h" +#if defined(WITH_REMOTE) +#import "UTMRemoteConnectInterface.h" +#else @import QEMUKitInternal; -#if defined(WITH_QEMU_TCI) +#endif +#if !defined(WITH_USB) @import CocoaSpiceNoUsb; #else @import CocoaSpice; @@ -34,14 +38,18 @@ typedef NS_OPTIONS(NSUInteger, UTMSpiceIOOptions) { NS_ASSUME_NONNULL_BEGIN +#if defined(WITH_REMOTE) +@interface UTMSpiceIO : NSObject<CSConnectionDelegate, UTMRemoteConnectInterface> +#else @interface UTMSpiceIO : NSObject<CSConnectionDelegate, QEMUInterface> +#endif @property (nonatomic, readonly, nullable) CSDisplay *primaryDisplay; @property (nonatomic, readonly, nullable) CSInput *primaryInput; @property (nonatomic, readonly, nullable) CSPort *primarySerial; @property (nonatomic, readonly) NSArray<CSDisplay *> *displays; @property (nonatomic, readonly) NSArray<CSPort *> *serials; -#if !defined(WITH_QEMU_TCI) +#if defined(WITH_USB) @property (nonatomic, readonly, nullable) CSUSBManager *primaryUsbManager; #endif @property (nonatomic, weak, nullable) id<UTMSpiceIODelegate> delegate; @@ -50,6 +58,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithSocketUrl:(NSURL *)socketUrl options:(UTMSpiceIOOptions)options NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey password:(NSString *)password options:(UTMSpiceIOOptions)options NS_DESIGNATED_INITIALIZER; - (void)changeSharedDirectory:(NSURL *)url; - (BOOL)startWithError:(NSError * _Nullable *)error; diff --git a/Services/UTMSpiceIO.m b/Services/UTMSpiceIO.m index 857c397b..423cf82b 100644 --- a/Services/UTMSpiceIO.m +++ b/Services/UTMSpiceIO.m @@ -22,20 +22,23 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; @interface UTMSpiceIO () -@property (nonatomic) NSURL *socketUrl; +@property (nonatomic, nullable) NSURL *socketUrl; +@property (nonatomic, nullable) NSString *host; +@property (nonatomic) NSInteger tlsPort; +@property (nonatomic, nullable) NSData *serverPublicKey; +@property (nonatomic, nullable) NSString *password; @property (nonatomic) UTMSpiceIOOptions options; @property (nonatomic, readwrite, nullable) CSDisplay *primaryDisplay; @property (nonatomic) NSMutableArray<CSDisplay *> *mutableDisplays; @property (nonatomic, readwrite, nullable) CSInput *primaryInput; @property (nonatomic, readwrite, nullable) CSPort *primarySerial; @property (nonatomic) NSMutableArray<CSPort *> *mutableSerials; -#if !defined(WITH_QEMU_TCI) +#if defined(WITH_USB) @property (nonatomic, readwrite, nullable) CSUSBManager *primaryUsbManager; #endif @property (nonatomic, nullable) CSConnection *spiceConnection; @property (nonatomic, nullable) CSMain *spice; @property (nonatomic, nullable, copy) NSURL *sharedDirectory; -@property (nonatomic) NSInteger port; @property (nonatomic) BOOL dynamicResolutionSupported; @property (nonatomic, readwrite) BOOL isConnected; @@ -72,10 +75,29 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; return self; } +- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey password:(NSString *)password options:(UTMSpiceIOOptions)options { + if (self = [super init]) { + self.host = host; + self.tlsPort = tlsPort; + self.serverPublicKey = serverPublicKey; + self.password = password; + self.options = options; + self.mutableDisplays = [NSMutableArray array]; + self.mutableSerials = [NSMutableArray array]; + } + + return self; +} + - (void)initializeSpiceIfNeeded { if (!self.spiceConnection) { - NSURL *relativeSocketFile = [NSURL fileURLWithPath:self.socketUrl.lastPathComponent]; - self.spiceConnection = [[CSConnection alloc] initWithUnixSocketFile:relativeSocketFile]; + if (self.socketUrl) { + NSURL *relativeSocketFile = [NSURL fileURLWithPath:self.socketUrl.lastPathComponent]; + self.spiceConnection = [[CSConnection alloc] initWithUnixSocketFile:relativeSocketFile]; + } else { + self.spiceConnection = [[CSConnection alloc] initWithHost:self.host tlsPort:[@(self.tlsPort) stringValue] serverPublicKey:self.serverPublicKey]; + self.spiceConnection.password = self.password; + } self.spiceConnection.delegate = self; self.spiceConnection.audioEnabled = (self.options & UTMSpiceIOOptionsHasAudio) == UTMSpiceIOOptionsHasAudio; self.spiceConnection.session.shareClipboard = (self.options & UTMSpiceIOOptionsHasClipboardSharing) == UTMSpiceIOOptionsHasClipboardSharing; @@ -94,13 +116,15 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; } // do not need to encode/decode audio locally g_setenv("SPICE_DISABLE_OPUS", "1", YES); - // need to chdir to workaround AF_UNIX sun_len limitations - NSString *curdir = self.socketUrl.URLByDeletingLastPathComponent.path; - if (!curdir || ![NSFileManager.defaultManager changeCurrentDirectoryPath:curdir]) { - if (error) { - *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to change current directory.", "UTMSpiceIO")}]; + if (self.socketUrl) { + // need to chdir to workaround AF_UNIX sun_len limitations + NSString *curdir = self.socketUrl.URLByDeletingLastPathComponent.path; + if (!curdir || ![NSFileManager.defaultManager changeCurrentDirectoryPath:curdir]) { + if (error) { + *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to change current directory.", "UTMSpiceIO")}]; + } + return NO; } - return NO; } if (![self.spice spiceStart]) { if (error) { @@ -135,7 +159,7 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; self.primaryInput = nil; self.primarySerial = nil; [self.mutableSerials removeAllObjects]; -#if !defined(WITH_QEMU_TCI) +#if defined(WITH_USB) self.primaryUsbManager = nil; #endif } @@ -154,10 +178,13 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; - (void)spiceConnected:(CSConnection *)connection { NSAssert(connection == self.spiceConnection, @"Unknown connection"); self.isConnected = YES; -#if !defined(WITH_QEMU_TCI) +#if defined(WITH_USB) self.primaryUsbManager = connection.usbManager; [self.delegate spiceDidChangeUsbManager:connection.usbManager]; #endif +#if defined(WITH_REMOTE) + [self.connectDelegate remoteInterfaceDidConnect:self]; +#endif } - (void)spiceInputAvailable:(CSConnection *)connection input:(CSInput *)input { @@ -177,12 +204,17 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; - (void)spiceDisconnected:(CSConnection *)connection { NSAssert(connection == self.spiceConnection, @"Unknown connection"); self.isConnected = NO; + [self.delegate spiceDidDisconnect]; } - (void)spiceError:(CSConnection *)connection code:(CSConnectionError)code message:(nullable NSString *)message { NSAssert(connection == self.spiceConnection, @"Unknown connection"); self.isConnected = NO; +#if defined(WITH_REMOTE) + [self.connectDelegate remoteInterface:self didErrorWithMessage:message]; +#else [self.connectDelegate qemuInterface:self didErrorWithMessage:message]; +#endif } - (void)spiceDisplayCreated:(CSConnection *)connection display:(CSDisplay *)display { @@ -202,6 +234,9 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; - (void)spiceDisplayDestroyed:(CSConnection *)connection display:(CSDisplay *)display { NSAssert(connection == self.spiceConnection, @"Unknown connection"); [self.mutableDisplays removeObject:display]; + if (self.primaryDisplay == display) { + self.primaryDisplay = nil; + } [self.delegate spiceDidDestroyDisplay:display]; } @@ -215,12 +250,16 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; - (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port { if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) { +#if !defined(WITH_REMOTE) UTMQemuPort *qemuPort = [[UTMQemuPort alloc] initFrom:port]; [self.connectDelegate qemuInterface:self didCreateMonitorPort:qemuPort]; +#endif } if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) { +#if !defined(WITH_REMOTE) UTMQemuPort *qemuPort = [[UTMQemuPort alloc] initFrom:port]; [self.connectDelegate qemuInterface:self didCreateGuestAgentPort:qemuPort]; +#endif } if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) { self.primarySerial = port; @@ -236,11 +275,11 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; } if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) { } - if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) { - self.primarySerial = port; - } if ([port.name hasPrefix:@"com.utmapp.terminal."]) { [self.mutableSerials removeObject:port]; + if (self.primarySerial == port) { + self.primarySerial = nil; + } [self.delegate spiceDidDestroySerial:port]; } } @@ -285,7 +324,7 @@ NSString *const kUTMErrorDomain = @"com.utmapp.utm"; if (self.primarySerial) { [self.delegate spiceDidCreateSerial:self.primarySerial]; } -#if !defined(WITH_QEMU_TCI) +#if defined(WITH_USB) if (self.primaryUsbManager) { [self.delegate spiceDidChangeUsbManager:self.primaryUsbManager]; } diff --git a/Services/UTMSpiceIODelegate.h b/Services/UTMSpiceIODelegate.h index a6376cda..e38ff7bb 100644 --- a/Services/UTMSpiceIODelegate.h +++ b/Services/UTMSpiceIODelegate.h @@ -32,12 +32,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)spiceDidUpdateDisplay:(CSDisplay *)display NS_SWIFT_NAME(spiceDidUpdateDisplay(_:)); - (void)spiceDidCreateSerial:(CSPort *)serial NS_SWIFT_NAME(spiceDidCreateSerial(_:)); - (void)spiceDidDestroySerial:(CSPort *)serial NS_SWIFT_NAME(spiceDidDestroySerial(_:)); -#if !defined(WITH_QEMU_TCI) +#if defined(WITH_USB) - (void)spiceDidChangeUsbManager:(nullable CSUSBManager *)usbManager NS_SWIFT_NAME(spiceDidChangeUsbManager(_:)); #endif @optional - (void)spiceDynamicResolutionSupportDidChange:(BOOL)supported; +- (void)spiceDidDisconnect; @end diff --git a/Services/UTMSpiceVirtualMachine.swift b/Services/UTMSpiceVirtualMachine.swift new file mode 100644 index 00000000..6adbf24a --- /dev/null +++ b/Services/UTMSpiceVirtualMachine.swift @@ -0,0 +1,177 @@ +// +// Copyright © 2024 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 Foundation + +/// Common methods for all SPICE virtual machines +protocol UTMSpiceVirtualMachine: UTMVirtualMachine where Configuration == UTMQemuConfiguration { + /// Set when VM is running with saving changes + var isRunningAsDisposible: Bool { get } + + /// Get and set screenshot + var screenshot: UTMVirtualMachineScreenshot? { get set } + + /// Handles IO + var ioServiceDelegate: UTMSpiceIODelegate? { get set } + + /// SPICE interface + var ioService: UTMSpiceIO? { get } + + /// Change input mode + /// - Parameter tablet: If true, mouse events will be absolute + func requestInputTablet(_ tablet: Bool) + + /// Eject a removable drive + /// - Parameter drive: Removable drive + func eject(_ drive: UTMQemuConfigurationDrive) async throws + + /// Change mount image of a removable drive + /// - Parameters: + /// - drive: Removable drive + /// - url: New mount image + func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws + + /// Release resources for accessing a path + /// - Parameter path: Path to stop accessing + func stopAccessingPath(_ path: String) async + + /// Setup access to a VirtFS shared directory + /// + /// Throw an exception if this is not supported. + /// - Parameters: + /// - bookmark: Bookmark to access + /// - isSecurityScoped: Is the bookmark security scoped? + func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws +} + +// MARK: - USB redirection +extension UTMSpiceVirtualMachine { + var hasUsbRedirection: Bool { + #if HAS_USB + return jb_has_usb_entitlement() + #else + return false + #endif + } +} + +// MARK: - Screenshot +extension UTMSpiceVirtualMachine { + @MainActor @discardableResult + func takeScreenshot() async -> Bool { + if let screenshot = await ioService?.screenshot() { + self.screenshot = UTMVirtualMachineScreenshot(wrapping: screenshot.image) + } + return true + } + + func reloadScreenshotFromFile() { + screenshot = loadScreenshot() + } +} + +// MARK: - External drives +extension UTMSpiceVirtualMachine { + @MainActor func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? { + registryEntry.externalDrives[drive.id]?.url + } +} + +// MARK: - Shared directory +extension UTMSpiceVirtualMachine { + @MainActor var sharedDirectoryURL: URL? { + registryEntry.sharedDirectories.first?.url + } + + func clearSharedDirectory() async { + if let oldPath = await registryEntry.sharedDirectories.first?.path { + await stopAccessingPath(oldPath) + } + await registryEntry.removeAllSharedDirectories() + } + + func changeSharedDirectory(to url: URL) async throws { + await clearSharedDirectory() + _ = url.startAccessingSecurityScopedResource() + defer { + url.stopAccessingSecurityScopedResource() + } + let file = try await UTMRegistryEntry.File(url: url, isReadOnly: config.sharing.isDirectoryShareReadOnly) + await registryEntry.setSingleSharedDirectory(file) + if await config.sharing.directoryShareMode == .webdav { + if let ioService = ioService { + ioService.changeSharedDirectory(url) + } + } else if await config.sharing.directoryShareMode == .virtfs { + let tempBookmark = try url.bookmarkData() + try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false) + } + } + + func restoreSharedDirectory(for ioService: UTMSpiceIO) async throws { + guard let share = await registryEntry.sharedDirectories.first else { + return + } + if await config.sharing.directoryShareMode == .virtfs { + if let bookmark = share.remoteBookmark { + // a share bookmark was saved while QEMU was running + try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true) + } else { + // a share bookmark was saved while QEMU was NOT running + let url = try URL(resolvingPersistentBookmarkData: share.bookmark) + try await changeSharedDirectory(to: url) + } + } else if await config.sharing.directoryShareMode == .webdav { + ioService.changeSharedDirectory(share.url) + } + } +} + +// MARK: - Registry syncing +extension UTMSpiceVirtualMachine { + @MainActor func updateRegistryFromConfig() async throws { + // save a copy to not collide with updateConfigFromRegistry() + let configShare = config.sharing.directoryShareUrl + let configDrives = config.drives + try await updateRegistryBasics() + for drive in configDrives { + if drive.isExternal, let url = drive.imageURL { + try await changeMedium(drive, to: url) + } else if drive.isExternal { + try await eject(drive) + } + } + if let url = configShare { + try await changeSharedDirectory(to: url) + } else { + await clearSharedDirectory() + } + // remove any unreferenced drives + registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in + configDrives.contains(where: { $0.id == element.key && $0.isExternal }) + }) + } + + @MainActor func updateConfigFromRegistry() { + config.sharing.directoryShareUrl = sharedDirectoryURL + for i in config.drives.indices { + let id = config.drives[i].id + if config.drives[i].isExternal { + config.drives[i].imageURL = registryEntry.externalDrives[id]?.url + } + } + } +} diff --git a/Services/UTMVirtualMachine.swift b/Services/UTMVirtualMachine.swift index c5e8d0f0..b7eec04e 100644 --- a/Services/UTMVirtualMachine.swift +++ b/Services/UTMVirtualMachine.swift @@ -24,7 +24,7 @@ import UIKit private let kUTMBundleExtension = "utm" private let kScreenshotPeriodSeconds = 60.0 -private let kUTMBundleScreenshotFilename = "screenshot.png" +let kUTMBundleScreenshotFilename = "screenshot.png" private let kUTMBundleViewFilename = "view.plist" /// UTM virtual machine backend @@ -66,8 +66,8 @@ protocol UTMVirtualMachine: AnyObject, Identifiable { var state: UTMVirtualMachineState { get } /// If non-null, is the most recent screenshot of the running VM - var screenshot: PlatformImage? { get } - + var screenshot: UTMVirtualMachineScreenshot? { get } + /// If non-null, `saveSnapshot` and `restoreSnapshot` will not work due to the reason specified var snapshotUnsupportedError: Error? { get } @@ -149,6 +149,9 @@ protocol UTMVirtualMachine: AnyObject, Identifiable { /// Request a screenshot of the primary graphics device /// - Returns: true if successful and the screenshot will be in `screenshot` @discardableResult func takeScreenshot() async -> Bool + + /// If screenshot is modified externally, this must be called + func reloadScreenshotFromFile() throws } /// Supported capabilities for a UTM backend @@ -167,6 +170,9 @@ protocol UTMVirtualMachineCapabilities { /// The backend supports booting into recoveryOS. var supportsRecoveryMode: Bool { get } + + /// The backend supports remote sessions. + var supportsRemoteSession: Bool { get } } /// Delegate for UTMVirtualMachine events @@ -201,7 +207,7 @@ protocol UTMVirtualMachineDelegate: AnyObject { } /// Virtual machine state -enum UTMVirtualMachineState { +enum UTMVirtualMachineState: Codable { case stopped case starting case started @@ -214,17 +220,19 @@ enum UTMVirtualMachineState { } /// Additional options for VM start -struct UTMVirtualMachineStartOptions: OptionSet { +struct UTMVirtualMachineStartOptions: OptionSet, Codable { let rawValue: UInt /// Boot without persisting any changes. static let bootDisposibleMode = Self(rawValue: 1 << 0) /// Boot into recoveryOS (when supported). static let bootRecovery = Self(rawValue: 1 << 1) + /// Start VDI session where a remote client will connect to. + static let remoteSession = Self(rawValue: 1 << 2) } /// Method to stop the VM -enum UTMVirtualMachineStopMethod { +enum UTMVirtualMachineStopMethod: Codable { /// Sends a request to the guest to shut down gracefully. case request /// Sends a hardware power down signal. @@ -282,6 +290,43 @@ extension UTMVirtualMachine { // MARK: - Screenshot +struct UTMVirtualMachineScreenshot { + let image: PlatformImage + let pngData: Data? + + init?(contentsOfURL url: URL) { + #if canImport(AppKit) + guard let image = NSImage(contentsOf: url) else { + return nil + } + #elseif canImport(UIKit) + guard let image = UIImage(contentsOfURL: url) else { + return nil + } + #endif + self.image = image + self.pngData = Self.createData(from: image) + } + + init(wrapping image: PlatformImage) { + self.image = image + self.pngData = Self.createData(from: image) + } + + private static func createData(from image: PlatformImage) -> Data? { + #if canImport(AppKit) + guard let cgref = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return nil + } + let newrep = NSBitmapImageRep(cgImage: cgref) + newrep.size = image.size + return newrep.representation(using: .png, properties: [:]) + #elseif canImport(UIKit) + return image.pngData() + #endif + } +} + extension UTMVirtualMachine { private var isScreenshotSaveEnabled: Bool { !UserDefaults.standard.bool(forKey: "NoSaveScreenshot") @@ -311,12 +356,8 @@ extension UTMVirtualMachine { return timer } - func loadScreenshot() -> PlatformImage? { - #if canImport(AppKit) - return NSImage(contentsOf: screenshotUrl) - #elseif canImport(UIKit) - return UIImage(contentsOfURL: screenshotUrl) - #endif + func loadScreenshot() -> UTMVirtualMachineScreenshot? { + UTMVirtualMachineScreenshot(contentsOfURL: screenshotUrl) } func saveScreenshot() throws { @@ -326,17 +367,7 @@ extension UTMVirtualMachine { guard let screenshot = screenshot else { return } - #if canImport(AppKit) - guard let cgref = screenshot.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - return - } - let newrep = NSBitmapImageRep(cgImage: cgref) - newrep.size = screenshot.size - let pngdata = newrep.representation(using: .png, properties: [:]) - try pngdata?.write(to: screenshotUrl) - #elseif canImport(UIKit) - try screenshot.pngData()?.write(to: screenshotUrl) - #endif + try screenshot.pngData?.write(to: screenshotUrl) } func deleteScreenshot() throws { diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index 68cf5cd0..17e3ed63 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -320,6 +320,7 @@ CE064C662A563F4B003C833D /* swtpm.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE064C642A563F4A003C833D /* swtpm.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE064C6A2A563F6E003C833D /* swtpm.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE064C642A563F4A003C833D /* swtpm.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE064C6C2A563F75003C833D /* swtpm.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE064C642A563F4A003C833D /* swtpm.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CE08334B2B784FD400522C03 /* RemoteContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE08334A2B784FD400522C03 /* RemoteContentView.swift */; }; CE0B6CEC24AD532500FE012D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE550BD52259479D0063E575 /* Assets.xcassets */; }; CE0B6CED24AD532A00FE012D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE550BD52259479D0063E575 /* Assets.xcassets */; }; CE0B6CF324AD568400FE012D /* UTMLegacyQemuConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CE31C244225E555600A965DD /* UTMLegacyQemuConfiguration.m */; }; @@ -423,6 +424,8 @@ CE19392626DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; }; CE19392726DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; }; CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; }; + CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; }; + CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; }; CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */; }; CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */; }; CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124A29BFE273000790AB /* UTMScriptable.swift */; }; @@ -610,6 +613,7 @@ CE2D958E24AD4F990059923A /* UTMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955524AD4F980059923A /* UTMApp.swift */; }; CE2D958F24AD4FF00059923A /* VMCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954324AD4F980059923A /* VMCardView.swift */; }; CE2D959024AD50D50059923A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 521F3EFB2414F73800130500 /* Localizable.strings */; }; + CE38EC692B5DB3AE008B324B /* UTMRemoteClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38EC682B5DB3AE008B324B /* UTMRemoteClient.swift */; }; CE4698F924C8FBD9008C1BD6 /* Icons in Resources */ = {isa = PBXBuildFile; fileRef = CE4698F824C8FBD9008C1BD6 /* Icons */; }; CE4698FA24C8FBD9008C1BD6 /* Icons in Resources */ = {isa = PBXBuildFile; fileRef = CE4698F824C8FBD9008C1BD6 /* Icons */; }; CE5076DB250AB55D00C26C19 /* VMDisplayMetalViewController+Pencil.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5076DA250AB55D00C26C19 /* VMDisplayMetalViewController+Pencil.m */; platformFilter = ios; }; @@ -627,8 +631,11 @@ CE612AC624D3B50700FA6300 /* VMDisplayWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */; }; CE65BABF26A4D8DD0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; }; CE65BAC026A4D8DE0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; }; + CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; }; + CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; }; CE6D21DC2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; }; CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; }; + CE70E8D52B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE70E8D42B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift */; }; CE772AAC25C8B0F600E4E379 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; }; CE772AAD25C8B0F600E4E379 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; }; CE772AB325C8B7B500E4E379 /* VMCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AB225C8B7B500E4E379 /* VMCommands.swift */; }; @@ -639,6 +646,9 @@ CE8813D324CD230300532628 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D224CD230300532628 /* ActivityView.swift */; }; CE8813D524CD265700532628 /* VMShareFileModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D424CD265700532628 /* VMShareFileModifier.swift */; }; CE8813D624CD265700532628 /* VMShareFileModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D424CD265700532628 /* VMShareFileModifier.swift */; }; + CE89CB0E2B8B1B5A006B2CC2 /* VisionKeyboardKit in Frameworks */ = {isa = PBXBuildFile; platformFilters = (xros, ); productRef = CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */; }; + CE89CB102B8B1B6A006B2CC2 /* VisionKeyboardKit in Frameworks */ = {isa = PBXBuildFile; platformFilters = (xros, ); productRef = CE89CB0F2B8B1B6A006B2CC2 /* VisionKeyboardKit */; }; + CE89CB122B8B1B7A006B2CC2 /* VisionKeyboardKit in Frameworks */ = {isa = PBXBuildFile; platformFilters = (xros, ); productRef = CE89CB112B8B1B7A006B2CC2 /* VisionKeyboardKit */; }; CE928C2A26ABE6690099F293 /* UTMAppleVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE928C2926ABE6690099F293 /* UTMAppleVirtualMachine.swift */; }; CE928C3126ACCDEA0099F293 /* VMAppleRemovableDrivesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE928C3026ACCDEA0099F293 /* VMAppleRemovableDrivesView.swift */; }; CE93758924B930270074066F /* BusyOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7D972B24B2B17D0080CB69 /* BusyOverlay.swift */; }; @@ -648,6 +658,19 @@ CE9A353426533A52005077CF /* JailbreakInterposer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9A352D26533A51005077CF /* JailbreakInterposer.framework */; platformFilter = ios; }; CE9A353526533A52005077CF /* JailbreakInterposer.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE9A352D26533A51005077CF /* JailbreakInterposer.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE9A354026533AE6005077CF /* JailbreakInterposer.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9A353F26533AE6005077CF /* JailbreakInterposer.c */; }; + CE9B15362B11A491003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B15352B11A491003A32DD /* SwiftConnect */; }; + CE9B15382B11A4A7003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B15372B11A4A7003A32DD /* SwiftConnect */; }; + CE9B153A2B11A4AE003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B15392B11A4AE003A32DD /* SwiftConnect */; }; + CE9B153C2B11A4B4003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B153B2B11A4B4003A32DD /* SwiftConnect */; }; + CE9B153F2B11A63E003A32DD /* UTMRemoteServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */; }; + CE9B15412B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; }; + CE9B15422B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; }; + CE9B15432B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; }; + CE9B15442B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; }; + CE9B15472B12A87E003A32DD /* GenerateKey.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15462B12A87E003A32DD /* GenerateKey.c */; }; + CE9B15482B12A87E003A32DD /* GenerateKey.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15462B12A87E003A32DD /* GenerateKey.c */; }; + CE9B15492B12A87E003A32DD /* GenerateKey.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15462B12A87E003A32DD /* GenerateKey.c */; }; + CE9B154A2B12A87E003A32DD /* GenerateKey.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15462B12A87E003A32DD /* GenerateKey.c */; }; CEA45E25263519B5002FA97D /* VMDisplayMetalViewController+Pointer.h in Sources */ = {isa = PBXBuildFile; fileRef = 83FBDD53242FA71900D2C5D7 /* VMDisplayMetalViewController+Pointer.h */; }; CEA45E27263519B5002FA97D /* VMRemovableDrivesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954524AD4F980059923A /* VMRemovableDrivesView.swift */; }; CEA45E37263519B5002FA97D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; }; @@ -692,7 +715,6 @@ CEA45ED1263519B5002FA97D /* UTMLocationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CE059DC7243E9E3400338317 /* UTMLocationManager.m */; platformFilter = ios; }; CEA45ED3263519B5002FA97D /* VMSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954C24AD4F980059923A /* VMSettingsView.swift */; }; CEA45ED8263519B5002FA97D /* VMKeyboardButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CEEB66452284B942002737B2 /* VMKeyboardButton.m */; }; - CEA45EDF263519B5002FA97D /* UTMJailbreak.m in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7924F469E300CAF323 /* UTMJailbreak.m */; }; CEA45EE8263519B5002FA97D /* VMDisplayMetalViewController+Keyboard.m in Sources */ = {isa = PBXBuildFile; fileRef = CE3ADD66240EFBCA002D6A5F /* VMDisplayMetalViewController+Keyboard.m */; }; CEA45EEA263519B5002FA97D /* UTMExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954624AD4F980059923A /* UTMExtensions.swift */; }; CEA45EEC263519B5002FA97D /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; }; @@ -867,13 +889,25 @@ CED8DF7528A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; }; CED8DF7628A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; }; CED8DF7728A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; }; + CEDD11C12B7C74D7004DDAC6 /* SwiftPortmap in Frameworks */ = {isa = PBXBuildFile; productRef = CEDD11C02B7C74D7004DDAC6 /* SwiftPortmap */; }; CEDF83F9258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; }; CEDF83FA258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; }; + CEE06B272B2FC89400A811AE /* UTMServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE06B262B2FC89400A811AE /* UTMServerView.swift */; }; + CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */; }; CEE7E936287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; }; CEE7E937287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; }; CEE7E938287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; }; + CEE8B4C22B71E0FB0035AE86 /* UTMLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCE1241DA0E900A719DC /* UTMLogging.m */; }; + CEE8B4C32B71E2BA0035AE86 /* UTMLoggingSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */; }; CEEC811B24E48EC700ACB0B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEC811A24E48EC600ACB0B3 /* SettingsView.swift */; }; CEECE13C25E47D9500A2AAB8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEECE13B25E47D9500A2AAB8 /* AppDelegate.swift */; }; + CEF01DB22B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; }; + CEF01DB32B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; }; + CEF01DB42B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; }; + CEF01DB52B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; }; + CEF01DB72B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; }; + CEF01DB82B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; }; + CEF01DB92B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; }; CEF0300826A25A6900667B63 /* VMWizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0300526A25A6900667B63 /* VMWizardView.swift */; }; CEF0304E26A2AFBE00667B63 /* BigButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */; }; CEF0304F26A2AFBF00667B63 /* BigButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */; }; @@ -913,6 +947,250 @@ CEF78EEB26B99F530022CAF4 /* GLESv2.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A326AF5F0F008594E5 /* GLESv2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CEF78EEF26B9B7870022CAF4 /* virglrenderer.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A126AF5F0F008594E5 /* virglrenderer.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CEF78EF026B9B7910022CAF4 /* virglrenderer.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A126AF5F0F008594E5 /* virglrenderer.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CEF7F5972AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.h in Sources */ = {isa = PBXBuildFile; fileRef = 83FBDD53242FA71900D2C5D7 /* VMDisplayMetalViewController+Pointer.h */; }; + CEF7F5982AEEDCC400E34952 /* VMSettingsAddDeviceMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C4D9012880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift */; }; + CEF7F5992AEEDCC400E34952 /* VMRemovableDrivesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954524AD4F980059923A /* VMRemovableDrivesView.swift */; }; + CEF7F59A2AEEDCC400E34952 /* UTMQemuConfigurationDrive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8443EFF12845641600B2E6E2 /* UTMQemuConfigurationDrive.swift */; }; + CEF7F59B2AEEDCC400E34952 /* UTMQemuConfigurationSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8443EFF928456F3B00B2E6E2 /* UTMQemuConfigurationSharing.swift */; }; + CEF7F59C2AEEDCC400E34952 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; }; + CEF7F59D2AEEDCC400E34952 /* VMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BF9A92A49C783000BD9AA /* VMData.swift */; }; + CEF7F59E2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+System.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5425332437C22A00E520F7 /* UTMLegacyQemuConfiguration+System.m */; }; + CEF7F59F2AEEDCC400E34952 /* UTMQemuConfigurationNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82F2844853E0029D60D /* UTMQemuConfigurationNetwork.swift */; }; + CEF7F5A12AEEDCC400E34952 /* VMWizardDrivesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820226A4C1B5007AAB12 /* VMWizardDrivesView.swift */; }; + CEF7F5A22AEEDCC400E34952 /* VMWindowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84018688288A44C20050AC51 /* VMWindowState.swift */; }; + CEF7F5A42AEEDCC400E34952 /* UTMLegacyQemuConfigurationPortForward.m in Sources */ = {isa = PBXBuildFile; fileRef = CE54252D2436E48D00E520F7 /* UTMLegacyQemuConfigurationPortForward.m */; }; + CEF7F5A52AEEDCC400E34952 /* VMWizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0307026A2B04300667B63 /* VMWizardView.swift */; }; + CEF7F5A62AEEDCC400E34952 /* UTMPlaceholderVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */; }; + CEF7F5A72AEEDCC400E34952 /* BusyOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7D972B24B2B17D0080CB69 /* BusyOverlay.swift */; }; + CEF7F5A82AEEDCC400E34952 /* UTMConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848A98C3286F332D006F0550 /* UTMConfiguration.swift */; }; + CEF7F5A92AEEDCC400E34952 /* UTMConfigurationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619A9284315F9000034B2 /* UTMConfigurationInfo.swift */; }; + CEF7F5AA2AEEDCC400E34952 /* VMConfigDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953724AD4F980059923A /* VMConfigDisplayView.swift */; }; + CEF7F5AB2AEEDCC400E34952 /* VMWizardOSWindowsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305626A2AFDD00667B63 /* VMWizardOSWindowsView.swift */; }; + CEF7F5AC2AEEDCC400E34952 /* UTMDownloadTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A004B826A8CC95001AC09E /* UTMDownloadTask.swift */; }; + CEF7F5AD2AEEDCC400E34952 /* UTMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58D02893AF5400137A20 /* UTMApp.swift */; platformFilter = ios; }; + CEF7F5AE2AEEDCC400E34952 /* VMConfigAdvancedNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EC516327CC8C98004A51DE /* VMConfigAdvancedNetworkView.swift */; }; + CEF7F5AF2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Miscellaneous.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE0421124418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.m */; }; + CEF7F5B02AEEDCC400E34952 /* UTMRegistryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */; }; + CEF7F5B12AEEDCC400E34952 /* UTMDataExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBBF1A424B56A2900C15049 /* UTMDataExtension.swift */; }; + CEF7F5B22AEEDCC400E34952 /* VMDisplayHostedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */; }; + CEF7F5B32AEEDCC400E34952 /* QEMUArgumentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99BF2866D9CE0055C215 /* QEMUArgumentBuilder.swift */; }; + CEF7F5B42AEEDCC400E34952 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EE24C7EB760042F0F1 /* ImagePicker.swift */; }; + CEF7F5B52AEEDCC400E34952 /* VMConfigSystemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955324AD4F980059923A /* VMConfigSystemView.swift */; }; + CEF7F5B62AEEDCC400E34952 /* FileBrowseField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432329328C2ED9000CFBC97 /* FileBrowseField.swift */; }; + CEF7F5B72AEEDCC400E34952 /* VMShareFileModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D424CD265700532628 /* VMShareFileModifier.swift */; }; + CEF7F5B82AEEDCC400E34952 /* UTMQemuConfigurationSerial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF83B2845494C0029D60D /* UTMQemuConfigurationSerial.swift */; }; + CEF7F5B92AEEDCC400E34952 /* Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304D26A2AFBE00667B63 /* Spinner.swift */; }; + CEF7F5BA2AEEDCC400E34952 /* UTMQemuConfigurationPortForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF83F284555E70029D60D /* UTMQemuConfigurationPortForward.swift */; }; + CEF7F5BB2AEEDCC400E34952 /* UTMReleaseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE611BE629F50CAD001817BC /* UTMReleaseHelper.swift */; }; + CEF7F5BC2AEEDCC400E34952 /* VMConfigNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955024AD4F980059923A /* VMConfigNetworkView.swift */; }; + CEF7F5BD2AEEDCC400E34952 /* UTMLegacyViewState.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */; }; + CEF7F5BF2AEEDCC400E34952 /* VMWizardOSLinuxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305726A2AFDE00667B63 /* VMWizardOSLinuxView.swift */; }; + CEF7F5C02AEEDCC400E34952 /* VMWizardSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820A26A4C8E0007AAB12 /* VMWizardSummaryView.swift */; }; + CEF7F5C12AEEDCC400E34952 /* VMConfigQEMUView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953924AD4F980059923A /* VMConfigQEMUView.swift */; }; + CEF7F5C22AEEDCC400E34952 /* VMDisplayMetalViewController+Touch.m in Sources */ = {isa = PBXBuildFile; fileRef = CE056CA5242454100004B68A /* VMDisplayMetalViewController+Touch.m */; }; + CEF7F5C32AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Display.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE0420B244117040001680F /* UTMLegacyQemuConfiguration+Display.m */; }; + CEF7F5C42AEEDCC400E34952 /* BigButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */; }; + CEF7F5C52AEEDCC400E34952 /* UTMVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */; }; + CEF7F5C62AEEDCC400E34952 /* UTMQemuConfigurationSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619AD28431952000034B2 /* UTMQemuConfigurationSystem.swift */; }; + CEF7F5C72AEEDCC400E34952 /* VMWizardSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820626A4C74E007AAB12 /* VMWizardSharingView.swift */; }; + CEF7F5C82AEEDCC400E34952 /* VMConfigInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */; }; + CEF7F5C92AEEDCC400E34952 /* MenuLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F909FE289488F90008DBE2 /* MenuLabel.swift */; }; + CEF7F5CA2AEEDCC400E34952 /* VMKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE4507D1226A5BE200A28D22 /* VMKeyboardView.m */; }; + CEF7F5CB2AEEDCC400E34952 /* VMWizardOSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305A26A2AFDE00667B63 /* VMWizardOSView.swift */; }; + CEF7F5CC2AEEDCC400E34952 /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B36D2827B790BE00C22685 /* DestructiveButton.swift */; }; + CEF7F5CD2AEEDCC400E34952 /* UTMConfigurationTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF83728451B380029D60D /* UTMConfigurationTerminal.swift */; }; + CEF7F5CE2AEEDCC400E34952 /* VMWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84018682288A3B2E0050AC51 /* VMWindowView.swift */; }; + CEF7F5CF2AEEDCC400E34952 /* UTMPendingVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83034C0626AB630F006B4BAF /* UTMPendingVMView.swift */; }; + CEF7F5D02AEEDCC400E34952 /* UTMSpiceIO.m in Sources */ = {isa = PBXBuildFile; fileRef = E2D64BC8241DB24B0034E0C6 /* UTMSpiceIO.m */; }; + CEF7F5D12AEEDCC400E34952 /* UTMUnavailableVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A9027CADAE0005605F1 /* UTMUnavailableVMView.swift */; }; + CEF7F5D22AEEDCC400E34952 /* VMDrivesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955124AD4F980059923A /* VMDrivesSettingsView.swift */; }; + CEF7F5D32AEEDCC400E34952 /* UTMConfigurationDrive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99BB28636AC90055C215 /* UTMConfigurationDrive.swift */; }; + CEF7F5D42AEEDCC400E34952 /* VMConfigDriveCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */; }; + CEF7F5D52AEEDCC400E34952 /* UTMPatches.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B9F8C28CC58B700031EE7 /* UTMPatches.swift */; }; + CEF7F5D62AEEDCC400E34952 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; }; + CEF7F5D72AEEDCC400E34952 /* VMReleaseNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE611BEA29F50D3E001817BC /* VMReleaseNotesView.swift */; }; + CEF7F5D82AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; }; + CEF7F5D92AEEDCC400E34952 /* InListButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */; }; + CEF7F5DA2AEEDCC400E34952 /* VMContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */; }; + CEF7F5DB2AEEDCC400E34952 /* VMDisplayMetalViewController+Pencil.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5076DA250AB55D00C26C19 /* VMDisplayMetalViewController+Pencil.m */; platformFilter = ios; }; + CEF7F5DC2AEEDCC400E34952 /* VMDisplayTerminalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401865D2887B1620050AC51 /* VMDisplayTerminalViewController.swift */; }; + CEF7F5DD2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Drives.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5425302437C09C00E520F7 /* UTMLegacyQemuConfiguration+Drives.m */; }; + CEF7F5DE2AEEDCC400E34952 /* UTMPendingVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835AA7B026AB7C85007A0411 /* UTMPendingVirtualMachine.swift */; }; + CEF7F5DF2AEEDCC400E34952 /* BusyIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84018696288B71BF0050AC51 /* BusyIndicator.swift */; }; + CEF7F5E02AEEDCC400E34952 /* VMSessionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84018685288A3B5B0050AC51 /* VMSessionState.swift */; }; + CEF7F5E12AEEDCC400E34952 /* VMConfigSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954724AD4F980059923A /* VMConfigSharingView.swift */; }; + CEF7F5E22AEEDCC400E34952 /* VMConfigInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954824AD4F980059923A /* VMConfigInputView.swift */; }; + CEF7F5E32AEEDCC400E34952 /* VMWizardOSOtherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305426A2AFDD00667B63 /* VMWizardOSOtherView.swift */; }; + CEF7F5E42AEEDCC400E34952 /* VMToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C60FB62681A41B00B58C00 /* VMToolbarView.swift */; }; + CEF7F5E52AEEDCC400E34952 /* VMDisplayMetalViewController+Gamepad.m in Sources */ = {isa = PBXBuildFile; fileRef = 5286EC8F2437488E007E6CBC /* VMDisplayMetalViewController+Gamepad.m */; }; + CEF7F5E62AEEDCC400E34952 /* VMWizardHardwareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0307326A2B40B00667B63 /* VMWizardHardwareView.swift */; }; + CEF7F5E72AEEDCC400E34952 /* UTMRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997428AA1191003C6CB6 /* UTMRegistry.swift */; }; + CEF7F5E82AEEDCC400E34952 /* VMDisplayViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401868E288A50B90050AC51 /* VMDisplayViewControllerDelegate.swift */; }; + CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99A7285DB5550055C215 /* VMConfigConstantPicker.swift */; }; + CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953824AD4F980059923A /* VMToolbarModifier.swift */; }; + CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */ = {isa = PBXBuildFile; fileRef = CE3ADD692411C661002D6A5F /* VMCursor.m */; }; + CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9375A024BBDDD10074066F /* VMConfigDriveDetailsView.swift */; }; + CEF7F5F02AEEDCC400E34952 /* NumberTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED234EC254796E500ED0A57 /* NumberTextField.swift */; }; + CEF7F5F12AEEDCC400E34952 /* VMToolbarOrnamentModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA51F862A81EAB700DDD7FA /* VMToolbarOrnamentModifier.swift */; platformFilters = (xros, ); }; + CEF7F5F22AEEDCC400E34952 /* VMCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AB225C8B7B500E4E379 /* VMCommands.swift */; }; + CEF7F5F32AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Networking.m in Sources */ = {isa = PBXBuildFile; fileRef = CEA02A982436C7A30087E45F /* UTMLegacyQemuConfiguration+Networking.m */; }; + CEF7F5F42AEEDCC400E34952 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; }; + CEF7F5F52AEEDCC400E34952 /* QEMUConstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619B52843226B000034B2 /* QEMUConstant.swift */; }; + CEF7F5F62AEEDCC400E34952 /* VMConfigPortForwardForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955224AD4F980059923A /* VMConfigPortForwardForm.swift */; }; + CEF7F5F82AEEDCC400E34952 /* DetailedSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471772727CD3CAB00D3A50B /* DetailedSection.swift */; }; + CEF7F5F92AEEDCC400E34952 /* VMToolbarDriveMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CF5DF4288F558400D01721 /* VMToolbarDriveMenuView.swift */; }; + CEF7F5FA2AEEDCC400E34952 /* VMSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954C24AD4F980059923A /* VMSettingsView.swift */; }; + CEF7F5FB2AEEDCC400E34952 /* VMDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C60FB9268269D700B58C00 /* VMDisplayViewController.swift */; }; + CEF7F5FC2AEEDCC400E34952 /* VMWizardStartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305926A2AFDE00667B63 /* VMWizardStartView.swift */; }; + CEF7F5FD2AEEDCC400E34952 /* QEMUConstantGenerated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82728441FAF0029D60D /* QEMUConstantGenerated.swift */; }; + CEF7F5FE2AEEDCC400E34952 /* VMKeyboardButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CEEB66452284B942002737B2 /* VMKeyboardButton.m */; }; + CEF7F5FF2AEEDCC400E34952 /* UTMDownloadVMTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B36D2427B704C200C22685 /* UTMDownloadVMTask.swift */; }; + CEF7F6002AEEDCC400E34952 /* GlobalFileImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432329728C3017F00CFBC97 /* GlobalFileImporter.swift */; }; + CEF7F6022AEEDCC400E34952 /* VMWizardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C2E8642AA429E800B17308 /* VMWizardContent.swift */; }; + CEF7F6032AEEDCC400E34952 /* UTMExternalSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */; platformFilter = ios; }; + CEF7F6052AEEDCC400E34952 /* UTMQemuConfigurationQEMU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619B128431DA5000034B2 /* UTMQemuConfigurationQEMU.swift */; }; + CEF7F6062AEEDCC400E34952 /* UTMQemuConfigurationDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82328441EAD0029D60D /* UTMQemuConfigurationDisplay.swift */; }; + CEF7F6072AEEDCC400E34952 /* UTMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE80111F2AD4E9E8009001C2 /* UTMApp.swift */; platformFilters = (xros, ); }; + CEF7F6082AEEDCC400E34952 /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; }; + CEF7F60A2AEEDCC400E34952 /* VMConfigSerialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99B728630A780055C215 /* VMConfigSerialView.swift */; }; + CEF7F60B2AEEDCC400E34952 /* VMWizardState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305526A2AFDD00667B63 /* VMWizardState.swift */; }; + CEF7F60C2AEEDCC400E34952 /* UTMQemuConfigurationInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82B284482C10029D60D /* UTMQemuConfigurationInput.swift */; }; + CEF7F60D2AEEDCC400E34952 /* VMDisplayMetalViewController+Keyboard.m in Sources */ = {isa = PBXBuildFile; fileRef = CE3ADD66240EFBCA002D6A5F /* VMDisplayMetalViewController+Keyboard.m */; }; + CEF7F60E2AEEDCC400E34952 /* UTMExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954624AD4F980059923A /* UTMExtensions.swift */; }; + CEF7F60F2AEEDCC400E34952 /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; }; + CEF7F6112AEEDCC400E34952 /* VMConfigSoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953A24AD4F980059923A /* VMConfigSoundView.swift */; }; + CEF7F6122AEEDCC400E34952 /* UTMLegacyQemuConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CE31C244225E555600A965DD /* UTMLegacyQemuConfiguration.m */; }; + CEF7F6142AEEDCC400E34952 /* VMDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954B24AD4F980059923A /* VMDetailsView.swift */; }; + CEF7F6152AEEDCC400E34952 /* VMDisplayMetalViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5286EC94243748C3007E6CBC /* VMDisplayMetalViewController.m */; }; + CEF7F6162AEEDCC400E34952 /* UTMQemuConfiguration+Arguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99C328670F650055C215 /* UTMQemuConfiguration+Arguments.swift */; }; + CEF7F6172AEEDCC400E34952 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7524F4654400CAF323 /* Main.swift */; }; + CEF7F6192AEEDCC400E34952 /* VMCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954324AD4F980059923A /* VMCardView.swift */; }; + CEF7F61A2AEEDCC400E34952 /* VMNavigationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432328F28C2CDAD00CFBC97 /* VMNavigationListView.swift */; }; + CEF7F61B2AEEDCC400E34952 /* UTMSingleWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58CD28937FED00137A20 /* UTMSingleWindowView.swift */; }; + CEF7F61C2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Sharing.m in Sources */ = {isa = PBXBuildFile; fileRef = CE059DC4243BFA3200338317 /* UTMLegacyQemuConfiguration+Sharing.m */; }; + CEF7F61D2AEEDCC400E34952 /* SizeTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C505AB28C588EC007CE8FF /* SizeTextField.swift */; }; + CEF7F61E2AEEDCC400E34952 /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; }; + CEF7F61F2AEEDCC400E34952 /* VMToolbarDisplayMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6F6FC289319AE00080EEF /* VMToolbarDisplayMenuView.swift */; }; + CEF7F6202AEEDCC400E34952 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D224CD230300532628 /* ActivityView.swift */; }; + CEF7F6212AEEDCC400E34952 /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; }; + CEF7F6222AEEDCC400E34952 /* QEMUArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99B3286300160055C215 /* QEMUArgument.swift */; }; + CEF7F6232AEEDCC400E34952 /* VMPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954224AD4F980059923A /* VMPlaceholderView.swift */; }; + CEF7F6242AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.m in Sources */ = {isa = PBXBuildFile; fileRef = 83FBDD55242FA7BC00D2C5D7 /* VMDisplayMetalViewController+Pointer.m */; }; + CEF7F6252AEEDCC400E34952 /* VMDisplayViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE72B4AC2463579D00716A11 /* VMDisplayViewController.m */; }; + CEF7F6282AEEDCC400E34952 /* UTMQemuConfigurationSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF83328450C0B0029D60D /* UTMQemuConfigurationSound.swift */; }; + CEF7F6292AEEDCC400E34952 /* VMScroll.m in Sources */ = {isa = PBXBuildFile; fileRef = CE20FAE72448D2BE0059AE11 /* VMScroll.m */; }; + CEF7F62A2AEEDCC400E34952 /* VMConfigNetworkPortForwardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954D24AD4F980059923A /* VMConfigNetworkPortForwardView.swift */; }; + CEF7F62B2AEEDCC400E34952 /* UTMDownloadSupportToolsTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843232B628C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift */; }; + CEF7F62C2AEEDCC400E34952 /* UTMQemuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619A5284315C1000034B2 /* UTMQemuConfiguration.swift */; }; + CEF7F62E2AEEDCC400E34952 /* libgstautodetect.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19522265425900355E14 /* libgstautodetect.a */; }; + CEF7F62F2AEEDCC400E34952 /* libgstaudiotestsrc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19532265425900355E14 /* libgstaudiotestsrc.a */; }; + CEF7F6302AEEDCC400E34952 /* libgstvideoconvert.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19542265425900355E14 /* libgstvideoconvert.a */; }; + CEF7F6312AEEDCC400E34952 /* libgstaudioconvert.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19552265425900355E14 /* libgstaudioconvert.a */; }; + CEF7F6322AEEDCC400E34952 /* libgstvideoscale.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19562265425900355E14 /* libgstvideoscale.a */; }; + CEF7F6332AEEDCC400E34952 /* IQKeyboardManagerSwift in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = CEF7F5862AEEDCC400E34952 /* IQKeyboardManagerSwift */; }; + CEF7F6342AEEDCC400E34952 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE66450C2269313200B0849A /* MetalKit.framework */; }; + CEF7F6352AEEDCC400E34952 /* libgstvolume.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19572265425900355E14 /* libgstvolume.a */; }; + CEF7F6362AEEDCC400E34952 /* libgstcoreelements.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19582265425900355E14 /* libgstcoreelements.a */; }; + CEF7F6372AEEDCC400E34952 /* libgstvideorate.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19592265425900355E14 /* libgstvideorate.a */; }; + CEF7F6382AEEDCC400E34952 /* libgstjpeg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195A2265425900355E14 /* libgstjpeg.a */; }; + CEF7F6392AEEDCC400E34952 /* libgstaudioresample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195B2265425900355E14 /* libgstaudioresample.a */; }; + CEF7F63A2AEEDCC400E34952 /* libgstplayback.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195C2265425900355E14 /* libgstplayback.a */; }; + CEF7F63C2AEEDCC400E34952 /* libgstadder.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195D2265425900355E14 /* libgstadder.a */; }; + CEF7F63D2AEEDCC400E34952 /* libgstaudiorate.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195F2265425900355E14 /* libgstaudiorate.a */; }; + CEF7F63F2AEEDCC400E34952 /* libgstvideofilter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19602265425900355E14 /* libgstvideofilter.a */; }; + CEF7F6402AEEDCC400E34952 /* SwiftUIVisualEffects in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F5902AEEDCC400E34952 /* SwiftUIVisualEffects */; }; + CEF7F6422AEEDCC400E34952 /* libgstapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19612265425900355E14 /* libgstapp.a */; }; + CEF7F6432AEEDCC400E34952 /* libgstgio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19622265425A00355E14 /* libgstgio.a */; }; + CEF7F6442AEEDCC400E34952 /* libgsttypefindfunctions.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19632265425A00355E14 /* libgsttypefindfunctions.a */; }; + CEF7F6452AEEDCC400E34952 /* libgstvideotestsrc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19642265425A00355E14 /* libgstvideotestsrc.a */; }; + CEF7F6462AEEDCC400E34952 /* libgstosxaudio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19652265425A00355E14 /* libgstosxaudio.a */; }; + CEF7F6472AEEDCC400E34952 /* gmodule-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63D822653C7300FC7E63 /* gmodule-2.0.0.framework */; }; + CEF7F6482AEEDCC400E34952 /* jpeg.62.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63D922653C7300FC7E63 /* jpeg.62.framework */; }; + CEF7F6492AEEDCC400E34952 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F5882AEEDCC400E34952 /* ZIPFoundation */; }; + CEF7F64A2AEEDCC400E34952 /* intl.8.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DA22653C7300FC7E63 /* intl.8.framework */; }; + CEF7F64B2AEEDCC400E34952 /* gstapp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DB22653C7300FC7E63 /* gstapp-1.0.0.framework */; }; + CEF7F64C2AEEDCC400E34952 /* gthread-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; }; + CEF7F64D2AEEDCC400E34952 /* gstrtp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DD22653C7400FC7E63 /* gstrtp-1.0.0.framework */; }; + CEF7F64E2AEEDCC400E34952 /* gstriff-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DE22653C7400FC7E63 /* gstriff-1.0.0.framework */; }; + CEF7F6512AEEDCC400E34952 /* AVFAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84818C0B2898A07A009EDB67 /* AVFAudio.framework */; }; + CEF7F6522AEEDCC400E34952 /* gstreamer-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E022653C7400FC7E63 /* gstreamer-1.0.0.framework */; }; + CEF7F6532AEEDCC400E34952 /* json-glib-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E222653C7400FC7E63 /* json-glib-1.0.0.framework */; }; + CEF7F6542AEEDCC400E34952 /* ffi.7.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E322653C7400FC7E63 /* ffi.7.framework */; }; + CEF7F6552AEEDCC400E34952 /* gstnet-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E522653C7400FC7E63 /* gstnet-1.0.0.framework */; }; + CEF7F6562AEEDCC400E34952 /* gstbase-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E822653C7400FC7E63 /* gstbase-1.0.0.framework */; }; + CEF7F6572AEEDCC400E34952 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F5842AEEDCC400E34952 /* Logging */; }; + CEF7F6582AEEDCC400E34952 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F58E2AEEDCC400E34952 /* SwiftTerm */; }; + CEF7F65A2AEEDCC400E34952 /* phodav-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; }; + CEF7F65C2AEEDCC400E34952 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE0E9B86252FD06B0026E02B /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + CEF7F65D2AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EE22653C7400FC7E63 /* gstcontroller-1.0.0.framework */; }; + CEF7F65E2AEEDCC400E34952 /* gstaudio-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EF22653C7400FC7E63 /* gstaudio-1.0.0.framework */; }; + CEF7F65F2AEEDCC400E34952 /* gpg-error.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; }; + CEF7F6602AEEDCC400E34952 /* gcrypt.20.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; }; + CEF7F6612AEEDCC400E34952 /* InAppSettingsKit in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = CEF7F5922AEEDCC400E34952 /* InAppSettingsKit */; }; + CEF7F6622AEEDCC400E34952 /* gobject-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F522653C7400FC7E63 /* gobject-2.0.0.framework */; }; + CEF7F6642AEEDCC400E34952 /* gsttag-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F622653C7400FC7E63 /* gsttag-1.0.0.framework */; }; + CEF7F6652AEEDCC400E34952 /* gio-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F822653C7400FC7E63 /* gio-2.0.0.framework */; }; + CEF7F6662AEEDCC400E34952 /* gstvideo-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F922653C7400FC7E63 /* gstvideo-1.0.0.framework */; }; + CEF7F6672AEEDCC400E34952 /* spice-client-glib-2.0.8.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63FE22653C7500FC7E63 /* spice-client-glib-2.0.8.framework */; }; + CEF7F6682AEEDCC400E34952 /* gstrtsp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640122653C7500FC7E63 /* gstrtsp-1.0.0.framework */; }; + CEF7F6692AEEDCC400E34952 /* opus.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640322653C7500FC7E63 /* opus.0.framework */; }; + CEF7F66A2AEEDCC400E34952 /* glib-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640422653C7500FC7E63 /* glib-2.0.0.framework */; }; + CEF7F66B2AEEDCC400E34952 /* png16.16.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640522653C7500FC7E63 /* png16.16.framework */; }; + CEF7F66C2AEEDCC400E34952 /* gstfft-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640922653C7500FC7E63 /* gstfft-1.0.0.framework */; }; + CEF7F66D2AEEDCC400E34952 /* crypto.1.1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640A22653C7500FC7E63 /* crypto.1.1.framework */; }; + CEF7F66E2AEEDCC400E34952 /* gstpbutils-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640E22653C7500FC7E63 /* gstpbutils-1.0.0.framework */; }; + CEF7F66F2AEEDCC400E34952 /* gstallocators-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641122653C7500FC7E63 /* gstallocators-1.0.0.framework */; }; + CEF7F6702AEEDCC400E34952 /* gstcheck-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641422653C7500FC7E63 /* gstcheck-1.0.0.framework */; }; + CEF7F6712AEEDCC400E34952 /* iconv.2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641522653C7500FC7E63 /* iconv.2.framework */; }; + CEF7F6722AEEDCC400E34952 /* gstsdp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641622653C7500FC7E63 /* gstsdp-1.0.0.framework */; }; + CEF7F6742AEEDCC400E34952 /* ssl.1.1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641722653C7500FC7E63 /* ssl.1.1.framework */; }; + CEF7F6762AEEDCC400E34952 /* pixman-1.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641922653C7600FC7E63 /* pixman-1.0.framework */; }; + CEF7F6792AEEDCC400E34952 /* Icons in Resources */ = {isa = PBXBuildFile; fileRef = CE4698F824C8FBD9008C1BD6 /* Icons */; }; + CEF7F67A2AEEDCC400E34952 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFB02A8A266CB09C006CD71A /* InfoPlist.strings */; }; + CEF7F67B2AEEDCC400E34952 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 521F3EFB2414F73800130500 /* Localizable.strings */; }; + CEF7F67C2AEEDCC400E34952 /* qemu in Resources */ = {isa = PBXBuildFile; fileRef = CE9D18F72265410E00355E14 /* qemu */; }; + CEF7F67D2AEEDCC400E34952 /* VMDisplayMetalViewInputAccessory.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE061CE9289EB6250000351C /* VMDisplayMetalViewInputAccessory.xib */; }; + CEF7F67E2AEEDCC400E34952 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; }; + CEF7F67F2AEEDCC400E34952 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5286EC91243748AC007E6CBC /* Settings.bundle */; }; + CEF7F6802AEEDCC400E34952 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE550BD52259479D0063E575 /* Assets.xcassets */; }; + CEF7F6832AEEDCC400E34952 /* gpg-error.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6852AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63EE22653C7400FC7E63 /* gstcontroller-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6862AEEDCC400E34952 /* gstallocators-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641122653C7500FC7E63 /* gstallocators-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6872AEEDCC400E34952 /* gstbase-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E822653C7400FC7E63 /* gstbase-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6882AEEDCC400E34952 /* ffi.7.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E322653C7400FC7E63 /* ffi.7.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6892AEEDCC400E34952 /* ssl.1.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641722653C7500FC7E63 /* ssl.1.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F68B2AEEDCC400E34952 /* gio-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F822653C7400FC7E63 /* gio-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F68D2AEEDCC400E34952 /* png16.16.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640522653C7500FC7E63 /* png16.16.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F68E2AEEDCC400E34952 /* gstnet-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E522653C7400FC7E63 /* gstnet-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6902AEEDCC400E34952 /* crypto.1.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640A22653C7500FC7E63 /* crypto.1.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6922AEEDCC400E34952 /* gstapp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DB22653C7300FC7E63 /* gstapp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6962AEEDCC400E34952 /* gsttag-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F622653C7400FC7E63 /* gsttag-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6982AEEDCC400E34952 /* gstrtp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DD22653C7400FC7E63 /* gstrtp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6992AEEDCC400E34952 /* gstriff-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DE22653C7400FC7E63 /* gstriff-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F69C2AEEDCC400E34952 /* phodav-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F69D2AEEDCC400E34952 /* gthread-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6A22AEEDCC400E34952 /* gobject-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F522653C7400FC7E63 /* gobject-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6A32AEEDCC400E34952 /* gmodule-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63D822653C7300FC7E63 /* gmodule-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6AD2AEEDCC400E34952 /* glib-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640422653C7500FC7E63 /* glib-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6AE2AEEDCC400E34952 /* EGL.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A426AF5F0F008594E5 /* EGL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CEF7F6B42AEEDCC400E34952 /* intl.8.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DA22653C7300FC7E63 /* intl.8.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6B52AEEDCC400E34952 /* gstreamer-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E022653C7400FC7E63 /* gstreamer-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6B62AEEDCC400E34952 /* GLESv2.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A326AF5F0F008594E5 /* GLESv2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + CEF7F6B72AEEDCC400E34952 /* gstvideo-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F922653C7400FC7E63 /* gstvideo-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6B82AEEDCC400E34952 /* json-glib-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E222653C7400FC7E63 /* json-glib-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6B92AEEDCC400E34952 /* pixman-1.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641922653C7600FC7E63 /* pixman-1.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6BA2AEEDCC400E34952 /* jpeg.62.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63D922653C7300FC7E63 /* jpeg.62.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6BE2AEEDCC400E34952 /* spice-client-glib-2.0.8.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63FE22653C7500FC7E63 /* spice-client-glib-2.0.8.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6BF2AEEDCC400E34952 /* opus.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640322653C7500FC7E63 /* opus.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6C02AEEDCC400E34952 /* gstsdp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641622653C7500FC7E63 /* gstsdp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6C42AEEDCC400E34952 /* gstaudio-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63EF22653C7400FC7E63 /* gstaudio-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6C52AEEDCC400E34952 /* gstcheck-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641422653C7500FC7E63 /* gstcheck-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6C72AEEDCC400E34952 /* iconv.2.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641522653C7500FC7E63 /* iconv.2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6C92AEEDCC400E34952 /* gstrtsp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640122653C7500FC7E63 /* gstrtsp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6CB2AEEDCC400E34952 /* gcrypt.20.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6CC2AEEDCC400E34952 /* gstfft-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640922653C7500FC7E63 /* gstfft-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6CE2AEEDCC400E34952 /* gstpbutils-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640E22653C7500FC7E63 /* gstpbutils-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEF7F6D62AEEEF7D00E34952 /* CocoaSpiceNoUsb in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F6D52AEEEF7D00E34952 /* CocoaSpiceNoUsb */; }; CEF83F262500901300557D15 /* qemu in Resources */ = {isa = PBXBuildFile; fileRef = CE9D18F72265410E00355E14 /* qemu */; }; CEF83F862500947D00557D15 /* gcrypt.20.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; }; CEF83F872500948800557D15 /* gpg-error.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; }; @@ -924,6 +1202,7 @@ CEF83F8D250094E700557D15 /* gthread-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; CEF83F8E250094EC00557D15 /* gpg-error.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; CEF83F8F250094EE00557D15 /* gcrypt.20.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */; }; CEFE98DF29485237007CB7A8 /* UTM.sdef in Resources */ = {isa = PBXBuildFile; fileRef = CEFE98DE29485237007CB7A8 /* UTM.sdef */; }; CEFE98E129485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */; }; FF0307552A84E3B70049979B /* QEMULauncher-InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FF0307532A84E3B70049979B /* QEMULauncher-InfoPlist.strings */; }; @@ -1243,6 +1522,53 @@ name = "Embed XPC Services"; runOnlyForDeploymentPostprocessing = 0; }; + CEF7F6822AEEDCC400E34952 /* Embed Libraries */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CEF7F6832AEEDCC400E34952 /* gpg-error.0.framework in Embed Libraries */, + CEF7F6852AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Embed Libraries */, + CEF7F6862AEEDCC400E34952 /* gstallocators-1.0.0.framework in Embed Libraries */, + CEF7F6872AEEDCC400E34952 /* gstbase-1.0.0.framework in Embed Libraries */, + CEF7F6882AEEDCC400E34952 /* ffi.7.framework in Embed Libraries */, + CEF7F6892AEEDCC400E34952 /* ssl.1.1.framework in Embed Libraries */, + CEF7F68B2AEEDCC400E34952 /* gio-2.0.0.framework in Embed Libraries */, + CEF7F68D2AEEDCC400E34952 /* png16.16.framework in Embed Libraries */, + CEF7F68E2AEEDCC400E34952 /* gstnet-1.0.0.framework in Embed Libraries */, + CEF7F6902AEEDCC400E34952 /* crypto.1.1.framework in Embed Libraries */, + CEF7F6922AEEDCC400E34952 /* gstapp-1.0.0.framework in Embed Libraries */, + CEF7F6962AEEDCC400E34952 /* gsttag-1.0.0.framework in Embed Libraries */, + CEF7F6982AEEDCC400E34952 /* gstrtp-1.0.0.framework in Embed Libraries */, + CEF7F6992AEEDCC400E34952 /* gstriff-1.0.0.framework in Embed Libraries */, + CEF7F69C2AEEDCC400E34952 /* phodav-2.0.0.framework in Embed Libraries */, + CEF7F69D2AEEDCC400E34952 /* gthread-2.0.0.framework in Embed Libraries */, + CEF7F6A22AEEDCC400E34952 /* gobject-2.0.0.framework in Embed Libraries */, + CEF7F6A32AEEDCC400E34952 /* gmodule-2.0.0.framework in Embed Libraries */, + CEF7F6AD2AEEDCC400E34952 /* glib-2.0.0.framework in Embed Libraries */, + CEF7F6AE2AEEDCC400E34952 /* EGL.framework in Embed Libraries */, + CEF7F6B42AEEDCC400E34952 /* intl.8.framework in Embed Libraries */, + CEF7F6B52AEEDCC400E34952 /* gstreamer-1.0.0.framework in Embed Libraries */, + CEF7F6B62AEEDCC400E34952 /* GLESv2.framework in Embed Libraries */, + CEF7F6B72AEEDCC400E34952 /* gstvideo-1.0.0.framework in Embed Libraries */, + CEF7F6B82AEEDCC400E34952 /* json-glib-1.0.0.framework in Embed Libraries */, + CEF7F6B92AEEDCC400E34952 /* pixman-1.0.framework in Embed Libraries */, + CEF7F6BA2AEEDCC400E34952 /* jpeg.62.framework in Embed Libraries */, + CEF7F6BE2AEEDCC400E34952 /* spice-client-glib-2.0.8.framework in Embed Libraries */, + CEF7F6BF2AEEDCC400E34952 /* opus.0.framework in Embed Libraries */, + CEF7F6C02AEEDCC400E34952 /* gstsdp-1.0.0.framework in Embed Libraries */, + CEF7F6C42AEEDCC400E34952 /* gstaudio-1.0.0.framework in Embed Libraries */, + CEF7F6C52AEEDCC400E34952 /* gstcheck-1.0.0.framework in Embed Libraries */, + CEF7F6C72AEEDCC400E34952 /* iconv.2.framework in Embed Libraries */, + CEF7F6C92AEEDCC400E34952 /* gstrtsp-1.0.0.framework in Embed Libraries */, + CEF7F6CB2AEEDCC400E34952 /* gcrypt.20.framework in Embed Libraries */, + CEF7F6CC2AEEDCC400E34952 /* gstfft-1.0.0.framework in Embed Libraries */, + CEF7F6CE2AEEDCC400E34952 /* gstpbutils-1.0.0.framework in Embed Libraries */, + ); + name = "Embed Libraries"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -1437,10 +1763,12 @@ CE061CE8289EB6250000351C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/VMDisplayMetalViewInputAccessory.xib; sourceTree = "<group>"; }; CE061CEB289EB62E0000351C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/VMDisplayMetalViewInputAccessory.strings; sourceTree = "<group>"; }; CE064C642A563F4A003C833D /* swtpm.0.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = swtpm.0.framework; path = "$(SYSROOT_DIR)/Frameworks/swtpm.0.framework"; sourceTree = "<group>"; }; + CE08334A2B784FD400522C03 /* RemoteContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteContentView.swift; sourceTree = "<group>"; }; CE0DF17025A80B6300A51894 /* Bootstrap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Bootstrap.h; sourceTree = "<group>"; }; CE0DF17125A80B6300A51894 /* Bootstrap.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = Bootstrap.c; sourceTree = "<group>"; }; CE0E9B86252FD06B0026E02B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; CE19392526DCB093005CEC17 /* RAMSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RAMSlider.swift; sourceTree = "<group>"; }; + CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacDeviceLabel.swift; sourceTree = "<group>"; }; CE20FAE62448D2BE0059AE11 /* VMScroll.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMScroll.h; sourceTree = "<group>"; }; CE20FAE72448D2BE0059AE11 /* VMScroll.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMScroll.m; sourceTree = "<group>"; }; CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestProcessImpl.swift; sourceTree = "<group>"; }; @@ -1542,6 +1870,7 @@ CE2D955624AD4F980059923A /* Swift-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Swift-Bridging-Header.h"; sourceTree = "<group>"; }; CE31C243225E553500A965DD /* UTMLegacyQemuConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLegacyQemuConfiguration.h; sourceTree = "<group>"; }; CE31C244225E555600A965DD /* UTMLegacyQemuConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLegacyQemuConfiguration.m; sourceTree = "<group>"; }; + CE38EC682B5DB3AE008B324B /* UTMRemoteClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteClient.swift; sourceTree = "<group>"; }; CE3ADD65240EFBCA002D6A5F /* VMDisplayMetalViewController+Keyboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VMDisplayMetalViewController+Keyboard.h"; sourceTree = "<group>"; }; CE3ADD66240EFBCA002D6A5F /* VMDisplayMetalViewController+Keyboard.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "VMDisplayMetalViewController+Keyboard.m"; sourceTree = "<group>"; }; CE3ADD682411C661002D6A5F /* VMCursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMCursor.h; sourceTree = "<group>"; }; @@ -1572,11 +1901,13 @@ CE6B240A25F1F3CE0020D43E /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = "<group>"; }; CE6B240F25F1F43A0020D43E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; CE6B241025F1F4B30020D43E /* QEMULauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMULauncher.entitlements; sourceTree = "<group>"; }; + CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteMessage.swift; sourceTree = "<group>"; }; CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfirmActionModifier.swift; sourceTree = "<group>"; }; CE6EDCDD241C4A6800A719DC /* UTMLegacyViewState.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLegacyViewState.h; sourceTree = "<group>"; }; CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLegacyViewState.m; sourceTree = "<group>"; }; CE6EDCE0241DA0E900A719DC /* UTMLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLogging.h; sourceTree = "<group>"; }; CE6EDCE1241DA0E900A719DC /* UTMLogging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLogging.m; sourceTree = "<group>"; }; + CE70E8D42B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteSpiceVirtualMachine.swift; sourceTree = "<group>"; }; CE72B4AB2463579D00716A11 /* VMDisplayViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMDisplayViewController.h; sourceTree = "<group>"; }; CE72B4AC2463579D00716A11 /* VMDisplayViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMDisplayViewController.m; sourceTree = "<group>"; }; CE772AAB25C8B0F600E4E379 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; @@ -1592,6 +1923,10 @@ CE9A352D26533A51005077CF /* JailbreakInterposer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JailbreakInterposer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE9A353026533A52005077CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; CE9A353F26533AE6005077CF /* JailbreakInterposer.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = JailbreakInterposer.c; sourceTree = "<group>"; }; + CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteServer.swift; sourceTree = "<group>"; }; + CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteKeyManager.swift; sourceTree = "<group>"; }; + CE9B15452B12A87E003A32DD /* GenerateKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GenerateKey.h; sourceTree = "<group>"; }; + CE9B15462B12A87E003A32DD /* GenerateKey.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = GenerateKey.c; sourceTree = "<group>"; }; CE9D18F72265410E00355E14 /* qemu */ = {isa = PBXFileReference; lastKnownFileType = folder; name = qemu; path = "$(SYSROOT_DIR)/share/qemu"; sourceTree = "<group>"; }; CE9D19522265425900355E14 /* libgstautodetect.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgstautodetect.a; path = "$(SYSROOT_DIR)/lib/gstreamer-1.0/libgstautodetect.a"; sourceTree = "<group>"; }; CE9D19532265425900355E14 /* libgstaudiotestsrc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgstaudiotestsrc.a; path = "$(SYSROOT_DIR)/lib/gstreamer-1.0/libgstaudiotestsrc.a"; sourceTree = "<group>"; }; @@ -1649,6 +1984,8 @@ CEC794B9294924E300121A9F /* UTMScriptingSerialPortImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingSerialPortImpl.swift; sourceTree = "<group>"; }; CEC794BB2949663C00121A9F /* UTMScripting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UTMScripting.swift; sourceTree = "<group>"; }; CEC9968328AA516000E7A025 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; + CECF02562B706ADD00409FC0 /* UTMRemoteConnectInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMRemoteConnectInterface.h; sourceTree = "<group>"; }; + CECF02572B70909900409FC0 /* Info-Remote.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Remote.plist"; sourceTree = "<group>"; }; CED234EC254796E500ED0A57 /* NumberTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTextField.swift; sourceTree = "<group>"; }; CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigDriveCreateView.swift; sourceTree = "<group>"; }; CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigInfoView.swift; sourceTree = "<group>"; }; @@ -1662,13 +1999,18 @@ CEE0420B244117040001680F /* UTMLegacyQemuConfiguration+Display.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Display.m"; sourceTree = "<group>"; }; CEE0421024418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Miscellaneous.h"; sourceTree = "<group>"; }; CEE0421124418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Miscellaneous.m"; sourceTree = "<group>"; }; + CEE06B262B2FC89400A811AE /* UTMServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMServerView.swift; sourceTree = "<group>"; }; + CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteConnectView.swift; sourceTree = "<group>"; }; CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Constants.m"; sourceTree = "<group>"; }; CEE7E935287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Constants.h"; sourceTree = "<group>"; }; CEE7ED472A90256100E6B4AB /* VMDisplayMetalViewController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VMDisplayMetalViewController+Private.h"; sourceTree = "<group>"; }; + CEE8B4C12B71DF4C0035AE86 /* UTMQemuSystemBackends.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMQemuSystemBackends.h; sourceTree = "<group>"; }; CEEB66442284B942002737B2 /* VMKeyboardButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMKeyboardButton.h; sourceTree = "<group>"; }; CEEB66452284B942002737B2 /* VMKeyboardButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMKeyboardButton.m; sourceTree = "<group>"; }; CEEC811A24E48EC600ACB0B3 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; CEECE13B25E47D9500A2AAB8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSpiceVirtualMachine.swift; sourceTree = "<group>"; }; + CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMPipeInterface.swift; sourceTree = "<group>"; }; CEF0300526A25A6900667B63 /* VMWizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardView.swift; sourceTree = "<group>"; }; CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BigButtonStyle.swift; sourceTree = "<group>"; }; CEF0304D26A2AFBE00667B63 /* Spinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spinner.swift; sourceTree = "<group>"; }; @@ -1684,7 +2026,9 @@ CEF6F5EA26DDD60500BC434D /* macOS-unsigned.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "macOS-unsigned.entitlements"; sourceTree = "<group>"; }; CEF6F5EB26DDD63100BC434D /* QEMUHelper-unsigned.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "QEMUHelper-unsigned.entitlements"; sourceTree = "<group>"; }; CEF6F5EC26DDD65700BC434D /* QEMULauncher-unsigned.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "QEMULauncher-unsigned.entitlements"; sourceTree = "<group>"; }; + CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "UTM Remote.app"; sourceTree = BUILT_PRODUCTS_DIR; }; CEF84ADA2887D7D300578F41 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; + CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMRemoteSessionState.swift; sourceTree = "<group>"; }; CEFE98DE29485237007CB7A8 /* UTM.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = UTM.sdef; sourceTree = "<group>"; }; CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingVirtualMachineImpl.swift; sourceTree = "<group>"; }; E2D64BC7241DB24B0034E0C6 /* UTMSpiceIO.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMSpiceIO.h; sourceTree = "<group>"; }; @@ -1780,6 +2124,7 @@ 84818C0C2898A07A009EDB67 /* AVFAudio.framework in Frameworks */, CE2D934924AD46670059923A /* gstreamer-1.0.0.framework in Frameworks */, CE2D934B24AD46670059923A /* json-glib-1.0.0.framework in Frameworks */, + CE89CB0E2B8B1B5A006B2CC2 /* VisionKeyboardKit in Frameworks */, CE2D934C24AD46670059923A /* ffi.7.framework in Frameworks */, CE2D934D24AD46670059923A /* gstnet-1.0.0.framework in Frameworks */, CE2D934E24AD46670059923A /* gstbase-1.0.0.framework in Frameworks */, @@ -1812,6 +2157,7 @@ CE2D936324AD46670059923A /* iconv.2.framework in Frameworks */, CE2D936424AD46670059923A /* gstsdp-1.0.0.framework in Frameworks */, 84B36D1E27B3264600C22685 /* CocoaSpice in Frameworks */, + CE9B15382B11A4A7003A32DD /* SwiftConnect in Frameworks */, CE2D936524AD46670059923A /* ssl.1.1.framework in Frameworks */, CE2D936624AD46670059923A /* spice-server.1.framework in Frameworks */, CE2D936724AD46670059923A /* pixman-1.0.framework in Frameworks */, @@ -1851,6 +2197,7 @@ CE03D0CE24D9A30100F76B84 /* iconv.2.framework in Frameworks */, CE0B6EF124AD677200FE012D /* libgstplayback.a in Frameworks */, CE0B6EF424AD677200FE012D /* json-glib-1.0.0.framework in Frameworks */, + CEDD11C12B7C74D7004DDAC6 /* SwiftPortmap in Frameworks */, CE0B6ED124AD677200FE012D /* phodav-2.0.0.framework in Frameworks */, CEF83F862500947D00557D15 /* gcrypt.20.framework in Frameworks */, CE0B6ECB24AD677200FE012D /* gstcheck-1.0.0.framework in Frameworks */, @@ -1860,6 +2207,7 @@ CE0B6EE524AD677200FE012D /* gstbase-1.0.0.framework in Frameworks */, CEF83F882500949D00557D15 /* gthread-2.0.0.framework in Frameworks */, CE03D08724D90F0700F76B84 /* gobject-2.0.0.framework in Frameworks */, + CE9B15362B11A491003A32DD /* SwiftConnect in Frameworks */, CE0B6F0A24AD677200FE012D /* spice-client-glib-2.0.8.framework in Frameworks */, CE0B6ECF24AD677200FE012D /* gstrtp-1.0.0.framework in Frameworks */, CE0B6ECC24AD677200FE012D /* gstriff-1.0.0.framework in Frameworks */, @@ -1907,6 +2255,7 @@ CEA45F25263519B5002FA97D /* libgstaudiotestsrc.a in Frameworks */, CEA45F26263519B5002FA97D /* libgstvideoconvert.a in Frameworks */, CEA45F27263519B5002FA97D /* libgstaudioconvert.a in Frameworks */, + CE89CB102B8B1B6A006B2CC2 /* VisionKeyboardKit in Frameworks */, 8401865C2887AFDC0050AC51 /* SwiftTerm in Frameworks */, CEA45F28263519B5002FA97D /* libgstvideoscale.a in Frameworks */, CEA45F29263519B5002FA97D /* IQKeyboardManagerSwift in Frameworks */, @@ -1919,6 +2268,7 @@ CEA45F2E263519B5002FA97D /* libgstjpeg.a in Frameworks */, CEA45F2F263519B5002FA97D /* libgstaudioresample.a in Frameworks */, CEA45F30263519B5002FA97D /* libgstplayback.a in Frameworks */, + CE9B153A2B11A4AE003A32DD /* SwiftConnect in Frameworks */, CEA45F31263519B5002FA97D /* libgstadder.a in Frameworks */, CE02C8B1294EE58C006DFE48 /* slirp.0.framework in Frameworks */, CEA45F32263519B5002FA97D /* libgstaudiorate.a in Frameworks */, @@ -1981,6 +2331,79 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEF7F62D2AEEDCC400E34952 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CEF7F62E2AEEDCC400E34952 /* libgstautodetect.a in Frameworks */, + CEF7F62F2AEEDCC400E34952 /* libgstaudiotestsrc.a in Frameworks */, + CEF7F6302AEEDCC400E34952 /* libgstvideoconvert.a in Frameworks */, + CEF7F6312AEEDCC400E34952 /* libgstaudioconvert.a in Frameworks */, + CEF7F6322AEEDCC400E34952 /* libgstvideoscale.a in Frameworks */, + CEF7F6332AEEDCC400E34952 /* IQKeyboardManagerSwift in Frameworks */, + CEF7F6342AEEDCC400E34952 /* MetalKit.framework in Frameworks */, + CEF7F6352AEEDCC400E34952 /* libgstvolume.a in Frameworks */, + CEF7F6362AEEDCC400E34952 /* libgstcoreelements.a in Frameworks */, + CEF7F6372AEEDCC400E34952 /* libgstvideorate.a in Frameworks */, + CEF7F6382AEEDCC400E34952 /* libgstjpeg.a in Frameworks */, + CEF7F6392AEEDCC400E34952 /* libgstaudioresample.a in Frameworks */, + CEF7F63A2AEEDCC400E34952 /* libgstplayback.a in Frameworks */, + CEF7F63C2AEEDCC400E34952 /* libgstadder.a in Frameworks */, + CEF7F63D2AEEDCC400E34952 /* libgstaudiorate.a in Frameworks */, + CEF7F63F2AEEDCC400E34952 /* libgstvideofilter.a in Frameworks */, + CEF7F6402AEEDCC400E34952 /* SwiftUIVisualEffects in Frameworks */, + CEF7F6422AEEDCC400E34952 /* libgstapp.a in Frameworks */, + CEF7F6432AEEDCC400E34952 /* libgstgio.a in Frameworks */, + CEF7F6442AEEDCC400E34952 /* libgsttypefindfunctions.a in Frameworks */, + CEF7F6452AEEDCC400E34952 /* libgstvideotestsrc.a in Frameworks */, + CEF7F6462AEEDCC400E34952 /* libgstosxaudio.a in Frameworks */, + CEF7F6472AEEDCC400E34952 /* gmodule-2.0.0.framework in Frameworks */, + CEF7F6482AEEDCC400E34952 /* jpeg.62.framework in Frameworks */, + CE9B153C2B11A4B4003A32DD /* SwiftConnect in Frameworks */, + CEF7F6492AEEDCC400E34952 /* ZIPFoundation in Frameworks */, + CEF7F64A2AEEDCC400E34952 /* intl.8.framework in Frameworks */, + CEF7F64B2AEEDCC400E34952 /* gstapp-1.0.0.framework in Frameworks */, + CEF7F64C2AEEDCC400E34952 /* gthread-2.0.0.framework in Frameworks */, + CEF7F64D2AEEDCC400E34952 /* gstrtp-1.0.0.framework in Frameworks */, + CEF7F64E2AEEDCC400E34952 /* gstriff-1.0.0.framework in Frameworks */, + CEF7F6512AEEDCC400E34952 /* AVFAudio.framework in Frameworks */, + CEF7F6522AEEDCC400E34952 /* gstreamer-1.0.0.framework in Frameworks */, + CEF7F6532AEEDCC400E34952 /* json-glib-1.0.0.framework in Frameworks */, + CEF7F6542AEEDCC400E34952 /* ffi.7.framework in Frameworks */, + CEF7F6552AEEDCC400E34952 /* gstnet-1.0.0.framework in Frameworks */, + CEF7F6562AEEDCC400E34952 /* gstbase-1.0.0.framework in Frameworks */, + CEF7F6572AEEDCC400E34952 /* Logging in Frameworks */, + CEF7F6582AEEDCC400E34952 /* SwiftTerm in Frameworks */, + CEF7F65A2AEEDCC400E34952 /* phodav-2.0.0.framework in Frameworks */, + CEF7F65C2AEEDCC400E34952 /* SwiftUI.framework in Frameworks */, + CEF7F65D2AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Frameworks */, + CEF7F65E2AEEDCC400E34952 /* gstaudio-1.0.0.framework in Frameworks */, + CEF7F65F2AEEDCC400E34952 /* gpg-error.0.framework in Frameworks */, + CEF7F6602AEEDCC400E34952 /* gcrypt.20.framework in Frameworks */, + CEF7F6612AEEDCC400E34952 /* InAppSettingsKit in Frameworks */, + CEF7F6622AEEDCC400E34952 /* gobject-2.0.0.framework in Frameworks */, + CEF7F6642AEEDCC400E34952 /* gsttag-1.0.0.framework in Frameworks */, + CEF7F6652AEEDCC400E34952 /* gio-2.0.0.framework in Frameworks */, + CEF7F6662AEEDCC400E34952 /* gstvideo-1.0.0.framework in Frameworks */, + CEF7F6672AEEDCC400E34952 /* spice-client-glib-2.0.8.framework in Frameworks */, + CEF7F6D62AEEEF7D00E34952 /* CocoaSpiceNoUsb in Frameworks */, + CEF7F6682AEEDCC400E34952 /* gstrtsp-1.0.0.framework in Frameworks */, + CEF7F6692AEEDCC400E34952 /* opus.0.framework in Frameworks */, + CEF7F66A2AEEDCC400E34952 /* glib-2.0.0.framework in Frameworks */, + CEF7F66B2AEEDCC400E34952 /* png16.16.framework in Frameworks */, + CE89CB122B8B1B7A006B2CC2 /* VisionKeyboardKit in Frameworks */, + CEF7F66C2AEEDCC400E34952 /* gstfft-1.0.0.framework in Frameworks */, + CEF7F66D2AEEDCC400E34952 /* crypto.1.1.framework in Frameworks */, + CEF7F66E2AEEDCC400E34952 /* gstpbutils-1.0.0.framework in Frameworks */, + CEF7F66F2AEEDCC400E34952 /* gstallocators-1.0.0.framework in Frameworks */, + CEF7F6702AEEDCC400E34952 /* gstcheck-1.0.0.framework in Frameworks */, + CEF7F6712AEEDCC400E34952 /* iconv.2.framework in Frameworks */, + CEF7F6722AEEDCC400E34952 /* gstsdp-1.0.0.framework in Frameworks */, + CEF7F6742AEEDCC400E34952 /* ssl.1.1.framework in Frameworks */, + CEF7F6762AEEDCC400E34952 /* pixman-1.0.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -2191,6 +2614,7 @@ CEBBF1A624B5730F00C15049 /* UTMDataExtension.swift */, 84E3A91A2946D2590024A740 /* UTMMenuBarExtraScene.swift */, CEB54C802931C43F000D2AA9 /* UTMPatches.swift */, + CEE06B262B2FC89400A811AE /* UTMServerView.swift */, 8401FD9F269D266E00265F0D /* VMConfigAppleBootView.swift */, 8401FDA3269D43CF00265F0D /* VMConfigAppleDisplayView.swift */, 8401FDAF269E1F7F00265F0D /* VMConfigAppleDriveCreateView.swift */, @@ -2210,6 +2634,7 @@ CE2D953D24AD4F980059923A /* VMSettingsView.swift */, CEF0300526A25A6900667B63 /* VMWizardView.swift */, 84BB99392899E8D500DF28B2 /* VMHeadlessSessionState.swift */, + CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */, 53A0BDD426D79FE40010EDC5 /* SavePanel.swift */, CE2D954124AD4F980059923A /* Info.plist */, FFB02A8E266CB09C006CD71A /* InfoPlist.strings */, @@ -2227,11 +2652,13 @@ CE8813D224CD230300532628 /* ActivityView.swift */, CED814EE24C7EB760042F0F1 /* ImagePicker.swift */, 84CE3DAD2904C17C00FF068B /* IASKAppSettings.swift */, + CE08334A2B784FD400522C03 /* RemoteContentView.swift */, 841E58D02893AF5400137A20 /* UTMApp.swift */, CEBBF1A424B56A2900C15049 /* UTMDataExtension.swift */, 841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */, 841E58CD28937FED00137A20 /* UTMSingleWindowView.swift */, 842B9F8C28CC58B700031EE7 /* UTMPatches.swift */, + CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */, 84CE3DB02904C7A100FF068B /* UTMSettingsView.swift */, CE2D954D24AD4F980059923A /* VMConfigNetworkPortForwardView.swift */, 84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */, @@ -2247,6 +2674,7 @@ CEF0307026A2B04300667B63 /* VMWizardView.swift */, CE95877426D74C2A0086BDE8 /* iOS.entitlements */, CE2D954F24AD4F980059923A /* Info.plist */, + CECF02572B70909900409FC0 /* Info-Remote.plist */, FFB02A8A266CB09C006CD71A /* InfoPlist.strings */, 5286EC91243748AC007E6CBC /* Settings.bundle */, ); @@ -2305,6 +2733,7 @@ CE9D18F72265410E00355E14 /* qemu */, CE6B240925F1F3CE0020D43E /* QEMULauncher */, CE9A352E26533A51005077CF /* JailbreakInterposer */, + CE9B153D2B11A4ED003A32DD /* Remote */, CE4698F824C8FBD9008C1BD6 /* Icons */, CEFE98DD2948518D007CB7A8 /* Scripting */, 84E3A8F1293DB37E0024A740 /* utmctl */, @@ -2324,6 +2753,7 @@ CE9A352D26533A51005077CF /* JailbreakInterposer.framework */, 8401FD62269BE9C500265F0D /* QEMULauncher.app */, 84E3A8F0293DB37E0024A740 /* utmctl */, + CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */, ); name = Products; sourceTree = "<group>"; @@ -2341,12 +2771,14 @@ CE6EDCE1241DA0E900A719DC /* UTMLogging.m */, CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */, CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */, + CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */, CE9D197A226542FE00355E14 /* UTMProcess.h */, CE9D197B226542FE00355E14 /* UTMProcess.m */, 8453DCB3278CE5410037A0DA /* UTMQemuImage.swift */, 84A0A8822A47D52E0038F329 /* UTMQemuPort.swift */, CE03D05424D90BE000F76B84 /* UTMQemuSystem.h */, CE03D05024D90B4E00F76B84 /* UTMQemuSystem.m */, + CEE8B4C12B71DF4C0035AE86 /* UTMQemuSystemBackends.h */, 841E997428AA1191003C6CB6 /* UTMRegistry.swift */, 841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */, 848F71E7277A2A4E006A0240 /* UTMSerialPort.swift */, @@ -2358,6 +2790,7 @@ CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */, CE928C2926ABE6690099F293 /* UTMAppleVirtualMachine.swift */, 841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */, + CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */, ); path = Services; sourceTree = "<group>"; @@ -2420,6 +2853,21 @@ path = JailbreakInterposer; sourceTree = "<group>"; }; + CE9B153D2B11A4ED003A32DD /* Remote */ = { + isa = PBXGroup; + children = ( + CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */, + CE38EC682B5DB3AE008B324B /* UTMRemoteClient.swift */, + CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */, + CE70E8D42B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift */, + CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */, + CE9B15452B12A87E003A32DD /* GenerateKey.h */, + CE9B15462B12A87E003A32DD /* GenerateKey.c */, + CECF02562B706ADD00409FC0 /* UTMRemoteConnectInterface.h */, + ); + path = Remote; + sourceTree = "<group>"; + }; CEB63A9624F47C1200CAF323 /* Shared */ = { isa = PBXGroup; children = ( @@ -2434,6 +2882,7 @@ CE772AAB25C8B0F600E4E379 /* ContentView.swift */, 8471770527CC974F00D3A50B /* DefaultTextField.swift */, 8432329328C2ED9000CFBC97 /* FileBrowseField.swift */, + CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */, 84F909FE289488F90008DBE2 /* MenuLabel.swift */, CED234EC254796E500ED0A57 /* NumberTextField.swift */, CE19392526DCB093005CEC17 /* RAMSlider.swift */, @@ -2596,6 +3045,8 @@ 84018694288B66370050AC51 /* SwiftUIVisualEffects */, 84CE3DAB2904C14100FF068B /* InAppSettingsKit */, 84A0A8892A47D5D10038F329 /* QEMUKit */, + CE9B15372B11A4A7003A32DD /* SwiftConnect */, + CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */, ); productName = UTM; productReference = CE2D93BE24AD46670059923A /* UTM.app */; @@ -2627,6 +3078,8 @@ 848F71E5277A2466006A0240 /* SwiftTerm */, 84B36D2127B3265400C22685 /* CocoaSpice */, 84A0A8872A47D5C50038F329 /* QEMUKit */, + CE9B15352B11A491003A32DD /* SwiftConnect */, + CEDD11C02B7C74D7004DDAC6 /* SwiftPortmap */, ); productName = UTM; productReference = CE2D951C24AD48BE0059923A /* UTM.app */; @@ -2650,9 +3103,9 @@ productReference = CE9A352D26533A51005077CF /* JailbreakInterposer.framework */; productType = "com.apple.product-type.framework"; }; - CEA45E1F263519B5002FA97D /* iOS-TCI */ = { + CEA45E1F263519B5002FA97D /* iOS-SE */ = { isa = PBXNativeTarget; - buildConfigurationList = CEA45FB6263519B5002FA97D /* Build configuration list for PBXNativeTarget "iOS-TCI" */; + buildConfigurationList = CEA45FB6263519B5002FA97D /* Build configuration list for PBXNativeTarget "iOS-SE" */; buildPhases = ( CEA45E24263519B5002FA97D /* Sources */, CEA45F23263519B5002FA97D /* Frameworks */, @@ -2664,7 +3117,7 @@ ); dependencies = ( ); - name = "iOS-TCI"; + name = "iOS-SE"; packageProductDependencies = ( CEA45E20263519B5002FA97D /* Logging */, CEA45E22263519B5002FA97D /* IQKeyboardManagerSwift */, @@ -2674,6 +3127,8 @@ 84CF5DF2288E433F00D01721 /* SwiftUIVisualEffects */, 846D878529050B6B0095F10B /* InAppSettingsKit */, 84A0A88B2A47D5D70038F329 /* QEMUKit */, + CE9B15392B11A4AE003A32DD /* SwiftConnect */, + CE89CB0F2B8B1B6A006B2CC2 /* VisionKeyboardKit */, ); productName = UTM; productReference = CEA45FB9263519B5002FA97D /* UTM SE.app */; @@ -2698,6 +3153,36 @@ productReference = CEBDA1DA24D8BDDA0010B5EC /* QEMUHelper.xpc */; productType = "com.apple.product-type.xpc-service"; }; + CEF7F5812AEEDCC400E34952 /* iOS-Remote */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEF7F6D02AEEDCC400E34952 /* Build configuration list for PBXNativeTarget "iOS-Remote" */; + buildPhases = ( + CEF7F5962AEEDCC400E34952 /* Sources */, + CEF7F62D2AEEDCC400E34952 /* Frameworks */, + CEF7F6782AEEDCC400E34952 /* Resources */, + CEF7F6812AEEDCC400E34952 /* Patch Settings bundle */, + CEF7F6822AEEDCC400E34952 /* Embed Libraries */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "iOS-Remote"; + packageProductDependencies = ( + CEF7F5842AEEDCC400E34952 /* Logging */, + CEF7F5862AEEDCC400E34952 /* IQKeyboardManagerSwift */, + CEF7F5882AEEDCC400E34952 /* ZIPFoundation */, + CEF7F58E2AEEDCC400E34952 /* SwiftTerm */, + CEF7F5902AEEDCC400E34952 /* SwiftUIVisualEffects */, + CEF7F5922AEEDCC400E34952 /* InAppSettingsKit */, + CEF7F6D52AEEEF7D00E34952 /* CocoaSpiceNoUsb */, + CE9B153B2B11A4B4003A32DD /* SwiftConnect */, + CE89CB112B8B1B7A006B2CC2 /* VisionKeyboardKit */, + ); + productName = UTM; + productReference = CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */; + productType = "com.apple.product-type.application"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -2762,13 +3247,17 @@ 84CE3DAA2904C14100FF068B /* XCRemoteSwiftPackageReference "InAppSettingsKit" */, 84E3A8FE293DBC290024A740 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, 84A0A8862A47D5C50038F329 /* XCRemoteSwiftPackageReference "QEMUKit" */, + CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */, + CEDD11BF2B7C74D7004DDAC6 /* XCRemoteSwiftPackageReference "SwiftPortmap" */, + CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */, ); productRefGroup = CE550BCA225947990063E575 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( CE2D926824AD46670059923A /* iOS */, - CEA45E1F263519B5002FA97D /* iOS-TCI */, + CEA45E1F263519B5002FA97D /* iOS-SE */, + CEF7F5812AEEDCC400E34952 /* iOS-Remote */, CE2D951B24AD48BE0059923A /* macOS */, CEBDA1D924D8BDDA0010B5EC /* QEMUHelper */, 8401FD61269BE9C500265F0D /* QEMULauncher */, @@ -2849,6 +3338,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEF7F6782AEEDCC400E34952 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CEF7F6792AEEDCC400E34952 /* Icons in Resources */, + CEF7F67A2AEEDCC400E34952 /* InfoPlist.strings in Resources */, + CEF7F67B2AEEDCC400E34952 /* Localizable.strings in Resources */, + CEF7F67C2AEEDCC400E34952 /* qemu in Resources */, + CEF7F67D2AEEDCC400E34952 /* VMDisplayMetalViewInputAccessory.xib in Resources */, + CEF7F67E2AEEDCC400E34952 /* Localizable.stringsdict in Resources */, + CEF7F67F2AEEDCC400E34952 /* Settings.bundle in Resources */, + CEF7F6802AEEDCC400E34952 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -2870,7 +3374,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Found entry $i for $platform, removing entry\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\n fi\ndone\n"; + shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n remove=0\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Exclude $i due to Platform\"\n remove=1\n fi\n fi\n excludetargets=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\" 2> /dev/null | sed '1d;$d' | xargs)\n if [ ! -z \"$excludetargets\" ]; then\n found=0\n for target in $excludetargets; do\n if [ \"$target\" == \"$TARGET_NAME\" ]; then\n found=1\n fi\n done\n if [ $found -eq 1 ]; then\n echo \"Exclude $i due to ExcludeTargets\"\n remove=1\n else\n echo \"Found entry $i for ExcludeTargets\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\"\n fi\n fi\n if [ $remove -eq 1 ]; then\n echo \"Removing entry $i\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\ndone\n"; showEnvVarsInLog = 0; }; CE59A7B22ABCCB7C00E5FFBD /* Patch Settings bundle */ = { @@ -2891,7 +3395,28 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Found entry $i for $platform, removing entry\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\n fi\ndone\n"; + shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n remove=0\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Exclude $i due to Platform\"\n remove=1\n fi\n fi\n excludetargets=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\" 2> /dev/null | sed '1d;$d' | xargs)\n if [ ! -z \"$excludetargets\" ]; then\n found=0\n for target in $excludetargets; do\n if [ \"$target\" == \"$TARGET_NAME\" ]; then\n found=1\n fi\n done\n if [ $found -eq 1 ]; then\n echo \"Exclude $i due to ExcludeTargets\"\n remove=1\n else\n echo \"Found entry $i for ExcludeTargets\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\"\n fi\n fi\n if [ $remove -eq 1 ]; then\n echo \"Removing entry $i\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\ndone\n"; + showEnvVarsInLog = 0; + }; + CEF7F6812AEEDCC400E34952 /* Patch Settings bundle */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Platform/iOS/Settings.bundle", + ); + name = "Patch Settings bundle"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/Settings.bundle/Root.plist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n remove=0\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Exclude $i due to Platform\"\n remove=1\n fi\n fi\n excludetargets=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\" 2> /dev/null | sed '1d;$d' | xargs)\n if [ ! -z \"$excludetargets\" ]; then\n found=0\n for target in $excludetargets; do\n if [ \"$target\" == \"$TARGET_NAME\" ]; then\n found=1\n fi\n done\n if [ $found -eq 1 ]; then\n echo \"Exclude $i due to ExcludeTargets\"\n remove=1\n else\n echo \"Found entry $i for ExcludeTargets\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\"\n fi\n fi\n if [ $remove -eq 1 ]; then\n echo \"Removing entry $i\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\ndone\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -2956,6 +3481,7 @@ CEF0305126A2AFBF00667B63 /* Spinner.swift in Sources */, 843BF840284555E70029D60D /* UTMQemuConfigurationPortForward.swift in Sources */, CE611BE729F50CAD001817BC /* UTMReleaseHelper.swift in Sources */, + CEF01DB22B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */, CE2D958324AD4F990059923A /* VMConfigNetworkView.swift in Sources */, CE2D929C24AD46670059923A /* UTMLegacyViewState.m in Sources */, CE020BAB24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */, @@ -2983,6 +3509,7 @@ CED814E924C79F070042F0F1 /* VMConfigDriveCreateView.swift in Sources */, 842B9F8D28CC58B700031EE7 /* UTMPatches.swift in Sources */, CE19392626DCB094005CEC17 /* RAMSlider.swift in Sources */, + CE9B15412B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */, CE611BEB29F50D3E001817BC /* VMReleaseNotesView.swift in Sources */, CEE7E936287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */, 4B224B9D279D4D8100B63CFF /* InListButtonStyle.swift in Sources */, @@ -2997,6 +3524,7 @@ CE2D957524AD4F990059923A /* VMConfigInputView.swift in Sources */, CEF0305B26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */, 84C60FB72681A41B00B58C00 /* VMToolbarView.swift in Sources */, + CEF01DB72B674BF000725A0F /* UTMPipeInterface.swift in Sources */, CE2D92CB24AD46670059923A /* VMDisplayMetalViewController+Gamepad.m in Sources */, CEF0307426A2B40B00667B63 /* VMWizardHardwareView.swift in Sources */, 841E997528AA1191003C6CB6 /* UTMRegistry.swift in Sources */, @@ -3056,6 +3584,7 @@ 84C505AC28C588EC007CE8FF /* SizeTextField.swift in Sources */, 8471770627CC974F00D3A50B /* DefaultTextField.swift in Sources */, 84E6F6FD289319AE00080EEF /* VMToolbarDisplayMenuView.swift in Sources */, + CE9B15472B12A87E003A32DD /* GenerateKey.c in Sources */, CE8813D324CD230300532628 /* ActivityView.swift in Sources */, CEDF83F9258AE24E0030E4AC /* UTMPasteboard.swift in Sources */, 848D99B4286300160055C215 /* QEMUArgument.swift in Sources */, @@ -3076,6 +3605,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CEE06B272B2FC89400A811AE /* UTMServerView.swift in Sources */, CEB63A7724F4654400CAF323 /* Main.swift in Sources */, 84E3A91B2946D2590024A740 /* UTMMenuBarExtraScene.swift in Sources */, CEB63A7B24F469E300CAF323 /* UTMJailbreak.m in Sources */, @@ -3084,6 +3614,7 @@ 2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */, CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */, 85EC516627CC8D10004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */, + CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */, CE020BB724B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */, 845F170B289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift in Sources */, CE772AAD25C8B0F600E4E379 /* ContentView.swift in Sources */, @@ -3112,6 +3643,7 @@ 8432329628C2ED9000CFBC97 /* FileBrowseField.swift in Sources */, 848A98C2286A2257006F0550 /* UTMAppleConfigurationMacPlatform.swift in Sources */, 84B36D2B27B790BE00C22685 /* DestructiveButton.swift in Sources */, + CE9B154A2B12A87E003A32DD /* GenerateKey.c in Sources */, CE020BAC24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */, 848D99BA28630A780055C215 /* VMConfigSerialView.swift in Sources */, 8401FDA2269D3E2500265F0D /* VMConfigAppleNetworkingView.swift in Sources */, @@ -3179,6 +3711,7 @@ CE25125129C806AF000790AB /* UTMScriptingDeleteCommand.swift in Sources */, CE0B6CFB24AD568400FE012D /* UTMLegacyQemuConfiguration+Networking.m in Sources */, 84C584E5268F8C65000FCABF /* VMAppleSettingsView.swift in Sources */, + CE9B15442B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */, 84F746BB276FF70700A20C87 /* VMDisplayQemuDisplayController.swift in Sources */, CE772AB425C8B7B500E4E379 /* VMCommands.swift in Sources */, CE2D958424AD4F990059923A /* VMConfigNetworkView.swift in Sources */, @@ -3198,6 +3731,7 @@ CEC0A30A2A7490D200980857 /* VMConfigQEMUArgumentsView.swift in Sources */, 843232B928C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift in Sources */, 8471772A27CD3CAB00D3A50B /* DetailedSection.swift in Sources */, + CEF01DB52B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */, 8432329A28C3084A00CFBC97 /* GlobalFileImporter.swift in Sources */, CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */, 2C33B3AA2566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */, @@ -3208,6 +3742,7 @@ CEF0305D26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */, CEEC811B24E48EC700ACB0B3 /* SettingsView.swift in Sources */, 8443EFF42845641600B2E6E2 /* UTMQemuConfigurationDrive.swift in Sources */, + CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */, CE2D957024AD4F990059923A /* VMRemovableDrivesView.swift in Sources */, CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */, CE0B6CFE24AD56AE00FE012D /* UTMLogging.m in Sources */, @@ -3222,6 +3757,7 @@ 848F71EE277A2F47006A0240 /* UTMSerialPortDelegate.swift in Sources */, 848A98BA286A17A8006F0550 /* UTMAppleConfigurationNetwork.swift in Sources */, 84018699288B71BF0050AC51 /* BusyIndicator.swift in Sources */, + CEF01DB92B674BF000725A0F /* UTMPipeInterface.swift in Sources */, CEB54C852931E32F000D2AA9 /* UTMPatches.swift in Sources */, 84C2E8672AA429E800B17308 /* VMWizardContent.swift in Sources */, 848A98C0286A20E3006F0550 /* UTMAppleConfigurationBoot.swift in Sources */, @@ -3229,6 +3765,7 @@ CE2D958A24AD4F990059923A /* VMConfigSystemView.swift in Sources */, CEBBF1A724B5730F00C15049 /* UTMDataExtension.swift in Sources */, 84C584E3268F8AE7000FCABF /* VMQEMUSettingsView.swift in Sources */, + CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */, 845F1707289B5E2600944904 /* VMAppleSettingsAddDeviceMenuView.swift in Sources */, 843BF83E2845494C0029D60D /* UTMQemuConfigurationSerial.swift in Sources */, 841E997728AA1191003C6CB6 /* UTMRegistry.swift in Sources */, @@ -3245,6 +3782,7 @@ CE020BA424AEDC7C00B44AB6 /* UTMData.swift in Sources */, CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */, 848A98C8287206AE006F0550 /* VMConfigAppleVirtualizationView.swift in Sources */, + CE9B153F2B11A63E003A32DD /* UTMRemoteServer.swift in Sources */, 847BF9AC2A49C783000BD9AA /* VMData.swift in Sources */, CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */, CE2D958824AD4F990059923A /* VMConfigPortForwardForm.swift in Sources */, @@ -3282,6 +3820,7 @@ 841619AF28431952000034B2 /* UTMQemuConfigurationSystem.swift in Sources */, 8432329528C2ED9000CFBC97 /* FileBrowseField.swift in Sources */, 843BF82528441EAD0029D60D /* UTMQemuConfigurationDisplay.swift in Sources */, + CE9B15422B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */, CEA45E3F263519B5002FA97D /* UTMProcess.m in Sources */, CEA45E43263519B5002FA97D /* UTMLegacyQemuConfigurationPortForward.m in Sources */, 843BF841284555E70029D60D /* UTMQemuConfigurationPortForward.swift in Sources */, @@ -3300,6 +3839,7 @@ CEA45E5A263519B5002FA97D /* VMConfigSystemView.swift in Sources */, CEA45E5B263519B5002FA97D /* VMShareFileModifier.swift in Sources */, 8471770727CC974F00D3A50B /* DefaultTextField.swift in Sources */, + CE9B15482B12A87E003A32DD /* GenerateKey.c in Sources */, 8432329128C2CDAD00CFBC97 /* VMNavigationListView.swift in Sources */, CEA45E61263519B5002FA97D /* VMConfigNetworkView.swift in Sources */, 84018698288B71BF0050AC51 /* BusyIndicator.swift in Sources */, @@ -3354,6 +3894,7 @@ CEF0305226A2AFBF00667B63 /* Spinner.swift in Sources */, CEA45EBD263519B5002FA97D /* UTMQemuSystem.m in Sources */, CEA45EBE263519B5002FA97D /* NumberTextField.swift in Sources */, + CEF01DB32B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */, 843232B828C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift in Sources */, CEF0306526A2AFDF00667B63 /* VMWizardOSLinuxView.swift in Sources */, CEA45EC3263519B5002FA97D /* VMCommands.swift in Sources */, @@ -3372,12 +3913,12 @@ CE19392726DCB094005CEC17 /* RAMSlider.swift in Sources */, 84E6F6FE289319AE00080EEF /* VMToolbarDisplayMenuView.swift in Sources */, 841E58CC28937EE200137A20 /* UTMExternalSceneDelegate.swift in Sources */, + CEF01DB82B674BF000725A0F /* UTMPipeInterface.swift in Sources */, CEF0307226A2B04400667B63 /* VMWizardView.swift in Sources */, 83034C0826AB630F006B4BAF /* UTMPendingVMView.swift in Sources */, CEA45ED8263519B5002FA97D /* VMKeyboardButton.m in Sources */, CEF0307526A2B40B00667B63 /* VMWizardHardwareView.swift in Sources */, CE8011212AD4E9E8009001C2 /* UTMApp.swift in Sources */, - CEA45EDF263519B5002FA97D /* UTMJailbreak.m in Sources */, CE611BEC29F50D3E001817BC /* VMReleaseNotesView.swift in Sources */, CE611BE829F50CAD001817BC /* UTMReleaseHelper.swift in Sources */, CEA45EE8263519B5002FA97D /* VMDisplayMetalViewController+Keyboard.m in Sources */, @@ -3427,6 +3968,159 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEF7F5962AEEDCC400E34952 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CEF7F5972AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.h in Sources */, + CEF7F5982AEEDCC400E34952 /* VMSettingsAddDeviceMenuView.swift in Sources */, + CEF7F5992AEEDCC400E34952 /* VMRemovableDrivesView.swift in Sources */, + CEF7F59A2AEEDCC400E34952 /* UTMQemuConfigurationDrive.swift in Sources */, + CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */, + CEF7F59B2AEEDCC400E34952 /* UTMQemuConfigurationSharing.swift in Sources */, + CEF7F59C2AEEDCC400E34952 /* ContentView.swift in Sources */, + CEF7F59D2AEEDCC400E34952 /* VMData.swift in Sources */, + CEF7F59E2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+System.m in Sources */, + CEF7F59F2AEEDCC400E34952 /* UTMQemuConfigurationNetwork.swift in Sources */, + CE70E8D52B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift in Sources */, + CEF7F5A12AEEDCC400E34952 /* VMWizardDrivesView.swift in Sources */, + CEF7F5A22AEEDCC400E34952 /* VMWindowState.swift in Sources */, + CEF7F5A42AEEDCC400E34952 /* UTMLegacyQemuConfigurationPortForward.m in Sources */, + CEF7F5A52AEEDCC400E34952 /* VMWizardView.swift in Sources */, + CEF7F5A62AEEDCC400E34952 /* UTMPlaceholderVMView.swift in Sources */, + CEF7F5A72AEEDCC400E34952 /* BusyOverlay.swift in Sources */, + CE9B15432B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */, + CEF7F5A82AEEDCC400E34952 /* UTMConfiguration.swift in Sources */, + CEF7F5A92AEEDCC400E34952 /* UTMConfigurationInfo.swift in Sources */, + CEF7F5AA2AEEDCC400E34952 /* VMConfigDisplayView.swift in Sources */, + CEF7F5AB2AEEDCC400E34952 /* VMWizardOSWindowsView.swift in Sources */, + CEF7F5AC2AEEDCC400E34952 /* UTMDownloadTask.swift in Sources */, + CEF7F5AD2AEEDCC400E34952 /* UTMApp.swift in Sources */, + CEF7F5AE2AEEDCC400E34952 /* VMConfigAdvancedNetworkView.swift in Sources */, + CEF7F5AF2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Miscellaneous.m in Sources */, + CEF7F5B02AEEDCC400E34952 /* UTMRegistryEntry.swift in Sources */, + CEF7F5B12AEEDCC400E34952 /* UTMDataExtension.swift in Sources */, + CEF7F5B22AEEDCC400E34952 /* VMDisplayHostedView.swift in Sources */, + CEF7F5B32AEEDCC400E34952 /* QEMUArgumentBuilder.swift in Sources */, + CEF7F5B42AEEDCC400E34952 /* ImagePicker.swift in Sources */, + CEF7F5B52AEEDCC400E34952 /* VMConfigSystemView.swift in Sources */, + CEF7F5B62AEEDCC400E34952 /* FileBrowseField.swift in Sources */, + CEF7F5B72AEEDCC400E34952 /* VMShareFileModifier.swift in Sources */, + CEF7F5B82AEEDCC400E34952 /* UTMQemuConfigurationSerial.swift in Sources */, + CEF7F5B92AEEDCC400E34952 /* Spinner.swift in Sources */, + CE9B15492B12A87E003A32DD /* GenerateKey.c in Sources */, + CEF7F5BA2AEEDCC400E34952 /* UTMQemuConfigurationPortForward.swift in Sources */, + CEF7F5BB2AEEDCC400E34952 /* UTMReleaseHelper.swift in Sources */, + CEF7F5BC2AEEDCC400E34952 /* VMConfigNetworkView.swift in Sources */, + CEF7F5BD2AEEDCC400E34952 /* UTMLegacyViewState.m in Sources */, + CEF7F5BF2AEEDCC400E34952 /* VMWizardOSLinuxView.swift in Sources */, + CEF7F5C02AEEDCC400E34952 /* VMWizardSummaryView.swift in Sources */, + CEF7F5C12AEEDCC400E34952 /* VMConfigQEMUView.swift in Sources */, + CEF7F5C22AEEDCC400E34952 /* VMDisplayMetalViewController+Touch.m in Sources */, + CEF7F5C32AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Display.m in Sources */, + CEF7F5C42AEEDCC400E34952 /* BigButtonStyle.swift in Sources */, + CEF7F5C52AEEDCC400E34952 /* UTMVirtualMachine.swift in Sources */, + CEF7F5C62AEEDCC400E34952 /* UTMQemuConfigurationSystem.swift in Sources */, + CEF7F5C72AEEDCC400E34952 /* VMWizardSharingView.swift in Sources */, + CEF7F5C82AEEDCC400E34952 /* VMConfigInfoView.swift in Sources */, + CEF7F5C92AEEDCC400E34952 /* MenuLabel.swift in Sources */, + CEF7F5CA2AEEDCC400E34952 /* VMKeyboardView.m in Sources */, + CEF7F5CB2AEEDCC400E34952 /* VMWizardOSView.swift in Sources */, + CEF7F5CC2AEEDCC400E34952 /* DestructiveButton.swift in Sources */, + CEF7F5CD2AEEDCC400E34952 /* UTMConfigurationTerminal.swift in Sources */, + CEF7F5CE2AEEDCC400E34952 /* VMWindowView.swift in Sources */, + CEF7F5CF2AEEDCC400E34952 /* UTMPendingVMView.swift in Sources */, + CEE8B4C32B71E2BA0035AE86 /* UTMLoggingSwift.swift in Sources */, + CEF7F5D02AEEDCC400E34952 /* UTMSpiceIO.m in Sources */, + CEF7F5D12AEEDCC400E34952 /* UTMUnavailableVMView.swift in Sources */, + CEF7F5D22AEEDCC400E34952 /* VMDrivesSettingsView.swift in Sources */, + CEF7F5D32AEEDCC400E34952 /* UTMConfigurationDrive.swift in Sources */, + CEF7F5D42AEEDCC400E34952 /* VMConfigDriveCreateView.swift in Sources */, + CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */, + CEF7F5D52AEEDCC400E34952 /* UTMPatches.swift in Sources */, + CEF7F5D62AEEDCC400E34952 /* RAMSlider.swift in Sources */, + CEF7F5D72AEEDCC400E34952 /* VMReleaseNotesView.swift in Sources */, + CEF7F5D82AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Constants.m in Sources */, + CEF7F5D92AEEDCC400E34952 /* InListButtonStyle.swift in Sources */, + CEF7F5DA2AEEDCC400E34952 /* VMContextMenuModifier.swift in Sources */, + CEF7F5DB2AEEDCC400E34952 /* VMDisplayMetalViewController+Pencil.m in Sources */, + CEF7F5DC2AEEDCC400E34952 /* VMDisplayTerminalViewController.swift in Sources */, + CEF7F5DD2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Drives.m in Sources */, + CEF7F5DE2AEEDCC400E34952 /* UTMPendingVirtualMachine.swift in Sources */, + CEF7F5DF2AEEDCC400E34952 /* BusyIndicator.swift in Sources */, + CEF7F5E02AEEDCC400E34952 /* VMSessionState.swift in Sources */, + CEF7F5E12AEEDCC400E34952 /* VMConfigSharingView.swift in Sources */, + CEF7F5E22AEEDCC400E34952 /* VMConfigInputView.swift in Sources */, + CEF7F5E32AEEDCC400E34952 /* VMWizardOSOtherView.swift in Sources */, + CEF7F5E42AEEDCC400E34952 /* VMToolbarView.swift in Sources */, + CEF7F5E52AEEDCC400E34952 /* VMDisplayMetalViewController+Gamepad.m in Sources */, + CEF7F5E62AEEDCC400E34952 /* VMWizardHardwareView.swift in Sources */, + CEF7F5E72AEEDCC400E34952 /* UTMRegistry.swift in Sources */, + CEF7F5E82AEEDCC400E34952 /* VMDisplayViewControllerDelegate.swift in Sources */, + CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */, + CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */, + CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */, + CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */, + CEF7F5F02AEEDCC400E34952 /* NumberTextField.swift in Sources */, + CEF7F5F12AEEDCC400E34952 /* VMToolbarOrnamentModifier.swift in Sources */, + CEF01DB42B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */, + CEF7F5F22AEEDCC400E34952 /* VMCommands.swift in Sources */, + CEF7F5F32AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Networking.m in Sources */, + CEF7F5F42AEEDCC400E34952 /* VMConfirmActionModifier.swift in Sources */, + CEF7F5F52AEEDCC400E34952 /* QEMUConstant.swift in Sources */, + CEF7F5F62AEEDCC400E34952 /* VMConfigPortForwardForm.swift in Sources */, + CEF7F5F82AEEDCC400E34952 /* DetailedSection.swift in Sources */, + CEF7F5F92AEEDCC400E34952 /* VMToolbarDriveMenuView.swift in Sources */, + CE08334B2B784FD400522C03 /* RemoteContentView.swift in Sources */, + CEF7F5FA2AEEDCC400E34952 /* VMSettingsView.swift in Sources */, + CEF7F5FB2AEEDCC400E34952 /* VMDisplayViewController.swift in Sources */, + CEF7F5FC2AEEDCC400E34952 /* VMWizardStartView.swift in Sources */, + CEF7F5FD2AEEDCC400E34952 /* QEMUConstantGenerated.swift in Sources */, + CEF7F5FE2AEEDCC400E34952 /* VMKeyboardButton.m in Sources */, + CEF7F5FF2AEEDCC400E34952 /* UTMDownloadVMTask.swift in Sources */, + CEF7F6002AEEDCC400E34952 /* GlobalFileImporter.swift in Sources */, + CEF7F6022AEEDCC400E34952 /* VMWizardContent.swift in Sources */, + CEF7F6032AEEDCC400E34952 /* UTMExternalSceneDelegate.swift in Sources */, + CEF7F6052AEEDCC400E34952 /* UTMQemuConfigurationQEMU.swift in Sources */, + CEF7F6062AEEDCC400E34952 /* UTMQemuConfigurationDisplay.swift in Sources */, + CEE8B4C22B71E0FB0035AE86 /* UTMLogging.m in Sources */, + CEF7F6072AEEDCC400E34952 /* UTMApp.swift in Sources */, + CEF7F6082AEEDCC400E34952 /* VMConfigDisplayConsoleView.swift in Sources */, + CEF7F60A2AEEDCC400E34952 /* VMConfigSerialView.swift in Sources */, + CE38EC692B5DB3AE008B324B /* UTMRemoteClient.swift in Sources */, + CEF7F60B2AEEDCC400E34952 /* VMWizardState.swift in Sources */, + CEF7F60C2AEEDCC400E34952 /* UTMQemuConfigurationInput.swift in Sources */, + CEF7F60D2AEEDCC400E34952 /* VMDisplayMetalViewController+Keyboard.m in Sources */, + CEF7F60E2AEEDCC400E34952 /* UTMExtensions.swift in Sources */, + CEF7F60F2AEEDCC400E34952 /* UTMData.swift in Sources */, + CEF7F6112AEEDCC400E34952 /* VMConfigSoundView.swift in Sources */, + CEF7F6122AEEDCC400E34952 /* UTMLegacyQemuConfiguration.m in Sources */, + CEF7F6142AEEDCC400E34952 /* VMDetailsView.swift in Sources */, + CEF7F6152AEEDCC400E34952 /* VMDisplayMetalViewController.m in Sources */, + CEF7F6162AEEDCC400E34952 /* UTMQemuConfiguration+Arguments.swift in Sources */, + CEF7F6172AEEDCC400E34952 /* Main.swift in Sources */, + CEF7F6192AEEDCC400E34952 /* VMCardView.swift in Sources */, + CEF7F61A2AEEDCC400E34952 /* VMNavigationListView.swift in Sources */, + CEF7F61B2AEEDCC400E34952 /* UTMSingleWindowView.swift in Sources */, + CEF7F61C2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Sharing.m in Sources */, + CEF7F61D2AEEDCC400E34952 /* SizeTextField.swift in Sources */, + CEF7F61E2AEEDCC400E34952 /* DefaultTextField.swift in Sources */, + CEF7F61F2AEEDCC400E34952 /* VMToolbarDisplayMenuView.swift in Sources */, + CEF7F6202AEEDCC400E34952 /* ActivityView.swift in Sources */, + CEF7F6212AEEDCC400E34952 /* UTMPasteboard.swift in Sources */, + CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */, + CEF7F6222AEEDCC400E34952 /* QEMUArgument.swift in Sources */, + CEF7F6232AEEDCC400E34952 /* VMPlaceholderView.swift in Sources */, + CEF7F6242AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.m in Sources */, + CEF7F6252AEEDCC400E34952 /* VMDisplayViewController.m in Sources */, + CEF7F6282AEEDCC400E34952 /* UTMQemuConfigurationSound.swift in Sources */, + CEF7F6292AEEDCC400E34952 /* VMScroll.m in Sources */, + CEF7F62A2AEEDCC400E34952 /* VMConfigNetworkPortForwardView.swift in Sources */, + CEF7F62B2AEEDCC400E34952 /* UTMDownloadSupportToolsTask.swift in Sources */, + CEF7F62C2AEEDCC400E34952 /* UTMQemuConfiguration.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -3720,6 +4414,12 @@ CODE_SIGN_IDENTITY = "$(CODE_SIGN_IDENTITY_IOS:default=Apple Development)"; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "WITH_JIT=1", + "WITH_SOLO_VM=1", + "WITH_USB=1", + "$(inherited)", + ); INFOPLIST_FILE = Platform/iOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3731,6 +4431,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_JIT WITH_SOLO_VM WITH_USB $(inherited)"; SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -3748,6 +4449,12 @@ CODE_SIGN_IDENTITY = "$(CODE_SIGN_IDENTITY_IOS:default=Apple Development)"; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "WITH_JIT=1", + "WITH_SOLO_VM=1", + "WITH_USB=1", + "$(inherited)", + ); INFOPLIST_FILE = Platform/iOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3759,6 +4466,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_JIT WITH_SOLO_VM WITH_USB $(inherited)"; SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; @@ -3777,6 +4485,12 @@ CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "WITH_JIT=1", + "WITH_SERVER=1", + "WITH_USB=1", + "$(inherited)", + ); INFOPLIST_FILE = Platform/macOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3786,7 +4500,7 @@ PRODUCT_NAME = "$(PROJECT_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(PROVISIONING_PROFILE_SPECIFIER_MAC:default=)"; SUPPORTED_PLATFORMS = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_JIT WITH_SERVER WITH_USB $(inherited)"; SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -3805,6 +4519,12 @@ CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "WITH_JIT=1", + "WITH_SERVER=1", + "WITH_USB=1", + "$(inherited)", + ); INFOPLIST_FILE = Platform/macOS/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3814,6 +4534,7 @@ PRODUCT_NAME = "$(PROJECT_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(PROVISIONING_PROFILE_SPECIFIER_MAC:default=)"; SUPPORTED_PLATFORMS = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_JIT WITH_SERVER WITH_USB $(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -3902,6 +4623,7 @@ QUOTED_SYSROOT_DIR = "\"$(SYSROOT_DIR)\""; SDKROOT = auto; SUPPORTED_PLATFORMS = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; SYSROOT_DIR = "sysroot-$(PLATFORM_DISPLAY_NAME:identifier)$(PLATFORM_SUFFIX)-$(ARCHS:identifier)"; }; name = Debug; @@ -3980,6 +4702,7 @@ QUOTED_SYSROOT_DIR = "\"$(SYSROOT_DIR)\""; SDKROOT = auto; SUPPORTED_PLATFORMS = ""; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; SYSROOT_DIR = "sysroot-$(PLATFORM_DISPLAY_NAME:identifier)$(PLATFORM_SUFFIX)-$(ARCHS:identifier)"; VALIDATE_PRODUCT = YES; @@ -4050,6 +4773,7 @@ ENABLE_PREVIEWS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "WITH_QEMU_TCI=1", + "WITH_SOLO_VM=1", "$(inherited)", ); INFOPLIST_FILE = Platform/iOS/Info.plist; @@ -4065,7 +4789,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = WITH_QEMU_TCI; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_QEMU_TCI WITH_SOLO_VM $(inherited)"; SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -4085,6 +4809,7 @@ ENABLE_PREVIEWS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "WITH_QEMU_TCI=1", + "WITH_SOLO_VM=1", "$(inherited)", ); INFOPLIST_FILE = Platform/iOS/Info.plist; @@ -4100,7 +4825,7 @@ SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = WITH_QEMU_TCI; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_QEMU_TCI WITH_SOLO_VM $(inherited)"; SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; @@ -4149,6 +4874,73 @@ }; name = Release; }; + CEF7F6D12AEEDCC400E34952 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "$(CODE_SIGN_IDENTITY_IOS:default=Apple Development)"; + ENABLE_BITCODE = NO; + ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "WITH_REMOTE=1", + "$(inherited)", + ); + INFOPLIST_FILE = "Platform/iOS/Info-Remote.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PLATFORM_SUFFIX = "-TCI"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=com.utmapp).UTM-Remote"; + PRODUCT_MODULE_NAME = UTM; + PRODUCT_NAME = "UTM Remote"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_REMOTE $(inherited)"; + SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Debug; + }; + CEF7F6D22AEEDCC400E34952 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "$(CODE_SIGN_IDENTITY_IOS:default=Apple Development)"; + ENABLE_BITCODE = NO; + ENABLE_PREVIEWS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "WITH_REMOTE=1", + "$(inherited)", + ); + INFOPLIST_FILE = "Platform/iOS/Info-Remote.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PLATFORM_SUFFIX = "-TCI"; + PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=com.utmapp).UTM-Remote"; + PRODUCT_MODULE_NAME = UTM; + PRODUCT_NAME = "UTM Remote"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_REMOTE $(inherited)"; + SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2,7"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -4206,7 +4998,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - CEA45FB6263519B5002FA97D /* Build configuration list for PBXNativeTarget "iOS-TCI" */ = { + CEA45FB6263519B5002FA97D /* Build configuration list for PBXNativeTarget "iOS-SE" */ = { isa = XCConfigurationList; buildConfigurations = ( CEA45FB7263519B5002FA97D /* Debug */, @@ -4224,6 +5016,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CEF7F6D02AEEDCC400E34952 /* Build configuration list for PBXNativeTarget "iOS-Remote" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CEF7F6D12AEEDCC400E34952 /* Debug */, + CEF7F6D22AEEDCC400E34952 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -4245,9 +5046,9 @@ }; 848F71E4277A2466006A0240 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/osy/SwiftTerm.git"; + repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git"; requirement = { - branch = visionos; + branch = main; kind = branch; }; }; @@ -4299,6 +5100,14 @@ minimumVersion = 1.5.3; }; }; + CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/utmapp/VisionKeyboardKit.git"; + requirement = { + branch = main; + kind = branch; + }; + }; CE93759724BB821F0074066F /* XCRemoteSwiftPackageReference "IQKeyboardManager" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/hackiftekhar/IQKeyboardManager.git"; @@ -4307,6 +5116,14 @@ version = 6.5.6; }; }; + CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/utmapp/SwiftConnect"; + requirement = { + branch = main; + kind = branch; + }; + }; CEA45E21263519B5002FA97D /* XCRemoteSwiftPackageReference "swift-log" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-log"; @@ -4323,6 +5140,62 @@ minimumVersion = 6.5.6; }; }; + CEDD11BF2B7C74D7004DDAC6 /* XCRemoteSwiftPackageReference "SwiftPortmap" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/osy/SwiftPortmap.git"; + requirement = { + branch = main; + kind = branch; + }; + }; + CEF7F5852AEEDCC400E34952 /* XCRemoteSwiftPackageReference "swift-log" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-log"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.5.3; + }; + }; + CEF7F5872AEEDCC400E34952 /* XCRemoteSwiftPackageReference "IQKeyboardManager" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/hackiftekhar/IQKeyboardManager.git"; + requirement = { + kind = exactVersion; + version = 6.5.6; + }; + }; + CEF7F5892AEEDCC400E34952 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/weichsel/ZIPFoundation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.17; + }; + }; + CEF7F58F2AEEDCC400E34952 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git"; + requirement = { + branch = main; + kind = branch; + }; + }; + CEF7F5912AEEDCC400E34952 /* XCRemoteSwiftPackageReference "swiftui-visual-effects" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/lucasbrown/swiftui-visual-effects.git"; + requirement = { + kind = exactVersion; + version = 1.0.3; + }; + }; + CEF7F5932AEEDCC400E34952 /* XCRemoteSwiftPackageReference "InAppSettingsKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/futuretap/InAppSettingsKit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 3.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -4440,11 +5313,46 @@ package = CE020BA524AEDEF000B44AB6 /* XCRemoteSwiftPackageReference "swift-log" */; productName = Logging; }; + CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */ = { + isa = XCSwiftPackageProductDependency; + package = CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */; + productName = VisionKeyboardKit; + }; + CE89CB0F2B8B1B6A006B2CC2 /* VisionKeyboardKit */ = { + isa = XCSwiftPackageProductDependency; + package = CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */; + productName = VisionKeyboardKit; + }; + CE89CB112B8B1B7A006B2CC2 /* VisionKeyboardKit */ = { + isa = XCSwiftPackageProductDependency; + package = CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */; + productName = VisionKeyboardKit; + }; CE93759824BB821F0074066F /* IQKeyboardManagerSwift */ = { isa = XCSwiftPackageProductDependency; package = CE93759724BB821F0074066F /* XCRemoteSwiftPackageReference "IQKeyboardManager" */; productName = IQKeyboardManagerSwift; }; + CE9B15352B11A491003A32DD /* SwiftConnect */ = { + isa = XCSwiftPackageProductDependency; + package = CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */; + productName = SwiftConnect; + }; + CE9B15372B11A4A7003A32DD /* SwiftConnect */ = { + isa = XCSwiftPackageProductDependency; + package = CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */; + productName = SwiftConnect; + }; + CE9B15392B11A4AE003A32DD /* SwiftConnect */ = { + isa = XCSwiftPackageProductDependency; + package = CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */; + productName = SwiftConnect; + }; + CE9B153B2B11A4B4003A32DD /* SwiftConnect */ = { + isa = XCSwiftPackageProductDependency; + package = CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */; + productName = SwiftConnect; + }; CEA45E20263519B5002FA97D /* Logging */ = { isa = XCSwiftPackageProductDependency; package = CEA45E21263519B5002FA97D /* XCRemoteSwiftPackageReference "swift-log" */; @@ -4455,6 +5363,46 @@ package = CEA45E23263519B5002FA97D /* XCRemoteSwiftPackageReference "IQKeyboardManager" */; productName = IQKeyboardManagerSwift; }; + CEDD11C02B7C74D7004DDAC6 /* SwiftPortmap */ = { + isa = XCSwiftPackageProductDependency; + package = CEDD11BF2B7C74D7004DDAC6 /* XCRemoteSwiftPackageReference "SwiftPortmap" */; + productName = SwiftPortmap; + }; + CEF7F5842AEEDCC400E34952 /* Logging */ = { + isa = XCSwiftPackageProductDependency; + package = CEF7F5852AEEDCC400E34952 /* XCRemoteSwiftPackageReference "swift-log" */; + productName = Logging; + }; + CEF7F5862AEEDCC400E34952 /* IQKeyboardManagerSwift */ = { + isa = XCSwiftPackageProductDependency; + package = CEF7F5872AEEDCC400E34952 /* XCRemoteSwiftPackageReference "IQKeyboardManager" */; + productName = IQKeyboardManagerSwift; + }; + CEF7F5882AEEDCC400E34952 /* ZIPFoundation */ = { + isa = XCSwiftPackageProductDependency; + package = CEF7F5892AEEDCC400E34952 /* XCRemoteSwiftPackageReference "ZIPFoundation" */; + productName = ZIPFoundation; + }; + CEF7F58E2AEEDCC400E34952 /* SwiftTerm */ = { + isa = XCSwiftPackageProductDependency; + package = CEF7F58F2AEEDCC400E34952 /* XCRemoteSwiftPackageReference "SwiftTerm" */; + productName = SwiftTerm; + }; + CEF7F5902AEEDCC400E34952 /* SwiftUIVisualEffects */ = { + isa = XCSwiftPackageProductDependency; + package = CEF7F5912AEEDCC400E34952 /* XCRemoteSwiftPackageReference "swiftui-visual-effects" */; + productName = SwiftUIVisualEffects; + }; + CEF7F5922AEEDCC400E34952 /* InAppSettingsKit */ = { + isa = XCSwiftPackageProductDependency; + package = CEF7F5932AEEDCC400E34952 /* XCRemoteSwiftPackageReference "InAppSettingsKit" */; + productName = InAppSettingsKit; + }; + CEF7F6D52AEEEF7D00E34952 /* CocoaSpiceNoUsb */ = { + isa = XCSwiftPackageProductDependency; + package = 84B36D1C27B3261E00C22685 /* XCRemoteSwiftPackageReference "CocoaSpice" */; + productName = CocoaSpiceNoUsb; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = CE550BC1225947990063E575 /* Project object */; diff --git a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 637d8647..3d420c8b 100644 --- a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,7 +15,16 @@ "location" : "https://github.com/utmapp/CocoaSpice.git", "state" : { "branch" : "visionos", - "revision" : "4529c9686259e8d1e94d6253ad2e3a563fd1498d" + "revision" : "9fd682e0f78c884036609d4a19db2cfb3ed50c33" + } + }, + { + "identity" : "cod", + "kind" : "remoteSourceControl", + "location" : "https://github.com/saagarjha/Cod.git", + "state" : { + "branch" : "main", + "revision" : "c359a08accfb49662a17cdfc5e333c7b4e5c2c56" } }, { @@ -63,13 +72,31 @@ "version" : "1.5.3" } }, + { + "identity" : "swiftconnect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/utmapp/SwiftConnect", + "state" : { + "branch" : "main", + "revision" : "af855e47ca222da163cc7f4f185230f36ba8694a" + } + }, + { + "identity" : "swiftportmap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/osy/SwiftPortmap.git", + "state" : { + "branch" : "main", + "revision" : "72782141ab6f6f6db58bd16bac96d4e7ce901e9a" + } + }, { "identity" : "swiftterm", "kind" : "remoteSourceControl", - "location" : "https://github.com/osy/SwiftTerm.git", + "location" : "https://github.com/migueldeicaza/SwiftTerm.git", "state" : { - "branch" : "visionos", - "revision" : "8b0900a4c516eb8c87813f11e797f349e7fca014" + "branch" : "main", + "revision" : "ea0f681b25c8385b4a5a48d435e61d11392216e0" } }, { @@ -81,6 +108,15 @@ "version" : "1.0.3" } }, + { + "identity" : "visionkeyboardkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/utmapp/VisionKeyboardKit.git", + "state" : { + "branch" : "main", + "revision" : "0804e4d64267acc8d08fb23160f5b6ac6134414f" + } + }, { "identity" : "zipfoundation", "kind" : "remoteSourceControl", diff --git a/scripts/build_dependencies.sh b/scripts/build_dependencies.sh index 75b8d66b..5bb3e059 100755 --- a/scripts/build_dependencies.sh +++ b/scripts/build_dependencies.sh @@ -409,6 +409,8 @@ build_angle () { pwd="$(pwd)" cd "$BUILD_DIR/WebKit.git/Source/ThirdParty/ANGLE" xcodebuild archive -archivePath "ANGLE" -scheme "ANGLE" -sdk $SDK -arch $ARCH -configuration Release WEBCORE_LIBRARY_DIR="/usr/local/lib" IPHONEOS_DEPLOYMENT_TARGET="14.0" MACOSX_DEPLOYMENT_TARGET="11.0" XROS_DEPLOYMENT_TARGET="1.0" + # strip broken entitlements from signature + find "ANGLE.xcarchive/Products/usr/local/lib/" -name '*.dylib' -exec codesign -fs - \{\} \; rsync -a "ANGLE.xcarchive/Products/usr/local/lib/" "$PREFIX/lib" rsync -a "include/" "$PREFIX/include" cd "$pwd" diff --git a/scripts/build_utm.sh b/scripts/build_utm.sh index 191e77a7..6ea37890 100755 --- a/scripts/build_utm.sh +++ b/scripts/build_utm.sh @@ -7,11 +7,12 @@ command -v realpath >/dev/null 2>&1 || realpath() { BASEDIR="$(dirname "$(realpath $0)")" usage () { - echo "Usage: $(basename $0) [-t teamid] [-p platform] [-a architecture] [-t targetversion] [-o output]" + echo "Usage: $(basename $0) [-t teamid] [-p platform] [-s scheme] [-a architecture] [-t targetversion] [-o output]" echo "" echo " -t teamid Team Identifier for app groups. Optional for iOS. Required for macOS." - echo " -p platform Target platform. Default ios. [ios|ios_simulator|ios-tci|ios_simulator-tci|macos|visionos|visionos_simulator]" - echo " -a architecture Target architecture. Default arm64. [armv7|armv7s|arm64|i386|x86_64]" + echo " -k sdk Target SDK. Default iphoneos. [iphoneos|iphonesimulator|xros|xrsimulator|macosx]" + echo " -s scheme Target scheme. Default iOS/macOS depending on platform. [iOS|iOS-TCI|iOS-Remote|macOS]" + echo " -a architecture Target architecture. Default arm64. [arm64|x86_64]" echo " -o output Output archive path. Default is current directory." echo "" exit 1 @@ -20,9 +21,8 @@ usage () { PRODUCT_BUNDLE_PREFIX="com.utmapp" TEAM_IDENTIFIER= ARCH=arm64 -PLATFORM=ios OUTPUT=$PWD -SDK= +SDK=iphoneos SCHEME= while [ "x$1" != "x" ]; do @@ -35,8 +35,12 @@ while [ "x$1" != "x" ]; do ARCH=$2 shift ;; - -p ) - PLATFORM=$2 + -k ) + SDK=$2 + shift + ;; + -s ) + SCHEME=$2 shift ;; -o ) @@ -50,39 +54,14 @@ while [ "x$1" != "x" ]; do shift done -case $PLATFORM in -*-tci ) - SCHEME="iOS-TCI" - ;; -ios* | visionos* ) - SCHEME="iOS" - ;; +case $SDK in macos ) SCHEME="macOS" ;; * ) - usage - ;; -esac - -case $PLATFORM in -visionos_simulator* ) - SDK=xrsimulator - ;; -visionos* ) - SDK=xros - ;; -ios_simulator* ) - SDK=iphonesimulator - ;; -ios* ) - SDK=iphoneos - ;; -macos ) - SDK=macosx - ;; -* ) - usage + if [ -z "$SCHEME" ]; then + SCHEME="iOS" + fi ;; esac @@ -94,8 +73,7 @@ fi xcodebuild archive -archivePath "$OUTPUT" -scheme "$SCHEME" -sdk "$SDK" $ARCH_ARGS -configuration Release CODE_SIGNING_ALLOWED=NO $TEAM_IDENTIFIER_PREFIX BUILT_PATH=$(find $OUTPUT.xcarchive -name '*.app' -type d | head -1) # Only retain the target architecture to address < iOS 15 crash & save disk space -case $PLATFORM in -ios | ios-tci ) +if [ "$SDK" == "iphoneos" ]; then find "$BUILT_PATH" -type f -path '*/Frameworks/*.dylib' | while read FILE; do if [[ $(lipo -info "$FILE") =~ "Architectures in the fat file" ]]; then lipo -thin $ARCH "$FILE" -output "$FILE" @@ -107,10 +85,9 @@ ios | ios-tci ) lipo -thin $ARCH "$FILE" -output "$FILE" fi done - ;; -esac +fi find "$BUILT_PATH" -type d -path '*/Frameworks/*.framework' -exec codesign --force --sign - --timestamp=none \{\} \; -if [ "$PLATFORM" == "macos" ]; then +if [ "$SDK" == "macosx" ]; then # always build with vm entitlements, package_mac.sh can strip it later # this way we can import into Xcode and re-sign from there UTM_ENTITLEMENTS="/tmp/utm.$$.entitlements" diff --git a/scripts/package.sh b/scripts/package.sh index 9c48bbe4..73b24931 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -12,7 +12,8 @@ usage() { echo " MODE is one of:" echo " deb (Cydia DEB)" echo " ipa (unsigned IPA of full build with all entitlements)" - echo " ipa-se (unsigned IPA of TCI build)" + echo " ipa-se (unsigned IPA of SE build)" + echo " ipa-remote (unsigned IPA of Remote build)" echo " ipa-hv (unsigned IPA of full build without JIT entitlement)" echo " ipa-signed (developer signed IPA with valid PROFILE_NAME and TEAM_ID)" echo " inputXcarchive is path to UTM.xcarchive" @@ -42,6 +43,11 @@ ipa-se ) BUNDLE_ID="com.utmapp.UTM-SE" INPUT_APP="$INPUT/Products/Applications/UTM SE.app" ;; +ipa-remote ) + NAME="UTM Remote" + BUNDLE_ID="com.utmapp.UTM-Remote" + INPUT_APP="$INPUT/Products/Applications/UTM Remote.app" + ;; * ) usage ;; @@ -298,7 +304,7 @@ EOL create_fake_ipa "$NAME" "$BUNDLE_ID" "$INPUT" "$OUTPUT" "$FAKEENT" rm "$FAKEENT" ;; -ipa-se ) +ipa-se | ipa-remote ) FAKEENT="/tmp/fakeent.$$.plist" cat >"$FAKEENT" <<EOL <?xml version="1.0" encoding="UTF-8"?>