Compare commits

...

3 Commits

Author SHA1 Message Date
Shelley Vohr
d0b644e80f fix: use uppercase trace event phases for perfetto compatibility
Node.js's trace_events_async_hooks.js uses lowercase phase characters
('b'/'e' for NESTABLE_ASYNC_BEGIN/END) when calling V8's trace builtin.
In perfetto mode, V8's builtins-trace.cc only handles uppercase phases
('B'/'E'/'I'), causing a "Trace event phase must be a number" TypeError
that breaks utility process startup when contentTracing is active.

This was already fixed for lib/internal/http.js in the existing
build_enable_perfetto patch but trace_events_async_hooks.js was missed.

Also fixes the --trace-startup spec by removing embedded quotes from
the glob pattern argument that triggered a PERFETTO_CHECK assertion in
perfetto's NameMatchesPattern.
2026-03-12 15:46:48 +01:00
Niklas Wenzel
378659c535 build: update NMV to 146 (#50214) 2026-03-11 23:32:42 +00:00
Samuel Attard
6be775ad83 fix: preserve staged update dir when pruning orphaned updates on macOS (#50210)
fix: preserve staged update dir when pruning orphaned update dirs on macOS

The previous squirrel.mac patch cleaned up all staged update directories
before starting a new download. This kept disk usage bounded but broke
quitAndInstall() if called while a subsequent checkForUpdates() was in
flight — the already-staged bundle would be deleted out from under it.

This reworks the patch to read ShipItState.plist and preserve the
directory it references, deleting only truly orphaned update.XXXXXXX
directories. Disk footprint stays bounded (at most 2 dirs: staged +
in-progress) and quitAndInstall() remains safe mid-check.

Also adds test coverage for the quitAndInstall/checkForUpdates race and
a triple-stack scenario where 3 updates arrive without a restart.

Refs https://github.com/electron/electron/issues/50200
2026-03-11 15:42:23 -07:00
11 changed files with 493 additions and 87 deletions

View File

@@ -2,7 +2,7 @@ is_electron_build = true
root_extra_deps = [ "//electron" ]
# Registry of NMVs --> https://github.com/nodejs/node/blob/main/doc/abi_version_registry.json
node_module_version = 145
node_module_version = 146
v8_promise_internal_field_count = 1
v8_embedder_string = "-electron.0"

View File

@@ -63,6 +63,31 @@ index f8b4fd7c4ca5a0907806c7e804de8c951675a36a..209e3bcf8be5a23ac528dcd673bed82c
}
function ipToInt(ip) {
diff --git a/lib/internal/trace_events_async_hooks.js b/lib/internal/trace_events_async_hooks.js
index a9f517ffc9e4eea5bc68997ffadc85d43dde2a52..d3db6bf119a6bb9cea1d069957f89cc7f99512b7 100644
--- a/lib/internal/trace_events_async_hooks.js
+++ b/lib/internal/trace_events_async_hooks.js
@@ -11,15 +11,13 @@ const { trace } = internalBinding('trace_events');
const async_wrap = internalBinding('async_wrap');
const async_hooks = require('async_hooks');
const {
- CHAR_LOWERCASE_B,
- CHAR_LOWERCASE_E,
+ CHAR_UPPERCASE_B,
+ CHAR_UPPERCASE_E,
} = require('internal/constants');
-// Use small letters such that chrome://tracing groups by the name.
-// The behavior is not only useful but the same as the events emitted using
-// the specific C++ macros.
-const kBeforeEvent = CHAR_LOWERCASE_B;
-const kEndEvent = CHAR_LOWERCASE_E;
+// See v8/src/builtins/builtins-trace.cc - must be uppercase for perfetto
+const kBeforeEvent = CHAR_UPPERCASE_B;
+const kEndEvent = CHAR_UPPERCASE_E;
const kTraceEventCategory = 'node,node.async_hooks';
const kEnabled = Symbol('enabled');
diff --git a/node.gyp b/node.gyp
index f5cd416b5fe7a51084bc4af9a4427a8e62599fd8..5eb70ce3820f2b82121bc102c5182ab768cbef36 100644
--- a/node.gyp

View File

@@ -9,5 +9,5 @@ refactor_use_non-deprecated_nskeyedarchiver_apis.patch
chore_turn_off_launchapplicationaturl_deprecation_errors_in_squirrel.patch
fix_crash_when_process_to_extract_zip_cannot_be_launched.patch
use_uttype_class_instead_of_deprecated_uttypeconformsto.patch
fix_clean_up_old_staged_updates_before_downloading_new_update.patch
fix_clean_up_orphaned_staged_updates_before_downloading_new_update.patch
fix_add_explicit_json_property_mappings_for_shipit_request_model.patch

View File

@@ -1,64 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andy Locascio <loc@anthropic.com>
Date: Tue, 6 Jan 2026 08:23:03 -0800
Subject: fix: clean up old staged updates before downloading new update
When checkForUpdates() is called while an update is already staged,
Squirrel creates a new temporary directory for the download without
cleaning up the old one. This can lead to significant disk usage if
the app keeps checking for updates without restarting.
This change adds a force parameter to pruneUpdateDirectories that
bypasses the AwaitingRelaunch state check. This is called before
creating a new temp directory, ensuring old staged updates are
cleaned up when a new download starts.
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
index d156616e81e6f25a3bded30e6216b8fc311f31bc..6cd4346bf43b191147aff819cb93387e71275a46 100644
--- a/Squirrel/SQRLUpdater.m
+++ b/Squirrel/SQRLUpdater.m
@@ -543,11 +543,17 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
#pragma mark File Management
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
- return [[[RACSignal
+ // Clean up any old staged update directories before creating a new one.
+ // This prevents disk usage from growing when checkForUpdates() is called
+ // multiple times without the app restarting.
+ return [[[[[self
+ pruneUpdateDirectoriesWithForce:YES]
+ ignoreValues]
+ concat:[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
- }]
+ }]]
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@@ -643,7 +649,7 @@ - (BOOL)isRunningOnReadOnlyVolume {
- (RACSignal *)performHousekeeping {
return [[RACSignal
- merge:@[ [self pruneUpdateDirectories], [self truncateLogs] ]]
+ merge:@[ [self pruneUpdateDirectoriesWithForce:NO], [self truncateLogs] ]]
catch:^(NSError *error) {
NSLog(@"Error doing housekeeping: %@", error);
return [RACSignal empty];
@@ -658,11 +664,12 @@ - (RACSignal *)performHousekeeping {
///
/// Sends each removed directory then completes, or errors, on an unspecified
/// thread.
-- (RACSignal *)pruneUpdateDirectories {
+- (RACSignal *)pruneUpdateDirectoriesWithForce:(BOOL)force {
return [[[RACSignal
defer:^{
- // If we already have updates downloaded we don't wanna prune them.
- if (self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
+ // If we already have updates downloaded we don't wanna prune them,
+ // unless force is YES (used when starting a new download).
+ if (!force && self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];

View File

@@ -0,0 +1,130 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Andy Locascio <loc@anthropic.com>
Date: Tue, 6 Jan 2026 08:23:03 -0800
Subject: fix: clean up orphaned staged updates before downloading new update
When checkForUpdates() is called while an update is already staged,
Squirrel creates a new temporary directory for the download without
cleaning up the old one. This can lead to significant disk usage if
the app keeps checking for updates without restarting.
This change adds a pruneOrphanedUpdateDirectories step before creating
a new temp directory. Unlike a blanket prune, this reads the current
ShipItState.plist and preserves the directory it references, deleting
only truly orphaned update directories. This keeps the on-disk
footprint bounded (at most 2 dirs) while ensuring quitAndInstall
remains safe to call even when a new check is in progress.
Refs https://github.com/electron/electron/issues/50200
diff --git a/Squirrel/SQRLUpdater.m b/Squirrel/SQRLUpdater.m
index d156616e81e6f25a3bded30e6216b8fc311f31bc..41856e5754228d33982db72f97f2ff241615a357 100644
--- a/Squirrel/SQRLUpdater.m
+++ b/Squirrel/SQRLUpdater.m
@@ -543,11 +543,19 @@ - (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL
#pragma mark File Management
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
- return [[[RACSignal
+ // Clean up any orphaned update directories before creating a new one.
+ // This prevents disk usage from growing when checkForUpdates() is called
+ // multiple times without the app restarting. The currently staged update
+ // (referenced by ShipItState.plist) is always preserved so quitAndInstall
+ // remains safe to call while a new check is in progress.
+ return [[[[[self
+ pruneOrphanedUpdateDirectories]
+ ignoreValues]
+ concat:[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
- }]
+ }]]
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@@ -668,25 +676,68 @@ - (RACSignal *)pruneUpdateDirectories {
return [directoryManager storageURL];
}]
flattenMap:^(NSURL *storageURL) {
- NSFileManager *manager = [[NSFileManager alloc] init];
- NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
- NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
- return YES;
- }];
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:nil];
+ }]
+ setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
+}
- return [[enumerator.rac_sequence.signal
- filter:^(NSURL *enumeratedURL) {
- NSString *name = enumeratedURL.lastPathComponent;
- return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
- }]
- doNext:^(NSURL *directoryURL) {
- NSError *error = nil;
- if (![manager removeItemAtURL:directoryURL error:&error]) {
- NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
- }
+/// Lazily removes orphaned temporary directories upon subscription, always
+/// preserving the directory currently referenced by ShipItState.plist so that
+/// quitAndInstall remains safe to call mid-check.
+///
+/// Safe to call in any state. Sends each removed directory then completes on
+/// an unspecified thread. Errors reading the staged request are swallowed
+/// (treated as "nothing staged").
+- (RACSignal *)pruneOrphanedUpdateDirectories {
+ return [[[[[SQRLShipItRequest
+ readUsingURL:self.shipItStateURL]
+ map:^(SQRLShipItRequest *request) {
+ // The request holds the URL to the staged .app bundle; its parent
+ // is the update.XXXXXXX directory we must preserve.
+ return [request.updateBundleURL URLByDeletingLastPathComponent];
+ }]
+ catch:^(NSError *error) {
+ // No staged request (or unreadable) — nothing to preserve.
+ return [RACSignal return:nil];
+ }]
+ flattenMap:^(NSURL *stagedDirectoryURL) {
+ SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
+ return [[directoryManager storageURL]
+ flattenMap:^(NSURL *storageURL) {
+ return [self removeUpdateDirectoriesInStorageURL:storageURL excludingURL:stagedDirectoryURL];
}];
}]
- setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
+ setNameWithFormat:@"%@ -pruneOrphanedUpdateDirectories", self];
+}
+
+/// Shared enumerate-and-delete logic for update temp directories.
+///
+/// storageURL - The Squirrel storage root to enumerate. Must not be nil.
+/// excludedURL - Directory to skip (compared by standardized path). May be nil.
+- (RACSignal *)removeUpdateDirectoriesInStorageURL:(NSURL *)storageURL excludingURL:(NSURL *)excludedURL {
+ NSParameterAssert(storageURL != nil);
+
+ NSFileManager *manager = [[NSFileManager alloc] init];
+ NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
+ NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
+ return YES;
+ }];
+
+ NSString *excludedPath = excludedURL.URLByStandardizingPath.path;
+
+ return [[enumerator.rac_sequence.signal
+ filter:^(NSURL *enumeratedURL) {
+ NSString *name = enumeratedURL.lastPathComponent;
+ if (![name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix]) return NO;
+ if (excludedPath != nil && [enumeratedURL.URLByStandardizingPath.path isEqualToString:excludedPath]) return NO;
+ return YES;
+ }]
+ doNext:^(NSURL *directoryURL) {
+ NSError *error = nil;
+ if (![manager removeItemAtURL:directoryURL error:&error]) {
+ NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
+ }
+ }];
}

View File

@@ -403,7 +403,7 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
});
});
it('should clean up old staged update directories when a new update is downloaded', async () => {
it('should preserve the staged update directory and prune orphaned ones when a new update is downloaded', async () => {
// Clean up any existing update directories before the test
await cleanSquirrelCache();
@@ -419,16 +419,23 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
}, async (_, updateZipPath3) => {
let updateCount = 0;
let downloadCount = 0;
let directoriesDuringSecondDownload: string[] = [];
let dirsDuringFirstDownload: string[] = [];
let dirsDuringSecondDownload: string[] = [];
server.get('/update-file', async (req, res) => {
downloadCount++;
// When the second download request arrives, Squirrel has already
// called uniqueTemporaryDirectoryForUpdate which (with our patch)
// cleans up old directories before creating the new one.
// Without the patch, both directories would exist at this point.
if (downloadCount === 2) {
directoriesDuringSecondDownload = await getUpdateDirectoriesInCache();
// Snapshot update directories at the moment each download begins.
// By this point uniqueTemporaryDirectoryForUpdate has already run
// (prune + mkdtemp). We want to verify:
// 1st download: 1 dir (nothing to preserve, nothing to prune)
// 2nd download: 2 dirs (staged dir from 1st check is preserved
// so quitAndInstall stays safe, + new temp dir)
// The count never exceeds 2 across repeated checks — orphaned dirs
// (no longer referenced by ShipItState.plist) get pruned.
if (downloadCount === 1) {
dirsDuringFirstDownload = await getUpdateDirectoriesInCache();
} else if (downloadCount === 2) {
dirsDuringSecondDownload = await getUpdateDirectoriesInCache();
}
res.download(updateCount > 1 ? updateZipPath3 : updateZipPath2);
});
@@ -455,15 +462,181 @@ ifdescribe(shouldRunCodesignTests)('autoUpdater behavior', function () {
await relaunchPromise;
// During the second download, the old staged update directory should
// have been cleaned up. With our patch, there should be exactly 1
// directory (the new one). Without the patch, there would be 2.
expect(directoriesDuringSecondDownload).to.have.lengthOf(1,
`Expected 1 update directory during second download but found ${directoriesDuringSecondDownload.length}: ${directoriesDuringSecondDownload.join(', ')}`);
// First download: exactly one temp dir (the first update).
expect(dirsDuringFirstDownload).to.have.lengthOf(1,
`Expected 1 update directory during first download but found ${dirsDuringFirstDownload.length}: ${dirsDuringFirstDownload.join(', ')}`);
// Second download: exactly two — the staged one preserved + the new
// one. Crucially the first download's directory must still be present,
// otherwise a mid-download quitAndInstall would find a dangling
// ShipItState.plist.
expect(dirsDuringSecondDownload).to.have.lengthOf(2,
`Expected 2 update directories during second download (staged + new) but found ${dirsDuringSecondDownload.length}: ${dirsDuringSecondDownload.join(', ')}`);
expect(dirsDuringSecondDownload).to.include(dirsDuringFirstDownload[0],
'The staged update directory from the first download must be preserved during the second download');
});
});
});
it('should keep the update directory count bounded across repeated checks', async () => {
// Verifies the orphan prune actually fires: after a second download
// completes and rewrites ShipItState.plist, the first directory is no
// longer referenced and must be removed when a third check begins.
// Without this, directories would accumulate forever.
await cleanSquirrelCache();
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (appPath, updateZipPath2) => {
await withUpdatableApp({
nextVersion: '3.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (_, updateZipPath3) => {
await withUpdatableApp({
nextVersion: '4.0.0',
startFixture: 'update-triple-stack',
endFixture: 'update-triple-stack'
}, async (__, updateZipPath4) => {
let downloadCount = 0;
const dirsPerDownload: string[][] = [];
server.get('/update-file', async (req, res) => {
downloadCount++;
// Snapshot after prune+mkdtemp but before the payload transfers.
dirsPerDownload.push(await getUpdateDirectoriesInCache());
const zips = [updateZipPath2, updateZipPath3, updateZipPath4];
res.download(zips[Math.min(downloadCount, zips.length) - 1]);
});
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');
});
await relaunchPromise;
expect(requests[requests.length - 1].url).to.equal('/update-check/updated/4.0.0');
expect(dirsPerDownload).to.have.lengthOf(3);
// 1st: fresh cache, 1 dir.
expect(dirsPerDownload[0]).to.have.lengthOf(1,
`1st download: ${dirsPerDownload[0].join(', ')}`);
// 2nd: staged (1st) preserved + new = 2 dirs.
expect(dirsPerDownload[1]).to.have.lengthOf(2,
`2nd download: ${dirsPerDownload[1].join(', ')}`);
expect(dirsPerDownload[1]).to.include(dirsPerDownload[0][0]);
// 3rd: 1st is now orphaned (plist points to 2nd) — must be pruned.
// Staged (2nd) preserved + new = still 2 dirs. Bounded.
expect(dirsPerDownload[2]).to.have.lengthOf(2,
`3rd download: ${dirsPerDownload[2].join(', ')}`);
expect(dirsPerDownload[2]).to.not.include(dirsPerDownload[0][0],
'The first (now orphaned) update directory must be pruned on the third check');
const secondDir = dirsPerDownload[1].find(d => d !== dirsPerDownload[0][0]);
expect(dirsPerDownload[2]).to.include(secondDir,
'The second (currently staged) update directory must be preserved on the third check');
});
});
});
});
// Regression test for https://github.com/electron/electron/issues/50200
//
// When checkForUpdates() is called again after an update has been staged,
// Squirrel creates a new temporary directory and prunes old ones. If the
// prune removes the directory that ShipItState.plist references while the
// second download is still in flight, a subsequent quitAndInstall() will
// fail with ENOENT and the app will never relaunch.
it('should install the staged update when quitAndInstall is called while a second check is in flight', async () => {
await cleanSquirrelCache();
await withUpdatableApp({
nextVersion: '2.0.0',
startFixture: 'update-race',
endFixture: 'update-race'
}, async (appPath, updateZipPath) => {
let downloadCount = 0;
let stalledResponse: express.Response | null = null;
server.get('/update-file', (req, res) => {
downloadCount++;
if (downloadCount === 1) {
// First download completes normally and stages the update.
res.download(updateZipPath);
} else {
// Second download: stall indefinitely to simulate a slow
// network. This keeps the second check "in progress" when
// quitAndInstall() fires. Hold onto the response so we can
// clean it up later.
stalledResponse = res;
}
});
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(launchResult.out).to.include('Calling quitAndInstall mid-download');
// First check + first download + second check + stalled second download.
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[2]).to.have.property('url', '/update-check');
expect(requests[3]).to.have.property('url', '/update-file');
// The second download must have been in flight (never completed)
// when quitAndInstall was called.
expect(launchResult.out).to.not.include('Unexpected second download completion');
});
// Unblock the stalled response now that the initial app has exited
// so the express server can shut down cleanly.
if (stalledResponse) {
(stalledResponse as express.Response).status(500).end();
}
// The originally staged update (2.0.0) must have been applied and
// the app must relaunch, proving the staged update directory was
// not pruned out from under ShipItState.plist.
await relaunchPromise;
expect(requests).to.have.lengthOf(5);
expect(requests[4].url).to.equal('/update-check/updated/2.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',

View File

@@ -567,14 +567,7 @@ describe('command line switches', () => {
});
it('creates startup trace', async () => {
// node.async_hooks relies on %trace builtin to log trace points from JS
// https://github.com/nodejs/node/blob/8b199eef3dd4de910a6521adc42ae611a62a19e1/lib/internal/trace_events_async_hooks.js#L48-L53
// The phase event arg TRACE_EVENT_PHASE_NESTABLE_ASYNC_(BEGIN | END) is not supported in v8_use_perfetto mode
// https://source.chromium.org/chromium/chromium/src/+/main:v8/src/builtins/builtins-trace.cc;l=201-216
// and leads to the following error: TypeError: Trace event phase must be a number.
// TODO: Identify why the error started appearing with roll https://github.com/electron/electron/pull/47561
// given both v8_use_perfetto has been enabled before the roll and builtins-trace macro hasn't changed.
const rc = await startRemoteControlApp(['--trace-startup="*,-node.async_hooks"', `--trace-startup-file=${outputFilePath}`, '--trace-startup-duration=1', '--enable-logging']);
const rc = await startRemoteControlApp(['--trace-startup=*', `--trace-startup-file=${outputFilePath}`, '--trace-startup-duration=1', '--enable-logging']);
const stderrComplete = new Promise<string>(resolve => {
let stderr = '';
rc.process.stderr!.on('data', (chunk) => {

View File

@@ -0,0 +1,82 @@
const { app, autoUpdater } = require('electron');
const fs = require('node:fs');
const path = require('node:path');
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});
let installInvoked = false;
autoUpdater.on('error', (err) => {
// Once quitAndInstall() has been invoked the second in-flight check may
// surface a cancellation/network error as the process tears down; ignore
// errors after that point so we test the actual install race, not teardown.
if (installInvoked) {
console.log('Ignoring post-install error:', err && err.message);
return;
}
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 downloadedOnce = false;
autoUpdater.on('update-downloaded', () => {
console.log('Update Downloaded');
if (!downloadedOnce) {
downloadedOnce = true;
// Simulate a periodic update check firing after an update was already
// staged. The test server is expected to stall this second download so
// that it remains in flight while we call quitAndInstall().
// The short delay lets checkForUpdatesCommand's RACCommand executing
// state settle; calling immediately would hit the command's "disabled"
// guard since RACCommand disallows concurrent execution.
setTimeout(() => {
autoUpdater.checkForUpdates();
// Give Squirrel enough time to enter the second check (creating a new
// temporary directory, which with the regression prunes the directory
// that the staged update lives in) before invoking the install.
setTimeout(() => {
console.log('Calling quitAndInstall mid-download');
installInvoked = true;
autoUpdater.quitAndInstall();
}, 3000);
}, 1000);
} else {
// Should not reach here — the second download is stalled on purpose.
console.log('Unexpected second download completion');
autoUpdater.quitAndInstall();
}
});
autoUpdater.on('update-not-available', () => {
console.error('No update available');
process.exit(1);
});
}

View File

@@ -0,0 +1,5 @@
{
"name": "electron-test-update-race",
"version": "1.0.0",
"main": "./index.js"
}

View File

@@ -0,0 +1,57 @@
const { app, autoUpdater } = require('electron');
const fs = require('node:fs');
const path = require('node:path');
process.on('uncaughtException', (err) => {
console.error(err);
process.exit(1);
});
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 > 2) {
autoUpdater.quitAndInstall();
} else {
setTimeout(() => {
autoUpdater.checkForUpdates();
}, 1000);
}
});
autoUpdater.on('update-not-available', () => {
console.error('No update available');
process.exit(1);
});
}

View File

@@ -0,0 +1,5 @@
{
"name": "electron-test-update-triple-stack",
"version": "1.0.0",
"main": "./index.js"
}