diff --git a/.changeset/serious-kings-own.md b/.changeset/serious-kings-own.md new file mode 100644 index 00000000000..e99143768ba --- /dev/null +++ b/.changeset/serious-kings-own.md @@ -0,0 +1,7 @@ +--- +"builder-util": patch +"builder-util-runtime": patch +"electron-updater": patch +--- + +fix: retry renaming update file when EBUSY error occurs due to file lock diff --git a/packages/builder-util-runtime/src/index.ts b/packages/builder-util-runtime/src/index.ts index c6c793f2be9..2947b1be854 100644 --- a/packages/builder-util-runtime/src/index.ts +++ b/packages/builder-util-runtime/src/index.ts @@ -37,6 +37,7 @@ export { parseXml, XElement } from "./xml" export { BlockMap } from "./blockMapApi" export { newError } from "./error" export { MemoLazy } from "./MemoLazy" +export { retry } from "./retry" // nsis export const CURRENT_APP_INSTALLER_FILE_NAME = "installer.exe" diff --git a/packages/builder-util-runtime/src/retry.ts b/packages/builder-util-runtime/src/retry.ts new file mode 100644 index 00000000000..b72f9b7c388 --- /dev/null +++ b/packages/builder-util-runtime/src/retry.ts @@ -0,0 +1,15 @@ +import { CancellationToken } from "./CancellationToken" + +export async function retry(task: () => Promise, retryCount: number, interval: number, backoff = 0, attempt = 0, shouldRetry?: (e: any) => boolean): Promise { + const cancellationToken = new CancellationToken() + try { + return await task() + } catch (error: any) { + if ((shouldRetry?.(error) ?? true) && retryCount > 0 && !cancellationToken.cancelled) { + await new Promise(resolve => setTimeout(resolve, interval + backoff * attempt)) + return await retry(task, retryCount - 1, interval, backoff, attempt + 1, shouldRetry) + } else { + throw error + } + } +} diff --git a/packages/builder-util/src/util.ts b/packages/builder-util/src/util.ts index 5a09a0e0939..0bc464579e5 100644 --- a/packages/builder-util/src/util.ts +++ b/packages/builder-util/src/util.ts @@ -1,5 +1,5 @@ import { appBuilderPath } from "app-builder-bin" -import { CancellationToken, safeStringifyJson } from "builder-util-runtime" +import { safeStringifyJson, retry as _retry } from "builder-util-runtime" import * as chalk from "chalk" import { ChildProcess, execFile, ExecFileOptions, SpawnOptions } from "child_process" import { spawn as _spawn } from "cross-spawn" @@ -408,16 +408,8 @@ export async function executeAppBuilder( } export async function retry(task: () => Promise, retryCount: number, interval: number, backoff = 0, attempt = 0, shouldRetry?: (e: any) => boolean): Promise { - const cancellationToken = new CancellationToken() - try { - return await task() - } catch (error: any) { + return await _retry(task, retryCount, interval, backoff, attempt, e => { log.info(`Above command failed, retrying ${retryCount} more times`) - if ((shouldRetry?.(error) ?? true) && retryCount > 0 && !cancellationToken.cancelled) { - await new Promise(resolve => setTimeout(resolve, interval + backoff * attempt)) - return await retry(task, retryCount - 1, interval, backoff, attempt + 1, shouldRetry) - } else { - throw error - } - } + return shouldRetry?.(e) ?? true + }) } diff --git a/packages/electron-updater/src/AppUpdater.ts b/packages/electron-updater/src/AppUpdater.ts index 12d615de74f..152a105c05b 100644 --- a/packages/electron-updater/src/AppUpdater.ts +++ b/packages/electron-updater/src/AppUpdater.ts @@ -10,6 +10,7 @@ import { CancellationError, ProgressInfo, BlockMap, + retry, } from "builder-util-runtime" import { randomBytes } from "crypto" import { release } from "os" @@ -712,7 +713,14 @@ export abstract class AppUpdater extends (EventEmitter as new () => TypedEmitter const tempUpdateFile = await createTempUpdateFile(`temp-${updateFileName}`, cacheDir, log) try { await taskOptions.task(tempUpdateFile, downloadOptions, packageFile, removeFileIfAny) - await rename(tempUpdateFile, updateFile) + await retry( + () => rename(tempUpdateFile, updateFile), + 60, + 500, + 0, + 0, + error => error instanceof Error && /^EBUSY:/.test(error.message) + ) } catch (e: any) { await removeFileIfAny()