Files
UTM/Services/UTMQemu.m
T
2023-06-28 20:02:38 -05:00

312 lines
10 KiB
Objective-C

//
// Copyright © 2019 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 "UTMQemu.h"
#import "UTMLogging.h"
#import "QEMUHelperDelegate.h"
#import <dlfcn.h>
#import <pthread.h>
#import <TargetConditionals.h>
extern NSString *const kUTMErrorDomain;
@interface UTMQemu ()
@property (nonatomic) dispatch_queue_t completionQueue;
@end
@implementation UTMQemu {
NSMutableArray<NSString *> *_argv;
NSMutableArray<NSURL *> *_urls;
NSXPCConnection *_connection;
}
#pragma mark - Properties
@synthesize argv = _argv;
- (NSURL *)libraryURL {
NSURL *bundleURL = [[NSBundle mainBundle] bundleURL];
NSURL *contentsURL = [bundleURL URLByAppendingPathComponent:@"Contents" isDirectory:YES];
NSURL *frameworksURL = [contentsURL URLByAppendingPathComponent:@"Frameworks" isDirectory:YES];
return frameworksURL;
}
- (BOOL)hasRemoteProcess {
return _connection != nil;
}
#pragma mark - Construction
- (instancetype)init {
return [self initWithArguments:[NSArray<NSString *> array]];
}
- (instancetype)initWithArguments:(NSArray<NSString *> *)arguments {
if (self = [super init]) {
_argv = [arguments mutableCopy];
_urls = [NSMutableArray<NSURL *> array];
if (![self setupXpc]) {
return nil;
}
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, QOS_MIN_RELATIVE_PRIORITY);
self.completionQueue = dispatch_queue_create("QEMU Completion Queue", attr);
}
return self;
}
- (void)dealloc {
[self stopQemu];
}
#pragma mark - Methods
- (BOOL)setupXpc {
#if TARGET_OS_IPHONE
return YES;
#else // only supported on macOS
NSString *helperIdentifier = NSBundle.mainBundle.infoDictionary[@"HelperIdentifier"];
if (!helperIdentifier) {
helperIdentifier = @"com.utmapp.QEMUHelper";
}
_connection = [[NSXPCConnection alloc] initWithServiceName:helperIdentifier];
_connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(QEMUHelperProtocol)];
_connection.exportedInterface = [NSXPCInterface interfaceWithProtocol:@protocol(QEMUHelperDelegate)];
_connection.exportedObject = self;
[_connection resume];
return _connection != nil;
#endif
}
- (void)pushArgv:(nullable NSString *)arg {
NSAssert(arg, @"Cannot push null argument!");
[_argv addObject:arg];
}
- (void)clearArgv {
[_argv removeAllObjects];
}
- (void)printArgv {
NSString *args = @"";
for (NSString *arg in _argv) {
if ([arg containsString:@" "]) {
args = [args stringByAppendingFormat:@" \"%@\"", arg];
} else {
args = [args stringByAppendingFormat:@" %@", arg];
}
}
NSString *line = [[NSString alloc] initWithFormat:@"Running: %@\n", args];
[self.logging writeLine:line];
NSLog(@"%@", line);
}
- (BOOL)didLoadDylib:(void *)handle {
return YES;
}
- (void)startDylibThread:(nonnull NSString *)dylib completion:(nonnull void (^)(NSError * _Nullable))completion {
void *dlctx;
__block pthread_t qemu_thread;
pthread_attr_t qosAttribute;
__weak typeof(self) wself = self;
NSAssert(self.entry != NULL, @"entry is NULL!");
self.status = self.fatal = 0;
self.done = dispatch_semaphore_create(0);
UTMLog(@"Loading %@", dylib);
dlctx = dlopen([dylib UTF8String], RTLD_LOCAL);
if (dlctx == NULL) {
NSString *err = [NSString stringWithUTF8String:dlerror()];
completion([self errorWithMessage:err]);
return;
}
if (![self didLoadDylib:dlctx]) {
NSString *err = [NSString stringWithUTF8String:dlerror()];
dlclose(dlctx);
completion([self errorWithMessage:err]);
return;
}
if (atexit_b(^{
if (pthread_self() == qemu_thread) {
__strong typeof(self) sself = wself;
if (sself) {
sself->_fatal = 1;
dispatch_semaphore_signal(sself->_done);
}
pthread_exit(NULL);
}
}) != 0) {
completion([self errorWithMessage:NSLocalizedString(@"Internal error has occurred.", @"UTMQemu")]);
return;
}
pthread_attr_init(&qosAttribute);
pthread_attr_set_qos_class_np(&qosAttribute, QOS_CLASS_USER_INTERACTIVE, 0);
pthread_create(&qemu_thread, &qosAttribute, self.entry, (__bridge_retained void *)self);
dispatch_async(self.completionQueue, ^{
if (dispatch_semaphore_wait(self.done, DISPATCH_TIME_FOREVER)) {
dlclose(dlctx);
[self qemuHasExited:-1 message:NSLocalizedString(@"Internal error has occurred.", @"UTMQemu")];
} else {
if (dlclose(dlctx) < 0) {
NSString *err = [NSString stringWithUTF8String:dlerror()];
[self qemuHasExited:-1 message:err];
} else if (self.fatal || self.status) {
[self qemuHasExited:-1 message:nil];
} else {
[self qemuHasExited:0 message:nil];
}
}
});
completion(nil);
}
- (void)startQemuRemote:(nonnull NSString *)name completion:(nonnull void (^)(NSError * _Nullable))completion {
NSError *error;
NSData *libBookmark = [self.libraryURL bookmarkDataWithOptions:0
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&error];
if (!libBookmark) {
completion(error);
return;
}
__weak typeof(self) _self = self;
NSFileHandle *standardOutput = self.logging.standardOutput.fileHandleForWriting;
NSFileHandle *standardError = self.logging.standardError.fileHandleForWriting;
[_connection.remoteObjectProxy setEnvironment:self.environment];
[[_connection remoteObjectProxyWithErrorHandler:^(NSError * _Nonnull error) {
if (error.domain == NSCocoaErrorDomain && error.code == NSXPCConnectionInvalid) {
// 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 {
completion(nil);
}
}];
}
- (void)startQemu:(nonnull NSString *)name completion:(nonnull void (^)(NSError * _Nullable))completion {
[self printArgv];
#if TARGET_OS_IPHONE
NSString *base = @"";
#else
NSString *base = @"Versions/A/";
#endif
NSString *dylib = [NSString stringWithFormat:@"%@.framework/%@%@", name, base, name];
if (_connection) {
[self startQemuRemote:dylib completion:completion];
} else {
[self startDylibThread:dylib completion:completion];
}
}
- (void)stopQemu {
if (_connection) {
[[_connection remoteObjectProxy] terminate];
[_connection invalidate];
}
for (NSURL *url in _urls) {
[url stopAccessingSecurityScopedResource];
}
}
- (void)accessDataWithBookmarkThread:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion {
BOOL stale = NO;
NSError *err;
NSURL *url = [NSURL URLByResolvingBookmarkData:bookmark
options:0
relativeToURL:nil
bookmarkDataIsStale:&stale
error:&err];
if (!url) {
UTMLog(@"Failed to access bookmark data.");
completion(NO, nil, nil);
return;
}
if (stale || !securityScoped) {
bookmark = [url bookmarkDataWithOptions:NSURLBookmarkCreationMinimalBookmark
includingResourceValuesForKeys:nil
relativeToURL:nil
error:&err];
if (!bookmark) {
UTMLog(@"Failed to create new bookmark!");
completion(NO, bookmark, url.path);
return;
}
}
if ([url startAccessingSecurityScopedResource]) {
[_urls addObject:url];
} else {
UTMLog(@"Failed to access security scoped resource for: %@", url);
}
completion(YES, bookmark, url.path);
}
- (void)accessDataWithBookmark:(NSData *)bookmark {
[self accessDataWithBookmark:bookmark securityScoped:NO completion:^(BOOL success, NSData *bookmark, NSString *path) {
if (!success) {
UTMLog(@"Access bookmark failed for: %@", path);
}
}];
}
- (void)accessDataWithBookmark:(NSData *)bookmark securityScoped:(BOOL)securityScoped completion:(void(^)(BOOL, NSData * _Nullable, NSString * _Nullable))completion {
if (_connection) {
[[_connection remoteObjectProxy] accessDataWithBookmark:bookmark securityScoped:securityScoped completion:completion];
} else {
[self accessDataWithBookmarkThread:bookmark securityScoped:securityScoped completion:completion];
}
}
- (void)stopAccessingPathThread:(nullable NSString *)path {
if (!path) {
return;
}
for (NSURL *url in _urls) {
if ([url.path isEqualToString:path]) {
[url stopAccessingSecurityScopedResource];
[_urls removeObject:url];
return;
}
}
UTMLog(@"Cannot find '%@' in existing scoped access.", path);
}
- (void)stopAccessingPath:(nullable NSString *)path {
if (_connection) {
[[_connection remoteObjectProxy] stopAccessingPath:path];
} else {
[self stopAccessingPathThread:path];
}
}
- (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