/* * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation. For more information, * see COPYING. */ #import #include #define SYSTEM_MONO_PATH @"/Library/Frameworks/Mono.framework/Versions/Current/" #define SYSTEM_MONO_MIN_VERSION @"6.4" typedef int (* mono_main)(int argc, char **argv); typedef void (* mono_free)(void *ptr); typedef char *(* mono_get_runtime_build_info)(void); @interface OpenRALauncher : NSObject - (void)launchGameWithArgs: (NSArray *)gameArgs; @end @implementation OpenRALauncher BOOL launched = NO; NSTask *gameTask; static int check_mono_version(const char *version, const char *req_version) { char *req_end, *end; long req_val, val; while (*req_version) { req_val = strtol(req_version, &req_end, 10); if (req_version == req_end || (*req_end && *req_end != '.')) { fprintf(stderr, "Bad version requirement string '%s'\n", req_end); return FALSE; } req_version = req_end; if (*req_version) req_version++; val = strtol (version, &end, 10); if (version == end || val < req_val) return FALSE; if (val > req_val) return TRUE; if (*req_version == '.' && *end != '.') return FALSE; version = end + 1; } return TRUE; } - (int)hasValidMono { void *libmono = dlopen([[SYSTEM_MONO_PATH stringByAppendingPathComponent: @"/lib/libmonosgen-2.0.dylib"] UTF8String], RTLD_LAZY); if (libmono == NULL) { fprintf (stderr, "Failed to load libmonosgen-2.0.dylib: %s\n", dlerror()); return FALSE; } mono_main _mono_main = (mono_main)dlsym(libmono, "mono_main"); if (!_mono_main) { fprintf(stderr, "Could not load mono_main(): %s\n", dlerror()); return FALSE; } mono_free _mono_free = (mono_free)dlsym(libmono, "mono_free"); if (!_mono_free) { fprintf(stderr, "Could not load mono_free(): %s\n", dlerror()); return FALSE; } mono_get_runtime_build_info _mono_get_runtime_build_info = (mono_get_runtime_build_info)dlsym(libmono, "mono_get_runtime_build_info"); if (!_mono_get_runtime_build_info) { fprintf(stderr, "Could not load mono_get_runtime_build_info(): %s\n", dlerror()); return FALSE; } char *mono_version = _mono_get_runtime_build_info(); return check_mono_version(mono_version, [SYSTEM_MONO_MIN_VERSION UTF8String]); } - (NSString *)modName { NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; if (plist) { NSString *title = [plist objectForKey:@"CFBundleDisplayName"]; if (title && [title length] > 0) return title; } return @"OpenRA"; } - (void)exitWithMonoPrompt { [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; NSString *modName = [self modName]; NSString *title = [NSString stringWithFormat: @"Cannot launch %@", modName]; NSString *message = [NSString stringWithFormat: @"%@ requires Mono %@ or later. Please install Mono and try again.", modName, SYSTEM_MONO_MIN_VERSION]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:title]; [alert setInformativeText:message]; [alert addButtonWithTitle:@"Download Mono"]; [alert addButtonWithTitle:@"Quit"]; NSInteger answer = [alert runModal]; [alert release]; if (answer == NSAlertFirstButtonReturn) [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:@"https://www.mono-project.com/download/"]]; exit(1); } - (void)exitWithCrashPrompt { [NSApp setActivationPolicy: NSApplicationActivationPolicyRegular]; [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; NSString *modName = [self modName]; NSString *message = [NSString stringWithFormat: @"%@ has encountered a fatal error and must close.\nPlease refer to the crash logs and FAQ for more information.", modName]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:@"Fatal Error"]; [alert setInformativeText:message]; [alert addButtonWithTitle:@"View Logs"]; [alert addButtonWithTitle:@"View FAQ"]; [alert addButtonWithTitle:@"Quit"]; NSInteger answer = [alert runModal]; [alert release]; if (answer == NSAlertFirstButtonReturn) { NSString *logDir = [@"~/Library/Application Support/OpenRA/Logs/" stringByExpandingTildeInPath]; [[NSWorkspace sharedWorkspace] openFile: logDir withApplication:@"Finder"]; } else if (answer == NSAlertSecondButtonReturn) { NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; if (plist) { NSString *faqUrl = [plist objectForKey:@"FaqUrl"]; if (faqUrl && [faqUrl length] > 0) [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:faqUrl]]; } } exit(1); } // Application was launched via a URL handler - (void)getUrl:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSMutableArray *gameArgs = [[[NSProcessInfo processInfo] arguments] mutableCopy]; NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; if (plist) { NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"]; if (joinServerUrl && [joinServerUrl length] > 0) { NSString *prefix = [joinServerUrl stringByAppendingString: @"://"]; if ([url hasPrefix: prefix]) { NSString *trimmed = [url substringFromIndex:[prefix length]]; NSArray *parts = [trimmed componentsSeparatedByString:@":"]; NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init]; if ([parts count] == 2 && [formatter numberFromString: [parts objectAtIndex:1]] != nil) [gameArgs addObject: [NSString stringWithFormat: @"Launch.Connect=%@", trimmed]]; [formatter release]; } } } [self launchGameWithArgs: gameArgs]; [gameArgs release]; } - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { // Register for url events NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; if (plist) { NSString *joinServerUrl = [plist objectForKey:@"JoinServerUrlScheme"]; NSString *bundleIdentifier = [plist objectForKey:@"CFBundleIdentifier"]; if (joinServerUrl && [joinServerUrl length] > 0 && bundleIdentifier) { LSSetDefaultHandlerForURLScheme((CFStringRef)joinServerUrl, (CFStringRef)bundleIdentifier); [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(getUrl:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; } } } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { [self launchGameWithArgs: [[NSProcessInfo processInfo] arguments]]; } - (BOOL)applicationShouldTerminateAfterLastWindowClosed: (NSApplication *)theApplication { return YES; } - (void)launchGameWithArgs: (NSArray *)gameArgs { if (launched) { NSLog(@"launchgame is already running... ignoring request."); return; } launched = YES; if (![self hasValidMono]) [self exitWithMonoPrompt]; // Default values - can be overriden by setting certain keys Info.plist NSString *gameName = @"OpenRA.dll"; NSString *modId = nil; NSDictionary *plist = [[NSBundle mainBundle] infoDictionary]; if (plist) { NSString *exeValue = [plist objectForKey:@"MonoGameExe"]; if (exeValue && [exeValue length] > 0) gameName = exeValue; NSString *modIdValue = [plist objectForKey:@"ModId"]; if (modIdValue && [modIdValue length] > 0) modId = modIdValue; } NSString *exePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/MacOS/"]; NSString *gamePath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent: @"Contents/Resources/"]; NSString *launchPath = [SYSTEM_MONO_PATH stringByAppendingPathComponent: @"Commands/mono"]; NSString *appPath = [exePath stringByAppendingPathComponent: @"OpenRA"]; NSString *engineLaunchPath = [self resolveTranslocatedPath: appPath]; NSMutableArray *launchArgs = [NSMutableArray arrayWithCapacity: [gameArgs count] + 2]; [launchArgs addObject: @"--debug"]; [launchArgs addObject: [gamePath stringByAppendingPathComponent: gameName]]; [launchArgs addObject: [NSString stringWithFormat:@"Engine.LaunchPath=\"%@\"", engineLaunchPath]]; if (modId) [launchArgs addObject: [NSString stringWithFormat:@"Game.Mod=%@", modId]]; [launchArgs addObjectsFromArray: gameArgs]; NSLog(@"Running launchgame with arguments:"); for (size_t i = 0; i < [launchArgs count]; i++) NSLog(@"%@", [launchArgs objectAtIndex: i]); gameTask = [[NSTask alloc] init]; [gameTask setCurrentDirectoryPath: gamePath]; [gameTask setLaunchPath: launchPath]; [gameTask setArguments: launchArgs]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(taskExited:) name: NSTaskDidTerminateNotification object: gameTask ]; [gameTask launch]; } - (NSString *)resolveTranslocatedPath: (NSString *)path { // macOS 10.12 introduced the "App Translocation" feature, which runs quarantined applications // from a transient read-only disk image. The read-only image isn't a problem, but the transient // path breaks the mod registration/switching feature. // This resolves the original path which can then be written into the mod metadata for future // launches (which will then be re-translocated) // Running on macOS < 10.12 if (floor(NSAppKitVersionNumber) <= 1404) return path; void *handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY); // Failed to load security framework if (handle == NULL) return path; Boolean (*mySecTranslocateIsTranslocatedURL)(CFURLRef path, bool *isTranslocated, CFErrorRef * __nullable error); mySecTranslocateIsTranslocatedURL = dlsym(handle, "SecTranslocateIsTranslocatedURL"); CFURLRef __nullable (*mySecTranslocateCreateOriginalPathForURL)(CFURLRef translocatedPath, CFErrorRef * __nullable error); mySecTranslocateCreateOriginalPathForURL = dlsym(handle, "SecTranslocateCreateOriginalPathForURL"); // Failed to resolve required functions if (mySecTranslocateIsTranslocatedURL == NULL || mySecTranslocateCreateOriginalPathForURL == NULL) return path; bool isTranslocated = false; CFURLRef pathURLRef = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, (__bridge CFStringRef)path, kCFURLPOSIXPathStyle, false); if (mySecTranslocateIsTranslocatedURL(pathURLRef, &isTranslocated, NULL)) { if (isTranslocated) { CFURLRef resolvedURL = mySecTranslocateCreateOriginalPathForURL(pathURLRef, NULL); path = [(NSURL *)(resolvedURL) path]; } } CFRelease(pathURLRef); return path; } - (void)taskExited:(NSNotification *)note { [[NSNotificationCenter defaultCenter] removeObserver:self name:NSTaskDidTerminateNotification object:gameTask ]; int ret = [gameTask terminationStatus]; NSLog(@"launchgame exited with code %d", ret); [gameTask release]; gameTask = nil; // We're done here if (ret != 0) [self exitWithCrashPrompt]; exit(0); } @end int main(int argc, char **argv) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSApplication *application = [NSApplication sharedApplication]; OpenRALauncher *launcher = [[OpenRALauncher alloc] init]; [NSApp setActivationPolicy: NSApplicationActivationPolicyProhibited]; [application setDelegate:launcher]; [application run]; [launcher release]; [pool drain]; return EXIT_SUCCESS; }