Forráskód Böngészése

manager: factor out QEMUKit as a separate project

osy 2 éve
szülő
commit
03d200422f
34 módosított fájl, 764 hozzáadás és 1049 törlés
  1. 2 1
      Configuration/UTMConfigurationDrive.swift
  2. 0 7
      Managers/UTMLogging.h
  3. 2 120
      Managers/UTMLogging.m
  4. 4 4
      Managers/UTMQemu.h
  5. 35 15
      Managers/UTMQemu.m
  6. 53 25
      Managers/UTMQemuImage.swift
  7. 80 0
      Managers/UTMQemuPort.swift
  8. 3 2
      Managers/UTMQemuSystem.h
  9. 6 1
      Managers/UTMQemuSystem.m
  10. 431 10
      Managers/UTMQemuVirtualMachine.swift
  11. 11 26
      Managers/UTMRegistryEntry.swift
  12. 4 8
      Managers/UTMSpiceIO.h
  13. 19 62
      Managers/UTMSpiceIO.m
  14. 0 3
      Managers/UTMVirtualMachine-Private.h
  15. 3 0
      Managers/UTMVirtualMachine-Protected.h
  16. 1 1
      Managers/UTMVirtualMachine.h
  17. 2 4
      Managers/UTMVirtualMachine.m
  18. 1 7
      Platform/Swift-Bridging-Header.h
  19. 0 2
      Platform/iOS/Display/VMDisplayMetalViewController+Pointer.m
  20. 0 2
      Platform/iOS/Display/VMDisplayMetalViewController+Touch.m
  21. 1 1
      Platform/iOS/VMSessionState.swift
  22. 1 1
      Platform/macOS/Display/VMDisplayQemuDisplayController.swift
  23. 4 2
      Platform/macOS/Display/VMDisplayWindowController.swift
  24. 4 2
      Platform/macOS/VMHeadlessSessionState.swift
  25. 2 0
      QEMUHelper/QEMUHelper.h
  26. 10 8
      QEMUHelper/QEMUHelper.m
  27. 27 0
      QEMUHelper/QEMUHelperDelegate.h
  28. 1 1
      QEMUHelper/QEMUHelperProtocol.h
  29. 5 0
      QEMUHelper/main.m
  30. 15 16
      Scripting/UTMScriptingGuestFileImpl.swift
  31. 1 3
      Scripting/UTMScriptingGuestProcessImpl.swift
  32. 7 4
      Scripting/UTMScriptingVirtualMachineImpl.swift
  33. 20 711
      UTM.xcodeproj/project.pbxproj
  34. 9 0
      UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

+ 2 - 1
Configuration/UTMConfigurationDrive.swift

@@ -15,6 +15,7 @@
 //
 //
 
 
 import Foundation
 import Foundation
+import QEMUKitInternal
 
 
 /// Settings for single disk device
 /// Settings for single disk device
 protocol UTMConfigurationDrive: Codable, Hashable, Identifiable {
 protocol UTMConfigurationDrive: Codable, Hashable, Identifiable {
@@ -104,7 +105,7 @@ extension UTMConfigurationDrive {
     
     
     private func createQcow2Image(at newURL: URL, size sizeMib: Int) async throws {
     private func createQcow2Image(at newURL: URL, size sizeMib: Int) async throws {
         try await Task.detached {
         try await Task.detached {
-            if !GenerateDefaultQcow2File(newURL as CFURL, sizeMib) {
+            if !QEMUGenerateDefaultQcow2File(newURL as CFURL, sizeMib) {
                 throw UTMConfigurationError.cannotCreateDiskImage
                 throw UTMConfigurationError.cannotCreateDiskImage
             }
             }
         }.value
         }.value

+ 0 - 7
Managers/UTMLogging.h

@@ -15,7 +15,6 @@
 //
 //
 
 
 #import <Foundation/Foundation.h>
 #import <Foundation/Foundation.h>
-#import "UTMLoggingDelegate.h"
 
 
 NS_ASSUME_NONNULL_BEGIN
 NS_ASSUME_NONNULL_BEGIN
 
 
@@ -23,14 +22,8 @@ void UTMLog(NSString *format, ...) NS_FORMAT_FUNCTION(1,2) NS_NO_TAIL_CALL;
 
 
 @interface UTMLogging : NSObject
 @interface UTMLogging : NSObject
 
 
-@property (nonatomic, readonly) NSPipe *standardOutput;
-@property (nonatomic, readonly) NSPipe *standardError;
-@property (nonatomic, weak) id<UTMLoggingDelegate> delegate;
-
 + (UTMLogging *)sharedInstance;
 + (UTMLogging *)sharedInstance;
 
 
-- (void)logToFile:(NSURL *)path;
-- (void)endLog;
 - (void)writeLine:(NSString *)line;
 - (void)writeLine:(NSString *)line;
 
 
 @end
 @end

+ 2 - 120
Managers/UTMLogging.m

@@ -14,11 +14,8 @@
 // limitations under the License.
 // limitations under the License.
 //
 //
 
 
-#import <pthread.h>
-#import <stdio.h>
-#import <TargetConditionals.h>
-#import <unistd.h>
 #import "UTMLogging.h"
 #import "UTMLogging.h"
+@import QEMUKitInternal;
 
 
 static UTMLogging *gLoggingInstance;
 static UTMLogging *gLoggingInstance;
 
 
@@ -31,16 +28,6 @@ void UTMLog(NSString *format, ...) {
     NSLog(@"%@", line);
     NSLog(@"%@", line);
 }
 }
 
 
-@interface UTMLogging ()
-
-@property (nonatomic, readwrite) NSPipe *standardOutput;
-@property (nonatomic, readwrite) NSPipe *standardError;
-@property (nonatomic, nullable) NSOutputStream *fileOutputStream;
-@property (nonatomic, nullable) NSFileHandle *originalStdoutWrite;
-@property (nonatomic, nullable) NSFileHandle *originalStderrWrite;
-
-@end
-
 @implementation UTMLogging
 @implementation UTMLogging
 
 
 + (void)initialize {
 + (void)initialize {
@@ -48,9 +35,6 @@ void UTMLog(NSString *format, ...) {
     if (!initialized) {
     if (!initialized) {
         initialized = YES;
         initialized = YES;
         gLoggingInstance = [[UTMLogging alloc] init];
         gLoggingInstance = [[UTMLogging alloc] init];
-#if TARGET_OS_IPHONE // not supported on macOS
-        [gLoggingInstance redirectStandardFds];
-#endif
     }
     }
 }
 }
 
 
@@ -58,110 +42,8 @@ void UTMLog(NSString *format, ...) {
     return gLoggingInstance;
     return gLoggingInstance;
 }
 }
 
 
-- (instancetype)init {
-    if (self = [super init]) {
-        __weak typeof(self) _weakSelf = self;
-        self.standardOutput = [NSPipe pipe];
-        __block NSString *outBuffer = [NSString string];
-        self.standardOutput.fileHandleForReading.readabilityHandler = ^(NSFileHandle *handle) {
-            typeof(self) _self = _weakSelf;
-            NSData *data = [handle availableData];
-            @try {
-                [_self.originalStdoutWrite writeData:data];
-            } @catch (NSException *e) {
-                // fd closed on us
-                _self.originalStdoutWrite = nil;
-            }
-            [_self.fileOutputStream write:data.bytes maxLength:data.length];
-            NSArray<NSString *> *lines = [_self parseLinesFromData:data buffer:&outBuffer];
-            for (NSString *line in lines) {
-                [_self.delegate logging:_self didRecieveOutputLine:line];
-            }
-        };
-        self.standardError = [NSPipe pipe];
-        __block NSString *errorBuffer = [NSString string];
-        self.standardError.fileHandleForReading.readabilityHandler = ^(NSFileHandle *handle) {
-            typeof(self) _self = _weakSelf;
-            NSData *data = [handle availableData];
-            @try {
-                [_self.originalStderrWrite writeData:data];
-            } @catch (NSException *e) {
-                // fd closed on us
-                _self.originalStderrWrite = nil;
-            }
-            [_self.fileOutputStream write:data.bytes maxLength:data.length];
-            NSArray<NSString *> *lines = [_self parseLinesFromData:data buffer:&errorBuffer];
-            for (NSString *line in lines) {
-                [_self.delegate logging:_self didRecieveErrorLine:line];
-            }
-        };
-    }
-    return self;
-}
-
-- (BOOL)redirectStandardFds {
-    int real_stdout = -1;
-    int real_stderr = -1;
-    if ((real_stdout = dup(STDOUT_FILENO)) < 0) {
-        perror("dup");
-        goto error;
-    }
-    if ((real_stderr = dup(STDERR_FILENO)) < 0) {
-        perror("dup");
-        goto error;
-    }
-    if (dup2(self.standardOutput.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) < 0) {
-        perror("dup2");
-        goto error;
-    }
-    if (dup2(self.standardError.fileHandleForWriting.fileDescriptor, STDERR_FILENO) < 0) {
-        perror("dup2");
-        goto error;
-    }
-    [self.standardOutput.fileHandleForWriting closeFile];
-    [self.standardError.fileHandleForWriting closeFile];
-    self.originalStdoutWrite = [[NSFileHandle alloc] initWithFileDescriptor:real_stdout closeOnDealloc:YES];
-    self.originalStderrWrite = [[NSFileHandle alloc] initWithFileDescriptor:real_stderr closeOnDealloc:YES];
-    return YES;
-error:
-    close(real_stdout);
-    close(real_stderr);
-    self.originalStdoutWrite = nil;
-    self.originalStderrWrite = nil;
-    return NO;
-}
-
-- (NSArray<NSString *> *)parseLinesFromData:(NSData *)data buffer:(NSString **)buffer {
-    NSString *string = [*buffer stringByAppendingString:[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]];
-    NSArray *lines = [string componentsSeparatedByString:@"\n"];
-    *buffer = [lines lastObject];
-    if (lines.count > 0) {
-        lines = [lines subarrayWithRange:NSMakeRange(0, lines.count - 1)];
-    }
-    return lines;
-}
-
-- (void)logToFile:(NSURL *)path {
-    [self endLog];
-    NSOutputStream *stream = [NSOutputStream outputStreamWithURL:path append:NO];
-    [stream open];
-    __weak typeof(stream) weakStream = stream;
-    atexit_b(^{
-        NSStreamStatus status = weakStream.streamStatus;
-        if (status == NSStreamStatusOpen || status == NSStreamStatusWriting) {
-            [weakStream close];
-        }
-    });
-    self.fileOutputStream = stream;
-}
-
-- (void)endLog {
-    [self.fileOutputStream close];
-    self.fileOutputStream = nil;
-}
-
 - (void)writeLine:(NSString *)line {
 - (void)writeLine:(NSString *)line {
-    [self.fileOutputStream write:(void *)[line cStringUsingEncoding:NSASCIIStringEncoding] maxLength:line.length];
+    [QEMULogging.sharedInstance writeLine:line];
 }
 }
 
 
 @end
 @end

+ 4 - 4
Managers/UTMQemu.h

@@ -16,11 +16,10 @@
 
 
 #import <Foundation/Foundation.h>
 #import <Foundation/Foundation.h>
 #import "QEMUHelperProtocol.h"
 #import "QEMUHelperProtocol.h"
+@import QEMUKitInternal;
 
 
 typedef void * _Nullable (* _Nonnull UTMQemuThreadEntry)(void * _Nullable args);
 typedef void * _Nullable (* _Nonnull UTMQemuThreadEntry)(void * _Nullable args);
 
 
-@class UTMLogging;
-
 NS_ASSUME_NONNULL_BEGIN
 NS_ASSUME_NONNULL_BEGIN
 
 
 @interface UTMQemu : NSObject
 @interface UTMQemu : NSObject
@@ -33,17 +32,18 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic) NSInteger status;
 @property (nonatomic) NSInteger status;
 @property (nonatomic) NSInteger fatal;
 @property (nonatomic) NSInteger fatal;
 @property (nonatomic) UTMQemuThreadEntry entry;
 @property (nonatomic) UTMQemuThreadEntry entry;
-@property (nonatomic, nullable) UTMLogging *logging;
+@property (nonatomic, nullable) QEMULogging *logging;
 
 
 - (instancetype)init;
 - (instancetype)init;
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments NS_DESIGNATED_INITIALIZER;
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments NS_DESIGNATED_INITIALIZER;
 - (void)pushArgv:(nullable NSString *)arg;
 - (void)pushArgv:(nullable NSString *)arg;
 - (void)clearArgv;
 - (void)clearArgv;
-- (void)startQemu:(nonnull NSString *)name completion:(void(^)(BOOL,NSString * _Nullable))completion;
+- (void)startQemu:(nonnull NSString *)name completion:(nonnull void (^)(NSError * _Nullable))completion;
 - (void)stopQemu;
 - (void)stopQemu;
 - (void)accessDataWithBookmark:(NSData *)bookmark;
 - (void)accessDataWithBookmark:(NSData *)bookmark;
 - (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion;
 - (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion;
 - (void)stopAccessingPath:(nullable NSString *)path;
 - (void)stopAccessingPath:(nullable NSString *)path;
+- (void)qemuHasExited:(NSInteger)exitCode message:(nullable NSString *)message;
 
 
 @end
 @end
 
 

+ 35 - 15
Managers/UTMQemu.m

@@ -16,10 +16,13 @@
 
 
 #import "UTMQemu.h"
 #import "UTMQemu.h"
 #import "UTMLogging.h"
 #import "UTMLogging.h"
+#import "QEMUHelperDelegate.h"
 #import <dlfcn.h>
 #import <dlfcn.h>
 #import <pthread.h>
 #import <pthread.h>
 #import <TargetConditionals.h>
 #import <TargetConditionals.h>
 
 
+extern NSString *const kUTMErrorDomain;
+
 @interface UTMQemu ()
 @interface UTMQemu ()
 
 
 @property (nonatomic) dispatch_queue_t completionQueue;
 @property (nonatomic) dispatch_queue_t completionQueue;
@@ -82,6 +85,8 @@
     }
     }
     _connection = [[NSXPCConnection alloc] initWithServiceName:helperIdentifier];
     _connection = [[NSXPCConnection alloc] initWithServiceName:helperIdentifier];
     _connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(QEMUHelperProtocol)];
     _connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(QEMUHelperProtocol)];
+    _connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(QEMUHelperDelegate)];
+    _connection.exportedObject = self;
     [_connection resume];
     [_connection resume];
     return _connection != nil;
     return _connection != nil;
 #endif
 #endif
