feat: add new ElectronSquirrelPreventDowngrades flag (#38625)
* sketch * feat: add new ElectronSquirrelPreventDowngrades flag * test: remove only * chore: fix lint
This commit is contained in:
parent
16aec702b4
commit
5bff0fe342
11 changed files with 449 additions and 15 deletions
|
@ -5,3 +5,4 @@ feat_add_new_squirrel_mac_bundle_installation_method_behind_flag.patch
|
|||
refactor_use_posix_spawn_instead_of_nstask_so_we_can_disclaim_the.patch
|
||||
fix_abort_installation_attempt_at_the_final_mile_if_the_app_is.patch
|
||||
chore_disable_api_deprecation_warnings_in_nskeyedarchiver.patch
|
||||
feat_add_ability_to_prevent_version_downgrades.patch
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Samuel Attard <marshallofsound@electronjs.org>
|
||||
Date: Tue, 6 Jun 2023 15:20:38 -0700
|
||||
Subject: feat: add ability to prevent version downgrades
|
||||
|
||||
The ElectronSquirrelPreventDowngrades flag in your Info.plist can enable this feature, it must be set to true. This feature prevents a class of downgrade / freeze issues but has significant drawbacks in that it may break existing apps that delibrately downgrade users via the updater and requires that version strings are exactly A.B.C in order for the version comparison logic to work. Because of this this feature can not (and is not) enabled by default.
|
||||
|
||||
diff --git a/Squirrel/SQRLUpdater.h b/Squirrel/SQRLUpdater.h
|
||||
index b3526b246b3729a7556ca0ec348fdc08825c89ed..87119399e8548e04134d1ee0cd18f85c2ad672c2 100644
|
||||
--- a/Squirrel/SQRLUpdater.h
|
||||
+++ b/Squirrel/SQRLUpdater.h
|
||||
@@ -117,6 +117,10 @@ typedef enum {
|
||||
// documentation for more information.
|
||||
@property (atomic, strong) Class updateClass;
|
||||
|
||||
+// Publicly exposed for testing purposes, compares two version strings to see if it's
|
||||
+// allowed. This assumes that the ElectronSquirrelPreventDowngrades flag is enabled.
|
||||
++ (bool) isVersionAllowedForUpdate:(NSString*)targetVersion from:(NSString*)currentVersion;
|
||||
+
|
||||
// Initializes an updater that will send the given request to check for updates.
|
||||
//
|
||||
// This is the designated initializer for this class.
|
||||
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
|
||||
index 4c703159a2bb0239b7d4e1793a985b5ec2edcfa9..592c7ea51515aab96934e0117df3c8065494fa09 100644
|
||||
--- a/Squirrel/SQRLUpdater.m
|
||||
+++ b/Squirrel/SQRLUpdater.m
|
||||
@@ -42,6 +42,18 @@
|
||||
// followed by a random string of characters.
|
||||
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
|
||||
+BOOL isVersionStandard(NSString* version) {
|
||||
+ NSCharacterSet *alphaNums = [NSCharacterSet decimalDigitCharacterSet];
|
||||
+
|
||||
+ NSArray* versionParts = [version componentsSeparatedByString:@"."];
|
||||
+ BOOL versionBad = [versionParts count] != 3;
|
||||
+ for (NSString* part in versionParts) {
|
||||
+ versionBad = versionBad || [alphaNums isSupersetOfSet:[NSCharacterSet characterSetWithCharactersInString:part]];
|
||||
+ }
|
||||
+
|
||||
+ return !versionBad;
|
||||
+}
|
||||
+
|
||||
@interface SQRLUpdater ()
|
||||
|
||||
@property (atomic, readwrite) SQRLUpdaterState state;
|
||||
@@ -59,6 +71,8 @@ @interface SQRLUpdater ()
|
||||
// Sends completed or error.
|
||||
@property (nonatomic, strong, readonly) RACSignal *shipItLauncher;
|
||||
|
||||
++ (bool) isVersionAllowedForUpdate:(NSString*)targetVersion from:(NSString*)currentVersion;
|
||||
+
|
||||
// Parses an update model from downloaded data.
|
||||
//
|
||||
// data - JSON data representing an update manifest. This must not be nil.
|
||||
@@ -372,6 +386,10 @@ - (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval {
|
||||
connect];
|
||||
}
|
||||
|
||||
++ (bool) isVersionAllowedForUpdate:(NSString*)targetVersion from:(NSString*)currentVersion {
|
||||
+ return [currentVersion compare:targetVersion options:NSNumericSearch] != NSOrderedDescending;
|
||||
+}
|
||||
+
|
||||
- (RACSignal *)updateFromJSONData:(NSData *)data {
|
||||
NSParameterAssert(data != nil);
|
||||
|
||||
@@ -711,6 +729,50 @@ - (RACSignal *)verifyAndPrepareUpdate:(SQRLUpdate *)update fromBundle:(NSBundle
|
||||
return [[[[self.signature
|
||||
verifyBundleAtURL:updateBundle.bundleURL]
|
||||
then:^{
|
||||
+ NSRunningApplication *currentApplication = NSRunningApplication.currentApplication;
|
||||
+ NSBundle *appBundle = [NSBundle bundleWithURL:currentApplication.bundleURL];
|
||||
+ BOOL preventDowngrades = [[appBundle objectForInfoDictionaryKey:@"ElectronSquirrelPreventDowngrades"] boolValue];
|
||||
+
|
||||
+ if (preventDowngrades == YES) {
|
||||
+ NSString* currentVersion = [appBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
||||
+ NSString* updateVersion = [updateBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
||||
+ if (!currentVersion || !updateVersion) {
|
||||
+ NSDictionary *errorInfo = @{
|
||||
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Cannot update to a bundle with a lower version number", nil),
|
||||
+ NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The application has ElectronSquirrelPreventDowngrades enabled and is missing a valid version string in either the current bundle or the target bundle", nil),
|
||||
+ };
|
||||
+ NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:errorInfo];
|
||||
+ return [RACSignal error:error];
|
||||
+ }
|
||||
+
|
||||
+ if (!isVersionStandard(currentVersion)) {
|
||||
+ NSDictionary *errorInfo = @{
|
||||
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Cannot update to a bundle with a lower version number", nil),
|
||||
+ NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"The application has ElectronSquirrelPreventDowngrades enabled and is trying to update from '%@' which is not a valid version string", nil), currentVersion],
|
||||
+ };
|
||||
+ NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:errorInfo];
|
||||
+ return [RACSignal error:error];
|
||||
+ }
|
||||
+
|
||||
+ if (!isVersionStandard(updateVersion)) {
|
||||
+ NSDictionary *errorInfo = @{
|
||||
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Cannot update to a bundle with a lower version number", nil),
|
||||
+ NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"The application has ElectronSquirrelPreventDowngrades enabled and is trying to update to '%@' which is not a valid version string", nil), updateVersion],
|
||||
+ };
|
||||
+ NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:errorInfo];
|
||||
+ return [RACSignal error:error];
|
||||
+ }
|
||||
+
|
||||
+ if (![SQRLUpdater isVersionAllowedForUpdate:updateVersion from:currentVersion]) {
|
||||
+ NSDictionary *errorInfo = @{
|
||||
+ NSLocalizedDescriptionKey: NSLocalizedString(@"Cannot update to a bundle with a lower version number", nil),
|
||||
+ NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"The application has ElectronSquirrelPreventDowngrades enabled and is trying to update from '%@' to '%@' which appears to be a downgrade", nil), currentVersion, updateVersion],
|
||||
+ };
|
||||
+ NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:errorInfo];
|
||||
+ return [RACSignal error:error];
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle];
|
||||
return [RACSignal return:downloadedUpdate];
|
||||
}]
|
|
@ -95,10 +95,6 @@ void AutoUpdater::OnWindowAllClosed() {
|
|||
QuitAndInstall();
|
||||
}
|
||||
|
||||
void AutoUpdater::SetFeedURL(gin::Arguments* args) {
|
||||
auto_updater::AutoUpdater::SetFeedURL(args);
|
||||
}
|
||||
|
||||
void AutoUpdater::QuitAndInstall() {
|
||||
Emit("before-quit-for-update");
|
||||
|
||||
|
@ -124,7 +120,11 @@ gin::ObjectTemplateBuilder AutoUpdater::GetObjectTemplateBuilder(
|
|||
isolate)
|
||||
.SetMethod("checkForUpdates", &auto_updater::AutoUpdater::CheckForUpdates)
|
||||
.SetMethod("getFeedURL", &auto_updater::AutoUpdater::GetFeedURL)
|
||||
.SetMethod("setFeedURL", &AutoUpdater::SetFeedURL)
|
||||
.SetMethod("setFeedURL", &auto_updater::AutoUpdater::SetFeedURL)
|
||||
#if DCHECK_IS_ON()
|
||||
.SetMethod("isVersionAllowedForUpdate",
|
||||
&auto_updater::AutoUpdater::IsVersionAllowedForUpdate)
|
||||
#endif
|
||||
.SetMethod("quitAndInstall", &AutoUpdater::QuitAndInstall);
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@ class AutoUpdater : public gin::Wrappable<AutoUpdater>,
|
|||
|
||||
private:
|
||||
std::string GetFeedURL();
|
||||
void SetFeedURL(gin::Arguments* args);
|
||||
void QuitAndInstall();
|
||||
};
|
||||
|
||||
|
|
|
@ -26,6 +26,11 @@ void AutoUpdater::SetFeedURL(gin::Arguments* args) {}
|
|||
void AutoUpdater::CheckForUpdates() {}
|
||||
|
||||
void AutoUpdater::QuitAndInstall() {}
|
||||
|
||||
bool AutoUpdater::IsVersionAllowedForUpdate(const std::string& current_version,
|
||||
const std::string& target_version) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace auto_updater
|
||||
|
|
|
@ -70,6 +70,9 @@ class AutoUpdater {
|
|||
static void CheckForUpdates();
|
||||
static void QuitAndInstall();
|
||||
|
||||
static bool IsVersionAllowedForUpdate(const std::string& current_version,
|
||||
const std::string& target_version);
|
||||
|
||||
private:
|
||||
static Delegate* delegate_;
|
||||
};
|
||||
|
|
|
@ -179,4 +179,11 @@ void AutoUpdater::QuitAndInstall() {
|
|||
}
|
||||
}
|
||||
|
||||
bool AutoUpdater::IsVersionAllowedForUpdate(const std::string& current_version,
|
||||
const std::string& target_version) {
|
||||
return [SQRLUpdater
|
||||
isVersionAllowedForUpdate:base::SysUTF8ToNSString(target_version)
|
||||
from:base::SysUTF8ToNSString(current_version)];
|
||||
}
|
||||
|
||||
} // namespace auto_updater
|
||||
|
|
|
@ -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',
|
||||
|
|
57
spec/fixtures/auto-update/update-stack/index.js
vendored
Normal file
57
spec/fixtures/auto-update/update-stack/index.js
vendored
Normal file
|
@ -0,0 +1,57 @@
|
|||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const { app, autoUpdater } = require('electron');
|
||||
|
||||
autoUpdater.on('error', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const urlPath = path.resolve(__dirname, '../../../../url.txt');
|
||||
let feedUrl = process.argv[1];
|
||||
|
||||
if (feedUrl === 'remain-open') {
|
||||
// Hold the event loop
|
||||
setInterval(() => {});
|
||||
} else {
|
||||
if (!feedUrl || !feedUrl.startsWith('http')) {
|
||||
feedUrl = `${fs.readFileSync(urlPath, 'utf8')}/${app.getVersion()}`;
|
||||
} else {
|
||||
fs.writeFileSync(urlPath, `${feedUrl}/updated`);
|
||||
}
|
||||
|
||||
autoUpdater.setFeedURL({
|
||||
url: feedUrl
|
||||
});
|
||||
|
||||
autoUpdater.checkForUpdates();
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
console.log('Update Available');
|
||||
});
|
||||
|
||||
let updateStackCount = 0;
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
updateStackCount++;
|
||||
console.log('Update Downloaded');
|
||||
if (updateStackCount > 1) {
|
||||
autoUpdater.quitAndInstall();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
autoUpdater.checkForUpdates();
|
||||
}, 1000);
|
||||
}
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
console.error('No update available');
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
5
spec/fixtures/auto-update/update-stack/package.json
vendored
Normal file
5
spec/fixtures/auto-update/update-stack/package.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"name": "electron-test-update-stack",
|
||||
"version": "1.0.0",
|
||||
"main": "./index.js"
|
||||
}
|
4
typings/internal-electron.d.ts
vendored
4
typings/internal-electron.d.ts
vendored
|
@ -19,6 +19,10 @@ declare namespace Electron {
|
|||
setAppPath(path: string | null): void;
|
||||
}
|
||||
|
||||
interface AutoUpdater {
|
||||
isVersionAllowedForUpdate(currentVersion: string, targetVersion: string): boolean;
|
||||
}
|
||||
|
||||
type TouchBarItemType = NonNullable<Electron.TouchBarConstructorOptions['items']>[0];
|
||||
|
||||
interface BaseWindow {
|
||||
|
|
Loading…
Add table
Reference in a new issue