mirror of
https://github.com/utmapp/UTM.git
synced 2026-10-12 02:24:01 +00:00
312 lines
10 KiB
Objective-C
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
|