Skip to content

Commit c34b531

Browse files
committed
guestagent: add support for QEMU guest agent
* Introduce delimiter and skipping to JSON parser * Add support for setting up and connecting to GA via SPICE * New class to handle GA commands
1 parent b911e95 commit c34b531

File tree

11 files changed

+210
-10
lines changed

11 files changed

+210
-10
lines changed

Configuration/UTMQemuConfiguration+Arguments.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -728,17 +728,23 @@ import Foundation
728728
}
729729
}
730730

731-
private var isAgentUsed: Bool {
731+
private var isSpiceAgentUsed: Bool {
732732
guard system.architecture.hasAgentSupport else {
733733
return false
734734
}
735735
return sharing.hasClipboardSharing || sharing.directoryShareMode == .webdav || displays.contains(where: { $0.isDynamicResolution })
736736
}
737737

738738
@QEMUArgumentBuilder private var sharingArguments: [QEMUArgument] {
739-
if isAgentUsed {
739+
if system.architecture.hasAgentSupport {
740740
f("-device")
741741
f("virtio-serial")
742+
f("-device")
743+
f("virtserialport,chardev=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
744+
f("-chardev")
745+
f("spiceport,id=org.qemu.guest_agent,name=org.qemu.guest_agent.0")
746+
}
747+
if isSpiceAgentUsed {
742748
f("-device")
743749
f("virtserialport,chardev=vdagent,name=com.redhat.spice.0")
744750
f("-chardev")

Managers/UTMJSONStream.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
2727

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

3232
@end
3333

Managers/UTMJSONStream.m

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
PARSER_NOT_IN_STRING,
2525
PARSER_IN_STRING,
2626
PARSER_IN_STRING_ESCAPE,
27+
PARSER_WAITING_FOR_DELIMITER,
2728
PARSER_INVALID
2829
};
2930

@@ -56,14 +57,23 @@ - (instancetype)initWithPort:(CSPort *)port {
5657
}
5758

5859
- (void)parseData {
60+
__block NSUInteger skipLength = 0;
5961
__block NSUInteger endIndex = 0;
6062
[self.data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) {
6163
const char *str = (const char *)bytes;
6264
if (byteRange.location + byteRange.length < self.parsedBytes) {
6365
return;
6466
}
6567
for (NSUInteger i = self.parsedBytes - byteRange.location; i < byteRange.length; i++) {
66-
if (self.state == PARSER_IN_STRING_ESCAPE) {
68+
if (self.state == PARSER_WAITING_FOR_DELIMITER) {
69+
skipLength++;
70+
if (str[i] == (char)0xFF) {
71+
self.state = PARSER_NOT_IN_STRING;
72+
self.openCurlyCount = 0;
73+
}
74+
self.parsedBytes++;
75+
continue;
76+
} else if (self.state == PARSER_IN_STRING_ESCAPE) {
6777
self.state = PARSER_IN_STRING;
6878
} else {
6979
switch (str[i]) {
@@ -126,8 +136,13 @@ - (void)parseData {
126136
}
127137
}
128138
}];
139+
if (skipLength > 0) {
140+
// discard any data before delimiter
141+
[self.data replaceBytesInRange:NSMakeRange(0, skipLength) withBytes:NULL length:0];
142+
self.parsedBytes -= skipLength;
143+
}
129144
if (endIndex > 0) {
130-
[self consumeJSONLength:endIndex];
145+
[self consumeJSONLength:endIndex-skipLength];
131146
}
132147
}
133148

@@ -169,7 +184,7 @@ - (void)port:(CSPort *)port didRecieveData:(NSData *)data {
169184
});
170185
}
171186

172-
- (BOOL)sendDictionary:(NSDictionary *)dict error:(NSError * _Nullable *)error {
187+
- (BOOL)sendDictionary:(NSDictionary *)dict shouldSynchronize:(BOOL)shouldSynchronize error:(NSError * _Nullable *)error {
173188
UTMLog(@"Debug JSON send -> %@", dict);
174189
if (!self.port || !self.port.isOpen) {
175190
if (error) {
@@ -181,7 +196,15 @@ - (BOOL)sendDictionary:(NSDictionary *)dict error:(NSError * _Nullable *)error {
181196
if (!data) {
182197
return NO;
183198
}
184-
[self.port writeData:data];
199+
if (shouldSynchronize) {
200+
dispatch_async(self.streamQueue, ^{
201+
[self.port writeData:[NSData dataWithBytes:"\xFF" length:1]];
202+
[self.port writeData:data];
203+
self.state = PARSER_WAITING_FOR_DELIMITER;
204+
});
205+
} else {
206+
[self.port writeData:data];
207+
}
185208
return YES;
186209
}
187210

Managers/UTMQemuGuestAgent.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// Copyright © 2023 osy. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
#import "UTMQemuManager.h"
18+
19+
NS_ASSUME_NONNULL_BEGIN
20+
21+
/// Interface with QEMU Guest Agent
22+
@interface UTMQemuGuestAgent : UTMQemuManager
23+
24+
/// Attempt synchronization with guest agent
25+
///
26+
/// If an error is returned, any number of things could have happened including:
27+
/// * Guest Agent has not started on the guest side
28+
/// * Guest Agent has not been installed yet
29+
/// * Guest Agent is too slow to respond
30+
/// - Parameter completion: Callback to run on completion
31+
- (void)synchronizeWithCompletion:(void (^ _Nullable)(NSError * _Nullable))completion;
32+
33+
@end
34+
35+
NS_ASSUME_NONNULL_END

Managers/UTMQemuGuestAgent.m

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//
2+
// Copyright © 2023 osy. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
#import "UTMQemuGuestAgent.h"
18+
#import "UTMQemuManager-Protected.h"
19+
#import "qga-qapi-commands.h"
20+
21+
extern NSString *const kUTMErrorDomain;
22+
23+
@interface UTMQemuGuestAgent ()
24+
25+
@property (nonatomic) BOOL isGuestAgentResponsive;
26+
@property (nonatomic, readwrite) BOOL shouldSynchronizeParser;
27+
@property (nonatomic) dispatch_queue_t guestAgentQueue;
28+
29+
@end
30+
31+
@implementation UTMQemuGuestAgent
32+
33+
- (NSInteger)timeoutSeconds {
34+
if (self.isGuestAgentResponsive) {
35+
return 10;
36+
} else {
37+
return 1;
38+
}
39+
}
40+
41+
- (instancetype)initWithPort:(CSPort *)port {
42+
if (self = [super initWithPort:port]) {
43+
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, QOS_MIN_RELATIVE_PRIORITY);
44+
self.guestAgentQueue = dispatch_queue_create("QEMU Guest Agent Server", attr);
45+
}
46+
return self;
47+
}
48+
49+
- (void)jsonStream:(UTMJSONStream *)stream seenError:(NSError *)error {
50+
self.isGuestAgentResponsive = NO;
51+
[super jsonStream:stream seenError:error];
52+
}
53+
54+
- (void)synchronizeWithCompletion:(void (^ _Nullable)(NSError * _Nullable))completion {
55+
self.isGuestAgentResponsive = NO;
56+
dispatch_async(self.guestAgentQueue, ^{
57+
Error *qerr = NULL;
58+
int64_t random = g_random_int();
59+
int64_t response = 0;
60+
self.shouldSynchronizeParser = YES;
61+
response = qmp_guest_sync_delimited(random, &qerr, (__bridge void *)self);
62+
self.shouldSynchronizeParser = NO;
63+
if (qerr) {
64+
if (completion) {
65+
completion([self errorForQerror:qerr]);
66+
}
67+
return;
68+
}
69+
if (response != random) {
70+
if (completion) {
71+
completion([NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Mismatched id from guest-sync-delimited.", "UTMQemuGuestAgent")}]);
72+
}
73+
return;
74+
}
75+
self.isGuestAgentResponsive = YES;
76+
if (completion) {
77+
completion(nil);
78+
}
79+
});
80+
}
81+
82+
- (void)_withSynchronizeBlock:(NSError * _Nullable (^)(void))block withCompletion:(void (^ _Nullable)(NSError * _Nullable))completion {
83+
dispatch_async(self.guestAgentQueue, ^{
84+
if (!self.isGuestAgentResponsive) {
85+
[self synchronizeWithCompletion:^(NSError *error) {
86+
if (error) {
87+
if (completion) {
88+
completion(error);
89+
}
90+
} else {
91+
NSError *error = block();
92+
if (completion) {
93+
completion(error);
94+
}
95+
}
96+
}];
97+
} else {
98+
NSError *error = block();
99+
if (completion) {
100+
completion(error);
101+
}
102+
}
103+
});
104+
}
105+
106+
@end

Managers/UTMQemuManager-Protected.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ NS_ASSUME_NONNULL_BEGIN
2222
@interface UTMQemuManager (Protected)
2323

2424
@property (nonatomic, readwrite) BOOL isConnected;
25+
@property (nonatomic, readonly) NSInteger timeoutSeconds;
26+
@property (nonatomic, readonly) BOOL shouldSynchronizeParser;
2527

2628
- (__autoreleasing NSError *)errorForQerror:(Error *)qerr;
2729
- (BOOL)didGetUnhandledKey:(NSString *)key value:(id)value;

Managers/UTMQemuManager.m

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
#import "qapi-emit-events.h"
2121

2222
extern NSString *const kUTMErrorDomain;
23-
const int64_t kRPCTimeout = (int64_t)10*NSEC_PER_SEC;
2423

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

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

42+
- (NSInteger)timeoutSeconds {
43+
return 10;
44+
}
45+
46+
- (BOOL)shouldSynchronizeParser {
47+
return NO;
48+
}
49+
4350
void qmp_rpc_call(CFDictionaryRef args, CFDictionaryRef *ret, Error **err, void *ctx) {
4451
UTMQemuManager *self = (__bridge UTMQemuManager *)ctx;
4552
dispatch_semaphore_t rpc_sema = dispatch_semaphore_create(0);
@@ -54,10 +61,10 @@ void qmp_rpc_call(CFDictionaryRef args, CFDictionaryRef *ret, Error **err, void
5461
_self.rpcCallback = nil;
5562
dispatch_semaphore_signal(rpc_sema); // copy to avoid race condition
5663
};
57-
if (![self.jsonStream sendDictionary:(__bridge NSDictionary *)args error:&nserr] && self.rpcCallback) {
64+
if (![self.jsonStream sendDictionary:(__bridge NSDictionary *)args shouldSynchronize:self.shouldSynchronizeParser error:&nserr] && self.rpcCallback) {
5865
self.rpcCallback(nil, nserr);
5966
}
60-
if (dispatch_semaphore_wait(rpc_sema, dispatch_time(DISPATCH_TIME_NOW, kRPCTimeout)) != 0) {
67+
if (dispatch_semaphore_wait(rpc_sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)self.timeoutSeconds*NSEC_PER_SEC)) != 0) {
6168
// possible race between this timeout and the callback being triggered
6269
self.rpcCallback = ^(NSDictionary *ret_dict, NSError *ret_err){
6370
_self.rpcCallback = nil;

Managers/UTMSpiceIO.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
@class UTMConfigurationWrapper;
2626
@class UTMQemuMonitor;
27+
@class UTMQemuGuestAgent;
2728

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

@@ -37,6 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
3738
@property (nonatomic, readonly, nullable) CSPort *primarySerial;
3839
@property (nonatomic, readonly) NSArray<CSDisplay *> *displays;
3940
@property (nonatomic, readonly) NSArray<CSPort *> *serials;
41+
@property (nonatomic, readonly, nullable) UTMQemuGuestAgent *qemuGuestAgent;
4042
#if !defined(WITH_QEMU_TCI)
4143
@property (nonatomic, readonly, nullable) CSUSBManager *primaryUsbManager;
4244
#endif

Managers/UTMSpiceIO.m

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#import <glib.h>
1818
#import "UTMSpiceIO.h"
1919
#import "UTMQemuMonitor.h"
20+
#import "UTMQemuGuestAgent.h"
2021
#import "UTMLogging.h"
2122
#import "UTM-Swift.h"
2223

@@ -32,6 +33,7 @@ @interface UTMSpiceIO ()
3233
@property (nonatomic, readwrite, nullable) CSInput *primaryInput;
3334
@property (nonatomic, readwrite, nullable) CSPort *primarySerial;
3435
@property (nonatomic) NSMutableArray<CSPort *> *mutableSerials;
36+
@property (nonatomic, readwrite, nullable) UTMQemuGuestAgent *qemuGuestAgent;
3537
#if !defined(WITH_QEMU_TCI)
3638
@property (nonatomic, readwrite, nullable) CSUSBManager *primaryUsbManager;
3739
#endif
@@ -235,6 +237,9 @@ - (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port
235237
}
236238
});
237239
}
240+
if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
241+
self.qemuGuestAgent = [[UTMQemuGuestAgent alloc] initWithPort:port];
242+
}
238243
if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
239244
self.primarySerial = port;
240245
}
@@ -247,6 +252,9 @@ - (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port
247252
- (void)spiceForwardedPortClosed:(CSConnection *)connection port:(CSPort *)port {
248253
if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) {
249254
}
255+
if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
256+
self.qemuGuestAgent = nil;
257+
}
250258
if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
251259
self.primarySerial = port;
252260
}

Platform/Swift-Bridging-Header.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include "UTMQemu.h"
3030
#include "UTMQemuMonitor.h"
3131
#include "UTMQemuMonitor+BlockDevices.h"
32+
#include "UTMQemuGuestAgent.h"
3233
#include "UTMQemuSystem.h"
3334
#include "UTMJailbreak.h"
3435
#include "UTMLogging.h"

0 commit comments

Comments
 (0)