#import "SoftwareUpdate.h" #import "DownloadWindowController.h" #import "ReleaseNotesWindowController.h" #import "sw_update.h" #import #import #import #import #import #import #import #import #import OAK_DEBUG_VAR(SoftwareUpdate_Check); OAK_DEBUG_VAR(SoftwareUpdate_ReleaseNotes); NSString* const kUserDefaultsDisableSoftwareUpdatesKey = @"SoftwareUpdateDisablePolling"; NSString* const kUserDefaultsSoftwareUpdateChannelKey = @"SoftwareUpdateChannel"; // release (default), beta, nightly NSString* const kUserDefaultsSubmitUsageInfoKey = @"SoftwareUpdateSubmitUsageInfo"; NSString* const kUserDefaultsAskBeforeUpdatingKey = @"SoftwareUpdateAskBeforeUpdating"; NSString* const kUserDefaultsLastSoftwareUpdateCheckKey = @"SoftwareUpdateLastPoll"; NSString* const kUserDefaultsSoftwareUpdateSuspendUntilKey = @"SoftwareUpdateSuspendUntil"; NSString* const kSoftwareUpdateChannelRelease = @"release"; NSString* const kSoftwareUpdateChannelBeta = @"beta"; NSString* const kSoftwareUpdateChannelNightly = @"nightly"; @interface SoftwareUpdate () @property (nonatomic, retain) NSDate* lastPoll; @property (nonatomic, assign) BOOL isChecking; @property (nonatomic, retain) NSString* errorString; @property (nonatomic, retain) NSTimer* pollTimer; @property (nonatomic, retain) DownloadWindowController* downloadWindowController; - (void)scheduleVersionCheck:(id)sender; @end static SoftwareUpdate* SharedInstance; @implementation SoftwareUpdate @synthesize channels, isChecking, errorString, pollTimer, downloadWindowController; + (SoftwareUpdate*)sharedInstance { return SharedInstance ?: [[self new] autorelease]; } + (void)initialize { [[NSUserDefaults standardUserDefaults] registerDefaults: [NSDictionary dictionaryWithObjectsAndKeys: kSoftwareUpdateChannelRelease, kUserDefaultsSoftwareUpdateChannelKey, nil]]; } - (void)showReleaseNotes:(id)sender { D(DBF_SoftwareUpdate_ReleaseNotes, bug("\n");); [ReleaseNotesWindowController showPath:[[NSBundle mainBundle] pathForResource:@"ReleaseNotes" ofType:@"html"]]; } - (void)installReleaseNotesMenuItem:(id)sender { for(NSMenuItem* item in [[[NSApp mainMenu] itemArray] reverseObjectEnumerator]) { if([[item submenu] indexOfItemWithTarget:nil andAction:@selector(showHelp:)] != -1) { [[[item submenu] addItemWithTitle:@"Release Notes" action:@selector(showReleaseNotes:) keyEquivalent:@""] setTarget:self]; break; } } } - (id)init { if(SharedInstance) { [self release]; } else if(self = SharedInstance = [[super init] retain]) { D(DBF_SoftwareUpdate_Check, bug("\n");); pollInterval = 60*60; [[[NSWorkspace sharedWorkspace] notificationCenter] addObserver:self selector:@selector(scheduleVersionCheck:) name:NSWorkspaceDidWakeNotification object:[NSWorkspace sharedWorkspace]]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]]; [ReleaseNotesWindowController showPathIfUpdated:[[NSBundle mainBundle] pathForResource:@"ReleaseNotes" ofType:@"html"]]; [self installReleaseNotesMenuItem:self]; } return SharedInstance; } - (void)scheduleVersionCheck:(id)sender { D(DBF_SoftwareUpdate_Check, bug("had pending check: %s\n", BSTR(self.pollTimer));); [self.pollTimer invalidate]; self.pollTimer = nil; struct statfs sfsb; BOOL readOnlyFileSystem = statfs(oak::application_t::path().c_str(), &sfsb) != 0 || (sfsb.f_flags & MNT_RDONLY); BOOL disablePolling = [[[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsDisableSoftwareUpdatesKey] boolValue]; D(DBF_SoftwareUpdate_Check, bug("download visible %s, disable polling %s, read only file system %s → %s\n", BSTR(downloadWindowController.isVisible), BSTR(disablePolling), BSTR(readOnlyFileSystem), BSTR(!downloadWindowController.isVisible && !disablePolling && !readOnlyFileSystem));); if(downloadWindowController.isVisible || disablePolling || readOnlyFileSystem) return; NSDate* nextCheck = [(self.lastPoll ?: [NSDate distantPast]) addTimeInterval:pollInterval]; if(NSDate* suspendUntil = [[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsSoftwareUpdateSuspendUntilKey]) nextCheck = [nextCheck laterDate:suspendUntil]; D(DBF_SoftwareUpdate_Check, bug("perform next check in %.1f hours\n", std::max(1, [nextCheck timeIntervalSinceNow])/60/60);); self.pollTimer = [NSTimer scheduledTimerWithTimeInterval:std::max(1, [nextCheck timeIntervalSinceNow]) target:self selector:@selector(performVersionCheck:) userInfo:nil repeats:NO]; } - (void)userDefaultsDidChange:(id)sender { [self scheduleVersionCheck:self]; } // ======================== // = Performing the check = // ======================== - (void)performVersionCheck:(NSTimer*)aTimer { D(DBF_SoftwareUpdate_Check, bug("last check was %.1f hours ago\n", -[self.lastPoll timeIntervalSinceNow] / (60*60));); if(!isChecking) { self.isChecking = YES; NSURL* url = [channels objectForKey:[[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsSoftwareUpdateChannelKey]]; [self performSelectorInBackground:@selector(performBackgroundVersionCheck:) withObject:@{ @"infoURL" : url, @"timer" : YES_obj }]; } } - (IBAction)checkForUpdates:(id)sender { D(DBF_SoftwareUpdate_ReleaseNotes, bug("\n");); if(!isChecking) { self.isChecking = YES; NSURL* url = [channels objectForKey:OakIsAlternateKeyOrMouseEvent(NSAlternateKeyMask) ? kSoftwareUpdateChannelNightly : [[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsSoftwareUpdateChannelKey]]; BOOL force = OakIsAlternateKeyOrMouseEvent(NSShiftKeyMask); [self performSelectorInBackground:@selector(performBackgroundVersionCheck:) withObject:@{ @"infoURL" : url, @"force" : @(force) }]; } } - (void)performBackgroundVersionCheck:(NSDictionary*)someOptions { NSAutoreleasePool* pool = [NSAutoreleasePool new]; NSURL* url = [someOptions objectForKey:@"infoURL"]; std::string error = NULL_STR; auto info = sw_update::download_info(to_s([url absoluteString]), &error); NSMutableDictionary* res = [[someOptions mutableCopy] autorelease]; if(error != NULL_STR) { [res setObject:[NSString stringWithCxxString:error] forKey:@"error"]; } else { [res setObject:[NSString stringWithCxxString:info.url] forKey:@"url"]; [res setObject:@(info.version) forKey:@"version"]; } [self performSelectorOnMainThread:@selector(didPerformBackgroundVersionCheck:) withObject:res waitUntilDone:NO]; [pool drain]; } - (void)didPerformBackgroundVersionCheck:(NSDictionary*)info { long version = [[info objectForKey:@"version"] longValue]; NSString* downloadURL = [info objectForKey:@"url"]; NSString* error = [info objectForKey:@"error"]; BOOL timer = [[info objectForKey:@"timer"] boolValue]; BOOL force = [[info objectForKey:@"force"] boolValue]; BOOL ask = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsAskBeforeUpdatingKey]; if(version && downloadURL) { NSString* appName = [NSString stringWithCxxString:oak::application_t::name()]; NSInteger thisVersion = force ? 0 : strtol(oak::application_t::revision().c_str(), NULL, 10); if(version <= thisVersion) { if(!timer) NSRunInformationalAlertPanel(@"Up To Date", @"%@ %ld is the latest version available—you have %ld.", @"Continue", nil, nil, appName, version, thisVersion); } else { bool interactive = ask || !timer; int choice = interactive ? NSRunInformationalAlertPanel(@"New Version Available", @"%@ %ld is now available—you have %ld. Would you like to download it now?", @"Download & Install", nil, @"Later", appName, version, thisVersion) : NSAlertDefaultReturn; if(choice == NSAlertDefaultReturn) // "Download & Install" { self.downloadWindowController = [[DownloadWindowController alloc] initWithURL:downloadURL displayString:[NSString stringWithFormat:@"Downloading %@ %ld…", appName, version] keyChain:keyChain]; downloadWindowController.versionOfDownload = [NSString stringWithFormat:@"%ld", version]; if(!interactive && [NSApp isActive]) [[downloadWindowController window] orderFront:self]; else [downloadWindowController showWindow:self]; } else if(choice == NSAlertOtherReturn) // "Later" { [[NSUserDefaults standardUserDefaults] setObject:[[NSDate date] addTimeInterval:24*60*60] forKey:kUserDefaultsSoftwareUpdateSuspendUntilKey]; } } } else if(error && !timer) { NSRunInformationalAlertPanel(@"Error checking for new version", @"%@", @"Continue", nil, nil, error); error = nil; } self.isChecking = NO; self.lastPoll = [NSDate date]; self.errorString = error; } // ============== // = Properties = // ============== - (void)setChannels:(NSDictionary*)someChannels { [channels autorelease]; channels = [someChannels retain]; [self scheduleVersionCheck:nil]; } - (void)setSignee:(key_chain_t::key_t const&)aSignee { keyChain.add(aSignee); } - (NSDate*)lastPoll { return [[NSUserDefaults standardUserDefaults] objectForKey:kUserDefaultsLastSoftwareUpdateCheckKey]; } - (void)setLastPoll:(NSDate*)newDate { [[NSUserDefaults standardUserDefaults] setObject:newDate forKey:kUserDefaultsLastSoftwareUpdateCheckKey]; } @end