/** * @file llappdelegate-objc.mm * @brief Class implementation for the Mac version's application delegate. * * $LicenseInfo:firstyear=2000&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #import "llappdelegate-objc.h" #if defined(LL_BUGSPLAT) #include <boost/filesystem.hpp> #include <vector> @import BugsplatMac; // derived from BugsplatMac's BugsplatTester/AppDelegate.m @interface LLAppDelegate () <BugsplatStartupManagerDelegate> @end #endif #include "llwindowmacosx-objc.h" #include "llappviewermacosx-for-objc.h" #include <Carbon/Carbon.h> // Used for Text Input Services ("Safe" API - it's supported) @implementation LLAppDelegate @synthesize window; @synthesize inputWindow; @synthesize inputView; @synthesize currentInputLanguage; - (void)dealloc { [currentInputLanguage release]; [super dealloc]; } - (void) applicationWillFinishLaunching:(NSNotification *)notification { [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; } - (void) applicationDidFinishLaunching:(NSNotification *)notification { // Call constructViewer() first so our logging subsystem is in place. This // risks missing crashes in the LLAppViewerMacOSX constructor, but for // present purposes it's more important to get the startup sequence // properly logged. // Someday I would like to modify the logging system so that calls before // it's initialized are cached in a std::ostringstream and then, once it's // initialized, "played back" into whatever handlers have been set up. constructViewer(); #if defined(LL_BUGSPLAT) infos("bugsplat setup"); // Engage BugsplatStartupManager *before* calling initViewer() to handle // any crashes during initialization. // https://www.bugsplat.com/docs/platforms/os-x#initialization [BugsplatStartupManager sharedManager].autoSubmitCrashReport = YES; [BugsplatStartupManager sharedManager].askUserDetails = NO; [BugsplatStartupManager sharedManager].delegate = self; [[BugsplatStartupManager sharedManager] start]; #endif infos("post-bugsplat setup"); frameTimer = nil; [self languageUpdated]; if (initViewer()) { // Set up recurring calls to oneFrame (repeating timer with timeout 0) // until applicationShouldTerminate. frameTimer = [NSTimer scheduledTimerWithTimeInterval:0.0 target:self selector:@selector(oneFrame) userInfo:nil repeats:YES]; } else { exit(0); } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(languageUpdated) name:@"NSTextInputContextKeyboardSelectionDidChangeNotification" object:nil]; // [[NSAppleEventManager sharedAppleEventManager] setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL]; } - (void) handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { NSString *url= nil; url = [[[[NSAppleEventManager sharedAppleEventManager]// 1 currentAppleEvent]// 2 paramDescriptorForKeyword:keyDirectObject]// 3 stringValue];// 4 const char* url_utf8 = [url UTF8String]; handleUrl(url_utf8); } - (void) applicationDidBecomeActive:(NSNotification *)notification { callWindowFocus(); } - (void) applicationDidResignActive:(NSNotification *)notification { callWindowUnfocus(); } - (void) applicationDidHide:(NSNotification *)notification { callWindowHide(); } - (void) applicationDidUnhide:(NSNotification *)notification { callWindowUnhide(); } - (NSApplicationTerminateReply) applicationShouldTerminate:(NSApplication *)sender { // run one frame to assess state if (!pumpMainLoop()) { // pumpMainLoop() returns true when done, false if it wants to be // called again. Since it returned false, do not yet cancel // frameTimer. handleQuit(); [[NSApplication sharedApplication] stopModal]; return NSTerminateCancel; } else { // pumpMainLoop() returned true: it's done. Okay, done with frameTimer. [frameTimer release]; cleanupViewer(); return NSTerminateNow; } } - (void) oneFrame { bool appExiting = pumpMainLoop(); if (appExiting) { // Once pumpMainLoop() reports that we're done, cancel frameTimer: // stop the repetitive calls. [frameTimer release]; [[NSApplication sharedApplication] terminate:self]; } } - (void) showInputWindow:(bool)show withEvent:(NSEvent*)textEvent { if (![self romanScript]) { if (show) { NSLog(@"Showing input window."); [inputWindow makeKeyAndOrderFront:inputWindow]; if (textEvent != nil) { [[inputView inputContext] discardMarkedText]; [[inputView inputContext] handleEvent:textEvent]; } } else { NSLog(@"Hiding input window."); [inputWindow orderOut:inputWindow]; [window makeKeyAndOrderFront:window]; } } } // This will get called multiple times by NSNotificationCenter. // It will be called every time that the window focus changes, and every time that the input language gets changed. // The primary use case for this selector is to update our current input language when the user, for whatever reason, changes the input language. // This is the more elegant way of handling input language changes instead of checking every time we want to use the input window. - (void) languageUpdated { TISInputSourceRef currentInput = TISCopyCurrentKeyboardInputSource(); CFArrayRef languages = (CFArrayRef)TISGetInputSourceProperty(currentInput, kTISPropertyInputSourceLanguages); #if 0 // In the event of ever needing to add new language sources, change this to 1 and watch the terminal for "languages:" NSLog(@"languages: %@", TISGetInputSourceProperty(currentInput, kTISPropertyInputSourceLanguages)); #endif // Typically the language we want is going to be the very first result in the array. currentInputLanguage = (NSString*)CFArrayGetValueAtIndex(languages, 0); } - (bool) romanScript { @autoreleasepool { // How to add support for new languages with the input window: // Simply append this array with the language code (ja for japanese, ko for korean, zh for chinese, etc.) NSArray* nonRomanScript = @[@"ja", @"ko", @"zh-Hant", @"zh-Hans"]; if ([nonRomanScript containsObject:currentInputLanguage]) { return false; } } return true; } #if defined(LL_BUGSPLAT) - (NSString *)applicationLogForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager { CrashMetadata& meta(CrashMetadata_instance()); // As of BugsplatMac 1.0.6, userName and userEmail properties are now // exposed by the BugsplatStartupManager. Set them here, since the // defaultUserNameForBugsplatStartupManager and // defaultUserEmailForBugsplatStartupManager methods are called later, for // the *current* run, rather than for the previous crashed run whose crash // report we are about to send. infos("applicationLogForBugsplatStartupManager setting userName = '" + meta.agentFullname + '"'); bugsplatStartupManager.userName = [NSString stringWithCString:meta.agentFullname.c_str() encoding:NSUTF8StringEncoding]; // Use the email field for OS version, just as we do on Windows, until // BugSplat provides more metadata fields. infos("applicationLogForBugsplatStartupManager setting userEmail = '" + meta.OSInfo + '"'); bugsplatStartupManager.userEmail = [NSString stringWithCString:meta.OSInfo.c_str() encoding:NSUTF8StringEncoding]; // This strangely-named override method's return value contributes the // User Description metadata field. infos("applicationLogForBugsplatStartupManager -> '" + meta.fatalMessage + "'"); return [NSString stringWithCString:meta.fatalMessage.c_str() encoding:NSUTF8StringEncoding]; } - (NSString *)applicationKeyForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager signal:(NSString *)signal exceptionName:(NSString *)exceptionName exceptionReason:(NSString *)exceptionReason { // TODO: exceptionName, exceptionReason // Windows sends location within region as well, but that's because // BugSplat for Windows intercepts crashes during the same run, and that // information can be queried once. On the Mac, any metadata we have is // written (and rewritten) to the static_debug_info.log file that we read // at the start of the next viewer run. It seems ridiculously expensive to // rewrite that file on every frame in which the avatar moves. std::string regionName(CrashMetadata_instance().regionName); infos("applicationKeyForBugsplatStartupManager -> '" + regionName + "'"); return [NSString stringWithCString:regionName.c_str() encoding:NSUTF8StringEncoding]; } - (NSString *)defaultUserNameForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager { std::string agentFullname(CrashMetadata_instance().agentFullname); infos("defaultUserNameForBugsplatStartupManager -> '" + agentFullname + "'"); return [NSString stringWithCString:agentFullname.c_str() encoding:NSUTF8StringEncoding]; } - (NSString *)defaultUserEmailForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager { // Use the email field for OS version, just as we do on Windows, until // BugSplat provides more metadata fields. std::string OSInfo(CrashMetadata_instance().OSInfo); infos("defaultUserEmailForBugsplatStartupManager -> '" + OSInfo + "'"); return [NSString stringWithCString:OSInfo.c_str() encoding:NSUTF8StringEncoding]; } - (void)bugsplatStartupManagerWillSendCrashReport:(BugsplatStartupManager *)bugsplatStartupManager { infos("bugsplatStartupManagerWillSendCrashReport"); } struct AttachmentInfo { AttachmentInfo(const std::string& path, const std::string& type): pathname(path), basename(boost::filesystem::path(path).filename().string()), mimetype(type) {} std::string pathname, basename, mimetype; }; - (NSArray<BugsplatAttachment *> *)attachmentsForBugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager { const CrashMetadata& metadata(CrashMetadata_instance()); // Since we must do very similar processing for each of several file // pathnames, start by collecting them into a vector so we can iterate // instead of spelling out the logic for each. std::vector<AttachmentInfo> info{ AttachmentInfo(metadata.logFilePathname, "text/plain"), AttachmentInfo(metadata.userSettingsPathname, "text/xml"), AttachmentInfo(metadata.accountSettingsPathname, "text/xml"), AttachmentInfo(metadata.staticDebugPathname, "text/xml") }; secondLogPath = metadata.secondLogFilePathname; if(!secondLogPath.empty()) { info.push_back(AttachmentInfo(secondLogPath, "text/xml")); } // We "happen to know" that info[0].basename is "SecondLife.old" -- due to // the fact that BugsplatMac only notices a crash during the viewer run // following the crash. // The Bugsplat service doesn't respect the MIME type above when returning // the log data to a browser, so take this opportunity to rename the file // from <base>.old to <base>_log.txt info[0].basename = boost::filesystem::path(info[0].pathname).stem().string() + "_log.txt"; infos("attachmentsForBugsplatStartupManager attaching log " + info[0].basename); NSMutableArray *attachments = [[NSMutableArray alloc] init]; // Iterate over each AttachmentInfo in info vector for (const AttachmentInfo& attach : info) { NSString *nspathname = [NSString stringWithCString:attach.pathname.c_str() encoding:NSUTF8StringEncoding]; NSString *nsbasename = [NSString stringWithCString:attach.basename.c_str() encoding:NSUTF8StringEncoding]; NSString *nsmimetype = [NSString stringWithCString:attach.mimetype.c_str() encoding:NSUTF8StringEncoding]; NSData *nsdata = [NSData dataWithContentsOfFile:nspathname]; BugsplatAttachment *attachment = [[BugsplatAttachment alloc] initWithFilename:nsbasename attachmentData:nsdata contentType:nsmimetype]; [attachments addObject:attachment]; infos("attachmentsForBugsplatStartupManager attaching " + attach.pathname); } return attachments; } - (void)bugsplatStartupManagerDidFinishSendingCrashReport:(BugsplatStartupManager *)bugsplatStartupManager { infos("Sent crash report to BugSplat"); if(!secondLogPath.empty()) { boost::filesystem::remove(secondLogPath); } clearDumpLogsDir(); } - (void)bugsplatStartupManager:(BugsplatStartupManager *)bugsplatStartupManager didFailWithError:(NSError *)error { // TODO: message string from NSError infos("Could not send crash report to BugSplat"); } #endif // LL_BUGSPLAT @end @implementation LLApplication - (void)sendEvent:(NSEvent *)event { [super sendEvent:event]; if ([event type] == NSKeyUp && ([event modifierFlags] & NSCommandKeyMask)) { [[self keyWindow] sendEvent:event]; } } @end