From ef970986315fbc5ca6236efb6a07d1620adb5497 Mon Sep 17 00:00:00 2001 From: Vincenzo Garambone Date: Tue, 27 Feb 2024 18:56:12 +0100 Subject: [PATCH] Merge branch 'main' into show-main-window-macos --- .github/workflows/build.yml | 54 +- Build.xcconfig | 4 +- Configuration/QEMUConstant.swift | 20 +- Configuration/UTMConfiguration.swift | 2 +- Configuration/UTMConfigurationDrive.swift | 7 +- .../UTMQemuConfiguration+Arguments.swift | 121 +- Configuration/UTMQemuConfigurationQEMU.swift | 9 + Platform/Main.swift | 2 +- Platform/Shared/BigButtonStyle.swift | 10 +- Platform/Shared/ContentView.swift | 19 +- Platform/Shared/MacDeviceLabel.swift | 111 + Platform/Shared/NumberTextField.swift | 11 +- Platform/Shared/UTMUnavailableVMView.swift | 28 +- Platform/Shared/VMCommands.swift | 2 + Platform/Shared/VMConfigInputView.swift | 2 +- Platform/Shared/VMConfigSystemView.swift | 4 +- Platform/Shared/VMContextMenuModifier.swift | 8 +- Platform/Shared/VMDetailsView.swift | 27 +- Platform/Shared/VMNavigationListView.swift | 10 +- Platform/Shared/VMPlaceholderView.swift | 91 +- Platform/Shared/VMRemovableDrivesView.swift | 8 +- Platform/Shared/VMToolbarModifier.swift | 4 + Platform/Shared/VMWizardStartView.swift | 4 +- Platform/UTMData.swift | 385 +++- Platform/UTMDownloadSupportToolsTask.swift | 6 +- Platform/UTMReleaseHelper.swift | 4 + Platform/VMData.swift | 135 +- .../VMDisplayMetalViewController+Pointer.m | 6 + .../VMDisplayMetalViewController+Touch.m | 6 +- .../Display/VMDisplayMetalViewController.h | 4 +- .../Display/VMDisplayMetalViewController.m | 125 +- .../iOS/Display/VMDisplayViewController.swift | 31 +- Platform/iOS/Display/VMKeyboardView.m | 14 +- .../VMDisplayMetalViewInputAccessory.strings | 2 +- Platform/iOS/Info-Remote.plist | 79 + Platform/iOS/RemoteContentView.swift | 32 + Platform/iOS/Settings.bundle/Root.plist | 40 + .../iOS/Settings.bundle/ja.lproj/Root.strings | 3 + .../Settings.bundle/zh-HK.lproj/Root.strings | 8 +- .../zh-Hans.lproj/Root.strings | 20 +- Platform/iOS/UTMDataExtension.swift | 13 +- Platform/iOS/UTMRemoteConnectView.swift | 300 +++ Platform/iOS/UTMSettingsView.swift | 10 +- Platform/iOS/UTMSingleWindowView.swift | 10 +- Platform/iOS/VMDisplayHostedView.swift | 24 +- Platform/iOS/VMSessionState.swift | 71 +- Platform/iOS/VMToolbarDriveMenuView.swift | 7 + Platform/iOS/VMToolbarView.swift | 10 +- Platform/iOS/VMWindowState.swift | 6 +- Platform/iOS/VMWindowView.swift | 44 +- .../iOS/en.lproj/Info-RemotePlist.strings | 1 + Platform/iOS/it.lproj/InfoPlist.strings | 20 + .../iOS/ja.lproj/Info-RemotePlist.strings | 9 + Platform/iOS/zh-HK.lproj/InfoPlist.strings | 8 +- Platform/iOS/zh-Hans.lproj/InfoPlist.strings | 4 +- Platform/it.lproj/Localizable.strings | 1855 +++++++++++++++++ Platform/it.lproj/Localizable.stringsdict | 22 + Platform/ja.lproj/Localizable.strings | 141 +- ...DisplayAppleTerminalWindowController.swift | 2 +- .../VMDisplayAppleWindowController.swift | 4 +- .../VMDisplayQemuMetalWindowController.swift | 2 +- .../Display/VMDisplayWindowController.swift | 8 +- .../Display/it.lproj/VMDisplayWindow.strings | 93 + .../zh-HK.lproj/VMDisplayWindow.strings | 12 +- Platform/macOS/SettingsView.swift | 63 + Platform/macOS/UTMApp.swift | 3 + Platform/macOS/UTMDataExtension.swift | 33 +- Platform/macOS/UTMServerView.swift | 173 ++ Platform/macOS/VMHeadlessSessionState.swift | 12 +- Platform/macOS/VMRemoteSessionState.swift | 35 + Platform/macOS/it.lproj/InfoPlist.strings | 8 + Platform/macOS/macOS-unsigned.entitlements | 6 + Platform/macOS/macOS.entitlements | 2 + Platform/pl.lproj/Localizable.strings | 12 +- Platform/visionOS/UTMApp.swift | 26 +- .../visionOS/VMToolbarOrnamentModifier.swift | 72 +- Platform/zh-HK.lproj/Localizable.strings | 957 ++++++++- Platform/zh-Hans.lproj/Localizable.strings | 895 +++++++- QEMUHelper/it.lproj/InfoPlist.strings | 9 + QEMUHelper/it.lproj/Localizable.strings | 9 + README.ko.md | 78 + README.zh-HK.md | 10 +- Remote/GenerateKey.c | 276 +++ Remote/GenerateKey.h | 33 + Remote/UTMRemoteClient.swift | 588 ++++++ Remote/UTMRemoteConnectInterface.h | 39 + Remote/UTMRemoteKeyManager.swift | 196 ++ Remote/UTMRemoteMessage.swift | 380 ++++ Remote/UTMRemoteServer.swift | 981 +++++++++ Remote/UTMRemoteSpiceVirtualMachine.swift | 424 ++++ Services/Swift-Bridging-Header.h | 9 +- Services/UTMAppleVirtualMachine.swift | 14 +- Services/UTMExtensions.swift | 41 + Services/UTMJailbreak.m | 12 +- Services/UTMLogging.m | 6 + Services/UTMPasteboard.swift | 2 +- Services/UTMPipeInterface.swift | 152 ++ Services/UTMQemuPort.swift | 2 +- Services/UTMQemuSystem.h | 17 +- Services/UTMQemuSystemBackends.h | 36 + Services/UTMQemuVirtualMachine.swift | 280 ++- Services/UTMRegistry.swift | 2 +- Services/UTMRegistryEntry.swift | 26 +- Services/UTMSpiceIO.h | 13 +- Services/UTMSpiceIO.m | 75 +- Services/UTMSpiceIODelegate.h | 3 +- Services/UTMSpiceVirtualMachine.swift | 177 ++ Services/UTMVirtualMachine.swift | 77 +- UTM.xcodeproj/project.pbxproj | 1003 ++++++++- .../xcshareddata/swiftpm/Package.resolved | 44 +- patches/sources | 4 +- scripts/build_dependencies.sh | 2 + scripts/build_utm.sh | 59 +- scripts/package.sh | 10 +- scripts/package_mac.sh | 3 +- 115 files changed, 10782 insertions(+), 741 deletions(-) create mode 100644 Platform/Shared/MacDeviceLabel.swift create mode 100644 Platform/iOS/Info-Remote.plist create mode 100644 Platform/iOS/RemoteContentView.swift create mode 100644 Platform/iOS/UTMRemoteConnectView.swift create mode 100644 Platform/iOS/en.lproj/Info-RemotePlist.strings create mode 100644 Platform/iOS/it.lproj/InfoPlist.strings create mode 100644 Platform/iOS/ja.lproj/Info-RemotePlist.strings create mode 100644 Platform/it.lproj/Localizable.strings create mode 100644 Platform/it.lproj/Localizable.stringsdict create mode 100644 Platform/macOS/Display/it.lproj/VMDisplayWindow.strings create mode 100644 Platform/macOS/UTMServerView.swift create mode 100644 Platform/macOS/VMRemoteSessionState.swift create mode 100644 Platform/macOS/it.lproj/InfoPlist.strings create mode 100644 QEMUHelper/it.lproj/InfoPlist.strings create mode 100644 QEMUHelper/it.lproj/Localizable.strings create mode 100644 README.ko.md create mode 100644 Remote/GenerateKey.c create mode 100644 Remote/GenerateKey.h create mode 100644 Remote/UTMRemoteClient.swift create mode 100644 Remote/UTMRemoteConnectInterface.h create mode 100644 Remote/UTMRemoteKeyManager.swift create mode 100644 Remote/UTMRemoteMessage.swift create mode 100644 Remote/UTMRemoteServer.swift create mode 100644 Remote/UTMRemoteSpiceVirtualMachine.swift create mode 100644 Services/UTMPipeInterface.swift create mode 100644 Services/UTMQemuSystemBackends.h create mode 100644 Services/UTMSpiceVirtualMachine.swift diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4ee0ba18..4124c5798 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 @@ -315,7 +319,9 @@ jobs: LAUNCHER_PROFILE_DATA: ${{ vars.LAUNCHER_PROFILE_DATA }} LAUNCHER_PROFILE_UUID: ${{ vars.LAUNCHER_PROFILE_UUID }} - name: Install appdmg - run: npm install -g appdmg + run: | + python3 -m pip install setuptools + npm install -g appdmg - name: Download Artifact uses: actions/download-artifact@v3 with: diff --git a/Build.xcconfig b/Build.xcconfig index 38654bdf7..d4e06dde9 100644 --- a/Build.xcconfig +++ b/Build.xcconfig @@ -17,8 +17,8 @@ // Configuration settings file format documentation can be found at: // https://help.apple.com/xcode/#/dev745c5c974 -MARKETING_VERSION = 4.4.4 -CURRENT_PROJECT_VERSION = 92 +MARKETING_VERSION = 4.4.5 +CURRENT_PROJECT_VERSION = 94 // Codesigning settings defined optionally, see Documentation/iOSDevelopment.md #include? "CodeSigning.xcconfig" diff --git a/Configuration/QEMUConstant.swift b/Configuration/QEMUConstant.swift index 75a6d9ae2..be78c41e2 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 2d458fba6..abfe34709 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 61103ac98..57122602a 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 2f445bec4..201b9cd6f 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 a04c4ada6..14c6d2619 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 6e024b430..23c448a4c 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 744cfc23a..377948b15 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 ce971971b..62c20ca6d 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 000000000..48f97dc05 --- /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 dadfcb5d6..67f1c4db4 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 3a9788229..8e687345d 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 558488d4b..ba675477e 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: .windowList, addition: { diff --git a/Platform/Shared/VMConfigInputView.swift b/Platform/Shared/VMConfigInputView.swift index f244be0f2..71cfbf776 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 1fcf9e48c..a2c1ce884 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 6c4859ddb..066f5dfd1 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 62fe46af8..b1cbe8d52 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 eaeea4001..5fee68f3f 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 c906b375d..307e965ff 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() + } + GridRow { + SecondRow() } - TileButton(Label(String.support, systemImage: "questionmark.circle")) { - openURL(URL(string: "https://docs.getutm.app/")!) + #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 73387f9f4..0682533bc 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 28e49c850..d8c345029 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 785e4c3b8..3c9a47747 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 77d4b81ce..0a69c3cf3 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 c08b34a8b..c7dabfecb 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 cffddf8ac..4848aa648 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 a5a901d00..f96826e22 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 5abd169ff..153a5840c 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController+Pointer.m +++ b/Platform/iOS/Display/VMDisplayMetalViewController+Pointer.m @@ -129,7 +129,11 @@ - (BOOL)hasTouchpadPointer { - (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 @@ - (bool)isPointOnVMDisplay:(CGPoint)pos { - (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 5e9148dd9..bf0b94de0 100644 --- a/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m +++ b/Platform/iOS/Display/VMDisplayMetalViewController+Touch.m @@ -181,11 +181,15 @@ - (VMMouseType)pencilMouseType { } - (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 @@ - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { 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 6a1a3566c..8bf301bd4 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 02a1ffe8b..4a3e4ccee 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 @@ - (instancetype)initWithDisplay:(CSDisplay *)display input:(CSInput *)input { 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)viewDidLoad { - (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 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIVi [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 @@ - (void)enterLive { [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 @@ - (CGSize)convertSizeToNative:(CGSize)size { 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)setVmDisplay:(CSDisplay *)display { - (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)setDisplayScaling:(CGFloat)scaling origin:(CGPoint)origin { - (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); + 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]; } - 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]; - }); + } + } +} + +- (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 - [self.delegate display:self.vmDisplay didResizeTo:self.vmDisplay.displaySize]; -#endif + 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 9991faa8c..3c368a9a6 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 07b15046f..7cbc0afc4 100644 --- a/Platform/iOS/Display/VMKeyboardView.m +++ b/Platform/iOS/Display/VMKeyboardView.m @@ -370,7 +370,7 @@ - (void)insertText:(nonnull NSString *)text { - (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 @@ - (void)insertUTF8Sequence:(const char *)ctext { 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 @@ - (void)insertUTF8Sequence:(const char *)ctext { } 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 @@ - (void)insertUTF8Sequence:(const char *)ctext { } 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/Display/zh-Hans.lproj/VMDisplayMetalViewInputAccessory.strings b/Platform/iOS/Display/zh-Hans.lproj/VMDisplayMetalViewInputAccessory.strings index 104014d15..b2d210189 100644 --- a/Platform/iOS/Display/zh-Hans.lproj/VMDisplayMetalViewInputAccessory.strings +++ b/Platform/iOS/Display/zh-Hans.lproj/VMDisplayMetalViewInputAccessory.strings @@ -95,7 +95,7 @@ "LU6-kH-vN3.accessibilityLabel" = "主页"; /* Class = "UIButton"; normalTitle = "Home"; ObjectID = "LU6-kH-vN3"; */ -"LU6-kH-vN3.normalTitle" = "主页"; +"LU6-kH-vN3.normalTitle" = "Home"; /* Class = "UIButton"; accessibilityLabel = "Escape"; ObjectID = "n12-9R-99C"; */ "n12-9R-99C.accessibilityLabel" = "Esc"; diff --git a/Platform/iOS/Info-Remote.plist b/Platform/iOS/Info-Remote.plist new file mode 100644 index 000000000..98f527f57 --- /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 000000000..05a092eda --- /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 d282f587e..75619e060 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/Settings.bundle/ja.lproj/Root.strings b/Platform/iOS/Settings.bundle/ja.lproj/Root.strings index 3aeb5b3cd..50cc7eab7 100644 --- a/Platform/iOS/Settings.bundle/ja.lproj/Root.strings +++ b/Platform/iOS/Settings.bundle/ja.lproj/Root.strings @@ -45,6 +45,9 @@ "Drag cursor" = "カーソルをドラッグ"; "Touch mode (always show cursor)" = "タッチモード(常にカーソルを表示)"; "Touch mode (try hiding cursor)" = "タッチモード(カーソルの非表示を試みる)"; +"Visibility" = "可視性"; +"Always show cursor" = "常にカーソルを表示"; +"Try hiding cursor" = "カーソルの非表示を試みる"; "Apple Pencil Input" = "Apple Pencil入力"; "Tablet mode (always show cursor)" = "タブレットモード(常にカーソルを表示)"; "Tablet mode (try hiding cursor)" = "タブレットモード(カーソルの非表示を試みる)"; diff --git a/Platform/iOS/Settings.bundle/zh-HK.lproj/Root.strings b/Platform/iOS/Settings.bundle/zh-HK.lproj/Root.strings index 0a2ad56d4..efedfdf34 100644 --- a/Platform/iOS/Settings.bundle/zh-HK.lproj/Root.strings +++ b/Platform/iOS/Settings.bundle/zh-HK.lproj/Root.strings @@ -26,16 +26,16 @@ "Cursor" = "指標"; /* (No Comment) */ -"D-DOWN" = "下方向鍵"; +"D-DOWN" = "向下鍵"; /* (No Comment) */ -"D-LEFT" = "左方向鍵"; +"D-LEFT" = "向左鍵"; /* (No Comment) */ -"D-RIGHT" = "右方向鍵"; +"D-RIGHT" = "向右鍵"; /* (No Comment) */ -"D-UP" = "上方向鍵"; +"D-UP" = "向上鍵"; /* (No Comment) */ "Disabled" = "已禁用"; diff --git a/Platform/iOS/Settings.bundle/zh-Hans.lproj/Root.strings b/Platform/iOS/Settings.bundle/zh-Hans.lproj/Root.strings index 133651329..85c5f20c8 100644 --- a/Platform/iOS/Settings.bundle/zh-Hans.lproj/Root.strings +++ b/Platform/iOS/Settings.bundle/zh-Hans.lproj/Root.strings @@ -5,7 +5,7 @@ "Auto save on background" = "在后台运行时自动保存"; /* (No Comment) */ -"Auto save on low memory" = "在内存低时自动保存"; +"Auto save on low memory" = "在内存不足时自动保存"; /* (No Comment) */ "Background" = "后台"; @@ -17,7 +17,7 @@ "Caps" = "大写锁定"; /* (No Comment) */ -"Click & Hold" = "单击并按住"; +"Click & Hold" = "点击并按住"; /* (No Comment) */ "Continue running VM in the background" = "在后台继续运行虚拟机"; @@ -89,7 +89,7 @@ "Mouse Wheel" = "鼠标滚轮"; /* (No Comment) */ -"Mouse Wheel (per swipe)" = "鼠标滚轮"; +"Mouse Wheel (per swipe)" = "鼠标滚轮 (每次滚动)"; /* (No Comment) */ "Move Screen" = "移动显示屏"; @@ -101,7 +101,7 @@ "none given" = "未指定"; /* (No Comment) */ -"Right" = "方向键右"; +"Right" = "右"; /* (No Comment) */ "Right Click" = "右键单击"; @@ -110,28 +110,28 @@ "Space" = "空格"; /* (No Comment) */ -"Tablet mode (always show cursor)" = "平板模式 (显示光标)"; +"Tablet mode (always show cursor)" = "平板模式 (总是显示光标)"; /* (No Comment) */ -"Tablet mode (try hiding cursor)" = "平板模式 (隐藏光标)"; +"Tablet mode (try hiding cursor)" = "平板模式 (尝试隐藏光标)"; /* (No Comment) */ -"Three Finger Pan" = "三指拖动"; +"Three Finger Pan" = "三指拖移"; /* (No Comment) */ "Touch Input" = "触摸输入"; /* (No Comment) */ -"Touch mode (always show cursor)" = "触摸模式 (显示光标)"; +"Touch mode (always show cursor)" = "触摸模式 (总是显示光标)"; /* (No Comment) */ -"Touch mode (try hiding cursor)" = "触摸模式 (隐藏光标)"; +"Touch mode (try hiding cursor)" = "触摸模式 (尝试隐藏光标)"; /* (No Comment) */ "Touchpad/Mouse Input" = "触控板/鼠标输入"; /* (No Comment) */ -"Two Finger Pan" = "双指拖动"; +"Two Finger Pan" = "双指拖移"; /* (No Comment) */ "Two Finger Scroll" = "双指滚动"; diff --git a/Platform/iOS/UTMDataExtension.swift b/Platform/iOS/UTMDataExtension.swift index 6d43fec06..3c109a489 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 000000000..6dc9ed6c9 --- /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 c65d0fa8b..11621fec7 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 a369942ba..419bcfa89 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 c471816a0..c8d7435cb 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 2c7ac20f2..0805b6353 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 + showWindow() + if vm.state == .paused { + vm.requestVmResume() + } else { + vm.requestVmStart(options: options) + } + } + + func showWindow() { NotificationCenter.default.post(name: .vmSessionCreated, object: nil, userInfo: ["Session": self]) - vm.requestVmStart(options: options) } - + @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 6e649db5a..c622da757 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 adaff2e12..5a9d2bfc0 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 c4db71d23..c9452fc83 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 2a55b676b..ce4e1b82f 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/iOS/en.lproj/Info-RemotePlist.strings b/Platform/iOS/en.lproj/Info-RemotePlist.strings new file mode 100644 index 000000000..f2180cd78 --- /dev/null +++ b/Platform/iOS/en.lproj/Info-RemotePlist.strings @@ -0,0 +1 @@ +"" = ""; diff --git a/Platform/iOS/it.lproj/InfoPlist.strings b/Platform/iOS/it.lproj/InfoPlist.strings new file mode 100644 index 000000000..a41ec9ed2 --- /dev/null +++ b/Platform/iOS/it.lproj/InfoPlist.strings @@ -0,0 +1,20 @@ +/* Bundle name */ +"CFBundleName" = "UTM"; + +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "La macchina virtuale può accedere alla rete locale. Inoltre, UTM, si avvale della rete locale per comunicare con AltServer"; + +/* Privacy - Location Always and When In Use Usage Description */ +"NSLocationAlwaysAndWhenInUseUsageDescription" = "UTM richiede il permesso di accedere alla posizione periodicamente per assicurarsi che il sistema mantenga il processo in background. I dati raccolti sulla posizione non lasceranno il tuo dispositivo."; + +/* Privacy - Location Always Usage Description */ +"NSLocationAlwaysUsageDescription" = "UTM richiede il permesso di accedere alla posizione periodicamente per assicurarsi che il sistema mantenga il processo in background. I dati raccolti sulla posizione non lasceranno il tuo dispositivo."; + +/* Privacy - Location When In Use Usage Description */ +"NSLocationWhenInUseUsageDescription" = "UTM richiede il permesso di accedere alla posizione periodicamente per assicurarsi che il sistema mantenga il processo in background. I dati raccolti sulla posizione non lasceranno il tuo dispositivo."; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "Permette alle Macchine Virtuali di accedere al Microfono"; + +/* (No Comment) */ +"UTM virtual machine" = "Macchina Virtuale UTM"; diff --git a/Platform/iOS/ja.lproj/Info-RemotePlist.strings b/Platform/iOS/ja.lproj/Info-RemotePlist.strings new file mode 100644 index 000000000..10de8484c --- /dev/null +++ b/Platform/iOS/ja.lproj/Info-RemotePlist.strings @@ -0,0 +1,9 @@ +/* Bundle name */ +"CFBundleName" = "UTMリモート"; + +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "UTMはローカルネットワークを使用してUTMリモートサーバを検索し、接続します。"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "仮想マシンがマイクから録音するには、アクセス許可が必要です。"; + diff --git a/Platform/iOS/zh-HK.lproj/InfoPlist.strings b/Platform/iOS/zh-HK.lproj/InfoPlist.strings index 436340a4d..89af126d7 100644 --- a/Platform/iOS/zh-HK.lproj/InfoPlist.strings +++ b/Platform/iOS/zh-HK.lproj/InfoPlist.strings @@ -1,17 +1,17 @@ /* Bundle name */ -"CFBundleName" = "UTM SE"; +"CFBundleName" = "UTM"; /* Privacy - Local Network Usage Description */ "NSLocalNetworkUsageDescription" = "虛擬電腦可以訪問本地網絡。UTM 還會使用本地網絡與 AltServer 進行通信。"; /* Privacy - Location Always and When In Use Usage Description */ -"NSLocationAlwaysAndWhenInUseUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永不會離開設備。"; +"NSLocationAlwaysAndWhenInUseUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永不離開設備。"; /* Privacy - Location Always Usage Description */ -"NSLocationAlwaysUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永不會離開設備。"; +"NSLocationAlwaysUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永不離開設備。"; /* Privacy - Location When In Use Usage Description */ -"NSLocationWhenInUseUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永遠不會離開設備。"; +"NSLocationWhenInUseUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永不離開設備。"; /* Privacy - Microphone Usage Description */ "NSMicrophoneUsageDescription" = "任何虛擬電腦都需要許可才能由咪高風進行錄製。"; diff --git a/Platform/iOS/zh-Hans.lproj/InfoPlist.strings b/Platform/iOS/zh-Hans.lproj/InfoPlist.strings index 831cdb052..ade2692ab 100644 --- a/Platform/iOS/zh-Hans.lproj/InfoPlist.strings +++ b/Platform/iOS/zh-Hans.lproj/InfoPlist.strings @@ -1,8 +1,8 @@ /* Bundle name */ -"CFBundleName" = "UTM SE"; +"CFBundleName" = "UTM"; /* Privacy - Local Network Usage Description */ -"NSLocalNetworkUsageDescription" = "虚拟机可以访问本地网络。UTM 还使用本地网络与 AltServer 通信。"; +"NSLocalNetworkUsageDescription" = "虚拟机可能会访问本地网络。UTM 还使用本地网络与 AltServer 通信。"; /* Privacy - Location Always and When In Use Usage Description */ "NSLocationAlwaysAndWhenInUseUsageDescription" = "UTM 定期请求位置数据,以确保系统保持后台进程处于活动状态。位置数据永远不会离开设备。"; diff --git a/Platform/it.lproj/Localizable.strings b/Platform/it.lproj/Localizable.strings new file mode 100644 index 000000000..2ccc771ce --- /dev/null +++ b/Platform/it.lproj/Localizable.strings @@ -0,0 +1,1855 @@ +/* A removable drive that has no image file inserted. */ +"(empty)" = "(vuoto)"; + +/* VMConfigAppleDriveDetailsView */ +"(New Drive)" = "(Nuovo Disco)"; + +/* No comment provided by engineer. */ +"(new)" = "(nuovo)"; + +/* UTMWrappedVirtualMachine */ +"(Unavailable)" = "(Non disponibile)"; + +/* QEMUConstant */ +"%@ (%@)" = "%1$@ (%2$@)"; + +/* VMToolbarDriveMenuView */ +"%@ (%@): %@" = "%1$@ (%2$@): %3$@"; + +/* VMDisplayMetalWindowController */ +"%@ (Display %lld)" = "%1$@ (Monitor %2$lld)"; + +/* VMDisplayAppleTerminalWindowController + VMDisplayQemuTerminalWindowController */ +"%@ (Terminal %lld)" = "%1$@ (Terminale %2$lld)"; + +/* No comment provided by engineer. */ +"%@ ➡️ %@" = "%1$@ ➡️ %2$@"; + +/* VMDrivesSettingsView */ +"%@ Drive" = "Disco %@"; + +/* VMDrivesSettingsView */ +"%@ Image" = "Immagine %@"; + +/* Format string for remaining time until a download finishes */ +"%@ remaining" = "%@ rimanente"; + +/* Format string for the 'per second' part of a download speed. */ +"%@/s" = "%@/s"; + +/* Format string for download progress and speed, e. g. 5 MB of 6 GB (200 kbit/s) */ +"%1$@ of %2$@ (%3$@)" = "%1$@ di %2$@ (%3$@)"; + +/* UTMAppleConfiguration */ +"A valid kernel image must be specified." = "È necessario specificare un'immagine del kernel valida"; + +/* VMConfigDriveCreateViewController */ +"A file already exists for this name, if you proceed, it will be replaced." = "Un file con questo nome esiste già, se procedi, verrà sovrascritto"; +"Cannot create directory for disk image." = "Non è stato possibile creare una cartella per l'immagine disco"; + +/* VMListViewController */ +"A VM already exists with this name." = "È già presente una Macchina Virtuale con questo nome."; +"Cannot find VM." = "Macchina Virtuale non trovata."; + +/* VMDisplayAppleController */ +"Add…" = "Aggiungi…"; + +/* No comment provided by engineer. */ +"Additional Options" = "Opzioni Aggiuntive"; + +/* No comment provided by engineer. */ +"Additional Settings" = "Impostazioni Aggiuntive"; + +/* No comment provided by engineer. */ +"Advanced: Bypass configuration and manually specify arguments" = "Avanzato: Ometti la configurazione e specifica gli argomenti"; + +/* No comment provided by engineer. */ +"Advanced" = "Avanzate"; + +/* VMConfigSystemView */ +"Allocating too much memory will crash the VM." = "Allocare troppa memoria causerà crash della Macchina Virtuale"; +"Allocating too much memory will crash the VM. Your device has %llu MB of memory and the estimated usage is %llu MB." = "Allocare troppa memoria causerà crash della Macchina Virtuale. Il tuo dispositivo dispone di %llu MB di memoria e l'uso stimato è di %llu MB."; + +/* VMConfigDirectoryPickerViewController */ +"Are you sure you want to delete this directory? All files and subdirectories WILL be deleted." = "Sei sicuro di voler eliminare questa cartella? Tutti i file e le cartelle contenute saranno elimininati."; + +/* Delete confirmation */ +"Are you sure you want to delete this VM? Any drives associated will also be deleted." = "Sei sicuro di voler eliminare questa Macchina Virtuale? Ogni disco associato sarà eliminato."; + +/* No comment provided by engineer. */ +"Auto Resolution" = "Risoluzione Automatica"; + +/* UTMData */ +"AltJIT error: %@" = "Errore AltJIT: %@"; +"AltJIT error: (error.localizedDescription)" = "Errore AltJIT: (error.localizedDescription)"; + +/* No comment provided by engineer. */ +"Always use native (HiDPI) resolution" = "Usa sempre la risoluzione nativa (HiDPI)"; + +/* UTMData */ +"An existing virtual machine already exists with this name." = "È già presente una Macchina Virtuale con questo nome."; + +/* CSConnection */ +"An error occurred trying to connect to SPICE." = "Errore di connessione a SPICE."; + +/* VMDrivesSettingsView */ +"An image already exists with that name." = "È già presente un'immagine con questo nome."; + +/* UTMConfiguration + UTMVirtualMachine */ +"An internal error has occurred." = "Si è verificato un errore interno"; +"An internal error has occured. UTM will terminate." = "Si è verificato un errore interno. UTM verrà terminato."; +"Cannot start shared directory before SPICE starts." = "Impossibile condividere la directory prima dell'avvio di SPICE."; + +/* No comment provided by engineer. */ +"Argument" = "Argomento"; + +/* UTMConfiguration */ +"An invalid value of '%@' is used in the configuration file." = "Un valore non corretto di “%@“ è presente nel file di configurazione."; + +/* VMConfigSystemView */ +"Any unsaved changes will be lost." = "Le modifiche non salvate andranno perse."; + +/* No comment provided by engineer. */ +"Application" = "Applicazione"; + +/* New VM window. */ +"Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "La Virtualizzazione Apple è sperimentale e consigliata per casi d'uso avanzati. È raccomandato lasciare questa opzione disabilitata per usare QEMU."; + +/* No comment provided by engineer. */ +"Architecture" = "Architettura"; + +/* No comment provided by engineer. */ +"Are you sure you want to exit UTM?" = "Sei sicuro di voler uscire da UTM?"; + +/* No comment provided by engineer. */ +"Are you sure you want to permanently delete this disk image?" = "Sei sicuro di voler eliminare questa immagine definitivamente?"; + +/* No comment provided by engineer. */ +"Are you sure you want to reset this VM? Any unsaved changes will be lost." = "Sei sicuro di voler riavviare questa Macchina Virtuale? Le modifiche non salvate andranno perse."; + +/* No comment provided by engineer. */ +"Are you sure you want to stop this VM and exit? Any unsaved changes will be lost." = "Sei sicuro di voler arrestare questa Macchina Virtuale e uscire? Le modifiche non salvate andranno perse."; + +/* UTMQemuConstants */ +"Automatic Serial Device (max 4)" = "Dispositivo seriale automatico (massimo 4)"; + +/* No comment provided by engineer. */ +"Background Color" = "Colore di Sfondo"; + +/* No comment provided by engineer. */ +"Balloon Device" = "Dispositivo Balloon"; + +/* UTMLegacyQemuConfiguration + UTMQemuConstants */ +"BIOS" = "BIOS"; + +/* No comment provided by engineer. */ +"Blinking Cursor" = "Cursore Lampeggiante"; + +/* UTMQemuConstants */ +"Bold" = "Grassetto"; + +/* No comment provided by engineer. */ +"Boot" = "Avvio"; + +/* No comment provided by engineer. */ +"Boot from kernel image" = "Avvia da immagine del kernel"; + +/* No comment provided by engineer. */ +"Boot Arguments" = "Argomenti di Avvio"; + +/* No comment provided by engineer. */ +"Boot Image" = "Immagine di Avvio"; + +/* No comment provided by engineer. */ +"Boot Image Type" = "Tipo di Immagine di Avvio"; + +/* No comment provided by engineer. */ +"Boot ISO Image" = "Immagine ISO di Avvio"; + +/* No comment provided by engineer. */ +"Boot ISO Image (optional)" = "Immagine ISO di Avvio (opzionale)"; + +/* No comment provided by engineer. */ +"Boot VHDX Image" = "Immagine VHDX di Avvio"; + +/* No comment provided by engineer. */ +"Boot into recovery mode." = "Avvia con modalità di ripristino"; + +/* UTMQemuConstants */ +"Bridged (Advanced)" = "Bridged (Avanzata)"; + +/* No comment provided by engineer. */ +"Bridged Settings" = "Impostazioni Bridged"; + +/* No comment provided by engineer. */ +"Bridged Interface" = "Interfaccia Bridget"; + +/* Welcome view */ +"Browse UTM Gallery" = "Scopri la UTM Gallery"; + +/* No comment provided by engineer. */ +"Browse" = "Scegli"; + +/* No comment provided by engineer. */ +"Browse…" = "Scegli..."; + +/* UTMQemuConstants */ +"Built-in Terminal" = "Terminale Integrato"; + +/* VMDisplayWindowController + VMQemuDisplayMetalWindowController */ +"Cancel" = "Annulla"; + +/* No comment provided by engineer. */ +"Cancel download" = "Annulla Download"; + +/* UTMAppleVirtualMachine */ +"Cannot access resource: %@" = "Impossibile accedere alla risorsa: %@"; + +/* UTMAppleVirtualMachine */ +"Cannot create virtual terminal." = "Impossibile creare il terminale virtuale."; + +/* UTMData */ +"Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled." = "Impossibile trovare AltServer per l'abilitazione di JIT. Non è possibile eseguire Macchine Virtuali senza che JIT sia abilitato."; + +/* UTMData */ +"Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM." = "Impossibile importare questa Macchina Virtuale. Potrebbe contenere una configurazione invalida, essere stata creata con una nuova versione di UTM, oppure su una piattaforma non supportata da questa versione di UTM."; + +/* No comment provided by engineer. */ +"Caps Lock (⇪) is treated as a key" = "Blocco Maiusc. (⇪) come tasto"; + +/* VMMetalView */ +"Capture Input" = "Cattura Input"; + +/* VMDisplayQemuMetalWindowController */ +"Captured mouse" = "Mouse catturato"; + +/* Configuration boot device */ +"CD/DVD" = "CD/DVD"; + +/* UTMLegacyQemuConfiguration + UTMQemuConstants */ +"CD/DVD (ISO) Image" = "Immagine (ISO) di CD/DVD"; + +/* VMDisplayWindowController */ +"Change" = "Cambia"; + +/* VMDisplayAppleController */ +"Change…" = "Cambia…"; + +/* No comment provided by engineer. */ +"Clear" = "Pulisci"; + +/* No comment provided by engineer. */ +"Clipboard Sharing" = "Condivisione Appunti"; + +/* VMQemuDisplayMetalWindowController */ +"Closing this window will kill the VM." = "Chiudere questa finestra terminerà la Macchina Virtuale"; + +/* Clone context menu */ +"Clone" = "Clona"; + +/* No comment provided by engineer. */ +"Clone selected VM" = "Clona la Macchina Virtuale selezionata"; + +/* No comment provided by engineer. */ +"Clone…" = "Clona..."; + +/* No comment provided by engineer. */ +"Close" = "Chiudi"; + +/* No comment provided by engineer. */ +"Command to send when resizing the console. Placeholder $COLS is the number of columns and $ROWS is the number of rows." = "Comando da inviare quando la console viene ridimensionata. La variabile $COLS è il numero di colonne, $ROWS è il numero di righe."; + +/* UTMVirtualMachine */ +"Config format incorrect." = "Formato di configurazione incorretto."; + +/* VMQemuDisplayMetalWindowController */ +"Confirm" = "Conferma"; + +/* No comment provided by engineer. */ +"Confirm Delete" = "Conferma Eliminazione"; + +/* VMDisplayWindowController */ +"Confirmation" = "Conferma"; + +/* No comment provided by engineer. */ +"Connection" = "Connessione"; + +/* No comment provided by engineer. */ +"Console Only" = "Solo Console"; + +/* VMWizardSummaryView */ +"Core" = "Core"; + +/* No comment provided by engineer. */ +"Cores" = "Core"; + +/* No comment provided by engineer. */ +"Continue" = "Continua"; + +/* No comment provided by engineer. */ +"CPU" = "CPU"; + +/* No comment provided by engineer. */ +"CPU Cores" = "Core della CPU"; + +/* No comment provided by engineer. */ +"CPU Flags" = "Flag della CPU"; + +/* No comment provided by engineer. */ +"Create" = "Crea"; + +/* Welcome view */ +"Create a New Virtual Machine" = "Crea una Nuova Macchina Virtuale (VM)"; + +/* No comment provided by engineer. */ +"Create a new VM with the same configuration as this one but without any data." = "Crea una nuova Macchina Virtuale con la stessa configurazione, ma senza alcun dato."; + +/* No comment provided by engineer. */ +"Custom" = "Personalizzata"; + +/* VMConfigDirectoryPickerViewController */ +"Create Directory" = "Crea Cartella"; + +/* VMConfigDriveCreateViewController */ +"Creating disk…" = "Creazione disco..."; + +/* No comment provided by engineer. */ +"Debug Logging" = "Registro dei Log"; + +/* QEMUConstantGenerated + UTMQemuConstants */ +"Default" = "Predefinito"; + +/* VMWizardSummaryView */ +"Default Cores" = "Core Predefiniti"; + +/* No comment provided by engineer. */ +"Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!" = "1/4 della RAM (sopra) come impostazione predefinita. La dimensione della cache JIT si aggiunge alla quantità di memoria utilizzata!"; + +/* No comment provided by engineer. */ +"Delete" = "Elimina"; + +/* VMConfigDrivesViewController */ +"Delete Data" = "Elimina Dati"; + +/* No comment provided by engineer. */ +"Delete Drive" = "Elimina Disco"; + +/* No comment provided by engineer. */ +"Delete selected VM" = "Elimina la Macchina Virtuale selezionata"; + +/* No comment provided by engineer. */ +"Delete…" = "Elimina..."; + +/* No comment provided by engineer. */ +"Delete this shortcut. The underlying data will not be deleted." = "Elimina questa scorciatoia. I dati originali non saranno eliminati."; + +/* No comment provided by engineer. */ +"Delete this VM and all its data." = "Elimina questa Macchina Virtuale e tutti i suoi dati."; + +/* Delete VM overlay */ +"Deleting %@…" = "Eliminazione di %@..."; + +/* No comment provided by engineer. */ +"DHCP Domain Name" = "Nome di Dominio DHCP"; + +/* No comment provided by engineer. */ +"DHCP Host" = "Host DHCP"; + +/* No comment provided by engineer. */ +"DHCP Start" = "Inzio DHCP"; + +/* No comment provided by engineer. */ +"Directory" = "Cartella"; + +/* VMConfigDirectoryPickerViewController */ +"Directory Name" = "Nome della Cartella"; + +/* No comment provided by engineer. */ +"Devices" = "Dispositivi"; + +/* VMDisplayAppleWindowController */ +"Directory sharing" = "Condivisione Cartella"; + +/* No comment provided by engineer. */ +"Directory Share Mode" = "Modalità di Condivisione Cartelle"; + +/* UTMQemuConstants */ +"Disabled" = "Disattivato"; + +/* VMDisplayTerminalViewController */ +"Disable this bar in Settings -> General -> Keyboards -> Shortcuts" = "Disattiva questa barra in Impostazioni -> Generali -> Tastiere -> Scorciatoie"; + +/* No comment provided by engineer. */ +"Disk" = "Disco"; + +/* UTMData + VMConfigDriveCreateViewController + VMWizardState */ +"Disk creation failed." = "Creazione del disco fallita."; + +/* UTMLegacyQemuConfiguration + UTMQemuConstants */ +"Disk Image" = "Immagine del Disco"; + +/* VMDisplayAppleWindowController */ +"Display" = "Monitor"; + +/* VMDisplayQemuDisplayController */ +"Display %lld: %@" = "Monitor %1$lld: %2$@"; + +/* VMDisplayQemuDisplayController */ +"Disposable Mode" = "Modo desechable"; + +/* No comment provided by engineer. */ +"DNS Search Domains" = "Dominio di Ricerca DNS"; + +/* No comment provided by engineer. */ +"DNS Server" = "Server DNS"; + +/* No comment provided by engineer. */ +"DNS Server (IPv6)" = "Server DNS (IPv6)"; + +/* No comment provided by engineer. */ +"Do not save VM screenshot to disk" = "Non salvare gli screenshot delle Macchine Virtuali su disco"; + +/* VMDisplayMetalWindowController */ +"Do Not Show Again" = "Non Mostrare di Nuovo"; + +/* No comment provided by engineer. */ +"Do not show prompt when USB device is plugged in" = "Non mostrare una richiesta quando un dispositivo USB viene collegato"; + +/* VMConfigDrivesViewController */ +"Do you want to also delete the disk image data? If yes, the data will be lost. Otherwise, you can create a new drive with the existing data." = "Vuoi anche eliminare i dati dell'immagine disco? Se sì, i dati saranno persi. In caso contrario, puoi creare un nuovo disco con i dati esistenti."; + +/* No comment provided by engineer. */ +"Do you want to delete this VM and all its data?" = "Vuoi eliminare questa Macchina Virtuale e tutti i suoi dati?"; + +/* No comment provided by engineer. */ +"Do you want to duplicate this VM and all its data?" = "Vuoi clonare questa Macchina Virtuale e tutti i suoi dati?"; + +/* No comment provided by engineer. */ +"Do you want to force stop this VM and lose all unsaved data?" = "Vuoi forzare l'arresto di questa Macchina Virtuale e perdere tutti i dati non salvati?"; + +/* No comment provided by engineer. */ +"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "Vuoi spostare questa Macchina Virtuale in una nuova posizione? I dati saranno copiati nella nuova posizione ed eliminati dalla posizione originale. Verrà creata una nuova scorciatoia."; + +/* No comment provided by engineer. */ +"Do you want to remove this shortcut? The data will not be deleted." = "Vuoi eliminare questa scorciatoia? I dati originali non saranno eliminati."; + +/* VMConfigDirectoryPickerViewController + VMConfigPortForwardingViewController */ +"Done" = "Completato"; + +/* No comment provided by engineer. */ +"Download and mount the guest support package for Windows. This is required for some features including dynamic resolution and clipboard sharing." = "Scarica e monta l'immagine del paccheto di strumenti di supporto per Windows. Questi sono necessari per alcune funzionalità come la risoluzione dinamica e la condivisione degli appunti."; + +/* No comment provided by engineer. */ +"Download and mount the guest tools for Windows." = "Scarica e monta gli strumenti guest per Windows."; + +/* No comment provided by engineer. */ +"Download prebuilt from UTM Gallery…" = "Scarica una VM pronta da usare da UTM Gallery..."; + +/* No comment provided by engineer. */ +"Download Ubuntu Server for ARM" = "Scarica Ubuntu Server per ARM"; + +/* No comment provided by engineer. */ +"Download Windows 11 for ARM64 Preview VHDX" = "Scarica l'immagine della Windows 11 ARM64 Preview VHDX"; + +/* No comment provided by engineer. */ +"Downscaling" = "Downscaling"; + +/* No comment provided by engineer. */ +"Drag and drop IPSW file here" = "Trascina il file IPSW qui"; + +/* VMRemovableDrivesViewController */ +"Drive Options" = "Opzioni Disco"; + +/* No comment provided by engineer. */ +"Drives" = "Dischi"; + +/* No comment provided by engineer. */ +"Duplicate this VM along with all its data." = "Duplica questa Macchina Virtuale e tutti i suoi dati"; + +/* No comment provided by engineer. */ +"Edit" = "Modifica"; + +/* No comment provided by engineer. */ +"Edit…" = "Modifica..."; + +/* No comment provided by engineer. */ +"Edit selected VM" = "Modifica la Macchina Virtuale selezionata"; + +/* VMDrivesSettingsView */ +"EFI Variables" = "Variabli EFI"; + +/* VMDisplayWindowController */ +"Eject" = "Espelli"; + +/* New VM window. */ +"Empty" = "Vuoto"; + +/* No comment provided by engineer. */ +"Emulate" = "Emula"; + +/* No comment provided by engineer. */ +"Emulated Audio Card" = "Scheda Audio Emulata"; + +/* No comment provided by engineer. */ +"Emulated Display Card" = "Scheda Video Emulata"; + +/* No comment provided by engineer. */ +"Emulated Network Card" = "Scheda di Rete Emulata"; + +/* UTMQemuConfiguration */ +"Emulated VLAN" = "VLAN Emulata"; + +/* No comment provided by engineer. */ +"Emulated Serial Device" = "Dispositivo Seriale Emulato"; + +/* No comment provided by engineer. */ +"Enable Balloon Device" = "Abilita Dispositivo Balloon"; + +/* No comment provided by engineer. */ +"Enable Entropy Device" = "Abilita Dispositivo Entropy"; + +/* No comment provided by engineer. */ +"Enable Clipboard Sharing" = "Abilita Condivisione Appunti"; + +/* No comment provided by engineer. */ +"Enable Directory Sharing" = "Abilita Condivisione Cartella"; + +/* No comment provided by engineer. */ +"Enable Keyboard" = "Abilita Tastiera"; + +/* No comment provided by engineer. */ +"Enable Sound" = "Abilita Suono"; + +/* No comment provided by engineer. */ +"Enable Pointer" = "Abilita Puntatore"; + +"Enable Rosetta on Linux (x86_64 Emulation)" = "Abilita Rosetta su Linux (Emulazione x86_64)"; + +/* No comment provided by engineer. */ +"Enable hardware OpenGL acceleration" = "Abilita l'accelerazione hardware OpenGL"; + +/* No comment provided by engineer. */ +"Enabled" = "Abilitato"; + +/* No comment provided by engineer. */ +"Engine" = "Engine"; + +/* VMDisplayWindowController */ +"Error" = "Errore"; + +/* UTMJSONStream */ +"Error parsing JSON." = "Errore di conversione JSON"; + +/* VMConfigDriveCreateViewController */ +"Error renaming file" = "Errore di ridenominazione file"; + +/* UTMVirtualMachine */ +"Error trying to restore external drives and shares: %@" = "Errore durante il tentativo di ripristino dei dischi esterni e delle condivisioni: %@"; + +/* UTMVirtualMachine */ +"Error trying to start shared directory: %@" = "Errore durante l'avvio della cartella condivisa: %@"; + +/* No comment provided by engineer. */ +"Existing" = "Esistente"; + +/* No comment provided by engineer. */ +"Export Debug Log" = "Esporta Log di Debug"; + +/* No comment provided by engineer. */ +"Export QEMU Command…" = "Esporta comando QEMU…"; + +/* Word for decompressing a compressed folder */ +"Extracting…" = "Estrazione…"; + +/* UTMVirtualMachine+Drives */ +"Failed create bookmark." = "Impossibile creare preferito."; + +/* UTMQemuVirtualMachine */ +"Failed to access data from shortcut." = "Impossibile accedere ai dati dalla scorciatoia."; + +/* UTMQemuVirtualMachine */ +"Failed to access drive image path." = "Impossibile accedere alla posizione dell'immagine disco."; + +/* UTMQemuVirtualMachine */ +"Failed to access shared directory." = "Impossibile accedere alla cartella condivisa."; + +/* ContentView */ +"Failed to attach to JitStreamer:\n%@" = "Errore di collegamento a JitStreamer: %@"; + +/* ContentView */ +"Failed to attach to JitStreamer." = "Errore di collegamento a JitStreamer."; + +/* VMConfigInfoView */ +"Failed to check name." = "Impossibile verificare il nome."; + +/* UTMData */ +"Failed to clone VM." = "Impossibile clonare la Macchina Virtuale"; + +/* UTMSpiceIO */ +"Failed to connect to SPICE server." = "Collegamento al server SPICE fallito"; + +/* ContentView */ +"Failed to decode JitStreamer response." = "Impossibile interpretare la risposta di JitStreamer."; + +/* UTMDataExtension */ +"Failed to delete saved state." = "Salvataggio dello stato fallito."; + +/* VMWizardState */ +"Failed to get latest macOS version from Apple." = "Non è stato possibile ottenere l'ultima versione di macOS da Apple."; + +/* VMRemovableDrivesViewController */ +"Failed to get VM object." = "Non è stato possibile ottenere l'oggetto della VM"; + +/* UTMVirtualMachine */ +"Failed to load plist" = "Caricamento del plist fallito"; + +/* UTMQemuConfigurationError */ +"Failed to migrate configuration from a previous UTM version." = "Migrazione da una precedente versione di UTM fallita."; + +/* UTMData */ +"Failed to parse download URL." = "Errore nella lettura dell'URL di download."; + +/* UTMData */ +"Failed to parse imported VM." = "Errore di lettura della la Macchina Virtuale importata"; + +/* UTMDownloadVMTask */ +"Failed to parse the downloaded VM." = "Errore di lettura della la Macchina Virtuale scaricata."; + +/* UTMQemuVirtualMachine */ +"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "Non è stato possibile salvare l'istantanea della VM. Generalmente, questo significa che almeno uno dei dispositivi non supporta le istantanee: %@"; +"Failed to save VM snapshot. Usually this means at least one device does not support snapshots." = "Non è stato possibile salvare l'istantanea della VM. Generalmente, questo significa che almeno uno dei dispositivi non supporta le istantanee."; + +/* UTMSpiceIO */ +"Failed to start SPICE client." = "Errore di avvio del Client SPICE"; + +/* No comment provided by engineer. */ +"Faster, but can only run the native CPU architecture." = "Più veloce, ma può solo eseguire VM con l'architettura nativa della CPU."; + +/* No comment provided by engineer. */ +"Fit To Screen" = "Adatta allo Schermo"; + +/* Configuration boot device + UTMQemuConstants */ +"Floppy" = "Dischetto"; + +/* No comment provided by engineer. */ +"Font" = "Font"; + +/* No comment provided by engineer. */ +"Font Size" = "Dimensione Font"; + +/* VMDisplayQemuDisplayController */ +"Force kill" = "Arresto Forzato"; + +/* No comment provided by engineer. */ +"Force Multicore" = "Forza Multicore"; + +/* No comment provided by engineer. */ +"Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation." = "Forzare l'uso del Multicore potrebbe incrementare la velocità emulazione, ma potrebbe risultare in emulazione scorretta o instabile."; + +/* No comment provided by engineer. */ +"Force PS/2 controller" = "Forza Controller PS/2"; + +/* No comment provided by engineer. */ +"Force Enable CPU Flags" = "Forza Abilitazione Flag CPU"; + +/* No comment provided by engineer. */ +"Force Disable CPU Flags" = "Forza Disattivazioen Flag CPU"; + +/* VMDisplayQemuDisplayController */ +"Force shut down" = "Forza Spegnimento"; + +"Force kill the VM process with high risk of data corruption." = "Forza l'interruzione del processo della VM, con un elevato rischio di compromettere dei dati."; + +/* No comment provided by engineer. */ +"Full Graphics" = "Grafica Completa"; + +/* No comment provided by engineer. */ +"GB" = "GB"; + +/* UTMQemuConstants */ +"GDB Debug Stub" = "GDB Debug Stub"; + +/* No comment provided by engineer. */ +"Generate Windows Installer ISO" = "Genera l'immagine ISO dell'Installer di Windows"; + +/* No comment provided by engineer. */ +"Generic" = "Generico"; + +/* No comment provided by engineer. */ +"Gesture and Cursor Settings" = "Impostazioni Gesti e Cursore"; + +/* VMWizardView */ +"Go Back" = "Indietro"; + +/* No comment provided by engineer. */ +"Guest Address" = "Indirizzo Guest"; + +/* VMConfigPortForwardingViewController */ +"Guest address (optional)" = "Indirizzo Guest (opcional)"; + +/* No comment provided by engineer. */ +"Guest Network" = "Rete Guest"; + +/* No comment provided by engineer. */ +"Guest Network (IPv6)" = "Rete Guest (IPv6)"; + +/* UTMQemuManager */ +"Guest panic" = "Guest panic"; + +/* No comment provided by engineer. */ +"Guest Port" = "Porta Guest"; + +/* VMConfigPortForwardingViewController */ +"Guest port (required)" = "Porta Guest (richiesto)"; + +/* Configuration boot device */ +"Hard Disk" = "Disco Rigido"; + +/* No comment provided by engineer. */ +"Hardware" = "Hardware"; + +/* No comment provided by engineer. */ +"Hardware OpenGL Acceleration" = "Accelerazione Hardware OpenGL"; + +/* No comment provided by engineer. */ +"Hello" = "Ciao"; + +/* No comment provided by engineer. */ +"Hide" = "Nascondi"; + +/* No comment provided by engineer. */ +"Hide Unused…" = "Nascondi inutilizzati..."; + +/* VMDisplayViewController */ +"Hint: To show the toolbar again, use a three-finger swipe down on the screen." = "Suggerimento: per mostrare di nuovo la barra degli strumenti, scorri con tre dita sullos chermo."; + +/* No comment provided by engineer. */ +"Hold Control (⌃) for right click" = "Tieni premuto Control (⌃) per il click destro"; + +/* No comment provided by engineer. */ +"Host Address" = "Indirizzo Host"; + +/* No comment provided by engineer. */ +"Host Address (IPv6)" = "Indirizzo Host (IPv6)"; + +/* VMConfigPortForwardingViewController */ +"Host address (optional)" = "Indirizzo Host (opcional)"; + +/* UTMQemuConstants */ +"Host Only" = "Solo Host"; + +/* No comment provided by engineer. */ +"Host Port" = "Porta Host"; + +/* VMConfigPortForwardingViewController */ +"Host port (required)" = "Porta Host (requerido)"; + +/* No comment provided by engineer. */ +"Hypervisor" = "Hypervisor"; + +/* No comment provided by engineer. */ +"I want to…" = "Voglio..."; + +/* No comment provided by engineer. */ +"If enabled, the default input devices will be emulated on the USB bus." = "Se abilitato, i dispositivi di input predefiniti saranno emulati sul bus USB."; + +"If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64." = "Se abilitato, una cartella condvisa virtiofs con nome 'rosetta' sarà disponibile sul guest Linux, permettendoti di installare Rosetta per emulare x86_64 su ARM64."; + +/* No comment provided by engineer. */ +"If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock." = "Se abilitato, usa l'ora locale per il RTC, richiesto da Windows. Altrimenti, usa UTC."; + +/* No comment provided by engineer. */ +"If checked, the CPU flag will be enabled. Otherwise, the default value will be used." = "Se abilitato, la flag CPU verrà abilitata. In alternativa, sarà usato il valore predefinito."; + +/* No comment provided by engineer. */ +"If checked, the CPU flag will be disabled. Otherwise, the default value will be used." = "Se abiltato, la flag CPU verrà disabilitata. In alternativa, sarà usato il valore predefinito."; + +/* No comment provided by engineer. */ +"Icon" = "Icona"; + +/* UTMQemuConstants */ +"IDE" = "IDE"; + +/* No comment provided by engineer. */ +"If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO." = "Se impostato, avvia direttamente da un'immagine del kernel e initrd. In alternativa, avvia da una ISO supportata."; + +/* No comment provided by engineer. */ +"Image File Type" = "Tipo di File di Immagine"; + +/* No comment provided by engineer. */ +"Image Type" = "Tipo di Immagine"; + +/* No comment provided by engineer. */ +"Import IPSW" = "Importa un IPSW"; + +/* No comment provided by engineer. */ +"Import…" = "Importa..."; + +/* No comment provided by engineer. */ +"Import Drive" = "Importa Disco"; + +/* No comment provided by engineer. */ +"Import VHDX Image" = "Importa un'Immagine VHDX"; + +/* No comment provided by engineer. */ +"Import Virtual Machine…" = "Importa una Macchina Virtuale..."; + +/* Save VM overlay */ +"Importing %@…" = "Importazione di %@..."; + +/* VMDetailsView */ +"Inactive" = "Inattivo"; + +/* No comment provided by engineer. */ +"Information" = "Informazioni"; + +/* No comment provided by engineer. */ +"Initial Ramdisk" = "RAMDisk Iniziale"; + +/* No comment provided by engineer. */ +"Input" = "Input"; + +/* No comment provided by engineer. */ +"Interface" = "Interfaccia"; + +/* VMDisplayWindowController */ +"Install Windows Guest Tools…" = "Installa gli Strumenti Guest per Windows…"; + +/* No comment provided by engineer. */ +"Install Windows 10 or higher" = "Installa Windows 10 o successivo"; + +/* No comment provided by engineer. */ +"Install drivers and SPICE tools" = "Installa i Driver e gli Strumenti SPICE"; + +/* VMDisplayAppleWindowController */ +"Installation: %@" = "Installazione: %@"; + +/* No comment provided by engineer. */ +"Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "Inizializza il controller PS/2 anche quando l'input USB è supportato. Richiesto per versioni più vecchie di Windows."; + +/* UTMQemu */ +"Internal error has occurred." = "Si è verificato un errore interno."; + +/* UTMSpiceIO */ +"Internal error trying to connect to SPICE server." = "Si è verificato un errore interno durante la connessione al server SPICE."; + +/* UTMVirtualMachine */ +"Internal error starting main loop." = "Si è verificato un errore interno all'avvio del loop principale."; + +/* UTMVirtualMachine */ +"Internal error starting VM." = "Si è verificato un errore interno all'avvio della VM."; + +/* VMDisplayMetalWindowController */ +"Internal error." = "Errore interno."; + +/* ContentView */ +"Invalid JitStreamer attach URL:\n%@" = "Attach URL di JitStramer non valido: %@"; + +/* VMConfigAppleNetworkingView */ +"Invalid MAC address." = "MAC address non corretto."; + +/* VMConfigSystemViewController */ +"Invalid core count." = "Numero di core non valido."; + +/* UTMData */ +"Invalid drive size." = "Dimensione del disco non valida."; + +/* VMRemovableDrivesViewController */ +"Invalid file selected." = "File selezionato non valido."; + +/* VMConfigSystemViewController */ +"Invalid memory size." = "Dimensione della memoria non valida."; + +/* VMConfigDriveCreateViewController */ +"Invalid name" = "Nome non valido"; + +/* VMConfigDriveCreateViewController */ +"Invalid size" = "Dimensione non valida"; + +/* VMListViewController */ +"Invalid UTM not imported." = "UTM non valido non importato."; + +/* No comment provided by engineer. */ +"Invert Mouse Scroll" = "Inverti lo scorrimento del mouse"; + +/* No comment provided by engineer. */ +"Invert scrolling" = "Inverti lo scorrimento"; + +/* No comment provided by engineer. */ +"IP Configuration" = "Impostazioni IP"; + +/* No comment provided by engineer. */ +"Isolate Guest from Host" = "Isola Guest"; + +/* UTMQemuConstants */ +"Italic" = "Cursivo"; + +/* UTMQemuConstants */ +"Italic, Bold" = "Cursivo, Grassetto"; + +/* No comment provided by engineer. */ +"JIT Cache" = "Cache JIT"; + +/* VMConfigSystemViewController */ +"JIT cache size cannot be larger than 2GB." = "La dimensione della cache JIT non può essere superiore a 2GB."; + +/* VMConfigSystemViewController */ +"JIT cache size too small." = "Dimensione della cache JIT insufficiente."; + +/* No comment provided by engineer. */ +"Kernel" = "Kernel"; + +/* No comment provided by engineer. */ +"Keyboard" = "Tastiera"; + +/* No comment provided by engineer. */ +"Keep UTM running after last window is closed and all VMs are shut down" = "Esegui UTM anche quando tutte le fineste sono chiuse e nessuna VM è in esecuzione"; + +/* No comment provided by engineer. */ +"Legacy" = "Legacy"; + +/* No comment provided by engineer. */ +"Legacy (PS/2) Mode" = "Modalità Legacy (PS/2)"; + +/* No comment provided by engineer. */ +"License" = "Licenza"; + +/* UTMQemuConstants */ +"Linear" = "Lineare"; + +/* UTMAppleConfigurationBoot */ +"Linux" = "Linux"; + +/* UTMLegacyQemuConfiguration + UTMQemuConstants */ +"Linux Device Tree Binary" = "Albero Binario dei Dispositivi Linux"; + +/* No comment provided by engineer. */ +"Linux initial ramdisk:" = "RAMDisk iniziale di Linux:"; + +/* No comment provided by engineer. */ +"Linux initial ramdisk (optional)" = "RAMDisk iniziale di Linux (opzionale)"; + +/* UTMLegacyQemuConfiguration + UTMQemuConstants */ +"Linux Kernel" = "Kernel Linux"; + +/* No comment provided by engineer. */ +"Linux kernel (required)" = "Kernel Linux (richiesto)"; + +/* UTMLegacyQemuConfiguration + UTMQemuConstants */ +"Linux RAM Disk" = "RAMDisk di Linux"; + +/* No comment provided by engineer. */ +"Linux Root FS Image (optional)" = "Immagine del File System Root di Linux"; + +/* No comment provided by engineer. */ +"Linux Settings" = "Impostazioni Linux"; + +/* No comment provided by engineer. */ +"Logging" = "Logging"; + +/* No comment provided by engineer. */ +"MAC Address" = "MAC Address"; + +/* No comment provided by engineer. */ +"Machine" = "Macchina"; + +/* UTMAppleConfigurationBoot */ +"macOS" = "macOS"; + +/* VMWizardOSMacView */ +"macOS guests are only supported on ARM64 devices." = "I guest macOS sono supportati solo dai dispositivi ARM64"; + +/* VMWizardState */ +"macOS is not supported with QEMU." = "macOS non è supportato da QEMU."; + +/* No comment provided by engineer. */ +"macOS Settings" = "Impostazioni macOS"; + +/* UTMQemuManager */ +"Manager being deallocated, killing pending RPC." = "Manager in deallocazione, arresto degli RPC in attesa."; + +/* UTMQemuConstants */ +"Manual Serial Device (advanced)" = "Dispositivo Seriale Manuale (avanzate)"; + +/* No comment provided by engineer. */ +"Maximum Shared USB Devices" = "Numero Massimo di Dispositivi USB Condivisi"; + +/* No comment provided by engineer. */ +"MB" = "MB"; + +/* No comment provided by engineer. */ +"Memory" = "Memoria"; + +/* VMDisplayMetalWindowController */ +"Metal is not supported on this device. Cannot render display." = "Metal non è supportato da questo dispositivo. Impossibile mostrare il display."; + +/* No comment provided by engineer. */ +"Minimum size: %@" = "Dimensione Minima: %@"; + +/* No comment provided by engineer. */ +"Mode" = "Modalità"; + +/* No comment provided by engineer. */ +"Modify settings for this VM." = "Modifica le impostazioni di questa VM"; + +/* UTMAppleConfigurationDevices */ +"Mouse" = "Mouse"; + +/* No comment provided by engineer. */ +"Mouse Wheel" = "Rotella del Mouse"; + +/* No comment provided by engineer. */ +"Move…" = "Sposta..."; + +/* No comment provided by engineer. */ +"Move this VM from internal storage to elsewhere." = "Sposta questa VM dalla memoria di archiviazione interna ad altrove."; + +/* No comment provided by engineer. */ +"Move Up" = "Su"; + +/* No comment provided by engineer. */ +"Move Down" = "Giù"; + +/* No comment provided by engineer. */ +"Move selected VM" = "Sposta la VM selezionata"; + +/* Save VM overlay */ +"Moving %@…" = "Spostamento di %@..."; + +/* UTMQemuConstants */ +"MTD (NAND/NOR)" = "MTD (NAND/NOR)"; + +/* No comment provided by engineer. */ +"Name" = "Nome"; + +/* VMConfigInfoView */ +"Name is an invalid filename." = "Il nome non è un nome di file valido."; + +/* UTMQemuConstants */ +"Nearest Neighbor" = "Nearest Neighbor"; + +/* No comment provided by engineer. */ +"Network" = "Rete"; + +/* No comment provided by engineer. */ +"Network Mode" = "Modalità di Rete"; + +/* No comment provided by engineer. */ +"New" = "Nuova"; + +/* No comment provided by engineer. */ +"New…" = "Nuova..."; + +/* No comment provided by engineer. */ +"New Drive" = "Nuovo Disco"; + +/* No comment provided by engineer. */ +"New from template…" = "Nuovo da template..."; + +/* VMConfigPortForwardingViewController */ +"New port forward" = "Nuovo Inoltro di Porta"; + +/* No comment provided by engineer. */ +"New Virtual Machine" = "Nuova Macchina Virtuale (VM)"; + +/* No comment provided by engineer. */ +"New VM" = "Nuova VM"; + +/* Clone VM name prompt message */ +"New VM name" = "Nuovo nome della VM"; + +/* No comment provided by engineer. */ +"No" = "No"; + +/* UTMQemuManager */ +"No connection for RPC." = "Nessuna connessione per RPC."; + +/* VMConfigExistingViewController */ +"No debug log found!" = "Nessun messaggio di debug trovato!"; + +/* No comment provided by engineer. */ +"No drives added." = "Nessun disco aggiunto."; + +/* VMDisplayWindowController */ +"No drives connected." = "Nessun disco connesso."; + +/* UTMDownloadSupportToolsTaskError */ +"No empty removable drive found. Make sure you have at least one removable drive that is not in use." = "Nessun disco rimovibile trovato. Assicurati di avere almeno un disco rimovibile attualmente non in uso"; + +/* UTMData */ +"No log found!" = "Nessun messaggio di log trovato!"; + +/* No comment provided by engineer. */ +"No output device is selected for this window." = "Nessun dispositivo di output selezionato per questa finestra."; + +/* VMQemuDisplayMetalWindowController */ +"No USB devices detected." = "Nessun dispositivo USB rilevato"; + +/* VMToolbarDriveMenuView */ +"none" = "nessuno"; + +/* UTMLegacyQemuConfiguration + UTMQemuConstants */ +"None" = "Nessuno"; + +/* UTMQemuConstants */ +"None (Advanced)" = "Nessuno (Avanzato)"; + +/* No comment provided by engineer. */ +"Not running" = "Non in essecuzione"; + +/* No comment provided by engineer. */ +"Note: Boot order is as listed." = "Nota: La priorità di avvio è come indicata."; + +/* No comment provided by engineer. */ +"Note: select the path to share from the main screen." = "Nota: seleziona il percorso da condividere dall'interfaccia principale."; + +/* No comment provided by engineer. */ +"Note: Shared directories will not be saved and will be reset when UTM quits." = "Nota: Le cartelle condivise non saranno salvate e verranno svuotate all'uscita da UTM."; + +/* No comment provided by engineer. */ +"Notes" = "Note"; + +/* UTMQemuConstants */ +"NVMe" = "NVMe"; + +/* VMDisplayWindowController */ +"OK" = "OK"; + +/* No comment provided by engineer. */ +"Only available if host architecture matches the target. Otherwise, TCG emulation is used." = "Disponibile solo se l'architettura dell'host combacia con la destinazione. In alternativa, l'emulazione TCG verrà utilizzata."; + +/* No comment provided by engineer. */ +"Open VM Settings" = "Apri la configurazione della VM"; + +/* No comment provided by engineer. */ +"Open…" = "Apri..."; + +/* No comment provided by engineer. */ +"Operating System" = "Sistema operativo"; + +/* No comment provided by engineer. */ +"Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details." = "Se vuoi, puoi selezionare una cartella da rendere accessibile all'interno della VM. Il supporto alle cartelle condivise varia in base al sistema operativo e potrebbe essere necessario installare driver sulla VM. Controlla le pagine di supporto di UTM per ulteriori informazioni."; + +/* No comment provided by engineer. */ +"Other" = "Altro"; + +/* No comment provided by engineer. */ +"Path" = "Percorso"; + +/* VMDisplayWindowController */ +"Pause" = "Pausa"; + +/* UTMVirtualMachine */ +"Paused" = "In Pausa"; + +/* UTMVirtualMachine */ +"Pausing" = "In Pausa..."; + +/* UTMQemuConstants */ +"PC System Flash" = "Flash del Sistema PC"; + +/* No comment provided by engineer. */ +"Pending" = "In attesa"; + +/* VMDisplayWindowController */ +"Play" = "Avvia"; + +/* VMWizardState */ +"Please select a boot image." = "Seleziona l'immagine di avvio."; + +/* VMWizardState */ +"Please select a kernel file." = "Seleziona un file del kernel."; + +/* No comment provided by engineer. */ +"Please select a macOS recovery IPSW." = "Seleziona un archivo IPSW di recupero di macOS."; + +/* VMWizardState */ +"Please select a system to emulate." = "Seleziona un sistema da emulare."; + +/* No comment provided by engineer. */ +"Please select an uncompressed Linux kernel image." = "Seleziona un'immagine non compressa del kernel Linux."; + +/* No comment provided by engineer. */ +"Port Forward" = "Inoltro di Porta"; + +/* UTMJSONStream */ +"Port is not connected." = "Porta non connessa"; + +/* No comment provided by engineer. */ +"Power Off" = "Spegni"; + +/* No comment provided by engineer. */ +"Preconfigured" = "Preconfigurato"; + +/* No comment provided by engineer. */ +"Protocol" = "Protocollo"; + +/* A download process is about to begin. */ +"Preparing…" = "Preparazione…"; + +/* VMDisplayQemuMetalWindowController */ +"Press %@ to release cursor" = "Premi %@ per rilasciare il cursore"; + +/* UTMQemuConstants */ +"Pseudo-TTY Device" = "Dispositivo pseudo-TTY"; + +/* No comment provided by engineer. */ +"PS/2 has higher compatibility with older operating systems but does not support custom cursor settings." = "PS/2 ha una compatibilità superiore con i sistemi operativi più vecchi, ma non supporta le impostazioni specifiche del cursore."; + +/* No comment provided by engineer. */ +"QEMU" = "QEMU"; + +/* No comment provided by engineer. */ +"QEMU Arguments" = "Argomenti di QEMU"; + +/* UTMQemuVirtualMachine */ +"QEMU exited from an error: %@" = "QEMU si è interrotto a causa di un errore: %@"; + +/* No comment provided by engineer. */ +"QEMU Machine Properties" = "Proprietà della macchina QEMU"; + +/* UTMQemuConstants */ +"QEMU Monitor (HMP)" = "Monitor QEMU (HMP)"; + +/* VMDisplayWindowController */ +"Querying drives status..." = "Verifica dello stato dei dischi…"; + +/* VMQemuDisplayMetalWindowController */ +"Querying USB devices..." = "Verifica dello stato dei dispositivi USB…"; + +/* No comment provided by engineer. */ +"Quit" = "Esci"; + +/* VMQemuDisplayMetalWindowController */ +"Quitting UTM will kill all running VMs." = "L'uscita da UTM causerà l'interruzione di tutte le VM in esecuzione."; + +/* No comment provided by engineer. */ +"RAM" = "RAM"; + +/* No comment provided by engineer. */ +"Random" = "Casuale"; + +/* No comment provided by engineer. */ +"Raw Image" = "Immagine Raw"; + +/* VMDisplayAppleController */ +"Read Only" = "Sola Lettura"; + +/* No comment provided by engineer. */ +"Reclaim" = "Recupera"; + +/* No comment provided by engineer. */ +"Reclaim Space" = "Recupera Spazio"; + +/* No comment provided by engineer. */ +"Reclaim disk space by re-converting the disk image." = "Recupera spazio su disco ri-convertendo l'immagine disco."; + +/* UTMQemuConstants */ +"Regular" = "Normale"; + +/* No comment provided by engineer. */ +"Removable" = "Rimovibile"; + +/* No comment provided by engineer. */ +"Removable Drive" = "Disco Rimovibile"; + +/* No comment provided by engineer. */ +"Remove" = "Elimina"; + +/* VMDisplayAppleController */ +"Remove…" = "Elimina…"; + +/* VMDisplayQemuDisplayController */ +"Request power down" = "Richiedi Spegnimento"; + +/* No comment provided by engineer. */ +"Requires SPICE guest agent tools to be installed." = "Richiede che gli strumenti guest di SPICE siano installati."; + +/* No comment provided by engineer. */ +"Requires SPICE guest agent tools to be installed. Retina Mode is recommended only if the guest OS supports HiDPI." = "Richiede che gli strumenti guest di SPICE siano installati. La Modalità Retina è raccomandata solo sui sistemi operativi che supportano HiDPI."; + +/* No comment provided by engineer. */ +"Requires SPICE WebDAV service to be installed." = "Richiede l'installazione del servizio WebDAV di SPICE."; + +/* No comment provided by engineer. */ +"Reset" = "Reset"; + +/* No comment provided by engineer. */ +"Resize" = "Ridimensiona"; + +/* No comment provided by engineer. */ +"Resize Console Command" = "Ridimensiona i Comandi della Console"; + +/* No comment provided by engineer. */ +"Resize display to screen size and orientation automatically" = "Ridimensiona e orienta il display in base alle dimensioni dello schermo."; + +/* No comment provided by engineer. */ +"Resize display to window size automatically" = "Ridimensiona e orienta il display in base alla finestra"; + +/* No comment provided by engineer. */ +"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "Il ridimensionamento è una funzionalità sperimentale e potrebbe causare perdita di dati. Sei incoraggiato ad effettuare un backup di questa VM prima di procedere. Ridimensionare a %@ GiB?"; + +/* No comment provided by engineer. */ +"Resolution" = "Risoluzione"; + +/* No comment provided by engineer. */ +"Restart" = "Ricomincia"; + +/* UTMVirtualMachine */ +"Resuming" = "Ripresa"; + +/* No comment provided by engineer. */ +"Retina Mode" = "Modalità Retina"; + +/* No comment provided by engineer. */ +"Reveal where the VM is stored." = "Mostra la posizione della VM nel file system."; + +/* UTMAppleConfiguration */ +"Rosetta is not supported on the current host machine." = "Rosetta non è supportato dalla macchina host attuale."; + +/* No comment provided by engineer. */ +"Root Image" = "Radice Immagine"; + +/* No comment provided by engineer. */ +"RNG Device" = "Dispositivo RNG"; + +/* No comment provided by engineer. */ +"Run" = "Avvia"; + +/* No comment provided by engineer. */ +"Run without saving changes" = "Avvia senza applicare le modifiche"; + +/* No comment provided by engineer. */ +"Run Recovery" = "Avvia Ripristino"; + +/* No comment provided by engineer. */ +"Run selected VM" = "Avvia VM selezionata"; + +/* No comment provided by engineer. */ +"Run the VM in the foreground." = "Avvia la VM in primo piano"; + +/* No comment provided by engineer. */ +"Run the VM in the foreground, without saving data changes to disk." = "Avvia la VM in primo piano, senza applicare le modifiche su disco."; + +/* No comment provided by engineer. */ +"Running" = "In esecuzione"; + +/* No comment provided by engineer. */ +"Running low on memory! UTM might soon be killed by iOS. You can prevent this by decreasing the amount of memory and/or JIT cache assigned to this VM" = "Poca memoria a disposizione! UTM potrebbe presto essere interrotto da iOS. Puoi prevenire che questo accada diminuendo la quantità di memoria e/o della cache JIT di questa VM"; + +/* No comment provided by engineer. */ +"Save" = "Salva"; + +/* Save VM overlay */ +"Saving %@…" = "Salvataggio di %@..."; + +/* No comment provided by engineer. */ +"Scaling" = "Scala"; + +/* No comment provided by engineer. */ +"Selected:" = "Selezionato:"; + +/* No comment provided by engineer. */ +"Set to 0 for default which is 1/4 of the allocated Memory size. This is in addition to the host memory!" = "Impostato al valore predefinito di 0, che corrisponde a un quarto della memoria allocata. Questa si aggiunge alla quantità di memoria utilizzata!"; + +/* No comment provided by engineer. */ +"Set to 0 to use maximum supported CPUs. Force multicore might result in incorrect emulation." = "Imposta a 0 per utilizzare il numero massimo di CPU supportate. Forzare l'uso di multicore potrebbe risultare in emulazione scorretta."; + +/* UTMQemuConstants */ +"SCSI" = "SCSI"; + +/* UTMQemuConstants */ +"SD Card" = "Scheda SD"; + +/* No comment provided by engineer. */ +"Select a file." = "Seleziona un file."; + +/* VMDisplayWindowController */ +"Select Drive Image" = "Seleziona un'Immagine Disco"; + +/* VMDisplayAppleWindowController + VMDisplayWindowController */ +"Select Shared Folder" = "Seleziona una Cartella Condivisa"; + +/* SavePanel */ +"Select where to export QEMU command:" = "Seleziona dove esportare il comando QEMU:"; + +/* SavePanel */ +"Select where to save debug log:" = "Seleziona dove salvare i messaggi di log:"; + +/* SavePanel */ +"Select where to save UTM Virtual Machine:" = "Seleziona dove salvare la Macchina Virtuale UTM:"; + +/* No comment provided by engineer. */ +"Selected:" = "Selezionato:"; + +/* VMDisplayQemuDisplayController */ +"Sends power down request to the guest. This simulates pressing the power button on a PC." = "Invia la richiesta di spegnimento al Guest. Questo simula la pressione del tasto di accensione su un PC."; + +/* No comment provided by engineer. */ +"Serial" = "Seriale"; + +/* VMDisplayAppleWindowController + VMDisplayQemuDisplayController */ +"Serial %lld" = "Seriale %lld"; + +/* No comment provided by engineer. */ +"Server Address" = "Indirizzo del Server"; + +/* No comment provided by engineer. */ +"Settings" = "Impostazioni"; + +/* Share context menu */ +"Share" = "Condividi"; + +/* Share context menu */ +"Share…" = "Condividi..."; + +/* No comment provided by engineer. */ +"Share a copy of this VM and all its data." = "Condividi una copia di questa VM e di tutti i suoi dati."; + +/* No comment provided by engineer. */ +"Share Directory" = "Condividi Cartella"; + +/* No comment provided by engineer. */ +"Share is read only" = "Condivisione in sola lettura"; + +/* No comment provided by engineer. */ +"Share USB devices from host" = "Condividi dispositivi USB dall'host"; + +/* No comment provided by engineer. */ +"Shared Directory" = "Cartella Condivisa"; + +/* VMConfigAppleSharingView */ +"Shared directories in macOS VMs are only available in macOS 13 and later." = "Le cartelle condivise nella VM macOS sono disponibili da macOS 13 alle versioni successive."; + +/* UTMQemuConstants */ +"Shared Network" = "Rete Condivisa"; + +/* VMConfigSharingViewController */ +"Shared path has moved. Please re-choose." = "Il percorso condiviso è cambiato. Sceglierne uno nuovo."; + +/* VMConfigSharingViewController */ +"Shared path is no longer valid. Please re-choose." = "Il percorso condiviso non è più valido. Sceglierne uno nuovo."; + +/* No comment provided by engineer. */ +"Share selected VM" = "Condividi la VM selezionata"; + +/* No comment provided by engineer. */ +"Sharing" = "Condivisione"; + +/* No comment provided by engineer. */ +"Show Advanced Settings" = "Mostra Impostazioni Avanzate"; + +/* No comment provided by engineer. */ +"Show All…" = "Mostra Tutto..."; + +/* No comment provided by engineer. */ +"Show in Finder" = "Mostra nel Finder"; + +/* No comment provided by engineer. */ +"Should be off for older operating systems such as Windows 7 or lower." = "Dovrebbe essere disattivato per sistemi operativi come Windows 7 o precedenti."; + +/* No comment provided by engineer. */ +"Should be on always unless the guest cannot boot because of this." = "Dovrebbe essere attivo, a meno che il sistema operativo Guest abbia problemi di avvio a causa di questa opzione."; + +/* No comment provided by engineer. */ +"Size" = "Dimensione"; + +/* No comment provided by engineer. */ +"Skip Boot Image" = "Salta Immagine di Avvio"; + +/* New VM window. */ +"Skip ISO boot" = "Salta Avvio da ISO"; + +/* No comment provided by engineer. */ +"Skip ISO boot (advanced)" = "Salta Avvio da ISO (Avanzato)"; + +/* No comment provided by engineer. */ +"Slower, but can run other CPU architectures." = "Più lento, ma in grado di eseguire più architetture di CPU."; + +/* No comment provided by engineer. */ +"Sound" = "Audio"; + +/* New VM window. */ +"Some older systems do not support UEFI boot, such as Windows 7 and below." = "Alcuni sistemi non supportano l'avvio UEFI, come Windows 7 e precedenti."; + +/* No comment provided by engineer. */ +"Specify the size of the drive where data will be stored into." = "Specifica la dimensione del disco dove salvare i dati."; + +/* UTMQemuConstants */ +"SPICE WebDAV" = "SPICE WebDAV"; + +/* No comment provided by engineer. */ +"Start" = "Inizia"; + +/* UTMVirtualMachine */ +"Started" = "Avviata"; + +/* UTMVirtualMachine */ +"Starting" = "In Avvio"; + +/* No comment provided by engineer. */ +"Stop" = "Arresta"; + +/* No comment provided by engineer. */ +"Stop the running VM." = "Arresta la VM in esecuzione."; + +/* No comment provided by engineer. */ +"Stop selected VM" = "Arresta la VM selezionata."; + +/* No comment provided by engineer. */ +"Stop…" = "Arresta..."; + +/* UTMVirtualMachine */ +"Stopped" = "Arrestata"; + +/* UTMVirtualMachine */ +"Stopping" = "In Arresto"; + +/* No comment provided by engineer. */ +"Storage" = "Archiviazione"; + +/* No comment provided by engineer. */ +"stty cols $COLS rows $ROWS\n" = "stty cols $COLS rows $ROWS\n"; + +/* No comment provided by engineer. */ +"Style" = "Stile"; + +/* No comment provided by engineer. */ +"Summary" = "Sommario"; + +/* Welcome view */ +"Support" = "Supporto"; + +/* UTMVirtualMachine */ +"Suspended" = "Sospesa"; + +/* No comment provided by engineer. */ +"Status" = "Stato"; + +/* No comment provided by engineer. */ +"System" = "Sistema"; + +/* No comment provided by engineer. */ +"Target" = "Destinazione"; + +/* UTMQemuConstants */ +"TCP" = "TCP"; + +/* UTMQemuConstants */ +"TCP Client Connection" = "Connessione del Client TCP"; + +/* VMConfigPortForwardingViewController */ +"TCP Forward" = "Inoltro TCP"; + +/* UTMQemuConstants */ +"TCP Server Connection" = "Connessione del Server TCP"; + +/* No comment provided by engineer. */ +"Test" = "Test"; + +/* No comment provided by engineer. */ +"Text Color" = "Colore del Testo"; + +/* VMDisplayQemuDisplayController */ +"Tells the VM process to shut down with risk of data corruption. This simulates holding down the power button on a PC." = "Informa il processo della VM di arrestarsi con il rischio di compromettere dei dati. Questo simula la pressione a lungo del tasto di accesione di un PC"; + +/* SizeTextField */ +"The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "La quantità di spazio da allocare per questa immagine. Ignorato se l'immagine viene importata. Se questa è un'immagine raw, un file vuoto con questa dimensione sarà salvato con la VM. In alternativa, l'immagine disco si espanderà fino alla dimensione espressa qui."; + +/* SizeTextField */ +"The amount of storage to allocate for this image. An empty file of this size will be stored with the VM." = "La quantità di spazio da allocare per questa immagine. Un file vuoto di questa dimensione verrà salvato con la VM."; + +/* UTMConfiguration */ +"The backend for this configuration is not supported." = "Il backend scelto per questa configurazione non è supportato."; + +/* UTMConfiguration */ +"The drive '%@' already exists and cannot be created." = "Il disco “%@” esiste già e non può essere creato."; + +/* UTMDownloadSupportToolsTaskError */ +"The guest support tools have already been mounted." = "Gli strumenti di supporto del guest sono stati già montati."; + +/* UTMAppleConfiguration */ +"The host operating system needs to be updated to support one or more features requested by the guest." = "Il sistema operativo host deve essere aggiornato per supportare una o più funzionalità richieste dal guest."; + +/* No comment provided by engineer. */ +"The selected architecture is unsupported in this version of UTM." = "L'architettura selezionata non è supportata da questa versione di UTM."; + +/* VMWizardState */ +"The selected boot image contains the word '%@' but the guest architecture is '%@'. Please ensure you have selected an image that is compatible with '%@'." = "L'immagine di avvio contiene la parola “%1$@“, mentre l'architettura guest è “%2$@“. Assicurati di aver selezionato un'immagine compatibile con “%3$@“."; + +/* VMConfigSystemViewController */ +"The total memory usage is close to your device's limit. iOS will kill the VM if it consumes too much memory." = "L'uso di memoria attuale è vicino al limite massimo del tuo dispositivo, iOS terminerà la VM se consuma troppa memoria."; + +/* No comment provided by engineer. */ +"The target does not support hardware emulated serial connections." = "La destinazione non supporta le connessioni seriali emulate."; + +/* UTMQemuVirtualMachine */ +"The virtual machine is in an invalid state." = "La macchina virtuale è in uno stato non valido."; + +/* No comment provided by engineer. */ +"Theme" = "Tema"; + +/* Error shown when importing a ZIP file from web that doesn't contain a UTM Virtual Machine. */ +"There is no UTM file in the downloaded ZIP archive." = "Non è presente un file UTM nell'archivio ZIP scaricato."; + +/* No comment provided by engineer. */ +"These are advanced settings affecting QEMU which should be kept default unless you are running into issues." = "Queste sono configurazioni avanzate che hanno impatto su QEMU e dovrebbero essere lasciate alle impostazioni predefinite a meno che non causino problemi."; + +/* No comment provided by engineer. */ +"These settings are unavailable in console display mode." = "Queste impostazioni non sono disponibili nella modalità console"; + +/* No comment provided by engineer. */ +"This build does not emulation." = "Questa build non supporta l'emulazione."; + +/* UTMQemuVirtualMachine */ +"This build of UTM does not support emulating the architecture of this VM." = "Questa build di UTM non supporta l'emulazione dell'architettura di questa VM."; + +/* VMConfigSystemView */ +"This change will reset all settings" = "Questa modifica riporterà alle impostazioni predefinite"; + +/* UTMConfiguration */ +"This configuration is saved with a newer version of UTM and is not compatible with this version." = "Questa configurazione è stata salvata con una versione più recente di UTM e non è compatibile con la versione attuale"; + +/* UTMConfiguration */ +"This configuration is too old and is not supported." = "Questa configurazione proviene da una versione precedente non più supportata."; + +/* VMConfigAppleSharingView */ +"This directory is already being shared." = "Questa cartella è già condivisa."; + +/* UTMQemuSystem */ +"This version of macOS does not support audio in console mode. Please change the VM configuration or upgrade macOS." = "Questa versione di macOS non supporta l'audio in modalità console. Cambia la configurazione della VM o aggiorna macOS."; + +/* UTMQemuSystem */ +"This version of macOS does not support GPU acceleration. Please change the VM configuration or upgrade macOS." = "Questa versione di macOS non supporta l'accelerazione della GPU. Cambia la configurazione della VM o aggiorna macOS."; + +/* No comment provided by engineer. */ +"This is appended to the -machine argument." = "Aggiunto all'argomento -machine"; + +/* UTMAppleConfiguration */ +"This is not a valid Apple Virtualization configuration." = "Questa non è una configurazione valida per la Virtualizzazione Apple."; + +/* VMDisplayWindowController */ +"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "Questo potrebbe compromettere la VM e ogni modifica non salvata verrà persa. Per uscire in sicurezza, spegni la macchina dal sistema guest."; + +/* No comment provided by engineer. */ +"This operating system is unsupported on your machine." = "Questo sistema operativo non è supportato dalla tua macchina."; + +/* UTMDataExtension */ +"This virtual machine cannot be run on this machine." = "Questo macchina virtuale non è supportato dalla tua macchina."; + +/* UTMAppleConfiguration */ +"This virtual machine cannot run on the current host machine." = "Questo macchina virtuale non è supportato dalla tua macchina host attuale."; + +/* UTMAppleConfiguration */ +"This virtual machine contains an invalid hardware model. The configuration may be corrupted or is outdated." = "Questa macchina virtuale contiene un modello hardware non valido. La configurazione potrebbe essere non aggiornata o non corretta."; + +/* No comment provided by engineer. */ +"This virtual machine has been removed." = "Questa macchina virtuale è stata eliminata."; + +/* VMDisplayWindowController */ +"This will reset the VM and any unsaved state will be lost." = "Questo riavvierà la VM e ogni modifica non salvata sarà persa."; + +/* UTMQemuManager */ +"Timed out waiting for RPC." = "Timeout di RPC."; + +/* VMDisplayAppleWindowController */ +"To access the shared directory, the guest OS must have Virtiofs drivers installed. You can then run `sudo mount -t virtiofs share /path/to/share` to mount to the share path." = "Per accedere alla cartella condivisa, il sistema operativo guest necessita dei driver Virtiofs. Succesivamente, puoi eseguire `sudo mount -t virtiofs share /path/to/share` per montare la cartella condivisa."; + +/* VMMetalView */ +"To capture input or to release the capture, press Command and Option at the same time." = "Per catturare o rilasciare la cattura dell'input, premi Command e Option contemporaneamente."; + +/* No comment provided by engineer. */ +"To install macOS, you need to download a recovery IPSW. If you do not select an existing IPSW, the latest macOS IPSW will be downloaded from Apple." = "Per installare macOS, avrai bisogno di scaricare un file IPSW per il ripristino. Se non selezioni un file IPSW esistente, verrà scaricata da Apple l'ultima versione del file di ripristino IPSW di macOS."; + +/* VMDisplayQemuMetalWindowController */ +"To release the mouse cursor, press %@ at the same time." = "Per rilasciare il cursore, premi %@ contemporaneamente."; + +/* UTMAppleConfigurationDevices */ +"Trackpad" = "Trackpad"; + +/* No comment provided by engineer. */ +"Tweaks" = "Ritocchi"; + +/* No comment provided by engineer. */ +"Type" = "Tipo"; + +/* UTMQemuConstants */ +"UDP" = "UDP"; + +/* VMConfigPortForwardingViewController */ +"UDP Forward" = "Inoltro UDP"; + +/* No comment provided by engineer. */ +"UEFI" = "UEFI"; + +/* No comment provided by engineer. */ +"UEFI Boot" = "Avvio UEFI"; + +/* UTMQemuConfigurationError */ +"UEFI is not supported with this architecture." = "L'avvio UEFI non è supportato da questa architettura."; + +/* UTMData */ +"Unable to add a shortcut to the new location." = "Impossibile aggiungere una scorciatoia alla nuova posizione."; + +/* UTMUnavailableVirtualMachine */ +"Unavailable" = "Non disponibile"; + +/* VMWizardState */ +"Unavailable for this platform." = "Non disponibile per questa piattaforma."; + +/* No comment provided by engineer. */ +"Uncompressed Linux initial ramdisk (optional)" = "RAMDisk iniziale di Linux non compressa (opzionale))"; + +/* No comment provided by engineer. */ +"Uncompressed Linux kernel (required)" = "Kernel du Linux non compresso (richiesto)"; + +/* UTMVirtualMachineExtension */ +"Unknown" = "Sconosciuto"; + +/* No comment provided by engineer. */ +"Upscaling" = "Upscaling"; + +/* UTMQemuConstants */ +"USB" = "USB"; + +/* UTMQemuConstants */ +"USB 2.0" = "USB 2.0"; + +/* UTMQemuConstants */ +"USB 3.0 (XHCI)" = "USB 3.0 (XHCI)"; + +/* VMQemuDisplayMetalWindowController */ +"USB Device" = "Dispositivo USB"; + +/* No comment provided by engineer. */ +"USB Sharing" = "Condivisione USB"; + +/* No comment provided by engineer. */ +"USB sharing not supported in this build of UTM." = "La condivisione USB non è supportata da questa versione di UTM."; + +/* No comment provided by engineer. */ +"USB Support" = "Supporto USB"; + +/* No comment provided by engineer. */ +"Use Apple Virtualization" = "Usa la Virtualizzazione di Apple"; + +/* No comment provided by engineer. */ +"Use Command+Option (⌘+⌥) for input capture/release" = "Usa Command+Option (⌘+⌥) per catturare/rilasciare l'input"; + +/* No comment provided by engineer. */ +"Use Hypervisor" = "Usa Hypervisor"; + +/* No comment provided by engineer. */ +"Use local time for base clock" = "Usa l'ora locale locale per l'orologio di sistema"; + +/* No comment provided by engineer. */ +"Use Virtualization" = "Usa la Virtualizzazione"; + +/* Welcome view */ +"User Guide" = "Guida Utente"; + +/* No comment provided by engineer. */ +"VGA Device RAM (MB)" = "RAM del dispositivo VGA (MB)"; + +/* UTMQemuConstants */ +"VirtFS" = "VirtFS"; + +/* UTMQemuConstants */ +"VirtIO" = "VirtIO"; + +/* UTMConfigurationInfo + UTMData */ +"Virtual Machine" = "Macchina Virtuale"; + +/* No comment provided by engineer. */ +"Virtual Machine Gallery" = "Libreria di VM (UTM Gallery)"; + +/* New VM window. */ +"Virtualization Engine" = "Motore di Virtualizzazione"; + +/* No comment provided by engineer. */ +"Virtualization is not supported on your system." = "La virtualizzazione non è supportata sul tuo sistema."; + +/* No comment provided by engineer. */ +"Virtualize" = "Virtualizza"; + +/* No comment provided by engineer. */ +"VM display size is fixed" = "La dimensione del display della VM è costante"; + +/* UTMVirtualMachine+Sharing */ +"VM frontend does not support shared directories." = "Il frontend della VM non supporta le cartelle condivise."; + +/* No comment provided by engineer. */ +"Waiting for VM to connect to display..." = "In attesa che la VM si colleghi al display…"; + +/* No comment provided by engineer. */ +"Welcome to UTM" = "Benvenuto a UTM"; + +/* No comment provided by engineer. */ +"WebDAV requires installing SPICE daemon. VirtFS requires installing device drivers." = "WebDAV richiede l'installazione del demone di sistema SPICE. VirtFS richiede l'installazione di driver."; + +/* No comment provided by engineer. */ +"Windows" = "Windows"; + +/* No comment provided by engineer. */ +"Wait for Connection" = "Attesa della Connessione"; + +/* UTMDownloadSupportToolsTask */ +"Windows Guest Support Tools" = "Strumenti di Supporto Guest per Windows"; + +/* VMQemuDisplayMetalWindowController */ +"Would you like to connect '%@' to this virtual machine?" = "Vuoi collegare '%@' a questa macchina virtuale?"; + +/* VMDisplayAppleWindowController */ +"Would you like to install macOS? If an existing operating system is already installed on the primary drive of this VM, then it will be erased." = "Vuoi installare macOS? Se un sistema operativo è già presente sul disco primario di questa VM, verrà eliminato."; + +/* No comment provided by engineer. */ +"Would you like to re-convert this disk image to reclaim unused space and apply compression? Note this will require enough temporary space to perform the conversion. Compression only applies to existing data and new data will still be written uncompressed. You are strongly encouraged to back-up this VM before proceeding." = "Vuoi ri-convertire questa immagine disco per recuperare spazio ed applicare la compressione? Nota che sarà necessaria la disponibilità di spazio su disco sufficiente per effettuare la conversione. La compressione si applica solo ai dati esistenti e i nuovi dati non verranno compressi. È fortemente consigliato fare un backup di questa VM prima di procedere."; + +/* No comment provided by engineer. */ +"Would you like to re-convert this disk image to reclaim unused space? Note this will require enough temporary space to perform the conversion. You are strongly encouraged to back-up this VM before proceeding." = "Vuoi ri-convertire questa immagine disco per recuperare spazio ed applicare la compressione? Nota che sarà necessaria la disponibilità di spazio su disco sufficiente per effettuare la conversione. È fortemente consigliato fare un backup di questa VM prima di procedere."; + +/* No comment provided by engineer. */ +"Yes" = "Sì"; + +/* VMConfigSystemView */ +"Your device has %llu MB of memory and the estimated usage is %llu MB." = "Il tuo dispositivo ha %1$llu MB di memoria e l'uso stimato è di %2$llu MB."; + +/* VMConfigAppleBootView + VMWizardOSMacView */ +"Your machine does not support running this IPSW." = "La tua macchina non supporta l'esecuzione di questo IPSW."; + +/* ContentView */ +"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." = "La tua versione di iOS non supporta l'esecuzione di VM senza modifiche. Dovrai eseguire UTM con debugger remoto o con un dispositivo jailbroken. Consulta https://getutm.app/install/ per maggiori informazioni."; + +/* No comment provided by engineer. */ +"Zoom" = "Zoom"; + +/* No comment provided by engineer. */ +"Maintenance" = "Manutenzione"; + +/* No comment provided by engineer. */ +"Reset UEFI Variables" = "Reimposta le Variabili UEFI"; + +/* No comment provided by engineer. */ +"Options here only apply on next boot and are not saved." = "Queste impostazioni vengo applicate solo all'avvio successivo e non sono salvate."; + +/* No comment provided by engineer. */ +"Create a new VM" = "Crea una nuova VM"; diff --git a/Platform/it.lproj/Localizable.stringsdict b/Platform/it.lproj/Localizable.stringsdict new file mode 100644 index 000000000..ee274d301 --- /dev/null +++ b/Platform/it.lproj/Localizable.stringsdict @@ -0,0 +1,22 @@ +<?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>%lld Cores</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@cores@</string> + <key>cores</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>lld</string> + <key>one</key> + <string>%lld Core</string> + <key>other</key> + <string>%lld Core</string> + </dict> + </dict> +</dict> +</plist> diff --git a/Platform/ja.lproj/Localizable.strings b/Platform/ja.lproj/Localizable.strings index 9d65d1105..ef338056a 100644 --- a/Platform/ja.lproj/Localizable.strings +++ b/Platform/ja.lproj/Localizable.strings @@ -48,8 +48,10 @@ // UTMAppleConfigurationVirtualization.swift "Disabled" = "無効"; -"Mouse" = "マウス"; -"Trackpad" = "トラックパッド"; +"Generic Mouse" = "汎用マウス"; +"Mac Trackpad (macOS 13+)" = "Macトラックパッド(macOS 13以降)"; +"Generic USB" = "汎用USB"; +"Mac Keyboard (macOS 14+)" = "Macキーボード(macOS 14以降)"; // UTMQemuConfiguration.swift "Failed to migrate configuration from a previous UTM version." = "以前のUTMバージョンからの構成の移行に失敗しました。"; @@ -92,13 +94,17 @@ /* Services */ -// UTMQemu.m +// UTMPipeInterface.swift +"Failed to create pipe for communications." = "通信用パイプの作成に失敗しました。"; + +// UTMProcess.m "Internal error has occurred." = "内部エラーが発生しました。"; // UTMQemuImage.swift "An unknown QEMU error has occurred." = "不明なQEMUエラーが発生しました。"; // UTMSpiceIO.m +"Failed to change current directory." = "作業ディレクトリの変更に失敗しました。"; "Failed to start SPICE client." = "SPICEクライアントの開始に失敗しました。"; "Internal error trying to connect to SPICE server." = "SPICEサーバへの接続試行中に内部エラーが発生しました。"; @@ -122,22 +128,46 @@ "Failed to access shared directory." = "共有ディレクトリのアクセスに失敗しました。"; "The virtual machine is in an invalid state." = "仮想マシンが無効な状態です。"; "Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "仮想マシンのスナップショットの保存に失敗しました。これは、通常1台以上のデバイスがスナップショットに対応していないことを意味します。%@"; +"Failed to generate TLS key for server." = "サーバ用のTLS鍵の生成に失敗しました。"; /* Platform/iOS */ -// UTMMainView.swift +// UTMDataExtension.swift +"This virtual machine is already running. In order to run it from this device, you must stop it first." = "この仮想マシンはすでに実行されています。このデバイスから実行するには、まず停止する必要があります。"; + +// UTMSingleWindowView.swift "Waiting for VM to connect to display..." = "仮想マシンがディスプレイに接続するのを待機中…"; +// UTMRemoteConnectView.swift +"Select a UTM Server" = "UTMサーバを選択してください"; +"Help" = "ヘルプ"; +"New Connection" = "新規接続"; +"Saved" = "保存済み"; +"Edit…" = "編集…"; +"Delete" = "削除"; +"Discovered" = "検出"; +"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." = "最新バージョンのUTMがMac上で実行されており、UTMサーバが有効になっていることを確認してください。UTMはMac App Storeからダウンロードできます。"; +"Name (optional)" = "名前(オプション)"; +"Hostname or IP address" = "ホスト名またはIPアドレス"; +"Port" = "ポート"; +"Host" = "ホスト"; +"Fingerprint" = "指紋"; +"Password" = "パスワード"; +"Save Password" = "パスワードを保存"; +"Close" = "閉じる"; +"Cancel" = "キャンセル"; +"Trust" = "信頼"; +"Connect" = "接続"; +"Timed out trying to connect." = "接続試行中にタイムアウトになりました。"; + // UTMSettingsView.swift "Settings" = "設定"; -"Close" = "閉じる"; // VMConfigNetworkPortForwardView.swift "Port Forward" = "ポート転送"; "%@ ➡️ %@" = "%1$@ ➡️ %2$@"; "New" = "新規"; -"Delete" = "削除"; "Save" = "保存"; // VMDrivesSettingsView.swift @@ -145,7 +175,6 @@ "Are you sure you want to permanently delete this disk image?" = "このディスクイメージを完全に削除してもよろしいですか?"; "EFI Variables" = "EFI変数"; "%@ Drive" = "%@ドライブ"; -"Cancel" = "キャンセル"; "Done" = "完了"; // VMSettingsView.swift @@ -166,7 +195,7 @@ // VMToolbarView.swift "Power Off" = "電源オフ"; -"Quit" = "終了"; +"Force Kill" = "強制終了"; "Pause" = "一時停止"; "Play" = "再生"; "Restart" = "再起動"; @@ -313,9 +342,20 @@ "If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "有効にすると、ゲストに対してNumLockが常にオンになります。これにより、キーボードのNumLockインジケータが同期しなくなる可能性があることに注意してください。"; "QEMU USB" = "QEMU USB"; "Do not show prompt when USB device is plugged in" = "USBデバイス挿入時にプロンプトを表示しない"; +"Startup" = "起動時"; +"Automatically start UTM server" = "UTMサーバを自動的に起動"; +"Reject unknown connections by default" = "デフォルトで不明な接続を拒否"; +"If checked, you will not be prompted about any unknown connection and they will be rejected." = "チェックを入れると、不明な接続についてのプロンプトは表示されず拒否されます。"; +"Allow access from external clients" = "外部クライアントからのアクセスを許可"; +"By default, the server is only available on LAN but setting this will use UPnP/NAT-PMP to port forward to WAN." = "デフォルトでは、サーバはLAN上でのみ利用可能ですが、これを設定すると、UPnP/NAT-PMPを使用してWANにポート転送します。"; +"Specify a port number to listen on. This is required if external clients are permitted." = "外部からの接続を受け入れるポート番号を指定します。これは、外部クライアントが許可されている場合に必要です。"; +"Authentication" = "認証"; +"Require Password" = "パスワードが必要"; +"If enabled, clients must enter a password. This is required if you want to access the server externally." = "有効にすると、クライアントはパスワードを入力する必要があります。これは、サーバに外部からアクセスする場合に必要です。"; // UTMApp.swift "UTM" = "UTM"; +"UTM Server" = "UTMサーバ"; // UTMDataExtension.swift "This virtual machine cannot be run on this machine." = "この仮想マシンはこのマシンでは実行できません。"; @@ -326,6 +366,7 @@ "Hide dock icon on next launch" = "次回起動時にDockアイコンを非表示にする"; "Requires restarting UTM to take affect." = "変更を適用するには、UTMを再起動する必要があります。"; "No virtual machines found." = "仮想マシンが見つかりません。"; +"Quit" = "終了"; "Terminate UTM and stop all running VMs." = "UTMを終了し、すべての実行中の仮想マシンを停止します。"; "Start" = "開始"; "Stop" = "停止"; @@ -333,6 +374,22 @@ "Reset" = "リセット"; "Busy…" = "処理中…"; +// UTMServer.swift +"Enable UTM Server" = "UTMサーバを有効にする"; +"Reset Identity" = "IDをリセット"; +"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." = "すべてのクライアントを削除して、新しいサーバIDを生成しますか? 以前このサーバとペアリングしていたクライアントは、再度接続する前に手動でこのサーバとのペアリングを解除するよう指示されます。"; +"Server IP: %s, Port: %s" = "サーバIP: %1$s、ポート: %2$s"; +"Running" = "動作中"; +"Name" = "名前"; +"Last Seen" = "最終接続日時"; +"Status" = "状態"; +"Connected" = "接続済み"; +"Blocked" = "ブロック済み"; +"Approve" = "承認"; +"Block" = "ブロック"; +"Disconnect" = "切断"; +"Do you want to forget the selected client(s)?" = "選択中のクライアントを削除しますか?"; + // VMConfigAppleBootView.swift "Operating System" = "オペレーティングシステム"; "Bootloader" = "ブートローダ"; @@ -364,7 +421,6 @@ // VMConfigAppleDriveDetailsView.swift "Removable Drive" = "リムーバブルドライブ"; -"Name" = "名前"; "(New Drive)" = "(新規ドライブ)"; "Read Only?" = "読み出しのみ"; "Delete Drive" = "ドライブを削除"; @@ -397,8 +453,7 @@ "Enable Balloon Device" = "バルーンデバイスを有効にする"; "Enable Entropy Device" = "エントロピデバイスを有効にする"; "Enable Sound" = "サウンドを有効にする"; -"Enable Keyboard" = "キーボードを有効にする"; -"Enable Pointer" = "ポインタを有効にする"; +"Pointer" = "ポインタ"; "Use Trackpad" = "トラックパッドを使用"; "Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "トラックパッドからの追加の入力をパススルーできるようになります。macOS 13以降のゲストでのみ対応しています。"; "Enable Rosetta on Linux (x86_64 Emulation)" = "Linux上でRosettaを有効にする(x86_64エミュレーション)"; @@ -412,9 +467,11 @@ "Guest Port" = "ゲストポート"; "Host Address" = "ホストアドレス"; "Host Port" = "ホストポート"; -"Edit…" = "編集…"; "New…" = "新規…"; +// VMSessionState.swift +"Connection to the server was lost." = "サーバへの接続が切断されました。"; + // VMConfigQEMUArgumentsView.swift "Arguments" = "引数"; "Export QEMU Command…" = "QEMUコマンドを書き出す…"; @@ -454,6 +511,13 @@ "Select where to export QEMU command:" = "QEMUコマンドの書き出し先を選択してください:"; +/* Platform/visionOS */ + +// VMToolbarOrnamentModifier.swift +"Hide Controls" = "コントロールを非表示"; +"Show Controls" = "コントロールを表示"; + + /* Platform/Shared */ // DestructiveButton.swift @@ -615,7 +679,6 @@ "Emulated Serial Device" = "仮想シリアルデバイス"; "TCP" = "TCP"; "Server Address" = "サーバアドレス"; -"Port" = "ポート"; "The target does not support hardware emulated serial connections." = "このターゲットはハードウェア仮想シリアル接続に対応していません。"; // VMConfigSharingView.swift @@ -700,6 +763,7 @@ "Browse UTM Gallery" = "UTMギャラリーをブラウズ"; "User Guide" = "ユーザガイド"; "Support" = "サポート"; +"Server" = "サーバ"; // VMRemovableDrivesView.swift "%@ %@" = "%1$@ %2$@"; @@ -720,6 +784,8 @@ "Stop selected VM" = "選択した仮想マシンを停止します"; "Run selected VM" = "選択した仮想マシンを実行します"; "Edit selected VM" = "選択した仮想マシンを編集します"; +"Preferences" = "環境設定"; +"Show UTM preferences" = "UTM環境設定を表示します"; // VMWizardDrivesView.swift "Storage" = "ストレージ"; @@ -727,7 +793,7 @@ // VMWizardHardwareView.swift "Hardware OpenGL Acceleration" = "ハードウェアOpenGLアクセラレーション"; -"There are known issues in some newer Linux drivers including black screen, broken compositing, and apps failing to render." = "一部の新しいLinuxドライバの中には、画面が黒くなる、表示が乱れる、Appのレンダリングに失敗するといった既知の問題があります。"; +"There are known issues in some newer Linux drivers including black screen, broken compositing, and apps failing to render." = "一部の新しいLinuxドライバの中には、画面が黒くなる、表示が乱れる、アプリのレンダリングに失敗するといった既知の問題があります。"; "Enable hardware OpenGL acceleration" = "ハードウェアOpenGLアクセラレーションを有効にする"; // VMWizardOSLinuxView.swift @@ -840,6 +906,7 @@ // UTMData.swift "An existing virtual machine already exists with this name." = "この名前の仮想マシンがすでに存在します。"; +"This virtual machine is currently unavailable, make sure it is not open in another session." = "この仮想マシンは現在使用できません。別のセッションで開かれていないことを確認してください。"; "Failed to clone VM." = "仮想マシンの複製に失敗しました。"; "Unable to add a shortcut to the new location." = "新しい場所にショートカットを追加できません。"; "Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM." = "この仮想マシンは読み込めません。構成が無効であるか、新しいバージョンのUTM、またはこのバージョンのUTMと互換性のないプラットフォームで作成されています。"; @@ -851,6 +918,8 @@ "Failed to decode JitStreamer response." = "JitStreamerの応答のデコードに失敗しました。"; "Failed to attach to JitStreamer." = "JitStreamerへのアタッチに失敗しました。"; "Invalid JitStreamer attach URL:\n%@" = "JitStreamerアタッチURLが無効です:\n%@"; +"This functionality is not yet implemented." = "この機能はまだ実装されていません。"; +"Failed to reconnect to the server." = "サーバへの再接続に失敗しました。"; // UTMDownloadVMTask.swift "There is no UTM file in the downloaded ZIP archive." = "ダウンロードされたZIPアーカイブ内にUTMファイルがありません。"; @@ -881,8 +950,51 @@ "Restoring" = "復元中"; +/* Remote */ + +// UTMRemoteKeyManager.swift +"Failed to generate a key pair." = "鍵ペアの生成に失敗しました。"; +"Failed to parse generated key pair." = "生成された鍵ペアの解析に失敗しました。"; +"Failed to import generated key." = "生成された鍵の読み込みに失敗しました。"; + +// UTMRemoteClient.swift +"Failed to determine host name." = "ホスト名の特定に失敗しました。"; +"Failed to get host fingerprint." = "ホストの指紋の取得に失敗しました。"; +"Password is required." = "パスワードが必要です。"; +"Password is incorrect." = "パスワードが間違っています。"; +"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." = "このホストはまだ信頼されていません。指紋がホストに表示されているものと一致していることを確認し、“信頼”を選択して続ける必要があります。"; +"The server interface version does not match the client." = "サーバインターフェイスのバージョンがクライアントと一致しません。"; + +// UTMRemoteSpiceVirtualMachine.swift +"Failed to connect to SPICE: %@" = "SPICEへの接続に失敗しました: %@"; +"An operation is already in progress." = "操作はすでに進行中です。"; + +// UTMRemoteServer.swift +"Allow" = "許可"; +"Deny" = "拒否"; +"Disconnect" = "切断"; +"New unknown remote client connection." = "新しい不明なリモートクライアントからの接続がありました。"; +"New trusted remote client connection." = "新しい信頼済みリモートクライアントからの接続がありました。"; +"Unknown Remote Client" = "不明なリモートクライアント"; +"A client with fingerprint '%@' is attempting to connect." = "指紋“%@”のクライアントが接続しようとしています。"; +"Remote Client Connected" = "リモートクライアント接続済み"; +"Established connection from %@." = "%@からの接続を確立しました。"; +"UTM Remote Server Error" = "UTMリモートサーバエラー"; +"Cannot reserve port '%@' for external access from NAT. Make sure no other device on the network has reserved it." = "NATからの外部アクセス用のポート“%@”を予約できません。ネットワーク上のほかのデバイスが予約していないことを確認してください。"; +"Not authenticated." = "認証されていません。"; +"The client interface version does not match the server." = "クライアントインターフェイスのバージョンがサーバと一致しません。"; +"Cannot find VM with ID: %@" = "指定されたIDの仮想マシンが見つかりません: %@"; +"Invalid backend." = "バックエンドが無効です。"; +"Failed to access file." = "ファイルへのアクセスに失敗しました。"; + + /* Scripting */ +// UTMScriptingUSBDeviceImpl.swift +"UTM is not ready to accept commands." = "UTMはコマンドを受け入れる準備ができていません。"; +"The device cannot be found." = "デバイスが見つかりません。"; +"The device is not currently connected." = "デバイスが現在接続されていません。"; + // UTMScriptingVirtualMachineImpl.swift "Operation not available." = "操作は利用できません。"; "Operation not supported by the backend." = "操作はバックエンドが対応していません。"; @@ -898,7 +1010,6 @@ "This device is not supported by the target." = "このデバイスはターゲットが対応していません。"; // UTMScriptingCreateCommand.swift -"UTM is not ready to accept commands." = "UTMはコマンドを受け入れる準備ができていません。"; "A valid backend must be specified." = "有効なバックエンドを指定する必要があります。"; "This backend is not supported on your machine." = "このバックエンドはお使いのマシンでは対応していません。"; "A valid configuration must be specified." = "有効な構成を指定する必要があります。"; diff --git a/Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift b/Platform/macOS/Display/VMDisplayAppleTerminalWindowController.swift index c1ac66455..45d5b5331 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 acbacd5e0..8b2c3bcd2 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 eaec9a91f..124b81801 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 eb40ce4c0..7686b008e 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/Display/it.lproj/VMDisplayWindow.strings b/Platform/macOS/Display/it.lproj/VMDisplayWindow.strings new file mode 100644 index 000000000..31faeecc1 --- /dev/null +++ b/Platform/macOS/Display/it.lproj/VMDisplayWindow.strings @@ -0,0 +1,93 @@ + +/* Class = "NSToolbarItem"; label = "Shared Folder"; ObjectID = "7EC-GE-fIl"; */ +"7EC-GE-fIl.label" = "Cartella Condivisa"; + +/* Class = "NSToolbarItem"; paletteLabel = "Shared Folder"; ObjectID = "7EC-GE-fIl"; */ +"7EC-GE-fIl.paletteLabel" = "Cartella Condivisa"; + +/* Class = "NSToolbarItem"; toolTip = "Shared folder"; ObjectID = "7EC-GE-fIl"; */ +"7EC-GE-fIl.toolTip" = "Cartella Condivisa"; + +/* Class = "NSToolbarItem"; label = "Stop"; ObjectID = "Bkx-Ph-j0D"; */ +"Bkx-Ph-j0D.label" = "Arresta"; + +/* Class = "NSToolbarItem"; paletteLabel = "Stop"; ObjectID = "Bkx-Ph-j0D"; */ +"Bkx-Ph-j0D.paletteLabel" = "Arresta"; + +/* Class = "NSToolbarItem"; toolTip = "Shuts down and stops the VM"; ObjectID = "Bkx-Ph-j0D"; */ +"Bkx-Ph-j0D.toolTip" = "Spegne e Arresta VM"; + +/* Class = "NSToolbarItem"; label = "Toolbar Item"; ObjectID = "C8Y-BQ-Y6m"; */ +"C8Y-BQ-Y6m.label" = "Elemento della Barra degli Strumenti"; + +/* Class = "NSToolbarItem"; paletteLabel = "Toolbar Item"; ObjectID = "C8Y-BQ-Y6m"; */ +"C8Y-BQ-Y6m.paletteLabel" = "Elemento della Barra degli Strumenti"; + +/* Class = "NSToolbarItem"; label = "Capture Input"; ObjectID = "FN7-zs-mWC"; */ +"FN7-zs-mWC.label" = "Cattura Input"; + +/* Class = "NSToolbarItem"; paletteLabel = "Capture Input"; ObjectID = "FN7-zs-mWC"; */ +"FN7-zs-mWC.paletteLabel" = "Cattura Input"; + +/* Class = "NSToolbarItem"; toolTip = "Capture input devices"; ObjectID = "FN7-zs-mWC"; */ +"FN7-zs-mWC.toolTip" = "Cattura Dispositivi di Input"; + +/* Class = "NSToolbarItem"; label = "Restart"; ObjectID = "G7P-HJ-bcy"; */ +"G7P-HJ-bcy.label" = "Riavvia"; + +/* Class = "NSToolbarItem"; paletteLabel = "Restart"; ObjectID = "G7P-HJ-bcy"; */ +"G7P-HJ-bcy.paletteLabel" = "Riavvia"; + +/* Class = "NSToolbarItem"; toolTip = "Restarts the VM"; ObjectID = "G7P-HJ-bcy"; */ +"G7P-HJ-bcy.toolTip" = "Riavvia la VM"; + +/* Class = "NSToolbarItem"; label = "Windows"; ObjectID = "MQ2-L1-yl7"; */ +"MQ2-L1-yl7.label" = "Finestre"; + +/* Class = "NSToolbarItem"; paletteLabel = "Windows"; ObjectID = "MQ2-L1-yl7"; */ +"MQ2-L1-yl7.paletteLabel" = "Finestre"; + +/* Class = "NSToolbarItem"; toolTip = "Windows"; ObjectID = "MQ2-L1-yl7"; */ +"MQ2-L1-yl7.toolTip" = "Finestre"; + +/* Class = "NSWindow"; title = "UTM"; ObjectID = "QvC-M9-y7g"; */ +"QvC-M9-y7g.title" = "UTM"; + +/* Class = "NSToolbarItem"; label = "Resize Console"; ObjectID = "Ulf-oT-4cP"; */ +"Ulf-oT-4cP.label" = "Ridimensiona Console"; + +/* Class = "NSToolbarItem"; paletteLabel = "Resize Console"; ObjectID = "Ulf-oT-4cP"; */ +"Ulf-oT-4cP.paletteLabel" = "Ridimensiona Console"; + +/* Class = "NSToolbarItem"; toolTip = "Send console resize command"; ObjectID = "Ulf-oT-4cP"; */ +"Ulf-oT-4cP.toolTip" = "Invia il comando Ridimensiona alla Console"; + +/* Class = "NSButton"; ibShadowedToolTip = "Starts/resumes the VM"; ObjectID = "ZTi-Hs-ge6"; */ +"ZTi-Hs-ge6.ibShadowedToolTip" = "Avvia/Riprende l'esecuzione della VM"; + +/* Class = "NSToolbarItem"; label = "Drives"; ObjectID = "bKL-Th-FFw"; */ +"bKL-Th-FFw.label" = "Dischi"; + +/* Class = "NSToolbarItem"; paletteLabel = "Drives"; ObjectID = "bKL-Th-FFw"; */ +"bKL-Th-FFw.paletteLabel" = "Dischi"; + +/* Class = "NSToolbarItem"; toolTip = "Drive image options"; ObjectID = "bKL-Th-FFw"; */ +"bKL-Th-FFw.toolTip" = "Opzioni Immagine Disco"; + +/* Class = "NSToolbarItem"; label = "Start/Pause"; ObjectID = "kT2-2U-cYm"; */ +"kT2-2U-cYm.label" = "Avvia/Metti in Pausa"; + +/* Class = "NSToolbarItem"; paletteLabel = "Start/Pause"; ObjectID = "kT2-2U-cYm"; */ +"kT2-2U-cYm.paletteLabel" = "Avvia/Metti in Pausa"; + +/* Class = "NSToolbarItem"; toolTip = "Start/pause the VM"; ObjectID = "kT2-2U-cYm"; */ +"kT2-2U-cYm.toolTip" = "Avvia/Metti in Pausa la VM"; + +/* Class = "NSToolbarItem"; label = "USB"; ObjectID = "tlw-Fb-ne3"; */ +"tlw-Fb-ne3.label" = "USB"; + +/* Class = "NSToolbarItem"; paletteLabel = "USB"; ObjectID = "tlw-Fb-ne3"; */ +"tlw-Fb-ne3.paletteLabel" = "USB"; + +/* Class = "NSToolbarItem"; toolTip = "USB devices"; ObjectID = "tlw-Fb-ne3"; */ +"tlw-Fb-ne3.toolTip" = "Dispositivi USB"; diff --git a/Platform/macOS/Display/zh-HK.lproj/VMDisplayWindow.strings b/Platform/macOS/Display/zh-HK.lproj/VMDisplayWindow.strings index 9245c9289..bfaa0491d 100644 --- a/Platform/macOS/Display/zh-HK.lproj/VMDisplayWindow.strings +++ b/Platform/macOS/Display/zh-HK.lproj/VMDisplayWindow.strings @@ -1,11 +1,11 @@ /* Class = "NSToolbarItem"; label = "Shared Folder"; ObjectID = "7EC-GE-fIl"; */ -"7EC-GE-fIl.label" = "共享資料夾"; +"7EC-GE-fIl.label" = "分享資料夾"; /* Class = "NSToolbarItem"; paletteLabel = "Shared Folder"; ObjectID = "7EC-GE-fIl"; */ -"7EC-GE-fIl.paletteLabel" = "共享資料夾"; +"7EC-GE-fIl.paletteLabel" = "分享資料夾"; /* Class = "NSToolbarItem"; toolTip = "Shared folder"; ObjectID = "7EC-GE-fIl"; */ -"7EC-GE-fIl.toolTip" = "共享資料夾"; +"7EC-GE-fIl.toolTip" = "分享資料夾"; /* Class = "NSToolbarItem"; label = "Drives"; ObjectID = "bKL-Th-FFw"; */ "bKL-Th-FFw.label" = "磁碟"; @@ -32,13 +32,13 @@ "C8Y-BQ-Y6m.paletteLabel" = "工具列項目"; /* Class = "NSToolbarItem"; label = "Capture Input"; ObjectID = "FN7-zs-mWC"; */ -"FN7-zs-mWC.label" = "捕獲輸入"; +"FN7-zs-mWC.label" = "擷取輸入"; /* Class = "NSToolbarItem"; paletteLabel = "Capture Input"; ObjectID = "FN7-zs-mWC"; */ -"FN7-zs-mWC.paletteLabel" = "捕獲輸入"; +"FN7-zs-mWC.paletteLabel" = "擷取輸入"; /* Class = "NSToolbarItem"; toolTip = "Capture input devices"; ObjectID = "FN7-zs-mWC"; */ -"FN7-zs-mWC.toolTip" = "捕獲輸入裝置"; +"FN7-zs-mWC.toolTip" = "擷取輸入裝置"; /* Class = "NSToolbarItem"; label = "Restart"; ObjectID = "G7P-HJ-bcy"; */ "G7P-HJ-bcy.label" = "重新啟動"; diff --git a/Platform/macOS/SettingsView.swift b/Platform/macOS/SettingsView.swift index 961ac1f31..4485a773f 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 ee11f7d00..4fcc0224c 100644 --- a/Platform/macOS/UTMApp.swift +++ b/Platform/macOS/UTMApp.swift @@ -61,6 +61,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 6c2f6659a..3acdb861e 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 000000000..9b6bee3cf --- /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 a6b9f020e..c26b863a6 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 000000000..66a946c77 --- /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/it.lproj/InfoPlist.strings b/Platform/macOS/it.lproj/InfoPlist.strings new file mode 100644 index 000000000..ee89e3992 --- /dev/null +++ b/Platform/macOS/it.lproj/InfoPlist.strings @@ -0,0 +1,8 @@ +/* Bundle name */ +"CFBundleName" = "UTM"; + +/* (No Comment) */ +"UTM virtual machine" = "Macchina Virtuale UTM"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "Permette alle Macchine Virtuali di accedere al Microfono"; diff --git a/Platform/macOS/macOS-unsigned.entitlements b/Platform/macOS/macOS-unsigned.entitlements index 216c2ec76..64b7e649d 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 4b3bdf542..de00e8fc0 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/pl.lproj/Localizable.strings b/Platform/pl.lproj/Localizable.strings index 2a314c61f..b23c0b2a1 100644 --- a/Platform/pl.lproj/Localizable.strings +++ b/Platform/pl.lproj/Localizable.strings @@ -73,6 +73,7 @@ "TCP Client Connection" = "Połączenie kilent TCP"; "TCP Server Connection" = "Połączenie serwer TCP"; "Automatic Serial Device (max 4)" = "Automatyczne urządzenie szeregowe (maks. 4)"; +"Automatic" = "Automatyczny"; "Manual Serial Device (advanced)" = "Manualne urządzenie szeregowe (zaawansowane)"; "GDB Debug Stub" = "GDB Debug Stub"; "QEMU Monitor (HMP)" = "Monitor QEMU (HMP)"; @@ -178,7 +179,7 @@ "Information" = "Informacje"; "System" = "System"; "QEMU" = "QEMU"; -"Input" = "Urządzenie WE/WY"; +"Input" = "Urządzenie peryferyjne"; "Sharing" = "Współdzielenie"; "Devices" = "Urządzenia"; "Display" = "Monitor"; @@ -295,10 +296,12 @@ "Sound Backend" = "Tryb dźwięku"; "SPICE with GStreamer (Input & Output)" = "SPICE z GStreamerem (WE/WY)"; "CoreAudio (Output Only)" = "CoreAudio (tylko wyjście)"; +"Mouse/Keyboard" = "Klawiatura/mysz"; +"Capture input automatically when entering full screen" = "Przechwytuj mysz automatycznie w trybie pełnoekranowym"; "Console" = "Konsola"; "Option (⌥) is Meta key" = "Option(⌥) to klawisz Meta"; "If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "Jeśli włączone, klawisz Option będzie zmapowany jako klawisz Meta, który może być przydatny dla eMacków. W innym wypadku, ta opcja będzie działać jak system przewiduje (tj: wpisywanie międzynarodowego tekstu)."; -"QEMU Pointer" = "Wskaźnik QEMU"; +"QEMU Pointer" = "Mysz QEMU"; "Hold Control (⌃) for right click" = "Przytrzymaj Control (⌃) aby wykonać prawe kliknięcie"; "Invert scrolling" = "Odwróć przewijanie"; "If enabled, scroll wheel input will be inverted." = "Jeśli włączone, przewijanie będzie odwrócone"; @@ -517,7 +520,8 @@ "Guest Network (IPv6)" = "Sieć gościa (IPv6)"; "Host Address" = "Adres hosta"; "Host Address (IPv6)" = "Adres hosta(IPv6)"; -"DHCP Start" = "Start DHCP"; +"DHCP Start" = "Pierwszy adres zakresu DHCP"; +"DHCP End" = "Ostatni adres zakresu DHCP"; "DHCP Domain Name" = "Nazwa domeny DHCP"; "DNS Server" = "Serwer DNS"; "DNS Server (IPv6)" = "Serwer DNS(IPv6)"; @@ -550,7 +554,7 @@ "Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "Utwórz instancję kontrolera PS/2 nawet jeśli kontroler USB jest wspierany. Wymagane dla starszych wersji systemu Windows."; "QEMU Machine Properties" = "Właściwości maszyny QEMU"; "This is appended to the -machine argument." = "To jest dołączone do argumentu -machine."; -"QEMU Arguments" = "Argumenty dla QEMU"; +"QEMU Arguments" = "Argumenty rozruchu QEMU"; "Export QEMU Command…" = "Eksportuj komendę QEMU…"; "(Delete)" = "(Usuń)"; diff --git a/Platform/visionOS/UTMApp.swift b/Platform/visionOS/UTMApp.swift index e3e1487fa..c82496e36 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 bc058a9de..372432cd2 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,30 @@ 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 +private struct ToolbarOrnamentViewModifier: ViewModifier { + func body(content: Content) -> some View { + content + .buttonBorderShape(.capsule) + .buttonStyle(.borderless) + .labelStyle(.iconOnly) + .padding(12) + .glassBackgroundEffect() + } } // the following was suggested by Apple via Feedback to look close to .toolbar() with .bottomOrnament diff --git a/Platform/zh-HK.lproj/Localizable.strings b/Platform/zh-HK.lproj/Localizable.strings index 29f65d128..f2bd35fc3 100644 --- a/Platform/zh-HK.lproj/Localizable.strings +++ b/Platform/zh-HK.lproj/Localizable.strings @@ -78,7 +78,7 @@ "An internal error has occurred." = "發生內部錯誤。"; /* UTMConfiguration */ -"An invalid value of '%@' is used in the configuration file." = "設定檔裡使用了無效值「%@」。"; +"An invalid value of '%@' is used in the configuration file." = "設定檔內使用了無效值「%@」。"; /* UTMQemuImage */ "An unknown QEMU error has occurred." = "發生未知的 QEMU 錯誤。"; @@ -90,7 +90,7 @@ "ANGLE (OpenGL)" = "ANGLE (OpenGL)"; /* VMConfigSystemView */ -"Any unsaved changes will be lost." = "任何未儲存的變更都將丟失。"; +"Any unsaved changes will be lost." = "任何未儲存的變更都將遺失。"; /* No comment provided by engineer. */ "Architecture" = "體系結構"; @@ -102,10 +102,10 @@ "Are you sure you want to permanently delete this disk image?" = "確定要永久刪除此磁碟映像檔嗎?"; /* No comment provided by engineer. */ -"Are you sure you want to reset this VM? Any unsaved changes will be lost." = "確定要重設此虛擬電腦嗎?任何未儲存的變更都將丟失。"; +"Are you sure you want to reset this VM? Any unsaved changes will be lost." = "確定要重設此虛擬電腦嗎?任何未儲存的變更都將遺失。"; /* No comment provided by engineer. */ -"Are you sure you want to stop this VM and exit? Any unsaved changes will be lost." = "確定要停止此虛擬電腦並退出嗎?任何未儲存的變更都將丟失。"; +"Are you sure you want to stop this VM and exit? Any unsaved changes will be lost." = "確定要停止此虛擬電腦並結束嗎?任何未儲存的變更都將遺失。"; /* No comment provided by engineer. */ "Automatic" = "自動"; @@ -154,7 +154,7 @@ "Build" = "構建"; /* UTMQemuConstants */ -"Built-in Terminal" = "內置終端機"; +"Built-in Terminal" = "預置終端機"; /* No comment provided by engineer. */ "Busy…" = "正忙⋯"; @@ -182,13 +182,13 @@ "Caps Lock (⇪) is treated as a key" = "將 Caps Lock (⇪) 視為按鍵"; /* VMMetalView */ -"Capture Input" = "截取輸入"; +"Capture Input" = "擷取輸入"; /* No comment provided by engineer. */ -"Capture input automatically when entering full screen" = "進入全螢幕時自動截取輸入"; +"Capture input automatically when entering full screen" = "進入全螢幕時自動擷取輸入"; /* VMDisplayQemuMetalWindowController */ -"Captured mouse" = "已截取滑鼠"; +"Captured mouse" = "已擷取滑鼠"; /* Configuration boot device */ "CD/DVD" = "CD/DVD"; @@ -215,7 +215,8 @@ /* No comment provided by engineer. */ "Confirm Delete" = "確認刪除"; -/* VMDisplayWindowController */ +/* AppDelegate + VMDisplayWindowController */ "Confirmation" = "確認"; /* No comment provided by engineer. */ @@ -268,10 +269,10 @@ "Devices" = "裝置"; /* VMDisplayAppleWindowController */ -"Directory sharing" = "目錄共享"; +"Directory sharing" = "目錄分享"; /* UTMQemuConstants */ -"Disabled" = "禁用"; +"Disabled" = "停用"; /* UTMLegacyQemuConfiguration UTMQemuConstants */ @@ -287,16 +288,16 @@ "Disposable Mode" = "即拋式模式"; /* No comment provided by engineer. */ -"Do not save VM screenshot to disk" = "不要將虛擬電腦快照儲存到磁碟"; +"Do not save VM screenshot to disk" = "不要將虛擬電腦快照儲存至磁碟"; /* No comment provided by engineer. */ -"Do not show confirmation when closing a running VM" = "關閉正在執行的虛擬電腦時不顯示確認"; +"Do not show confirmation when closing a running VM" = "關閉正在執行的虛擬電腦時不要顯示確認"; /* No comment provided by engineer. */ -"Do not show prompt when USB device is plugged in" = "插入 USB 裝置時不顯示提示"; +"Do not show prompt when USB device is plugged in" = "插入 USB 裝置時不要顯示提示"; /* No comment provided by engineer. */ -"Do you want to copy this VM and all its data to internal storage?" = "你要複製此虛擬電腦及其所有資料到內部儲存空間嗎?"; +"Do you want to copy this VM and all its data to internal storage?" = "你要複製此虛擬電腦及其所有資料至內部儲存空間嗎?"; /* No comment provided by engineer. */ "Do you want to delete this VM and all its data?" = "你要刪除此虛擬電腦及其所有資料嗎?"; @@ -305,10 +306,10 @@ "Do you want to duplicate this VM and all its data?" = "你要製作此虛擬電腦及其所有資料的副本嗎?"; /* No comment provided by engineer. */ -"Do you want to force stop this VM and lose all unsaved data?" = "你要強制停止此虛擬電腦並丟失所有未儲存的資料嗎?"; +"Do you want to force stop this VM and lose all unsaved data?" = "你要強行停止此虛擬電腦並遺失所有未儲存的資料嗎?"; /* No comment provided by engineer. */ -"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "你要將此虛擬電腦移動到其他位置嗎?這將會複製資料到新位置,刪除原先位置資料,並製作捷徑。"; +"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "你要將此虛擬電腦移動至其他位置嗎?這將會複製資料至新位置,刪除原先位置資料,並製作捷徑。"; /* No comment provided by engineer. */ "Do you want to remove this shortcut? The data will not be deleted." = "你要刪除此捷徑嗎?資料不會被刪除。"; @@ -317,7 +318,7 @@ "Download prebuilt from UTM Gallery…" = "從 UTM 虛擬電腦庫下載預構建⋯"; /* No comment provided by engineer. */ -"Drag and drop IPSW file here" = "拖移 IPSW 檔到此"; +"Drag and drop IPSW file here" = "拖放 IPSW 檔至此"; /* UTMScriptingConfigImpl */ "Drive description is invalid." = "磁碟描述無效。"; @@ -332,19 +333,19 @@ "Eject" = "退出"; /* No comment provided by engineer. */ -"Emulate" = "模擬"; +"Emulate" = "仿真"; /* UTMQemuConstants */ -"Emulated VLAN" = "模擬 VLAN"; +"Emulated VLAN" = "仿真 VLAN"; /* No comment provided by engineer. */ -"Enable Clipboard Sharing" = "啟用剪貼板共享"; +"Enable Clipboard Sharing" = "啟用剪貼板分享"; /* VMDisplayWindowController */ "Error" = "錯誤"; /* No comment provided by engineer. */ -"Existing" = "已經存在"; +"Existing" = "現存"; /* No comment provided by engineer. */ "Export QEMU Command…" = "輸出 QEMU 指令⋯"; @@ -353,13 +354,13 @@ "Extracting…" = "正在解壓縮⋯"; /* UTMQemuVirtualMachine */ -"Failed to access data from shortcut." = "無法從捷徑取用資料。"; +"Failed to access data from shortcut." = "無法由捷徑取用資料。"; /* UTMQemuVirtualMachine */ "Failed to access drive image path." = "無法取用磁碟映像檔路徑。"; /* UTMQemuVirtualMachine */ -"Failed to access shared directory." = "無法取用共享目錄。"; +"Failed to access shared directory." = "無法取用分享目錄。"; /* ContentView */ "Failed to attach to JitStreamer:\n%@" = "無法附加至 JitStreamer:%@"; @@ -371,16 +372,16 @@ "Failed to change current directory." = "無法變更當前目錄。"; /* UTMData */ -"Failed to clone VM." = "無法克隆虛擬電腦。"; +"Failed to clone VM." = "無法複製虛擬電腦。"; /* UTMData */ "Failed to decode JitStreamer response." = "無法解碼 JitStreamer 回應。"; /* VMWizardState */ -"Failed to get latest macOS version from Apple." = "無法從 Apple 取得最新的 macOS 版本。"; +"Failed to get latest macOS version from Apple." = "無法由 Apple 取得最新的 macOS 版本。"; /* UTMQemuConfigurationError */ -"Failed to migrate configuration from a previous UTM version." = "無法從以前版本的 UTM 轉移設定。"; +"Failed to migrate configuration from a previous UTM version." = "無法由之前版本的 UTM 轉移設定。"; /* UTMData */ "Failed to parse download URL." = "無法解析已經下載的 URL。"; @@ -391,11 +392,12 @@ /* UTMDownloadVMTask */ "Failed to parse the downloaded VM." = "無法解析已經下載的虛擬電腦。"; -/* VMDisplayWindowController */ +/* AppDelegate + VMDisplayWindowController */ "Failed to save suspend state" = "無法儲存暫停狀態。"; /* UTMQemuVirtualMachine */ -"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "無法儲存虛擬電腦快照。通常這意味著至少有一個裝置不支援快照。%@"; +"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "無法儲存虛擬電腦快照。這通常意味著至少有一個裝置不支援快照。%@"; /* UTMSpiceIO */ "Failed to start SPICE client." = "無法啟動 SPICE 客戶端。"; @@ -411,16 +413,16 @@ "Font Size" = "字體大小"; /* VMDisplayWindowController */ -"Force kill" = "強制結束"; +"Force kill" = "強行結束"; /* VMDisplayWindowController */ -"Force kill the VM process with high risk of data corruption." = "強制結束虛擬電腦程序 (會有高風險使資料損壞)。"; +"Force kill the VM process with high risk of data corruption." = "強行結束虛擬電腦程序 (會有高風險使資料損毀)。"; /* No comment provided by engineer. */ -"Force Multicore" = "強制多核心"; +"Force Multicore" = "強行多核心"; /* VMDisplayWindowController */ -"Force shut down" = "強制關機"; +"Force shut down" = "強行關機"; /* No comment provided by engineer. */ "GB" = "GB"; @@ -431,6 +433,12 @@ /* No comment provided by engineer. */ "Generic" = "一般"; +/* UTMAppleConfigurationDevices */ +"Generic Mouse" = "一般滑鼠"; + +/* UTMAppleConfigurationDevices */ +"Generic USB" = "一般 USB"; + /* No comment provided by engineer. */ "Gesture and Cursor Settings" = "手勢與指標設定"; @@ -492,7 +500,7 @@ "Internal error has occurred." = "發生內部錯誤。"; /* UTMSpiceIO */ -"Internal error trying to connect to SPICE server." = "在連線 SPICE 伺服器時發生內部錯誤。"; +"Internal error trying to connect to SPICE server." = "在連接 SPICE 伺服器時發生內部錯誤。"; /* VMDisplayMetalWindowController */ "Internal error." = "內部錯誤。"; @@ -557,6 +565,12 @@ /* No comment provided by engineer. */ "Logging" = "日誌"; +/* UTMAppleConfigurationDevices */ +"Mac Keyboard (macOS 14+)" = "Mac 鍵盤 (macOS 14+)"; + +/* UTMAppleConfigurationDevices */ +"Mac Trackpad (macOS 13+)" = "Mac 觸控板 (macOS 13+)"; + /* UTMAppleConfigurationBoot */ "macOS" = "macOS"; @@ -573,7 +587,7 @@ "Manual Serial Device (advanced)" = "手動序列裝置 (進階)"; /* No comment provided by engineer. */ -"Maximum Shared USB Devices" = "最大共享 USB 裝置數"; +"Maximum Shared USB Devices" = "最多分享 USB 裝置"; /* No comment provided by engineer. */ "MB" = "MB"; @@ -587,9 +601,6 @@ /* No comment provided by engineer. */ "Minimum size: %@" = "最小大小:%@"; -/* UTMAppleConfigurationDevices */ -"Mouse" = "滑鼠"; - /* No comment provided by engineer. */ "Mouse/Keyboard" = "滑鼠/鍵盤"; @@ -618,22 +629,22 @@ "No" = "否"; /* UTMScriptingAppDelegate */ -"No architecture specified in the configuration." = "未在設定裡指定體系結構。"; +"No architecture specified in the configuration." = "設定內未指定體系結構。"; /* VMDisplayWindowController */ -"No drives connected." = "無已經連線的磁碟。"; +"No drives connected." = "未有已經連接的磁碟。"; /* UTMDownloadSupportToolsTaskError */ -"No empty removable drive found. Make sure you have at least one removable drive that is not in use." = "無法找到空的可移除式磁碟。請確定你至少有一個未使用的可移除式磁碟。"; +"No empty removable drive found. Make sure you have at least one removable drive that is not in use." = "無法找到空的可移除式磁碟。確保你至少有一個未使用的可移除式磁碟。"; /* UTMScriptingAppDelegate */ -"No name specified in the configuration." = "設定裡沒有指定名稱。"; +"No name specified in the configuration." = "設定內未指定名稱。"; /* No comment provided by engineer. */ "No output device is selected for this window." = "在此視窗內未選取任何輸出裝置。"; /* No comment provided by engineer. */ -"No release notes found for version %@." = "無法找到版本 %@ 的版本附註。"; +"No release notes found for version %@." = "無法找到版本 %@ 的發行註記。"; /* VMQemuDisplayMetalWindowController */ "No USB devices detected." = "未偵測到 USB 裝置。"; @@ -655,10 +666,10 @@ "Not implemented." = "此功能未實現。"; /* No comment provided by engineer. */ -"Notes" = "注意"; +"Notes" = "備註"; /* No comment provided by engineer. */ -"Num Lock is forced on" = "Num Lock 強制開啟"; +"Num Lock is forced on" = "Num Lock 強行開啟"; /* UTMQemuConstants */ "NVMe" = "NVMe"; @@ -724,7 +735,7 @@ "Preconfigured" = "預設定"; /* A download process is about to begin. */ -"Preparing…" = "正在準備..."; +"Preparing…" = "正在準備⋯"; /* VMDisplayQemuMetalWindowController */ "Press %@ to release cursor" = "按下 %@ 來放開指標"; @@ -805,7 +816,7 @@ "Resize display to window size automatically" = "自動將顯示大小調整為視窗大小"; /* No comment provided by engineer. */ -"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "調整空間大小屬於實驗性功能,可能會導致資料丟失。強烈建議你先備份此虛擬電腦,然後再繼續操作。你要調整大小為 %@ GB 嗎?"; +"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "調整空間大小屬於實驗性功能,可能會導致資料遺失。強烈建議你先備份此虛擬電腦,然後再繼續操作。你要調整大小為 %@ GB 嗎?"; /* VMData */ "Restoring" = "正在還原"; @@ -845,7 +856,7 @@ /* VMDisplayAppleWindowController VMDisplayWindowController */ -"Select Shared Folder" = "選取共享的資料夾"; +"Select Shared Folder" = "選取分享的資料夾"; /* SavePanel */ "Select where to export QEMU command:" = "選取輸出 QEMU 指令的位置:"; @@ -860,26 +871,26 @@ "Selected:" = "已選取:"; /* VMDisplayWindowController */ -"Sends power down request to the guest. This simulates pressing the power button on a PC." = "向客戶端發送關閉電源請求。此操作模擬了按下 PC 上的電源按鈕。"; +"Sends power down request to the guest. This simulates pressing the power button on a PC." = "向客戶端發送關閉電源請求。此操作仿真了按下 PC 上的電源按鈕。"; /* VMDisplayAppleWindowController VMDisplayQemuDisplayController */ "Serial %lld" = "序列裝置 %lld"; /* No comment provided by engineer. */ -"Share USB devices from host" = "從主機共享 USB 裝置"; +"Share USB devices from host" = "從主機分享 USB 裝置"; /* No comment provided by engineer. */ -"Shared directories in macOS VMs are only available in macOS 13 and later." = "macOS 虛擬電腦共享目錄僅在 macOS 13 及更高版本可用。"; +"Shared directories in macOS VMs are only available in macOS 13 and later." = "macOS 虛擬電腦分享目錄僅在 macOS 13 及更高版本可用。"; /* No comment provided by engineer. */ -"Shared Directory" = "已共享目錄"; +"Shared Directory" = "已分享目錄"; /* UTMQemuConstants */ -"Shared Network" = "共享網絡"; +"Shared Network" = "已分享網絡"; /* No comment provided by engineer. */ -"Sharing" = "共享"; +"Sharing" = "分享"; /* No comment provided by engineer. */ "Show Advanced Settings" = "顯示進階設定"; @@ -906,7 +917,7 @@ "Socket not specified." = "未指定 socket。"; /* No comment provided by engineer. */ -"Specify the size of the drive where data will be stored into." = "指定資料儲存到的磁碟的大小。"; +"Specify the size of the drive where data will be stored into." = "指定資料儲存至的磁碟的大小。"; /* UTMQemuConstants */ "SPICE WebDAV" = "SPICE WebDAV"; @@ -942,7 +953,7 @@ "Suspend is not supported for virtualization." = "暫停功能不支援虛擬化。"; /* UTMQemuVirtualMachine */ -"Suspend is not supported when an emulated NVMe device is active." = "当模擬 NVMe 裝置處於啟用狀態時,不支援暫停功能。"; +"Suspend is not supported when an emulated NVMe device is active." = "当仿真 NVMe 裝置處於啟用狀態時,不支援暫停功能。"; /* UTMQemuVirtualMachine */ "Suspend is not supported when GPU acceleration is enabled." = "当 GPU 加速處於啟用狀態時,不支援暫停功能。"; @@ -969,7 +980,7 @@ "TCP Server Connection" = "TCP 伺服器連線"; /* VMDisplayWindowController */ -"Tells the VM process to shut down with risk of data corruption. This simulates holding down the power button on a PC." = "告知關閉虛擬電腦程序(有損毀資料的風險)。此操作模擬了按下 PC 上的電源按鈕。"; +"Tells the VM process to shut down with risk of data corruption. This simulates holding down the power button on a PC." = "告知虛擬電腦程序關閉,並有損毀資料的風險。此操作仿真了在 PC 上按下電源按鈕。"; /* No comment provided by engineer. */ "Test" = "測試"; @@ -987,7 +998,7 @@ "The device cannot be found." = "無法找到此裝置。"; /* UTMScriptingUSBDeviceImpl */ -"The device is not currently connected." = "此裝置當前未連線。"; +"The device is not currently connected." = "此裝置當前未連接。"; /* UTMConfiguration */ "The drive '%@' already exists and cannot be created." = "磁碟「%@」已經存在,無法製作。"; @@ -1014,7 +1025,7 @@ "The selected boot image contains the word '%@' but the guest architecture is '%@'. Please ensure you have selected an image that is compatible with '%@'." = "選取的啟動映像檔包含單字「%1$@」,但客戶端體系結構為「%2$@」。請確保你選取了與「%3$@」體系結構相容的映像檔。"; /* No comment provided by engineer. */ -"The target does not support hardware emulated serial connections." = "目標平台不支援硬件模擬序列連線。"; +"The target does not support hardware emulated serial connections." = "目標平台不支援硬件仿真序列連線。"; /* UTMQemuVirtualMachine */ "The virtual machine is in an invalid state." = "虛擬電腦處於無效狀態。"; @@ -1026,7 +1037,7 @@ "The virtual machine must be stopped before this operation can be performed." = "必須先停止虛擬電腦,然後才能執行此操作。"; /* Error shown when importing a ZIP file from web that doesn't contain a UTM Virtual Machine. */ -"There is no UTM file in the downloaded ZIP archive." = "在已下載的 ZIP 封存檔裡無 UTM 檔案。"; +"There is no UTM file in the downloaded ZIP archive." = "在已經下載的 ZIP 封存檔內未有 UTM 檔案。"; /* No comment provided by engineer. */ "This audio card is not supported." = "此聲卡不支援。"; @@ -1035,10 +1046,10 @@ "This backend is not supported on your machine." = "你的電腦不支援此後端。"; /* No comment provided by engineer. */ -"This build does not emulation." = "此 UTM 構建不支援模擬。"; +"This build does not emulation." = "此 UTM 構建不支援仿真。"; /* UTMQemuVirtualMachine */ -"This build of UTM does not support emulating the architecture of this VM." = "此 UTM 構建不支援模擬該虛擬電腦的體系結構。"; +"This build of UTM does not support emulating the architecture of this VM." = "此 UTM 構建不支援仿真該虛擬電腦的體系結構。"; /* VMConfigSystemView */ "This change will reset all settings" = "此變更會重設所有設定"; @@ -1053,13 +1064,13 @@ "This device is not supported by the target." = "目標不支援此裝置。"; /* VMConfigAppleSharingView */ -"This directory is already being shared." = "此目錄已經被共享。"; +"This directory is already being shared." = "此目錄已被分享。"; /* UTMAppleConfiguration */ "This is not a valid Apple Virtualization configuration." = "並非有效的 Apple 虛擬化設定。"; /* VMDisplayWindowController */ -"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "這可能會損毀虛擬電腦,任何未儲存的變更都將丟失。如要安全退出,請從客戶端關機。"; +"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "這可能會損毀虛擬電腦,任何未儲存的變更都將遺失。如要安全退出,請從客戶端關機。"; /* No comment provided by engineer. */ "This operating system is unsupported on your machine." = "你的電腦不支援此作業系統。"; @@ -1077,13 +1088,13 @@ "This virtual machine has been removed." = "此虛擬電腦已經被刪除。"; /* VMDisplayWindowController */ -"This will reset the VM and any unsaved state will be lost." = "這將重設虛擬電腦,任何未儲存的狀態都將丟失。"; +"This will reset the VM and any unsaved state will be lost." = "這將重設虛擬電腦,任何未儲存的狀態都將遺失。"; /* VMDisplayAppleWindowController */ -"To access the shared directory, the guest OS must have Virtiofs drivers installed. You can then run `sudo mount -t virtiofs share /path/to/share` to mount to the share path." = "要取用共享目錄,客戶端作業系統必須安裝 VirtioFS 驅動程式。然後,你可以執行「sudo mount -t virtiofs share /path/to/share」來裝載到共享路徑。"; +"To access the shared directory, the guest OS must have Virtiofs drivers installed. You can then run `sudo mount -t virtiofs share /path/to/share` to mount to the share path." = "要取用分享目錄,客戶端作業系統必須安裝 VirtioFS 驅動程式。然後,你可以執行「sudo mount -t virtiofs share /path/to/share」來裝載至分享路徑。"; /* VMMetalView */ -"To capture input or to release the capture, press Command and Option at the same time." = "要截取或放開輸入,請同時按下 Command + Option。"; +"To capture input or to release the capture, press Command and Option at the same time." = "要擷取或放開輸入,請同時按下 Command + Option。"; /* No comment provided by engineer. */ "To install macOS, you need to download a recovery IPSW. If you do not select an existing IPSW, the latest macOS IPSW will be downloaded from Apple." = "如要安裝 macOS,你需要下載 IPSW 恢復檔。如你未選取現有的 IPSW,將從 Apple 下載最新的 macOS IPSW。"; @@ -1091,9 +1102,6 @@ /* VMDisplayQemuMetalWindowController */ "To release the mouse cursor, press %@ at the same time." = "如要放開滑鼠指標,請同時按下 %@。"; -/* UTMAppleConfigurationDevices */ -"Trackpad" = "觸控板"; - /* No comment provided by engineer. */ "u{2022} " = "u{2022}"; @@ -1137,13 +1145,13 @@ "USB Device" = "USB 裝置"; /* No comment provided by engineer. */ -"USB Sharing" = "USB 共享"; +"USB Sharing" = "USB 分享"; /* No comment provided by engineer. */ -"USB sharing not supported in this build of UTM." = "此 UTM 構建不支援 USB 共享。"; +"USB sharing not supported in this build of UTM." = "此 UTM 構建不支援 USB 分享。"; /* No comment provided by engineer. */ -"Use Command+Option (⌘+⌥) for input capture/release" = "使用 Command + Option (⌘ + ⌥) 來截取/放開輸入"; +"Use Command+Option (⌘+⌥) for input capture/release" = "使用 Command + Option (⌘ + ⌥) 來擷取/放開輸入"; /* Welcome view */ "User Guide" = "用戶指南"; @@ -1181,7 +1189,7 @@ "VM display size is fixed" = "虛擬電腦顯示大小固定"; /* No comment provided by engineer. */ -"Waiting for VM to connect to display..." = "正在等待虛擬電腦連線到顯示..."; +"Waiting for VM to connect to display..." = "正在等待虛擬電腦連接至顯示..."; /* No comment provided by engineer. */ "Welcome to UTM" = "歡迎使用 UTM"; @@ -1193,27 +1201,818 @@ "Windows Guest Support Tools" = "Windows 客戶端支援工具"; /* VMQemuDisplayMetalWindowController */ -"Would you like to connect '%@' to this virtual machine?" = "你要連線「%@」到此虛擬電腦嗎?"; +"Would you like to connect '%@' to this virtual machine?" = "你要連接「%@」至此虛擬電腦嗎?"; /* VMDisplayAppleWindowController */ "Would you like to install macOS? If an existing operating system is already installed on the primary drive of this VM, then it will be erased." = "你要安裝 macOS 嗎?如果此虛擬電腦的主磁碟上已經安裝了現有的作業系統,則會將其清除。"; /* No comment provided by engineer. */ -"Would you like to re-convert this disk image to reclaim unused space and apply compression? Note this will require enough temporary space to perform the conversion. Compression only applies to existing data and new data will still be written uncompressed. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間並應用壓縮嗎?請注意,這將需要足夠的臨時空間來執行轉換,壓縮僅適用於現有資料,新資料仍將以未壓縮寫入。強烈建議你先備份此虛擬電腦,然後再繼續操作。"; +"Would you like to re-convert this disk image to reclaim unused space and apply compression? Note this will require enough temporary space to perform the conversion. Compression only applies to existing data and new data will still be written uncompressed. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間並應用壓縮嗎?請緊記,這將需要足夠的臨時空間來執行轉換,壓縮僅適用於現有資料,新資料仍將以未壓縮寫入。強烈建議你先備份此虛擬電腦,然後再繼續操作。"; /* No comment provided by engineer. */ -"Would you like to re-convert this disk image to reclaim unused space? Note this will require enough temporary space to perform the conversion. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間嗎?請注意,這將需要足夠的臨時空間來執行轉換。強烈建議你先備份此虛擬電腦,然後再繼續操作。"; +"Would you like to re-convert this disk image to reclaim unused space? Note this will require enough temporary space to perform the conversion. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間嗎?請緊記,這將需要足夠的臨時空間來執行轉換。強烈建議你先備份此虛擬電腦,然後再繼續操作。"; /* No comment provided by engineer. */ "Yes" = "是"; /* VMConfigSystemView */ -"Your device has %llu MB of memory and the estimated usage is %llu MB." = "你的裝置有 %1$llu MB 的記憶體,大約使用量為 %2$llu MB。"; +"Your device has %llu MB of memory and the estimated usage is %llu MB." = "你的裝置有 %1$llu MB 大小的記憶體,大約使用量為 %2$llu MB。"; /* VMConfigAppleBootView VMWizardOSMacView */ "Your machine does not support running this IPSW." = "你的電腦不支援執行此 IPSW。"; /* ContentView */ -"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." = "你的 iOS 版本不支援在未作更動的情況下執行虛擬電腦,必須在越獄 (jailbreak) 時執行 UTM,或是在附加遠程除錯器的情況下執行 UTM。有關更多詳細訊息,請參閲 https://getutm.app/install/。"; +"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." = "你的 iOS 版本不支援在未作更動的情況下執行虛擬電腦,必須在越獄 (jailbreak) 時執行 UTM,或是在附加遠程除錯器的情況下執行 UTM。有關更多詳細訊息,請見 https://getutm.app/install/。"; + +// Additonal Strings (Unable to be exported by Xcode) + +/* No comment provided by engineer. */ +"(Delete)" = "(刪除)"; + +/* No comment provided by engineer. */ +"Add" = "加入"; + +/* No comment provided by engineer. */ +"Add a new device." = "加入一個新裝置。"; + +/* No comment provided by engineer. */ +"Add a new drive." = "加入一個新磁碟。"; + +/* No comment provided by engineer. */ +"Add read only" = "加入唯讀"; + +/* No comment provided by engineer. */ +"Advanced. If checked, a raw disk image is used. Raw disk image does not support snapshots and will not dynamically expand in size." = "進階選項。如選取,將會使用 Raw 磁碟映像。Raw 磁碟映像不支援快照,也不會動態擴充套件大小。"; + +/* No comment provided by engineer. */ +"Allow Remote Connection" = "允許遠端連線"; + +/* No comment provided by engineer. */ +"Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "允許透過觸控板額外輸入。僅支援 macOS 13+ 客戶端。"; + +/* No comment provided by engineer. */ +"Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "Apple 虛擬化為試驗性質,僅可用作進階用例,不選取此剔選框以使用推介的 QEMU。"; + +/* No comment provided by engineer. */ +"Application" = "應用程式"; + +/* No comment provided by engineer. */ +"Architecture" = "體系結構"; + +/* No comment provided by engineer. */ +"Arguments" = "參數"; + +/* No comment provided by engineer. */ +"Auto Resolution" = "自動調整解像度"; + +/* No comment provided by engineer. */ +"Automatic" = "自動"; + +/* No comment provided by engineer. */ +"Background Color" = "背景顏色"; + +/* No comment provided by engineer. */ +"Balloon Device" = "Balloon 裝置"; + +/* No comment provided by engineer. */ +"Blinking cursor?" = "閃爍指標?"; + +/* No comment provided by engineer. */ +"Boot arguments" = "啟動參數"; + +/* No comment provided by engineer. */ +"Boot Arguments" = "啟動參數"; + +/* No comment provided by engineer. */ +"Boot from kernel image" = "由核心映像檔啟動"; + +/* No comment provided by engineer. */ +"Boot Image" = "啟動映像檔"; + +/* No comment provided by engineer. */ +"Boot Image Type" = "啟動映像檔類型"; + +/* No comment provided by engineer. */ +"Boot into recovery mode." = "啟動至還原模式。"; + +/* No comment provided by engineer. */ +"Bootloader" = "Bootloader"; + +/* No comment provided by engineer. */ +"Bridged Interface" = "橋連介面"; + +/* No comment provided by engineer. */ +"Bridged Settings" = "橋連設定"; + +/* No comment provided by engineer. */ +"By default, the best backend for the target will be used. If the selected backend is not available for any reason, an alternative will automatically be selected." = "於預設情況下,將會使用目標的最好後端。如果所選後端由於任何原因不可用,將會自動選取替代方案。"; + +/* No comment provided by engineer. */ +"By default, the best renderer for this device will be used. You can override this with to always use a specific renderer. This only applies to QEMU VMs with GPU accelerated graphics." = "於預設情況下,將會使用最適合此裝置的渲染器。你可以覆蓋它以始終使用特定的渲染器,這僅適用於具有 GPU 加速圖形的 QEMU 虛擬電腦。"; + +/* No comment provided by engineer. */ +"Calculating current size..." = "計算現時大小⋯"; + +/* No comment provided by engineer. */ +"Cancel Download" = "取消下載"; + +/* No comment provided by engineer. */ +"Clipboard Sharing" = "剪貼板分享"; + +/* No comment provided by engineer. */ +"Clone" = "複製"; + +/* No comment provided by engineer. */ +"Clone selected VM" = "複製已選取的虛擬電腦"; + +/* No comment provided by engineer. */ +"Clone…" = "複製⋯"; + +/* No comment provided by engineer. */ +"Close" = "關閉"; + +/* No comment provided by engineer. */ +"Closing a VM without properly shutting it down could result in data loss." = "在未正確關閉電源的情況下關閉虛擬電腦可能會導致資料遺失。"; + +/* No comment provided by engineer. */ +"Compress" = "壓縮"; + +/* No comment provided by engineer. */ +"Compress by re-converting the disk image and compressing the data." = "透過重新轉換磁碟映像檔與壓縮資料來壓縮。"; + +/* No comment provided by engineer. */ +"Create a new VM" = "製作一個新虛擬電腦"; + +/* No comment provided by engineer. */ +"Create a new VM with the same configuration as this one but without any data." = "製作一個與此配置相同的新虛擬電腦,但無任何資料。"; + +/* No comment provided by engineer. */ +"Create an empty drive." = "製作一個空磁碟。"; + +/* No comment provided by engineer. */ +"Debian Install Guide" = "Debian 安裝指引"; + +/* No comment provided by engineer. */ +"Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!" = "預設值為記憶體大小的 1/4 (見上)。JIT 快取資料大小亦會包括於全部的記憶體使用量當中!"; + +/* No comment provided by engineer. */ +"Delete this drive." = "刪除此磁碟。"; + +/* No comment provided by engineer. */ +"Delete selected VM" = "刪除已選取的虛擬電腦"; + +/* No comment provided by engineer. */ +"Delete this shortcut. The underlying data will not be deleted." = "刪除此捷徑。當中的資料不會被刪除。"; + +/* No comment provided by engineer. */ +"Delete this VM and all its data." = "刪除此虛擬電腦與其所有資料。"; + +/* No comment provided by engineer. */ +"Delete Drive" = "刪除磁碟機"; + +/* No comment provided by engineer. */ +"Description" = "註解"; + +/* No comment provided by engineer. */ +"Devices" = "裝置"; + +/* No comment provided by engineer. */ +"Directory" = "目錄"; + +/* No comment provided by engineer. */ +"Directory Share Mode" = "目錄分享模式"; + +/* No comment provided by engineer. */ +"Disk" = "磁碟"; + +/* No comment provided by engineer. */ +"DHCP Domain Name" = "DHCP 網域名稱"; + +/* No comment provided by engineer. */ +"DHCP End" = "DHCP 結束"; + +/* No comment provided by engineer. */ +"DNS Search Domains" = "DNS 搜尋網域"; + +/* No comment provided by engineer. */ +"DNS Server" = "DNS 伺服器"; + +/* No comment provided by engineer. */ +"DNS Server (IPv6)" = "DNS 伺服器 (IPv6)"; + +/* No comment provided by engineer. */ +"DHCP Start" = "DHCP 開始"; + +/* No comment provided by engineer. */ +"Done" = "完成"; + +/* No comment provided by engineer. */ +"Duplicate this VM along with all its data." = "複製此虛擬電腦及其所有資料。"; + +/* No comment provided by engineer. */ +"Download and mount the guest support package for Windows. This is required for some features including dynamic resolution and clipboard sharing." = "下載並裝載 Windows 的客戶端支援套件。這對於一些功能為必需,當中包括動態解像度與剪貼板分享。"; + +/* No comment provided by engineer. */ +"Download and mount the guest tools for Windows." = "下載並裝載 Windows 的客戶端工具。"; + +/* No comment provided by engineer. */ +"Download Windows 11 for ARM64 Preview VHDX" = "下載 Windows 11 ARM64 Preview VHDX 映像檔"; + +/* No comment provided by engineer. */ +"Downscaling" = "細化解像度"; + +/* No comment provided by engineer. */ +"Edit" = "編輯"; + +/* No comment provided by engineer. */ +"Edit selected VM" = "編輯已選取的虛擬電腦"; + +/* No comment provided by engineer. */ +"Edit…" = "編輯⋯"; + +/* No comment provided by engineer. */ +"Emulated Audio Card" = "仿真聲卡"; + +/* No comment provided by engineer. */ +"Emulated Display Card" = "仿真顯卡"; + +/* No comment provided by engineer. */ +"Emulated Network Card" = "仿真網卡"; + +/* No comment provided by engineer. */ +"Emulated Serial Device" = "仿真序列裝置"; + +/* No comment provided by engineer. */ +"Enable Balloon Device" = "啟用 Balloon 裝置"; + +/* No comment provided by engineer. */ +"Enable Entropy Device" = "啟用 Entropy 裝置"; + +/* No comment provided by engineer. */ +"Enable hardware OpenGL acceleration" = "啟用硬體 OpenGL 加速"; + +/* No comment provided by engineer. */ +"Enable Keyboard" = "啟用鍵盤"; + +/* No comment provided by engineer. */ +"Enable Pointer" = "啟用指標"; + +/* No comment provided by engineer. */ +"Enable Rosetta (x86_64 Emulation)" = "啟用 Rosetta (x86_64 仿真)"; + +/* No comment provided by engineer. */ +"Enable Rosetta on Linux (x86_64 Emulation)" = "於 Linux 中啟用 Rosetta (x86_64 仿真)"; + +/* No comment provided by engineer. */ +"Enable Sound" = "啟用聲音"; + +/* No comment provided by engineer. */ +"Engine" = "引擎"; + +/* No comment provided by engineer. */ +"Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments." = "將所有參數輸出為一個文字文件。這僅用於除錯目的,因為 UTM 的內建 QEMU 在支援的參數上與上游的 QEMU 不同。"; + +/* No comment provided by engineer. */ +"Export Debug Log" = "輸出除錯記錄"; + +/* No comment provided by engineer. */ +"External Drive" = "外部磁碟"; + +/* No comment provided by engineer. */ +"Fetch latest Windows installer…" = "取得最新的 Windows 安裝工具⋯"; + +/* No comment provided by engineer. */ +"Font" = "字體"; + +/* No comment provided by engineer. */ +"Force Disable CPU Flags" = "強制停用 CPU 標記"; + +/* No comment provided by engineer. */ +"Force Enable CPU Flags" = "強制啟用 CPU 標記"; + +/* No comment provided by engineer. */ +"Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation." = "強行多核心可能會提高仿真速度,但也會導致不穩定與錯誤的仿真。"; + +/* No comment provided by engineer. */ +"Force PS/2 controller" = "強行使用 PS/2 控制器"; + +/* No comment provided by engineer. */ +"FPS Limit" = "FPS 限制"; + +/* No comment provided by engineer. */ +"Go Back" = "返回"; + +/* No comment provided by engineer. */ +"GPU Acceleration Supported" = "支援 GPU 加速"; + +/* No comment provided by engineer. */ +"Guest Address" = "客戶端位址"; + +/* No comment provided by engineer. */ +"Guest Network" = "客戶端網絡"; + +/* No comment provided by engineer. */ +"Guest Network (IPv6)" = "客戶端網絡 (IPv6)"; + +/* No comment provided by engineer. */ +"Guest Port" = "客戶端埠"; + +/* No comment provided by engineer. */ +"Hardware interface on the guest used to mount this image. Different operating systems support different interfaces. The default will be the most common interface." = "用作裝載此映像的客戶端硬體介面。不同的作業系統支援不同的介面。預設將會設定為最常見的介面。"; + +/* No comment provided by engineer. */ +"Hardware OpenGL Acceleration" = "硬體 OpenGL 加速"; + +/* No comment provided by engineer. */ +"Height" = "高度"; + +/* No comment provided by engineer. */ +"Hide" = "隱藏"; + +/* No comment provided by engineer. */ +"Hide dock icon on next launch" = "下次啟動時隱藏 Dock 圖示"; + +/* No comment provided by engineer. */ +"Host Address" = "主機位址"; + +/* No comment provided by engineer. */ +"Host Address (IPv6)" = "主機位址 (IPv6)"; + +/* No comment provided by engineer. */ +"Host Port" = "主機埠"; + +/* No comment provided by engineer. */ +"If checked, no drive image will be stored with the VM. Instead you can mount/unmount image while the VM is running." = "如選取,將不會儲存磁碟映像檔至虛擬電腦內。然而,你可以於虛擬電腦執行時裝載/卸除安裝映像。"; + +/* No comment provided by engineer. */ +"If checked, the CPU flag will be enabled. Otherwise, the default value will be used." = "如選取,將會啟用此 CPU 標記。否則將會使用預設值。"; + +/* No comment provided by engineer. */ +"If checked, the CPU flag will be disabled. Otherwise, the default value will be used." = "如選取,將會停用此 CPU 標記。否則將會使用預設值。"; + +/* No comment provided by engineer. */ +"If checked, the drive image will be stored with the VM." = "如選取,磁碟映像檔將會與虛擬電腦一齊儲存。"; + +/* No comment provided by engineer. */ +"If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock." = "如選取,將會使用 Windows 所需要的 RTC 本地時間。否則將會使用 UTC 時鐘。"; + +/* No comment provided by engineer. */ +"If disabled, the default combination Control+Option (⌃+⌥) will be used." = "如停用,將會使用預設組合鍵 Control + Option (⌃ + ⌥)。"; + +/* No comment provided by engineer. */ +"If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64." = "如啟用,標記為「rosetta」的 virtiofs 分享將會於 Linux 客戶端上可用,用作安裝 Rosetta,可以於 arm64 上仿真 x86_64。"; + +/* No comment provided by engineer. */ +"If enabled, any existing screenshot will be deleted the next time the VM is started." = "如啟用,下次啟動虛擬電腦時,任何現存的快照將會被刪除。"; + +/* No comment provided by engineer. */ +"If enabled, caps lock will be handled like other keys. If disabled, it is treated as a toggle that is synchronized with the host." = "如啟用,Caps Lock 將會同其他鍵一樣處理。如停用,它將會被視為開關鍵,並與主機同步。"; + +/* No comment provided by engineer. */ +"If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "如啟用,於進入和離開全螢幕模式時,將會自動切換輸入擷取。"; + +/* No comment provided by engineer. */ +"If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "如啟用,Num Lock 將會始終對客戶端開啟。請緊記,這可能會令鍵盤的 Num Lock 指示器不同步。"; + +/* No comment provided by engineer. */ +"If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "如啟用,Option 鍵將會對映至 Meta 鍵,這對於 Emacs 很有用。否則,Option 鍵將會以系統預設工作(例如輸入國際文字)。"; + +/* No comment provided by engineer. */ +"If enabled, resizing of the VM window will not be allowed." = "如啟用,將不會允許調整虛擬電腦視窗的大小。"; + +/* No comment provided by engineer. */ +"If enabled, scroll wheel input will be inverted." = "如啟用,將會反轉滾輪輸入。"; + +/* No comment provided by engineer. */ +"If enabled, the default input devices will be emulated on the USB bus." = "如啟用,預設輸入裝置將會於 USB 匯流排上仿真。"; + +/* No comment provided by engineer. */ +"If set, a frame limit can improve smoothness in rendering by preventing stutters when set to the lowest value your device can handle." = "如設定了幀限制,則當設定為你的裝置可以處理的最低值時,幀限制可以防止卡頓,藉此提升渲染的平滑度。"; + +/* No comment provided by engineer. */ +"If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO." = "如設定,直接由 Raw 核心映像檔與 initrd 啟動。否則由受支援的 ISO 啟動。"; + +/* No comment provided by engineer. */ +"Image Type" = "映像檔類型"; + +/* No comment provided by engineer. */ +"Import Drive" = "輸入磁碟機"; + +/* No comment provided by engineer. */ +"Import VHDX Image" = "輸入 VHDX 映像檔"; + +/* No comment provided by engineer. */ +"Increase the size of the disk image." = "增加磁碟映像檔的大小。"; + +/* No comment provided by engineer. */ +"Initial Ramdisk" = "初始 ramdisk"; + +/* No comment provided by engineer. */ +"Input" = "輸入"; + +/* No comment provided by engineer. */ +"Install drivers and SPICE tools" = "安裝驅動程式與 SPICE 工具"; + +/* No comment provided by engineer. */ +"Install Windows 10 or higher" = "安裝 Windows 10 或者更高版本"; + +/* No comment provided by engineer. */ +"Installation Instructions" = "安裝指引"; + +/* No comment provided by engineer. */ +"Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "即便支援 USB 輸入,仍然例項化 PS/2 控制器。此項對於舊版 Windows 為必需。"; + +/* No comment provided by engineer. */ +"Interface" = "介面"; + +/* No comment provided by engineer. */ +"IPSW Install Image" = "IPSW 安裝映像檔"; + +/* No comment provided by engineer. */ +"JIT Cache" = "JIT 快取資料"; + +/* No comment provided by engineer. */ +"Kernel" = "核心"; + +/* No comment provided by engineer. */ +"Kernel Image" = "核心映像檔"; + +/* No comment provided by engineer. */ +"Keyboard" = "鍵盤"; + +/* No comment provided by engineer. */ +"MAC Address" = "MAC 位址"; + +/* No comment provided by engineer. */ +"Machine" = "電腦"; + +/* No comment provided by engineer. */ +"Maintenance" = "維護"; + +/* No comment provided by engineer. */ +"Mode" = "模式"; + +/* No comment provided by engineer. */ +"Modify settings for this VM." = "修改此虛擬電腦的設定。"; + +/* UTMAppleConfigurationDevices */ +"Mouse" = "滑鼠"; + +/* No comment provided by engineer. */ +"Move" = "移動"; + +/* No comment provided by engineer. */ +"Move…" = "移動⋯"; + +/* No comment provided by engineer. */ +"Move selected VM" = "移動已選取的虛擬電腦"; + +/* No comment provided by engineer. */ +"Move this VM from internal storage to elsewhere." = "將此虛擬電腦由內部儲存空間移動至其他地方。"; + +/* No comment provided by engineer. */ +"Network" = "網絡"; + +/* No comment provided by engineer. */ +"Network Mode" = "網絡模式"; + +/* No comment provided by engineer. */ +"New Drive" = "新增磁碟機"; + +/* No comment provided by engineer. */ +"New from template…" = "由此範本新增⋯"; + +/* No comment provided by engineer. */ +"New Shared Directory…" = "新增分享目錄⋯"; + +/* No comment provided by engineer. */ +"New VM" = "新增虛擬電腦"; + +/* No comment provided by engineer. */ +"Older versions of UTM added each IDE device to a separate bus. Check this to change the configuration to place two units on each bus." = "舊版本的 UTM 將每個 IDE 裝置加至單獨的匯流排中。檢查此項以更改配置,以便於每個匯流排上放置兩個單元。"; + +/* No comment provided by engineer. */ +"Only available if host architecture matches the target. Otherwise, TCG emulation is used." = "僅當主機架構與目標匹配時才可用。否則,將使用 TCG 仿真。"; + +/* No comment provided by engineer. */ +"Only available on macOS virtual machines." = "僅可用於 macOS 虛擬電腦。"; + +/* No comment provided by engineer. */ +"Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general." = "僅當於受支援的硬體上使用 Hypervisor 時才可用。TSO 提升了客戶端的 Intel 仿真速度,但以總體的效能降低為代價。"; + +/* No comment provided by engineer. */ +"Open VM Settings" = "開啟虛擬電腦設定"; + +/* No comment provided by engineer. */ +"Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details." = "(可選) 選取一個目錄,使得可以在虛擬電腦內取用。請緊記,分享目錄的支援視乎客戶端作業系統而定,可能需要安裝額外的客戶端驅動程式。有關更多詳細訊息,請見 UTM 支援頁面。"; + +/* No comment provided by engineer. */ +"Options here only apply on next boot and are not saved." = "此處的選項僅於下次啟動時生效,且不會儲存。"; + +/* No comment provided by engineer. */ +"Path" = "路徑"; + +/* No comment provided by engineer. */ +"Port" = "埠"; + +/* No comment provided by engineer. */ +"Power Off" = "關閉電源"; + +/* No comment provided by engineer. */ +"Prompt" = "提示"; + +/* No comment provided by engineer. */ +"Protocol" = "協定"; + +/* No comment provided by engineer. */ +"QEMU Machine Properties" = "QEMU 電腦屬性"; + +/* No comment provided by engineer. */ +"Quit" = "結束"; + +/* No comment provided by engineer. */ +"RAM" = "記憶體"; + +/* No comment provided by engineer. */ +"Ramdisk (optional)" = "Ramdisk (選填)"; + +/* No comment provided by engineer. */ +"Random" = "隨機"; + +/* No comment provided by engineer. */ +"Read Only?" = "唯讀?"; + +/* No comment provided by engineer. */ +"Reclaim disk space by re-converting the disk image." = "透過重新轉換來回收磁碟空間。"; + +/* No comment provided by engineer. */ +"Reclaim Space" = "釋放空間"; + +/* No comment provided by engineer. */ +"Remove selected shortcut" = "移除已選取的捷徑"; + +/* No comment provided by engineer. */ +"Renderer Backend" = "渲染器後端"; + +/* No comment provided by engineer. */ +"Requires restarting UTM to take affect." = "需要重新開啟 UTM 以生效。"; + +/* No comment provided by engineer. */ +"Requires SPICE guest agent tools to be installed." = "需要安裝 SPICE 客戶端代理程式工具。"; + +/* No comment provided by engineer. */ +"Reset UEFI Variables" = "重設 UEFI 變數"; + +/* No comment provided by engineer. */ +"Resize Console Command" = "調整主控台大小指令"; + +/* No comment provided by engineer. */ +"Resize…" = "調整大小⋯"; + +/* No comment provided by engineer. */ +"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %lld GiB?" = "調整大小為實驗性功能,可能會導致資料遺失。強烈建議你在繼續之前備份此虛擬電腦。你要將大小調整為 %lld GB 嗎?"; + +/* No comment provided by engineer. */ +"Resolution" = "解像度"; + +/* No comment provided by engineer. */ +"Restart" = "重新啟動"; + +/* No comment provided by engineer. */ +"Resume" = "繼續"; + +/* No comment provided by engineer. */ +"Resume running VM." = "繼續正在執行的虛擬電腦。"; + +/* No comment provided by engineer. */ +"Reveal where the VM is stored." = "顯示虛擬電腦的儲存位置。"; + +/* No comment provided by engineer. */ +"RNG Device" = "RNG 裝置"; + +/* No comment provided by engineer. */ +"Root Image" = "Root 映像檔"; + +/* No comment provided by engineer. */ +"Run" = "執行"; + +/* No comment provided by engineer. */ +"Run Recovery" = "執行 Recovery 模式"; + +/* No comment provided by engineer. */ +"Run selected VM" = "執行已選取的虛擬電腦"; + +/* No comment provided by engineer. */ +"Run the VM in the foreground." = "在螢幕前執行虛擬電腦。"; + +/* No comment provided by engineer. */ +"Run the VM in the foreground, without saving data changes to disk." = "在螢幕前執行虛擬電腦,但無需將資料更改儲存到磁碟。"; + +/* No comment provided by engineer. */ +"Run without saving changes" = "執行但不儲存更改"; + +/* No comment provided by engineer. */ +"Section" = "區域"; + +/* No comment provided by engineer. */ +"Secure Boot with TPM 2.0" = "使用 TPM 2.0 的保安啟動"; + +/* No comment provided by engineer. */ +"Select an existing disk image." = "選取一個現存的磁碟映像。"; + +/* No comment provided by engineer. */ +"Serial" = "序列"; + +/* No comment provided by engineer. */ +"Server Address" = "伺服器位址"; + +/* No comment provided by engineer. */ +"Settings" = "設定"; + +/* No comment provided by engineer. */ +"Share" = "分享"; + +/* No comment provided by engineer. */ +"Share…" = "分享⋯"; + +/* No comment provided by engineer. */ +"Share a copy of this VM and all its data." = "分享此虛擬電腦與其所有資料的複製。"; + +/* No comment provided by engineer. */ +"Share Directory" = "分享目錄"; + +/* No comment provided by engineer. */ +"Share is read only" = "分享資料唯讀"; + +/* No comment provided by engineer. */ +"Share selected VM" = "分享已選取的虛擬電腦"; + +/* No comment provided by engineer. */ +"Shared Directory Path" = "分享目錄路徑"; + +/* No comment provided by engineer. */ +"Shared Path" = "分享路徑"; + +/* No comment provided by engineer. */ +"Should be off for older operating systems such as Windows 7 or lower." = "對於較舊的作業系統應關閉,例如 Windows 7 或者更低版本。"; + +/* No comment provided by engineer. */ +"Should be on always unless the guest cannot boot because of this." = "除非客戶端因此而無法啟動,否則應始終開啟。"; + +/* No comment provided by engineer. */ +"Show all devices…" = "顯示所有裝置⋯"; + +/* No comment provided by engineer. */ +"Show in Finder" = "在 Finder 中顯示"; + +/* No comment provided by engineer. */ +"Show the main window." = "顯示主視窗。"; + +/* No comment provided by engineer. */ +"Show UTM" = "顯示 UTM"; + +/* No comment provided by engineer. */ +"Show UTM preferences" = "顯示 UTM 偏好設定"; + +/* No comment provided by engineer. */ +"Skip Boot Image" = "略過啟動映像檔"; + +/* No comment provided by engineer. */ +"Skip ISO boot" = "略過 ISO 啟動"; + +/* No comment provided by engineer. */ +"Some older systems do not support UEFI boot, such as Windows 7 and below." = "一些舊版系統不支援 UEFI 啟動,比如 Windows 7 及更舊版本。"; + +/* No comment provided by engineer. */ +"Sound" = "聲音"; + +/* No comment provided by engineer. */ +"Sound Backend" = "聲音後端"; + +/* No comment provided by engineer. */ +"Start" = "開始"; + +/* No comment provided by engineer. */ +"Status" = "狀態"; + +/* No comment provided by engineer. */ +"Stop selected VM" = "停止已選取的虛擬電腦"; + +/* No comment provided by engineer. */ +"Stop the running VM." = "停止正在執行的虛擬電腦。"; + +/* No comment provided by engineer. */ +"Storage" = "儲存空間"; + +/* No comment provided by engineer. */ +"stty cols $COLS rows $ROWS\n" = "stty cols $COLS rows $ROWS\n"; + +/* No comment provided by engineer. */ +"Suspend" = "暫停"; + +/* No comment provided by engineer. */ +"Target" = "目標"; + +/* No comment provided by engineer. */ +"Terminate UTM and stop all running VMs." = "終止 UTM 並停止所有正在執行的虛擬電腦。"; + +/* No comment provided by engineer. */ +"Text" = "文字"; + +/* No comment provided by engineer. */ +"Text Color" = "文字顏色"; + +/* No comment provided by engineer. */ +"The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "為此映像檔分配的儲存量。於輸入映像檔時會忽略此參數。如果此為一個 Raw 映像檔,則此大小的空檔案將會與虛擬電腦一齊儲存。否則,磁碟映像檔將會動態擴充至此大小。"; + +/* No comment provided by engineer. */ +"Theme" = "主題"; + +/* No comment provided by engineer. */ +"There are known issues in some newer Linux drivers including black screen, broken compositing, and apps failing to render." = "一些較新的 Linux 驅動程式存在已知問題,當中包括螢幕變黑、顯示合成損毀,以及應用程式無法渲染。"; + +/* No comment provided by engineer. */ +"These are advanced settings affecting QEMU which should be kept default unless you are running into issues." = "這些為影響 QEMU 的進階設定,除非遇到問題,否則你應當保持預設值。"; + +/* No comment provided by engineer. */ +"This is appended to the -machine argument." = "這會加至 -machine 參數的尾端。"; + +/* No comment provided by engineer. */ +"This virtual machine cannot be found at: %@" = "虛擬電腦無法於此處找到:%@"; + +/* No comment provided by engineer. */ +"This virtual machine must be re-added to UTM by opening it with Finder. You can find it at the path: %@" = "必須使用 Finder 開啟此虛擬電腦,將其重新加至 UTM 中。你可以於此路徑中找到它:%@"; + +/* No comment provided by engineer. */ +"TPM 2.0 Device" = "TPM 2.0 裝置"; + +/* No comment provided by engineer. */ +"TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided." = "TPM 可以用來保護客戶端作業系統中的私密資料。請緊記,主機將始終可以讀取這些私密資料,因此無法提供預期的物理保安性。"; + +/* UTMAppleConfigurationDevices */ +"Trackpad" = "觸控板"; + +/* No comment provided by engineer. */ +"Tweaks" = "調整"; + +/* No comment provided by engineer. */ +"Ubuntu Install Guide" = "Ubuntu 安裝指引"; + +/* No comment provided by engineer. */ +"UEFI Boot" = "UEFI 啟動"; + +/* No comment provided by engineer. */ +"Upscaling" = "粗化解像度"; + +/* No comment provided by engineer. */ +"USB Support" = "USB 支援"; + +/* No comment provided by engineer. */ +"Use Apple Virtualization" = "使用 Apple 虛擬化"; + +/* No comment provided by engineer. */ +"Use Hypervisor" = "使用 Hypervisor"; + +/* No comment provided by engineer. */ +"Use local time for base clock" = "使用本地時間作為基本時鐘"; + +/* No comment provided by engineer. */ +"Use Rosetta" = "使用 Rosetta"; + +/* No comment provided by engineer. */ +"Use Trackpad" = "使用觸控板"; + +/* No comment provided by engineer. */ +"Use TSO" = "使用 TSO"; + +/* No comment provided by engineer. */ +"Use Virtualization" = "使用虛擬化"; + +/* No comment provided by engineer. */ +"VGA Device RAM (MB)" = "VGA 裝置記憶體 (MB)"; + +/* No comment provided by engineer. */ +"Virtualization" = "虛擬化"; + +/* No comment provided by engineer. */ +"Virtualization Engine" = "虛擬化引擎"; + +/* No comment provided by engineer. */ +"Wait for Connection" = "等待連線"; + +/* No comment provided by engineer. */ +"WebDAV requires installing SPICE daemon. VirtFS requires installing device drivers." = "WebDAV 需要安裝 SPICE 守護程式。VirtFS 需要安裝裝置驅動程式。"; + +/* No comment provided by engineer. */ +"Width" = "寬度"; + +/* No comment provided by engineer. */ +"Windows Install Guide" = "Windows 安裝指引"; + +/* No comment provided by engineer. */ +"You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot." = "如果你的引導選項已經損毀,或者你希望重新登入保安啟動的預設鑰匙,可以使用此選項。"; + +/* No comment provided by engineer. */ +"Zoom" = "縮放"; diff --git a/Platform/zh-Hans.lproj/Localizable.strings b/Platform/zh-Hans.lproj/Localizable.strings index 85c2e7145..875129212 100644 --- a/Platform/zh-Hans.lproj/Localizable.strings +++ b/Platform/zh-Hans.lproj/Localizable.strings @@ -66,13 +66,13 @@ "Advanced" = "高级"; /* VMConfigSystemView */ -"Allocating too much memory will crash the VM." = "分配过多的内存会导致虚拟机崩溃。"; +"Allocating too much memory will crash the VM." = "分配过多内存会使虚拟机崩溃。"; /* UTMData */ -"AltJIT error: %@" = "AltJIT 错误: %@"; +"AltJIT error: %@" = "AltJIT 错误:%@"; /* UTMData */ -"An existing virtual machine already exists with this name." = "已经存在一个具有此名称的虚拟机。"; +"An existing virtual machine already exists with this name." = "已存在一个有此名称的虚拟机。"; /* UTMConfiguration */ "An internal error has occurred." = "发生了内部错误。"; @@ -164,7 +164,7 @@ "Cancel" = "取消"; /* UTMAppleVirtualMachine */ -"Cannot access resource: %@" = "无法访问资源: %@"; +"Cannot access resource: %@" = "无法访问资源:%@"; /* UTMSWTPM */ "Cannot access TPM data." = "无法访问 TPM 数据。"; @@ -173,10 +173,10 @@ "Cannot create virtual terminal." = "无法创建虚拟终端。"; /* UTMData */ -"Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled." = "找不到用于 JIT 启用的 AltServer。在启用 JIT 之前,你无法运行虚拟机。"; +"Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled." = "找不到用于 JIT 启用的 AltServer。在启用 JIT 之前,你将无法运行虚拟机。"; /* UTMData */ -"Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM." = "无法导入此虚拟机。此虚拟机配置无效,或是在较新版本的 UTM 中创建,或是在与此版本的 UTM 不兼容的平台上创建。"; +"Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM." = "无法导入此虚拟机。此虚拟机可能配置无效,或者可能是在较新版本的 UTM 中创建的,也可能是在与此版本的 UTM 不兼容的平台上创建的。"; /* No comment provided by engineer. */ "Caps Lock (⇪) is treated as a key" = "将 Caps Lock (⇪) 视为按键"; @@ -188,7 +188,7 @@ "Capture input automatically when entering full screen" = "进入全屏时自动捕获输入"; /* VMDisplayQemuMetalWindowController */ -"Captured mouse" = "鼠标已捕获"; +"Captured mouse" = "已捕获鼠标"; /* Configuration boot device */ "CD/DVD" = "CD/DVD"; @@ -215,7 +215,8 @@ /* No comment provided by engineer. */ "Confirm Delete" = "确认删除"; -/* VMDisplayWindowController */ +/* AppDelegate + VMDisplayWindowController */ "Confirmation" = "确认"; /* No comment provided by engineer. */ @@ -281,13 +282,13 @@ "Display" = "显示"; /* VMDisplayQemuDisplayController */ -"Display %lld: %@" = "显示 %1$lld: %2$@"; +"Display %lld: %@" = "显示 %1$lld:%2$@"; /* VMDisplayQemuDisplayController */ "Disposable Mode" = "一次性模式"; /* No comment provided by engineer. */ -"Do not save VM screenshot to disk" = "不要将虚拟机屏幕截图保存到磁盘"; +"Do not save VM screenshot to disk" = "不将虚拟机的屏幕截图保存到磁盘"; /* No comment provided by engineer. */ "Do not show confirmation when closing a running VM" = "关闭正在运行的虚拟机时不显示确认"; @@ -308,16 +309,16 @@ "Do you want to force stop this VM and lose all unsaved data?" = "要强制停止此虚拟机并丢失所有未保存的数据吗?"; /* No comment provided by engineer. */ -"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "要将此虚拟机移动到别处吗?这将把数据复制到新位置,从原始位置删除数据,然后创建一个快捷方式。"; +"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "要将此虚拟机移动到别处吗?这将会复制数据到新位置,删除原始位置的数据,然后创建一个快捷方式。"; /* No comment provided by engineer. */ -"Do you want to remove this shortcut? The data will not be deleted." = "要删除这个快捷方式吗?数据不会被删除。"; +"Do you want to remove this shortcut? The data will not be deleted." = "要删除此快捷方式吗?数据不会被删除。"; /* No comment provided by engineer. */ -"Download prebuilt from UTM Gallery…" = "从 UTM 库中下载预构建的虚拟机…"; +"Download prebuilt from UTM Gallery…" = "从 UTM 库中下载预构建虚拟机…"; /* No comment provided by engineer. */ -"Drag and drop IPSW file here" = "在此处拖放 IPSW 文件"; +"Drag and drop IPSW file here" = "拖放 IPSW 文件到此处"; /* UTMScriptingConfigImpl */ "Drive description is invalid." = "驱动器描述无效。"; @@ -362,7 +363,7 @@ "Failed to access shared directory." = "无法访问共享目录。"; /* ContentView */ -"Failed to attach to JitStreamer:\n%@" = "未能附加到 JitStreamer: %@"; +"Failed to attach to JitStreamer:\n%@" = "未能附加到 JitStreamer:%@"; /* UTMData */ "Failed to attach to JitStreamer." = "未能附加到 JitStreamer。"; @@ -371,7 +372,7 @@ "Failed to change current directory." = "更改当前目录失败。"; /* UTMData */ -"Failed to clone VM." = "克隆虚拟机失败。"; +"Failed to clone VM." = "复制虚拟机失败。"; /* UTMData */ "Failed to decode JitStreamer response." = "未能解码 JitStreamer 响应。"; @@ -383,19 +384,20 @@ "Failed to migrate configuration from a previous UTM version." = "无法从以前的 UTM 版本迁移配置。"; /* UTMData */ -"Failed to parse download URL." = "未能解析下载 URL。"; +"Failed to parse download URL." = "无法解析下载 URL。"; /* UTMData */ "Failed to parse imported VM." = "无法解析导入的虚拟机。"; /* UTMDownloadVMTask */ -"Failed to parse the downloaded VM." = "解析下载的虚拟机失败。"; +"Failed to parse the downloaded VM." = "无法解析下载的虚拟机。"; -/* VMDisplayWindowController */ +/* AppDelegate + VMDisplayWindowController */ "Failed to save suspend state" = "无法保存挂起状态。"; /* UTMQemuVirtualMachine */ -"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "保存虚拟机快照失败。通常这意味着至少有一台设备不支持快照。%@"; +"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "保存虚拟机快照失败。通常这意味着至少有一个设备不支持快照。%@"; /* UTMSpiceIO */ "Failed to start SPICE client." = "无法启动 SPICE 客户端。"; @@ -414,7 +416,7 @@ "Force kill" = "强制终止"; /* VMDisplayWindowController */ -"Force kill the VM process with high risk of data corruption." = "强制杀死具有高数据损坏风险的虚拟机进程。"; +"Force kill the VM process with high risk of data corruption." = "强制杀死虚拟机进程 (会有高风险数据损坏)。"; /* No comment provided by engineer. */ "Force Multicore" = "强制多核"; @@ -431,6 +433,12 @@ /* No comment provided by engineer. */ "Generic" = "通用"; +/* UTMAppleConfigurationDevices */ +"Generic Mouse" = "通用鼠标"; + +/* UTMAppleConfigurationDevices */ +"Generic USB" = "通用 USB"; + /* No comment provided by engineer. */ "Gesture and Cursor Settings" = "手势和光标设置"; @@ -486,7 +494,7 @@ "Install Windows Guest Tools…" = "安装 Windows 客户机工具…"; /* VMDisplayAppleWindowController */ -"Installation: %@" = "安装: %@"; +"Installation: %@" = "安装:%@"; /* UTMProcess */ "Internal error has occurred." = "发生内部错误。"; @@ -498,7 +506,7 @@ "Internal error." = "内部错误。"; /* UTMData */ -"Invalid JitStreamer attach URL:\n%@" = "无效的 JitStreamer 附加 URL: %@"; +"Invalid JitStreamer attach URL:\n%@" = "无效的 JitStreamer 附加 URL:%@"; /* VMConfigAppleNetworkingView */ "Invalid MAC address." = "MAC 地址无效。"; @@ -519,7 +527,7 @@ "Italic, Bold" = "斜体,粗体"; /* No comment provided by engineer. */ -"Keep UTM running after last window is closed and all VMs are shut down" = "最后一个窗口关闭且所有虚拟机关机后保持 UTM 运行"; +"Keep UTM running after last window is closed and all VMs are shut down" = "在关闭最后一个窗口和关闭所有虚拟机后继续运行 UTM"; /* No comment provided by engineer. */ "License" = "许可"; @@ -557,6 +565,12 @@ /* No comment provided by engineer. */ "Logging" = "日志"; +/* UTMAppleConfigurationDevices */ +"Mac Keyboard (macOS 14+)" = "Mac 键盘 (macOS 14+)"; + +/* UTMAppleConfigurationDevices */ +"Mac Trackpad (macOS 13+)" = "Mac 触控板 (macOS 14+)"; + /* UTMAppleConfigurationBoot */ "macOS" = "macOS"; @@ -585,10 +599,7 @@ "Metal is not supported on this device. Cannot render display." = "此设备不支持 Metal。无法渲染显示。"; /* No comment provided by engineer. */ -"Minimum size: %@" = "最小文件大小: %@"; - -/* UTMAppleConfigurationDevices */ -"Mouse" = "鼠标"; +"Minimum size: %@" = "最小文件大小:%@"; /* No comment provided by engineer. */ "Mouse/Keyboard" = "鼠标/键盘"; @@ -618,19 +629,19 @@ "No" = "否"; /* UTMScriptingAppDelegate */ -"No architecture specified in the configuration." = "未在配置中指定架构。"; +"No architecture specified in the configuration." = "配置中未指定架构。"; /* VMDisplayWindowController */ -"No drives connected." = "未连接驱动器。"; +"No drives connected." = "没有连接的驱动器。"; /* UTMDownloadSupportToolsTaskError */ "No empty removable drive found. Make sure you have at least one removable drive that is not in use." = "未找到空的可移动驱动器。确保你至少有一个未使用的可移动驱动器。"; /* UTMScriptingAppDelegate */ -"No name specified in the configuration." = "未在配置中指定名称。"; +"No name specified in the configuration." = "配置中未指定名称。"; /* No comment provided by engineer. */ -"No output device is selected for this window." = "没有为此窗口选择输出设备。"; +"No output device is selected for this window." = "此窗口未选择输出设备。"; /* No comment provided by engineer. */ "No release notes found for version %@." = "未找到 %@ 版本的发布说明。"; @@ -682,7 +693,7 @@ "Operation not supported by the backend." = "操作不受后端支持。"; /* No comment provided by engineer. */ -"Option (⌥) is Meta key" = "将 Option (⌥) 键作为 Meta 键"; +"Option (⌥) is Meta key" = "Option (⌥) 键作为 Meta 键"; /* No comment provided by engineer. */ "Other" = "其他"; @@ -700,7 +711,7 @@ "PC System Flash" = "PC 系统闪存"; /* No comment provided by engineer. */ -"Pending" = "正在队列"; +"Pending" = "等待中"; /* VMDisplayWindowController */ "Play" = "启动"; @@ -805,7 +816,7 @@ "Resize display to window size automatically" = "自动将显示调整为窗口大小"; /* No comment provided by engineer. */ -"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "调整驱动器大小是实验性功能,可能导致数据丢失。在继续操作之前,强烈建议你备份此虚拟机。要将大小调整为 %@ GB 吗?"; +"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "调整驱动器大小是实验性功能,可能会导致数据丢失。在继续操作之前,强烈建议你备份此虚拟机。要将大小调整为 %@ GB 吗?"; /* VMData */ "Restoring" = "正在恢复"; @@ -848,16 +859,16 @@ "Select Shared Folder" = "选择共享的文件夹"; /* SavePanel */ -"Select where to export QEMU command:" = "选择导出 QEMU 命令的位置:"; +"Select where to export QEMU command:" = "选择导出 QEMU 命令的位置:"; /* SavePanel */ -"Select where to save debug log:" = "选择保存调试日志的位置:"; +"Select where to save debug log:" = "选择保存调试日志的位置:"; /* SavePanel */ -"Select where to save UTM Virtual Machine:" = "选择保存 UTM 虚拟机的位置:"; +"Select where to save UTM Virtual Machine:" = "选择保存 UTM 虚拟机的位置:"; /* No comment provided by engineer. */ -"Selected:" = "已选择:"; +"Selected:" = "已选择:"; /* VMDisplayWindowController */ "Sends power down request to the guest. This simulates pressing the power button on a PC." = "向客户机发送关闭电源请求。此操作模拟了按住 PC 上的电源按钮。"; @@ -939,7 +950,7 @@ "Support" = "支持"; /* UTMQemuVirtualMachine */ -"Suspend is not supported for virtualization." = "虚拟化不支持挂起。"; +"Suspend is not supported for virtualization." = "挂起功能不支持虚拟化。"; /* UTMQemuVirtualMachine */ "Suspend is not supported when an emulated NVMe device is active." = "当模拟的 NVMe 设备处于活动状态时不支持挂起。"; @@ -993,7 +1004,7 @@ "The drive '%@' already exists and cannot be created." = "驱动器 '%@' 已存在,无法创建。"; /* UTMDownloadSupportToolsTaskError */ -"The guest support tools have already been mounted." = "客户机支持工具已在挂载。"; +"The guest support tools have already been mounted." = "客户机支持工具已挂载。"; /* UTMAppleConfiguration */ "The host operating system needs to be updated to support one or more features requested by the guest." = "需要更新主机的操作系统,以支持客户机请求的一个或多个功能。"; @@ -1011,7 +1022,7 @@ "The selected architecture is unsupported in this version of UTM." = "此版本的 UTM 不支持所选架构。"; /* VMWizardState */ -"The selected boot image contains the word '%@' but the guest architecture is '%@'. Please ensure you have selected an image that is compatible with '%@'." = "所选的引导映像名称包含 '%@',但客户机的架构为 %@。请确保你选择了与 '%@' 兼容的映像。"; +"The selected boot image contains the word '%@' but the guest architecture is '%@'. Please ensure you have selected an image that is compatible with '%@'." = "所选的引导映像名称包含 '%@',但客户机的架构为 '%@'。请确保你选择了与 '%@' 兼容的映像。"; /* No comment provided by engineer. */ "The target does not support hardware emulated serial connections." = "此目标系统不支持硬件模拟串行连接。"; @@ -1044,10 +1055,10 @@ "This change will reset all settings" = "此更改将重置所有设置。"; /* UTMConfiguration */ -"This configuration is saved with a newer version of UTM and is not compatible with this version." = "此配置是使用较新版本的 UTM 保存的,并且与此版本不兼容。"; +"This configuration is saved with a newer version of UTM and is not compatible with this version." = "此配置是用较新版本的 UTM 保存的,并且与此版本不兼容。"; /* UTMConfiguration */ -"This configuration is too old and is not supported." = "此配置因太旧而无法支持。"; +"This configuration is too old and is not supported." = "此配置过旧,无法支持。"; /* UTMScriptingConfigImpl */ "This device is not supported by the target." = "目标不支持此设备。"; @@ -1056,7 +1067,7 @@ "This directory is already being shared." = "此目录已被共享。"; /* UTMAppleConfiguration */ -"This is not a valid Apple Virtualization configuration." = "这不是有效的 Apple 虚拟化配置。"; +"This is not a valid Apple Virtualization configuration." = "并非有效的 Apple 虚拟化配置。"; /* VMDisplayWindowController */ "This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "这可能会损坏虚拟机,任何未保存的更改都将丢失。为了安全退出,请从客户机操作系统关闭。"; @@ -1091,9 +1102,6 @@ /* VMDisplayQemuMetalWindowController */ "To release the mouse cursor, press %@ at the same time." = "要释放鼠标光标,请同时按 %@。"; -/* UTMAppleConfigurationDevices */ -"Trackpad" = "触控板"; - /* No comment provided by engineer. */ "u{2022} " = "u{2022}"; @@ -1217,3 +1225,794 @@ /* ContentView */ "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." = "你的 iOS 版本不支持在未经修改的情况下运行虚拟机,必须在越狱时运行 UTM,或者连接远程调试器。有关更多详细信息,请参阅 https://getutm.app/install/。"; +// Additonal Strings (Unable to be extracted by Xcode) + +/* No comment provided by engineer. */ +"(Delete)" = "(删除)"; + +/* No comment provided by engineer. */ +"Add" = "添加"; + +/* No comment provided by engineer. */ +"Add a new device." = "添加一个新设备。"; + +/* No comment provided by engineer. */ +"Add a new drive." = "添加一个新驱动器。"; + +/* No comment provided by engineer. */ +"Add read only" = "添加只读"; + +/* No comment provided by engineer. */ +"Advanced. If checked, a raw disk image is used. Raw disk image does not support snapshots and will not dynamically expand in size." = "高级选项。若选中,将使用 Raw 磁盘映像。Raw 磁盘映像不支持快照,也不会动态地扩充大小。"; + +/* No comment provided by engineer. */ +"Allow Remote Connection" = "允许远程连接"; + +/* No comment provided by engineer. */ +"Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "允许通过触控板额外输入。仅支持 macOS 13 及以上的客户机。"; + +/* No comment provided by engineer. */ +"Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "Apple 虚拟化属于实验性功能,仅适用于高级用例。推荐不选中此复选框,以使用 QEMU。"; + +/* No comment provided by engineer. */ +"Application" = "应用程序"; + +/* No comment provided by engineer. */ +"Architecture" = "架构"; + +/* No comment provided by engineer. */ +"Arguments" = "参数"; + +/* No comment provided by engineer. */ +"Auto Resolution" = "自动调整分辨率"; + +/* No comment provided by engineer. */ +"Automatic" = "自动"; + +/* No comment provided by engineer. */ +"Background Color" = "背景颜色"; + +/* No comment provided by engineer. */ +"Balloon Device" = "Balloon 设备"; + +/* No comment provided by engineer. */ +"Blinking cursor?" = "闪烁光标?"; + +/* No comment provided by engineer. */ +"Boot arguments" = "启动参数"; + +/* No comment provided by engineer. */ +"Boot Arguments" = "启动参数"; + +/* No comment provided by engineer. */ +"Boot from kernel image" = "从内核映像启动"; + +/* No comment provided by engineer. */ +"Boot Image" = "启动映像"; + +/* No comment provided by engineer. */ +"Boot Image Type" = "启动映像类型"; + +/* No comment provided by engineer. */ +"Boot into recovery mode." = "启动到还原模式。"; + +/* No comment provided by engineer. */ +"Bootloader" = "Bootloader"; + +/* No comment provided by engineer. */ +"Bridged Interface" = "桥接接口"; + +/* No comment provided by engineer. */ +"Bridged Settings" = "桥接设置"; + +/* No comment provided by engineer. */ +"By default, the best backend for the target will be used. If the selected backend is not available for any reason, an alternative will automatically be selected." = "默认情況下,将使用目标虚拟机的最佳后端。若所选的后端因任何原因而不可用,将自动选择替代方案。"; + +/* No comment provided by engineer. */ +"By default, the best renderer for this device will be used. You can override this with to always use a specific renderer. This only applies to QEMU VMs with GPU accelerated graphics." = "默认情況下,将使用最适合此设备的渲染器。你可以覆盖此选项,以始终使用特定的渲染器。此选项仅适用于具有 GPU 加速图形的 QEMU 虚拟机。"; + +/* No comment provided by engineer. */ +"Calculating current size..." = "计算当前大小…"; + +/* No comment provided by engineer. */ +"Cancel Download" = "取消下载"; + +/* No comment provided by engineer. */ +"Clipboard Sharing" = "剪贴板共享"; + +/* No comment provided by engineer. */ +"Clone" = "复制"; + +/* No comment provided by engineer. */ +"Clone selected VM" = "复制已选中的虚拟机"; + +/* No comment provided by engineer. */ +"Clone…" = "复制…"; + +/* No comment provided by engineer. */ +"Close" = "关闭"; + +/* No comment provided by engineer. */ +"Closing a VM without properly shutting it down could result in data loss." = "在不正确关闭电源的情况下关闭虚拟机可能会导致数据丢失。"; + +/* No comment provided by engineer. */ +"Compress" = "压缩"; + +/* No comment provided by engineer. */ +"Compress by re-converting the disk image and compressing the data." = "通过重新转换磁盘映像和压缩数据映像来实现压缩。"; + +/* No comment provided by engineer. */ +"Create a new VM" = "创建一个新虚拟机"; + +/* No comment provided by engineer. */ +"Create a new VM with the same configuration as this one but without any data." = "创建一个和此配置相同,但不带有任何数据的新虚拟机。"; + +/* No comment provided by engineer. */ +"Create an empty drive." = "创建一个空驱动器。"; + +/* No comment provided by engineer. */ +"Debian Install Guide" = "Debian 安装指南"; + +/* No comment provided by engineer. */ +"Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!" = "默认为内存大小的 1/4 (如上)。在总内存使用量中,JIT 缓存与内存的大小都会包括在内!"; + +/* No comment provided by engineer. */ +"Delete this drive." = "删除此驱动器。"; + +/* No comment provided by engineer. */ +"Delete selected VM" = "刪除已选中的虚拟机"; + +/* No comment provided by engineer. */ +"Delete this shortcut. The underlying data will not be deleted." = "删除此快捷方式。快捷方式背后的数据不会被删除。"; + +/* No comment provided by engineer. */ +"Delete this VM and all its data." = "刪除此虚拟机及其所有数据。"; + +/* No comment provided by engineer. */ +"Delete Drive" = "刪除驱动器"; + +/* No comment provided by engineer. */ +"Description" = "注释"; + +/* No comment provided by engineer. */ +"Devices" = "设备"; + +/* No comment provided by engineer. */ +"Directory" = "目录"; + +/* No comment provided by engineer. */ +"Directory Share Mode" = "目录共享模式"; + +/* No comment provided by engineer. */ +"Disk" = "磁盘"; + +/* No comment provided by engineer. */ +"DHCP Domain Name" = "DHCP 域名"; + +/* No comment provided by engineer. */ +"DHCP End" = "DHCP 结束地址"; + +/* No comment provided by engineer. */ +"DNS Search Domains" = "DNS 搜索域"; + +/* No comment provided by engineer. */ +"DNS Server" = "DNS 服务器"; + +/* No comment provided by engineer. */ +"DNS Server (IPv6)" = "DNS 服务器 (IPv6)"; + +/* No comment provided by engineer. */ +"DHCP Start" = "DHCP 起始地址"; + +/* No comment provided by engineer. */ +"Done" = "完成"; + +/* No comment provided by engineer. */ +"Duplicate this VM along with all its data." = "复制此虚拟机及其所有数据。"; + +/* No comment provided by engineer. */ +"Download and mount the guest support package for Windows. This is required for some features including dynamic resolution and clipboard sharing." = "下载并装载 Windows 的客户机支持包。此支持包对于一些功能而言为必需,例如动态分辨率与剪贴板共享。"; + +/* No comment provided by engineer. */ +"Download and mount the guest tools for Windows." = "下载并装载 Windows 客户机工具。"; + +/* No comment provided by engineer. */ +"Download Windows 11 for ARM64 Preview VHDX" = "下载 Windows 11 ARM64 Preview VHDX 映像"; + +/* No comment provided by engineer. */ +"Downscaling" = "细化"; + +/* No comment provided by engineer. */ +"Edit" = "编辑"; + +/* No comment provided by engineer. */ +"Edit selected VM" = "编辑已选中的虚拟机"; + +/* No comment provided by engineer. */ +"Edit…" = "编辑…"; + +/* No comment provided by engineer. */ +"Emulated Audio Card" = "模拟声卡"; + +/* No comment provided by engineer. */ +"Emulated Display Card" = "模拟显卡"; + +/* No comment provided by engineer. */ +"Emulated Network Card" = "模拟网卡"; + +/* No comment provided by engineer. */ +"Emulated Serial Device" = "模拟序列设备"; + +/* No comment provided by engineer. */ +"Enable Balloon Device" = "启用 Balloon 设备"; + +/* No comment provided by engineer. */ +"Enable Entropy Device" = "启用 Entropy 设备"; + +/* No comment provided by engineer. */ +"Enable hardware OpenGL acceleration" = "启用 OpenGL 硬件加速"; + +/* No comment provided by engineer. */ +"Enable Keyboard" = "启用键盘"; + +/* No comment provided by engineer. */ +"Enable Pointer" = "启用光标"; + +/* No comment provided by engineer. */ +"Enable Rosetta (x86_64 Emulation)" = "启用 Rosetta (x86_64 模拟)"; + +/* No comment provided by engineer. */ +"Enable Rosetta on Linux (x86_64 Emulation)" = "在 Linux 中启用 Rosetta (x86_64 模拟)"; + +/* No comment provided by engineer. */ +"Enable Sound" = "启用声音"; + +/* No comment provided by engineer. */ +"Engine" = "引擎"; + +/* No comment provided by engineer. */ +"Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments." = "将所有参数导出为文本文件。此功能仅用于调试目的,因为 UTM 的内置 QEMU 与上游 QEMU 在支持的参数上有所不同。"; + +/* No comment provided by engineer. */ +"Export Debug Log" = "导出调试日志"; + +/* No comment provided by engineer. */ +"External Drive" = "外部磁盘"; + +/* No comment provided by engineer. */ +"Fetch latest Windows installer…" = "获取最新的 Windows 安装程序…"; + +/* No comment provided by engineer. */ +"Font" = "字体"; + +/* No comment provided by engineer. */ +"Force Disable CPU Flags" = "强制禁用 CPU 标志"; + +/* No comment provided by engineer. */ +"Force Enable CPU Flags" = "强制启用 CPU 标志"; + +/* No comment provided by engineer. */ +"Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation." = "强制多核可以提高模拟速度,但也可能导致模拟不稳定和不正确。"; + +/* No comment provided by engineer. */ +"Force PS/2 controller" = "强制使用 PS/2 控制器"; + +/* No comment provided by engineer. */ +"FPS Limit" = "FPS 限制"; + +/* No comment provided by engineer. */ +"Go Back" = "返回"; + +/* No comment provided by engineer. */ +"GPU Acceleration Supported" = "支持 GPU 加速"; + +/* No comment provided by engineer. */ +"Guest Address" = "客户机地址"; + +/* No comment provided by engineer. */ +"Guest Network" = "客户机网络"; + +/* No comment provided by engineer. */ +"Guest Network (IPv6)" = "客户机网络 (IPv6)"; + +/* No comment provided by engineer. */ +"Guest Port" = "客户机端口"; + +/* No comment provided by engineer. */ +"Hardware interface on the guest used to mount this image. Different operating systems support different interfaces. The default will be the most common interface." = "用于挂载此映像的客户机硬件接口。不同的操作系统支持不同的接口。默认值为最常用的接口。"; + +/* No comment provided by engineer. */ +"Hardware OpenGL Acceleration" = "硬件 OpenGL 加速"; + +/* No comment provided by engineer. */ +"Height" = "高度"; + +/* No comment provided by engineer. */ +"Hide" = "隐藏"; + +/* No comment provided by engineer. */ +"Hide dock icon on next launch" = "下次启动时隐藏程序坞图标"; + +/* No comment provided by engineer. */ +"Host Address" = "主机地址"; + +/* No comment provided by engineer. */ +"Host Address (IPv6)" = "主机地址 (IPv6)"; + +/* No comment provided by engineer. */ +"Host Port" = "主机端口"; + +/* No comment provided by engineer. */ +"If checked, no drive image will be stored with the VM. Instead you can mount/unmount image while the VM is running." = "若选中,虚拟机将不会存储驱动器映像。相反,你可以在虚拟机运行时挂载/卸载映像。"; + +/* No comment provided by engineer. */ +"If checked, the CPU flag will be enabled. Otherwise, the default value will be used." = "若选中,将启用 CPU 标志。否则将使用默认值。"; + +/* No comment provided by engineer. */ +"If checked, the CPU flag will be disabled. Otherwise, the default value will be used." = "若选中,将禁用 CPU 标志。否则将使用默认值。"; + +/* No comment provided by engineer. */ +"If checked, the drive image will be stored with the VM." = "若选中,驱动器映像将和虚拟机一起存储。"; + +/* No comment provided by engineer. */ +"If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock." = "若选中,将使用 Windows 需要的 RTC 本地时间,否则使用 UTC 时钟。"; + +/* No comment provided by engineer. */ +"If disabled, the default combination Control+Option (⌃+⌥) will be used." = "若禁用,将使用默认组合键 Control + Option (⌃ + ⌥)。"; + +/* No comment provided by engineer. */ +"If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64." = "若启用,标为“rosetta”的 virtiofs 共享将在 Linux 客户机上可用,用于安装 Rosetta,并在 arm64 上模拟 x86_64。"; + +/* No comment provided by engineer. */ +"If enabled, any existing screenshot will be deleted the next time the VM is started." = "若启用,下次启动虚拟机时,任何现有的快照都将被删除。"; + +/* No comment provided by engineer. */ +"If enabled, caps lock will be handled like other keys. If disabled, it is treated as a toggle that is synchronized with the host." = "若启用,Caps Lock 将和其他按键一样处理。若禁用,它将被视为与主机同步的切换键。"; + +/* No comment provided by engineer. */ +"If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "若启用,输入捕捉会在进入和退出全屏模式时自动切换。"; + +/* No comment provided by engineer. */ +"If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "若启用,Num Lock 将始终对客户机开启。注意,这可能会使键盘的 Num Lock 指示灯不同步。"; + +/* No comment provided by engineer. */ +"If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "若启用,Option 键将映射到 Meta 键,这对 Emacs 很有用。否则,Option 键将按照系统默认方式工作(例如输入国际文本)。"; + +/* No comment provided by engineer. */ +"If enabled, resizing of the VM window will not be allowed." = "若启用,将不允许调整虚拟机窗口的大小。"; + +/* No comment provided by engineer. */ +"If enabled, scroll wheel input will be inverted." = "若启用,将反转滚轮输入。"; + +/* No comment provided by engineer. */ +"If enabled, the default input devices will be emulated on the USB bus." = "若启用,默认输入设备将在 USB 总线上模拟。"; + +/* No comment provided by engineer. */ +"If set, a frame limit can improve smoothness in rendering by preventing stutters when set to the lowest value your device can handle." = "若设置,则当设置为设备可以处理的最低值时,帧限制可以防止卡顿,从而提高渲染的流畅度。"; + +/* No comment provided by engineer. */ +"If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO." = "如设置,直接由 Raw 内核映像和 initrd 启动。否則由受支持的 ISO 启动。"; + +/* No comment provided by engineer. */ +"Image Type" = "映像类型"; + +/* No comment provided by engineer. */ +"Import Drive" = "导入驱动器"; + +/* No comment provided by engineer. */ +"Import VHDX Image" = "导入 VHDX 映像"; + +/* No comment provided by engineer. */ +"Increase the size of the disk image." = "增加磁盘映像的大小。"; + +/* No comment provided by engineer. */ +"Initial Ramdisk" = "初始 ramdisk"; + +/* No comment provided by engineer. */ +"Input" = "输入"; + +/* No comment provided by engineer. */ +"Install drivers and SPICE tools" = "安装驱动程序和 SPICE 工具"; + +/* No comment provided by engineer. */ +"Install Windows 10 or higher" = "安装 Windows 10 或更高版本"; + +/* No comment provided by engineer. */ +"Installation Instructions" = "安装指南"; + +/* No comment provided by engineer. */ +"Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "即便支持 USB 输入,仍然实例化 PS/2 控制器。此功能对于旧版 Windows 为必需。"; + +/* No comment provided by engineer. */ +"Interface" = "接口"; + +/* No comment provided by engineer. */ +"IPSW Install Image" = "IPSW 安装映像"; + +/* No comment provided by engineer. */ +"JIT Cache" = "JIT 缓存数据"; + +/* No comment provided by engineer. */ +"Kernel" = "内核"; + +/* No comment provided by engineer. */ +"Kernel Image" = "内核映像"; + +/* No comment provided by engineer. */ +"Keyboard" = "键盘"; + +/* No comment provided by engineer. */ +"MAC Address" = "MAC 地址"; + +/* No comment provided by engineer. */ +"Machine" = "虚拟机"; + +/* No comment provided by engineer. */ +"Maintenance" = "维护"; + +/* No comment provided by engineer. */ +"Mode" = "模式"; + +/* No comment provided by engineer. */ +"Modify settings for this VM." = "修改此虚拟机的设置。"; + +/* UTMAppleConfigurationDevices */ +"Mouse" = "鼠标"; + +/* No comment provided by engineer. */ +"Move" = "移动"; + +/* No comment provided by engineer. */ +"Move…" = "移动…"; + +/* No comment provided by engineer. */ +"Move selected VM" = "移动已选中的虚拟机"; + +/* No comment provided by engineer. */ +"Move this VM from internal storage to elsewhere." = "将此虚拟机从内部存储空间移动到其他地方。"; + +/* No comment provided by engineer. */ +"Network" = "网络"; + +/* No comment provided by engineer. */ +"Network Mode" = "网络模式"; + +/* No comment provided by engineer. */ +"New Drive" = "新建驱动器"; + +/* No comment provided by engineer. */ +"New from template…" = "从此模板新建…"; + +/* No comment provided by engineer. */ +"New Shared Directory…" = "新建共享目錄…"; + +/* No comment provided by engineer. */ +"New VM" = "新建虚拟机"; + +/* No comment provided by engineer. */ +"Older versions of UTM added each IDE device to a separate bus. Check this to change the configuration to place two units on each bus." = "旧版本的 UTM 会将每个 IDE 设备添加到单独的总线上。选中此项可更改配置,以在每条总线上放置两个设备。"; + +/* No comment provided by engineer. */ +"Only available if host architecture matches the target. Otherwise, TCG emulation is used." = "仅当主机架构和目标匹配时才可用。否则将使用 TCG 模拟。"; + +/* No comment provided by engineer. */ +"Only available on macOS virtual machines." = "仅限 macOS 虚拟机可用。"; + +/* No comment provided by engineer. */ +"Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general." = "只有在支持的硬件上使用 Hypervisor 时才可用。TSO 加快了客户机中的 Intel 仿真速度,但总体性能却有所下降。"; + +/* No comment provided by engineer. */ +"Open VM Settings" = "打开虚拟机设置"; + +/* No comment provided by engineer. */ +"Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details." = "(可选) 选择一个可在虚拟机内访问的目录。请注意,客户操作系统对共享目录的支持各不相同,可能需要安装额外的客户驱动程序。有关详细信息,请参阅 UTM 支持页面。"; + +/* No comment provided by engineer. */ +"Options here only apply on next boot and are not saved." = "此处的选项只在下次启动时生效,不会保存。"; + +/* No comment provided by engineer. */ +"Path" = "路径"; + +/* No comment provided by engineer. */ +"Port" = "端口"; + +/* No comment provided by engineer. */ +"Power Off" = "关闭电源"; + +/* No comment provided by engineer. */ +"Prompt" = "提示"; + +/* No comment provided by engineer. */ +"Protocol" = "协议"; + +/* No comment provided by engineer. */ +"QEMU Machine Properties" = "QEMU 虚拟机属性"; + +/* No comment provided by engineer. */ +"Quit" = "退出"; + +/* No comment provided by engineer. */ +"RAM" = "内存"; + +/* No comment provided by engineer. */ +"Ramdisk (optional)" = "Ramdisk (选填)"; + +/* No comment provided by engineer. */ +"Random" = "随机"; + +/* No comment provided by engineer. */ +"Read Only?" = "只读?"; + +/* No comment provided by engineer. */ +"Reclaim disk space by re-converting the disk image." = "通过重新转换来回收磁盘空间。"; + +/* No comment provided by engineer. */ +"Reclaim Space" = "释放空间"; + +/* No comment provided by engineer. */ +"Remove selected shortcut" = "移除选中的快捷方式"; + +/* No comment provided by engineer. */ +"Renderer Backend" = "渲染器后端"; + +/* No comment provided by engineer. */ +"Requires restarting UTM to take affect." = "需要重新打开 UTM 以生效。"; + +/* No comment provided by engineer. */ +"Requires SPICE guest agent tools to be installed." = "需要安装 SPICE 客户机代理工具。"; + +/* No comment provided by engineer. */ +"Reset UEFI Variables" = "重设 UEFI 变量"; + +/* No comment provided by engineer. */ +"Resize Console Command" = "调整控制台大小指令"; + +/* No comment provided by engineer. */ +"Resize…" = "调整大小…"; + +/* No comment provided by engineer. */ +"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %lld GiB?" = "调整大小是实验性功能,可能会导致数据丢失。强烈建议你在继续操作前备份此虚拟机。要将大小调整为 %lld GiB 吗?"; + +/* No comment provided by engineer. */ +"Resolution" = "分辨率"; + +/* No comment provided by engineer. */ +"Restart" = "重新启动"; + +/* No comment provided by engineer. */ +"Resume" = "继续"; + +/* No comment provided by engineer. */ +"Resume running VM." = "继续正在运行的虚拟机。"; + +/* No comment provided by engineer. */ +"Reveal where the VM is stored." = "显示虚拟机的存储位置。"; + +/* No comment provided by engineer. */ +"RNG Device" = "RNG 设备"; + +/* No comment provided by engineer. */ +"Root Image" = "Root 映像"; + +/* No comment provided by engineer. */ +"Run" = "运行"; + +/* No comment provided by engineer. */ +"Run Recovery" = "运行 Recovery 模式"; + +/* No comment provided by engineer. */ +"Run selected VM" = "运行已选中的虚拟机"; + +/* No comment provided by engineer. */ +"Run the VM in the foreground." = "在前台运行虚拟机。"; + +/* No comment provided by engineer. */ +"Run the VM in the foreground, without saving data changes to disk." = "在前台运行虚拟机,但不会将数据存储到磁盘。"; + +/* No comment provided by engineer. */ +"Run without saving changes" = "运行但不存储更改"; + +/* No comment provided by engineer. */ +"Section" = "区域"; + +/* No comment provided by engineer. */ +"Secure Boot with TPM 2.0" = "使用 TPM 2.0 的安全启动"; + +/* No comment provided by engineer. */ +"Select an existing disk image." = "选择一个现有的磁盘映像。"; + +/* No comment provided by engineer. */ +"Serial" = "序列"; + +/* No comment provided by engineer. */ +"Server Address" = "服务器地址"; + +/* No comment provided by engineer. */ +"Settings" = "设置"; + +/* No comment provided by engineer. */ +"Share" = "共享"; + +/* No comment provided by engineer. */ +"Share…" = "共享…"; + +/* No comment provided by engineer. */ +"Share a copy of this VM and all its data." = "共享此虚拟机及其所有数据的副本。"; + +/* No comment provided by engineer. */ +"Share Directory" = "共享目录"; + +/* No comment provided by engineer. */ +"Share is read only" = "共享为只读"; + +/* No comment provided by engineer. */ +"Share selected VM" = "共享已选中的虚拟机"; + +/* No comment provided by engineer. */ +"Shared Directory Path" = "共享目录路径"; + +/* No comment provided by engineer. */ +"Shared Path" = "共享路径"; + +/* No comment provided by engineer. */ +"Should be off for older operating systems such as Windows 7 or lower." = "对于较旧的操作系统应关闭此选项,例如 Windows 7 或者更低版本。"; + +/* No comment provided by engineer. */ +"Should be on always unless the guest cannot boot because of this." = "除非客户机因此无法启动,否则此项应始终打开。"; + +/* No comment provided by engineer. */ +"Show all devices…" = "显示所有设备…"; + +/* No comment provided by engineer. */ +"Show in Finder" = "在访达中显示"; + +/* No comment provided by engineer. */ +"Show the main window." = "显示主窗口。"; + +/* No comment provided by engineer. */ +"Show UTM" = "显示 UTM"; + +/* No comment provided by engineer. */ +"Show UTM preferences" = "显示 UTM 偏好设置"; + +/* No comment provided by engineer. */ +"Skip Boot Image" = "跳过启动映像"; + +/* No comment provided by engineer. */ +"Skip ISO boot" = "跳过 ISO 启动"; + +/* No comment provided by engineer. */ +"Some older systems do not support UEFI boot, such as Windows 7 and below." = "一些旧版系统不支持 UEFI 启动,例如 Windows 7 及更低版本。"; + +/* No comment provided by engineer. */ +"Sound" = "声音"; + +/* No comment provided by engineer. */ +"Sound Backend" = "声音后端"; + +/* No comment provided by engineer. */ +"Start" = "开始"; + +/* No comment provided by engineer. */ +"Status" = "状态"; + +/* No comment provided by engineer. */ +"Stop selected VM" = "停止已选中的虚拟机"; + +/* No comment provided by engineer. */ +"Stop the running VM." = "停止正在运行的虚拟机。"; + +/* No comment provided by engineer. */ +"Storage" = "存储空间"; + +/* No comment provided by engineer. */ +"stty cols $COLS rows $ROWS\n" = "stty 行 $ROWS 列 $COLS\n"; + +/* No comment provided by engineer. */ +"Suspend" = "挂起"; + +/* No comment provided by engineer. */ +"Target" = "目标"; + +/* No comment provided by engineer. */ +"Terminate UTM and stop all running VMs." = "终止 UTM 并停止所有正在运行的虚拟机。"; + +/* No comment provided by engineer. */ +"Text" = "文字"; + +/* No comment provided by engineer. */ +"Text Color" = "文字颜色"; + +/* No comment provided by engineer. */ +"The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "为此映像分配的存储空间。在导入映像时,此参数会被忽略。若导入的是 Raw 映像,则虚拟机将存储一个与此相同大小的空文件。否则,磁盘映像将动态扩展至此大小。"; + +/* No comment provided by engineer. */ +"Theme" = "主题"; + +/* No comment provided by engineer. */ +"There are known issues in some newer Linux drivers including black screen, broken compositing, and apps failing to render." = "一些较新的 Linux 驱动程序存在已知问题,包括黑屏、合成失败和应用程序无法渲染。"; + +/* No comment provided by engineer. */ +"These are advanced settings affecting QEMU which should be kept default unless you are running into issues." = "这些属于 QEMU 的高级设置,除非遇到问题,否则应当保持默认值。"; + +/* No comment provided by engineer. */ +"This is appended to the -machine argument." = "这会添加到 -machine 参数的末端。"; + +/* No comment provided by engineer. */ +"This virtual machine cannot be found at: %@" = "虚拟机无法在此路径中找到:%@"; + +/* No comment provided by engineer. */ +"This virtual machine must be re-added to UTM by opening it with Finder. You can find it at the path: %@" = "必须使用访达打开此虚拟机,将其重新添加到 UTM 中。你可以在此路径中找到:%@"; + +/* No comment provided by engineer. */ +"TPM 2.0 Device" = "TPM 2.0 设备"; + +/* No comment provided by engineer. */ +"TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided." = "TPM 可用于保护客户机操作系统中的机密。需要注意的是,主机始终可以读取这些机密,因此无法提供预期的物理安全性。"; + +/* UTMAppleConfigurationDevices */ +"Trackpad" = "触控板"; + +/* No comment provided by engineer. */ +"Tweaks" = "调整"; + +/* No comment provided by engineer. */ +"Ubuntu Install Guide" = "Ubuntu 安装指南"; + +/* No comment provided by engineer. */ +"UEFI Boot" = "UEFI 启动"; + +/* No comment provided by engineer. */ +"Upscaling" = "粗化"; + +/* No comment provided by engineer. */ +"USB Support" = "USB 支持"; + +/* No comment provided by engineer. */ +"Use Apple Virtualization" = "使用 Apple 虚拟化"; + +/* No comment provided by engineer. */ +"Use Hypervisor" = "使用 Hypervisor"; + +/* No comment provided by engineer. */ +"Use local time for base clock" = "使用本地时间作为基本时钟"; + +/* No comment provided by engineer. */ +"Use Rosetta" = "使用 Rosetta"; + +/* No comment provided by engineer. */ +"Use Trackpad" = "使用触控板"; + +/* No comment provided by engineer. */ +"Use TSO" = "使用 TSO"; + +/* No comment provided by engineer. */ +"Use Virtualization" = "使用虚拟化"; + +/* No comment provided by engineer. */ +"VGA Device RAM (MB)" = "VGA 设备内存 (MB)"; + +/* No comment provided by engineer. */ +"Virtualization" = "虚拟化"; + +/* No comment provided by engineer. */ +"Virtualization Engine" = "虚拟化引擎"; + +/* No comment provided by engineer. */ +"Wait for Connection" = "等待连接"; + +/* No comment provided by engineer. */ +"WebDAV requires installing SPICE daemon. VirtFS requires installing device drivers." = "WebDAV 需要安装 SPICE 守护程序。VirtFS 需要安装设备驱动程序。"; + +/* No comment provided by engineer. */ +"Width" = "宽度"; + +/* No comment provided by engineer. */ +"Windows Install Guide" = "Windows 安装指南"; + +/* No comment provided by engineer. */ +"You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot." = "若启动选项已损坏,或者希望重新注册安全启动的默认密钥,可以使用此功能。"; + +/* No comment provided by engineer. */ +"Zoom" = "缩放"; + diff --git a/QEMUHelper/it.lproj/InfoPlist.strings b/QEMUHelper/it.lproj/InfoPlist.strings new file mode 100644 index 000000000..d3205b168 --- /dev/null +++ b/QEMUHelper/it.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "QEMUHelper"; + +/* Bundle name */ +"CFBundleName" = "QEMUHelper"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2020 osy. Tutti i diritti riservati."; + diff --git a/QEMUHelper/it.lproj/Localizable.strings b/QEMUHelper/it.lproj/Localizable.strings new file mode 100644 index 000000000..fccbdfd04 --- /dev/null +++ b/QEMUHelper/it.lproj/Localizable.strings @@ -0,0 +1,9 @@ +/* QEMUHelper */ +"Cannot find QEMU support libraries." = "Impossibile trovare le librerie di supporto di QEMU"; + +/* QEMUHelper */ +"Error starting QEMU." = "Erorre durante l'avvio di QUEMU"; + +/* QEMUHelper */ +"QEMU exited unexpectedly." = "QEMU si è interrotto inaspettatamente"; + diff --git a/README.ko.md b/README.ko.md new file mode 100644 index 000000000..7980a72fc --- /dev/null +++ b/README.ko.md @@ -0,0 +1,78 @@ +# UTM +[![Build](https://github.com/utmapp/UTM/workflows/Build/badge.svg?branch=master&event=push)][1] + +> 계산 가능한 수열을 계산하는 단일 기계를 발명할 수 있습니다.(It is possible to invent a single machine which can be used to compute any computable sequence.) + +-- <cite>엘런 튜링, 1936</cite> + +UTM은 iOS와 macOS를 위한 완전한 시스템 에뮬레이터, 가상머신입니다. 이것은 QEMU를 기반으로 합니다. 요컨데 당신은 이것을 통해, Windows나 Linux와 같은 운영체제들을 Mac, iPhone, iPad 등에서 구동할 수 있습니다. 자세한 내용은 https://getutm.app/ 와 https://mac.getutm.app/ 를 읽어주세요. + +<p align="center"> + <img width="450px" alt="iPhone에서 동작하는 UTM" src="screen.png"> + <br> + <img width="450px" alt="MacBook에서 동작하는 UTM" src="screenmac.png"> +</p> + +## 주요기능 + +* QMEU를 활용한 완전한 시스템 에뮬레이션(MMU, 기타 기기들) +* x86_64, ARM64, and RISC-V를 포함한 30가지 이상의 프로세서 지원 +* SPICE와 QXL을 활용한 VGA 그래픽 모드 +* 텍스트 터미널 모드 +* USB 장치들 +* QEMU TCG를 활용한 JIT 기반 가속 +* 초기부터 macOS 11과 iOS 11+를 위해 디자인된, 최신 및 최고의 API를 활용한 프론트엔드 +* 당신의 기기에서 바로 가상머신을 생성하고, 관리하고, 구동하기 + +## macOS 추가 기능 + +* Hypervisor.framework와 QEMU를 활용한 하드웨어 가속 가상화 +* macOS 12+에서 Virtualization.framework를 통해 macOS 게스트 구동 + +## UTM SE + +UTM/QEMU이 최고의 성능을 내기 위해서는 동적 코드 생성이(JIT) 필요합니다. iOS 기기에서 JIT는 jailbroken를 요구하거나, 특정 iOS 버전에서 발견된 다양한 해결책 중 하나를 필요로 합니다.(자세한 내용은 "설치" 부분을 참고해주세요." + +UTM SE("slow edition")은 [threaded interpreter][3]를 사용합니다. 이는 전통적인 인터프리터보다는 좋지만, 그래도 여전히 JIT보다는 느립니다. 이 기술은 [iSH][4]가 동적 실행을 위해 하는 일과 유사한데요. 결과적으로 UTM SE는 탈옥이나 JIT 해결책을 요구하진 않고, 정규 앱으로 나란히 메모리에 적재될 수 있습니다. + +빌드 시간과 크기를 최적하기 위해서, UTM SE에는 ARM, PPC, RISC-V, x86(32bit와 64bit 변종 모두) 아키텍처들만이 포함되어 있습니다. + +## 설치 + +iOS를 위한 UTM (SE): https://getutm.app/install/ + +macOS를 위한 UTM: https://mac.getutm.app/ + +## 개발 + +### [macOS 개발](Documentation/MacDevelopment.md) + +### [iOS 개발](Documentation/iOSDevelopment.md) + +## 관련사항 + +* [iSH][4]: iOS에서 x86 Linux 앱을 실행하기 위해, 사용자 모드 Linux 터미널 인터페이스를 에뮬레이트 +* [a-shell][5]: 기본적으로 iOS용으로 구축되면서, 터미널 인터페이스를 통해 액세스할 수 있는, 범용 유닉스 명령 및 유틸리티 패키지 + +## 라이센스 + +UTM은 permissive Apache 2.0 license를 따르며 배포되었습니다. 하지만 몇몇 (L)GPL 컴포넌트들을 사용하는데요. 대부분은 동적으로 연결되어있지만, gstreamer 플러그인은 정적으로 연결되어 있고, 일부 코드는 qemu에서 가져왔습니다. 이 앱을 재배포 하려는 경우 꼭 이에 유의하시길 바랍니다. + +일부 아이콘은 [www.flaticon.com](https://www.flaticon.com/)에서 [Freepik](https://www.freepik.com)를 통해 만들어졌습니다. + +추가적으로 UTM 프론트엔드는 아래의 MIT/BSD 라이센스를 사용하는 컴포넌트들에 의존하고 있습니다. + +* [IQKeyboardManager](https://github.com/hackiftekhar/IQKeyboardManager) +* [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm) +* [ZIP Foundation](https://github.com/weichsel/ZIPFoundation) +* [InAppSettingsKit](https://github.com/futuretap/InAppSettingsKit) + +지속 통합 호스팅은 다음을 통해 제공됩니다. [MacStadium](https://www.macstadium.com/opensource) + +[<img src="https://uploads-ssl.webflow.com/5ac3c046c82724970fc60918/5c019d917bba312af7553b49_MacStadium-developerlogo.png" alt="MacStadium logo" width="250">](https://www.macstadium.com) + + [1]: https://github.com/utmapp/UTM/actions?query=event%3Arelease+workflow%3ABuild + [2]: screen.png + [3]: https://github.com/ktemkin/qemu/blob/with_tcti/tcg/aarch64-tcti/README.md + [4]: https://github.com/ish-app/ish + [5]: https://github.com/holzschu/a-shell diff --git a/README.zh-HK.md b/README.zh-HK.md index 1c4b4f063..e13e251ae 100644 --- a/README.zh-HK.md +++ b/README.zh-HK.md @@ -4,7 +4,7 @@ > 發明一台可用於計算任何可計算序列的機器是可行的。 -- <cite>艾倫·圖靈(Alan Turing), 1936 年</cite> -UTM 是一個功能完备的系統模擬工具和虛擬电脑主機,適用於 iOS 和 macOS。它以 QEMU 為基礎。簡言之,它允許你在 Mac、iPhone 和 iPad 上執行 Windows、Linux 等。更多訊息請參閱 https://getutm.app/ 與 https://mac.getutm.app/。 +UTM 是一個功能完备的系統模擬工具與虛擬电脑主機,適用於 iOS 和 macOS。它基於 QEMU。簡言之,它允許你在 Mac、iPhone 和 iPad 上執行 Windows、Linux 等。更多訊息請見 https://getutm.app/ 與 https://mac.getutm.app/。 <p align="center"> <img width="450px" alt=「在 iPhone 上執行 UTM" src="screen.png"> @@ -14,7 +14,7 @@ UTM 是一個功能完备的系統模擬工具和虛擬电脑主機,適用於 ## 特性 -* 使用 QEMU 進行全作業系統模擬(MMU、設備等) +* 使用 QEMU 進行全作業系統模擬(MMU、裝置等) * 支援逾三十種體系結構 CPU,包括 x86_64、ARM64 和 RISC-V * 使用 SPICE 與 QXL 的 VGA 圖形模式 * 文本終端機模式 @@ -23,14 +23,14 @@ UTM 是一個功能完备的系統模擬工具和虛擬电脑主機,適用於 * 採用了最新最靚的 API,從零開始設計前端,支援 macOS 11+ 與 iOS 11+ * 從你的裝置上直接製作、管理和執行虛擬機 -## macOS 的附加功能 +## 於 macOS 的附加功能 * 使用 Hypervisor.framework 與 QEMU 實現硬件加速虛擬化 * 在 macOS 12+ 上使用 Virtualization.framework 來啓動 macOS 客戶端 ## UTM SE -UTM/QEMU 需要動態程式碼生成(JIT)以得到最大性能。iOS 上的 JIT 需要已經越獄(Jailbreak)的裝置(iOS 11.0~14.3 無需越獄,iOS 14.4+ 需要),或者為特定版本的 iOS 找到其他變通方法之一(有關更多詳細訊息,請參閱「安裝」)。 +UTM/QEMU 需要動態程式碼生成(JIT)以得到最大性能。iOS 上的 JIT 需要已經越獄(Jailbreak)的裝置(iOS 11.0~14.3 毋需越獄,iOS 14.4+ 需要),或者為特定版本的 iOS 找到其他變通方法之一(有關更多詳細訊息,請見「安裝」)。 UTM SE(「較慢版」)使用了「[執行緒解釋器][3]」,其性能優於傳統解釋器,但仍然比 JIT 要慢。此種技術類似於 [iSH][4] 的動態執行。因此,UTM SE 無需越獄或任何 JIT 的變通方法,可以作為常規應用程式側載(Sideload)。 @@ -55,7 +55,7 @@ UTM 同時支援 macOS:https://mac.getutm.app/ ## 許可證 -UTM 於 Apache 2.0 許可證下發佈,但它採用了若干 GPL 與 LGPL 元件。這其中,大多數元件是動態連接的,但 gstreamer 元件是靜態連接的,部分程式碼來自 QEMU。如果你打算重新分發此應用程式,請務必謹記這一點。 +UTM 於 Apache 2.0 許可證下發佈,但它採用了若干 GPL 與 LGPL 元件。這其中,大多數元件為動態連接,但 gstreamer 元件為靜態連接,部分程式碼來自 QEMU。如你打算重新分發此應用程式,請務必緊記這一點。 某些图示由 [Freepik](https://www.freepik.com) 從 [www.flaticon.com](https://www.flaticon.com/) 製作。 diff --git a/Remote/GenerateKey.c b/Remote/GenerateKey.c new file mode 100644 index 000000000..f7bad39ac --- /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 000000000..7e73d5f27 --- /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 000000000..f2cc3d673 --- /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 000000000..814132ce5 --- /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 000000000..4df5da5aa --- /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 000000000..9901d0be2 --- /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 000000000..ee39e3bee --- /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 000000000..c4736e4f9 --- /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 8c5774c99..24b31c470 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 61f2596aa..f42008659 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 4e464941b..bb4eabbfd 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 cec1d60ea..850031107 100644 --- a/Services/UTMJailbreak.m +++ b/Services/UTMJailbreak.m @@ -65,7 +65,7 @@ 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}; @@ -397,7 +397,7 @@ bool jb_spawn_ptrace_child(int argc, char **argv) { return false; } childArgv[0] = argv[0]; - if ((ret = posix_spawnp(&pid, argv[0], NULL, NULL, (void *)childArgv, (void *)environ)) != 0) { + if ((ret = posix_spawnp(&pid, argv[0], NULL, NULL, (void *)childArgv, NULL)) != 0) { return false; } return true; diff --git a/Services/UTMLogging.m b/Services/UTMLogging.m index 2d7812c51..7e15efa4c 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 @@ + (UTMLogging *)sharedInstance { } - (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 bea8de9df..7fb5a8be4 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 000000000..8f1fc5c0e --- /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 f9df727ea..62023bbf7 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 f1fbfa3ed..218ef1721 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 000000000..f8eb12d7e --- /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 c162e6a76..122acac91 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 monitor = await monitor else { return } - guard let spiceIO = ioService else { + 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 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 stopAccessingPath(_ path: String) async { + await system?.stopAccessingPath(path) } - + 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 7007b388a..1b5e69517 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 16a81c186..fa812b909 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 2b3b87646..1e0f9dac8 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 857c397bd..2128ad737 100644 --- a/Services/UTMSpiceIO.m +++ b/Services/UTMSpiceIO.m @@ -22,20 +22,23 @@ @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 @@ - (instancetype)initWithSocketUrl:(NSURL *)socketUrl options:(UTMSpiceIOOptions) 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 @@ - (BOOL)startWithError:(NSError * _Nullable *)error { } // 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 @@ - (void)disconnect { 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 @@ - (void)screenshotWithCompletion:(screenshotCallback_t)completion { - (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,19 @@ - (void)spiceInputUnavailable:(CSConnection *)connection input:(CSInput *)input - (void)spiceDisconnected:(CSConnection *)connection { NSAssert(connection == self.spiceConnection, @"Unknown connection"); self.isConnected = NO; + if ([self.delegate respondsToSelector:@selector(spiceDidDisconnect)]) { + [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 +236,9 @@ - (void)spiceDisplayUpdated:(CSConnection *)connection display:(CSDisplay *)disp - (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 +252,16 @@ - (void)spiceAgentDisconnected:(CSConnection *)connection { - (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 +277,11 @@ - (void)spiceForwardedPortClosed:(CSConnection *)connection port:(CSPort *)port } 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 +326,7 @@ - (void)setDelegate:(id<UTMSpiceIODelegate>)delegate { 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 a6376cda6..e38ff7bb7 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 000000000..6adbf24a7 --- /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 c5e8d0f03..b7eec04e1 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 68cf5cd0e..b28e0edf5 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 */; }; @@ -833,6 +855,7 @@ CEA9059625FC6A3B00801E7C /* usbredirparser.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CEA9058825FC69D100801E7C /* usbredirparser.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; CEB20EEA282053320033EFB5 /* DoubleClickHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB20EE9282053320033EFB5 /* DoubleClickHandler.swift */; }; CEB54C852931E32F000D2AA9 /* UTMPatches.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB54C802931C43F000D2AA9 /* UTMPatches.swift */; }; + CEB5C1172B8C4CD4008AAE5C /* Info-RemotePlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CEB5C1192B8C4CD4008AAE5C /* Info-RemotePlist.strings */; }; CEB63A7624F4654400CAF323 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7524F4654400CAF323 /* Main.swift */; }; CEB63A7724F4654400CAF323 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7524F4654400CAF323 /* Main.swift */; }; CEB63A7A24F469E300CAF323 /* UTMJailbreak.m in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7924F469E300CAF323 /* UTMJailbreak.m */; }; @@ -867,13 +890,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 +948,249 @@ 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 */; }; + 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,9 +1522,63 @@ 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 */ + 037DAA1C2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/VMDisplayWindow.strings; sourceTree = "<group>"; }; + 037DAA1D2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + 037DAA1E2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; }; + 037DAA1F2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; }; + 037DAA202B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; + 037DAA212B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; }; + 037DAA222B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMContextMenuModifier.swift; sourceTree = "<group>"; }; 2C6D9E02256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayQemuTerminalWindowController.swift; sourceTree = "<group>"; }; 4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InListButtonStyle.swift; sourceTree = "<group>"; }; @@ -1437,10 +1770,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 +1877,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 +1908,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 +1930,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>"; }; @@ -1630,6 +1972,8 @@ CEB54C1829300C1B000D2AA9 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = "<group>"; }; CEB54C1929300C20000D2AA9 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; CEB54C802931C43F000D2AA9 /* UTMPatches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMPatches.swift; sourceTree = "<group>"; }; + CEB5C1182B8C4CD4008AAE5C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = "en.lproj/Info-RemotePlist.strings"; sourceTree = "<group>"; }; + CEB5C11A2B8C4D30008AAE5C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = "ja.lproj/Info-RemotePlist.strings"; sourceTree = "<group>"; }; CEB63A7524F4654400CAF323 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Main.swift; sourceTree = "<group>"; }; CEB63A7824F468BA00CAF323 /* UTMJailbreak.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMJailbreak.h; sourceTree = "<group>"; }; CEB63A7924F469E300CAF323 /* UTMJailbreak.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMJailbreak.m; sourceTree = "<group>"; }; @@ -1649,6 +1993,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 +2008,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 +2035,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 +2133,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 +2166,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 +2206,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 +2216,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 +2264,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 +2277,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 +2340,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 +2623,7 @@ CEBBF1A624B5730F00C15049 /* UTMDataExtension.swift */, 84E3A91A2946D2590024A740 /* UTMMenuBarExtraScene.swift */, CEB54C802931C43F000D2AA9 /* UTMPatches.swift */, + CEE06B262B2FC89400A811AE /* UTMServerView.swift */, 8401FD9F269D266E00265F0D /* VMConfigAppleBootView.swift */, 8401FDA3269D43CF00265F0D /* VMConfigAppleDisplayView.swift */, 8401FDAF269E1F7F00265F0D /* VMConfigAppleDriveCreateView.swift */, @@ -2210,6 +2643,7 @@ CE2D953D24AD4F980059923A /* VMSettingsView.swift */, CEF0300526A25A6900667B63 /* VMWizardView.swift */, 84BB99392899E8D500DF28B2 /* VMHeadlessSessionState.swift */, + CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */, 53A0BDD426D79FE40010EDC5 /* SavePanel.swift */, CE2D954124AD4F980059923A /* Info.plist */, FFB02A8E266CB09C006CD71A /* InfoPlist.strings */, @@ -2227,11 +2661,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,7 +2683,9 @@ CEF0307026A2B04300667B63 /* VMWizardView.swift */, CE95877426D74C2A0086BDE8 /* iOS.entitlements */, CE2D954F24AD4F980059923A /* Info.plist */, + CECF02572B70909900409FC0 /* Info-Remote.plist */, FFB02A8A266CB09C006CD71A /* InfoPlist.strings */, + CEB5C1192B8C4CD4008AAE5C /* Info-RemotePlist.strings */, 5286EC91243748AC007E6CBC /* Settings.bundle */, ); path = iOS; @@ -2305,6 +2743,7 @@ CE9D18F72265410E00355E14 /* qemu */, CE6B240925F1F3CE0020D43E /* QEMULauncher */, CE9A352E26533A51005077CF /* JailbreakInterposer */, + CE9B153D2B11A4ED003A32DD /* Remote */, CE4698F824C8FBD9008C1BD6 /* Icons */, CEFE98DD2948518D007CB7A8 /* Scripting */, 84E3A8F1293DB37E0024A740 /* utmctl */, @@ -2324,6 +2763,7 @@ CE9A352D26533A51005077CF /* JailbreakInterposer.framework */, 8401FD62269BE9C500265F0D /* QEMULauncher.app */, 84E3A8F0293DB37E0024A740 /* utmctl */, + CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */, ); name = Products; sourceTree = "<group>"; @@ -2341,12 +2781,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 +2800,7 @@ CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */, CE928C2926ABE6690099F293 /* UTMAppleVirtualMachine.swift */, 841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */, + CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */, ); path = Services; sourceTree = "<group>"; @@ -2420,6 +2863,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 +2892,7 @@ CE772AAB25C8B0F600E4E379 /* ContentView.swift */, 8471770527CC974F00D3A50B /* DefaultTextField.swift */, 8432329328C2ED9000CFBC97 /* FileBrowseField.swift */, + CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */, 84F909FE289488F90008DBE2 /* MenuLabel.swift */, CED234EC254796E500ED0A57 /* NumberTextField.swift */, CE19392526DCB093005CEC17 /* RAMSlider.swift */, @@ -2596,6 +3055,8 @@ 84018694288B66370050AC51 /* SwiftUIVisualEffects */, 84CE3DAB2904C14100FF068B /* InAppSettingsKit */, 84A0A8892A47D5D10038F329 /* QEMUKit */, + CE9B15372B11A4A7003A32DD /* SwiftConnect */, + CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */, ); productName = UTM; productReference = CE2D93BE24AD46670059923A /* UTM.app */; @@ -2627,6 +3088,8 @@ 848F71E5277A2466006A0240 /* SwiftTerm */, 84B36D2127B3265400C22685 /* CocoaSpice */, 84A0A8872A47D5C50038F329 /* QEMUKit */, + CE9B15352B11A491003A32DD /* SwiftConnect */, + CEDD11C02B7C74D7004DDAC6 /* SwiftPortmap */, ); productName = UTM; productReference = CE2D951C24AD48BE0059923A /* UTM.app */; @@ -2650,9 +3113,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 +3127,7 @@ ); dependencies = ( ); - name = "iOS-TCI"; + name = "iOS-SE"; packageProductDependencies = ( CEA45E20263519B5002FA97D /* Logging */, CEA45E22263519B5002FA97D /* IQKeyboardManagerSwift */, @@ -2674,6 +3137,8 @@ 84CF5DF2288E433F00D01721 /* SwiftUIVisualEffects */, 846D878529050B6B0095F10B /* InAppSettingsKit */, 84A0A88B2A47D5D70038F329 /* QEMUKit */, + CE9B15392B11A4AE003A32DD /* SwiftConnect */, + CE89CB0F2B8B1B6A006B2CC2 /* VisionKeyboardKit */, ); productName = UTM; productReference = CEA45FB9263519B5002FA97D /* UTM SE.app */; @@ -2698,6 +3163,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 */ @@ -2749,6 +3244,7 @@ pl, "zh-HK", ru, + it, ); mainGroup = CE550BC0225947990063E575; packageReferences = ( @@ -2762,13 +3258,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 +3349,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEF7F6782AEEDCC400E34952 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CEF7F6792AEEDCC400E34952 /* Icons in Resources */, + CEB5C1172B8C4CD4008AAE5C /* Info-RemotePlist.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 +3385,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 +3406,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 +3492,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 +3520,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 +3535,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 +3595,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 +3616,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 +3625,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 +3654,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 +3722,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 +3742,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 +3753,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 +3768,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 +3776,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 +3793,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 +3831,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 +3850,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 +3905,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 +3924,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 +3979,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 */ @@ -3477,6 +4182,7 @@ CEB54C16293009C7000D2AA9 /* pl */, F6DA2DA92AAFED5F0070DCD1 /* zh-HK */, 61EBDEA22AACA83100B959A2 /* ru */, + 037DAA1F2B0B92580061ACB3 /* it */, ); name = Localizable.strings; sourceTree = "<group>"; @@ -3505,6 +4211,7 @@ F6DA2DA62AAFED5F0070DCD1 /* zh-HK */, F6DA2DAF2AAFEE060070DCD1 /* zh-Hans */, 61EBDE9F2AACA83100B959A2 /* ru */, + 037DAA1C2B0B92580061ACB3 /* it */, ); name = VMDisplayWindow.xib; sourceTree = "<group>"; @@ -3526,6 +4233,15 @@ name = VMDisplayMetalViewInputAccessory.xib; sourceTree = "<group>"; }; + CEB5C1192B8C4CD4008AAE5C /* Info-RemotePlist.strings */ = { + isa = PBXVariantGroup; + children = ( + CEB5C1182B8C4CD4008AAE5C /* en */, + CEB5C11A2B8C4D30008AAE5C /* ja */, + ); + name = "Info-RemotePlist.strings"; + sourceTree = "<group>"; + }; CED8DF7928A120C100C34345 /* Localizable.stringsdict */ = { isa = PBXVariantGroup; children = ( @@ -3539,6 +4255,7 @@ F6DA2DAA2AAFED5F0070DCD1 /* zh-HK */, F6DA2DB12AAFF0640070DCD1 /* zh-Hans */, 61EBDEA32AACA83100B959A2 /* ru */, + 037DAA202B0B92580061ACB3 /* it */, ); name = Localizable.stringsdict; sourceTree = "<group>"; @@ -3566,6 +4283,7 @@ 83FE63B828F617CE0047FFEF /* de */, F6DA2DA72AAFED5F0070DCD1 /* zh-HK */, 61EBDEA02AACA83100B959A2 /* ru */, + 037DAA1D2B0B92580061ACB3 /* it */, ); name = InfoPlist.strings; sourceTree = "<group>"; @@ -3584,6 +4302,7 @@ CEB54C14293009C6000D2AA9 /* pl */, F6DA2DA82AAFED5F0070DCD1 /* zh-HK */, 61EBDEA12AACA83100B959A2 /* ru */, + 037DAA1E2B0B92580061ACB3 /* it */, ); name = InfoPlist.strings; sourceTree = "<group>"; @@ -3602,6 +4321,7 @@ 9786BB59294056960032B858 /* ja */, F6DA2DAC2AAFED5F0070DCD1 /* zh-HK */, 61EBDEA52AACA83100B959A2 /* ru */, + 037DAA222B0B92580061ACB3 /* it */, ); name = InfoPlist.strings; sourceTree = "<group>"; @@ -3620,6 +4340,7 @@ CEB54C1829300C1B000D2AA9 /* pl */, F6DA2DAB2AAFED5F0070DCD1 /* zh-HK */, 61EBDEA42AACA83100B959A2 /* ru */, + 037DAA212B0B92580061ACB3 /* it */, ); name = Localizable.strings; sourceTree = "<group>"; @@ -3720,6 +4441,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 +4458,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 +4476,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 +4493,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 +4512,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 +4527,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 +4546,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 +4561,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 +4650,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 +4729,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 +4800,7 @@ ENABLE_PREVIEWS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "WITH_QEMU_TCI=1", + "WITH_SOLO_VM=1", "$(inherited)", ); INFOPLIST_FILE = Platform/iOS/Info.plist; @@ -4065,7 +4816,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 +4836,7 @@ ENABLE_PREVIEWS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "WITH_QEMU_TCI=1", + "WITH_SOLO_VM=1", "$(inherited)", ); INFOPLIST_FILE = Platform/iOS/Info.plist; @@ -4100,7 +4852,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 +4901,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 +5025,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 +5043,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 +5073,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 +5127,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 +5143,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 +5167,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 +5340,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 +5390,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 637d86472..3d420c8b7 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/patches/sources b/patches/sources index 123dd9c08..b8a0bff52 100644 --- a/patches/sources +++ b/patches/sources @@ -37,7 +37,7 @@ SPICE_CLIENT_SRC="https://www.spice-space.org/download/gtk/spice-gtk-0.40.tar.xz # Source files for GPU acceleration WEBKIT_REPO="https://github.com/utmapp/WebKit.git" -WEBKIT_COMMIT="9ce1f852e15f6f322cee0ce727c8fd73657705c9" +WEBKIT_COMMIT="b5f22a32a49682059749b2cccac06231a20c1387" WEBKIT_SUBDIRS="Source/ThirdParty/ANGLE Configurations Tools/ccache" EPOXY_REPO="https://github.com/utmapp/libepoxy.git" EPOXY_COMMIT="266d2290a437c655f7419e85af06bfbb73a720c4" @@ -46,4 +46,4 @@ VIRGLRENDERER_COMMIT="dc039d9ecd74fc671a85bfbe7c4e4bc552b7b855" # Decompiled Hypervisor for iOS HYPERVISOR_REPO="https://github.com/utmapp/Hypervisor.git" -HYPERVISOR_COMMIT="b4eb0e00c03692016944fad3e8e3a6613839912e" +HYPERVISOR_COMMIT="bcec1a8b0cb3b07ecd0f81b33f26fa3048ffd307" diff --git a/scripts/build_dependencies.sh b/scripts/build_dependencies.sh index 75b8d66b2..5bb3e059d 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 191e77a72..6ea37890c 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 9c48bbe46..73b24931a 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"?> diff --git a/scripts/package_mac.sh b/scripts/package_mac.sh index beb6fa0f5..7a68b2435 100755 --- a/scripts/package_mac.sh +++ b/scripts/package_mac.sh @@ -115,8 +115,7 @@ if [ "$MODE" == "app-store" ]; then cp "$SIGNED/UTM.pkg" "$OUTPUT/UTM.pkg" else rm -f "$OUTPUT/UTM.dmg" - command -v appdmg >/dev/null 2>&1 - if [ $? -eq 0 ]; then + if command -v appdmg >/dev/null 2>&1; then RESOURCES="/tmp/resources.$$" cp -r "$BASEDIR/resources" "$RESOURCES" sed -i '' "s/\/tmp\/signed\/UTM.app/\/tmp\/signed.$$\/UTM.app/g" "$RESOURCES/appdmg.json"