@@ -114,7 +119,7 @@
     return YES;
     return YES;
 }
 }
 
 
-- (void)startDylibThread:(nonnull NSString *)dylib completion:(void(^)(BOOL,NSString * _Nullable))completion {
+- (void)startDylibThread:(nonnull NSString *)dylib completion:(nonnull void (^)(NSError * _Nullable))completion {
     void *dlctx;
     void *dlctx;
     __block pthread_t qemu_thread;
     __block pthread_t qemu_thread;
     pthread_attr_t qosAttribute;
     pthread_attr_t qosAttribute;
@@ -127,13 +132,13 @@
     dlctx = dlopen([dylib UTF8String], RTLD_LOCAL);
     dlctx = dlopen([dylib UTF8String], RTLD_LOCAL);
     if (dlctx == NULL) {
     if (dlctx == NULL) {
         NSString *err = [NSString stringWithUTF8String:dlerror()];
         NSString *err = [NSString stringWithUTF8String:dlerror()];
-        completion(NO, err);
+        completion([self errorWithMessage:err]);
         return;
         return;
     }
     }
     if (![self didLoadDylib:dlctx]) {
     if (![self didLoadDylib:dlctx]) {
         NSString *err = [NSString stringWithUTF8String:dlerror()];
         NSString *err = [NSString stringWithUTF8String:dlerror()];
         dlclose(dlctx);
         dlclose(dlctx);
-        completion(NO, err);
+        completion([self errorWithMessage:err]);
         return;
         return;
     }
     }
     if (atexit_b(^{
     if (atexit_b(^{
@@ -146,7 +151,7 @@
             pthread_exit(NULL);
             pthread_exit(NULL);
         }
         }
     }) != 0) {
     }) != 0) {
-        completion(NO, NSLocalizedString(@"Internal error has occurred.", @"UTMQemu"));
+        completion([self errorWithMessage:NSLocalizedString(@"Internal error has occurred.", @"UTMQemu")]);
         return;
         return;
     }
     }
     pthread_attr_init(&qosAttribute);
     pthread_attr_init(&qosAttribute);
@@ -155,45 +160,52 @@
     dispatch_async(self.completionQueue, ^{
     dispatch_async(self.completionQueue, ^{
         if (dispatch_semaphore_wait(self.done, DISPATCH_TIME_FOREVER)) {
         if (dispatch_semaphore_wait(self.done, DISPATCH_TIME_FOREVER)) {
             dlclose(dlctx);
             dlclose(dlctx);
-            completion(NO, NSLocalizedString(@"Internal error has occurred.", @"UTMQemu"));
+            [self qemuHasExited:-1 message:NSLocalizedString(@"Internal error has occurred.", @"UTMQemu")];
         } else {
         } else {
             if (dlclose(dlctx) < 0) {
             if (dlclose(dlctx) < 0) {
                 NSString *err = [NSString stringWithUTF8String:dlerror()];
                 NSString *err = [NSString stringWithUTF8String:dlerror()];
-                completion(NO, err);
+                [self qemuHasExited:-1 message:err];
             } else if (self.fatal || self.status) {
             } else if (self.fatal || self.status) {
-                completion(NO, nil);
+                [self qemuHasExited:-1 message:nil];
             } else {
             } else {
-                completion(YES, nil);
+                [self qemuHasExited:0 message:nil];
             }
             }
         }
         }
     });
     });
+    completion(nil);
 }
 }
 
 
-- (void)startQemuRemote:(nonnull NSString *)name completion:(void(^)(BOOL,NSString * _Nullable))completion {
+- (void)startQemuRemote:(nonnull NSString *)name completion:(nonnull void (^)(NSError * _Nullable))completion {
     NSError *error;
     NSError *error;
     NSData *libBookmark = [self.libraryURL bookmarkDataWithOptions:0
     NSData *libBookmark = [self.libraryURL bookmarkDataWithOptions:0
                                     includingResourceValuesForKeys:nil
                                     includingResourceValuesForKeys:nil
                                                      relativeToURL:nil
                                                      relativeToURL:nil
                                                              error:&error];
                                                              error:&error];
     if (!libBookmark) {
     if (!libBookmark) {
-        completion(NO, error.localizedDescription);
+        completion(error);
         return;
         return;
     }
     }
+    __weak typeof(self) _self = self;
     NSFileHandle *standardOutput = self.logging.standardOutput.fileHandleForWriting;
     NSFileHandle *standardOutput = self.logging.standardOutput.fileHandleForWriting;
     NSFileHandle *standardError = self.logging.standardError.fileHandleForWriting;
     NSFileHandle *standardError = self.logging.standardError.fileHandleForWriting;
     [_connection.remoteObjectProxy setEnvironment:self.environment];
     [_connection.remoteObjectProxy setEnvironment:self.environment];
     [[_connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
     [[_connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
         if (error.domain == NSCocoaErrorDomain && error.code == NSXPCConnectionInvalid) {
         if (error.domain == NSCocoaErrorDomain && error.code == NSXPCConnectionInvalid) {
-            completion(YES, nil); // inhibit this error since we always see it on quit
+            // inhibit this error since we always see it on quit
+            [_self qemuHasExited:0 message:nil];
+        } else {
+            [_self qemuHasExited:error.code message:error.localizedDescription];
+        }
+    }] startQemu:name standardOutput:standardOutput standardError:standardError libraryBookmark:libBookmark argv:self.argv completion:^(BOOL success, NSString *msg){
+        if (!success) {
+            completion([self errorWithMessage:msg]);
         } else {
         } else {
-            completion(NO, error.localizedDescription);
+            completion(nil);
         }
         }
-    }] startQemu:name standardOutput:standardOutput standardError:standardError libraryBookmark:libBookmark argv:self.argv onExit:^(BOOL success, NSString *msg){
-        completion(success, msg);
     }];
     }];
 }
 }
 
 
-- (void)startQemu:(nonnull NSString *)name completion:(void(^)(BOOL,NSString * _Nullable))completion {
+- (void)startQemu:(nonnull NSString *)name completion:(nonnull void (^)(NSError * _Nullable))completion {
     [self printArgv];
     [self printArgv];
 #if TARGET_OS_IPHONE
 #if TARGET_OS_IPHONE
     NSString *base = @"";
     NSString *base = @"";
@@ -288,4 +300,12 @@
     }
     }
 }
 }
 
 
+- (NSError *)errorWithMessage:(nullable NSString *)message {
+    return [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: message}];
+}
+
+- (void)qemuHasExited:(NSInteger)exitCode message:(nullable NSString *)message {
+    UTMLog(@"QEMU has exited with code %ld and message %@", exitCode, message);
+}
+
 @end
 @end

+ 53 - 25
Managers/UTMQemuImage.swift

@@ -15,14 +15,43 @@
 //
 //
 
 
 import Foundation
 import Foundation
