From ba61f8769658f438cbf1cf9c811c88f550fae39b Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:10:48 -0700 Subject: [PATCH] Run test-release in a temporary folder --- ts/scripts/test-release.node.ts | 48 ++++++++++----- ts/updater/common.main.ts | 5 +- ts/updater/util.node.ts | 96 +----------------------------- ts/util/gracefulFs.node.ts | 100 ++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+), 113 deletions(-) create mode 100644 ts/util/gracefulFs.node.ts diff --git a/ts/scripts/test-release.node.ts b/ts/scripts/test-release.node.ts index 7a10304afd..1dbb427abd 100644 --- a/ts/scripts/test-release.node.ts +++ b/ts/scripts/test-release.node.ts @@ -4,9 +4,14 @@ import asar from '@electron/asar'; import assert from 'node:assert'; import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { mkdtemp, cp } from 'node:fs/promises'; +import { constants as fsConstants } from 'node:fs'; import { _electron as electron } from 'playwright'; import { productName, name } from '../util/packageJson.node.js'; +import { gracefulRmRecursive } from '../util/gracefulFs.node.js'; +import { consoleLogger } from '../util/consoleLogger.std.js'; const ENVIRONMENT = 'production'; const RELEASE_DIR = join(__dirname, '..', '..', 'release'); @@ -56,25 +61,38 @@ for (const fileName of files) { // A simple test to verify a visible window is opened with a title const main = async () => { - const executablePath = join(RELEASE_DIR, exe); - console.log('Starting path', executablePath); - const app = await electron.launch({ - executablePath, - locale: 'en', - }); + const tmpFolder = await mkdtemp(join(tmpdir(), 'test-release')); + const tmpApp = join(tmpFolder, 'Signal'); - console.log('Waiting for a first window'); - const window = await app.firstWindow(); + try { + await cp(RELEASE_DIR, tmpApp, { + recursive: true, + mode: fsConstants.COPYFILE_FICLONE, + }); - console.log('Waiting for app to fully load'); - await window.waitForSelector( - '.App, .app-loading-screen:has-text("Optimizing")' - ); + const executablePath = join(tmpApp, exe); + console.log('Starting path', executablePath); + const app = await electron.launch({ + executablePath, + locale: 'en', + cwd: tmpApp, + }); - console.log('Checking window title'); - assert.strictEqual(await window.title(), productName); + console.log('Waiting for a first window'); + const window = await app.firstWindow(); - await app.close(); + console.log('Waiting for app to fully load'); + await window.waitForSelector( + '.App, .app-loading-screen:has-text("Optimizing")' + ); + + console.log('Checking window title'); + assert.strictEqual(await window.title(), productName); + + await app.close(); + } finally { + await gracefulRmRecursive(consoleLogger, tmpFolder); + } }; main().catch(error => { diff --git a/ts/updater/common.main.ts b/ts/updater/common.main.ts index bd3aa51d8b..0710e7e18b 100644 --- a/ts/updater/common.main.ts +++ b/ts/updater/common.main.ts @@ -53,12 +53,11 @@ import { prepareDownload as prepareDifferentialDownload, } from './differential.node.js'; import { getGotOptions } from './got.node.js'; +import { checkIntegrity, isTimeToUpdate } from './util.node.js'; import { - checkIntegrity, gracefulRename, gracefulRmRecursive, - isTimeToUpdate, -} from './util.node.js'; +} from '../util/gracefulFs.node.js'; import type { LoggerType } from '../types/Logging.std.js'; import type { PrepareDownloadResultType as DifferentialDownloadDataType } from './differential.node.js'; diff --git a/ts/updater/util.node.ts b/ts/updater/util.node.ts index ab833d6a3e..437f8eb8b7 100644 --- a/ts/updater/util.node.ts +++ b/ts/updater/util.node.ts @@ -3,14 +3,11 @@ import { createHash } from 'node:crypto'; import { createReadStream } from 'node:fs'; -import { rename, rm } from 'node:fs/promises'; import { pipeline } from 'node:stream/promises'; import type { LoggerType } from '../types/Logging.std.js'; import * as Errors from '../types/errors.std.js'; -import { SECOND, MINUTE, HOUR } from '../util/durations/index.std.js'; -import { sleep } from '../util/sleep.std.js'; -import { isOlderThan } from '../util/timestamp.std.js'; +import { MINUTE, HOUR } from '../util/durations/index.std.js'; export type CheckIntegrityResultType = Readonly< | { @@ -48,97 +45,6 @@ export async function checkIntegrity( } } -async function doGracefulFSOperation>({ - name, - operation, - args, - logger, - startedAt, - retryCount, - retryAfter = 5 * SECOND, - timeout = 5 * MINUTE, -}: { - name: string; - operation: (...args: Args) => Promise; - args: Args; - logger: LoggerType; - startedAt: number; - retryCount: number; - retryAfter?: number; - timeout?: number; -}): Promise { - const logId = `gracefulFS(${name})`; - try { - await operation(...args); - - if (retryCount !== 0) { - logger.info( - `${logId}: succeeded after ${retryCount} retries, ${args.join(', ')}` - ); - } - } catch (error) { - if (error.code !== 'EACCES' && error.code !== 'EPERM') { - throw error; - } - - if (isOlderThan(startedAt, timeout)) { - logger.warn(`${logId}: timed out, ${args.join(', ')}`); - throw error; - } - - logger.warn( - `${logId}: got ${error.code} when running on ${args.join(', ')}; ` + - `retrying in one second. (retryCount=${retryCount})` - ); - - await sleep(retryAfter); - - return doGracefulFSOperation({ - name, - operation, - args, - logger, - startedAt, - retryCount: retryCount + 1, - retryAfter, - timeout, - }); - } -} - -export async function gracefulRename( - logger: LoggerType, - fromPath: string, - toPath: string -): Promise { - return doGracefulFSOperation({ - name: 'rename', - operation: rename, - args: [fromPath, toPath], - logger, - startedAt: Date.now(), - retryCount: 0, - }); -} - -function rmRecursive(path: string): Promise { - return rm(path, { recursive: true, force: true }); -} - -export async function gracefulRmRecursive( - logger: LoggerType, - path: string -): Promise { - return doGracefulFSOperation({ - name: 'rmRecursive', - operation: rmRecursive, - args: [path], - logger, - startedAt: Date.now(), - retryCount: 0, - }); -} - const MAX_UPDATE_DELAY = 6 * HOUR; export function isTimeToUpdate({ diff --git a/ts/util/gracefulFs.node.ts b/ts/util/gracefulFs.node.ts new file mode 100644 index 0000000000..8e9ad9eb12 --- /dev/null +++ b/ts/util/gracefulFs.node.ts @@ -0,0 +1,100 @@ +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { rename, rm } from 'node:fs/promises'; + +import type { LoggerType } from '../types/Logging.std.js'; +import { SECOND, MINUTE } from './durations/index.std.js'; +import { isOlderThan } from './timestamp.std.js'; +import { sleep } from './sleep.std.js'; + +async function doGracefulFSOperation>({ + name, + operation, + args, + logger, + startedAt, + retryCount, + retryAfter = 5 * SECOND, + timeout = 5 * MINUTE, +}: { + name: string; + operation: (...args: Args) => Promise; + args: Args; + logger: LoggerType; + startedAt: number; + retryCount: number; + retryAfter?: number; + timeout?: number; +}): Promise { + const logId = `gracefulFS(${name})`; + try { + await operation(...args); + + if (retryCount !== 0) { + logger.info( + `${logId}: succeeded after ${retryCount} retries, ${args.join(', ')}` + ); + } + } catch (error) { + if (error.code !== 'EACCES' && error.code !== 'EPERM') { + throw error; + } + + if (isOlderThan(startedAt, timeout)) { + logger.warn(`${logId}: timed out, ${args.join(', ')}`); + throw error; + } + + logger.warn( + `${logId}: got ${error.code} when running on ${args.join(', ')}; ` + + `retrying in one second. (retryCount=${retryCount})` + ); + + await sleep(retryAfter); + + return doGracefulFSOperation({ + name, + operation, + args, + logger, + startedAt, + retryCount: retryCount + 1, + retryAfter, + timeout, + }); + } +} + +export async function gracefulRename( + logger: LoggerType, + fromPath: string, + toPath: string +): Promise { + return doGracefulFSOperation({ + name: 'rename', + operation: rename, + args: [fromPath, toPath], + logger, + startedAt: Date.now(), + retryCount: 0, + }); +} + +function rmRecursive(path: string): Promise { + return rm(path, { recursive: true, force: true }); +} + +export async function gracefulRmRecursive( + logger: LoggerType, + path: string +): Promise { + return doGracefulFSOperation({ + name: 'rmRecursive', + operation: rmRecursive, + args: [path], + logger, + startedAt: Date.now(), + retryCount: 0, + }); +}