From 1d0366052dbd26261265dfbbcd708e74aa2809d7 Mon Sep 17 00:00:00 2001 From: Lior Halphon Date: Sun, 25 Apr 2021 22:28:24 +0300 Subject: [PATCH] Updater support --- Cocoa/AppDelegate.h | 13 +- Cocoa/AppDelegate.m | 309 +++++++++++++++++++++++++++++++++++- Cocoa/Document.m | 2 +- Cocoa/GBPreferencesWindow.h | 1 + Cocoa/GBPreferencesWindow.m | 19 +++ Cocoa/Preferences.xib | 66 ++++++-- Cocoa/UpdateWindow.xib | 139 ++++++++++++++++ Makefile | 3 + 8 files changed, 537 insertions(+), 15 deletions(-) create mode 100644 Cocoa/UpdateWindow.xib diff --git a/Cocoa/AppDelegate.h b/Cocoa/AppDelegate.h index e74b191..a9b0048 100644 --- a/Cocoa/AppDelegate.h +++ b/Cocoa/AppDelegate.h @@ -1,16 +1,25 @@ #import +#import -@interface AppDelegate : NSObject +@interface AppDelegate : NSObject @property (nonatomic, strong) IBOutlet NSWindow *preferencesWindow; @property (nonatomic, strong) IBOutlet NSView *graphicsTab; @property (nonatomic, strong) IBOutlet NSView *emulationTab; @property (nonatomic, strong) IBOutlet NSView *audioTab; @property (nonatomic, strong) IBOutlet NSView *controlsTab; +@property (nonatomic, strong) IBOutlet NSView *updatesTab; - (IBAction)showPreferences: (id) sender; - (IBAction)toggleDeveloperMode:(id)sender; - (IBAction)switchPreferencesTab:(id)sender; @property (nonatomic, weak) IBOutlet NSMenuItem *linkCableMenuItem; - +@property (nonatomic, strong) IBOutlet NSWindow *updateWindow; +@property (nonatomic, strong) IBOutlet WebView *updateChanges; +@property (nonatomic, strong) IBOutlet NSProgressIndicator *updatesSpinner; +@property (strong) IBOutlet NSButton *updatesButton; +@property (strong) IBOutlet NSTextField *updateProgressLabel; +@property (strong) IBOutlet NSButton *updateProgressButton; +@property (strong) IBOutlet NSWindow *updateProgressWindow; +@property (strong) IBOutlet NSProgressIndicator *updateProgressSpinner; @end diff --git a/Cocoa/AppDelegate.m b/Cocoa/AppDelegate.m index aee2111..01b1527 100644 --- a/Cocoa/AppDelegate.m +++ b/Cocoa/AppDelegate.m @@ -4,11 +4,35 @@ #include #import #import +#import + +#define UPDATE_SERVER "https://sameboy.github.io" +#define str(x) #x +#define xstr(x) str(x) + +static uint32_t color_to_int(NSColor *color) +{ + color = [color colorUsingColorSpace:[NSColorSpace deviceRGBColorSpace]]; + return (((unsigned)(color.redComponent * 0xFF)) << 16) | + (((unsigned)(color.greenComponent * 0xFF)) << 8) | + ((unsigned)(color.blueComponent * 0xFF)); +} @implementation AppDelegate { NSWindow *preferences_window; NSArray *preferences_tabs; + NSString *_lastVersion; + NSString *_updateURL; + NSURLSessionDownloadTask *_updateTask; + enum { + UPDATE_DOWNLOADING, + UPDATE_EXTRACTING, + UPDATE_WAIT_INSTALL, + UPDATE_INSTALLING, + UPDATE_FAILED, + } _updateState; + NSString *_downloadDirectory; } - (void) applicationDidFinishLaunching:(NSNotification *)notification @@ -54,6 +78,16 @@ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBNotificationsUsed"]) { [NSUserNotificationCenter defaultUserNotificationCenter].delegate = self; } + + [self askAutoUpdates]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]) { + [self checkForUpdates]; + } + + if ([[NSProcessInfo processInfo].arguments containsObject:@"--update-launch"]) { + [NSApp activateIgnoringOtherApps:YES]; + } } - (IBAction)toggleDeveloperMode:(id)sender @@ -111,15 +145,19 @@ [[NSBundle mainBundle] loadNibNamed:@"Preferences" owner:self topLevelObjects:&objects]; NSToolbarItem *first_toolbar_item = [_preferencesWindow.toolbar.items firstObject]; _preferencesWindow.toolbar.selectedItemIdentifier = [first_toolbar_item itemIdentifier]; - preferences_tabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab]; + preferences_tabs = @[self.emulationTab, self.graphicsTab, self.audioTab, self.controlsTab, self.updatesTab]; [self switchPreferencesTab:first_toolbar_item]; [_preferencesWindow center]; +#ifndef UPDATE_SUPPORT + [_preferencesWindow.toolbar removeItemAtIndex:4]; +#endif } [_preferencesWindow makeKeyAndOrderFront:self]; } - (BOOL)applicationOpenUntitledFile:(NSApplication *)sender { + [self askAutoUpdates]; /* Bring an existing panel to the foreground */ for (NSWindow *window in [[NSApplication sharedApplication] windows]) { if ([window isKindOfClass:[NSOpenPanel class]]) { @@ -136,6 +174,275 @@ [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfFile:notification.identifier display:YES]; } +- (void)updateFound +{ + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/raw_changes"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + + NSColor *linkColor = [NSColor colorWithRed:0.125 green:0.325 blue:1.0 alpha:1.0]; + if (@available(macOS 10.10, *)) { + linkColor = [NSColor linkColor]; + } + + NSString *changes = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSRange cutoffRange = [changes rangeOfString:@""]; + if (cutoffRange.location != NSNotFound) { + changes = [changes substringToIndex:cutoffRange.location]; + } + + NSString *html = [NSString stringWithFormat:@"" + "" + "%@", + color_to_int([NSColor textColor]), + color_to_int(linkColor), + changes]; + + if ([(NSHTTPURLResponse *)response statusCode] == 200) { + dispatch_async(dispatch_get_main_queue(), ^{ + NSArray *objects; + [[NSBundle mainBundle] loadNibNamed:@"UpdateWindow" owner:self topLevelObjects:&objects]; + self.updateChanges.preferences.standardFontFamily = [NSFont systemFontOfSize:0].familyName; + self.updateChanges.preferences.fixedFontFamily = @"Menlo"; + self.updateChanges.drawsBackground = false; + [self.updateChanges.mainFrame loadHTMLString:html baseURL:nil]; + }); + } + }] resume]; +} + +- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems +{ + // Disable reload context menu + if ([defaultMenuItems count] <= 2) { + return nil; + } + return defaultMenuItems; +} + +- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sender.mainFrame.frameView.documentView.enclosingScrollView.drawsBackground = true; + sender.mainFrame.frameView.documentView.enclosingScrollView.backgroundColor = [NSColor textBackgroundColor]; + sender.policyDelegate = self; + [self.updateWindow center]; + [self.updateWindow makeKeyAndOrderFront:nil]; + }); +} + +- (void)webView:(WebView *)webView decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener +{ + [listener ignore]; + [[NSWorkspace sharedWorkspace] openURL:[request URL]]; +} + +- (void)checkForUpdates +{ +#ifdef UPDATE_SUPPORT + [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@UPDATE_SERVER "/latest_version"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.updatesSpinner stopAnimation:nil]; + [self.updatesButton setEnabled:YES]; + }); + if ([(NSHTTPURLResponse *)response statusCode] == 200) { + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + NSArray *components = [string componentsSeparatedByString:@"|"]; + if (components.count != 2) return; + _lastVersion = components[0]; + _updateURL = components[1]; + if (![@xstr(VERSION) isEqualToString:_lastVersion] && + ![[[NSUserDefaults standardUserDefaults] stringForKey:@"GBSkippedVersion"] isEqualToString:_lastVersion]) { + [self updateFound]; + } + } + }] resume]; +#endif +} + +- (IBAction)userCheckForUpdates:(id)sender +{ + if (self.updateWindow) { + [self.updateWindow makeKeyAndOrderFront:sender]; + } + else { + [[NSUserDefaults standardUserDefaults] setObject:nil forKey:@"GBSkippedVersion"]; + [self checkForUpdates]; + [sender setEnabled:false]; + [self.updatesSpinner startAnimation:sender]; + } +} + +- (void)askAutoUpdates +{ +#ifdef UPDATE_SUPPORT + if (![[NSUserDefaults standardUserDefaults] boolForKey:@"GBAskedAutoUpdates"]) { + NSAlert *alert = [[NSAlert alloc] init]; + alert.messageText = @"Should SameBoy check for updates when launched?"; + alert.informativeText = @"SameBoy is frequently updated with new features, accuracy improvements, and bug fixes. This setting can always be changed in the preferences window."; + [alert addButtonWithTitle:@"Check on Launch"]; + [alert addButtonWithTitle:@"Don't Check on Launch"]; + + [[NSUserDefaults standardUserDefaults] setBool:[alert runModal] == NSAlertFirstButtonReturn forKey:@"GBAutoUpdatesEnabled"]; + [[NSUserDefaults standardUserDefaults] setBool:true forKey:@"GBAskedAutoUpdates"]; + } +#endif +} + +- (IBAction)skipVersion:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setObject:_lastVersion forKey:@"GBSkippedVersion"]; + [self.updateWindow performClose:sender]; +} + +- (IBAction)installUpdate:(id)sender +{ + [self.updateProgressSpinner startAnimation:nil]; + self.updateProgressButton.title = @"Cancel"; + self.updateProgressButton.enabled = true; + self.updateProgressLabel.stringValue = @"Downloading update..."; + _updateState = UPDATE_DOWNLOADING; + _updateTask = [[NSURLSession sharedSession] downloadTaskWithURL: [NSURL URLWithString:_updateURL] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { + _updateTask = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Extracting update..."; + _updateState = UPDATE_EXTRACTING; + }); + + _downloadDirectory = [[[NSFileManager defaultManager] URLForDirectory:NSItemReplacementDirectory + inDomain:NSUserDomainMask + appropriateForURL:[[NSBundle mainBundle] bundleURL] + create:YES + error:nil] path]; + NSTask *unzipTask; + if (!_downloadDirectory) { + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to extract update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + } + + unzipTask = [[NSTask alloc] init]; + unzipTask.launchPath = @"/usr/bin/unzip"; + unzipTask.arguments = @[location.path, @"-d", _downloadDirectory]; + [unzipTask launch]; + [unzipTask waitUntilExit]; + if (unzipTask.terminationStatus != 0 || unzipTask.terminationReason != NSTaskTerminationReasonExit) { + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to extract update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Update ready, save your game progress and click Install."; + _updateState = UPDATE_WAIT_INSTALL; + self.updateProgressButton.title = @"Install"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + }]; + [_updateTask resume]; + + self.updateProgressWindow.preventsApplicationTerminationWhenModal = false; + [self.updateWindow beginSheet:self.updateProgressWindow completionHandler:^(NSModalResponse returnCode) { + [self.updateWindow close]; + }]; +} + +- (void)performUpgrade +{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Instaling update..."; + _updateState = UPDATE_INSTALLING; + self.updateProgressButton.enabled = false; + [self.updateProgressSpinner startAnimation:nil]; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSString *executablePath = [[NSBundle mainBundle] executablePath]; + NSString *contentsPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"Contents"]; + NSString *contentsTempPath = [[[NSBundle mainBundle] bundlePath] stringByAppendingPathComponent:@"TempContents"]; + NSString *updateContentsPath = [_downloadDirectory stringByAppendingPathComponent:@"SameBoy.app/Contents"]; + NSError *error = nil; + [[NSFileManager defaultManager] moveItemAtPath:contentsPath toPath:contentsTempPath error:&error]; + if (error) { + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + _downloadDirectory = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to install update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + [[NSFileManager defaultManager] moveItemAtPath:updateContentsPath toPath:contentsPath error:&error]; + if (error) { + [[NSFileManager defaultManager] moveItemAtPath:contentsTempPath toPath:contentsPath error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + _downloadDirectory = nil; + dispatch_sync(dispatch_get_main_queue(), ^{ + self.updateProgressButton.enabled = false; + self.updateProgressLabel.stringValue = @"Failed to install update."; + _updateState = UPDATE_FAILED; + self.updateProgressButton.title = @"Close"; + self.updateProgressButton.enabled = true; + [self.updateProgressSpinner stopAnimation:nil]; + }); + return; + } + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + [[NSFileManager defaultManager] removeItemAtPath:contentsTempPath error:nil]; + _downloadDirectory = nil; + atexit_b(^{ + execl(executablePath.UTF8String, executablePath.UTF8String, "--update-launch", NULL); + }); + + dispatch_async(dispatch_get_main_queue(), ^{ + [NSApp terminate:nil]; + }); + }); +} + +- (IBAction)updateAction:(id)sender +{ + switch (_updateState) { + case UPDATE_DOWNLOADING: + [_updateTask cancelByProducingResumeData:nil]; + _updateTask = nil; + [self.updateProgressWindow close]; + break; + case UPDATE_WAIT_INSTALL: + [self performUpgrade]; + break; + case UPDATE_EXTRACTING: + case UPDATE_INSTALLING: + break; + case UPDATE_FAILED: + [self.updateProgressWindow close]; + break; + } +} + +- (void)dealloc +{ + if (_downloadDirectory) { + [[NSFileManager defaultManager] removeItemAtPath:_downloadDirectory error:nil]; + } +} + - (IBAction)nop:(id)sender { } diff --git a/Cocoa/Document.m b/Cocoa/Document.m index fd0c448..8977ce9 100644 --- a/Cocoa/Document.m +++ b/Cocoa/Document.m @@ -1586,7 +1586,7 @@ static unsigned *multiplication_table_for_frequency(unsigned frequency) - (void)cameraRequestUpdate { - dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @try { if (!cameraSession) { if (@available(macOS 10.14, *)) { diff --git a/Cocoa/GBPreferencesWindow.h b/Cocoa/GBPreferencesWindow.h index b25e476..0c7b422 100644 --- a/Cocoa/GBPreferencesWindow.h +++ b/Cocoa/GBPreferencesWindow.h @@ -25,5 +25,6 @@ @property (nonatomic, weak) IBOutlet NSPopUpButton *cgbPopupButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *preferredJoypadButton; @property (nonatomic, weak) IBOutlet NSPopUpButton *playerListButton; +@property (nonatomic, weak) IBOutlet NSButton *autoUpdatesCheckbox; @end diff --git a/Cocoa/GBPreferencesWindow.m b/Cocoa/GBPreferencesWindow.m index 54d190f..217c7e1 100644 --- a/Cocoa/GBPreferencesWindow.m +++ b/Cocoa/GBPreferencesWindow.m @@ -29,6 +29,8 @@ NSPopUpButton *_rumbleModePopupButton; NSSlider *_temperatureSlider; NSSlider *_interferenceSlider; + NSButton *_autoUpdatesCheckbox; + } + (NSArray *)filterList @@ -381,6 +383,12 @@ } +- (IBAction)changeAutoUpdates:(id)sender +{ + [[NSUserDefaults standardUserDefaults] setBool: [(NSButton *)sender state] == NSOnState + forKey:@"GBAutoUpdatesEnabled"]; +} + - (IBAction) configureJoypad:(id)sender { [self.configureJoypadButton setEnabled:NO]; @@ -705,4 +713,15 @@ } [[NSUserDefaults standardUserDefaults] setObject:default_joypads forKey:@"JoyKitDefaultControllers"]; } + +- (NSButton *)autoUpdatesCheckbox +{ + return _autoUpdatesCheckbox; +} + +- (void)setAutoUpdatesCheckbox:(NSButton *)autoUpdatesCheckbox +{ + _autoUpdatesCheckbox = autoUpdatesCheckbox; + [_autoUpdatesCheckbox setState: [[NSUserDefaults standardUserDefaults] boolForKey:@"GBAutoUpdatesEnabled"]]; +} @end diff --git a/Cocoa/Preferences.xib b/Cocoa/Preferences.xib index ce3cb7c..b1d67f7 100644 --- a/Cocoa/Preferences.xib +++ b/Cocoa/Preferences.xib @@ -13,6 +13,9 @@ + + + @@ -49,17 +52,24 @@ + + + + + + + @@ -694,17 +704,6 @@ - + + + + + + + + + + + + + + + diff --git a/Cocoa/UpdateWindow.xib b/Cocoa/UpdateWindow.xib new file mode 100644 index 0000000..e34f8f2 --- /dev/null +++ b/Cocoa/UpdateWindow.xib @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Makefile b/Makefile index fcaae75..2c75bd0 100644 --- a/Makefile +++ b/Makefile @@ -122,6 +122,9 @@ endif CFLAGS += $(WARNINGS) CFLAGS += -std=gnu11 -D_GNU_SOURCE -DVERSION="$(VERSION)" -I. -D_USE_MATH_DEFINES +ifneq (,$(UPDATE_SUPPORT)) +CFLAGS += -DUPDATE_SUPPORT +endif ifeq (,$(PKG_CONFIG)) SDL_CFLAGS := $(shell sdl2-config --cflags)