Files
OpenRA/packaging/macos/launcher-mono.m
2021-01-01 19:42:01 +01:00

373 lines
11 KiB
Objective-C

/*
* 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 <Cocoa/Cocoa.h>
#include <dlfcn.h>
#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 <NSApplicationDelegate>
- (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;
}