29 Commits
Author SHA1 Message Date
Samuel Attard 0e5d146ba1 align terminationHandler with dealloc in SQRLZipArchiver (#259)
For some reason even though the `NSTask` is terminated the file handle is still being read.  Fixes #247

Verified locally using the electron test suite and `chown nobody:nogroup Electron.app` during the test suite
2021-06-18 15:08:36 -07:00
Devin Foley cdc0729c8b fix: try multiple times to get a writeable log file (#255) 2020-11-03 13:05:04 -08:00
Samuel Attard a3a5b3f03b fix: use new ShipIt binary on macOS 11.0 to workaround flaw in launchApplicationAtURL (#253)
* fix: use /usr/bin/open on macOS 11.0 to workaround flaw in launchApplicationAtURL

* Use the new ShipIt as the launch pad

* chore: clean up diff

* Update cibuild

* build: use xcode 9.4
2020-10-09 15:52:19 -07:00
Samuel Attard 44468f858c chore: fix missing deallocs, deprecation warnings and invalid casts (#249) 2020-06-29 16:03:32 -07:00
Devin Foley adbe40a86a Set a 20 minute download timeout. (#241) 2019-11-05 13:29:29 -08:00
Samuel Attard b5ce589c4c Revert "fix: link ShipIt to ReactiveCocoa and Mantle correctly (#235)"
This reverts commit b426994d83.
2019-05-15 19:13:30 -07:00
Samuel Attard b426994d83 fix: link ShipIt to ReactiveCocoa and Mantle correctly (#235) 2019-03-25 13:49:42 -07:00
Josh Abernathy 279b5f9eb8 Merge pull request #231 from olegsklyarov/patch-1
fix jump link to update-file-json-format
2018-10-26 12:13:53 -04:00
Oleg Sklyarov 20a17327a6 fix jump link to update-file-json-format 2018-10-26 18:27:47 +03:00
Josh Abernathy 2a001cc575 Merge pull request #227 from tonyganch/xcode_build
Fix build for Xcode 9
2018-07-02 09:40:21 -04:00
Josh Abernathy 9a763fdab6 Merge pull request #225 from tonyganch/patch-1
Update README.md
2018-07-02 09:23:49 -04:00
Tony Ganch c3e3bddac9 Fix build for Xcode 9 2018-07-02 09:16:26 +02:00
Tony Ganch 3b419eb2e3 Update README.md
Reflect correct path to local checkouts of ReactiveCocoa and Mantle.
2018-06-01 21:02:38 +02:00
Josh Abernathy cb838776ce Merge pull request #221 from MarshallOfSound/patch-1
Fix invalid JSON in readme
2018-02-05 05:53:30 -10:00
Samuel Attard 8a5c88c9fa Fix invalid JSON in readme 2018-01-13 14:35:26 +11:00
Josh Abernathy 91f86ea8da Merge pull request #219 from MarshallOfSound/cdn-releases-fixed
Add CDN based releases
2018-01-12 16:27:36 -05:00
Samuel Attard 64c3fbe198 remove timeout 2018-01-12 15:34:19 +11:00
Thomas Moenicke 9a30a196ec submodules as in master 2018-01-11 02:09:03 +11:00
Samuel Attard 2abaad19b6 Document JSON structure 2018-01-11 02:07:27 +11:00
Samuel Attard 7e5dab08d1 Code cleanup 2018-01-11 02:01:38 +11:00
Samuel Attard d7233e04a3 Handle bad JSON and fix usage of NSURLRequest 2018-01-10 11:44:08 +11:00
Thomas Moenicke c80f006649 keep existing signatures for initWithUpdateRequest 2018-01-10 11:44:08 +11:00
Thomas Moenicke 23f184c1dd cleanup 2018-01-10 11:44:08 +11:00
Thomas Moenicke db58a91497 adding api to init SQRLi to init in CDN mode 2018-01-10 11:44:08 +11:00
Thomas Moenicke 3f398df659 using env variables instead of query parameter to switch between release server and json file mode
use: MODE=JSONFILE VERSION=1.5.0 open MyApp.app
defaults to release server
2018-01-10 11:44:08 +11:00
Thomas Moenicke 8536201df9 removed support for text based releases file
SquirrelApp is now an external project
2018-01-10 11:44:08 +11:00
Thomas Moenicke 417f4d8e91 adding cdn-based updates: a file (simple text or json) containing release information together with release files on a cdn 2018-01-10 11:44:08 +11:00
Josh Abernathy 3053fa59e2 Merge pull request #217 from pornel/master
Suggest only secure URLs for app download
2017-10-27 08:19:38 -04:00
Kornel 1d2388b30f Suggest only secure URLs for app download 2017-10-27 00:21:14 +01:00
14 changed files with 475 additions and 51 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
osx_image: xcode8.3
osx_image: xcode9.4
language: objective-c
install: script/bootstrap
+39 -5
View File
@@ -47,7 +47,7 @@ version as Squirrel.
Otherwise, add a target dependency and Copy Files build phase entry for the
ReactiveCocoa.framework target included in Squirrel's repository, in
External/ReactiveCocoa.
`Carthage/Checkouts/ReactiveCocoa`.
Similarly, ensure your application includes Mantle, or copies in the Squirrel
version.
@@ -66,7 +66,7 @@ Once Squirrel is added to your project, you need to configure and start it.
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSURLComponents *components = [[NSURLComponents alloc] init];
components.scheme = @"http";
components.scheme = @"https";
components.host = @"mycompany.com";
components.path = @"/myapp/latest";
@@ -140,7 +140,7 @@ Your server should determine whether an update is required based on the
If an update is required your server should respond with a status code of
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
[update JSON](#update-json-format) in the body. Squirrel **will** download and
[update JSON](#update-server-json-format) in the body. Squirrel **will** download and
install this update, even if the version of the update is the same as the
currently running version. To save redundantly downloading the same version
multiple times your server must not inform the client to update.
@@ -149,14 +149,14 @@ If no update is required your server must respond with a status code of
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
will check for an update again at the interval you specify.
## Update JSON Format
## Update Server JSON Format
When an update is available, Squirrel expects the following schema in response
to the update request provided:
```json
{
"url": "http://mycompany.com/myapp/releases/myrelease",
"url": "https://mycompany.example.com/myapp/releases/myrelease",
"name": "My Release Name",
"notes": "Theses are some release notes innit",
"pub_date": "2013-09-18T12:29:53+01:00"
@@ -172,6 +172,40 @@ appropriate format.
"pub_date" if present must be formatted according to ISO 8601.
## Update File JSON Format
The alternate update technique uses a plain JSON file meaning you can store your
update metadata on S3 or another static file store. The format of this file is
detailed below:
```json
{
"currentRelease": "1.2.3",
"releases": [
{
"version": "1.2.1",
"updateTo": {
"version": "1.2.1",
"pub_date": "2013-09-18T12:29:53+01:00",
"notes": "Theses are some release notes innit",
"name": "1.2.1",
"url": "https://mycompany.example.com/myapp/releases/myrelease"
}
},
{
"version": "1.2.3",
"updateTo": {
"version": "1.2.3",
"pub_date": "2014-09-18T12:29:53+01:00",
"notes": "Theses are some more release notes innit",
"name": "1.2.3",
"url": "https://mycompany.example.com/myapp/releases/myrelease3"
}
}
]
}
```
# User Interface
Squirrel does not provide any GUI components for presenting updates. If you want
+4
View File
@@ -1577,6 +1577,7 @@
baseConfigurationReference = D0C22C1A179CC02E00158214 /* Mac-Framework.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = NO;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
FRAMEWORK_VERSION = A;
@@ -1595,6 +1596,7 @@
baseConfigurationReference = D0C22C1A179CC02E00158214 /* Mac-Framework.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = NO;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
FRAMEWORK_VERSION = A;
@@ -1666,6 +1668,7 @@
baseConfigurationReference = D0C22C1A179CC02E00158214 /* Mac-Framework.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = NO;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
FRAMEWORK_VERSION = A;
@@ -1716,6 +1719,7 @@
baseConfigurationReference = D0C22C1A179CC02E00158214 /* Mac-Framework.xcconfig */;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = NO;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = NO;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
FRAMEWORK_VERSION = A;
@@ -68,6 +68,16 @@
value = "com.github.Squirrel.SquirrelTests"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "SQUIRREL_TEST_LOCAL_CDN"
value = "True"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "SQUIRREL_TEST_LOCAL_SERVER"
value = "True"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
+39 -18
View File
@@ -66,31 +66,52 @@
setNameWithFormat:@"%@ -storageURL", self];
}
- (RACSignal *)shipItStateURL {
// Returns a signal to a URL with the given filename and job name inside of the storage folder
//
// filename - The name of the file in the storage folder to return in this URL
// jobname - The name to give the returned RACSignal
// ensureWritable - If set to true, we will append an integer to the end of the filename in order to find a writeable URL.
// This fixes an issue wherein the stdout and stderr files may have been owned by root, and thus prevented ShipIt from launching.
- (RACSignal *)URLForFileNamed:(NSString *)filename withJobNamed:(NSString *)jobname ensureWritable:(BOOL)ensureWritable {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipItState.plist"];
}]
setNameWithFormat:@"%@ -shipItStateURL", self];
storageURL]
map:^(NSURL *folderURL) {
NSURL *fileURL = [folderURL URLByAppendingPathComponent:filename];
if (ensureWritable) {
int attempts = 0;
while (attempts < 100) {
NSNumber *writable = nil;
NSError *writableError = nil;
BOOL gotWritable = [fileURL getResourceValue:&writable forKey:NSURLIsWritableKey error:&writableError];
if (!gotWritable || writable.boolValue) {
return fileURL;
}
attempts++;
fileURL = [folderURL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%i", filename, attempts]];
}
NSLog(@"Error: Unable to find writable log file after 100 attempts.");
}
return fileURL;
}]
setNameWithFormat:@"%@ -%@", self, jobname];
}
- (RACSignal *)shipItStateURL {
return [self URLForFileNamed:@"ShipItState.plist" withJobNamed:@"shipItStateURL" ensureWritable:false];
}
- (RACSignal *)shipItStdoutURL {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipIt_stdout.log"];
}]
setNameWithFormat:@"%@ -shipItStdoutURL", self];
return [self URLForFileNamed:@"ShipIt_stdout.log" withJobNamed:@"shipItStdoutURL" ensureWritable:true];
}
- (RACSignal *)shipItStderrURL {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipIt_stderr.log"];
}]
setNameWithFormat:@"%@ -shipItStderrURL", self];
return [self URLForFileNamed:@"ShipIt_stderr.log" withJobNamed:@"shipItStderrURL" ensureWritable:true];
}
#pragma mark NSObject
+1 -1
View File
@@ -517,7 +517,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
setNameWithFormat:@"%@ -installItemAtURL: %@ fromURL: %@", self, targetURL, sourceURL];
}
#pragma Quarantine Bit Removal
#pragma mark Quarantine Bit Removal
- (RACSignal *)clearQuarantineForDirectory:(NSURL *)directory {
NSParameterAssert(directory != nil);
+5
View File
@@ -127,6 +127,9 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
setNameWithFormat:@"+shipItAuthorization"];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ (RACSignal *)launchPrivileged:(BOOL)privileged {
return [[[RACSignal
zip:@[
@@ -157,4 +160,6 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
setNameWithFormat:@"+launchPrivileged: %i", (int)privileged];
}
#pragma clang diagnostic pop
@end
+36
View File
@@ -67,6 +67,12 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
@class RACDisposable;
@class RACSignal;
/// Type of mode used to download the release
typedef enum {
JSONFILE=1,
RELEASESERVER
} SQRLUpdaterMode;
// Checks for, downloads, and installs updates.
@interface SQRLUpdater : NSObject
@@ -123,6 +129,19 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// Returns the initialized `SQRLUpdater`.
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest;
// Initializes an updater that will send the given request to check for updates
// on a CDN reading a release file in json format.
//
// updateRequest - A request to send to check for updates. This request can be
// customized as desired, like by including an `Authorization`
// header to authenticate with a private update server, or
// pointing to a local URL for testing. This must not be nil.
// forVersion - the currently installed version
//
// Returns the initialized `SQRLUpdater`.
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest
forVersion:(NSString*)version;
// Initializes an updater that will send the given request to check for updates
// and passes a block to provide requests for the update downloads.
//
@@ -135,6 +154,20 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// Returns the initialized `SQRLUpdater`.
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload;
// Initializes an updater that will send the given request to check for updates
// and passes a block to provide requests for the update downloads.
//
// updateRequest - Same as with initWithUpdateRequest
// requestForDownload - Once the update url is found for the update download, allow
// providing custom requests that can be costomized as desired.
// Useful for including `Authorization` headers just like the
// updateRequest param.
// forVersion - currently running version
// useMode - either RELEASESERVER or JSONFILE
//
// Returns the initialized `SQRLUpdater`.
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload forVersion:(NSString*) version useMode:(SQRLUpdaterMode) mode;
// Executes `checkForUpdatesCommand` (if enabled) every `interval` seconds.
//
// The first check will not occur until `interval` seconds have passed.
@@ -160,6 +193,9 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// wrong before termination. The signal will never complete.
- (RACSignal *)relaunchToInstallUpdate;
- (BOOL)isRunningOnReadOnlyVolume;
- (RACSignal *)updateFromJSONData:(NSData *)data;
@end
@interface SQRLUpdater (Unavailable)
+97 -16
View File
@@ -36,6 +36,8 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
/// The application's being run on a read-only volume.
const NSInteger SQRLUpdaterErrorReadOnlyVolume = 8;
const NSTimeInterval SQURLUpdaterZipDownloadTimeoutSeconds = 20 * 60;
// The prefix used when creating temporary directories for updates. This will be
// followed by a random string of characters.
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@@ -149,17 +151,41 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
}];
}
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest forVersion: (NSString*) version {
return [self initWithUpdateRequest:updateRequest requestForDownload:^(NSURL *downloadURL) {
return [NSURLRequest requestWithURL:downloadURL];
} forVersion:version useMode:JSONFILE];
}
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload {
return [self initWithUpdateRequest:updateRequest requestForDownload:^(NSURL *downloadURL) {
return [NSURLRequest requestWithURL:downloadURL];
} forVersion:nil useMode:RELEASESERVER];
}
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload
forVersion:(NSString*) version useMode:(SQRLUpdaterMode) mode {
//! download simple file
NSParameterAssert(updateRequest != nil);
NSParameterAssert(requestForDownload != nil);
if (mode == JSONFILE) {
NSParameterAssert(version != nil);
}
self = [super init];
if (self == nil) return nil;
_requestForDownload = [requestForDownload copy];
_updateRequest = [updateRequest copy];
_updateClass = SQRLUpdate.class;
NSMutableURLRequest* mutableUpdateRequest = [updateRequest mutableCopy];
if (mode == JSONFILE) {
mutableUpdateRequest.cachePolicy = NSURLRequestReloadIgnoringCacheData;
}
_updateRequest = mutableUpdateRequest;
_updateClass = SQRLUpdate.class;
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
if (_signature == nil) {
@@ -185,6 +211,8 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return [[[[[[[[self
performHousekeeping]
//! get file from server
then:^{
self.state = SQRLUpdaterStateCheckingForUpdate;
@@ -193,19 +221,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
BOOL readOnlyVolume = [self isRunningOnReadOnlyVolume];
if (readOnlyVolume) {
@@ -216,8 +231,72 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorReadOnlyVolume userInfo:errorInfo];
return [RACSignal error:error];
}
}
if (mode == RELEASESERVER) {
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
} else if (mode == JSONFILE) {
NSError *error = nil;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:bodyData options:0 error:&error];
if (dict == nil) {
NSMutableDictionary *userInfo = [error.userInfo mutableCopy] ?: [NSMutableDictionary dictionary];
userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Update check failed", nil);
userInfo[NSLocalizedRecoverySuggestionErrorKey] = NSLocalizedString(@"The server sent an invalid response. Try again later.", nil);
userInfo[SQRLUpdaterServerDataErrorKey] = bodyData;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerBody userInfo:userInfo]];
}
NSString *currentRelease = dict[@"currentRelease"];
if(currentRelease) {
//! if CDN points to the currently running version as the latest version, bail out
if([currentRelease isEqualToString:version]) {
NSLog(@"The running client is already the latest version.");
return [RACSignal empty];
}
if ([version compare:currentRelease options:NSNumericSearch] == NSOrderedDescending) {
// currentRelease is lower than version.
// Might be a new version for testing that is not deployed yet
// no roll back
NSLog(@"The running client is newer than the latest deployed release. Not downgrading.");
return [RACSignal empty];
}
//! @todo find latest
NSArray *releases = dict[@"releases"];
for(NSDictionary* release in releases) {
if([currentRelease isEqualToString:release[@"version"]]) {
bodyData = [NSJSONSerialization dataWithJSONObject:release[@"updateTo"]
options:0 error:&error];
break;
}
}
}
}
}
if (bodyData == nil) {
NSMutableDictionary *userInfo = [error.userInfo mutableCopy] ?: [NSMutableDictionary dictionary];
userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Update check failed", nil);
userInfo[NSLocalizedRecoverySuggestionErrorKey] = NSLocalizedString(@"The server sent an invalid response. Try again later.", nil);
userInfo[SQRLUpdaterServerDataErrorKey] = bodyData;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerBody userInfo:userInfo]];
}
return [RACSignal return:bodyData];
}]
flatten]
@@ -405,6 +484,8 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
[zipDownloadRequest setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
[zipDownloadRequest setTimeoutInterval:SQURLUpdaterZipDownloadTimeoutSeconds];
return [[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
+2 -1
View File
@@ -54,7 +54,7 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
_dittoTask = [[NSTask alloc] init];
_dittoTask.launchPath = @"/usr/bin/ditto";
_dittoTask.environment = @{ @"DITTOABORT": @1 };
_dittoTask.environment = @{ @"DITTOABORT": @"1" };
_dittoTask.standardError = self.standardErrorPipe;
@weakify(self);
@@ -63,6 +63,7 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
if (self == nil) return;
[self->_taskTerminated sendNext:@(task.terminationStatus)];
[self.standardErrorPipe.fileHandleForReading closeFile];
};
RACSubject *errorDataChunks = [[RACSubject subject] setNameWithFormat:@"errorDataChunks"];
+46 -8
View File
@@ -27,6 +27,8 @@
// updating will abort.
static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
static NSString * launchSignal = @"___launch___";
// The domain for errors generated here.
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
@@ -113,19 +115,43 @@ static void installRequest(RACSignal *readRequestSignal, NSString *applicationId
action = [[action
deliverOn:RACScheduler.mainThreadScheduler]
doNext:^(SQRLShipItRequest *finalRequest) {
NSLog(@"On main thread and launching: %@", finalRequest.targetBundleURL);
NSURL *bundleURL = finalRequest.targetBundleURL;
if (bundleURL == nil) {
NSLog(@"Missing target bundle URL, cannot launch application");
return;
}
NSError *error;
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:@{} error:&error]) {
NSLog(@"Could not launch application at %@: %@", bundleURL, error);
return;
}
NSLog(@"Bundle URL is valid");
NSLog(@"Application launched at %@", bundleURL);
NSError *error;
// Temporary workaround, on Big Sur and higher the executable
// using NSWorkspace needs to actually exist on disk, at this point
// this executable no longer exists on disk so we need to launch the
// new one (which should be in the exact same spot) and ask for it
// to launch the new app bundle URL
if (@available(macOS 11.0, *)) {
NSLog(@"Attempting to launch app on 11.0 or higher");
NSString *exe = NSProcessInfo.processInfo.arguments[0];
NSLog(@"Launching new ShipIt at %@ with instructions to launch %@", exe, bundleURL);
NSTask *task = [[NSTask alloc] init];
[task setLaunchPath: exe];
[task setArguments: @[launchSignal, bundleURL.path]];
[task launch];
[task waitUntilExit];
NSLog(@"New ShipIt exited");
} else {
NSLog(@"Attempting to launch app on lower than 11.0");
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:@{} error:&error]) {
NSLog(@"Could not launch application at %@: %@", bundleURL, error);
return;
}
NSLog(@"Application launched at %@", bundleURL);
}
}];
}
@@ -153,9 +179,21 @@ int main(int argc, const char * argv[]) {
char const *jobLabel = argv[1];
const char *statePath = argv[2];
NSURL *shipItStateURL = [NSURL fileURLWithPath:@(statePath)];
installRequest([SQRLShipItRequest readUsingURL:[RACSignal return:shipItStateURL]], @(jobLabel));
dispatch_main();
if (strcmp(jobLabel, [launchSignal UTF8String]) == 0) {
NSLog(@"Detected this as a launch request");
NSError *error;
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:shipItStateURL options:NSWorkspaceLaunchDefault configuration:@{} error:&error]) {
NSLog(@"Could not launch application at %@: %@", shipItStateURL, error);
} else {
NSLog(@"Successfully launched application at %@", shipItStateURL);
}
exit(EXIT_SUCCESS);
} else {
NSLog(@"Detected this as an install request");
installRequest([SQRLShipItRequest readUsingURL:[RACSignal return:shipItStateURL]], @(jobLabel));
dispatch_main();
}
}
return EXIT_SUCCESS;
+47
View File
@@ -0,0 +1,47 @@
***
Test updates via release server or cdn-based updates
***
Control test behavior via env variables:
SQUIRREL_TEST_LOCAL_SERVER=True
SQUIRREL_LOCAL_SERVER_URL=http://localhost:8123/update/osx/1.0.1-stable/stable
SQUIRREL_TEST_LOCAL_CDN=True
SQUIRREL_CDN_URL=@"http://localhost/RELEASES.json"
Test Setup:
Create a fake dmg release file
Release Server:
git clone https://github.com/ArekSredzki/electron-release-server.git
refer to their doc, here is a short summary:
- install a local PostgreSQL, any default setup will do it
- edit config/local.js to match your PostgreSQL setup
- npm start
point your browser to the server and upload your release dmg file
CDN:
setup a local webserver such as apache or nginx and copy your dmg file there
create a RELEASE.json file like this:
{
"currentRelease" : "1.0.1",
"releases" : [
{
"version" : "1.0.1",
"updateTo" : {
"pub_date" : "2017-03-09T15:24:55-05:00",
"notes" : "the next release via CDN",
"name" : "TestApplication_1.0.1",
"url" : "http:\/\/localhost/TestApplication_1.0.1.dmg",
"version" : "0.0.145-alpha"
}
}
]
}
+147
View File
@@ -12,12 +12,31 @@
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
#import "SQRLUpdater.h"
#import "SQRLZipArchiver.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLTestUpdate.h"
#import "TestAppConstants.h"
#import <objc/objc-class.h>
//! force Updater to believe we are not on readonly, to continue downloading the release
bool isRunningOnReadOnlyVolumeImp(id self, SEL _cmd)
{
return false;
}
//! we don't want updateFromJSONData to be executed. It is enough to know if it was called
bool updateFromJSONDataIsCalled = false;
RACSignal * updateFromJSONDataImp(id self, SEL _cmd, NSData * data)
{
NSString* str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"updateFromJSONDataImp called with %@", str);
updateFromJSONDataIsCalled = true;
return false;
}
QuickSpecBegin(SQRLUpdaterSpec)
@@ -59,6 +78,134 @@ beforeEach(^{
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
});
describe(@"checkForUpdatesCommand", ^{
/**
control test behavior via env variables:
SQUIRREL_TEST_LOCAL_SERVER=True
SQUIRREL_LOCAL_SERVER_URL=http://localhost:8123/update/osx/1.0.1-stable/stable
SQUIRREL_TEST_LOCAL_CDN=True
SQUIRREL_CDN_URL=@"http://localhost/RELEASES.json"
*/
OHHTTPStubs *stubsJson = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL.absoluteString isEqualToString:@"http://localhost/RELEASES.json?method=Json"];
} withStubResponse:^(NSURLRequest *request) {
NSDictionary *newReleaseJSON = @{
@"version": @"0.0.145",
@"name": @"my-stub-release",
@"notes": @"mock release for automated tests, json",
@"pub_date": @"2017-03-09T15:24:55-05:00",
@"url": @"http://localhost/myapp-0.0.145.zip"
};
NSError * err;
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:newReleaseJSON options:0 error:&err];
return [OHHTTPStubsResponse responseWithData:jsonData statusCode:200 responseTime:0 headers:nil];
}];
OHHTTPStubs *stubsReleaseServer = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL.absoluteString isEqualToString:@"http://localhost:8123/update/osx/1.0.1-stable/stable?method=ReleaseServer"];
} withStubResponse:^(NSURLRequest *request) {
NSDictionary *newReleaseJSON = @{
@"version": @"0.0.145",
@"name": @"my-stub-release",
@"notes": @"mock release for automated tests, release server",
@"pub_date": @"2017-03-09T15:24:55-05:00",
@"url": @"http://localmyappng-0.0.145.zip"
};
NSError * err;
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:newReleaseJSON options:0 error:&err];
return [OHHTTPStubsResponse responseWithData:jsonData statusCode:200 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubsJson];
[OHHTTPStubs removeRequestHandler:stubsReleaseServer];
}];
BOOL testLocalServer = [[[[NSProcessInfo processInfo]environment]objectForKey:@"SQUIRREL_TEST_LOCAL_SERVER"] boolValue];
NSString* localServerURL = [[[NSProcessInfo processInfo]environment]objectForKey:@"SQUIRREL_LOCAL_SERVER_URL"];
if(!localServerURL) {
localServerURL = @"http://localhost:8123/update/osx/1.0.1-stable/stable?method=ReleaseServer";
}
BOOL testLocalCdn = [[[[NSProcessInfo processInfo]environment]objectForKey:@"SQUIRREL_TEST_LOCAL_CDN"] boolValue];
NSString* localCdnURL = [[[NSProcessInfo processInfo]environment]objectForKey:@"SQUIRREL_CDN_URL"];
if(!localCdnURL) {
localCdnURL = @"http://localhost/RELEASES.json?method=Json";
}
NSLog(@"testLocalServer %d %@", testLocalServer, localServerURL);
NSLog(@"testLocalCdn %d %@", testLocalCdn, localCdnURL);
__block SQRLUpdater *updater = nil;
__block NSURLRequest *localRequest = nil;
it(@"Squirrel should work with a release server", ^{
if(testLocalServer) {
updateFromJSONDataIsCalled = false;
//! setup the updater
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:localServerURL]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
//! replace updateFromJSONData() and isRunningOnReadOnlyVolume() methods
method_setImplementation(class_getInstanceMethod([SQRLUpdater class]
, @selector(updateFromJSONData:))
, (IMP) updateFromJSONDataImp);
method_setImplementation(class_getInstanceMethod([SQRLUpdater class]
, @selector(isRunningOnReadOnlyVolume))
, (IMP) isRunningOnReadOnlyVolumeImp);
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
//! now check the results
expect((int)updater.state).toEventually(equal((int)SQRLUpdaterStateIdle));
expect( (BOOL) updateFromJSONDataIsCalled ).to(beTrue());
expect( (BOOL) result ).to(beTrue());
}
});
it(@"Squirrel should work with a CDN", ^{
if(testLocalCdn) {
updateFromJSONDataIsCalled = false;
//! setup the updater
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:localCdnURL]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
//! replace updateFromJSONData() and isRunningOnReadOnlyVolume() methods
method_setImplementation(class_getInstanceMethod([SQRLUpdater class]
, @selector(updateFromJSONData:))
, (IMP) updateFromJSONDataImp);
method_setImplementation(class_getInstanceMethod([SQRLUpdater class]
, @selector(isRunningOnReadOnlyVolume))
, (IMP) isRunningOnReadOnlyVolumeImp);
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
//! now check the results
expect((int)updater.state).toEventually(equal((int)SQRLUpdaterStateIdle));
expect( (BOOL) updateFromJSONDataIsCalled ).to(beTrue());
expect( (BOOL) result ).to(beTrue());
}
});
});
describe(@"updating", ^{
__block NSURL *updateURL;
+1 -1
View File
@@ -112,7 +112,7 @@ build_scheme ()
echo "*** Building Squirrel..."
echo
xcodebuild -scheme Squirrel -workspace Squirrel.xcworkspace build >/dev/null || exit $?
xcodebuild -scheme Squirrel -workspace Squirrel.xcworkspace build
}
export -f build_scheme