Files
UTM/Platform/Shared/VMDetailsView.swift
T
osy f3645d5018 project: remove code for versions below the new minimum
Now that the deployment targets are iOS 15 and macOS 12, every guard,
fallback and workaround for an older release is dead code that only
obscures what the supported path is. Remove them:

- Drop the `@available` attributes for iOS 15, macOS 11 and macOS 12, and
  unwrap the matching `#available`/`#unavailable` guards in Swift and the
  `@available`/`API_AVAILABLE`/`NS_AVAILABLE_IOS` ones in Objective-C.
- Delete the legacy-only views and helpers left unreachable by that:
  NumberTextFieldOld, VMConfigNetworkPortForwardLegacyView,
  VMContextMenuModifier.bodyBigSur, the nonbrokenSheet, uniqued,
  customBadge, borderedList and legacySettingsToolbar shims, the
  .unsupported BootloaderSelection case, and the iOS 14 sacrificial
  version Section in the QEMU settings list.
- jb_has_cs_execseg_allow_unsigned() has returned false for every host
  since iOS 14.4, so drop it along with the is_device_A12_or_newer()
  helper that only it used. The entitlement parser no longer needs to
  copy the blob either, since that copy existed only so the psychic paper
  needle could be blanked in place.
- VMDisplayTerminalViewController tracked the keyboard frame by hand for
  iOS 14. `useAutoLayout` is a constant that cannot be overridden, so
  nothing ever registered the notification observers and only the
  keyboardLayoutGuide path is reachable.
- VMConfigQEMUView's additionalArguments is now referenced on iOS only,
  which leaves its macOS branch, and the macOS 11 ForEach crash
  workaround inside it, unreachable.
- Raise the Cydia package floor and the postinst/prerm helpers to match
  the new minimum.

Two of the removed guards dropped an error path rather than just a
branch. The shared directory `featureNotSupported` throw covered
VZVirtioFileSystemDeviceConfiguration and friends being macOS 12+, which
no longer applies to any supported host. The macOS guest
`platformUnsupported` throw when decoding a boot config on Intel only
fired on macOS 11; the Apple Silicon requirement it really guards is
enforced unconditionally in
UTMAppleConfigurationSystem.fillVZConfiguration, which both the start and
the save path go through.

Assisted-by: Claude:claude-opus-5
2026-09-16 04:48:37 +02:00

502 lines
19 KiB
Swift

