313 lines
11 KiB
Objective-C
313 lines
11 KiB
Objective-C
//
|
|
// Copyright © 2022 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 <glib.h>
|
|
#import "UTMSpiceIO.h"
|
|
#import "UTM-Swift.h"
|
|
|
|
NSString *const kUTMErrorDomain = @"com.utmapp.utm";
|
|
|
|
@interface UTMSpiceIO ()
|
|
|
|
@property (nonatomic) NSURL *socketUrl;
|
|
@property (nonatomic) UTMSpiceIOOptions options;
|
|
@property (nonatomic, readwrite, nullable) CSDisplay *primaryDisplay;
|
|
@property (nonatomic) NSMutableArray<CSDisplay *> *mutableDisplays;
|
|
@property (nonatomic, readwrite, nullable) CSInput *primaryInput;
|
|
@property (nonatomic, readwrite, nullable) CSPort *primarySerial;
|
|
@property (nonatomic) NSMutableArray<CSPort *> *mutableSerials;
|
|
#if !defined(WITH_QEMU_TCI)
|
|
@property (nonatomic, readwrite, nullable) CSUSBManager *primaryUsbManager;
|
|
#endif
|
|
@property (nonatomic, nullable) CSConnection *spiceConnection;
|
|
@property (nonatomic, nullable) CSMain *spice;
|
|
@property (nonatomic, nullable, copy) NSURL *sharedDirectory;
|
|
@property (nonatomic) NSInteger port;
|
|
@property (nonatomic) BOOL dynamicResolutionSupported;
|
|
@property (nonatomic, readwrite) BOOL isConnected;
|
|
|
|
@end
|
|
|
|
@implementation UTMSpiceIO
|
|
|
|
@synthesize connectDelegate;
|
|
|
|
- (NSArray<CSDisplay *> *)displays {
|
|
return self.mutableDisplays;
|
|
}
|
|
|
|
- (NSArray<CSPort *> *)serials {
|
|
return self.mutableSerials;
|
|
}
|
|
|
|
- (LogHandler_t)logHandler {
|
|
return CSMain.sharedInstance.logHandler;
|
|
}
|
|
|
|
- (void)setLogHandler:(LogHandler_t)logHandler {
|
|
CSMain.sharedInstance.logHandler = logHandler;
|
|
}
|
|
|
|
- (instancetype)initWithSocketUrl:(NSURL *)socketUrl options:(UTMSpiceIOOptions)options {
|
|
if (self = [super init]) {
|
|
self.socketUrl = socketUrl;
|
|
self.options = options;
|
|
self.mutableDisplays = [NSMutableArray array];
|
|
self.mutableSerials = [NSMutableArray array];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)initializeSpiceIfNeeded {
|
|
if (!self.spiceConnection) {
|
|
NSURL *relativeSocketFile = [NSURL fileURLWithPath:self.socketUrl.lastPathComponent];
|
|
self.spiceConnection = [[CSConnection alloc] initWithUnixSocketFile:relativeSocketFile];
|
|
self.spiceConnection.delegate = self;
|
|
self.spiceConnection.audioEnabled = (self.options & UTMSpiceIOOptionsHasAudio) == UTMSpiceIOOptionsHasAudio;
|
|
self.spiceConnection.session.shareClipboard = (self.options & UTMSpiceIOOptionsHasClipboardSharing) == UTMSpiceIOOptionsHasClipboardSharing;
|
|
self.spiceConnection.session.pasteboardDelegate = [UTMPasteboard generalPasteboard];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Actions
|
|
|
|
- (BOOL)startWithError:(NSError * _Nullable *)error {
|
|
if (!self.spice) {
|
|
self.spice = [CSMain sharedInstance];
|
|
}
|
|
if ((self.options & UTMSpiceIOOptionsHasDebugLog) == UTMSpiceIOOptionsHasDebugLog) {
|
|
[self.spice spiceSetDebug:YES];
|
|
}
|
|
// do not need to encode/decode audio locally
|
|
g_setenv("SPICE_DISABLE_OPUS", "1", YES);
|
|
// need to chdir to workaround AF_UNIX sun_len limitations
|
|
NSString *curdir = self.socketUrl.URLByDeletingLastPathComponent.path;
|
|
if (!curdir || ![NSFileManager.defaultManager changeCurrentDirectoryPath:curdir]) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to change current directory.", "UTMSpiceIO")}];
|
|
}
|
|
return NO;
|
|
}
|
|
if (![self.spice spiceStart]) {
|
|
if (error) {
|
|
*error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to start SPICE client.", "UTMSpiceIO")}];
|
|
}
|
|
return NO;
|
|
}
|
|
[self initializeSpiceIfNeeded];
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (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 {
|
|
[self endSharingDirectory];
|
|
[self.spiceConnection disconnect];
|
|
self.spiceConnection.delegate = nil;
|
|
self.spiceConnection = nil;
|
|
self.spice = nil;
|
|
self.primaryDisplay = nil;
|
|
[self.mutableDisplays removeAllObjects];
|
|
self.primaryInput = nil;
|
|
self.primarySerial = nil;
|
|
[self.mutableSerials removeAllObjects];
|
|
#if !defined(WITH_QEMU_TCI)
|
|
self.primaryUsbManager = nil;
|
|
#endif
|
|
}
|
|
|
|
- (void)screenshotWithCompletion:(screenshotCallback_t)completion {
|
|
return [self.primaryDisplay screenshotWithCompletion:completion];
|
|
}
|
|
|
|
#pragma mark - CSConnectionDelegate
|
|
|
|
- (void)spiceConnected:(CSConnection *)connection {
|
|
NSAssert(connection == self.spiceConnection, @"Unknown connection");
|
|
self.isConnected = YES;
|
|
#if !defined(WITH_QEMU_TCI)
|
|
self.primaryUsbManager = connection.usbManager;
|
|
[self.delegate spiceDidChangeUsbManager:connection.usbManager];
|
|
#endif
|
|
}
|
|
|
|
- (void)spiceInputAvailable:(CSConnection *)connection input:(CSInput *)input {
|
|
if (self.primaryInput == nil) {
|
|
self.primaryInput = input;
|
|
[self.delegate spiceDidCreateInput:input];
|
|
}
|
|
}
|
|
|
|
- (void)spiceInputUnavailable:(CSConnection *)connection input:(CSInput *)input {
|
|
if (self.primaryInput == input) {
|
|
self.primaryInput = nil;
|
|
[self.delegate spiceDidDestroyInput:input];
|
|
}
|
|
}
|
|
|
|
- (void)spiceDisconnected:(CSConnection *)connection {
|
|
NSAssert(connection == self.spiceConnection, @"Unknown connection");
|
|
self.isConnected = NO;
|
|
}
|
|
|
|
- (void)spiceError:(CSConnection *)connection code:(CSConnectionError)code message:(nullable NSString *)message {
|
|
NSAssert(connection == self.spiceConnection, @"Unknown connection");
|
|
self.isConnected = NO;
|
|
[self.connectDelegate qemuInterface:self didErrorWithMessage:message];
|
|
}
|
|
|
|
- (void)spiceDisplayCreated:(CSConnection *)connection display:(CSDisplay *)display {
|
|
NSAssert(connection == self.spiceConnection, @"Unknown connection");
|
|
if (display.isPrimaryDisplay) {
|
|
self.primaryDisplay = display;
|
|
}
|
|
[self.mutableDisplays addObject:display];
|
|
[self.delegate spiceDidCreateDisplay:display];
|
|
}
|
|
|
|
- (void)spiceDisplayUpdated:(CSConnection *)connection display:(CSDisplay *)display {
|
|
NSAssert(connection == self.spiceConnection, @"Unknown connection");
|
|
[self.delegate spiceDidUpdateDisplay:display];
|
|
}
|
|
|
|
- (void)spiceDisplayDestroyed:(CSConnection *)connection display:(CSDisplay *)display {
|
|
NSAssert(connection == self.spiceConnection, @"Unknown connection");
|
|
[self.mutableDisplays removeObject:display];
|
|
[self.delegate spiceDidDestroyDisplay:display];
|
|
}
|
|
|
|
- (void)spiceAgentConnected:(CSConnection *)connection supportingFeatures:(CSConnectionAgentFeature)features {
|
|
self.dynamicResolutionSupported = (features & kCSConnectionAgentFeatureMonitorsConfig) != kCSConnectionAgentFeatureNone;
|
|
}
|
|
|
|
- (void)spiceAgentDisconnected:(CSConnection *)connection {
|
|
self.dynamicResolutionSupported = NO;
|
|
}
|
|
|
|
- (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port {
|
|
if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) {
|
|
UTMQemuPort *qemuPort = [[UTMQemuPort alloc] initFrom:port];
|
|
[self.connectDelegate qemuInterface:self didCreateMonitorPort:qemuPort];
|
|
}
|
|
if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
|
|
UTMQemuPort *qemuPort = [[UTMQemuPort alloc] initFrom:port];
|
|
[self.connectDelegate qemuInterface:self didCreateGuestAgentPort:qemuPort];
|
|
}
|
|
if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
|
|
self.primarySerial = port;
|
|
}
|
|
if ([port.name hasPrefix:@"com.utmapp.terminal."]) {
|
|
[self.mutableSerials addObject:port];
|
|
[self.delegate spiceDidCreateSerial: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"]) {
|
|
}
|
|
if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
|
|
self.primarySerial = port;
|
|
}
|
|
if ([port.name hasPrefix:@"com.utmapp.terminal."]) {
|
|
[self.mutableSerials removeObject:port];
|
|
[self.delegate spiceDidDestroySerial:port];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Shared Directory
|
|
|
|
- (void)changeSharedDirectory:(NSURL *)url {
|
|
if (self.sharedDirectory) {
|
|
[self endSharingDirectory];
|
|
}
|
|
self.sharedDirectory = url;
|
|
[self startSharingDirectory];
|
|
}
|
|
|
|
- (void)startSharingDirectory {
|
|
if (self.sharedDirectory) {
|
|
UTMLog(@"setting share directory to %@", self.sharedDirectory.path);
|
|
[self.sharedDirectory startAccessingSecurityScopedResource];
|
|
[self.spiceConnection.session setSharedDirectory:self.sharedDirectory.path readOnly:(self.options & UTMSpiceIOOptionsIsShareReadOnly) == UTMSpiceIOOptionsIsShareReadOnly];
|
|
}
|
|
}
|
|
|
|
- (void)endSharingDirectory {
|
|
if (self.sharedDirectory) {
|
|
[self.sharedDirectory stopAccessingSecurityScopedResource];
|
|
self.sharedDirectory = nil;
|
|
UTMLog(@"ended share directory sharing");
|
|
}
|
|
}
|
|
|
|
#pragma mark - Properties
|
|
|
|
- (void)setDelegate:(id<UTMSpiceIODelegate>)delegate {
|
|
_delegate = delegate;
|
|
// make sure to send initial data
|
|
if (self.primaryInput) {
|
|
[self.delegate spiceDidCreateInput:self.primaryInput];
|
|
}
|
|
if (self.primaryDisplay) {
|
|
[self.delegate spiceDidCreateDisplay:self.primaryDisplay];
|
|
}
|
|
if (self.primarySerial) {
|
|
[self.delegate spiceDidCreateSerial:self.primarySerial];
|
|
}
|
|
#if !defined(WITH_QEMU_TCI)
|
|
if (self.primaryUsbManager) {
|
|
[self.delegate spiceDidChangeUsbManager:self.primaryUsbManager];
|
|
}
|
|
#endif
|
|
if ([self.delegate respondsToSelector:@selector(spiceDynamicResolutionSupportDidChange:)]) {
|
|
[self.delegate spiceDynamicResolutionSupportDidChange:self.dynamicResolutionSupported];
|
|
}
|
|
for (CSDisplay *display in self.mutableDisplays) {
|
|
if (display != self.primaryDisplay) {
|
|
[self.delegate spiceDidCreateDisplay:display];
|
|
}
|
|
}
|
|
for (CSPort *port in self.mutableSerials) {
|
|
if (port != self.primarySerial) {
|
|
[self.delegate spiceDidCreateSerial:port];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)setDynamicResolutionSupported:(BOOL)dynamicResolutionSupported {
|
|
if (_dynamicResolutionSupported != dynamicResolutionSupported) {
|
|
if ([self.delegate respondsToSelector:@selector(spiceDynamicResolutionSupportDidChange:)]) {
|
|
[self.delegate spiceDynamicResolutionSupportDidChange:dynamicResolutionSupported];
|
|
}
|
|
}
|
|
_dynamicResolutionSupported = dynamicResolutionSupported;
|
|
}
|
|
|
|
@end
|