feat: add new Squirrel.Mac bundle installation method behind flag (#33470)
This commit is contained in:
parent
4c988a5a24
commit
479f652f90
3 changed files with 190 additions and 1 deletions
|
@ -1,3 +1,4 @@
|
|||
build_add_gn_config.patch
|
||||
fix_ensure_that_self_is_retained_until_the_racsignal_is_complete.patch
|
||||
fix_use_kseccschecknestedcode_kseccsstrictvalidate_in_the_sec.patch
|
||||
feat_add_new_squirrel_mac_bundle_installation_method_behind_flag.patch
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Samuel Attard <samuel.r.attard@gmail.com>
|
||||
Date: Mon, 28 Mar 2022 02:43:18 -0700
|
||||
Subject: feat: add new Squirrel.Mac bundle installation method behind flag
|
||||
|
||||
The 'SquirrelMacEnableDirectContentsWrite' user default in your apps defaults suite
|
||||
can be used to control this new installation method. It is designed to remove the
|
||||
requirement that the updating process have write access to its parent directory.
|
||||
|
||||
With this feature enabled the updating process only needs write access to it's own
|
||||
.app bundle folder, not the owning /Applications folder. This should allow more
|
||||
non-admin users to update applications when appropriately granted group based permissions.
|
||||
|
||||
E.g. 775 and chown :staff
|
||||
|
||||
diff --git a/Squirrel/SQRLInstaller.m b/Squirrel/SQRLInstaller.m
|
||||
index 7dd98ddee4ae0f4e01fd7aaa3486083bff7d0da1..c1f328fa8c3689218ef260347cb8f9d30b789efe 100644
|
||||
--- a/Squirrel/SQRLInstaller.m
|
||||
+++ b/Squirrel/SQRLInstaller.m
|
||||
@@ -249,6 +249,7 @@ - (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request {
|
||||
] reduce:^(NSURL *directoryURL, SQRLCodeSignature *codeSignature) {
|
||||
NSURL *targetBundleURL = request.targetBundleURL;
|
||||
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
|
||||
+ [NSFileManager.defaultManager createDirectoryAtURL:newBundleURL withIntermediateDirectories:FALSE attributes:nil error:nil];
|
||||
|
||||
return [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:request.targetBundleURL temporaryURL:newBundleURL codeSignature:codeSignature];
|
||||
}]
|
||||
@@ -481,10 +482,50 @@ - (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
|
||||
NSParameterAssert(targetURL != nil);
|
||||
NSParameterAssert(sourceURL != nil);
|
||||
|
||||
+ NSLog(@"Moving bundle from %@ to %@", sourceURL, targetURL);
|
||||
+
|
||||
+ // If both the sourceURL and the targetURL exist we can try to skip a permissions check
|
||||
+ // by moving Thing.app/Contents directly. This allows us to update applications without
|
||||
+ // permission to write files into the parent directory of Thing.app
|
||||
+ //
|
||||
+ // There is no known case where these directories don't exist but in order to handle
|
||||
+ // edge cases / race conditions we'll handle it anyway.
|
||||
+ //
|
||||
+ // This exists check is non-atomic with the rename call below but that's OK
|
||||
+ BOOL canRenameContentsDirectly = FALSE;
|
||||
+ // For now while this is tested at scale this new option is behind a user default, this
|
||||
+ // can be set by applications wishing to test this feature at runtime. If it causes issues
|
||||
+ // it can be opted out by individual users by setting this key to false explicitly.
|
||||
+ // Once this has bene tested at scale it will become the default for all Squirrel.Mac
|
||||
+ // users.
|
||||
+ NSUserDefaults *defaults = [[NSUserDefaults alloc] init];
|
||||
+ [defaults addSuiteNamed:_applicationIdentifier];
|
||||
+ // In cases where this code is being executed under the ShipIt executable it's running
|
||||
+ // under an application identifier equal to {parent_identifier}.ShipIt
|
||||
+ // In this case we need to use the true parent identifier too as that is 99% of the time
|
||||
+ // where the key will be set.
|
||||
+ if ([_applicationIdentifier hasSuffix:@".ShipIt"]) {
|
||||
+ [defaults addSuiteNamed:[_applicationIdentifier substringToIndex:[_applicationIdentifier length] - 7]];
|
||||
+ }
|
||||
+
|
||||
+ if ([defaults boolForKey:@"SquirrelMacEnableDirectContentsWrite"]) {
|
||||
+ canRenameContentsDirectly = [NSFileManager.defaultManager fileExistsAtPath:targetURL.path] && [NSFileManager.defaultManager fileExistsAtPath:sourceURL.path];
|
||||
+
|
||||
+ if (canRenameContentsDirectly) {
|
||||
+ NSLog(@"Moving bundles via 'Contents' folder rename");
|
||||
+ } else {
|
||||
+ NSLog(@"Moving bundles directly as one of source / target does not exist. This is unexpected.");
|
||||
+ }
|
||||
+ } else {
|
||||
+ NSLog(@"Moving bundles directly as SquirrelMacEnableDirectContentsWrite is disabled for app: %@", _applicationIdentifier);
|
||||
+ }
|
||||
+ NSURL *targetContentsURL = canRenameContentsDirectly ? [targetURL URLByAppendingPathComponent:@"Contents"] : targetURL;
|
||||
+ NSURL *sourceContentsURL = canRenameContentsDirectly ? [sourceURL URLByAppendingPathComponent:@"Contents"] : sourceURL;
|
||||
+
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
// rename() is atomic, NSFileManager sucks.
|
||||
- if (rename(sourceURL.path.fileSystemRepresentation, targetURL.path.fileSystemRepresentation) == 0) {
|
||||
+ if (rename(sourceContentsURL.path.fileSystemRepresentation, targetContentsURL.path.fileSystemRepresentation) == 0) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
int code = errno;
|
||||
@@ -497,24 +538,24 @@ - (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
|
||||
}
|
||||
}]
|
||||
doCompleted:^{
|
||||
- NSLog(@"Moved bundle from %@ to %@", sourceURL, targetURL);
|
||||
+ NSLog(@"Moved bundle contents from %@ to %@", sourceContentsURL, targetContentsURL);
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
if (![error.domain isEqual:NSPOSIXErrorDomain] || error.code != EXDEV) return [RACSignal error:error];
|
||||
|
||||
// If the locations lie on two different volumes, remove the
|
||||
// destination by hand, then perform a move.
|
||||
- [NSFileManager.defaultManager removeItemAtURL:targetURL error:NULL];
|
||||
+ [NSFileManager.defaultManager removeItemAtURL:targetContentsURL error:NULL];
|
||||
|
||||
- if ([NSFileManager.defaultManager moveItemAtURL:sourceURL toURL:targetURL error:&error]) {
|
||||
- NSLog(@"Moved bundle across volumes from %@ to %@", sourceURL, targetURL);
|
||||
+ if ([NSFileManager.defaultManager moveItemAtURL:sourceContentsURL toURL:targetContentsURL error:&error]) {
|
||||
+ NSLog(@"Moved bundle contents across volumes from %@ to %@", sourceContentsURL, targetContentsURL);
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
- NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Couldn't move bundle %@ across volumes to %@", nil), sourceURL, targetURL];
|
||||
+ NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Couldn't move bundle contents %@ across volumes to %@", nil), sourceContentsURL, targetContentsURL];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorMovingAcrossVolumes toError:error]];
|
||||
}
|
||||
}]
|
||||
- setNameWithFormat:@"%@ -installItemAtURL: %@ fromURL: %@", self, targetURL, sourceURL];
|
||||
+ setNameWithFormat:@"%@ -installItemAtURL: %@ fromURL: %@", self, targetContentsURL, sourceContentsURL];
|
||||
}
|
||||
|
||||
#pragma mark Quarantine Bit Removal
|
||||
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
|
||||
index c81c820d61da3c7d1cfd2c516147c954a5773a0c..4c703159a2bb0239b7d4e1793a985b5ec2edcfa9 100644
|
||||
--- a/Squirrel/SQRLUpdater.m
|
||||
+++ b/Squirrel/SQRLUpdater.m
|
||||
@@ -329,7 +329,12 @@ - (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQ
|
||||
|
||||
BOOL targetWritable = [self canWriteToURL:targetURL];
|
||||
BOOL parentWritable = [self canWriteToURL:targetURL.URLByDeletingLastPathComponent];
|
||||
- return [SQRLShipItLauncher launchPrivileged:!targetWritable || !parentWritable];
|
||||
+ BOOL launchPrivileged = !targetWritable || !parentWritable;
|
||||
+ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"SquirrelMacEnableDirectContentsWrite"]) {
|
||||
+ // If SquirrelMacEnableDirectContentsWrite is enabled we don't care if the parent directory is writeable or not
|
||||
+ BOOL launchPrivileged = !targetWritable;
|
||||
+ }
|
||||
+ return [SQRLShipItLauncher launchPrivileged:launchPrivileged];
|
||||
}]
|
||||
replayLazily]
|
||||
setNameWithFormat:@"shipItLauncher"];
|
|
@ -7,6 +7,8 @@ import * as os from 'os';
|
|||
import * as path from 'path';
|
||||
import { AddressInfo } from 'net';
|
||||
import { ifdescribe, ifit } from './spec-helpers';
|
||||
import * as uuid from 'uuid';
|
||||
import { systemPreferences } from 'electron';
|
||||
|
||||
const features = process._linkedBinding('electron_common_features');
|
||||
|
||||
|
@ -132,7 +134,7 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
|||
await signApp(secondAppPath);
|
||||
await mutateAppPostSign?.mutate(secondAppPath);
|
||||
updateZipPath = path.resolve(dir, 'update.zip');
|
||||
await spawn('zip', ['-r', '--symlinks', updateZipPath, './'], {
|
||||
await spawn('zip', ['-0', '-r', '--symlinks', updateZipPath, './'], {
|
||||
cwd: dir
|
||||
});
|
||||
}, false);
|
||||
|
@ -321,6 +323,64 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
|
|||
});
|
||||
});
|
||||
|
||||
describe('with SquirrelMacEnableDirectContentsWrite enabled', () => {
|
||||
let previousValue: any;
|
||||
|
||||
beforeEach(() => {
|
||||
previousValue = systemPreferences.getUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean');
|
||||
systemPreferences.setUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean', true as any);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
systemPreferences.setUserDefault('SquirrelMacEnableDirectContentsWrite', 'boolean', previousValue as any);
|
||||
});
|
||||
|
||||
it('should hit the download endpoint when an update is available and update successfully when the zip is provided leaving the parent directory untouched', async () => {
|
||||
await withUpdatableApp({
|
||||
nextVersion: '2.0.0',
|
||||
startFixture: 'update',
|
||||
endFixture: 'update'
|
||||
}, async (appPath, updateZipPath) => {
|
||||
const randomID = uuid.v4();
|
||||
cp.spawnSync('xattr', ['-w', 'spec-id', randomID, appPath]);
|
||||
server.get('/update-file', (req, res) => {
|
||||
res.download(updateZipPath);
|
||||
});
|
||||
server.get('/update-check', (req, res) => {
|
||||
res.json({
|
||||
url: `http://localhost:${port}/update-file`,
|
||||
name: 'My Release Name',
|
||||
notes: 'Theses are some release notes innit',
|
||||
pub_date: (new Date()).toString()
|
||||
});
|
||||
});
|
||||
const relaunchPromise = new Promise<void>((resolve) => {
|
||||
server.get('/update-check/updated/:version', (req, res) => {
|
||||
res.status(204).send();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
|
||||
logOnError(launchResult, () => {
|
||||
expect(launchResult).to.have.property('code', 0);
|
||||
expect(launchResult.out).to.include('Update Downloaded');
|
||||
expect(requests).to.have.lengthOf(2);
|
||||
expect(requests[0]).to.have.property('url', '/update-check');
|
||||
expect(requests[1]).to.have.property('url', '/update-file');
|
||||
expect(requests[0].header('user-agent')).to.include('Electron/');
|
||||
expect(requests[1].header('user-agent')).to.include('Electron/');
|
||||
});
|
||||
|
||||
await relaunchPromise;
|
||||
expect(requests).to.have.lengthOf(3);
|
||||
expect(requests[2].url).to.equal('/update-check/updated/2.0.0');
|
||||
expect(requests[2].header('user-agent')).to.include('Electron/');
|
||||
const result = cp.spawnSync('xattr', ['-l', appPath]);
|
||||
expect(result.stdout.toString()).to.include(`spec-id: ${randomID}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should hit the download endpoint when an update is available and fail when the zip signature is invalid', async () => {
|
||||
await withUpdatableApp({
|
||||
nextVersion: '2.0.0',
|
||||
|
|
Loading…
Reference in a new issue