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