diff --git a/Managers/UTMQemuGuestAgent.h b/Managers/UTMQemuGuestAgent.h index b9e27b4e0..d7e101a85 100644 --- a/Managers/UTMQemuGuestAgent.h +++ b/Managers/UTMQemuGuestAgent.h @@ -30,6 +30,12 @@ NS_ASSUME_NONNULL_BEGIN /// - Parameter completion: Callback to run on completion - (void)synchronizeWithCompletion:(void (^ _Nullable)(NSError * _Nullable))completion; +/// Set guest time +/// - Parameters: +/// - time: time in seconds, relative to the Epoch of 1970-01-01 in UTC. +/// - completion: Callback to run on completion +- (void)guestSetTime:(NSTimeInterval)time withCompletion:(void (^ _Nullable)(NSError * _Nullable))completion; + @end NS_ASSUME_NONNULL_END diff --git a/Managers/UTMQemuGuestAgent.m b/Managers/UTMQemuGuestAgent.m index c7554d5f1..7d093d1e6 100644 --- a/Managers/UTMQemuGuestAgent.m +++ b/Managers/UTMQemuGuestAgent.m @@ -103,4 +103,17 @@ - (void)_withSynchronizeBlock:(NSError * _Nullable (^)(void))block withCompletio }); } +- (void)guestSetTime:(NSTimeInterval)time withCompletion:(void (^ _Nullable)(NSError * _Nullable))completion { + int64_t timeNanoseconds = (int64_t)(time * NSEC_PER_SEC); + [self _withSynchronizeBlock:^{ + Error *qerr = NULL; + qmp_guest_set_time(true, timeNanoseconds, &qerr, (__bridge void *)self); + if (qerr) { + return [self errorForQerror:qerr]; + } else { + return (NSError *)nil; + } + } withCompletion:completion]; +} + @end diff --git a/Managers/UTMQemuVirtualMachine.h b/Managers/UTMQemuVirtualMachine.h index 137e038e9..c986a7c54 100644 --- a/Managers/UTMQemuVirtualMachine.h +++ b/Managers/UTMQemuVirtualMachine.h @@ -25,6 +25,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, weak, nullable) id ioDelegate; +/// If non-null, provides access to the QEMU guest agent interface +@property (nonatomic, readonly, nullable) UTMQemuGuestAgent *guestAgent; + /// Set to true to request guest tools install. /// /// This property is observable and must only be accessed on the main thread. diff --git a/Managers/UTMQemuVirtualMachine.m b/Managers/UTMQemuVirtualMachine.m index cc2f1fa3b..a2a3aa982 100644 --- a/Managers/UTMQemuVirtualMachine.m +++ b/Managers/UTMQemuVirtualMachine.m @@ -22,6 +22,7 @@ #import "UTMQemuVirtualMachine.h" #import "UTMQemuVirtualMachine+SPICE.h" #import "UTMQemuMonitor.h" +#import "UTMQemuGuestAgent.h" #import "UTMQemuSystem.h" #import "UTMSpiceIO.h" #import "UTMLogging.h" @@ -33,9 +34,12 @@ extern NSString *const kUTMBundleConfigFilename; NSString *const kSuspendSnapshotName = @"suspend"; +static void *SpiceIoServiceGuestAgentContext = &SpiceIoServiceGuestAgentContext; + @interface UTMQemuVirtualMachine () @property (nonatomic, readwrite, nullable) UTMQemuMonitor *qemu; +@property (nonatomic, readwrite, nullable) UTMQemuGuestAgent *guestAgent; @property (nonatomic, readwrite, nullable) UTMQemuSystem *system; @property (nonatomic, readwrite, nullable) UTMSpiceIO *ioService; @property (nonatomic, weak) id ioServiceDelegate; @@ -188,6 +192,10 @@ - (void)_vmStartWithCompletion:(void (^)(NSError * _Nullable))completion { self.ioService = [[UTMSpiceIO alloc] initWithConfiguration:self.config]; self.ioService.delegate = self.ioServiceDelegate; self.ioServiceDelegate = nil; + [self.ioService addObserver:self + forKeyPath:@"qemuGuestAgent" + options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial + context:SpiceIoServiceGuestAgentContext]; NSError *spiceError; if (![self.ioService startWithError:&spiceError]) { @@ -336,6 +344,7 @@ - (void)_vmStopForce:(BOOL)force completion:(void (^)(NSError * _Nullable))compl } self.qemu.delegate = nil; self.qemu = nil; + [self.ioService removeObserver:self forKeyPath:@"qemuGuestAgent" context:SpiceIoServiceGuestAgentContext]; self.ioService = nil; if (force || dispatch_semaphore_wait(self.qemuDidExitEvent, dispatch_time(DISPATCH_TIME_NOW, kStopTimeout)) != 0) { @@ -586,6 +595,32 @@ - (void)vmGuestPowerDownWithCompletion:(void (^)(NSError * _Nullable))completion }); } +#pragma mark - QEMU Guest agent + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (context == SpiceIoServiceGuestAgentContext) { + UTMQemuGuestAgent *guestAgent = ((UTMSpiceIO *)object).qemuGuestAgent; + if (guestAgent == nil && self.guestAgent != nil) { + [self _didDisconnectGuestAgent:self.guestAgent]; + } + if (guestAgent != nil) { + [self _didConnectGuestAgent:guestAgent]; + } + self.guestAgent = guestAgent; + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)_didConnectGuestAgent:(UTMQemuGuestAgent *)guestAgent { + UTMLog(@"QEMU guest agent has connected."); + [guestAgent guestSetTime:NSDate.now.timeIntervalSince1970 withCompletion:nil]; +} + +- (void)_didDisconnectGuestAgent:(UTMQemuGuestAgent *)guestAgent { + UTMLog(@"QEMU guest agent has disconnected."); +} + #pragma mark - Qemu manager delegate - (void)qemuHasWakeup:(UTMQemuMonitor *)monitor { @@ -594,6 +629,7 @@ - (void)qemuHasWakeup:(UTMQemuMonitor *)monitor { - (void)qemuHasResumed:(UTMQemuMonitor *)monitor { UTMLog(@"qemuHasResumed"); + [self.guestAgent guestSetTime:NSDate.now.timeIntervalSince1970 withCompletion:nil]; } - (void)qemuHasStopped:(UTMQemuMonitor *)monitor {