Files
maka/scripts/install-electron-with-retry.mjs
agent-radar 7be535059e
Dependency audit / audit (push) Canceled after 0s
CI / plan (push) Canceled after 0s
Gitoxide helper admission / macos-latest (push) Canceled after 0s
Gitoxide helper admission / ubuntu-latest (push) Canceled after 0s
Gitoxide helper admission / windows-latest (push) Canceled after 0s
Runtime Host peer admission / quality (push) Canceled after 0s
Windows recovery / windows_recovery (push) Canceled after 0s
CI / heavy (push) Canceled after 0s
CI / test (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 19:05:14 +08:00

145 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { spawnSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { dirname } from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';
import { fileURLToPath } from 'node:url';
const DEFAULT_MAX_ATTEMPTS = 3;
const DEFAULT_BASE_DELAY_MS = 1_000;
const transientCodes = new Set([
'ECONNRESET',
'EAI_AGAIN',
'ETIMEDOUT',
'UND_ERR_BODY_TIMEOUT',
'UND_ERR_CONNECT_TIMEOUT',
'UND_ERR_HEADERS_TIMEOUT',
'UND_ERR_SOCKET',
]);
const transientHttpStatuses = new Set([408, 425, 429, 500, 502, 503, 504]);
export function isTransientElectronInstallFailure(contextOutput) {
return contextOutput.split(/\r?\n/).some((line) => {
if (!line) return false;
try {
const context = JSON.parse(line);
return (
(context.kind === 'fetch' && transientCodes.has(context.code)) ||
(context.kind === 'http' && transientHttpStatuses.has(context.status))
);
} catch {
return false;
}
});
}
export async function installWithRetry(
runAttempt,
{
maxAttempts = DEFAULT_MAX_ATTEMPTS,
baseDelayMs = DEFAULT_BASE_DELAY_MS,
wait = sleep,
warn = console.warn,
} = {},
) {
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const result = await runAttempt(attempt);
if (
result.status === 0 ||
typeof result.status !== 'number' ||
result.signal ||
result.spawnError ||
!isTransientElectronInstallFailure(result.context ?? '')
) {
return result;
}
if (attempt === maxAttempts) {
return result;
}
const delayMs = baseDelayMs * 2 ** (attempt - 1);
warn(
`[install-electron] transient download failure on attempt ${attempt}/${maxAttempts}; retrying in ${delayMs}ms`,
);
await wait(delayMs);
}
throw new Error('Electron install retry loop ended unexpectedly');
}
export function electronInstallerSpawnOptions(repositoryRoot) {
return {
cwd: repositoryRoot,
encoding: 'utf8',
stdio: ['inherit', 'inherit', 'pipe', 'pipe'],
};
}
async function main() {
// electron is declared by apps/desktop, not the workspace root, so anchor
// resolution at that package. Resolving from this script (under scripts/)
// only reaches electron when the installer hoists it to the root
// node_modules; anchoring at apps/desktop works under any layout.
const require = createRequire(new URL('../apps/desktop/package.json', import.meta.url));
const installer = require.resolve('electron/install.js');
const launcher = fileURLToPath(new URL('./run-electron-installer.cjs', import.meta.url));
const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const result = await installWithRetry(() => {
const child = spawnSync(
process.execPath,
[launcher, installer],
electronInstallerSpawnOptions(repositoryRoot),
);
if (child.stderr) {
process.stderr.write(child.stderr);
}
if (child.error) {
const message = child.error.stack ?? child.error.message;
process.stderr.write(`${message}\n`);
return {
status: child.status,
signal: child.signal,
spawnError: true,
context: child.output?.[3] ?? '',
stderr: `${child.stderr ?? ''}\n${message}`,
};
}
return {
status: child.status,
signal: child.signal,
context: child.output?.[3] ?? '',
stderr: child.stderr ?? '',
};
});
if (result.signal) {
process.kill(process.pid, result.signal);
} else {
process.exitCode = result.status ?? 1;
}
}
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
await main();
}