109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
import { spawn, type ChildProcess } from 'node:child_process';
|
|
import { existsSync } from 'node:fs';
|
|
import { createRequire } from 'node:module';
|
|
import { dirname, join } from 'node:path';
|
|
import { app } from 'electron';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const OUTPUT_LIMIT = 64 * 1024;
|
|
|
|
export class PublishRuntimeError extends Error {
|
|
constructor(readonly code: 'LOCAL_BUILD_RUNTIME_UNAVAILABLE' | 'LOCAL_BUILD_FAILED' | 'LOCAL_BUILD_TIMEOUT') {
|
|
super(code);
|
|
this.name = 'PublishRuntimeError';
|
|
}
|
|
}
|
|
|
|
export type PublishRuntime = {
|
|
npmCli: string;
|
|
nodeVersion: string;
|
|
npmVersion: string;
|
|
};
|
|
|
|
function childEnvironment(): NodeJS.ProcessEnv {
|
|
const allowed = ['ALLUSERSPROFILE', 'APPDATA', 'COMMONPROGRAMFILES', 'COMMONPROGRAMFILES(X86)',
|
|
'COMMONPROGRAMW6432', 'COMSPEC', 'HOME', 'HOMEDRIVE', 'HOMEPATH', 'LOCALAPPDATA',
|
|
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'NODE_EXTRA_CA_CERTS',
|
|
'http_proxy', 'https_proxy', 'no_proxy',
|
|
'NUMBER_OF_PROCESSORS', 'OS', 'PATH', 'PATHEXT', 'PROGRAMDATA', 'PROGRAMFILES',
|
|
'PROGRAMFILES(X86)', 'PROGRAMW6432', 'SYSTEMDRIVE', 'SYSTEMROOT', 'TEMP', 'TMP',
|
|
'USERPROFILE', 'WINDIR', 'XDG_CACHE_HOME', 'XDG_CONFIG_HOME'];
|
|
const env: NodeJS.ProcessEnv = { ELECTRON_RUN_AS_NODE: '1', CI: '1', npm_config_update_notifier: 'false' };
|
|
for (const key of allowed) {
|
|
const value = process.env[key];
|
|
if (value !== undefined) env[key] = value;
|
|
}
|
|
return env;
|
|
}
|
|
|
|
async function terminateTree(child: ChildProcess): Promise<void> {
|
|
if (!child.pid || child.exitCode !== null) return;
|
|
if (process.platform === 'win32') {
|
|
const taskkill = join(process.env.SYSTEMROOT ?? 'C:\\Windows', 'System32', 'taskkill.exe');
|
|
await new Promise<void>((resolve) => {
|
|
const killer = spawn(taskkill, ['/pid', String(child.pid), '/t', '/f'], { shell: false, windowsHide: true });
|
|
killer.once('close', () => resolve());
|
|
killer.once('error', () => resolve());
|
|
});
|
|
} else {
|
|
try { process.kill(-child.pid, 'SIGKILL'); } catch { child.kill('SIGKILL'); }
|
|
}
|
|
}
|
|
|
|
export async function runElectronNode(input: {
|
|
args: string[];
|
|
cwd?: string;
|
|
deadlineMs: number;
|
|
}): Promise<{ stdout: string; stderr: string }> {
|
|
return await new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, input.args, {
|
|
cwd: input.cwd,
|
|
env: childEnvironment(),
|
|
shell: false,
|
|
windowsHide: true,
|
|
detached: process.platform !== 'win32',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
let stdout = '';
|
|
let stderr = '';
|
|
const append = (current: string, chunk: Buffer) => (current + chunk.toString('utf8')).slice(-OUTPUT_LIMIT);
|
|
child.stdout?.on('data', (chunk: Buffer) => { stdout = append(stdout, chunk); });
|
|
child.stderr?.on('data', (chunk: Buffer) => { stderr = append(stderr, chunk); });
|
|
let timedOut = false;
|
|
const timer = setTimeout(() => {
|
|
timedOut = true;
|
|
void terminateTree(child);
|
|
}, input.deadlineMs);
|
|
child.once('error', () => {
|
|
clearTimeout(timer);
|
|
reject(new PublishRuntimeError('LOCAL_BUILD_RUNTIME_UNAVAILABLE'));
|
|
});
|
|
child.once('close', (code) => {
|
|
clearTimeout(timer);
|
|
if (timedOut) reject(new PublishRuntimeError('LOCAL_BUILD_TIMEOUT'));
|
|
else if (code !== 0) reject(new PublishRuntimeError('LOCAL_BUILD_FAILED'));
|
|
else resolve({ stdout, stderr });
|
|
});
|
|
});
|
|
}
|
|
|
|
export async function resolvePublishRuntime(): Promise<PublishRuntime> {
|
|
try {
|
|
const packagedNpmPackage = join(process.resourcesPath, 'publish-runtime', 'package.json');
|
|
const npmPackage = app.isPackaged && existsSync(packagedNpmPackage)
|
|
? packagedNpmPackage
|
|
: require.resolve('npm/package.json');
|
|
const npmCli = join(dirname(npmPackage), 'bin', 'npm-cli.js');
|
|
const npmResult = await runElectronNode({ args: [npmCli, '--version'], deadlineMs: 10_000 });
|
|
const nodeResult = await runElectronNode({ args: ['--version'], deadlineMs: 10_000 });
|
|
return {
|
|
npmCli,
|
|
nodeVersion: nodeResult.stdout.trim().replace(/^v/, ''),
|
|
npmVersion: npmResult.stdout.trim(),
|
|
};
|
|
} catch (error) {
|
|
if (error instanceof PublishRuntimeError) throw error;
|
|
throw new PublishRuntimeError('LOCAL_BUILD_RUNTIME_UNAVAILABLE');
|
|
}
|
|
}
|