Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce UTM Remote client for iOS/visionOS #6115

Merged
merged 75 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
4706e22
project: rename preprocessor identifier for conditional features
osy Oct 29, 2023
6155ea8
project: add UTM Remote target
osy Oct 29, 2023
26ceb8b
project: rename scheme iOS-TCI to iOS-SE
osy Oct 29, 2023
ac2d7c4
github: configure build for iOS-Remote
osy Oct 29, 2023
7985cde
remote: implement key manager
osy Jan 24, 2024
1bd1b5f
remote: add remote server
osy Jan 24, 2024
9f5a586
remote(server): add server view
osy Jan 24, 2024
54acd99
remote: add remote client
osy Jan 24, 2024
50ef326
remote(client): add connect view
osy Jan 24, 2024
932ded4
github: update to Xcode 15.2
osy Jan 24, 2024
e2e827b
remote: establish handshake between client and server
osy Jan 27, 2024
75bb06f
vm(qemu): refactor SPICE related functionality to separate protocol
osy Jan 29, 2024
80b88bf
remote: implement listing and displaying VMs
osy Jan 29, 2024
58df784
remote: hide QEMU internals from remote SPICE VM
osy Jan 29, 2024
6414dcc
data: implement remote session management
osy Feb 4, 2024
894a691
vm(qemu): implement starting with remote SPICE client
osy Feb 4, 2024
d2d1418
remote: implement start remote vm
osy Feb 4, 2024
0ace64f
spice: support connecting from socket
osy Feb 5, 2024
be1f4ec
project: separate Info.plist for UTM Remote
osy Feb 5, 2024
a258ec3
project: remove UTMQemuVirtualMachine from Remote client builds
osy Feb 5, 2024
f5169c8
build: strip broken entitlements from ANGLE signature
osy Feb 5, 2024
545e36b
remote: implement client side connection
osy Feb 5, 2024
27103a9
remote: implement other VM actions
osy Feb 6, 2024
4fada3a
settings: exclude non-compatible settings for iOS SE and Remote builds
osy Feb 6, 2024
67f127a
project: remove QEMUKit dependency from iOS-Remote
osy Feb 6, 2024
6c9a660
remote: rework how client state transitions are handled
osy Feb 9, 2024
dbdf749
remote: send custom icon
osy Feb 9, 2024
ea958e6
remote: support SPICE TLS
osy Feb 11, 2024
1afd66a
home(visionOS): fix crash
osy Feb 11, 2024
745cd38
remote: add SPICE ticket password auth
osy Feb 11, 2024
fd4f173
connect: show model of Mac
osy Feb 11, 2024
38b5144
remote(client): implement fingerprint verification and improved conne…
osy Feb 12, 2024
427a201
remote(client): support connecting to specified host and port
osy Feb 12, 2024
eda9e94
remote: add fingerprint verification for client and server
osy Feb 12, 2024
d966e1c
preferences: add server settings
osy Feb 12, 2024
c3939e3
remote: add password authentication
osy Feb 12, 2024
a3b0c76
remote(client): implement timeout for connect attempt
osy Feb 13, 2024
ac5842c
remote(server): implement autostart, autoblock, and specify port
osy Feb 13, 2024
dd4de86
remote(server): add automatic NAT configuration
osy Feb 14, 2024
3232168
remote: support NAT mapping of SPICE port
osy Feb 14, 2024
c9927a3
connect: change trust button back to connect when entering password
osy Feb 14, 2024
db038df
vm(qemu): if serverPort is set, allocate unused port after it
osy Feb 16, 2024
452f8c2
remote: implement ReorderVirtualMachines and GetPackageSize
osy Feb 16, 2024
4c910d8
home: disable features not implemented for remote yet
osy Feb 16, 2024
1bed953
remote: sync home view between multiple clients and server
osy Feb 18, 2024
324f981
keyboard(iOS): disable logging for privacy reasons
osy Feb 18, 2024
2538c20
vm(remote): avoid state update collisions
osy Feb 18, 2024
5f7e11e
display(visionOS): dynamic resolution from window resize
osy Feb 21, 2024
45d216a
config(qemu): fix invalid GPU remapping for remote
osy Feb 21, 2024
8654fb3
remote: fix duplicate list changed events
osy Feb 21, 2024
8c88fd9
display(iOS): make QEMU errors non-fatal
osy Feb 21, 2024
689367a
home(iOS): support multiple sessions
osy Feb 21, 2024
9a871c0
display(visionOS): disable background auto-suspend on visionOS
osy Feb 21, 2024
bc30839
remote: do not discard saved password on connect
osy Feb 21, 2024
8ea2fb4
display(iOS): fix crash due to race when re-sizing while a view is be…
osy Feb 21, 2024
f812ab2
toolbar(iOS): disable removable drives from remote clients
osy Feb 21, 2024
07650fa
remote: fix continuation bug
osy Feb 21, 2024
da9c5c4
vm: get screenshot PNG data early
osy Feb 21, 2024
e6653fd
remote: add mount support tools command
osy Feb 21, 2024
d83fedf
vmdata: fix memory leak
osy Feb 23, 2024
f2f9db1
vm(qemu): fix hang when vm was improperly stopped
osy Feb 23, 2024
31ebc6f
remote: use separate queue for handling connections
osy Feb 23, 2024
52a1f45
session: force kill vm when multiple VMs are supported
osy Feb 24, 2024
0a8bff6
data: busyWorkAsync should return the task
osy Feb 24, 2024
4dca247
remote: re-connect when server is disconnected
osy Feb 24, 2024
f446c1c
vm(remote): fix memory leak
osy Feb 24, 2024
b762149
vm(remote): handle SPICE disconnect
osy Feb 24, 2024
51a7969
remote: support takeover of existing session and auto-pause of orphan…
osy Feb 25, 2024
3a57588
vm(remote): keep existing VM object when refreshing list
osy Feb 25, 2024
7b94235
display(visionOS): disable GCMouse due to it not working
osy Feb 25, 2024
1a966d2
home(visionOS): use plain window style
osy Feb 25, 2024
2947306
display(iOS): do not stop session until after popup is dismissed
osy Feb 25, 2024
e4dab5d
display(visionOS): disable hidden cursor because it is broken
osy Feb 25, 2024
9835e2b
project: update dependencies
osy Feb 25, 2024
aa071bd
display(visionOS): integrate VisionKeyboardKit
osy Feb 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 27 additions & 23 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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*
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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 }}
Expand All @@ -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:
Expand All @@ -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
Expand Down
20 changes: 10 additions & 10 deletions Configuration/QEMUConstant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Configuration/UTMConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions Configuration/UTMConfigurationDrive.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
//

import Foundation
import QEMUKitInternal

/// Settings for single disk device
protocol UTMConfigurationDrive: Codable, Hashable, Identifiable {
Expand Down Expand Up @@ -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)
Expand Down
121 changes: 105 additions & 16 deletions Configuration/UTMQemuConfiguration+Arguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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)"
}
Expand All @@ -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")
}
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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" }) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand Down
9 changes: 9 additions & 0 deletions Configuration/UTMQemuConfigurationQEMU.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading
Loading