2022-12-14 09:16:07 -08:00
//
// C o p y r i g h t © 2 0 2 2 o s y . A l l r i g h t s r e s e r v e d .
//
// L i c e n s e d u n d e r t h e A p a c h e L i c e n s e , V e r s i o n 2 . 0 ( t h e " L i c e n s e " ) ;
// y o u m a y n o t u s e t h i s f i l e e x c e p t i n c o m p l i a n c e w i t h t h e L i c e n s e .
// Y o u m a y o b t a i n a c o p y o f t h e L i c e n s e a t
//
// h t t p : / / w w w . a p a c h e . o r g / l i c e n s e s / L I C E N S E - 2 . 0
//
// U n l e s s r e q u i r e d b y a p p l i c a b l e l a w o r a g r e e d t o i n w r i t i n g , s o f t w a r e
// d i s t r i b u t e d u n d e r t h e L i c e n s e i s d i s t r i b u t e d o n a n " A S I S " B A S I S ,
// W I T H O U T W A R R A N T I E S O R C O N D I T I O N S O F A N Y K I N D , e i t h e r e x p r e s s o r i m p l i e d .
// S e e t h e L i c e n s e f o r t h e s p e c i f i c l a n g u a g e g o v e r n i n g p e r m i s s i o n s a n d
// l i m i t a t i o n s u n d e r t h e L i c e n s e .
//
import Foundation
2023-06-24 20:50:05 -05:00
import QEMUKitInternal
2022-12-14 09:16:07 -08:00
@ MainActor
2022-12-14 18:44:15 -08:00
@objc ( UTMScriptingVirtualMachineImpl )
2023-03-14 01:39:20 -07:00
class UTMScriptingVirtualMachineImpl : NSObject , UTMScriptable {
2023-06-26 23:28:18 -05:00
@ nonobjc var box : VMData
2023-03-19 16:21:51 -07:00
@ nonobjc var data : UTMData
2023-06-28 19:45:05 -05:00
@ nonobjc var vm : ( any UTMVirtualMachine ) ! {
2023-06-26 23:28:18 -05:00
box . wrapped
}
2022-12-14 09:16:07 -08:00
2022-12-14 18:44:15 -08:00
@objc var id : String {
2022-12-14 09:16:07 -08:00
vm . id . uuidString
}
2022-12-14 18:44:15 -08:00
@objc var name : String {
2023-06-28 19:45:05 -05:00
box . detailsTitleLabel
2022-12-14 09:16:07 -08:00
}
2022-12-14 18:44:15 -08:00
@objc var notes : String {
2023-06-28 19:45:05 -05:00
box . detailsNotes ? ? " "
2022-12-14 09:16:07 -08:00
}
2022-12-14 18:44:15 -08:00
@objc var machine : String {
2023-06-28 19:45:05 -05:00
box . detailsSystemTargetLabel
2022-12-14 09:16:07 -08:00
}
2022-12-14 18:44:15 -08:00
@objc var architecture : String {
2023-06-28 19:45:05 -05:00
box . detailsSystemArchitectureLabel
2022-12-14 09:16:07 -08:00
}
2022-12-14 18:44:15 -08:00
@objc var memory : String {
2023-06-28 19:45:05 -05:00
box . detailsSystemMemoryLabel
2022-12-14 09:16:07 -08:00
}
2022-12-14 18:44:15 -08:00
@objc var backend : UTMScriptingBackend {
2022-12-14 09:16:07 -08:00
if vm is UTMQemuVirtualMachine {
return . qemu
} else if vm is UTMAppleVirtualMachine {
return . apple
} else {
return . unavailable
}
}
2022-12-14 18:44:15 -08:00
@objc var status : UTMScriptingStatus {
switch vm . state {
2023-06-28 19:45:05 -05:00
case . stopped : return . stopped
case . starting : return . starting
case . started : return . started
case . pausing : return . pausing
case . paused : return . paused
case . resuming : return . resuming
case . stopping : return . stopping
case . saving : return . pausing // FIXME: n e w e n t r i e s
case . restoring : return . resuming // FIXME: n e w e n t r i e s
2022-12-14 18:44:15 -08:00
}
}
@objc var serialPorts : [ UTMScriptingSerialPortImpl ] {
2023-06-28 19:45:05 -05:00
if let config = vm . config as ? UTMQemuConfiguration {
2022-12-14 09:16:07 -08:00
return config . serials . indices . map ( { UTMScriptingSerialPortImpl ( qemuSerial : config . serials [ $0 ] , parent : self , index : $0 ) } )
2023-06-28 19:45:05 -05:00
} else if let config = vm . config as ? UTMAppleConfiguration {
2022-12-14 09:16:07 -08:00
return config . serials . indices . map ( { UTMScriptingSerialPortImpl ( appleSerial : config . serials [ $0 ] , parent : self , index : $0 ) } )
} else {
return [ ]
}
}
2023-06-24 20:50:05 -05:00
var guestAgent : QEMUGuestAgent ! {
get async {
await ( vm as ? UTMQemuVirtualMachine ) ? . guestAgent
}
2023-03-14 01:39:20 -07:00
}
2025-01-18 16:55:32 -07:00
var qemuProcess : UTMQemuSystem ? {
get async {
await ( vm as ? UTMQemuVirtualMachine ) ? . system
}
}
2022-12-14 09:16:07 -08:00
override var objectSpecifier : NSScriptObjectSpecifier ? {
2022-12-14 18:44:15 -08:00
let appDescription = NSApplication . classDescription ( ) as ! NSScriptClassDescription
2022-12-14 09:16:07 -08:00
return NSUniqueIDSpecifier ( containerClassDescription : appDescription ,
containerSpecifier : nil ,
key : " scriptingVirtualMachines " ,
uniqueID : id )
}
2023-06-26 23:28:18 -05:00
init ( for vm : VMData , data : UTMData ) {
self . box = vm
2022-12-14 09:16:07 -08:00
self . data = data
}
2022-12-14 22:13:22 -08:00
@objc func start ( _ command : NSScriptCommand ) {
2022-12-16 13:56:56 -08:00
let shouldSaveState = command . evaluatedArguments ? [ " saveFlag " ] as ? Bool ? ? true
2025-04-16 12:16:49 +02:00
let bootRecoveryMode = command . evaluatedArguments ? [ " bootRecoveryFlag " ] as ? Bool ? ? false
2022-12-14 09:16:07 -08:00
withScriptCommand ( command ) { [ self ] in
2025-04-16 12:16:49 +02:00
var options : UTMVirtualMachineStartOptions = [ ]
2022-12-16 13:56:56 -08:00
if ! shouldSaveState {
2023-06-28 19:45:05 -05:00
guard type ( of : vm ) . capabilities . supportsDisposibleMode else {
2022-12-16 13:56:56 -08:00
throw ScriptingError . operationNotSupported
}
2025-04-16 12:16:49 +02:00
options . insert ( . bootDisposibleMode )
}
if bootRecoveryMode {
guard type ( of : vm ) . capabilities . supportsRecoveryMode else {
throw ScriptingError . operationNotSupported
}
options . insert ( . bootRecovery )
2022-12-16 13:56:56 -08:00
}
2025-04-16 12:16:49 +02:00
2023-06-26 23:28:18 -05:00
data . run ( vm : box , startImmediately : false )
2023-06-28 19:45:05 -05:00
if vm . state = = . stopped {
2025-04-16 12:16:49 +02:00
try await vm . start ( options : options )
2023-06-28 19:45:05 -05:00
} else if vm . state = = . paused {
try await vm . resume ( )
2022-12-14 09:16:07 -08:00
} else {
throw ScriptingError . operationNotAvailable
}
}
}
2022-12-14 22:13:22 -08:00
@objc func suspend ( _ command : NSScriptCommand ) {
2022-12-16 13:56:56 -08:00
let shouldSaveState = command . evaluatedArguments ? [ " saveFlag " ] as ? Bool ? ? false
2022-12-14 09:16:07 -08:00
withScriptCommand ( command ) { [ self ] in
2023-06-28 19:45:05 -05:00
guard vm . state = = . started else {
2023-01-03 18:32:08 -08:00
throw ScriptingError . notRunning
}
2023-06-28 19:45:05 -05:00
try await vm . pause ( )
if shouldSaveState {
try await vm . saveSnapshot ( name : nil )
}
2022-12-14 09:16:07 -08:00
}
}
2022-12-14 22:13:22 -08:00
@objc func stop ( _ command : NSScriptCommand ) {
2022-12-27 16:36:00 -08:00
let stopMethod : UTMScriptingStopMethod
if let stopMethodValue = command . evaluatedArguments ? [ " stopBy " ] as ? AEKeyword {
stopMethod = UTMScriptingStopMethod ( rawValue : stopMethodValue ) ? ? . force
} else {
stopMethod = . force
}
2022-12-14 09:16:07 -08:00
withScriptCommand ( command ) { [ self ] in
2023-06-28 19:45:05 -05:00
guard vm . state = = . started || stopMethod = = . kill else {
2023-01-03 18:32:08 -08:00
throw ScriptingError . notRunning
}
2022-12-14 09:16:07 -08:00
switch stopMethod {
case . force :
2023-06-28 19:45:05 -05:00
try await vm . stop ( usingMethod : . force )
2022-12-14 09:16:07 -08:00
case . kill :
2023-06-28 19:45:05 -05:00
try await vm . stop ( usingMethod : . kill )
2022-12-14 09:16:07 -08:00
case . request :
2023-06-28 19:45:05 -05:00
try await vm . stop ( usingMethod : . request )
2022-12-14 09:16:07 -08:00
}
}
}
2023-03-19 20:37:39 -07:00
@objc func delete ( _ command : NSDeleteCommand ) {
withScriptCommand ( command ) { [ self ] in
2023-06-28 19:45:05 -05:00
guard vm . state = = . stopped else {
2023-03-19 20:37:39 -07:00
throw ScriptingError . notStopped
}
2023-06-26 23:28:18 -05:00
try await data . delete ( vm : box , alsoRegistry : true )
2023-03-19 20:37:39 -07:00
}
}
@objc func clone ( _ command : NSCloneCommand ) {
let properties = command . evaluatedArguments ? [ " WithProperties " ] as ? [ AnyHashable : Any ]
withScriptCommand ( command ) { [ self ] in
2023-06-28 19:45:05 -05:00
guard vm . state = = . stopped else {
2023-03-19 20:37:39 -07:00
throw ScriptingError . notStopped
}
2023-06-26 23:28:18 -05:00
let newVM = try await data . clone ( vm : box )
2023-03-19 20:37:39 -07:00
if let properties = properties , let newConfiguration = properties [ " configuration " ] as ? [ AnyHashable : Any ] {
2023-06-26 23:28:18 -05:00
let wrapper = UTMScriptingConfigImpl ( newVM . config ! )
2023-03-19 20:37:39 -07:00
try wrapper . updateConfiguration ( from : newConfiguration )
try await data . save ( vm : newVM )
}
}
}
2024-10-04 01:33:50 -06:00
@objc func export ( _ command : NSCloneCommand ) {
let exportUrl = command . evaluatedArguments ? [ " file " ] as ? URL
withScriptCommand ( command ) { [ self ] in
guard vm . state = = . stopped else {
throw ScriptingError . notStopped
}
try await data . export ( vm : box , to : exportUrl ! )
}
}
2022-12-14 09:16:07 -08:00
}
2023-03-14 01:39:20 -07:00
// MARK: - G u e s t a g e n t s u i t e
@objc extension UTMScriptingVirtualMachineImpl {
2023-06-24 20:50:05 -05:00
@ nonobjc private func withGuestAgent < Result > ( _ block : ( QEMUGuestAgent ) async throws -> Result ) async throws -> Result {
2023-06-28 19:45:05 -05:00
guard vm . state = = . started else {
2023-03-14 01:39:20 -07:00
throw ScriptingError . notRunning
}
guard let vm = vm as ? UTMQemuVirtualMachine else {
throw ScriptingError . operationNotSupported
}
2023-06-24 20:50:05 -05:00
guard let guestAgent = await vm . guestAgent else {
2023-03-14 01:39:20 -07:00
throw ScriptingError . guestAgentNotRunning
}
return try await block ( guestAgent )
}
@objc func valueInOpenFilesWithUniqueID ( _ id : Int ) -> UTMScriptingGuestFileImpl {
UTMScriptingGuestFileImpl ( from : id , parent : self )
}
@objc func openFile ( _ command : NSScriptCommand ) {
let path = command . evaluatedArguments ? [ " path " ] as ? String
let mode = command . evaluatedArguments ? [ " mode " ] as ? AEKeyword
let isUpdate = command . evaluatedArguments ? [ " isUpdate " ] as ? Bool ? ? false
withScriptCommand ( command ) { [ self ] in
guard let path = path else {
throw ScriptingError . invalidParameter
}
let modeValue : String
if let mode = mode {
switch UTMScriptingOpenMode ( rawValue : mode ) {
case . reading : modeValue = " r "
case . writing : modeValue = " w "
case . appending : modeValue = " a "
default : modeValue = " r "
}
} else {
modeValue = " r "
}
return try await withGuestAgent { guestAgent in
let handle = try await guestAgent . guestFileOpen ( path , mode : modeValue + ( isUpdate ? " + " : " " ) )
return UTMScriptingGuestFileImpl ( from : handle , parent : self )
}
}
}
@objc func valueInProcessesWithUniqueID ( _ id : Int ) -> UTMScriptingGuestProcessImpl {
UTMScriptingGuestProcessImpl ( from : id , parent : self )
}
@objc func execute ( _ command : NSScriptCommand ) {
let path = command . evaluatedArguments ? [ " path " ] as ? String
let argv = command . evaluatedArguments ? [ " argv " ] as ? [ String ]
let envp = command . evaluatedArguments ? [ " envp " ] as ? [ String ]
let input = command . evaluatedArguments ? [ " input " ] as ? String
let isBase64Encoded = command . evaluatedArguments ? [ " isBase64Encoded " ] as ? Bool ? ? false
let isCaptureOutput = command . evaluatedArguments ? [ " isCaptureOutput " ] as ? Bool ? ? false
let inputData = dataFromText ( input , isBase64Encoded : isBase64Encoded )
withScriptCommand ( command ) { [ self ] in
guard let path = path else {
throw ScriptingError . invalidParameter
}
return try await withGuestAgent { guestAgent in
let pid = try await guestAgent . guestExec ( path , argv : argv , envp : envp , input : inputData , captureOutput : isCaptureOutput )
return UTMScriptingGuestProcessImpl ( from : pid , parent : self )
}
}
}
@objc func queryIp ( _ command : NSScriptCommand ) {
withScriptCommand ( command ) { [ self ] in
try await withGuestAgent { guestAgent in
let interfaces = try await guestAgent . guestNetworkGetInterfaces ( )
var ipv4 : [ String ] = [ ]
var ipv6 : [ String ] = [ ]
for interface in interfaces {
for ip in interface . ipAddresses {
if ip . isIpV6Address {
if ip . ipAddress != " ::1 " && ip . ipAddress != " 0:0:0:0:0:0:0:1 " {
ipv6 . append ( ip . ipAddress )
}
} else {
if ip . ipAddress != " 127.0.0.1 " {
ipv4 . append ( ip . ipAddress )
}
}
}
}
return ipv4 + ipv6
}
}
}
}
// MARK: - E r r o r s
2022-12-14 09:16:07 -08:00
extension UTMScriptingVirtualMachineImpl {
enum ScriptingError : Error , LocalizedError {
case operationNotAvailable
2022-12-16 13:56:56 -08:00
case operationNotSupported
2023-01-03 18:32:08 -08:00
case notRunning
2023-03-19 16:21:51 -07:00
case notStopped
2023-03-14 01:39:20 -07:00
case guestAgentNotRunning
case invalidParameter
2022-12-14 09:16:07 -08:00
2023-01-03 18:29:00 -08:00
var errorDescription : String ? {
2022-12-14 09:16:07 -08:00
switch self {
2022-12-15 18:17:24 -08:00
case . operationNotAvailable : return NSLocalizedString ( " Operation not available. " , comment : " UTMScriptingVirtualMachineImpl " )
2022-12-16 13:56:56 -08:00
case . operationNotSupported : return NSLocalizedString ( " Operation not supported by the backend. " , comment : " UTMScriptingVirtualMachineImpl " )
2023-01-03 18:32:08 -08:00
case . notRunning : return NSLocalizedString ( " The virtual machine is not running. " , comment : " UTMScriptingVirtualMachineImpl " )
2023-03-19 16:21:51 -07:00
case . notStopped : return NSLocalizedString ( " The virtual machine must be stopped before this operation can be performed. " , comment : " UTMScriptingVirtualMachineImpl " )
2023-03-14 01:39:20 -07:00
case . guestAgentNotRunning : return NSLocalizedString ( " The QEMU guest agent is not running or not installed on the guest. " , comment : " UTMScriptingVirtualMachineImpl " )
case . invalidParameter : return NSLocalizedString ( " One or more required parameters are missing or invalid. " , comment : " UTMScriptingVirtualMachineImpl " )
2022-12-14 09:16:07 -08:00
}
}
}
}