mirror of
https://github.com/textmate/textmate.git
synced 2026-01-15 01:38:02 -05:00
387 lines
14 KiB
Plaintext
387 lines
14 KiB
Plaintext
#import "SoftwareUpdate.h"
|
|
#import "DownloadWindowController.h"
|
|
#import "sw_update.h"
|
|
#import <version/version.h>
|
|
#import <OakAppKit/OakAppKit.h>
|
|
#import <OakAppKit/NSAlert Additions.h>
|
|
#import <OakAppKit/OakSound.h>
|
|
#import <OakAppKit/NSMenu Additions.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);
|
|
|
|
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";
|
|
|
|
struct shared_state_t
|
|
{
|
|
double progress = 0;
|
|
bool stop = false;
|
|
};
|
|
|
|
typedef std::shared_ptr<shared_state_t> shared_state_ptr;
|
|
|
|
@interface SoftwareUpdate ()
|
|
{
|
|
key_chain_t keyChain;
|
|
NSTimeInterval pollInterval;
|
|
|
|
shared_state_ptr sharedState;
|
|
CGFloat secondsLeft;
|
|
}
|
|
@property (nonatomic) NSDate* lastPoll;
|
|
@property (nonatomic, readwrite, getter = isChecking) BOOL checking;
|
|
@property (nonatomic) NSString* lastVersionDownloaded;
|
|
@property (nonatomic) NSString* errorString;
|
|
@property (nonatomic) NSTimer* pollTimer;
|
|
@property (nonatomic) DownloadWindowController* downloadWindow;
|
|
@property (nonatomic) NSString* archive;
|
|
|
|
- (void)scheduleVersionCheck:(id)sender;
|
|
- (void)checkVersionAtURL:(NSURL*)anURL inBackground:(BOOL)backgroundFlag allowRedownload:(BOOL)redownloadFlag;
|
|
@end
|
|
|
|
@implementation SoftwareUpdate
|
|
+ (instancetype)sharedInstance
|
|
{
|
|
static SoftwareUpdate* sharedInstance = [self new];
|
|
return sharedInstance;
|
|
}
|
|
|
|
+ (void)initialize
|
|
{
|
|
[[NSUserDefaults standardUserDefaults] registerDefaults:@{
|
|
kUserDefaultsSoftwareUpdateChannelKey : kSoftwareUpdateChannelRelease
|
|
}];
|
|
}
|
|
|
|
- (id)init
|
|
{
|
|
if(self = [super init])
|
|
{
|
|
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]];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (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([NSBundle mainBundle].bundlePath.fileSystemRepresentation, &sfsb) != 0 || (sfsb.f_flags & MNT_RDONLY);
|
|
BOOL disablePolling = [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsDisableSoftwareUpdatesKey];
|
|
D(DBF_SoftwareUpdate_Check, bug("download visible %s, disable polling %s, read only file system %s → %s\n", BSTR(self.downloadWindow), BSTR(disablePolling), BSTR(readOnlyFileSystem), BSTR(!self.downloadWindow && !disablePolling && !readOnlyFileSystem)););
|
|
if(_downloadWindow.isWorking || disablePolling || readOnlyFileSystem)
|
|
return;
|
|
|
|
NSDate* nextCheck = [(self.lastPoll ?: [NSDate distantPast]) dateByAddingTimeInterval: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", [[NSDate date] timeIntervalSinceDate:self.lastPoll] / (60*60)););
|
|
if(_downloadWindow.isWorking)
|
|
return;
|
|
|
|
NSURL* url = [self.channels objectForKey:[[NSUserDefaults standardUserDefaults] stringForKey:kUserDefaultsSoftwareUpdateChannelKey]];
|
|
[self checkVersionAtURL:url inBackground:YES allowRedownload:NO];
|
|
}
|
|
|
|
- (IBAction)checkForUpdates:(id)sender
|
|
{
|
|
D(DBF_SoftwareUpdate_Check, bug("\n"););
|
|
|
|
BOOL isShiftDown = OakIsAlternateKeyOrMouseEvent(NSShiftKeyMask);
|
|
NSURL* url = [self.channels objectForKey:OakIsAlternateKeyOrMouseEvent(NSAlternateKeyMask) ? kSoftwareUpdateChannelNightly : [[NSUserDefaults standardUserDefaults] stringForKey:kUserDefaultsSoftwareUpdateChannelKey]];
|
|
[self checkVersionAtURL:url inBackground:NO allowRedownload:isShiftDown];
|
|
}
|
|
|
|
- (void)checkVersionAtURL:(NSURL*)anURL inBackground:(BOOL)backgroundFlag allowRedownload:(BOOL)redownloadFlag
|
|
{
|
|
if(self.isChecking)
|
|
return;
|
|
self.checking = YES;
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
|
|
std::string error = NULL_STR;
|
|
auto info = sw_update::download_info(to_s([anURL absoluteString]), &error);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
self.errorString = [NSString stringWithCxxString:error];
|
|
self.lastPoll = [NSDate date];
|
|
self.checking = NO;
|
|
|
|
if(self.errorString)
|
|
{
|
|
if(!backgroundFlag)
|
|
{
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
alert.alertStyle = NSAlertStyleInformational;
|
|
alert.messageText = @"Error checking for new version";
|
|
alert.informativeText = self.errorString;
|
|
[alert addButtonWithTitle:@"Continue"];
|
|
[alert runModal];
|
|
}
|
|
}
|
|
else if(info.version != NULL_STR && info.url != NULL_STR)
|
|
{
|
|
NSString* version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
|
NSString* newVersion = [NSString stringWithFormat:@"%@ %@", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"], [NSString stringWithCxxString:info.version]];
|
|
|
|
BOOL downloadAndInstall = NO;
|
|
|
|
if(version::equal(info.version, to_s(version)) && !backgroundFlag)
|
|
{
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
alert.alertStyle = NSAlertStyleInformational;
|
|
alert.messageText = @"Up To Date";
|
|
alert.informativeText = [NSString stringWithFormat:@"%@ is the latest version available—you have version %@.", newVersion, version];
|
|
[alert addButtonWithTitle:@"Continue"];
|
|
if(redownloadFlag)
|
|
[alert addButtonWithTitle:@"Redownload"];
|
|
|
|
if([alert runModal] == NSAlertSecondButtonReturn) // “Redownload”
|
|
downloadAndInstall = YES;
|
|
}
|
|
else if(version::less(info.version, to_s(version)) && !backgroundFlag)
|
|
{
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
alert.alertStyle = NSAlertStyleInformational;
|
|
alert.messageText = @"Up To Date";
|
|
alert.informativeText = [NSString stringWithFormat:@"%@ is the latest version available—you have version %@.", newVersion, version];
|
|
[alert addButtons:@"Continue", @"Downgrade", nil];
|
|
if([alert runModal] == NSAlertSecondButtonReturn) // “Downgrade”
|
|
downloadAndInstall = YES;
|
|
}
|
|
else if(version::less(to_s(version), info.version))
|
|
{
|
|
if(!backgroundFlag || [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsAskBeforeUpdatingKey])
|
|
{
|
|
NSAlert* alert = [[NSAlert alloc] init];
|
|
alert.alertStyle = NSAlertStyleInformational;
|
|
alert.messageText = @"New Version Available";
|
|
alert.informativeText = [NSString stringWithFormat: @"%@ is now available—you have version %@. Would you like to download it now?", newVersion, version];
|
|
[alert addButtons:@"Download & Install", @"Later", nil];
|
|
|
|
NSModalResponse choice = [alert runModal];
|
|
if(choice == NSAlertFirstButtonReturn) // “Download & Install”
|
|
downloadAndInstall = YES;
|
|
else if(choice == NSAlertSecondButtonReturn) // “Later”
|
|
[[NSUserDefaults standardUserDefaults] setObject:[[NSDate date] dateByAddingTimeInterval:24*60*60] forKey:kUserDefaultsSoftwareUpdateSuspendUntilKey];
|
|
}
|
|
else if(version::less(to_s(self.lastVersionDownloaded), info.version))
|
|
{
|
|
downloadAndInstall = YES;
|
|
}
|
|
}
|
|
|
|
if(downloadAndInstall)
|
|
{
|
|
BOOL interactive = !backgroundFlag || [[NSUserDefaults standardUserDefaults] boolForKey:kUserDefaultsAskBeforeUpdatingKey];
|
|
self.lastVersionDownloaded = [NSString stringWithCxxString:info.version];
|
|
[self downloadVersion:newVersion atURL:[NSString stringWithCxxString:info.url] interactively:interactive];
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// ===================
|
|
// = Download Update =
|
|
// ===================
|
|
|
|
- (DownloadWindowController*)downloadWindow
|
|
{
|
|
return _downloadWindow = _downloadWindow ?: [DownloadWindowController new];
|
|
}
|
|
|
|
- (void)downloadVersion:(NSString*)versionName atURL:(NSString*)downloadURL interactively:(BOOL)interactive
|
|
{
|
|
sharedState = std::make_shared<shared_state_t>();
|
|
secondsLeft = CGFLOAT_MAX;
|
|
|
|
self.downloadWindow.delegate = self;
|
|
self.downloadWindow.activityText = [NSString stringWithFormat:@"Downloading %@…", versionName];
|
|
self.downloadWindow.statusText = @"Estimating time remaining";
|
|
|
|
self.downloadWindow.isIndeterminate = NO;
|
|
self.downloadWindow.progress = 0;
|
|
self.downloadWindow.isWorking = YES;
|
|
|
|
self.downloadWindow.canInstall = NO;
|
|
self.downloadWindow.canCancel = YES;
|
|
|
|
if(!interactive && [NSApp isActive])
|
|
[self.downloadWindow.window orderFront:self];
|
|
else [self.downloadWindow showWindow:self];
|
|
|
|
NSTimer* updateProgressTimer = [NSTimer scheduledTimerWithTimeInterval:0.04 target:self selector:@selector(updateProgress:) userInfo:[NSDate date] repeats:YES];
|
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
|
|
shared_state_ptr state = sharedState;
|
|
std::string error = NULL_STR;
|
|
std::string path = sw_update::download_update(to_s(downloadURL), keyChain, &error, &state->progress, &state->stop);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[updateProgressTimer invalidate];
|
|
|
|
self.downloadWindow.progress = 1;
|
|
self.downloadWindow.statusText = @"";
|
|
self.downloadWindow.isWorking = NO;
|
|
|
|
if(sharedState->stop)
|
|
return;
|
|
|
|
if(path != NULL_STR)
|
|
{
|
|
self.archive = [NSString stringWithCxxString:path];
|
|
|
|
self.downloadWindow.activityText = [NSString stringWithFormat:@"Downloaded %@", versionName];
|
|
self.downloadWindow.canInstall = YES;
|
|
self.downloadWindow.showUpdateBadge = YES;
|
|
|
|
if([NSApp isActive])
|
|
OakPlayUISound(OakSoundDidCompleteSomethingUISound);
|
|
|
|
[NSApp requestUserAttention:NSInformationalRequest];
|
|
}
|
|
else if(error != NULL_STR)
|
|
{
|
|
self.downloadWindow.activityText = [NSString stringWithFormat:@"Failed: %@", [NSString stringWithCxxString:error]];
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
- (void)updateProgress:(NSTimer*)aTimer
|
|
{
|
|
self.downloadWindow.progress = sharedState->progress;
|
|
|
|
NSDate* downloadStartDate = [aTimer userInfo];
|
|
NSTimeInterval secondsElapsed = [[NSDate date] timeIntervalSinceDate:downloadStartDate];
|
|
if(secondsElapsed < 1 || self.downloadWindow.progress < 0.01)
|
|
return;
|
|
|
|
NSTimeInterval left = secondsElapsed / self.downloadWindow.progress - secondsElapsed;
|
|
if(left < 2.6)
|
|
{
|
|
self.downloadWindow.statusText = @"Time remaining: a few seconds";
|
|
}
|
|
else
|
|
{
|
|
NSTimeInterval roundedSecondsLeft = 5 * round(left / 5);
|
|
if(roundedSecondsLeft < secondsLeft || roundedSecondsLeft - secondsLeft > 10)
|
|
{
|
|
NSTimeInterval const kMinute = 60;
|
|
NSTimeInterval const kHour = 60*60;
|
|
|
|
if(roundedSecondsLeft < kMinute)
|
|
self.downloadWindow.statusText = [NSString stringWithFormat:@"Time remaining: about %.0f seconds", roundedSecondsLeft];
|
|
else if(roundedSecondsLeft < 2*kMinute)
|
|
self.downloadWindow.statusText = [NSString stringWithFormat:@"Time remaining: a few minutes"];
|
|
else if(roundedSecondsLeft < kHour)
|
|
self.downloadWindow.statusText = [NSString stringWithFormat:@"Time remaining: about %.0f minutes", roundedSecondsLeft / kMinute];
|
|
else
|
|
self.downloadWindow.statusText = [NSString stringWithFormat:@"Time remaining: hours"];
|
|
secondsLeft = roundedSecondsLeft;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ====================
|
|
// = Delegate Methods =
|
|
// ====================
|
|
|
|
- (void)install:(DownloadWindowController*)sender
|
|
{
|
|
D(DBF_SoftwareUpdate_Check, bug("\n"););
|
|
|
|
self.downloadWindow.activityText = @"Installing…";
|
|
self.downloadWindow.isIndeterminate = YES;
|
|
self.downloadWindow.isWorking = YES;
|
|
self.downloadWindow.canInstall = NO;
|
|
|
|
std::string err = sw_update::install_update(to_s(self.archive));
|
|
if(err == NULL_STR)
|
|
{
|
|
self.downloadWindow.activityText = @"Relaunching…";
|
|
NSString* args = [NSString stringWithFormat:@"-disableSessionRestore NO -didUpdateFrom '%@'", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"]];
|
|
oak::application_t::relaunch([args UTF8String]);
|
|
}
|
|
else
|
|
{
|
|
self.downloadWindow.activityText = [NSString stringWithCxxString:err];
|
|
self.downloadWindow.isWorking = NO;
|
|
OakRunIOAlertPanel("%s", err.c_str());
|
|
}
|
|
}
|
|
|
|
- (void)cancel:(DownloadWindowController*)sender
|
|
{
|
|
D(DBF_SoftwareUpdate_Check, bug("\n"););
|
|
path::remove(to_s(self.archive));
|
|
[sender.window performClose:self];
|
|
}
|
|
|
|
- (void)windowWillClose:(DownloadWindowController*)sender
|
|
{
|
|
D(DBF_SoftwareUpdate_Check, bug("\n"););
|
|
sharedState->stop = true;
|
|
|
|
self.downloadWindow.showUpdateBadge = NO;
|
|
self.downloadWindow = nil;
|
|
}
|
|
|
|
// ==============
|
|
// = Properties =
|
|
// ==============
|
|
|
|
- (void)setChannels:(NSDictionary*)someChannels
|
|
{
|
|
_channels = someChannels;
|
|
[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
|