Files
textmate/Frameworks/SoftwareUpdate/src/SoftwareUpdate.mm
Allan Odgaard 9894969e67 Initial commit
2012-08-09 16:25:56 +02:00

237 lines
9.4 KiB
Plaintext

#import "SoftwareUpdate.h"
#import "DownloadWindowController.h"
#import "ReleaseNotesWindowController.h"
#import "sw_update.h"
#import <OakAppKit/OakAppKit.h>
#import <OakAppKit/NSMenu Additions.h>
#import <OakFoundation/OakFoundation.h>
#import <OakFoundation/NSDate Additions.h>
#import <OakFoundation/NSString Additions.h>
#import <OakSystem/application.h>
#import <network/network.h>
#import <ns/ns.h>
#import <oak/debug.h>
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<NSTimeInterval>(1, [nextCheck timeIntervalSinceNow])/60/60););
self.pollTimer = [NSTimer scheduledTimerWithTimeInterval:std::max<NSTimeInterval>(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