feat: add new ElectronSquirrelPreventDowngrades flag (#38625)

* sketch

* feat: add new ElectronSquirrelPreventDowngrades flag

* test: remove only

* chore: fix lint
This commit is contained in:
Samuel Attard 2023-09-11 11:54:51 -07:00 committed by GitHub
parent 16aec702b4
commit 5bff0fe342
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 449 additions and 15 deletions

View file

@ -9,7 +9,7 @@ import * as psList from 'ps-list';
import { AddressInfo } from 'node:net';
import { ifdescribe, ifit } from './lib/spec-helpers';
import * as uuid from 'uuid';
import { systemPreferences } from 'electron';
import { autoUpdater, systemPreferences } from 'electron';
const features = process._linkedBinding('electron_common_features');
@ -129,11 +129,13 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
const cachedZips: Record<string, string> = {};
const getOrCreateUpdateZipPath = async (version: string, fixture: string, mutateAppPostSign?: {
type Mutation = {
mutate: (appPath: string) => Promise<void>,
mutationKey: string,
}) => {
const key = `${version}-${fixture}-${mutateAppPostSign?.mutationKey || 'no-mutation'}`;
};
const getOrCreateUpdateZipPath = async (version: string, fixture: string, mutateAppPreSign?: Mutation, mutateAppPostSign?: Mutation) => {
const key = `${version}-${fixture}-${mutateAppPreSign?.mutationKey || 'no-pre-mutation'}-${mutateAppPostSign?.mutationKey || 'no-post-mutation'}`;
if (!cachedZips[key]) {
let updateZipPath: string;
await withTempDirectory(async (dir) => {
@ -143,6 +145,12 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
appPJPath,
(await fs.readFile(appPJPath, 'utf8')).replace('1.0.0', version)
);
const infoPath = path.resolve(secondAppPath, 'Contents', 'Info.plist');
await fs.writeFile(
infoPath,
(await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, `$1${version}`)
);
await mutateAppPreSign?.mutate(secondAppPath);
await signApp(secondAppPath);
await mutateAppPostSign?.mutate(secondAppPath);
updateZipPath = path.resolve(dir, 'update.zip');
@ -279,16 +287,20 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
nextVersion: string;
startFixture: string;
endFixture: string;
mutateAppPostSign?: {
mutate: (appPath: string) => Promise<void>,
mutationKey: string,
}
mutateAppPreSign?: Mutation;
mutateAppPostSign?: Mutation;
}, fn: (appPath: string, zipPath: string) => Promise<void>) => {
await withTempDirectory(async (dir) => {
const appPath = await copyApp(dir, opts.startFixture);
await opts.mutateAppPreSign?.mutate(appPath);
const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
await fs.writeFile(
infoPath,
(await fs.readFile(infoPath, 'utf8')).replace(/(<key>CFBundleShortVersionString<\/key>\s+<string>)[^<]+/g, '$11.0.0')
);
await signApp(appPath);
const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPostSign);
const updateZipPath = await getOrCreateUpdateZipPath(opts.nextVersion, opts.endFixture, opts.mutateAppPreSign, opts.mutateAppPostSign);
await fn(appPath, updateZipPath);
});
@ -335,6 +347,231 @@ ifdescribe(process.platform === 'darwin' && !(process.env.CI && process.arch ===
});
});
it('should hit the download endpoint when an update is available and update successfully when the zip is provided even after a different update was staged', async () => {
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-stack',
endFixture: 'update-stack'
}, async (appPath, updateZipPath2) => {
await withUpdatableApp({
nextVersion: '3.0.0',
startFixture: 'update-stack',
endFixture: 'update-stack'
}, async (_, updateZipPath3) => {
let updateCount = 0;
server.get('/update-file', (req, res) => {
res.download(updateCount > 1 ? updateZipPath3 : updateZipPath2);
});
server.get('/update-check', (req, res) => {
updateCount++;
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(4);
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/');
expect(requests[2]).to.have.property('url', '/update-check');
expect(requests[3]).to.have.property('url', '/update-file');
expect(requests[2].header('user-agent')).to.include('Electron/');
expect(requests[3].header('user-agent')).to.include('Electron/');
});
await relaunchPromise;
expect(requests).to.have.lengthOf(5);
expect(requests[4].url).to.equal('/update-check/updated/3.0.0');
expect(requests[4].header('user-agent')).to.include('Electron/');
});
});
});
it('should update to lower version numbers', async () => {
await withUpdatableApp({
nextVersion: '0.0.1',
startFixture: 'update',
endFixture: 'update'
}, async (appPath, updateZipPath) => {
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/0.0.1');
expect(requests[2].header('user-agent')).to.include('Electron/');
});
});
describe('with ElectronSquirrelPreventDowngrades enabled', () => {
it('should not update to lower version numbers', async () => {
await withUpdatableApp({
nextVersion: '0.0.1',
startFixture: 'update',
endFixture: 'update',
mutateAppPreSign: {
mutationKey: 'prevent-downgrades',
mutate: async (appPath) => {
const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
await fs.writeFile(
infoPath,
(await fs.readFile(infoPath, 'utf8')).replace('<key>NSSupportsAutomaticGraphicsSwitching</key>', '<key>ElectronSquirrelPreventDowngrades</key><true/><key>NSSupportsAutomaticGraphicsSwitching</key>')
);
}
}
}, async (appPath, updateZipPath) => {
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 launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult).to.have.property('code', 1);
expect(launchResult.out).to.include('Cannot update to a bundle with a lower version number');
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/');
});
});
});
it('should not update to version strings that are not simple Major.Minor.Patch', async () => {
await withUpdatableApp({
nextVersion: '2.0.0-bad',
startFixture: 'update',
endFixture: 'update',
mutateAppPreSign: {
mutationKey: 'prevent-downgrades',
mutate: async (appPath) => {
const infoPath = path.resolve(appPath, 'Contents', 'Info.plist');
await fs.writeFile(
infoPath,
(await fs.readFile(infoPath, 'utf8')).replace('<key>NSSupportsAutomaticGraphicsSwitching</key>', '<key>ElectronSquirrelPreventDowngrades</key><true/><key>NSSupportsAutomaticGraphicsSwitching</key>')
);
}
}
}, async (appPath, updateZipPath) => {
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 launchResult = await launchApp(appPath, [`http://localhost:${port}/update-check`]);
logOnError(launchResult, () => {
expect(launchResult).to.have.property('code', 1);
expect(launchResult.out).to.include('Cannot update to a bundle with a lower version number');
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/');
});
});
});
it('should still update to higher version numbers', async () => {
await withUpdatableApp({
nextVersion: '1.0.1',
startFixture: 'update',
endFixture: 'update'
}, async (appPath, updateZipPath) => {
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/1.0.1');
expect(requests[2].header('user-agent')).to.include('Electron/');
});
});
it('should compare version numbers correctly', () => {
expect(autoUpdater.isVersionAllowedForUpdate('1.0.0', '2.0.0')).to.equal(true);
expect(autoUpdater.isVersionAllowedForUpdate('1.0.1', '1.0.10')).to.equal(true);
expect(autoUpdater.isVersionAllowedForUpdate('1.0.10', '1.0.1')).to.equal(false);
expect(autoUpdater.isVersionAllowedForUpdate('1.31.1', '1.32.0')).to.equal(true);
expect(autoUpdater.isVersionAllowedForUpdate('1.31.1', '0.32.0')).to.equal(false);
});
});
it('should abort the update if the application is still running when ShipIt kicks off', async () => {
await withUpdatableApp({
nextVersion: '2.0.0',