+import QEMUKitInternal
 
 
 @objc class UTMQemuImage: UTMQemu {
 @objc class UTMQemuImage: UTMQemu {
     private var logOutput: String = ""
     private var logOutput: String = ""
+    private var processExitContinuation: CheckedContinuation<Void, any Error>?
     
     
     private init() {
     private init() {
         super.init(arguments: [])
         super.init(arguments: [])
     }
     }
     
     
+    override func qemuHasExited(_ exitCode: Int, message: String?) {
+        if let processExitContinuation = processExitContinuation {
+            self.processExitContinuation = nil
+            if exitCode != 0 {
+                if let message = message {
+                    processExitContinuation.resume(throwing: UTMQemuImageError.qemuError(message))
+                } else {
+                    processExitContinuation.resume(throwing: UTMQemuImageError.unknown)
+                }
+            } else {
+                processExitContinuation.resume()
+            }
+        }
+    }
+    
+    private func start() async throws {
+        try await withCheckedThrowingContinuation { continuation in
+            processExitContinuation = continuation
+            start("qemu-img") { error in
+                if let error = error {
+                    self.processExitContinuation = nil
+                    continuation.resume(throwing: error)
+                }
+            }
+        }
+    }
+    
     static func convert(from url: URL, toQcow2 dest: URL, withCompression compressed: Bool = false) async throws {
     static func convert(from url: URL, toQcow2 dest: URL, withCompression compressed: Bool = false) async throws {
         let qemuImg = UTMQemuImage()
         let qemuImg = UTMQemuImage()
         let srcBookmark = try url.bookmarkData()
         let srcBookmark = try url.bookmarkData()
@@ -39,14 +68,9 @@ import Foundation
         qemuImg.pushArgv(url.path)
         qemuImg.pushArgv(url.path)
         qemuImg.accessData(withBookmark: dstBookmark)
         qemuImg.accessData(withBookmark: dstBookmark)
         qemuImg.pushArgv(dest.path)
         qemuImg.pushArgv(dest.path)
-        let logging = UTMLogging()
+        let logging = QEMULogging()
         qemuImg.logging = logging
         qemuImg.logging = logging
-        try await Task.detached {
-            let (success, message) = await qemuImg.start("qemu-img")
-            if !success, let message = message {
-                throw message
-            }
-        }.value
+        try await qemuImg.start()
     }
     }
     
     
     /*
     /*
@@ -99,15 +123,10 @@ import Foundation
         qemuImg.pushArgv("--output=json")
         qemuImg.pushArgv("--output=json")
         qemuImg.accessData(withBookmark: srcBookmark)
         qemuImg.accessData(withBookmark: srcBookmark)
         qemuImg.pushArgv(url.path)
         qemuImg.pushArgv(url.path)
-        let logging = UTMLogging()
+        let logging = QEMULogging()
         logging.delegate = qemuImg
         logging.delegate = qemuImg
         qemuImg.logging = logging
         qemuImg.logging = logging
-        try await Task.detached {
-            let (success, message) = await qemuImg.start("qemu-img")
-            if !success, let message = message {
-                throw message
-            }
-        }.value
+        try await qemuImg.start()
 
 
         let decoder = JSONDecoder()
         let decoder = JSONDecoder()
         decoder.keyDecodingStrategy = .convertFromSnakeCase
         decoder.keyDecodingStrategy = .convertFromSnakeCase
@@ -127,25 +146,34 @@ import Foundation
         qemuImg.accessData(withBookmark: srcBookmark)
         qemuImg.accessData(withBookmark: srcBookmark)
         qemuImg.pushArgv(url.path)
         qemuImg.pushArgv(url.path)
         qemuImg.pushArgv(String(size))
         qemuImg.pushArgv(String(size))
-        let logging = UTMLogging()
+        let logging = QEMULogging()
         logging.delegate = qemuImg
         logging.delegate = qemuImg
         qemuImg.logging = logging
         qemuImg.logging = logging
-        try await Task.detached {
-            let (success, message) = await qemuImg.start("qemu-img")
-            if !success, let message = message {
-                throw message
-            }
-        }.value
+        try await qemuImg.start()
+    }
+}
+
+private enum UTMQemuImageError: Error {
+    case qemuError(String)
+    case unknown
+}
+
+extension UTMQemuImageError: LocalizedError {
+    var errorDescription: String? {
+        switch self {
+        case .qemuError(let message): return message
+        case .unknown: return NSLocalizedString("An unknown QEMU error has occurred.", comment: "UTMQemuImage")
+        }
     }
     }
 }
 }
 
 
-// MARK: - Logging delegate
+// MARK: - Logging
 
 
-extension UTMQemuImage: UTMLoggingDelegate {
-    func logging(_ logging: UTMLogging, didRecieveOutputLine line: String) {
+extension UTMQemuImage: QEMULoggingDelegate {
+    func logging(_ logging: QEMULogging, didRecieveOutputLine line: String) {
         logOutput += line
         logOutput += line
     }
     }
     
     
-    func logging(_ logging: UTMLogging, didRecieveErrorLine line: String) {
+    func logging(_ logging: QEMULogging, didRecieveErrorLine line: String) {
     }
     }
 }
 }

+ 80 - 0
Managers/UTMQemuPort.swift

@@ -0,0 +1,80 @@
+//
+// 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 QEMUKitInternal
+#if WITH_QEMU_TCI
+import CocoaSpiceNoUsb
+#else
+import CocoaSpice
+#endif
+
+@objc class UTMQemuPort: NSObject, QEMUPort {
+    var readDataHandler: readDataHandler_t? {
+        didSet {
+            updateDelegate()
+        }
+    }
+    
+    var errorHandler: errorHandler_t? {
+        didSet {
+            updateDelegate()
+        }
+    }
+    
+    var disconnectHandler: disconnectHandler_t? {
+        didSet {
+            updateDelegate()
+        }
+    }
+    
+    var isOpen: Bool = true
+    
+    private let port: CSPort
+    
+    func write(_ data: Data) {
+        port.write(data)
+    }
+    
+    @objc init(from port: CSPort) {
+        self.port = port
+        super.init()
+        port.delegate = self
+    }
+    
+    /// We defer setting of delegate to after `readDataHandler` is set in order to handle cached data.
+    private func updateDelegate() {
+        if readDataHandler != nil || errorHandler != nil || disconnectHandler != nil {
+            port.delegate = self
+        } else {
+            port.delegate = nil
+        }
+    }
+}
+
+extension UTMQemuPort: CSPortDelegate {
+    func portDidDisconect(_ port: CSPort) {
+        isOpen = false
+        disconnectHandler?()
+    }
+    
+    func port(_ port: CSPort, didError error: String) {
+        errorHandler?(error)
+    }
+    
+    func port(_ port: CSPort, didRecieveData data: Data) {
+        readDataHandler?(data)
+    }
+}

+ 3 - 2
Managers/UTMQemuSystem.h

@@ -34,16 +34,17 @@ typedef NS_ENUM(NSInteger, UTMQEMUSoundBackend) {
 
 
 NS_ASSUME_NONNULL_BEGIN
 NS_ASSUME_NONNULL_BEGIN
 
 
-@interface UTMQemuSystem : UTMQemu
+@interface UTMQemuSystem : UTMQemu <QEMULauncher>
 
 
 @property (nonatomic, nullable, copy) NSArray<NSURL *> *resources;
 @property (nonatomic, nullable, copy) NSArray<NSURL *> *resources;
 @property (nonatomic, nullable, weak) NSDictionary<NSURL *, NSData *> *remoteBookmarks;
 @property (nonatomic, nullable, weak) NSDictionary<NSURL *, NSData *> *remoteBookmarks;
 @property (nonatomic) UTMQEMURendererBackend rendererBackend;
 @property (nonatomic) UTMQEMURendererBackend rendererBackend;
+@property (nonatomic, weak) id<QEMULauncherDelegate> launcherDelegate;
 
 
 - (instancetype)init NS_UNAVAILABLE;
 - (instancetype)init NS_UNAVAILABLE;
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments NS_UNAVAILABLE;
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments NS_UNAVAILABLE;
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments architecture:(NSString *)architecture NS_DESIGNATED_INITIALIZER;
 - (instancetype)initWithArguments:(NSArray<NSString *> *)arguments architecture:(NSString *)architecture NS_DESIGNATED_INITIALIZER;
-- (void)startWithCompletion:(void(^)(BOOL, NSString *))completion;
+- (void)startQemuWithCompletion:(void(^)(NSError * _Nullable))completion;
 
 
 @end
 @end
 
 

+ 6 - 1
Managers/UTMQemuSystem.m

@@ -97,7 +97,7 @@ static void *start_qemu(void *args) {
     return (_qemu_init != NULL) && (_qemu_main_loop != NULL) && (_qemu_cleanup != NULL);
     return (_qemu_init != NULL) && (_qemu_main_loop != NULL) && (_qemu_cleanup != NULL);
 }
 }
 
 
-- (void)startWithCompletion:(void (^)(BOOL, NSString * _Nonnull))completion {
+- (void)startQemuWithCompletion:(nonnull void (^)(NSError * _Nullable))completion {
     dispatch_group_t group = dispatch_group_create();
     dispatch_group_t group = dispatch_group_create();
     for (NSURL *resourceURL in self.resources) {
     for (NSURL *resourceURL in self.resources) {
         NSData *bookmark = self.remoteBookmarks[resourceURL];
         NSData *bookmark = self.remoteBookmarks[resourceURL];
@@ -124,4 +124,9 @@ static void *start_qemu(void *args) {
     [self startQemu:name completion:completion];
     [self startQemu:name completion:completion];
 }
 }
 
 
+/// Called by superclass
+- (void)qemuHasExited:(NSInteger)exitCode message:(nullable NSString *)message {
+    [self.launcherDelegate qemuLauncher:self didExitWithExitCode:exitCode message:message];
+}
+
 @end
 @end

+ 431 - 10
Managers/UTMQemuVirtualMachine.swift

@@ -15,9 +15,419 @@
 //
 //
 
 
 import Foundation
 import Foundation
+import QEMUKit
+
+private var SpiceIoServiceGuestAgentContext = 0
+private let kSuspendSnapshotName = "suspend"
+
+/// QEMU backend virtual machine
+@objc class UTMQemuVirtualMachine: UTMVirtualMachine {
+    /// Set to true to request guest tools install.
+    ///
+    /// This property is observable and must only be accessed on the main thread.
+    @Published var isGuestToolsInstallRequested: Bool = false
+    
+    /// Handle SPICE IO related events
+    weak var ioServiceDelegate: UTMSpiceIODelegate? {
+        didSet {
+            if let ioService = ioService {
+                ioService.delegate = ioServiceDelegate
+            }
+        }
+    }
+    
+    /// SPICE interface
+    private(set) var ioService: UTMSpiceIO? {
+        didSet {
+            oldValue?.delegate = nil
+            ioService?.delegate = ioServiceDelegate
+        }
+    }
+    
+    private let qemuVM = QEMUVirtualMachine()
+    
+    private var system: UTMQemuSystem? {
+        get async {
+            await qemuVM.launcher as? UTMQemuSystem
+        }
+    }
+    
+    /// QEMU QMP interface
+    var monitor: QEMUMonitor? {
+        get async {
+            await qemuVM.monitor
+        }
+    }
+    
+    /// QEMU Guest Agent interface
+    var guestAgent: QEMUGuestAgent? {
+        get async {
+            await qemuVM.guestAgent
+        }
+    }
+    
+    private var startTask: Task<Void, any Error>?
+}
+
+// MARK: - Shortcut access
+extension UTMQemuVirtualMachine {
+    override func accessShortcut() async throws {
+        guard isShortcut else {
+            return
+        }
+        // if VM has not started yet, we create a temporary process
+        let system = await system ?? UTMQemu()
+        var bookmark = await registryEntry.package.remoteBookmark
+        let existing = bookmark != nil
+        if !existing {
+            // create temporary bookmark
+            bookmark = try path.bookmarkData()
+        } else {
+            let bookmarkPath = await registryEntry.package.path
+            // in case old path is still accessed
+            system.stopAccessingPath(bookmarkPath)
+        }
+        let (success, newBookmark, newPath) = await system.accessData(withBookmark: bookmark!, securityScoped: existing)
+        if success {
+            await registryEntry.setPackageRemoteBookmark(newBookmark, path: newPath)
+        } else if existing {
+            // the remote bookmark is invalid but the local one still might be valid
+            await registryEntry.setPackageRemoteBookmark(nil)
+            try await accessShortcut()
+        } else {
+            throw UTMQemuVirtualMachineError.failedToAccessShortcut
+        }
+    }
+}
+
+// MARK: - VM actions
+
+extension UTMQemuVirtualMachine {
+    private var rendererBackend: UTMQEMURendererBackend {
+        let rawValue = UserDefaults.standard.integer(forKey: "QEMURendererBackend")
+        return UTMQEMURendererBackend(rawValue: rawValue) ?? .qemuRendererBackendDefault
+    }
+    
+    @MainActor private func qemuEnsureEfiVarsAvailable() async throws {
+        guard let efiVarsURL = qemuConfig.qemu.efiVarsURL else {
+            return
+        }
+        guard qemuConfig.isLegacy else {
+            return
+        }
+        _ = try await qemuConfig.qemu.saveData(to: efiVarsURL.deletingLastPathComponent(), for: qemuConfig.system)
+    }
+    
+    private func _vmStart() async throws {
+        // check if we can actually start this VM
+        guard isSupported else {
+            throw UTMQemuVirtualMachineError.emulationNotSupported
+        }
+        // start logging
+        if await qemuConfig.qemu.hasDebugLog, let debugLogURL = await qemuConfig.qemu.debugLogURL {
+            logging.log(toFile: debugLogURL)
+        }
+        await MainActor.run {
+            qemuConfig.qemu.isDisposable = isRunningAsSnapshot
+        }
+        
+        let allArguments = await qemuConfig.allArguments
+        let arguments = allArguments.map({ $0.string })
+        let resources = allArguments.compactMap({ $0.fileUrls }).flatMap({ $0 })
+        let remoteBookmarks = await remoteBookmarks
+        
+        let system = await UTMQemuSystem(arguments: arguments, architecture: qemuConfig.system.architecture.rawValue)
+        system.resources = resources
+        system.remoteBookmarks = remoteBookmarks as NSDictionary
+        system.rendererBackend = rendererBackend
+        try Task.checkCancellation()
+        
+        if isShortcut {
+            try await accessShortcut()
+            try Task.checkCancellation()
+        }
+        
+        let ioService = UTMSpiceIO(configuration: config)
+        try ioService.start()
+        try Task.checkCancellation()
+        
+        // create EFI variables for legacy config
+        // this is ugly code and should be removed when legacy config support is removed
+        try await qemuEnsureEfiVarsAvailable()
+        try Task.checkCancellation()
+        
+        // start QEMU
+        await qemuVM.setDelegate(self)
+        try await qemuVM.start(launcher: system, interface: ioService)
+        let monitor = await monitor!
+        try Task.checkCancellation()
+        
+        // load saved state if requested
+        if !isRunningAsSnapshot, await registryEntry.isSuspended {
+            try await monitor.qemuRestoreSnapshot(kSuspendSnapshotName)
+            try Task.checkCancellation()
+        }
+        
+        // set up SPICE sharing and removable drives
+        try await self.restoreExternalDrives()
+        try await self.restoreSharedDirectory()
+        try Task.checkCancellation()
+        
+        // continue VM boot
+        try await monitor.continueBoot()
+        
+        // delete saved state
+        if await registryEntry.isSuspended {
+            try? await _vmDeleteState()
+        }
+        
+        // save ioService and let it set the delegate
+        self.ioService = ioService
+    }
+    
+    override func vmStart() async throws {
+        guard state == .vmStopped else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        changeState(.vmStarting)
+        do {
+            startTask = Task {
+                try await _vmStart()
+            }
+            defer {
+                startTask = nil
+            }
+            try await startTask!.value
+            changeState(.vmStarted)
+        } catch {
+            // delete suspend state on error
+            await registryEntry.setIsSuspended(false)
+            changeState(.vmStopped)
+            throw error
+        }
+    }
+    
+    override func vmStop(force: Bool) async throws {
+        if force {
+            // prevent deadlock force stopping during startup
+            ioService?.disconnect()
+        }
+        guard state != .vmStopped else {
+            return // nothing to do
+        }
+        guard force || state == .vmStarted else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        if !force {
+            changeState(.vmStopping)
+        }
+        defer {
+            changeState(.vmStopped)
+        }
+        if force {
+            await qemuVM.kill()
+        } else {
+            try await qemuVM.stop()
+        }
+    }
+    
+    private func _vmReset() async throws {
+        if await registryEntry.isSuspended {
+            try? await _vmDeleteState()
+        }
+        guard let monitor = await qemuVM.monitor else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        try await monitor.qemuReset()
+    }
+    
+    override func vmReset() async throws {
+        guard state == .vmStarted || state == .vmPaused else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        changeState(.vmStopping)
+        do {
+            try await _vmReset()
+            changeState(.vmStarted)
+        } catch {
+            changeState(.vmStopped)
+            throw error
+        }
+    }
+    
+    private func _vmPause() async throws {
+        guard let monitor = await monitor else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        await updateScreenshot()
+        await saveScreenshot()
+        try await monitor.qemuStop()
+    }
+    
+    override func vmPause(save: Bool) async throws {
+        guard state == .vmStarted else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        changeState(.vmPausing)
+        do {
+            try await _vmPause()
+            if save {
+                try? await _vmSaveState()
+            }
+            changeState(.vmPaused)
+        } catch {
+            changeState(.vmStopped)
+            throw error
+        }
+    }
+    
+    private func _vmSaveState() async throws {
+        guard let monitor = await monitor else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        do {
+            let result = try await monitor.qemuSaveSnapshot(kSuspendSnapshotName)
+            if result.localizedCaseInsensitiveContains("Error") {
+                throw UTMQemuVirtualMachineError.qemuError(result)
+            }
+            await registryEntry.setIsSuspended(true)
+            await saveScreenshot()
+        } catch {
+            throw UTMQemuVirtualMachineError.saveSnapshotFailed(error)
+        }
+    }
+    
+    override func vmSaveState() async throws {
+        guard state == .vmPaused || state == .vmStarted else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        try await _vmSaveState()
+    }
+    
+    private func _vmDeleteState() async throws {
+        if let monitor = await monitor { // if QEMU is running
+            let result = try await monitor.qemuDeleteSnapshot(kSuspendSnapshotName)
+            if result.localizedCaseInsensitiveContains("Error") {
+                throw UTMQemuVirtualMachineError.qemuError(result)
+            }
+        }
+        await registryEntry.setIsSuspended(false)
+    }
+    
+    override func vmDeleteState() async throws {
+        try await _vmDeleteState()
+    }
+    
+    private func _vmResume() async throws {
+        guard let monitor = await monitor else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        try await monitor.qemuResume()
+        if await registryEntry.isSuspended {
+            try? await _vmDeleteState()
+        }
+    }
+    
+    override func vmResume() async throws {
+        guard state == .vmPaused else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        changeState(.vmResuming)
+        do {
+            try await _vmResume()
+            changeState(.vmStarted)
+        } catch {
+            changeState(.vmStopped)
+            throw error
+        }
+    }
+    
+    override func vmGuestPowerDown() async throws {
+        guard let monitor = await monitor else {
+            throw UTMQemuVirtualMachineError.invalidVmState
+        }
+        try await monitor.qemuPowerDown()
+    }
+    
+    /// Attempt to cancel the current operation
+    ///
+    /// Currently only `vmStart()` can be cancelled.
+    func cancelOperation() {
+        startTask?.cancel()
+    }
+}
+
+// MARK: - VM delegate
+extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate {
+    func qemuVMDidStart(_ qemuVM: QEMUVirtualMachine) {
+        // not used
+    }
+    
+    func qemuVMWillStop(_ qemuVM: QEMUVirtualMachine) {
+        // not used
+    }
+    
+    func qemuVMDidStop(_ qemuVM: QEMUVirtualMachine) {
+        changeState(.vmStopped)
+    }
+    
+    func qemuVM(_ qemuVM: QEMUVirtualMachine, didError error: Error) {
+        delegate?.virtualMachine(self, didErrorWithMessage: error.localizedDescription)
+    }
+    
+    func qemuVM(_ qemuVM: QEMUVirtualMachine, didCreatePttyDevice path: String, label: String) {
+        let scanner = Scanner(string: label)
+        guard scanner.scanString("term") != nil else {
+            logger.error("Invalid terminal device '\(label)'")
+            return
+        }
+        var term: Int = -1
+        guard scanner.scanInt(&term) else {
+            logger.error("Cannot get index from terminal device '\(label)'")
+            return
+        }
+        let index = term
+        Task { @MainActor in
+            guard index >= 0 && index < qemuConfig.serials.count else {
+                logger.error("Serial device '\(path)' out of bounds for index \(index)")
+                return
+            }
+            qemuConfig.serials[index].pttyDevice = URL(fileURLWithPath: path)
+        }
+    }
+}
+
+// MARK: - Input device switching
+extension UTMQemuVirtualMachine {
+    func requestInputTablet(_ tablet: Bool) {
+        
+    }
+}
+
+// MARK: - USB redirection
+extension UTMQemuVirtualMachine {
+    var hasUsbRedirection: Bool {
+        return jb_has_usb_entitlement()
+    }
+}
+
+// MARK: - Screenshot
+extension UTMQemuVirtualMachine {
+    @MainActor
+    override func updateScreenshot() {
+        ioService?.screenshot(completion: { screenshot in
+            self.screenshot = screenshot
+        })
+    }
+    
+    @MainActor
+    override func saveScreenshot() {
+        super.saveScreenshot()
+    }
+}
 
 
 // MARK: - Display details
 // MARK: - Display details
-public extension UTMQemuVirtualMachine {
+extension UTMQemuVirtualMachine {
     internal var qemuConfig: UTMQemuConfiguration {
     internal var qemuConfig: UTMQemuConfiguration {
         config.qemuConfig!
         config.qemuConfig!
     }
     }
@@ -77,11 +487,11 @@ extension UTMQemuVirtualMachine {
         guard drive.isExternal else {
         guard drive.isExternal else {
             return
             return
         }
         }
-        if let qemu = qemu, qemu.isConnected {
+        if let qemu = await monitor, qemu.isConnected {
             try qemu.ejectDrive("drive\(drive.id)", force: isForced)
             try qemu.ejectDrive("drive\(drive.id)", force: isForced)
         }
         }
         if let oldPath = await registryEntry.externalDrives[drive.id]?.path {
         if let oldPath = await registryEntry.externalDrives[drive.id]?.path {
-            system?.stopAccessingPath(oldPath)
+            await system?.stopAccessingPath(oldPath)
         }
         }
         await registryEntry.removeExternalDrive(forId: drive.id)
         await registryEntry.removeExternalDrive(forId: drive.id)
     }
     }
@@ -99,20 +509,19 @@ extension UTMQemuVirtualMachine {
     }
     }
     
     
     private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, url: URL?, isSecurityScoped: Bool) async throws {
     private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, url: URL?, isSecurityScoped: Bool) async throws {
-        let system = system ?? UTMQemu()
+        let system = await system ?? UTMQemu()
         let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped)
         let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped)
         guard let bookmark = bookmark, let path = path, success else {
         guard let bookmark = bookmark, let path = path, success else {
             throw UTMQemuVirtualMachineError.accessDriveImageFailed
             throw UTMQemuVirtualMachineError.accessDriveImageFailed
         }
         }
         await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id)
         await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id)
-        let newUrl = url ?? URL(fileURLWithPath: path)
-        if let qemu = qemu, qemu.isConnected {
+        if let qemu = await monitor, qemu.isConnected {
             try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path)
             try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path)
         }
         }
     }
     }
     
     
     func restoreExternalDrives() async throws {
     func restoreExternalDrives() async throws {
-        guard system != nil else {
+        guard await system != nil else {
             throw UTMQemuVirtualMachineError.invalidVmState
             throw UTMQemuVirtualMachineError.invalidVmState
         }
         }
         for drive in await qemuConfig.drives {
         for drive in await qemuConfig.drives {
@@ -159,7 +568,7 @@ extension UTMQemuVirtualMachine {
     
     
     func clearSharedDirectory() async {
     func clearSharedDirectory() async {
         if let oldPath = await registryEntry.sharedDirectories.first?.path {
         if let oldPath = await registryEntry.sharedDirectories.first?.path {
-            system?.stopAccessingPath(oldPath)
+            await system?.stopAccessingPath(oldPath)
         }
         }
         await registryEntry.removeAllSharedDirectories()
         await registryEntry.removeAllSharedDirectories()
     }
     }
@@ -183,9 +592,9 @@ extension UTMQemuVirtualMachine {
     }
     }
     
     
     func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws {
     func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws {
-        let system = system ?? UTMQemu()
+        let system = await system ?? UTMQemu()
         let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped)
         let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped)
-        guard let bookmark = bookmark, let path = path, success else {
+        guard let bookmark = bookmark, let _ = path, success else {
             throw UTMQemuVirtualMachineError.accessDriveImageFailed
             throw UTMQemuVirtualMachineError.accessDriveImageFailed
         }
         }
         await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark)
         await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark)
@@ -261,17 +670,29 @@ extension UTMQemuVirtualMachine {
 }
 }
 
 
 enum UTMQemuVirtualMachineError: Error {
 enum UTMQemuVirtualMachineError: Error {
+    case failedToAccessShortcut
+    case emulationNotSupported
+    case qemuError(String)
     case accessDriveImageFailed
     case accessDriveImageFailed
     case accessShareFailed
     case accessShareFailed
     case invalidVmState
     case invalidVmState
+    case saveSnapshotFailed(Error)
 }
 }
 
 
 extension UTMQemuVirtualMachineError: LocalizedError {
 extension UTMQemuVirtualMachineError: LocalizedError {
     var errorDescription: String? {
     var errorDescription: String? {
         switch self {
         switch self {
+        case .failedToAccessShortcut:
+            return NSLocalizedString("Failed to access data from shortcut.", comment: "UTMQemuVirtualMachine")
+        case .emulationNotSupported:
+            return NSLocalizedString("This build of UTM does not support emulating the architecture of this VM.", comment: "UTMQemuVirtualMachine")
+        case .qemuError(let message):
+            return message
         case .accessDriveImageFailed: return NSLocalizedString("Failed to access drive image path.", comment: "UTMQemuVirtualMachine")
         case .accessDriveImageFailed: return NSLocalizedString("Failed to access drive image path.", comment: "UTMQemuVirtualMachine")
         case .accessShareFailed: return NSLocalizedString("Failed to access shared directory.", comment: "UTMQemuVirtualMachine")
         case .accessShareFailed: return NSLocalizedString("Failed to access shared directory.", comment: "UTMQemuVirtualMachine")
         case .invalidVmState: return NSLocalizedString("The virtual machine is in an invalid state.", comment: "UTMQemuVirtualMachine")
         case .invalidVmState: return NSLocalizedString("The virtual machine is in an invalid state.", comment: "UTMQemuVirtualMachine")
+        case .saveSnapshotFailed(let error):
+            return String.localizedStringWithFormat(NSLocalizedString("Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@", comment: "UTMQemuVirtualMachine"), error.localizedDescription)
         }
         }
     }
     }
 }
 }

+ 11 - 26
Managers/UTMRegistryEntry.swift

@@ -244,6 +244,17 @@ extension UTMRegistryEntryDecodable {
         terminalSettings = other.terminalSettings
         terminalSettings = other.terminalSettings
         hasMigratedConfig = other.hasMigratedConfig
         hasMigratedConfig = other.hasMigratedConfig
     }
     }
+    
+    func setIsSuspended(_ isSuspended: Bool) {
+        self.isSuspended = isSuspended
+    }
+    
+    func setPackageRemoteBookmark(_ remoteBookmark: Data?, path: String? = nil) {
+        package.remoteBookmark = remoteBookmark
+        if let path = path {
+            package.path = path
+        }
+    }
 }
 }
 
 
 // MARK: - Migration from UTMViewState
 // MARK: - Migration from UTMViewState
@@ -352,32 +363,6 @@ extension UTMRegistryEntry {
             _isSuspended = newValue
             _isSuspended = newValue
         }
         }
     }
     }
-    
-    var packageRemoteBookmark: Data? {
-        get {
-            _package.remoteBookmark
-        }
-        
-        set {
-            _package.remoteBookmark = newValue
-        }
-    }
-    
-    var packageRemotePath: String? {
-        get {
-            if _package.remoteBookmark != nil {
-                return _package.path
-            } else {
-                return nil
-            }
-        }
-        
-        set {
-            if newValue != nil {
-                _package.path = newValue!
-            }
-        }
-    }
 }
 }
 
 
 extension UTMRegistryEntry {
 extension UTMRegistryEntry {

+ 4 - 8
Managers/UTMSpiceIO.h

@@ -16,6 +16,7 @@
 
 
 #import <Foundation/Foundation.h>
 #import <Foundation/Foundation.h>
 #import "UTMSpiceIODelegate.h"
 #import "UTMSpiceIODelegate.h"
+@import QEMUKitInternal;
 #if defined(WITH_QEMU_TCI)
 #if defined(WITH_QEMU_TCI)
 @import CocoaSpiceNoUsb;
 @import CocoaSpiceNoUsb;
 #else
 #else
@@ -23,14 +24,10 @@
 #endif
 #endif
 
 
 @class UTMConfigurationWrapper;
 @class UTMConfigurationWrapper;
-@class UTMQemuMonitor;
-@class UTMQemuGuestAgent;
-
-typedef void (^ioConnectCompletionHandler_t)(UTMQemuMonitor * _Nullable, NSError * _Nullable);
 
 
 NS_ASSUME_NONNULL_BEGIN
 NS_ASSUME_NONNULL_BEGIN
 
 
-@interface UTMSpiceIO : NSObject<CSConnectionDelegate>
+@interface UTMSpiceIO : NSObject<CSConnectionDelegate, QEMUInterface>
 
 
 @property (nonatomic, readonly, nonnull) UTMConfigurationWrapper* configuration;
 @property (nonatomic, readonly, nonnull) UTMConfigurationWrapper* configuration;
 @property (nonatomic, readonly, nullable) CSDisplay *primaryDisplay;
 @property (nonatomic, readonly, nullable) CSDisplay *primaryDisplay;
@@ -38,7 +35,6 @@ NS_ASSUME_NONNULL_BEGIN
 @property (nonatomic, readonly, nullable) CSPort *primarySerial;
 @property (nonatomic, readonly, nullable) CSPort *primarySerial;
 @property (nonatomic, readonly) NSArray<CSDisplay *> *displays;
 @property (nonatomic, readonly) NSArray<CSDisplay *> *displays;
 @property (nonatomic, readonly) NSArray<CSPort *> *serials;
 @property (nonatomic, readonly) NSArray<CSPort *> *serials;
-@property (nonatomic, readonly, nullable) UTMQemuGuestAgent *qemuGuestAgent;
 #if !defined(WITH_QEMU_TCI)
 #if !defined(WITH_QEMU_TCI)
 @property (nonatomic, readonly, nullable) CSUSBManager *primaryUsbManager;
 @property (nonatomic, readonly, nullable) CSUSBManager *primaryUsbManager;
 #endif
 #endif
@@ -49,8 +45,8 @@ NS_ASSUME_NONNULL_BEGIN
 - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration NS_DESIGNATED_INITIALIZER;
 - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration NS_DESIGNATED_INITIALIZER;
 - (void)changeSharedDirectory:(NSURL *)url;
 - (void)changeSharedDirectory:(NSURL *)url;
 
 
-- (BOOL)startWithError:(NSError **)err;
-- (void)connectWithCompletion:(ioConnectCompletionHandler_t)block;
+- (BOOL)startWithError:(NSError * _Nullable *)error;
+- (BOOL)connectWithError:(NSError * _Nullable *)error;
 - (void)disconnect;
 - (void)disconnect;
 
 
 - (void)screenshotWithCompletion:(screenshotCallback_t)completion;
 - (void)screenshotWithCompletion:(screenshotCallback_t)completion;

+ 19 - 62
Managers/UTMSpiceIO.m

@@ -16,13 +16,8 @@
 
 
 #import <glib.h>
 #import <glib.h>
 #import "UTMSpiceIO.h"
 #import "UTMSpiceIO.h"
-#import "UTMQemuMonitor.h"
-#import "UTMQemuGuestAgent.h"
-#import "UTMLogging.h"
 #import "UTM-Swift.h"
 #import "UTM-Swift.h"
 
 
-const int kMaxSpiceStartAttempts = 15; // qemu needs to start spice server first
-const int64_t kSpiceStartRetryTimeout = (int64_t)1*NSEC_PER_SEC;
 extern NSString *const kUTMErrorDomain;
 extern NSString *const kUTMErrorDomain;
 
 
 @interface UTMSpiceIO ()
 @interface UTMSpiceIO ()
@@ -33,7 +28,6 @@ extern NSString *const kUTMErrorDomain;
 @property (nonatomic, readwrite, nullable) CSInput *primaryInput;
 @property (nonatomic, readwrite, nullable) CSInput *primaryInput;
 @property (nonatomic, readwrite, nullable) CSPort *primarySerial;
 @property (nonatomic, readwrite, nullable) CSPort *primarySerial;
 @property (nonatomic) NSMutableArray<CSPort *> *mutableSerials;
 @property (nonatomic) NSMutableArray<CSPort *> *mutableSerials;
-@property (nonatomic, readwrite, nullable) UTMQemuGuestAgent *qemuGuestAgent;
 #if !defined(WITH_QEMU_TCI)
 #if !defined(WITH_QEMU_TCI)
 @property (nonatomic, readwrite, nullable) CSUSBManager *primaryUsbManager;
 @property (nonatomic, readwrite, nullable) CSUSBManager *primaryUsbManager;
 #endif
 #endif
@@ -43,14 +37,13 @@ extern NSString *const kUTMErrorDomain;
 @property (nonatomic) NSInteger port;
 @property (nonatomic) NSInteger port;
 @property (nonatomic) BOOL dynamicResolutionSupported;
 @property (nonatomic) BOOL dynamicResolutionSupported;
 @property (nonatomic, readwrite) BOOL isConnected;
 @property (nonatomic, readwrite) BOOL isConnected;
-@property (nonatomic) dispatch_queue_t connectQueue;
-@property (nonatomic, nullable) void (^connectAttemptCallback)(void);
-@property (nonatomic, nullable) void (^connectFinishedCallback)(UTMQemuMonitor *, CSConnectionError, NSError * _Nullable);
 
 
 @end
 @end
 
 
 @implementation UTMSpiceIO
 @implementation UTMSpiceIO
 
 
+@synthesize connectDelegate;
+
 - (NSArray<CSDisplay *> *)displays {
 - (NSArray<CSDisplay *> *)displays {
     return self.mutableDisplays;
     return self.mutableDisplays;
 }
 }
@@ -62,7 +55,6 @@ extern NSString *const kUTMErrorDomain;
 - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration {
 - (instancetype)initWithConfiguration:(UTMConfigurationWrapper *)configuration {
     if (self = [super init]) {
     if (self = [super init]) {
         self.configuration = configuration;
         self.configuration = configuration;
-        self.connectQueue = dispatch_queue_create("SPICE Connect Attempt", NULL);
         self.mutableDisplays = [NSMutableArray array];
         self.mutableDisplays = [NSMutableArray array];
         self.mutableSerials = [NSMutableArray array];
         self.mutableSerials = [NSMutableArray array];
     }
     }
@@ -82,7 +74,7 @@ extern NSString *const kUTMErrorDomain;
 
 
 #pragma mark - Actions
 #pragma mark - Actions
 
 
-- (BOOL)startWithError:(NSError **)err {
+- (BOOL)startWithError:(NSError * _Nullable *)error {
     if (!self.spice) {
     if (!self.spice) {
         self.spice = [CSMain sharedInstance];
         self.spice = [CSMain sharedInstance];
     }
     }
@@ -92,8 +84,8 @@ extern NSString *const kUTMErrorDomain;
     // do not need to encode/decode audio locally
     // do not need to encode/decode audio locally
     g_setenv("SPICE_DISABLE_OPUS", "1", TRUE);
     g_setenv("SPICE_DISABLE_OPUS", "1", TRUE);
     if (![self.spice spiceStart]) {
     if (![self.spice spiceStart]) {
-        if (err) {
-            *err = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to start SPICE client.", "UTMSpiceIO")}];
+        if (error) {
+            *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to start SPICE client.", "UTMSpiceIO")}];
         }
         }
         return NO;
         return NO;
     }
     }
@@ -102,44 +94,18 @@ extern NSString *const kUTMErrorDomain;
     return YES;
     return YES;
 }
 }
 
 
-- (void)connectWithCompletion:(ioConnectCompletionHandler_t)block {
-    __weak typeof(self) weakSelf = self;
-    __block int attemptsLeft = kMaxSpiceStartAttempts;
-    dispatch_async(self.connectQueue, ^{
-        self.connectFinishedCallback = ^(UTMQemuMonitor *monitor, CSConnectionError code, NSError *error) {
-            typeof(self) _self = weakSelf;
-            if (!_self) {
-                return;
-            }
-            if (monitor) {
-                _self.connectAttemptCallback = nil;
-                block(monitor, nil);
-            } else if (_self.connectAttemptCallback && code == kCSConnectionErrorConnect && attemptsLeft --> 0) {
-                dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kSpiceStartRetryTimeout), _self.connectQueue, _self.connectAttemptCallback);
-            } else {
-                _self.connectAttemptCallback = nil;
-                block(nil, error);
-            }
-        };
-        self.connectAttemptCallback = ^{
-            typeof(self) _self = weakSelf;
-            if (!_self) {
-                return;
-            }
-            if (![_self.spiceConnection connect]) {
-                _self.connectFinishedCallback(nil, 0, [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Internal error trying to connect to SPICE server.", "UTMSpiceIO")}]);
-            }
-        };
-        self.connectAttemptCallback();
-    });
+- (BOOL)connectWithError:(NSError * _Nullable *)error {
+    if (![self.spiceConnection connect]) {
+        if (error) {
+            *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Internal error trying to connect to SPICE server.", "UTMSpiceIO")}];
+        }
+        return NO;
+    } else {
+        return YES;
+    }
 }
 }
 
 
 - (void)disconnect {
 - (void)disconnect {
-    dispatch_async(self.connectQueue, ^{
-        if (self.connectFinishedCallback) {
-            self.connectFinishedCallback(nil, 0, nil);
-        }
-    });
     [self endSharingDirectory];
     [self endSharingDirectory];
     [self.spiceConnection disconnect];
     [self.spiceConnection disconnect];
     self.spiceConnection.delegate = nil;
     self.spiceConnection.delegate = nil;
@@ -192,12 +158,7 @@ extern NSString *const kUTMErrorDomain;
 - (void)spiceError:(CSConnection *)connection code:(CSConnectionError)code message:(nullable NSString *)message {
 - (void)spiceError:(CSConnection *)connection code:(CSConnectionError)code message:(nullable NSString *)message {
     NSAssert(connection == self.spiceConnection, @"Unknown connection");
     NSAssert(connection == self.spiceConnection, @"Unknown connection");
     self.isConnected = NO;
     self.isConnected = NO;
-    NSError *error = [NSError errorWithDomain:kUTMErrorDomain code:-code userInfo:@{NSLocalizedDescriptionKey: message}];
-    dispatch_async(self.connectQueue, ^{
-        if (self.connectFinishedCallback) {
-            self.connectFinishedCallback(nil, code, error);
-        }
-    });
+    [self.connectDelegate qemuInterface:self didErrorWithMessage:message];
 }
 }
 
 
 - (void)spiceDisplayCreated:(CSConnection *)connection display:(CSDisplay *)display {
 - (void)spiceDisplayCreated:(CSConnection *)connection display:(CSDisplay *)display {
@@ -230,15 +191,12 @@ extern NSString *const kUTMErrorDomain;
 
 
 - (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port {
 - (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port {
     if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) {
     if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) {
-        UTMQemuMonitor *monitor = [[UTMQemuMonitor alloc] initWithPort:port];
-        dispatch_async(self.connectQueue, ^{
-            if (self.connectFinishedCallback) {
-                self.connectFinishedCallback(monitor, 0, nil);
-            }
-        });
+        UTMQemuPort *qemuPort = [[UTMQemuPort alloc] initFrom:port];
+        [self.connectDelegate qemuInterface:self didCreateMonitorPort:qemuPort];
     }
     }
     if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
     if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
-        self.qemuGuestAgent = [[UTMQemuGuestAgent alloc] initWithPort:port];
+        UTMQemuPort *qemuPort = [[UTMQemuPort alloc] initFrom:port];
+        [self.connectDelegate qemuInterface:self didCreateGuestAgentPort:qemuPort];
     }
     }
     if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
     if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
         self.primarySerial = port;
         self.primarySerial = port;
@@ -253,7 +211,6 @@ extern NSString *const kUTMErrorDomain;
     if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) {
     if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) {
     }
     }
     if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
     if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
-        self.qemuGuestAgent = nil;
     }
     }
     if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
     if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
         self.primarySerial = port;
         self.primarySerial = port;

+ 0 - 3
Managers/UTMVirtualMachine-Private.h

@@ -20,9 +20,6 @@ NS_ASSUME_NONNULL_BEGIN
 
 
 @interface UTMVirtualMachine ()
 @interface UTMVirtualMachine ()
 
 
-/// Reference to logger for VM stdout/stderr
-@property (nonatomic) UTMLogging *logging;
-
 @property (nonatomic, assign, readwrite) UTMVMState state;
 @property (nonatomic, assign, readwrite) UTMVMState state;
 
 
 - (instancetype)init NS_UNAVAILABLE;
 - (instancetype)init NS_UNAVAILABLE;

+ 3 - 0
Managers/UTMVirtualMachine-Protected.h

@@ -64,6 +64,9 @@ extern NSString *const kUTMBundleConfigFilename;
 /// This property is observable and must only be accessed on the main thread.
 /// This property is observable and must only be accessed on the main thread.
 @property (nonatomic, readonly) NSString *stateLabel;
 @property (nonatomic, readonly) NSString *stateLabel;
 
 
+/// Reference to logger for VM stdout/stderr
+@property (nonatomic) QEMULogging *logging;
+
 @property (nonatomic, readwrite) NSURL *path;
 @property (nonatomic, readwrite) NSURL *path;
 @property (nonatomic, readwrite) UTMConfigurationWrapper *config;
 @property (nonatomic, readwrite) UTMConfigurationWrapper *config;
 @property (nonatomic, readwrite, nullable) CSScreenshot *screenshot;
 @property (nonatomic, readwrite, nullable) CSScreenshot *screenshot;

+ 1 - 1
Managers/UTMVirtualMachine.h

@@ -16,9 +16,9 @@
 
 
 #import <Foundation/Foundation.h>
 #import <Foundation/Foundation.h>
 #import "UTMVirtualMachineDelegate.h"
 #import "UTMVirtualMachineDelegate.h"
+@import QEMUKitInternal;
 
 
 @class UTMConfigurationWrapper;
 @class UTMConfigurationWrapper;
-@class UTMLogging;
 @class UTMRegistryEntry;
 @class UTMRegistryEntry;
 @class CSScreenshot;
 @class CSScreenshot;
 
 

+ 2 - 4
Managers/UTMVirtualMachine.m

@@ -17,8 +17,6 @@
 #import <TargetConditionals.h>
 #import <TargetConditionals.h>
 #import "UTMVirtualMachine.h"
 #import "UTMVirtualMachine.h"
 #import "UTMVirtualMachine-Private.h"
 #import "UTMVirtualMachine-Private.h"
-#import "UTMQemuVirtualMachine.h"
-#import "UTMLogging.h"
 #import "UTM-Swift.h"
 #import "UTM-Swift.h"
 #if defined(WITH_QEMU_TCI)
 #if defined(WITH_QEMU_TCI)
 @import CocoaSpiceNoUsb;
 @import CocoaSpiceNoUsb;
@@ -171,9 +169,9 @@ const dispatch_time_t kScreenshotPeriodSeconds = 60 * NSEC_PER_SEC;
     if (self) {
     if (self) {
         _state = kVMStopped;
         _state = kVMStopped;
 #if TARGET_OS_IPHONE
 #if TARGET_OS_IPHONE
-        self.logging = [UTMLogging sharedInstance];
+        self.logging = [QEMULogging sharedInstance];
 #else
 #else
-        self.logging = [UTMLogging new];
+        self.logging = [QEMULogging new];
 #endif
 #endif
         _config = configuration;
         _config = configuration;
         self.path = packageURL;
         self.path = packageURL;

+ 1 - 7
Platform/Swift-Bridging-Header.h

@@ -25,20 +25,14 @@
 #include "UTMLegacyQemuConfiguration+Sharing.h"
 #include "UTMLegacyQemuConfiguration+Sharing.h"
 #include "UTMLegacyQemuConfiguration+System.h"
 #include "UTMLegacyQemuConfiguration+System.h"
 #include "UTMLegacyQemuConfigurationPortForward.h"
 #include "UTMLegacyQemuConfigurationPortForward.h"
-#include "UTMQcow2.h"
 #include "UTMQemu.h"
 #include "UTMQemu.h"
-#include "UTMQemuMonitor.h"
-#include "UTMQemuMonitor+BlockDevices.h"
-#include "UTMQemuGuestAgent.h"
 #include "UTMQemuSystem.h"
 #include "UTMQemuSystem.h"
 #include "UTMJailbreak.h"
 #include "UTMJailbreak.h"
 #include "UTMLogging.h"
 #include "UTMLogging.h"
 #include "UTMLegacyViewState.h"
 #include "UTMLegacyViewState.h"
 #include "UTMVirtualMachine.h"
 #include "UTMVirtualMachine.h"
 #include "UTMVirtualMachine-Protected.h"
 #include "UTMVirtualMachine-Protected.h"
-#include "UTMQemuVirtualMachine.h"
-#include "UTMQemuVirtualMachine-Protected.h"
-#include "UTMQemuVirtualMachine+SPICE.h"
+#include "UTMVirtualMachineDelegate.h"
 #include "UTMSpiceIO.h"
 #include "UTMSpiceIO.h"
 #if TARGET_OS_IPHONE
 #if TARGET_OS_IPHONE
 #include "UTMLocationManager.h"
 #include "UTMLocationManager.h"

+ 0 - 2
Platform/iOS/Display/VMDisplayMetalViewController+Pointer.m

@@ -20,8 +20,6 @@
 #import "VMCursor.h"
 #import "VMCursor.h"
 #import "CSDisplay.h"
 #import "CSDisplay.h"
 #import "VMScroll.h"
 #import "VMScroll.h"
-#import "UTMQemuVirtualMachine.h"
-#import "UTMQemuVirtualMachine+SPICE.h"
 #import "UTMLogging.h"
 #import "UTMLogging.h"
 #import "UTM-Swift.h"
 #import "UTM-Swift.h"
 
 

+ 0 - 2
Platform/iOS/Display/VMDisplayMetalViewController+Touch.m

@@ -22,8 +22,6 @@
 #import "CSDisplay.h"
 #import "CSDisplay.h"
 #import "UTMSpiceIO.h"
 #import "UTMSpiceIO.h"
 #import "UTMLogging.h"
 #import "UTMLogging.h"
-#import "UTMQemuVirtualMachine.h"
-#import "UTMQemuVirtualMachine+SPICE.h"
 #import "UTM-Swift.h"
 #import "UTM-Swift.h"
 
 
 const CGFloat kScrollSpeedReduction = 100.0f;
 const CGFloat kScrollSpeedReduction = 100.0f;

+ 1 - 1
Platform/iOS/VMSessionState.swift

@@ -70,7 +70,7 @@ import SwiftUI
         self.vm = vm
         self.vm = vm
         super.init()
         super.init()
         vm.delegate = self
         vm.delegate = self
-        vm.ioDelegate = self
+        vm.ioServiceDelegate = self
     }
     }
     
     
     func registerWindow(_ window: UUID, isExternal: Bool = false) {
     func registerWindow(_ window: UUID, isExternal: Bool = false) {

+ 1 - 1
Platform/macOS/Display/VMDisplayQemuDisplayController.swift

@@ -73,7 +73,7 @@ class VMDisplayQemuWindowController: VMDisplayWindowController {
     
     
     override func enterLive() {
     override func enterLive() {
         if !isSecondary {
         if !isSecondary {
-            qemuVM.ioDelegate = self
+            qemuVM.ioServiceDelegate = self
         }
         }
         startPauseToolbarItem.isEnabled = true
         startPauseToolbarItem.isEnabled = true
         if defaultPauseTooltip == nil {
         if defaultPauseTooltip == nil {

+ 4 - 2
Platform/macOS/Display/VMDisplayWindowController.swift

@@ -383,8 +383,10 @@ extension VMDisplayWindowController: UTMVirtualMachineDelegate {
 // MARK: - Computer wakeup
 // MARK: - Computer wakeup
 extension VMDisplayWindowController {
 extension VMDisplayWindowController {
     @objc private func didWake(_ notification: NSNotification) {
     @objc private func didWake(_ notification: NSNotification) {
-        if let qemuVM = vm as? UTMQemuVirtualMachine, let ga = qemuVM.guestAgent {
-            ga.guestSetTime(NSDate.now.timeIntervalSince1970)
+        if let qemuVM = vm as? UTMQemuVirtualMachine {
+            Task {
+                try? await qemuVM.guestAgent?.guestSetTime(NSDate.now.timeIntervalSince1970)
+            }
         }
         }
     }
     }
 }
 }

+ 4 - 2
Platform/macOS/VMHeadlessSessionState.swift

@@ -105,8 +105,10 @@ extension Notification.Name {
 // MARK: - Computer wakeup
 // MARK: - Computer wakeup
 extension VMHeadlessSessionState {
 extension VMHeadlessSessionState {
     @objc private func didWake(_ notification: NSNotification) {
     @objc private func didWake(_ notification: NSNotification) {
-        if let qemuVM = vm as? UTMQemuVirtualMachine, let ga = qemuVM.guestAgent {
-            ga.guestSetTime(NSDate.now.timeIntervalSince1970)
+        if let qemuVM = vm as? UTMQemuVirtualMachine {
+            Task {
+                try? await qemuVM.guestAgent?.guestSetTime(NSDate.now.timeIntervalSince1970)
+            }
         }
         }
     }
     }
 }
 }

+ 2 - 0
QEMUHelper/QEMUHelper.h

@@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN
 // This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection.
 // This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection.
 @interface QEMUHelper : NSObject <QEMUHelperProtocol>
 @interface QEMUHelper : NSObject <QEMUHelperProtocol>
 
 
+@property (nonatomic) NSXPCConnection *connection;
+
 @end
 @end
 
 
 NS_ASSUME_NONNULL_END
 NS_ASSUME_NONNULL_END

+ 10 - 8
QEMUHelper/QEMUHelper.m

@@ -15,6 +15,7 @@
 //
 //
 
 
 #import "QEMUHelper.h"
 #import "QEMUHelper.h"
+#import "QEMUHelperDelegate.h"
 #import <stdio.h>
 #import <stdio.h>
 
 
 @interface QEMUHelper ()
 @interface QEMUHelper ()
@@ -94,7 +95,7 @@
     NSLog(@"Cannot find '%@' in existing scoped access.", path);
     NSLog(@"Cannot find '%@' in existing scoped access.", path);
 }
 }
 
 
-- (void)startQemu:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryBookmark:(NSData *)libBookmark argv:(NSArray<NSString *> *)argv onExit:(void(^)(BOOL,NSString *))onExit {
+- (void)startQemu:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryBookmark:(NSData *)libBookmark argv:(NSArray<NSString *> *)argv completion:(void(^)(BOOL,NSString *))completion {
     NSError *err;
     NSError *err;
     NSURL *libraryPath = [NSURL URLByResolvingBookmarkData:libBookmark
     NSURL *libraryPath = [NSURL URLByResolvingBookmarkData:libBookmark
                                                    options:0
                                                    options:0
@@ -103,14 +104,14 @@
                                                      error:&err];
                                                      error:&err];
     if (!libraryPath || ![[NSFileManager defaultManager] fileExistsAtPath:libraryPath.path]) {
     if (!libraryPath || ![[NSFileManager defaultManager] fileExistsAtPath:libraryPath.path]) {
         NSLog(@"Cannot resolve library path: %@", err);
         NSLog(@"Cannot resolve library path: %@", err);
-        onExit(NO, NSLocalizedString(@"Cannot find QEMU support libraries.", @"QEMUHelper"));
+        completion(NO, NSLocalizedString(@"Cannot find QEMU support libraries.", @"QEMUHelper"));
         return;
         return;
     }
     }
     
     
-    [self startQemuTask:binName standardOutput:standardOutput standardError:standardError libraryPath:libraryPath argv:argv onExit:onExit];
+    [self startQemuTask:binName standardOutput:standardOutput standardError:standardError libraryPath:libraryPath argv:argv completion:completion];
 }
 }
 
 
-- (void)startQemuTask:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryPath:(NSURL *)libraryPath argv:(NSArray<NSString *> *)argv onExit:(void(^)(BOOL,NSString *))onExit {
+- (void)startQemuTask:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryPath:(NSURL *)libraryPath argv:(NSArray<NSString *> *)argv completion:(void(^)(BOOL,NSString *))completion {
     NSError *err;
     NSError *err;
     NSTask *task = [NSTask new];
     NSTask *task = [NSTask new];
     NSMutableArray<NSString *> *newArgv = [argv mutableCopy];
     NSMutableArray<NSString *> *newArgv = [argv mutableCopy];
@@ -129,15 +130,16 @@
     task.environment = environment;
     task.environment = environment;
     task.qualityOfService = NSQualityOfServiceUserInitiated;
     task.qualityOfService = NSQualityOfServiceUserInitiated;
     task.terminationHandler = ^(NSTask *task) {
     task.terminationHandler = ^(NSTask *task) {
-        BOOL normalExit = task.terminationReason == NSTaskTerminationReasonExit && task.terminationStatus == 0;
         _self.childTask = nil;
         _self.childTask = nil;
-        onExit(normalExit, nil);
+        [_self.connection.remoteObjectProxy qemuHasExited:task.terminationStatus message:nil];
     };
     };
     if (![task launchAndReturnError:&err]) {
     if (![task launchAndReturnError:&err]) {
         NSLog(@"Error starting QEMU: %@", err);
         NSLog(@"Error starting QEMU: %@", err);
-        onExit(NO, NSLocalizedString(@"Error starting QEMU.", @"QEMUHelper"));
+        completion(NO, err.localizedDescription);
+    } else {
+        self.childTask = task;
+        completion(YES, nil);
     }
     }
-    self.childTask = task;
 }
 }
 
 
 - (void)terminate {
 - (void)terminate {

+ 27 - 0
QEMUHelper/QEMUHelperDelegate.h

@@ -0,0 +1,27 @@
+//
+// 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/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol QEMUHelperDelegate <NSObject>
+
+- (void)qemuHasExited:(NSInteger)exitCode message:(nullable NSString *)message;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 1 - 1
QEMUHelper/QEMUHelperProtocol.h

@@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 
 - (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion;
 - (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion;
 - (void)stopAccessingPath:(nullable NSString *)path;
 - (void)stopAccessingPath:(nullable NSString *)path;
-- (void)startQemu:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryBookmark:(NSData *)libBookmark argv:(NSArray<NSString *> *)argv onExit:(void(^)(BOOL,NSString *))onExit;
+- (void)startQemu:(NSString *)binName standardOutput:(NSFileHandle *)standardOutput standardError:(NSFileHandle *)standardError libraryBookmark:(NSData *)libBookmark argv:(NSArray<NSString *> *)argv completion:(void(^)(BOOL,NSString *))completion;
 - (void)terminate;
 - (void)terminate;
 
 
 @end
 @end

+ 5 - 0
QEMUHelper/main.m

@@ -16,6 +16,7 @@
 
 
 #import <Foundation/Foundation.h>
 #import <Foundation/Foundation.h>
 #import "QEMUHelper.h"
 #import "QEMUHelper.h"
+#import "QEMUHelperDelegate.h"
 
 
 @interface ServiceDelegate : NSObject <NSXPCListenerDelegate>
 @interface ServiceDelegate : NSObject <NSXPCListenerDelegate>
 @end
 @end
@@ -29,8 +30,12 @@
     // First, set the interface that the exported object implements.
     // First, set the interface that the exported object implements.
     newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(QEMUHelperProtocol)];
     newConnection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(QEMUHelperProtocol)];
     
     
+    // Set the remote interface as well
+    newConnection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(QEMUHelperDelegate)];
+    
     // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object.
     // Next, set the object that the connection exports. All messages sent on the connection to this service will be sent to the exported object to handle. The connection retains the exported object.
     QEMUHelper *exportedObject = [QEMUHelper new];
     QEMUHelper *exportedObject = [QEMUHelper new];
+    exportedObject.connection = newConnection;
     newConnection.exportedObject = exportedObject;
     newConnection.exportedObject = exportedObject;
     
     
     // Resuming the connection allows the system to deliver more incoming messages.
     // Resuming the connection allows the system to deliver more incoming messages.

+ 15 - 16
Scripting/UTMScriptingGuestFileImpl.swift

@@ -15,6 +15,7 @@
 //
 //
 
 
 import Foundation
 import Foundation
+import QEMUKitInternal
 
 
 @MainActor
 @MainActor
 @objc(UTMScriptingGuestFileImpl)
 @objc(UTMScriptingGuestFileImpl)
@@ -22,12 +23,10 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
     @objc private(set) var id: Int
     @objc private(set) var id: Int
     
     
     weak private var parent: UTMScriptingVirtualMachineImpl?
     weak private var parent: UTMScriptingVirtualMachineImpl?
-    weak private var guestAgent: UTMQemuGuestAgent?
     
     
     init(from handle: Int, parent: UTMScriptingVirtualMachineImpl) {
     init(from handle: Int, parent: UTMScriptingVirtualMachineImpl) {
         self.id = handle
         self.id = handle
         self.parent = parent
         self.parent = parent
-        self.guestAgent = parent.guestAgent
     }
     }
     
     
     override var objectSpecifier: NSScriptObjectSpecifier? {
     override var objectSpecifier: NSScriptObjectSpecifier? {
@@ -44,17 +43,17 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
                                    uniqueID: id)
                                    uniqueID: id)
     }
     }
     
     
-    private func seek(to offset: Int, whence: AEKeyword?, using guestAgent: UTMQemuGuestAgent) async throws {
-        let seek: QGASeek
+    private func seek(to offset: Int, whence: AEKeyword?, using guestAgent: QEMUGuestAgent) async throws {
+        let seek: QEMUGuestAgentSeek
         if let whence = whence {
         if let whence = whence {
             switch UTMScriptingWhence(rawValue: whence) {
             switch UTMScriptingWhence(rawValue: whence) {
-            case .startPosition: seek = QGA_SEEK_SET
-            case .currentPosition: seek = QGA_SEEK_CUR
-            case .endPosition: seek = QGA_SEEK_END
-            default: seek = QGA_SEEK_SET
+            case .startPosition: seek = .set
+            case .currentPosition: seek = .cur
+            case .endPosition: seek = .end
+            default: seek = .set
             }
             }
         } else {
         } else {
-            seek = QGA_SEEK_SET
+            seek = .set
         }
         }
         try await guestAgent.guestFileSeek(id, offset: offset, whence: seek)
         try await guestAgent.guestFileSeek(id, offset: offset, whence: seek)
     }
     }
@@ -67,7 +66,7 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
         let isBase64Encoded = command.evaluatedArguments?["isBase64Encoded"] as? Bool ?? false
         let isBase64Encoded = command.evaluatedArguments?["isBase64Encoded"] as? Bool ?? false
         let isClosing = command.evaluatedArguments?["isClosing"] as? Bool ?? true
         let isClosing = command.evaluatedArguments?["isClosing"] as? Bool ?? true
         withScriptCommand(command) { [self] in
         withScriptCommand(command) { [self] in
-            guard let guestAgent = guestAgent else {
+            guard let guestAgent = await parent?.guestAgent else {
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
             }
             }
             defer {
             defer {
@@ -97,7 +96,7 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
         let file = command.evaluatedArguments?["file"] as? URL
         let file = command.evaluatedArguments?["file"] as? URL
         let isClosing = command.evaluatedArguments?["isClosing"] as? Bool ?? true
         let isClosing = command.evaluatedArguments?["isClosing"] as? Bool ?? true
         withScriptCommand(command) { [self] in
         withScriptCommand(command) { [self] in
-            guard let guestAgent = guestAgent else {
+            guard let guestAgent = await parent?.guestAgent else {
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
             }
             }
             defer {
             defer {
@@ -108,7 +107,7 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
             guard let file = file else {
             guard let file = file else {
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.invalidParameter
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.invalidParameter
             }
             }
-            try await guestAgent.guestFileSeek(id, offset: 0, whence: QGA_SEEK_SET)
+            try await guestAgent.guestFileSeek(id, offset: 0, whence: .set)
             _ = file.startAccessingSecurityScopedResource()
             _ = file.startAccessingSecurityScopedResource()
             defer {
             defer {
                 file.stopAccessingSecurityScopedResource()
                 file.stopAccessingSecurityScopedResource()
@@ -130,7 +129,7 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
         let isBase64Encoded = command.evaluatedArguments?["isBase64Encoded"] as? Bool ?? false
         let isBase64Encoded = command.evaluatedArguments?["isBase64Encoded"] as? Bool ?? false
         let isClosing = command.evaluatedArguments?["isClosing"] as? Bool ?? true
         let isClosing = command.evaluatedArguments?["isClosing"] as? Bool ?? true
         withScriptCommand(command) { [self] in
         withScriptCommand(command) { [self] in
-            guard let guestAgent = guestAgent else {
+            guard let guestAgent = await parent?.guestAgent else {
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
             }
             }
             defer {
             defer {
@@ -154,7 +153,7 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
         let file = command.evaluatedArguments?["file"] as? URL
         let file = command.evaluatedArguments?["file"] as? URL
         let isClosing = command.evaluatedArguments?["isClosing"] as? Bool ?? true
         let isClosing = command.evaluatedArguments?["isClosing"] as? Bool ?? true
         withScriptCommand(command) { [self] in
         withScriptCommand(command) { [self] in
-            guard let guestAgent = guestAgent else {
+            guard let guestAgent = await parent?.guestAgent else {
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
             }
             }
             defer {
             defer {
@@ -165,7 +164,7 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
             guard let file = file else {
             guard let file = file else {
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.invalidParameter
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.invalidParameter
             }
             }
-            try await guestAgent.guestFileSeek(id, offset: 0, whence: QGA_SEEK_SET)
+            try await guestAgent.guestFileSeek(id, offset: 0, whence: .set)
             _ = file.startAccessingSecurityScopedResource()
             _ = file.startAccessingSecurityScopedResource()
             defer {
             defer {
                 file.stopAccessingSecurityScopedResource()
                 file.stopAccessingSecurityScopedResource()
@@ -181,7 +180,7 @@ class UTMScriptingGuestFileImpl: NSObject, UTMScriptable {
     
     
     @objc func close(_ command: NSScriptCommand) {
     @objc func close(_ command: NSScriptCommand) {
         withScriptCommand(command) { [self] in
         withScriptCommand(command) { [self] in
-            guard let guestAgent = guestAgent else {
+            guard let guestAgent = await parent?.guestAgent else {
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
             }
             }
             try await guestAgent.guestFileClose(id)
             try await guestAgent.guestFileClose(id)

+ 1 - 3
Scripting/UTMScriptingGuestProcessImpl.swift

@@ -22,12 +22,10 @@ class UTMScriptingGuestProcessImpl: NSObject, UTMScriptable {
     @objc private(set) var id: Int
     @objc private(set) var id: Int
     
     
     weak private var parent: UTMScriptingVirtualMachineImpl?
     weak private var parent: UTMScriptingVirtualMachineImpl?
-    weak private var guestAgent: UTMQemuGuestAgent?
     
     
     init(from pid: Int, parent: UTMScriptingVirtualMachineImpl) {
     init(from pid: Int, parent: UTMScriptingVirtualMachineImpl) {
         self.id = pid
         self.id = pid
         self.parent = parent
         self.parent = parent
-        self.guestAgent = parent.guestAgent
     }
     }
     
     
     override var objectSpecifier: NSScriptObjectSpecifier? {
     override var objectSpecifier: NSScriptObjectSpecifier? {
@@ -46,7 +44,7 @@ class UTMScriptingGuestProcessImpl: NSObject, UTMScriptable {
     
     
     @objc func getResult(_ command: NSScriptCommand) {
     @objc func getResult(_ command: NSScriptCommand) {
         withScriptCommand(command) { [self] in
         withScriptCommand(command) { [self] in
-            guard let guestAgent = guestAgent else {
+            guard let guestAgent = await parent?.guestAgent else {
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
                 throw UTMScriptingVirtualMachineImpl.ScriptingError.guestAgentNotRunning
             }
             }
             let status = try await guestAgent.guestExecStatus(id)
             let status = try await guestAgent.guestExecStatus(id)

+ 7 - 4
Scripting/UTMScriptingVirtualMachineImpl.swift

@@ -15,6 +15,7 @@
 //
 //
 
 
 import Foundation
 import Foundation
+import QEMUKitInternal
 
 
 @MainActor
 @MainActor
 @objc(UTMScriptingVirtualMachineImpl)
 @objc(UTMScriptingVirtualMachineImpl)
@@ -79,8 +80,10 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable {
         }
         }
     }
     }
     
     
-    var guestAgent: UTMQemuGuestAgent? {
-        (vm as? UTMQemuVirtualMachine)?.guestAgent
+    var guestAgent: QEMUGuestAgent! {
+        get async {
+            await (vm as? UTMQemuVirtualMachine)?.guestAgent
+        }
     }
     }
     
     
     override var objectSpecifier: NSScriptObjectSpecifier? {
     override var objectSpecifier: NSScriptObjectSpecifier? {
@@ -175,14 +178,14 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable {
 
 
 // MARK: - Guest agent suite
 // MARK: - Guest agent suite
 @objc extension UTMScriptingVirtualMachineImpl {
 @objc extension UTMScriptingVirtualMachineImpl {
-    @nonobjc private func withGuestAgent<Result>(_ block: (UTMQemuGuestAgent) async throws -> Result) async throws -> Result {
+    @nonobjc private func withGuestAgent<Result>(_ block: (QEMUGuestAgent) async throws -> Result) async throws -> Result {
         guard vm.state == .vmStarted else {
         guard vm.state == .vmStarted else {
             throw ScriptingError.notRunning
             throw ScriptingError.notRunning
         }
         }
         guard let vm = vm as? UTMQemuVirtualMachine else {
         guard let vm = vm as? UTMQemuVirtualMachine else {
             throw ScriptingError.operationNotSupported
             throw ScriptingError.operationNotSupported
         }
         }
-        guard let guestAgent = vm.guestAgent else {
+        guard let guestAgent = await vm.guestAgent else {
             throw ScriptingError.guestAgentNotRunning
             throw ScriptingError.guestAgentNotRunning
         }
         }
         return try await block(guestAgent)
         return try await block(guestAgent)

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 20 - 711
UTM.xcodeproj/project.pbxproj


+ 9 - 0
UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@@ -36,6 +36,15 @@
         "version" : "6.5.6"
         "version" : "6.5.6"
       }
       }
     },
     },
+    {
+      "identity" : "qemukit",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/utmapp/QEMUKit.git",
+      "state" : {
+        "branch" : "main",
+        "revision" : "0ead7dd3ce538191c57c95484039ee0e133a639f"
+      }
+    },
     {
     {
       "identity" : "swift-argument-parser",
       "identity" : "swift-argument-parser",
       "kind" : "remoteSourceControl",
       "kind" : "remoteSourceControl",

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott