/* * This file is part of nzbget. See . * * Copyright (C) 2007-2016 Andrey Prygunkov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #import "MainApp.h" #import "PreferencesDialog.h" #import "WelcomeDialog.h" #import "PFMoveApplication.h" NSString *PreferencesContext = @"PreferencesContext"; const NSTimeInterval NORMAL_UPDATE_INTERVAL = 10.000; const NSTimeInterval MENUOPEN_UPDATE_INTERVAL = 1.000; const NSTimeInterval START_UPDATE_INTERVAL = 0.500; int main(int argc, char *argv[]) { return NSApplicationMain(argc, (const char **)argv); } /* * Signal handler */ void SignalProc(int iSignal) { switch (iSignal) { case SIGINT: case SIGTERM: signal(iSignal, SIG_DFL); // Reset the signal handler [NSApp terminate:nil]; break; } } // we install seignal handler in order to properly terminat app from Activity Mo1nitor void InstallSignalHandlers() { signal(SIGINT, SignalProc); signal(SIGTERM, SignalProc); signal(SIGPIPE, SIG_IGN); } @implementation MainApp - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { [self checkOtherRunningInstances]; #ifndef DEBUG PFMoveToApplicationsFolderIfNecessary(); #endif } - (void)checkOtherRunningInstances { for (NSRunningApplication *runningApplication in [NSRunningApplication runningApplicationsWithBundleIdentifier:[[NSBundle mainBundle] bundleIdentifier]]) { if (![[runningApplication executableURL] isEqualTo:[[NSRunningApplication currentApplication] executableURL]]) { NSString *executablePath = [[runningApplication executableURL] path]; executablePath = [[[executablePath stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent]; DLog(@"Switching to an already running instance: %@", executablePath); [[NSTask launchedTaskWithLaunchPath:@"/usr/bin/open" arguments:[NSArray arrayWithObjects:executablePath, @"--args", @"--second-instance", nil]] waitUntilExit]; exit(0); } } } - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { BOOL autoStartWebUI = [[NSUserDefaults standardUserDefaults] boolForKey:@"AutoStartWebUI"]; daemonController = [[DaemonController alloc] init]; daemonController.updateInterval = autoStartWebUI ? START_UPDATE_INTERVAL : NORMAL_UPDATE_INTERVAL; daemonController.delegate = self; [self setupDefaultsObserver]; [self userDefaultsDidChange:nil]; if (![MainApp wasLaunchedAsLoginItem]) { [self showWelcomeScreen]; } InstallSignalHandlers(); DLog(@"Start Daemon"); [daemonController start]; } - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication hasVisibleWindows:(BOOL)flag { DLog(@"applicationShouldHandleReopen"); [self showAlreadyRunning]; return YES; } + (void)initialize { [self setupAppDefaults]; } - (void)awakeFromNib { DLog(@"awakeFromNib"); [statusMenu setDelegate:self]; } + (void)setupAppDefaults { NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; NSDictionary *appDefaults = [NSDictionary dictionaryWithObjectsAndKeys: @"YES", @"ShowInMenubar", @"NO", @"Autostart", @"YES", @"AutoStartWebUI", nil]; [userDefaults registerDefaults:appDefaults]; } - (void)setupDefaultsObserver { NSUserDefaultsController *sdc = [NSUserDefaultsController sharedUserDefaultsController]; [sdc addObserver:self forKeyPath:@"values.ShowInMenubar" options:0 context:(__bridge void *)(PreferencesContext)]; [sdc addObserver:self forKeyPath:@"values.AutoStartWebUI" options:0 context:(__bridge void *)(PreferencesContext)]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == (__bridge void *)(PreferencesContext)) { [self userDefaultsDidChange:nil]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)userDefaultsDidChange:(id)sender { DLog(@"userDefaultsDidChange: %@", sender); BOOL showInMenubar = [[NSUserDefaults standardUserDefaults] boolForKey:@"ShowInMenubar"]; if (showInMenubar != (statusItem != nil)) { if (showInMenubar) { statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength]; [statusItem setHighlightMode:YES]; [statusItem setMenu:statusMenu]; NSImage* icon = [NSImage imageNamed:@"statusicon.png"]; [icon setTemplate:YES]; [statusItem setImage:icon]; } else { statusItem = nil; } } } - (IBAction)preferencesClicked:(id)sender { [self showPreferences]; } - (void)showPreferences { [NSApp activateIgnoringOtherApps:TRUE]; if (preferencesDialog) { return; } preferencesDialog = [[PreferencesDialog alloc] init]; [[preferencesDialog window] center]; [preferencesDialog showWindow:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(preferencesWillClose:) name:NSWindowWillCloseNotification object:[preferencesDialog window]]; } - (void)preferencesWillClose:(NSNotification *)notification { DLog(@"Pref Closed"); [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:[preferencesDialog window]]; preferencesDialog = nil; [self userDefaultsDidChange:nil]; } - (IBAction)aboutClicked:(id)sender { [NSApp activateIgnoringOtherApps:TRUE]; [NSApp orderFrontStandardAboutPanel:nil]; } - (IBAction)quitClicked:(id)sender { DLog(@"Quit"); [NSApp terminate:nil]; } - (void)showWelcomeScreen { welcomeDialog = [[WelcomeDialog alloc] init]; [(WelcomeDialog*)welcomeDialog setMainDelegate:self]; [(WelcomeDialog*)welcomeDialog showDialog]; } - (void)showAlreadyRunning { BOOL showInMenubar = [[NSUserDefaults standardUserDefaults] boolForKey:@"ShowInMenubar"]; [NSApp activateIgnoringOtherApps:TRUE]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"AlreadyRunning.MessageText", nil)]; [alert setInformativeText:NSLocalizedString(showInMenubar ? @"AlreadyRunning.InformativeTextWithIcon" : @"AlreadyRunning.InformativeTextWithoutIcon", nil)]; NSButton* webUIButton = [alert addButtonWithTitle:NSLocalizedString(@"AlreadyRunning.WebUI", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"AlreadyRunning.Preferences", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"AlreadyRunning.Quit", nil)]; [alert.window makeFirstResponder:webUIButton]; [webUIButton setKeyEquivalent:@"\r"]; switch ([alert runModal]) { case NSAlertFirstButtonReturn: [self showWebUI]; break; case NSAlertSecondButtonReturn: [self showPreferences]; break; case NSAlertThirdButtonReturn: [self quitClicked:nil]; break; } } + (BOOL)wasLaunchedByProcess:(NSString*)creator { BOOL wasLaunchedByProcess = NO; // Get our PSN OSStatus err; ProcessSerialNumber currPSN; err = GetCurrentProcess (&currPSN); if (!err) { // We don't use ProcessInformationCopyDictionary() because the 'ParentPSN' item in the dictionary // has endianness problems in 10.4, fixed in 10.5 however. ProcessInfoRec procInfo; bzero (&procInfo, sizeof (procInfo)); procInfo.processInfoLength = (UInt32)sizeof (ProcessInfoRec); err = GetProcessInformation (&currPSN, &procInfo); if (!err) { ProcessSerialNumber parentPSN = procInfo.processLauncher; // Get info on the launching process NSDictionary* parentDict = (__bridge NSDictionary*)ProcessInformationCopyDictionary (&parentPSN, kProcessDictionaryIncludeAllInformationMask); // Test the creator code of the launching app if (parentDict) { wasLaunchedByProcess = [[parentDict objectForKey:@"FileCreator"] isEqualToString:creator]; } } } return wasLaunchedByProcess; } + (BOOL)wasLaunchedAsLoginItem { // If the launching process was 'loginwindow', we were launched as a login item return [self wasLaunchedByProcess:@"lgnw"]; } - (IBAction)webuiClicked:(id)sender { if (daemonController.connected) { [self showWebUI]; } else { [NSApp activateIgnoringOtherApps:TRUE]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"ShowWebUINoConnection.MessageText", nil)]; [alert setInformativeText:NSLocalizedString(@"ShowWebUINoConnection.InformativeText", nil)]; [alert setAlertStyle:NSWarningAlertStyle]; [alert runModal]; } } - (void)showWebUI { DLog(@"showWebUI"); [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:daemonController.browserUrl]]; } - (IBAction)openConfigInTextEditClicked:(id)sender { NSString *configFile = [daemonController configFilePath]; [[NSWorkspace sharedWorkspace] openFile:configFile withApplication:@"TextEdit"]; } - (IBAction)restartClicked:(id)sender { if (sender == factoryResetItem) { [NSApp activateIgnoringOtherApps:TRUE]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"FactoryReset.MessageText", nil)]; [alert setInformativeText:NSLocalizedString(@"FactoryReset.InformativeText", nil)]; [alert setAlertStyle:NSCriticalAlertStyle]; NSButton* cancelButton = [alert addButtonWithTitle:NSLocalizedString(@"FactoryReset.Cancel", nil)]; // we use middle invisible button to align the third RESET-button at left side [[alert addButtonWithTitle:@"Cancel"] setHidden:YES]; [alert addButtonWithTitle:NSLocalizedString(@"FactoryReset.Reset", nil)]; [alert.window makeFirstResponder:cancelButton]; [cancelButton setKeyEquivalent:@"\E"]; if ([alert runModal] != NSAlertThirdButtonReturn) { return; } } restarting = YES; resetting = sender == factoryResetItem; [self updateStatus]; [daemonController restartInRecoveryMode: sender == restartRecoveryItem withFactoryReset: sender == factoryResetItem]; daemonController.updateInterval = START_UPDATE_INTERVAL; restartTimer = [NSTimer timerWithTimeInterval:10.000 target:self selector:@selector(restartFailed) userInfo:nil repeats:NO]; [[NSRunLoop currentRunLoop] addTimer:restartTimer forMode:NSRunLoopCommonModes]; } - (void)welcomeContinue { DLog(@"welcomeContinue"); BOOL autoStartWebUI = [[NSUserDefaults standardUserDefaults] boolForKey:@"AutoStartWebUI"]; if (autoStartWebUI) { if (daemonController.connected) { if (daemonController.updateInterval == START_UPDATE_INTERVAL) { daemonController.updateInterval = NORMAL_UPDATE_INTERVAL; } [self showWebUI]; } else { // try again in 100 msec for max. 25 seconds, then give up connectionAttempts++; if (connectionAttempts < 250) { [self performSelector:@selector(welcomeContinue) withObject:nil afterDelay: 0.100]; } else { // show error message [self webuiClicked:nil]; } } } } - (void)applicationWillTerminate:(NSNotification *)aNotification { DLog(@"Stop Daemon"); [daemonController stop]; } - (void)menuWillOpen:(NSMenu *)menu { DLog(@"menuWillOpen"); daemonController.updateInterval = MENUOPEN_UPDATE_INTERVAL; } - (void)menuDidClose:(NSMenu *)menu { DLog(@"menuDidClose"); daemonController.updateInterval = NORMAL_UPDATE_INTERVAL; } - (IBAction)infoLinkClicked:(id)sender { NSString *url; if (sender == homePageItem) { url = NSLocalizedString(@"Menu.LinkHomePage", nil); } else if (sender == downloadsItem) { url = NSLocalizedString(@"Menu.LinkDownloads", nil); } else if (sender == forumItem) { url = NSLocalizedString(@"Menu.LinkForum", nil); } [[NSWorkspace sharedWorkspace] openURL: [NSURL URLWithString:url]]; } - (void)restartFailed { if (restarting) { restarting = NO; resetting = NO; daemonController.updateInterval = NORMAL_UPDATE_INTERVAL; [NSApp activateIgnoringOtherApps:TRUE]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(@"RestartNoConnection.MessageText", nil)]; [alert setInformativeText:NSLocalizedString(@"RestartNoConnection.InformativeText", nil)]; [alert setAlertStyle:NSWarningAlertStyle]; [alert runModal]; } } - (IBAction)showInFinderClicked:(id)sender { NSString* dir = nil; NSString* option = nil; if (sender == destDirItem) { option = @"DestDir"; } else if (sender == interDirItem) { option = @"InterDir"; } else if (sender == nzbDirItem) { option = @"NzbDir"; } else if (sender == scriptDirItem) { option = @"ScriptDir"; } else if (sender == logFileItem) { option = @"LogFile"; } else if (sender == configFileItem) { dir = [daemonController configFilePath]; } else if ([categoryItems containsObject:sender]) { int index = [categoryItems indexOfObject:sender]; dir = [categoryDirs objectAtIndex:index]; } else { return; } if (dir == nil) { NSString* mainDir = [[daemonController valueForOption:@"MainDir"] stringByExpandingTildeInPath]; dir = [[daemonController valueForOption:option] stringByExpandingTildeInPath]; dir = [dir stringByReplacingOccurrencesOfString:@"${MainDir}" withString:mainDir options:NSCaseInsensitiveSearch range:NSMakeRange(0, [dir length])]; } dir = [[dir componentsSeparatedByCharactersInSet: [NSCharacterSet characterSetWithCharactersInString:@";,"]] objectAtIndex:0]; if (dir.length == 0 || ![[NSFileManager defaultManager] fileExistsAtPath:dir]) { [NSApp activateIgnoringOtherApps:TRUE]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:[NSString stringWithFormat:NSLocalizedString(@"CantShowInFinder.MessageText", nil), dir]]; [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(option == nil ? @"CantShowInFinder.InformativeTextForCategory" : @"CantShowInFinder.InformativeTextWithOption", nil), option]]; [alert runModal]; return; } [[NSWorkspace sharedWorkspace] activateFileViewerSelectingURLs:@[[NSURL fileURLWithPath:dir isDirectory:YES]]]; } - (void)daemonStatusUpdated { if (restarting && daemonController.connected) { restarting = NO; [restartTimer invalidate]; [NSApp activateIgnoringOtherApps:TRUE]; NSAlert *alert = [[NSAlert alloc] init]; [alert setMessageText:NSLocalizedString(resetting ? @"FactoryResetted.MessageText" : daemonController.recoveryMode ? @"RestartedRecoveryMode.MessageText" : @"Restarted.MessageText", nil)]; [alert setInformativeText:NSLocalizedString(resetting ? @"FactoryResetted.InformativeText" : daemonController.recoveryMode ? @"RestartedRecoveryMode.InformativeText" : @"Restarted.InformativeText", nil)]; [alert setAlertStyle:NSInformationalAlertStyle]; [alert addButtonWithTitle:NSLocalizedString(@"Restarted.OK", nil)]; [alert addButtonWithTitle:NSLocalizedString(@"Restarted.WebUI", nil)]; if ([alert runModal] == NSAlertSecondButtonReturn) { [self showWebUI]; } resetting = NO; } else { [self updateStatus]; } } - (void)updateStatus { //DLog(@"updateStatus"); NSString* info1 = @""; NSString* info2 = nil; BOOL preventSleep = NO; NSDictionary* status = [daemonController status]; if (restarting || daemonController.restarting) { info1 = NSLocalizedString(@"Status.Restarting", nil); } else if (!daemonController.connected) { info1 = NSLocalizedString(@"Status.NoConnection", nil); } else if ([(NSNumber*)[status objectForKey:@"ServerStandBy"] integerValue] == 1) { if ([(NSNumber*)[status objectForKey:@"PostJobCount"] integerValue] > 0) { info1 = NSLocalizedString(@"Status.Post-Processing", nil); preventSleep = YES; } else if ([(NSNumber*)[status objectForKey:@"UrlCount"] integerValue] > 0) { info1 = NSLocalizedString(@"Status.Fetching NZBs", nil); } else if ([(NSNumber*)[status objectForKey:@"FeedActive"] integerValue] == 1) { info1 = NSLocalizedString(@"Status.Fetching Feeds", nil); } else if ([(NSNumber*)[status objectForKey:@"DownloadPaused"] integerValue] == 1 || [(NSNumber*)[status objectForKey:@"Download2Paused"] integerValue] == 1) { info1 = NSLocalizedString(@"Status.Paused", nil); } else { info1 = NSLocalizedString(@"Status.Idle", nil); } } else { int speed = [(NSNumber*)[status objectForKey:@"DownloadRate"] integerValue]; if (speed >= 1024 * 1024 * 10) { info1 = [NSString stringWithFormat:NSLocalizedString(@"Status.DownloadingMB10", nil), speed / 1024 / 1024]; } else if (speed >= 1024 * 1024) { info1 = [NSString stringWithFormat:NSLocalizedString(@"Status.DownloadingMB", nil), (float)speed / 1024.0 / 1024.0]; } else { info1 = [NSString stringWithFormat:NSLocalizedString(@"Status.DownloadingKB", nil), speed / 1024]; } preventSleep = YES; if (speed > 0) { long long remaining = ([(NSNumber*)[status objectForKey:@"RemainingSizeHi"] integerValue] << 32) + [(NSNumber*)[status objectForKey:@"RemainingSizeLo"] integerValue]; int secondsLeft = remaining / speed; info2 = [NSString stringWithFormat:NSLocalizedString(@"Status.Left", nil), [self formatTimeLeft:secondsLeft]]; } } if (preventSleep != preventingSleep) { [self updateSleepState:preventSleep]; } [info1Item setTitle:info1]; [info2Item setHidden:info2 == nil]; if (info2 != nil) { [info2Item setTitle:info2]; } } - (void)updateSleepState:(BOOL)preventSleep { if (preventSleep) { sleepID = 0; NSString* reason = NSLocalizedString(@"Status.PreventSleep", nil); IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleSystemSleep, kIOPMAssertionLevelOn, (__bridge CFStringRef)reason, &sleepID); } else if (sleepID != 0) { IOPMAssertionRelease(sleepID); } preventingSleep = preventSleep; } - (NSString*)formatTimeLeft:(int)sec { int days = floor(sec / 86400); int hours = floor((sec % 86400) / 3600); int minutes = floor((sec / 60) % 60); int seconds = floor(sec % 60); if (days > 10) { return [NSString stringWithFormat:NSLocalizedString(@"Left.Days", nil), days]; } if (days > 0) { return [NSString stringWithFormat:NSLocalizedString(@"Left.Days.Hours", nil), days, hours]; } if (hours > 10) { return [NSString stringWithFormat:NSLocalizedString(@"Left.Hours", nil), hours]; } if (hours > 0) { return [NSString stringWithFormat:NSLocalizedString(@"Left.Hours.Minutes", nil), hours, minutes]; } if (minutes > 10) { return [NSString stringWithFormat:NSLocalizedString(@"Left.Minutes", nil), minutes]; } if (minutes > 0) { return [NSString stringWithFormat:NSLocalizedString(@"Left.Minutes.Seconds", nil), minutes, seconds]; } return [NSString stringWithFormat:NSLocalizedString(@"Left.Seconds", nil), seconds]; } - (void)daemonConfigLoaded { DLog(@"config loaded"); [self updateCategoriesMenu]; } - (void)updateCategoriesMenu { NSMenu *submenu = destDirItem.parentItem.submenu; for (NSMenuItem* item in categoryItems) { [submenu removeItem:item]; } categoryItems = [NSMutableArray array]; categoryDirs = [NSMutableArray array]; NSString* mainDir = [[daemonController valueForOption:@"MainDir"] stringByExpandingTildeInPath]; NSString* destDir = [[daemonController valueForOption:@"DestDir"] stringByExpandingTildeInPath]; for (int i=1; ; i++) { NSString* catName = [daemonController valueForOption:[NSString stringWithFormat:@"Category%i.Name", i]]; NSString* catDir = [daemonController valueForOption:[NSString stringWithFormat:@"Category%i.DestDir", i]]; if (catName.length == 0) { break; } if (catDir.length == 0) { catDir = [destDir stringByAppendingPathComponent:catName]; } NSString* dir = [catDir stringByExpandingTildeInPath]; dir = [dir stringByReplacingOccurrencesOfString:@"${MainDir}" withString:mainDir options:NSCaseInsensitiveSearch range:NSMakeRange(0, dir.length)]; dir = [dir stringByReplacingOccurrencesOfString:@"${DestDir}" withString:destDir options:NSCaseInsensitiveSearch range:NSMakeRange(0, dir.length)]; NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"Category%i: %@", i, catName] action:@selector(showInFinderClicked:) keyEquivalent:@""]; [item setTarget:self]; [submenu insertItem:item atIndex:[submenu indexOfItem:destDirSeparator]]; [categoryItems addObject:item]; [categoryDirs addObject:dir]; } } @end