Skip to content

Commit

Permalink
guestagent: add support for QEMU guest agent
Browse files Browse the repository at this point in the history
* Introduce delimiter and skipping to JSON parser
* Add support for setting up and connecting to GA via SPICE
* New class to handle GA commands
  • Loading branch information
osy committed Mar 12, 2023
1 parent b911e95 commit 7377b3a
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 10 deletions.
10 changes: 8 additions & 2 deletions Configuration/UTMQemuConfiguration+Arguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -728,17 +728,23 @@ import Foundation
}
}

private var isAgentUsed: Bool {
private var isSpiceAgentUsed: Bool {
guard system.architecture.hasAgentSupport else {
return false
}
return sharing.hasClipboardSharing || sharing.directoryShareMode == .webdav || displays.contains(where: { $0.isDynamicResolution })
}

@QEMUArgumentBuilder private var sharingArguments: [QEMUArgument] {
if isAgentUsed {
if system.architecture.hasAgentSupport {
f("-device")
f("virtio-serial")
f("-device")
f("virtserialport,chardev=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
f("-chardev")
f("spiceport,id=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
}
if isSpiceAgentUsed {
f("-device")
f("virtserialport,chardev=vdagent,name=com.redhat.spice.0")
f("-chardev")
Expand Down
2 changes: 1 addition & 1 deletion Managers/UTMJSONStream.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN

- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithPort:(CSPort *)port NS_DESIGNATED_INITIALIZER;
- (BOOL)sendDictionary:(NSDictionary *)dict error:(NSError * _Nullable *)error;
- (BOOL)sendDictionary:(NSDictionary *)dictionary shouldSynchronize:(BOOL)shouldSynchronize error:(NSError * _Nullable *)error;

@end

Expand Down
31 changes: 27 additions & 4 deletions Managers/UTMJSONStream.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
PARSER_NOT_IN_STRING,
PARSER_IN_STRING,
PARSER_IN_STRING_ESCAPE,
PARSER_WAITING_FOR_DELIMITER,
PARSER_INVALID
};

Expand Down Expand Up @@ -56,14 +57,23 @@ - (instancetype)initWithPort:(CSPort *)port {
}

- (void)parseData {
__block NSUInteger skipLength = 0;
__block NSUInteger endIndex = 0;
[self.data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) {
const char *str = (const char *)bytes;
if (byteRange.location + byteRange.length < self.parsedBytes) {
return;
}
for (NSUInteger i = self.parsedBytes - byteRange.location; i < byteRange.length; i++) {
if (self.state == PARSER_IN_STRING_ESCAPE) {
if (self.state == PARSER_WAITING_FOR_DELIMITER) {
skipLength++;
if (str[i] == (char)0xFF) {
self.state = PARSER_NOT_IN_STRING;
self.openCurlyCount = 0;
}
self.parsedBytes++;
continue;
} else if (self.state == PARSER_IN_STRING_ESCAPE) {
self.state = PARSER_IN_STRING;
} else {
switch (str[i]) {
Expand Down Expand Up @@ -126,8 +136,13 @@ - (void)parseData {
}
}
}];
if (skipLength > 0) {
// discard any data before delimiter
[self.data replaceBytesInRange:NSMakeRange(0, skipLength) withBytes:NULL length:0];
self.parsedBytes -= skipLength;
}
if (endIndex > 0) {
[self consumeJSONLength:endIndex];
[self consumeJSONLength:endIndex-skipLength];
}
}

Expand Down Expand Up @@ -169,7 +184,7 @@ - (void)port:(CSPort *)port didRecieveData:(NSData *)data {
});
}

- (BOOL)sendDictionary:(NSDictionary *)dict error:(NSError * _Nullable *)error {
- (BOOL)sendDictionary:(NSDictionary *)dict shouldSynchronize:(BOOL)shouldSynchronize error:(NSError * _Nullable *)error {
UTMLog(@"Debug JSON send -> %@", dict);
if (!self.port || !self.port.isOpen) {
if (error) {
Expand All @@ -181,7 +196,15 @@ - (BOOL)sendDictionary:(NSDictionary *)dict error:(NSError * _Nullable *)error {
if (!data) {
return NO;
}
[self.port writeData:data];
if (shouldSynchronize) {
dispatch_async(self.streamQueue, ^{
[self.port writeData:[NSData dataWithBytes:"\xFF" length:1]];
[self.port writeData:data];
self.state = PARSER_WAITING_FOR_DELIMITER;
});
} else {
[self.port writeData:data];
}
return YES;
}

Expand Down
35 changes: 35 additions & 0 deletions Managers/UTMQemuGuestAgent.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// 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 "UTMQemuManager.h"

NS_ASSUME_NONNULL_BEGIN

/// Interface with QEMU Guest Agent
@interface UTMQemuGuestAgent : UTMQemuManager

/// Attempt synchronization with guest agent
///
/// If an error is returned, any number of things could have happened including:
/// * Guest Agent has not started on the guest side
/// * Guest Agent has not been installed yet
/// * Guest Agent is too slow to respond
/// - Parameter completion: Callback to run on completion
- (void)synchronizeWithCompletion:(void (^ _Nullable)(NSError * _Nullable))completion;

@end

NS_ASSUME_NONNULL_END
106 changes: 106 additions & 0 deletions Managers/UTMQemuGuestAgent.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// 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 "UTMQemuGuestAgent.h"
#import "UTMQemuManager-Protected.h"
#import "qga-qapi-commands.h"

extern NSString *const kUTMErrorDomain;

@interface UTMQemuGuestAgent ()

@property (nonatomic) BOOL isGuestAgentResponsive;
@property (nonatomic, readwrite) BOOL shouldSynchronizeParser;
@property (nonatomic) dispatch_queue_t guestAgentQueue;

@end

@implementation UTMQemuGuestAgent

- (NSInteger)timeoutSeconds {
if (self.isGuestAgentResponsive) {
return 10;
} else {
return 1;
}
}

- (instancetype)initWithPort:(CSPort *)port {
if (self = [super initWithPort:port]) {
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, QOS_MIN_RELATIVE_PRIORITY);
self.guestAgentQueue = dispatch_queue_create("QEMU Guest Agent Server", attr);
}
return self;
}

- (void)jsonStream:(UTMJSONStream *)stream seenError:(NSError *)error {
self.isGuestAgentResponsive = NO;
[super jsonStream:stream seenError:error];
}

- (void)synchronizeWithCompletion:(void (^ _Nullable)(NSError * _Nullable))completion {
self.isGuestAgentResponsive = NO;
dispatch_async(self.guestAgentQueue, ^{
Error *qerr = NULL;
int64_t random = g_random_int();
int64_t response = 0;
self.shouldSynchronizeParser = YES;
response = qmp_guest_sync_delimited(random, &qerr, (__bridge void *)self);
self.shouldSynchronizeParser = NO;
if (qerr) {
if (completion) {
completion([self errorForQerror:qerr]);
}
return;
}
if (response != random) {
if (completion) {
completion([NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Mismatched id from guest-sync-delimited.", "UTMQemuGuestAgent")}]);
}
return;
}
self.isGuestAgentResponsive = YES;
if (completion) {
completion(nil);
}
});
}

- (void)_withSynchronizeBlock:(NSError * _Nullable (^)(void))block withCompletion:(void (^ _Nullable)(NSError * _Nullable))completion {
dispatch_async(self.guestAgentQueue, ^{
if (!self.isGuestAgentResponsive) {
[self synchronizeWithCompletion:^(NSError *error) {
if (error) {
if (completion) {
completion(error);
}
} else {
NSError *error = block();
if (completion) {
completion(error);
}
}
}];
} else {
NSError *error = block();
if (completion) {
completion(error);
}
}
});
}

@end
2 changes: 2 additions & 0 deletions Managers/UTMQemuManager-Protected.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN
@interface UTMQemuManager (Protected)

@property (nonatomic, readwrite) BOOL isConnected;
@property (nonatomic, readonly) NSInteger timeoutSeconds;
@property (nonatomic, readonly) BOOL shouldSynchronizeParser;

- (__autoreleasing NSError *)errorForQerror:(Error *)qerr;
- (BOOL)didGetUnhandledKey:(NSString *)key value:(id)value;
Expand Down
13 changes: 10 additions & 3 deletions Managers/UTMQemuManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
#import "qapi-emit-events.h"

extern NSString *const kUTMErrorDomain;
const int64_t kRPCTimeout = (int64_t)10*NSEC_PER_SEC;

typedef void(^rpcCompletionHandler_t)(NSDictionary *, NSError *);

Expand All @@ -40,6 +39,14 @@ - (void)setIsConnected:(BOOL)isConnected {
_isConnected = isConnected;
}

- (NSInteger)timeoutSeconds {
return 10;
}

- (BOOL)shouldSynchronizeParser {
return NO;
}

void qmp_rpc_call(CFDictionaryRef args, CFDictionaryRef *ret, Error **err, void *ctx) {
UTMQemuManager *self = (__bridge UTMQemuManager *)ctx;
dispatch_semaphore_t rpc_sema = dispatch_semaphore_create(0);
Expand All @@ -54,10 +61,10 @@ void qmp_rpc_call(CFDictionaryRef args, CFDictionaryRef *ret, Error **err, void
_self.rpcCallback = nil;
dispatch_semaphore_signal(rpc_sema); // copy to avoid race condition
};
if (![self.jsonStream sendDictionary:(__bridge NSDictionary *)args error:&nserr] && self.rpcCallback) {
if (![self.jsonStream sendDictionary:(__bridge NSDictionary *)args shouldSynchronize:self.shouldSynchronizeParser error:&nserr] && self.rpcCallback) {
self.rpcCallback(nil, nserr);
}
if (dispatch_semaphore_wait(rpc_sema, dispatch_time(DISPATCH_TIME_NOW, kRPCTimeout)) != 0) {
if (dispatch_semaphore_wait(rpc_sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)self.timeoutSeconds*NSEC_PER_SEC)) != 0) {
// possible race between this timeout and the callback being triggered
self.rpcCallback = ^(NSDictionary *ret_dict, NSError *ret_err){
_self.rpcCallback = nil;
Expand Down
2 changes: 2 additions & 0 deletions Managers/UTMQemuVirtualMachine.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#import "UTMVirtualMachine.h"
#import "UTMSpiceIODelegate.h"

@class UTMQemuGuestAgent;

NS_ASSUME_NONNULL_BEGIN

@interface UTMQemuVirtualMachine : UTMVirtualMachine
Expand Down
2 changes: 2 additions & 0 deletions Managers/UTMSpiceIO.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

@class UTMConfigurationWrapper;
@class UTMQemuMonitor;
@class UTMQemuGuestAgent;

typedef void (^ioConnectCompletionHandler_t)(UTMQemuMonitor * _Nullable, NSError * _Nullable);

Expand All @@ -37,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly, nullable) CSPort *primarySerial;
@property (nonatomic, readonly) NSArray<CSDisplay *> *displays;
@property (nonatomic, readonly) NSArray<CSPort *> *serials;
@property (nonatomic, readonly, nullable) UTMQemuGuestAgent *qemuGuestAgent;
#if !defined(WITH_QEMU_TCI)
@property (nonatomic, readonly, nullable) CSUSBManager *primaryUsbManager;
#endif
Expand Down
8 changes: 8 additions & 0 deletions Managers/UTMSpiceIO.m
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#import <glib.h>
#import "UTMSpiceIO.h"
#import "UTMQemuMonitor.h"
#import "UTMQemuGuestAgent.h"
#import "UTMLogging.h"
#import "UTM-Swift.h"

Expand All @@ -32,6 +33,7 @@ @interface UTMSpiceIO ()
@property (nonatomic, readwrite, nullable) CSInput *primaryInput;
@property (nonatomic, readwrite, nullable) CSPort *primarySerial;
@property (nonatomic) NSMutableArray<CSPort *> *mutableSerials;
@property (nonatomic, readwrite, nullable) UTMQemuGuestAgent *qemuGuestAgent;
#if !defined(WITH_QEMU_TCI)
@property (nonatomic, readwrite, nullable) CSUSBManager *primaryUsbManager;
#endif
Expand Down Expand Up @@ -235,6 +237,9 @@ - (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port
}
});
}
if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
self.qemuGuestAgent = [[UTMQemuGuestAgent alloc] initWithPort:port];
}
if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
self.primarySerial = port;
}
Expand All @@ -247,6 +252,9 @@ - (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port
- (void)spiceForwardedPortClosed:(CSConnection *)connection port:(CSPort *)port {
if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) {
}
if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
self.qemuGuestAgent = nil;
}
if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
self.primarySerial = port;
}
Expand Down
1 change: 1 addition & 0 deletions Platform/Swift-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "UTMQemu.h"
#include "UTMQemuMonitor.h"
#include "UTMQemuMonitor+BlockDevices.h"
#include "UTMQemuGuestAgent.h"
#include "UTMQemuSystem.h"
#include "UTMJailbreak.h"
#include "UTMLogging.h"
Expand Down
Loading

0 comments on commit 7377b3a

Please sign in to comment.