//
// Copyright © 2020 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 VMDetailsView: View {
@ObservedObject var vm: VMData
@EnvironmentObject private var data: UTMData
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
#if !os(macOS)
@Environment(\.horizontalSizeClass) private var horizontalSizeClass: UserInterfaceSizeClass?
private var regularScreenSizeClass: Bool {
horizontalSizeClass == .regular
}
#else
private let regularScreenSizeClass: Bool = true
#endif
@State private var size: Int64 = 0
@State private var updateTask: Task<Void, Never>?
private let updateIpIntervalNs = UInt64(10 * 1_000_000_000)
private var sizeLabel: String {
return ByteCountFormatter.string(fromByteCount: size, countStyle: .binary)
}
var body: some View {
if vm.isDeleted {
VStack {
Spacer()
HStack {
Spacer()
Text("This virtual machine has been removed.")
.font(.headline)
Spacer()
}
Spacer()
}
} else {
ScrollView {
Screenshot(vm: vm, large: regularScreenSizeClass)
let notes = vm.detailsNotes ?? ""
if regularScreenSizeClass && !notes.isEmpty {
HStack(alignment: .top) {
Details(vm: vm, sizeLabel: sizeLabel)
.frame(maxWidth: .infinity)
Text(notes)
.font(.body)
.frame(maxWidth: .infinity, alignment: .topLeading)
.fixedSize(horizontal: false, vertical: true)
.padding([.leading, .trailing])
}.padding([.leading, .trailing])
#if os(macOS)
if let appleVM = vm.wrapped as? UTMAppleVirtualMachine {
VMAppleRemovableDrivesView(vm: vm, config: appleVM.config, registryEntry: appleVM.registryEntry)
.padding([.leading, .trailing, .bottom])
} else if let qemuVM = vm.wrapped as? UTMQemuVirtualMachine {
VMRemovableDrivesView(vm: vm, config: qemuVM.config)
.padding([.leading, .trailing, .bottom])
}
#else
let qemuConfig = vm.config as! UTMQemuConfiguration
VMRemovableDrivesView(vm: vm, config: qemuConfig)
.padding([.leading, .trailing, .bottom])
#endif
} else {
VStack {
Details(vm: vm, sizeLabel: sizeLabel)
if !notes.isEmpty {
Text(notes)
.font(.body)
.fixedSize(horizontal: false, vertical: true)
}
#if os(macOS)
if let appleVM = vm.wrapped as? UTMAppleVirtualMachine {
VMAppleRemovableDrivesView(vm: vm, config: appleVM.config, registryEntry: appleVM.registryEntry)
} else if let qemuVM = vm.wrapped as? UTMQemuVirtualMachine {
VMRemovableDrivesView(vm: vm, config: qemuVM.config)
}
#else
let qemuConfig = vm.config as! UTMQemuConfiguration
VMRemovableDrivesView(vm: vm, config: qemuConfig)
#endif
}.padding([.leading, .trailing, .bottom])
}
}.labelStyle(DetailsLabelStyle())
.modifier(VMOptionalNavigationTitleModifier(vm: vm))
.modifier(VMToolbarModifier(vm: vm, bottom: !regularScreenSizeClass))
.sheet(isPresented: $data.showSettingsModal) {
if let qemuConfig = vm.config as? UTMQemuConfiguration {
VMSettingsView(vm: vm, config: qemuConfig)
.environmentObject(data)
}
#if os(macOS)
if let appleConfig = vm.config as? UTMAppleConfiguration {
VMSettingsView(vm: vm, config: appleConfig)
.environmentObject(data)
}
#endif
}
.taskOnAppear(id: vm.id) {
size = await data.computeSize(for: vm)
#if WITH_REMOTE
if let vm = vm.wrapped as? UTMRemoteSpiceVirtualMachine {
await vm.loadScreenshotFromServer()
}
#else
while !Task.isCancelled {
await updateGuestIPs()
try? await Task.sleep(nanoseconds: updateIpIntervalNs)
}
#endif
}
}
}
#if !WITH_REMOTE
private func clearGuestIPs() {
guard let qemuVM = vm.wrapped as? UTMQemuVirtualMachine else {
return
}
for i in qemuVM.config.networks.indices {
qemuVM.config.networks[i].currentIpAddresses = []
}
}
private func updateGuestIPs() async {
guard let qemuVM = vm.wrapped as? UTMQemuVirtualMachine else {
clearGuestIPs()
return
}
guard let guestAgent = await qemuVM.guestAgent, qemuVM.state == .started else {
clearGuestIPs()
return
}
guard let interfaces = try? await guestAgent.guestNetworkGetInterfaces() else {
clearGuestIPs()
logger.error("Failed to get guest IP addresses")
return
}
for i in qemuVM.config.networks.indices {
let macAddress = qemuVM.config.networks[i].macAddress
if let interface = interfaces.first(where: { $0.hardwareAddress?.caseInsensitiveCompare(macAddress) == .orderedSame }) {
let ipAddresses = interface.ipAddresses.compactMap {
if !$0.isIpV6Address && !$0.ipAddress.hasPrefix("127.") {
return $0.ipAddress
} else if $0.isIpV6Address && $0.ipAddress != "::1" && $0.ipAddress != "0:0:0:0:0:0:0:1" {
return $0.ipAddress
} else {
return nil
}
}
qemuVM.config.networks[i].currentIpAddresses = ipAddresses
}
}
}
#endif
}
private extension View {
@ViewBuilder
func taskOnAppear<T>(id value: T, priority: TaskPriority = .userInitiated, _ action: @escaping @Sendable @MainActor () async -> Void) -> some View where T : Equatable & Hashable {
#if os(visionOS) // FIXME: visionOS crashes with task()
self.cancellableTask(priority: priority, action).id(value)
#else
self.task(id: value, priority: priority, action)
#endif
}
@ViewBuilder
private func cancellableTask(priority: TaskPriority, _ action: @escaping @Sendable @MainActor () async -> Void) -> some View {
self.modifier(CancellableTaskViewModifier(priority: priority, action: action))
}
}
private struct CancellableTaskViewModifier: ViewModifier {
let priority: TaskPriority
let action: @Sendable @MainActor () async -> Void
@State private var task: Task<Void, Never>?
func body(content: Content) -> some View {
content.onAppear {
task = Task(priority: priority, operation: action)
}.onDisappear {
task?.cancel()
}
}
}
/// Returns just the content under macOS but adds the title on iOS. #3099
private struct VMOptionalNavigationTitleModifier: ViewModifier {
@ObservedObject var vm: VMData
func body(content: Content) -> some View {
#if os(macOS)
return content.navigationSubtitle(vm.detailsTitleLabel)
#else
return content.navigationTitle(vm.detailsTitleLabel)
#endif
}
}
struct Screenshot: View {
@ObservedObject var vm: VMData
let large: Bool
@EnvironmentObject private var data: UTMData
@Environment(\.colorScheme) private var colorScheme
private let darkOverlay = Color(red: 75/255, green: 74/255, blue: 77/255)
private let lightOverlay = Color(red: 230/255, green: 229/255, blue: 235/255)
var body: some View {
ZStack {
Rectangle()
.fill(Color.black)
if let screenshotImage = vm.screenshotImage {
#if os(macOS)
Image(nsImage: screenshotImage)
.resizable()
.aspectRatio(contentMode: .fit)
#else
Image(uiImage: screenshotImage)
.resizable()
.aspectRatio(contentMode: .fit)
#endif
}
Rectangle()
.fill(colorScheme == .dark ? darkOverlay : lightOverlay)
.blendMode(colorScheme == .dark ? .multiply : .hardLight)
.optionalBackgroundExtensionEffect()
#if os(visionOS)
.overlay {
if vm.isStopped || vm.isTakeoverAllowed {
Image(systemName: "play.circle.fill")
.resizable()
.frame(width: 100, height: 100)
}
}
.hoverEffect()
.onTapGesture {
data.run(vm: vm)
}
#endif
if vm.isBusy {
Spinner(size: .large)
} else if vm.isStopped || vm.isTakeoverAllowed {
#if !os(visionOS)
Button(action: { data.run(vm: vm) }, label: {
Label("Run", systemImage: "play.circle.fill")
.labelStyle(.iconOnly)
.font(Font.system(size: 96))
.foregroundColor(.primary)
}).buttonStyle(.plain)
#endif
}
}.aspectRatio(CGSize(width: 16, height: 9), contentMode: large ? .fill : .fit)
#if os(visionOS)
.frame(maxWidth: 500)
#endif
}
}
private extension View {
@ViewBuilder
func optionalBackgroundExtensionEffect() -> some View {
if #available(iOS 26, macOS 26, visionOS 26, *) {
self.backgroundExtensionEffect()
} else {
self
}
}
}
struct Details: View {
@ObservedObject var vm: VMData
let sizeLabel: String
@EnvironmentObject private var data: UTMData
private func formatPortForward(_ forward: UTMQemuConfigurationPortForward) -> String {
let hostAddr = forward.hostAddress ?? "*"
let guestAddr = forward.guestAddress ?? "*"
return "\(forward.protocol.rawValue) \(hostAddr):\(forward.hostPort) : \(guestAddr):\(forward.guestPort)"
}
var body: some View {
VStack {
if vm.isShortcut {
HStack {
plainLabel("Path", systemImage: "folder")
Spacer()
Text(vm.pathUrl.path)
.foregroundColor(.secondary)
}
}
HStack {
plainLabel("Status", systemImage: "info.circle")
Spacer()
Text(vm.stateLabel)
.foregroundColor(.secondary)
}
HStack {
plainLabel("Architecture", systemImage: "cpu")
Spacer()
Text(vm.detailsSystemArchitectureLabel)
.foregroundColor(.secondary)
}
HStack {
plainLabel("Machine", systemImage: "desktopcomputer")
Spacer()
Text(vm.detailsSystemTargetLabel)
.foregroundColor(.secondary)
}
HStack {
plainLabel("Memory", systemImage: "memorychip")
Spacer()
Text(vm.detailsSystemMemoryLabel)
.foregroundColor(.secondary)
}
HStack {
plainLabel("Size", systemImage: "internaldrive")
Spacer()
Text(sizeLabel)
.foregroundColor(.secondary)
}
#if os(macOS)
if let appleConfig = vm.config as? UTMAppleConfiguration {
ForEach(appleConfig.networks) { network in
HStack {
plainLabel("Network", systemImage: "network")
Spacer()
Text("\(network.mode.prettyValue) (\(network.macAddress))")
.foregroundColor(.secondary)
}
if network.mode == .bridged, let interface = network.bridgeInterface {
HStack {
plainLabel("Bridge Interface", systemImage: "arrow.triangle.branch")
Spacer()
Text(interface)
.foregroundColor(.secondary)
}
}
}
}
#endif
if let qemuConfig = vm.config as? UTMQemuConfiguration {
ForEach(qemuConfig.networks) { network in
HStack {
plainLabel("Network", systemImage: "network")
Spacer()
Text("\(network.mode.prettyValue) (\(network.hardware.prettyValue))")
.foregroundColor(.secondary)
}
HStack {
plainLabel("MAC Address", systemImage: "number")
Spacer()
OptionalSelectableText(network.macAddress)
}
ForEach(network.currentIpAddresses) { guestIP in
HStack {
plainLabel("Guest IP", systemImage: "network.badge.shield.half.filled")
Spacer()
OptionalSelectableText(guestIP)
}
}
if network.mode == .bridged, let interface = network.bridgeInterface {
HStack {
plainLabel("Bridge Interface", systemImage: "arrow.triangle.branch")
Spacer()
Text(interface)
.foregroundColor(.secondary)
}
}
if network.mode == .emulated && !network.portForward.isEmpty {
ForEach(network.portForward) { forward in
HStack {
plainLabel("Port Forward", systemImage: "arrow.triangle.2.circlepath")
Spacer()
Text(formatPortForward(forward))
.foregroundColor(.secondary)
}
}
}
}
}
#if os(macOS)
if let appleConfig = vm.config as? UTMAppleConfiguration {
ForEach(appleConfig.serials) { serial in
if serial.mode == .ptty {
HStack {
plainLabel("Serial (TTY)", systemImage: "phone.connection")
Spacer()
OptionalSelectableText(serial.interface?.name)
}
}
}
}
#endif
if let qemuConfig = vm.config as? UTMQemuConfiguration {
ForEach(qemuConfig.serials) { serial in
if serial.mode == .tcpClient {
HStack {
plainLabel("Serial (Client)", systemImage: "network")
Spacer()
let address = "\(serial.tcpHostAddress ?? "example.com"):\(serial.tcpPort ?? 1234)"
OptionalSelectableText(vm.state == .started ? address : nil)
}
} else if serial.mode == .tcpServer {
HStack {
plainLabel("Serial (Server)", systemImage: "network")
Spacer()
let address = "\(serial.tcpPort ?? 1234)"
OptionalSelectableText(vm.state == .started ? address : nil)
}
}
#if os(macOS)
if serial.mode == .ptty {
HStack {
plainLabel("Serial (TTY)", systemImage: "phone.connection")
Spacer()
OptionalSelectableText(serial.pttyDevice?.path)
}
}
#endif
}
}
}.lineLimit(1)
.truncationMode(.tail)
}
private func plainLabel(_ text: LocalizedStringKey, systemImage: String) -> some View {
return Label {
Text(text)
} icon: {
Image(systemName: systemImage).foregroundColor(.primary)
}
}
}
struct DetailsLabelStyle: LabelStyle {
var color: Color = .accentColor
func makeBody(configuration: Configuration) -> some View {
Label(
title: { configuration.title.font(.headline) },
icon: {
ZStack(alignment: .center) {
Rectangle()
.frame(width: 32, height: 32)
.foregroundColor(.clear)
configuration.icon.foregroundColor(color)
}
})
}
}
private struct OptionalSelectableText: View {
var content: String?
init(_ content: String?) {
self.content = content
}
var body: some View {
(content.map { Text($0) } ?? Text("Inactive", comment: "VMDetailsView"))
.foregroundColor(.secondary)
.textSelection(.enabled)
}
}
struct VMDetailsView_Previews: PreviewProvider {
@State static private var config = UTMQemuConfiguration()
static var previews: some View {
VMDetailsView(vm: VMData(from: .empty))
.onAppear {
config.sharing.directoryShareMode = .webdav
var drive = UTMQemuConfigurationDrive()
drive.imageType = .disk
drive.interface = .ide
config.drives.append(drive)
drive.interface = .scsi
config.drives.append(drive)
drive.imageType = .cd
config.drives.append(drive)
}
}
}