Răsfoiți Sursa

scripting: add make virtual machine

osy 2 ani în urmă
părinte
comite
1d42ceb7fe

+ 4 - 0
Platform/macOS/UTMPatches.swift

@@ -123,6 +123,10 @@ extension NSApplication {
         }
         }
     }
     }
     
     
+    @objc func handleCreateCommand(_ command: NSCreateCommand) {
+        (scriptingDelegate as? AppDelegate)?.handleCreateCommand(command)
+    }
+    
     fileprivate static func patchApplicationScripting() {
     fileprivate static func patchApplicationScripting() {
         patch(#selector(Self.value(forKey:)),
         patch(#selector(Self.value(forKey:)),
               with: #selector(Self.xxx_value(forKey:)),
               with: #selector(Self.xxx_value(forKey:)),

+ 6 - 97
Scripting/UTM.sdef

@@ -1,103 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
 <!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
 
 
-<dictionary title="UTM Terminology">
+<dictionary title="UTM Terminology" xmlns:xi="http://www.w3.org/2003/XInclude">
 
 
-    <suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
-        <access-group identifier="com.utmapp.UTM.vm-access" />
-
-        <enumeration name="printing error handling" code="enum">
-            <enumerator name="standard" code="lwst" description="Standard PostScript error handling">
-                <cocoa boolean-value="NO"/>
-            </enumerator>
-            <enumerator name="detailed" code="lwdt" description="print a detailed report of PostScript errors">
-                <cocoa boolean-value="YES"/>
-            </enumerator>
-        </enumeration>
-        
-        <command name="close" code="coreclos" description="Close a document.">
-            <cocoa class="NSCloseCommand"/>
-            <access-group identifier="*"/>
-            <direct-parameter type="specifier" requires-access="r" description="the document(s) or window(s) to close."/>
-        </command>
-
-        <command name="quit" code="aevtquit" description="Quit the application.">
-            <cocoa class="NSQuitCommand"/>
-        </command>
-
-        <command name="count" code="corecnte" description="Return the number of elements of a particular class within an object.">
-            <cocoa class="NSCountCommand"/>
-            <access-group identifier="*"/>
-            <direct-parameter type="specifier" requires-access="r" description="The objects to be counted."/>
-            <parameter name="each" code="kocl" type="type" optional="yes" description="The class of objects to be counted." hidden="yes">
-                <cocoa key="ObjectClass"/>
-            </parameter>
-            <result type="integer" description="The count."/>
-        </command>
-
-        <command name="exists" code="coredoex" description="Verify that an object exists.">
-            <cocoa class="NSExistsCommand"/>
-            <access-group identifier="*"/>
-            <direct-parameter type="any" requires-access="r" description="The object(s) to check."/>
-            <result type="boolean" description="Did the object(s) exist?"/>
-        </command>
-
-        <class name="application" code="capp" description="The application's top-level scripting object.">
-            <cocoa class="NSApplication"/>
-            <property name="name" code="pnam" type="text" access="r" description="The name of the application."/>
-            <property name="frontmost" code="pisf" type="boolean" access="r" description="Is this the active application?">
-                <cocoa key="isActive"/>
-            </property>
-            <property name="version" code="vers" type="text" access="r" description="The version number of the application."/>
-            <element type="window" access="r">
-                <cocoa key="orderedWindows"/>
-            </element>
-            <responds-to command="quit">
-                <cocoa method="handleQuitScriptCommand:"/>
-            </responds-to>
-        </class>
-
-        <class name="window" code="cwin" description="A window.">
-            <cocoa class="NSWindow"/>
-            <property name="name" code="pnam" type="text" access="r" description="The title of the window.">
-                <cocoa key="title"/>
-            </property>
-            <property name="id" code="ID  " type="integer" access="r" description="The unique identifier of the window.">
-                <cocoa key="uniqueID"/>
-            </property>
-            <property name="index" code="pidx" type="integer" description="The index of the window, ordered front to back.">
-                <cocoa key="orderedIndex"/>
-            </property>
-            <property name="bounds" code="pbnd" type="rectangle" description="The bounding rectangle of the window.">
-                <cocoa key="boundsAsQDRect"/>
-            </property>
-            <property name="closeable" code="hclb" type="boolean" access="r" description="Does the window have a close button?">
-                <cocoa key="hasCloseBox"/>
-            </property>
-            <property name="miniaturizable" code="ismn" type="boolean" access="r" description="Does the window have a minimize button?">
-                <cocoa key="isMiniaturizable"/>
-            </property>
-            <property name="miniaturized" code="pmnd" type="boolean" description="Is the window minimized right now?">
-                <cocoa key="isMiniaturized"/>
-            </property>
-            <property name="resizable" code="prsz" type="boolean" access="r" description="Can the window be resized?">
-                <cocoa key="isResizable"/>
-            </property>
-            <property name="visible" code="pvis" type="boolean" description="Is the window visible right now?">
-                <cocoa key="isVisible"/>
-            </property>
-            <property name="zoomable" code="iszm" type="boolean" access="r" description="Does the window have a zoom button?">
-                <cocoa key="isZoomable"/>
-            </property>
-            <property name="zoomed" code="pzum" type="boolean" description="Is the window zoomed right now?">
-                <cocoa key="isZoomed"/>
-            </property>
-            <responds-to command="close">
-                <cocoa method="handleCloseScriptCommand:"/>
-            </responds-to>
-        </class>
-
-    </suite>
+    <xi:include href="file:///System/Library/ScriptingDefinitions/CocoaStandard.sdef" xpointer="xpointer(/dictionary/suite)"/>
     
     
     <suite name="UTM Suite" code="UTMs" description="UTM virtual machines scripting suite.">
     <suite name="UTM Suite" code="UTMs" description="UTM virtual machines scripting suite.">
         <access-group identifier="com.utmapp.UTM.vm-access" />
         <access-group identifier="com.utmapp.UTM.vm-access" />
@@ -109,6 +15,9 @@
           <property name="auto terminate" code="kRlW" type="boolean" description="Auto terminate the application when all windows are closed?">
           <property name="auto terminate" code="kRlW" type="boolean" description="Auto terminate the application when all windows are closed?">
               <cocoa key="isAutoTerminate"/>
               <cocoa key="isAutoTerminate"/>
           </property>
           </property>
+          <responds-to command="make">
+            <cocoa method="handleCreateCommand:"/>
+          </responds-to>
         </class-extension>
         </class-extension>
         
         
         <enumeration name="backend" code="VmEb" description="Backend type.">
         <enumeration name="backend" code="VmEb" description="Backend type.">
@@ -463,7 +372,7 @@
           <property name="name" code="pnam" type="text"
           <property name="name" code="pnam" type="text"
             description="Virtual machine name."/>
             description="Virtual machine name."/>
             
             
-          <property name="notes" code="QcAr" type="text"
+          <property name="notes" code="QcNt" type="text"
             description="User-specified notes."/>
             description="User-specified notes."/>
             
             
           <property name="architecture" code="QcAr" type="text"
           <property name="architecture" code="QcAr" type="text"

+ 0 - 3
Scripting/UTMScriptable.swift

@@ -25,9 +25,6 @@ extension UTMScriptable {
     ///   - body: What to do
     ///   - body: What to do
     @MainActor
     @MainActor
     func withScriptCommand<Result>(_ command: NSScriptCommand, body: @MainActor @escaping () async throws -> Result) {
     func withScriptCommand<Result>(_ command: NSScriptCommand, body: @MainActor @escaping () async throws -> Result) {
-        guard command.evaluatedReceivers as? Self == self else {
-            return
-        }
         command.suspendExecution()
         command.suspendExecution()
         // we need to run this in next event loop due to the need to return before calling resume
         // we need to run this in next event loop due to the need to return before calling resume
         DispatchQueue.main.async {
         DispatchQueue.main.async {

+ 26 - 3
Scripting/UTMScripting.swift

@@ -22,7 +22,6 @@ public enum UTMScripting: String {
     case guestProcess = "guest process"
     case guestProcess = "guest process"
     case serialPort = "serial port"
     case serialPort = "serial port"
     case virtualMachine = "virtual machine"
     case virtualMachine = "virtual machine"
-    case window = "window"
 }
 }
 
 
 import AppKit
 import AppKit
@@ -38,6 +37,13 @@ import ScriptingBridge
     var isRunning: Bool { get }
     var isRunning: Bool { get }
 }
 }
 
 
+// MARK: UTMScriptingSaveOptions
+@objc public enum UTMScriptingSaveOptions : AEKeyword {
+    case yes = 0x79657320 /* 'yes ' */
+    case no = 0x6e6f2020 /* 'no  ' */
+    case ask = 0x61736b20 /* 'ask ' */
+}
+
 // MARK: UTMScriptingPrintingErrorHandling
 // MARK: UTMScriptingPrintingErrorHandling
 @objc public enum UTMScriptingPrintingErrorHandling : AEKeyword {
 @objc public enum UTMScriptingPrintingErrorHandling : AEKeyword {
     case standard = 0x6c777374 /* 'lwst' */
     case standard = 0x6c777374 /* 'lwst' */
@@ -133,16 +139,24 @@ import ScriptingBridge
 
 
 // MARK: UTMScriptingGenericMethods
 // MARK: UTMScriptingGenericMethods
 @objc public protocol UTMScriptingGenericMethods {
 @objc public protocol UTMScriptingGenericMethods {
-    @objc optional func close() // Close a document.
+    @objc optional func closeSaving(_ saving: UTMScriptingSaveOptions, savingIn: URL!) // Close a document.
+    @objc optional func saveIn(_ in_: URL!, as: Any!) // Save a document.
+    @objc optional func printWithProperties(_ withProperties: [AnyHashable : Any]!, printDialog: Bool) // Print a document.
+    @objc optional func delete() // Delete an object.
+    @objc optional func duplicateTo(_ to: SBObject!, withProperties: [AnyHashable : Any]!) // Copy an object.
+    @objc optional func moveTo(_ to: SBObject!) // Move an object to a new location.
 }
 }
 
 
 // MARK: UTMScriptingApplication
 // MARK: UTMScriptingApplication
 @objc public protocol UTMScriptingApplication: SBApplicationProtocol {
 @objc public protocol UTMScriptingApplication: SBApplicationProtocol {
+    @objc optional func documents() -> SBElementArray
     @objc optional func windows() -> SBElementArray
     @objc optional func windows() -> SBElementArray
     @objc optional var name: String { get } // The name of the application.
     @objc optional var name: String { get } // The name of the application.
     @objc optional var frontmost: Bool { get } // Is this the active application?
     @objc optional var frontmost: Bool { get } // Is this the active application?
     @objc optional var version: String { get } // The version number of the application.
     @objc optional var version: String { get } // The version number of the application.
-    @objc optional func quit() // Quit the application.
+    @objc optional func `open`(_ x: Any!) -> Any // Open a document.
+    @objc optional func print(_ x: Any!, withProperties: [AnyHashable : Any]!, printDialog: Bool) // Print a document.
+    @objc optional func quitSaving(_ saving: UTMScriptingSaveOptions) // Quit the application.
     @objc optional func exists(_ x: Any!) -> Bool // Verify that an object exists.
     @objc optional func exists(_ x: Any!) -> Bool // Verify that an object exists.
     @objc optional func virtualMachines() -> SBElementArray
     @objc optional func virtualMachines() -> SBElementArray
     @objc optional var autoTerminate: Bool { get } // Auto terminate the application when all windows are closed?
     @objc optional var autoTerminate: Bool { get } // Auto terminate the application when all windows are closed?
@@ -150,6 +164,14 @@ import ScriptingBridge
 }
 }
 extension SBApplication: UTMScriptingApplication {}
 extension SBApplication: UTMScriptingApplication {}
 
 
+// MARK: UTMScriptingDocument
+@objc public protocol UTMScriptingDocument: SBObjectProtocol, UTMScriptingGenericMethods {
+    @objc optional var name: String { get } // Its name.
+    @objc optional var modified: Bool { get } // Has it been modified since the last save?
+    @objc optional var file: URL { get } // Its location on disk, if it has one.
+}
+extension SBObject: UTMScriptingDocument {}
+
 // MARK: UTMScriptingWindow
 // MARK: UTMScriptingWindow
 @objc public protocol UTMScriptingWindow: SBObjectProtocol, UTMScriptingGenericMethods {
 @objc public protocol UTMScriptingWindow: SBObjectProtocol, UTMScriptingGenericMethods {
     @objc optional var name: String { get } // The title of the window.
     @objc optional var name: String { get } // The title of the window.
@@ -163,6 +185,7 @@ extension SBApplication: UTMScriptingApplication {}
     @objc optional var visible: Bool { get } // Is the window visible right now?
     @objc optional var visible: Bool { get } // Is the window visible right now?
     @objc optional var zoomable: Bool { get } // Does the window have a zoom button?
     @objc optional var zoomable: Bool { get } // Does the window have a zoom button?
     @objc optional var zoomed: Bool { get } // Is the window zoomed right now?
     @objc optional var zoomed: Bool { get } // Is the window zoomed right now?
+    @objc optional var document: UTMScriptingDocument { get } // The document whose contents are displayed in the window.
     @objc optional func setIndex(_ index: Int) // The index of the window, ordered front to back.
     @objc optional func setIndex(_ index: Int) // The index of the window, ordered front to back.
     @objc optional func setBounds(_ bounds: NSRect) // The bounding rectangle of the window.
     @objc optional func setBounds(_ bounds: NSRect) // The bounding rectangle of the window.
     @objc optional func setMiniaturized(_ miniaturized: Bool) // Is the window minimized right now?
     @objc optional func setMiniaturized(_ miniaturized: Bool) // Is the window minimized right now?

+ 141 - 0
Scripting/UTMScriptingAppDelegate.swift

@@ -0,0 +1,141 @@
+//
+// 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
+
+@MainActor
+@objc extension AppDelegate: UTMScriptable {
+    private var bytesInMib: Int {
+        1048576
+    }
+    
+    private var bytesInGib: Int {
+        1073741824
+    }
+    
+    @objc func handleCreateCommand(_ command: NSCreateCommand) {
+        if command.createClassDescription.implementationClassName == "UTMScriptingVirtualMachineImpl" {
+            let properties = command.resolvedKeyDictionary
+            withScriptCommand(command) { [self] in
+                guard let backend = properties["backend"] as? AEKeyword, let backend = UTMScriptingBackend(rawValue: backend) else {
+                    throw ScriptingError.backendNotFound
+                }
+                guard let configuration = properties["configuration"] as? [AnyHashable : Any] else {
+                    throw ScriptingError.configurationNotFound
+                }
+                if backend == .qemu {
+                    return try await createQemuVirtualMachine(from: configuration).objectSpecifier
+                } else if backend == .apple {
+                    return try await createAppleVirtualMachine(from: configuration).objectSpecifier
+                } else {
+                    throw ScriptingError.backendNotFound
+                }
+            }
+        } else {
+            command.performDefaultImplementation()
+        }
+    }
+    
+    private func createQemuVirtualMachine(from record: [AnyHashable : Any]) async throws -> UTMScriptingVirtualMachineImpl {
+        guard let data = data else {
+            throw ScriptingError.notReady
+        }
+        guard record["name"] as? String != nil else {
+            throw ScriptingError.nameNotSpecified
+        }
+        guard let architecture = record["architecture"] as? String, let architecture = QEMUArchitecture(rawValue: architecture) else {
+            throw ScriptingError.architectureNotSpecified
+        }
+        let machine = record["machine"] as? String
+        let target = architecture.targetType.init(rawValue: machine ?? "") ?? architecture.targetType.default
+        let config = UTMQemuConfiguration()
+        config.system.architecture = architecture
+        config.system.target = target
+        config.reset(forArchitecture: architecture, target: target)
+        config.qemu.hasHypervisor = true
+        config.qemu.hasUefiBoot = true
+        // add default drives
+        config.drives.append(UTMQemuConfigurationDrive(forArchitecture: architecture, target: target, isExternal: true))
+        var fixed = UTMQemuConfigurationDrive(forArchitecture: architecture, target: target)
+        fixed.sizeMib = 64 * bytesInGib / bytesInMib
+        config.drives.append(fixed)
+        // add a default serial device
+        var serial = UTMQemuConfigurationSerial()
+        serial.mode = .ptty
+        config.serials = [serial]
+        // remove GUI devices
+        config.displays = []
+        config.sound = []
+        // parse the remaining config
+        let wrapper = UTMScriptingConfigImpl(config)
+        try wrapper.updateConfiguration(from: record)
+        // create the vm
+        let vm = try await data.create(config: config)
+        return UTMScriptingVirtualMachineImpl(for: vm, data: data)
+    }
+    
+    private func createAppleVirtualMachine(from record: [AnyHashable : Any]) async throws -> UTMScriptingVirtualMachineImpl {
+        guard let data = data else {
+            throw ScriptingError.notReady
+        }
+        guard #available(macOS 13, *) else {
+            throw ScriptingError.backendNotSupported
+        }
+        guard record["name"] as? String != nil else {
+            throw ScriptingError.nameNotSpecified
+        }
+        let config = UTMAppleConfiguration()
+        config.system.boot = try UTMAppleConfigurationBoot(for: .linux)
+        config.virtualization.hasBalloon = true
+        config.virtualization.hasEntropy = true
+        config.networks = [UTMAppleConfigurationNetwork()]
+        // remove any display devices
+        config.displays = []
+        // add a default serial device
+        var serial = UTMAppleConfigurationSerial()
+        serial.mode = .ptty
+        config.serials = [serial]
+        // add default drives
+        config.drives.append(UTMAppleConfigurationDrive(existingURL: nil, isExternal: true))
+        config.drives.append(UTMAppleConfigurationDrive(newSize: 64 * bytesInGib / bytesInMib))
+        // parse the remaining config
+        let wrapper = UTMScriptingConfigImpl(config)
+        try wrapper.updateConfiguration(from: record)
+        // create the vm
+        let vm = try await data.create(config: config)
+        return UTMScriptingVirtualMachineImpl(for: vm, data: data)
+    }
+    
+    enum ScriptingError: Error, LocalizedError {
+        case notReady
+        case backendNotFound
+        case backendNotSupported
+        case configurationNotFound
+        case nameNotSpecified
+        case architectureNotSpecified
+        
+        var errorDescription: String? {
+            switch self {
+            case .notReady: return NSLocalizedString("UTM is not ready to accept commands.", comment: "UTMScriptingAppDelegate")
+            case .backendNotFound: return NSLocalizedString("A valid backend must be specified.", comment: "UTMScriptingAppDelegate")
+            case .backendNotSupported: return NSLocalizedString("This backend is not supported on your machine.", comment: "UTMScriptingAppDelegate")
+            case .configurationNotFound: return NSLocalizedString("A valid configuration must be specified.", comment: "UTMScriptingAppDelegate")
+            case .nameNotSpecified: return NSLocalizedString("No name specified in the configuration.", comment: "UTMScriptingAppDelegate")
+            case .architectureNotSpecified: return NSLocalizedString("No architecture specified in the configuration.", comment: "UTMScriptingAppDelegate")
+            }
+        }
+    }
+}

+ 59 - 35
Scripting/UTMScriptingConfigImpl.swift

@@ -18,13 +18,8 @@ import Foundation
 
 
 @objc extension UTMScriptingVirtualMachineImpl {
 @objc extension UTMScriptingVirtualMachineImpl {
     @objc var configuration: [AnyHashable : Any] {
     @objc var configuration: [AnyHashable : Any] {
-        if let vm = vm as? UTMQemuVirtualMachine {
-            return serializeQemuConfiguration(vm.qemuConfig)
-        } else if let vm = vm as? UTMAppleVirtualMachine {
-            return serializeAppleConfiguration(vm.appleConfig)
-        } else {
-            fatalError()
-        }
+        let wrapper = UTMScriptingConfigImpl(vm.config.wrappedValue as! any UTMConfiguration, data: data)
+        return wrapper.serializeConfiguration()
     }
     }
     
     
     @objc func updateConfiguration(_ command: NSScriptCommand) {
     @objc func updateConfiguration(_ command: NSScriptCommand) {
@@ -36,24 +31,60 @@ import Foundation
             guard vm.state == .vmStopped else {
             guard vm.state == .vmStopped else {
                 throw ScriptingError.notStopped
                 throw ScriptingError.notStopped
             }
             }
-            if backend == .qemu {
-                try updateQemuConfiguration(from: newConfiguration)
-            } else if backend == .apple {
-                try updateAppleConfiguration(from: newConfiguration)
-            } else {
-                fatalError()
-            }
+            let wrapper = UTMScriptingConfigImpl(vm.config.wrappedValue as! any UTMConfiguration)
+            try wrapper.updateConfiguration(from: newConfiguration)
             try await data.save(vm: vm)
             try await data.save(vm: vm)
         }
         }
     }
     }
 }
 }
 
 
 @MainActor
 @MainActor
-extension UTMScriptingVirtualMachineImpl {
+class UTMScriptingConfigImpl {
     private var bytesInMib: Int64 {
     private var bytesInMib: Int64 {
         1048576
         1048576
     }
     }
     
     
+    private(set) var config: any UTMConfiguration
+    private weak var data: UTMData?
+    
+    init(_ config: any UTMConfiguration, data: UTMData? = nil) {
+        self.config = config
+        self.data = data
+    }
+    
+    func serializeConfiguration() -> [AnyHashable : Any] {
+        if let qemuConfig = config as? UTMQemuConfiguration {
+            return serializeQemuConfiguration(qemuConfig)
+        } else if let appleConfig = config as? UTMAppleConfiguration {
+            return serializeAppleConfiguration(appleConfig)
+        } else {
+            fatalError()
+        }
+    }
+    
+    func updateConfiguration(from record: [AnyHashable : Any]) throws {
+        if let _ = config as? UTMQemuConfiguration {
+            try updateQemuConfiguration(from: record)
+        } else if let _ = config as? UTMAppleConfiguration {
+            try updateAppleConfiguration(from: record)
+        } else {
+            fatalError()
+        }
+    }
+    
+    private func size(of drive: any UTMConfigurationDrive) -> Int {
+        guard let data = data else {
+            return 0
+        }
+        guard let url = drive.imageURL else {
+            return 0
+        }
+        return Int(data.computeSize(for: url) / bytesInMib)
+    }
+}
+
+@MainActor
+extension UTMScriptingConfigImpl {
     private func qemuDirectoryShareMode(from mode: QEMUFileShareMode) -> UTMScriptingQemuDirectoryShareMode {
     private func qemuDirectoryShareMode(from mode: QEMUFileShareMode) -> UTMScriptingQemuDirectoryShareMode {
         switch mode {
         switch mode {
         case .none: return .none
         case .none: return .none
@@ -79,13 +110,6 @@ extension UTMScriptingVirtualMachineImpl {
         ]
         ]
     }
     }
     
     
-    private func size(of drive: any UTMConfigurationDrive) -> Int {
-        guard let url = drive.imageURL else {
-            return 0
-        }
-        return Int(data.computeSize(for: url) / bytesInMib)
-    }
-    
     private func qemuDriveInterface(from interface: QEMUDriveInterface) -> UTMScriptingQemuDriveInterface {
     private func qemuDriveInterface(from interface: QEMUDriveInterface) -> UTMScriptingQemuDriveInterface {
         switch interface {
         switch interface {
         case .none: return .none
         case .none: return .none
@@ -224,7 +248,7 @@ extension UTMScriptingVirtualMachineImpl {
 }
 }
 
 
 @MainActor
 @MainActor
-extension UTMScriptingVirtualMachineImpl {
+extension UTMScriptingConfigImpl {
     private func updateElements<T>(_ array: inout [T], with records: [[AnyHashable : Any]], onExisting: @MainActor (inout T, [AnyHashable : Any]) throws -> Void, onNew: @MainActor ([AnyHashable : Any]) throws -> T) throws {
     private func updateElements<T>(_ array: inout [T], with records: [[AnyHashable : Any]], onExisting: @MainActor (inout T, [AnyHashable : Any]) throws -> Void, onNew: @MainActor ([AnyHashable : Any]) throws -> T) throws {
         var unseenIndicies = IndexSet(integersIn: array.indices)
         var unseenIndicies = IndexSet(integersIn: array.indices)
         for record in records {
         for record in records {
@@ -270,7 +294,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateQemuConfiguration(from record: [AnyHashable : Any]) throws {
     private func updateQemuConfiguration(from record: [AnyHashable : Any]) throws {
-        let config = (vm as! UTMQemuVirtualMachine).qemuConfig
+        let config = config as! UTMQemuConfiguration
         if let name = record["name"] as? String, !name.isEmpty {
         if let name = record["name"] as? String, !name.isEmpty {
             config.information.name = name
             config.information.name = name
         }
         }
@@ -336,7 +360,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateQemuDrives(from records: [[AnyHashable : Any]]) throws {
     private func updateQemuDrives(from records: [[AnyHashable : Any]]) throws {
-        let config = (vm as! UTMQemuVirtualMachine).qemuConfig
+        let config = config as! UTMQemuConfiguration
         try updateIdentifiedElements(&config.drives, with: records, onExisting: updateQemuExistingDrive, onNew: unserializeQemuDriveNew)
         try updateIdentifiedElements(&config.drives, with: records, onExisting: updateQemuExistingDrive, onNew: unserializeQemuDriveNew)
     }
     }
     
     
@@ -347,7 +371,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func unserializeQemuDriveNew(from record: [AnyHashable : Any]) throws -> UTMQemuConfigurationDrive {
     private func unserializeQemuDriveNew(from record: [AnyHashable : Any]) throws -> UTMQemuConfigurationDrive {
-        let config = (vm as! UTMQemuVirtualMachine).qemuConfig
+        let config = config as! UTMQemuConfiguration
         let removable = record["removable"] as? Bool ?? false
         let removable = record["removable"] as? Bool ?? false
         var newDrive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: removable)
         var newDrive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: removable)
         if let importUrl = record["source"] as? URL {
         if let importUrl = record["source"] as? URL {
@@ -365,7 +389,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateQemuNetworks(from records: [[AnyHashable : Any]]) throws {
     private func updateQemuNetworks(from records: [[AnyHashable : Any]]) throws {
-        let config = (vm as! UTMQemuVirtualMachine).qemuConfig
+        let config = config as! UTMQemuConfiguration
         try updateElements(&config.networks, with: records, onExisting: updateQemuExistingNetwork, onNew: { record in
         try updateElements(&config.networks, with: records, onExisting: updateQemuExistingNetwork, onNew: { record in
             guard var newNetwork = UTMQemuConfigurationNetwork(forArchitecture: config.system.architecture, target: config.system.target) else {
             guard var newNetwork = UTMQemuConfigurationNetwork(forArchitecture: config.system.architecture, target: config.system.target) else {
                 throw ConfigurationError.deviceNotSupported
                 throw ConfigurationError.deviceNotSupported
@@ -389,7 +413,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateQemuExistingNetwork(_ network: inout UTMQemuConfigurationNetwork, from record: [AnyHashable : Any]) throws {
     private func updateQemuExistingNetwork(_ network: inout UTMQemuConfigurationNetwork, from record: [AnyHashable : Any]) throws {
-        let config = (vm as! UTMQemuVirtualMachine).qemuConfig
+        let config = config as! UTMQemuConfiguration
         if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.networkDeviceType.init(rawValue: hardware) {
         if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.networkDeviceType.init(rawValue: hardware) {
             network.hardware = hardware
             network.hardware = hardware
         }
         }
@@ -439,7 +463,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateQemuSerials(from records: [[AnyHashable : Any]]) throws {
     private func updateQemuSerials(from records: [[AnyHashable : Any]]) throws {
-        let config = (vm as! UTMQemuVirtualMachine).qemuConfig
+        let config = config as! UTMQemuConfiguration
         try updateElements(&config.serials, with: records, onExisting: updateQemuExistingSerial, onNew: { record in
         try updateElements(&config.serials, with: records, onExisting: updateQemuExistingSerial, onNew: { record in
             guard var newSerial = UTMQemuConfigurationSerial(forArchitecture: config.system.architecture, target: config.system.target) else {
             guard var newSerial = UTMQemuConfigurationSerial(forArchitecture: config.system.architecture, target: config.system.target) else {
                 throw ConfigurationError.deviceNotSupported
                 throw ConfigurationError.deviceNotSupported
@@ -461,7 +485,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateQemuExistingSerial(_ serial: inout UTMQemuConfigurationSerial, from record: [AnyHashable : Any]) throws {
     private func updateQemuExistingSerial(_ serial: inout UTMQemuConfigurationSerial, from record: [AnyHashable : Any]) throws {
-        let config = (vm as! UTMQemuVirtualMachine).qemuConfig
+        let config = config as! UTMQemuConfiguration
         if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.serialDeviceType.init(rawValue: hardware) {
         if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.serialDeviceType.init(rawValue: hardware) {
             serial.hardware = hardware
             serial.hardware = hardware
         }
         }
@@ -474,7 +498,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateAppleConfiguration(from record: [AnyHashable : Any]) throws {
     private func updateAppleConfiguration(from record: [AnyHashable : Any]) throws {
-        let config = (vm as! UTMAppleVirtualMachine).appleConfig
+        let config = config as! UTMAppleConfiguration
         if let name = record["name"] as? String, !name.isEmpty {
         if let name = record["name"] as? String, !name.isEmpty {
             config.information.name = name
             config.information.name = name
         }
         }
@@ -502,7 +526,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateAppleDirectoryShares(from records: [[AnyHashable : Any]]) throws {
     private func updateAppleDirectoryShares(from records: [[AnyHashable : Any]]) throws {
-        let config = (vm as! UTMAppleVirtualMachine).appleConfig
+        let config = config as! UTMAppleConfiguration
         try updateElements(&config.sharedDirectories, with: records, onExisting: updateAppleExistingDirectoryShare, onNew: { record in
         try updateElements(&config.sharedDirectories, with: records, onExisting: updateAppleExistingDirectoryShare, onNew: { record in
             var newShare = UTMAppleConfigurationSharedDirectory(directoryURL: nil, isReadOnly: false)
             var newShare = UTMAppleConfigurationSharedDirectory(directoryURL: nil, isReadOnly: false)
             try updateAppleExistingDirectoryShare(&newShare, from: record)
             try updateAppleExistingDirectoryShare(&newShare, from: record)
@@ -517,7 +541,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateAppleDrives(from records: [[AnyHashable : Any]]) throws {
     private func updateAppleDrives(from records: [[AnyHashable : Any]]) throws {
-        let config = (vm as! UTMAppleVirtualMachine).appleConfig
+        let config = config as! UTMAppleConfiguration
         try updateIdentifiedElements(&config.drives, with: records, onExisting: { _, _  in }, onNew: unserializeAppleNewDrive)
         try updateIdentifiedElements(&config.drives, with: records, onExisting: { _, _  in }, onNew: unserializeAppleNewDrive)
     }
     }
     
     
@@ -533,7 +557,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateAppleNetworks(from records: [[AnyHashable : Any]]) throws {
     private func updateAppleNetworks(from records: [[AnyHashable : Any]]) throws {
-        let config = (vm as! UTMAppleVirtualMachine).appleConfig
+        let config = config as! UTMAppleConfiguration
         try updateElements(&config.networks, with: records, onExisting: updateAppleExistingNetwork, onNew: { record in
         try updateElements(&config.networks, with: records, onExisting: updateAppleExistingNetwork, onNew: { record in
             var newNetwork = UTMAppleConfigurationNetwork()
             var newNetwork = UTMAppleConfigurationNetwork()
             try updateAppleExistingNetwork(&newNetwork, from: record)
             try updateAppleExistingNetwork(&newNetwork, from: record)
@@ -565,7 +589,7 @@ extension UTMScriptingVirtualMachineImpl {
     }
     }
     
     
     private func updateAppleSerials(from records: [[AnyHashable : Any]]) throws {
     private func updateAppleSerials(from records: [[AnyHashable : Any]]) throws {
-        let config = (vm as! UTMAppleVirtualMachine).appleConfig
+        let config = config as! UTMAppleConfiguration
         try updateElements(&config.serials, with: records, onExisting: updateAppleExistingSerial, onNew: { record in
         try updateElements(&config.serials, with: records, onExisting: updateAppleExistingSerial, onNew: { record in
             var newSerial = UTMAppleConfigurationSerial()
             var newSerial = UTMAppleConfigurationSerial()
             try updateAppleExistingSerial(&newSerial, from: record)
             try updateAppleExistingSerial(&newSerial, from: record)

+ 4 - 0
UTM.xcodeproj/project.pbxproj

@@ -583,6 +583,7 @@
 		CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */; };
 		CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */; };
 		CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124A29BFE273000790AB /* UTMScriptable.swift */; };
 		CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124A29BFE273000790AB /* UTMScriptable.swift */; };
 		CE25124D29C55816000790AB /* UTMScriptingConfigImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124C29C55816000790AB /* UTMScriptingConfigImpl.swift */; };
 		CE25124D29C55816000790AB /* UTMScriptingConfigImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124C29C55816000790AB /* UTMScriptingConfigImpl.swift */; };
+		CE25124F29C7E379000790AB /* UTMScriptingAppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124E29C7E379000790AB /* UTMScriptingAppDelegate.swift */; };
 		CE2D926A24AD46670059923A /* VMDisplayMetalViewController+Pointer.h in Sources */ = {isa = PBXBuildFile; fileRef = 83FBDD53242FA71900D2C5D7 /* VMDisplayMetalViewController+Pointer.h */; };
 		CE2D926A24AD46670059923A /* VMDisplayMetalViewController+Pointer.h in Sources */ = {isa = PBXBuildFile; fileRef = 83FBDD53242FA71900D2C5D7 /* VMDisplayMetalViewController+Pointer.h */; };
 		CE2D926B24AD46670059923A /* qapi-types-rocker.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C14D23FCEC09001177D6 /* qapi-types-rocker.c */; };
 		CE2D926B24AD46670059923A /* qapi-types-rocker.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C14D23FCEC09001177D6 /* qapi-types-rocker.c */; };
 		CE2D926E24AD46670059923A /* qapi-commands-crypto.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0AE23FCEC01001177D6 /* qapi-commands-crypto.c */; };
 		CE2D926E24AD46670059923A /* qapi-commands-crypto.c in Sources */ = {isa = PBXBuildFile; fileRef = CE23C0AE23FCEC01001177D6 /* qapi-commands-crypto.c */; };
@@ -2103,6 +2104,7 @@
 		CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestFileImpl.swift; sourceTree = "<group>"; };
 		CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestFileImpl.swift; sourceTree = "<group>"; };
 		CE25124A29BFE273000790AB /* UTMScriptable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptable.swift; sourceTree = "<group>"; };
 		CE25124A29BFE273000790AB /* UTMScriptable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptable.swift; sourceTree = "<group>"; };
 		CE25124C29C55816000790AB /* UTMScriptingConfigImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingConfigImpl.swift; sourceTree = "<group>"; };
 		CE25124C29C55816000790AB /* UTMScriptingConfigImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingConfigImpl.swift; sourceTree = "<group>"; };
+		CE25124E29C7E379000790AB /* UTMScriptingAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingAppDelegate.swift; sourceTree = "<group>"; };
 		CE258ACC22715F8300E5A333 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
 		CE258ACC22715F8300E5A333 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
 		CE2B89332262A21E00C6D9D8 /* UTMVirtualMachine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMVirtualMachine.h; sourceTree = "<group>"; };
 		CE2B89332262A21E00C6D9D8 /* UTMVirtualMachine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMVirtualMachine.h; sourceTree = "<group>"; };
 		CE2B89352262B2F600C6D9D8 /* UTMVirtualMachineDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMVirtualMachineDelegate.h; sourceTree = "<group>"; };
 		CE2B89352262B2F600C6D9D8 /* UTMVirtualMachineDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMVirtualMachineDelegate.h; sourceTree = "<group>"; };
@@ -3457,6 +3459,7 @@
 				CEFE98DE29485237007CB7A8 /* UTM.sdef */,
 				CEFE98DE29485237007CB7A8 /* UTM.sdef */,
 				CE25124A29BFE273000790AB /* UTMScriptable.swift */,
 				CE25124A29BFE273000790AB /* UTMScriptable.swift */,
 				CEC794BB2949663C00121A9F /* UTMScripting.swift */,
 				CEC794BB2949663C00121A9F /* UTMScripting.swift */,
+				CE25124E29C7E379000790AB /* UTMScriptingAppDelegate.swift */,
 				CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */,
 				CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */,
 				CEC794B9294924E300121A9F /* UTMScriptingSerialPortImpl.swift */,
 				CEC794B9294924E300121A9F /* UTMScriptingSerialPortImpl.swift */,
 				CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */,
 				CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */,
@@ -4113,6 +4116,7 @@
 				CE0B6D3724AD57FD00FE012D /* qapi-events-block-core.c in Sources */,
 				CE0B6D3724AD57FD00FE012D /* qapi-events-block-core.c in Sources */,
 				848A98B0286A0F74006F0550 /* UTMAppleConfiguration.swift in Sources */,
 				848A98B0286A0F74006F0550 /* UTMAppleConfiguration.swift in Sources */,
 				CE0B6D5E24AD584D00FE012D /* qapi-visit-authz.c in Sources */,
 				CE0B6D5E24AD584D00FE012D /* qapi-visit-authz.c in Sources */,
+				CE25124F29C7E379000790AB /* UTMScriptingAppDelegate.swift in Sources */,
 				CE0B6D4624AD584C00FE012D /* qapi-events-rdma.c in Sources */,
 				CE0B6D4624AD584C00FE012D /* qapi-events-rdma.c in Sources */,
 				2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */,
 				2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */,
 				CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */,
 				CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */,

+ 1 - 1
utmctl/UTMCtl.swift

@@ -49,7 +49,7 @@ extension UTMAPICommand {
             if let windows = utmApp.windows!() as? [UTMScriptingWindow] {
             if let windows = utmApp.windows!() as? [UTMScriptingWindow] {
                 for window in windows {
                 for window in windows {
                     if window.name == "UTM" {
                     if window.name == "UTM" {
-                        window.close!()
+                        window.closeSaving!(.no, savingIn: nil)
                         break
                         break
                     }
                     }
                 }
                 }