Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e5d146ba1 | ||
|
|
cdc0729c8b | ||
|
|
a3a5b3f03b | ||
|
|
44468f858c | ||
|
|
adbe40a86a | ||
|
|
b5ce589c4c | ||
|
|
b426994d83 | ||
|
|
279b5f9eb8 | ||
|
|
20a17327a6 | ||
|
|
2a001cc575 | ||
|
|
9a763fdab6 | ||
|
|
c3e3bddac9 | ||
|
|
3b419eb2e3 | ||
|
|
cb838776ce | ||
|
|
8a5c88c9fa | ||
|
|
91f86ea8da | ||
|
|
64c3fbe198 | ||
|
|
9a30a196ec | ||
|
|
2abaad19b6 | ||
|
|
7e5dab08d1 | ||
|
|
d7233e04a3 | ||
|
|
c80f006649 | ||
|
|
23f184c1dd | ||
|
|
db58a91497 | ||
|
|
3f398df659 | ||
|
|
8536201df9 | ||
|
|
417f4d8e91 | ||
|
|
3053fa59e2 | ||
|
|
1d2388b30f |
+1
-1
@@ -1,4 +1,4 @@
|
||||
osx_image: xcode8.3
|
||||
osx_image: xcode9.4
|
||||
language: objective-c
|
||||
|
||||
install: script/bootstrap
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user