mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
"Run without saving changes" has only been available for QEMU, where it maps to `-snapshot`. Apple Virtualization has no such option, but DiskImageKit on macOS 27 can stack a copy-on-write layer on top of an image and Virtualization accepts the stacked image as a storage attachment. When started with the disposable option, the Apple backend now creates a temporary directory in the sandbox and gives every writable drive an ephemeral ASIF overlay there. The base image is opened read-only and is never touched. The EFI variable store and the macOS auxiliary storage are cloned into the same directory so firmware settings are not persisted either. The directory is deleted when the guest stops or when start fails, any leftover from a previous start is discarded before the next start, and directories orphaned by a crash are swept at app launch. Removable drives attached at start or hot-plugged during the run are covered as well. A disposable run boots fresh and leaves any saved suspend state and the modification date alone, and reports the save/restore path as unsupported so closing the window offers to kill the VM instead of suspending it, matching the QEMU behaviour. The stop paths that delete a saved suspend state skip that while running disposably. The context menu, the Start intent and the menu bar extra now check the backend capability instead of assuming QEMU, and the Apple display window shows the same "Disposable Mode" subtitle as QEMU. Assisted-by: Claude:claude-fable-5-1
217 lines
8.0 KiB
Swift
217 lines
8.0 KiB
Swift
//
|
|
// Copyright © 2025 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 AppIntents
|
|
#if os(macOS)
|
|
import AppKit
|
|
#endif
|
|
|
|
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
|
|
struct UTMStatusActionIntent: AppIntent, UTMIntent {
|
|
static let title: LocalizedStringResource = "Get Virtual Machine Status"
|
|
static let description = IntentDescription("Get the status of a virtual machine.")
|
|
static var parameterSummary: some ParameterSummary {
|
|
Summary("Get \(\.$vmEntity) status")
|
|
}
|
|
|
|
@Dependency
|
|
var data: UTMData
|
|
|
|
@Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
|
|
var vmEntity: UTMVirtualMachineEntity
|
|
|
|
@MainActor
|
|
func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some ReturnsValue<UTMVirtualMachineState> {
|
|
return .result(value: vm.state)
|
|
}
|
|
}
|
|
|
|
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
|
|
struct UTMStartActionIntent: AppIntent, UTMIntent {
|
|
static let title: LocalizedStringResource = "Start Virtual Machine"
|
|
static let description = IntentDescription("Start a virtual machine.")
|
|
static var parameterSummary: some ParameterSummary {
|
|
Summary("Start \(\.$vmEntity)") {
|
|
\.$isRecovery
|
|
\.$isDisposible
|
|
}
|
|
}
|
|
|
|
@Dependency
|
|
var data: UTMData
|
|
|
|
@Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
|
|
var vmEntity: UTMVirtualMachineEntity
|
|
|
|
@Parameter(title: "Recovery", description: "Boot into recovery mode. Only supported on Apple Virtualization backend.", default: false)
|
|
var isRecovery: Bool
|
|
|
|
@Parameter(title: "Disposible", description: "Do not save any changes to disk.", default: false)
|
|
var isDisposible: Bool
|
|
|
|
@MainActor
|
|
func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
|
|
var options = UTMVirtualMachineStartOptions()
|
|
if isRecovery {
|
|
#if os(macOS)
|
|
guard vm is UTMAppleVirtualMachine else {
|
|
throw UTMIntentError.unsupportedBackend
|
|
}
|
|
options.insert(.bootRecovery)
|
|
#else
|
|
throw UTMIntentError.unsupportedBackend
|
|
#endif
|
|
}
|
|
if isDisposible {
|
|
guard type(of: vm).capabilities.supportsDisposibleMode else {
|
|
throw UTMIntentError.unsupportedBackend
|
|
}
|
|
options.insert(.bootDisposibleMode)
|
|
}
|
|
#if os(macOS)
|
|
// Ensure the app comes to the foreground before presenting VM UI
|
|
NSApp.activate(ignoringOtherApps: true)
|
|
#endif
|
|
data.run(vm: boxed, options: options)
|
|
// For platforms that support foreground continuation, request it (no-op on older SDKs).
|
|
if !vm.isHeadless {
|
|
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *), systemContext.currentMode.canContinueInForeground {
|
|
try await continueInForeground(alwaysConfirm: false)
|
|
}
|
|
}
|
|
return .result()
|
|
}
|
|
}
|
|
|
|
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
|
|
struct UTMStopActionIntent: AppIntent, UTMIntent {
|
|
static let title: LocalizedStringResource = "Stop Virtual Machine"
|
|
static let description = IntentDescription("Stop a virtual machine.")
|
|
static var parameterSummary: some ParameterSummary {
|
|
Summary("Stop \(\.$vmEntity) by \(\.$method)")
|
|
}
|
|
|
|
@Dependency
|
|
var data: UTMData
|
|
|
|
@Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
|
|
var vmEntity: UTMVirtualMachineEntity
|
|
|
|
@Parameter(title: "Stop Method", description: "Intensity of the stop action.", default: .force)
|
|
var method: UTMVirtualMachineStopMethod
|
|
|
|
@MainActor
|
|
func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
|
|
try await vm.stop(usingMethod: method)
|
|
return .result()
|
|
}
|
|
}
|
|
|
|
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
|
|
extension UTMVirtualMachineStopMethod: AppEnum {
|
|
static let typeDisplayRepresentation: TypeDisplayRepresentation =
|
|
TypeDisplayRepresentation(
|
|
name: "Stop Method"
|
|
)
|
|
|
|
static let caseDisplayRepresentations: [UTMVirtualMachineStopMethod: DisplayRepresentation] = [
|
|
.request: DisplayRepresentation(title: "Request", subtitle: "Sends power down request to the guest. This simulates pressing the power button on a PC."),
|
|
.force: DisplayRepresentation(title: "Force", subtitle: "Tells the VM process to shut down with risk of data corruption. This simulates holding down the power button on a PC."),
|
|
.kill: DisplayRepresentation(title: "Killing", subtitle: "Force kill the VM process with high risk of data corruption."),
|
|
]
|
|
}
|
|
|
|
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
|
|
struct UTMPauseActionIntent: AppIntent, UTMIntent {
|
|
static let title: LocalizedStringResource = "Pause Virtual Machine"
|
|
static let description = IntentDescription("Pause a virtual machine.")
|
|
static var parameterSummary: some ParameterSummary {
|
|
Summary("Pause \(\.$vmEntity)") {
|
|
\.$isSaveState
|
|
}
|
|
}
|
|
|
|
@Dependency
|
|
var data: UTMData
|
|
|
|
@Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
|
|
var vmEntity: UTMVirtualMachineEntity
|
|
|
|
@Parameter(title: "Save State", description: "Create a snapshot of the virtual machine state.", default: false)
|
|
var isSaveState: Bool
|
|
|
|
@MainActor
|
|
func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
|
|
try await vm.pause()
|
|
if isSaveState {
|
|
try await vm.save()
|
|
}
|
|
return .result()
|
|
}
|
|
}
|
|
|
|
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
|
|
struct UTMResumeActionIntent: AppIntent, UTMIntent {
|
|
static let title: LocalizedStringResource = "Resume Virtual Machine"
|
|
static let description = IntentDescription("Resume a virtual machine.")
|
|
static var parameterSummary: some ParameterSummary {
|
|
Summary("Resume \(\.$vmEntity)")
|
|
}
|
|
|
|
@Dependency
|
|
var data: UTMData
|
|
|
|
@Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
|
|
var vmEntity: UTMVirtualMachineEntity
|
|
|
|
@MainActor
|
|
func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
|
|
try await vm.resume()
|
|
if !vm.isHeadless {
|
|
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *), systemContext.currentMode.canContinueInForeground {
|
|
try await continueInForeground(alwaysConfirm: false)
|
|
}
|
|
}
|
|
return .result()
|
|
}
|
|
}
|
|
|
|
@available(iOS 16, macOS 13, tvOS 16, watchOS 9, *)
|
|
struct UTMRestartActionIntent: AppIntent, UTMIntent {
|
|
static let title: LocalizedStringResource = "Restart Virtual Machine"
|
|
static let description = IntentDescription("Restart a virtual machine.")
|
|
static var parameterSummary: some ParameterSummary {
|
|
Summary("Restart \(\.$vmEntity)")
|
|
}
|
|
|
|
@Dependency
|
|
var data: UTMData
|
|
|
|
@Parameter(title: "Virtual Machine", requestValueDialog: "Select a virtual machine")
|
|
var vmEntity: UTMVirtualMachineEntity
|
|
|
|
@MainActor
|
|
func perform(with vm: any UTMVirtualMachine, boxed: VMData) async throws -> some IntentResult {
|
|
try await vm.restart()
|
|
if !vm.isHeadless {
|
|
if #available(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *), systemContext.currentMode.canContinueInForeground {
|
|
try await continueInForeground(alwaysConfirm: false)
|
|
}
|
|
}
|
|
return .result()
|
|
}
|
|
}
|