mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-25 15:01:49 +00:00
1909c2ef22
* build(deps): update dependency execa to v10 | datasource | package | from | to | | ---------- | ------- | ----- | ------ | | npm | execa | 8.0.1 | 10.0.1 | * fix(exec): adapt to execa v10 types and subprocess API * fix(tools): type execa v10 results in hook exec and mkdocs --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: secustor <sebastian@poxhofer.at>
263 lines
7.7 KiB
TypeScript
263 lines
7.7 KiB
TypeScript
import type { ChildProcess } from 'node:child_process';
|
|
import type { Readable } from 'node:stream';
|
|
import { isNullOrUndefined } from '@sindresorhus/is';
|
|
import { execa } from 'execa';
|
|
import { join, split } from 'shlex';
|
|
import { instrument } from '../../instrumentation/index.ts';
|
|
import { logger } from '../../logger/index.ts';
|
|
import { getEnv } from '../env.ts';
|
|
import { sanitize } from '../sanitize.ts';
|
|
import type { ExecErrorData } from './exec-error.ts';
|
|
import { ExecError } from './exec-error.ts';
|
|
import type {
|
|
CommandWithOptions,
|
|
DataListener,
|
|
ExecResult,
|
|
OutputWriter,
|
|
RawExecOptions,
|
|
} from './types.ts';
|
|
import { asRawCommand, isCommandWithOptions } from './utils.ts';
|
|
|
|
// https://man7.org/linux/man-pages/man7/signal.7.html#NAME
|
|
// Non TERM/CORE signals
|
|
// The following is step 3. in https://github.com/renovatebot/renovate/issues/16197#issuecomment-1171423890
|
|
const NONTERM = [
|
|
'SIGCHLD',
|
|
'SIGCLD',
|
|
'SIGCONT',
|
|
'SIGSTOP',
|
|
'SIGTSTP',
|
|
'SIGTTIN',
|
|
'SIGTTOU',
|
|
'SIGURG',
|
|
'SIGWINCH',
|
|
];
|
|
|
|
const encoding = 'utf8';
|
|
|
|
function stringify(list: Buffer[], writer: OutputWriter | undefined): string {
|
|
return writer?.toString() ?? Buffer.concat(list).toString(encoding);
|
|
}
|
|
|
|
function initStreamListeners(
|
|
cp: ChildProcess,
|
|
opts: RawExecOptions & { maxBuffer: number },
|
|
): [Buffer[], Buffer[]] {
|
|
const stdout: Buffer[] = [];
|
|
const stderr: Buffer[] = [];
|
|
let stdoutLen = 0;
|
|
let stderrLen = 0;
|
|
|
|
registerDataListeners(cp.stdout, opts.outputListeners?.stdout);
|
|
registerDataListeners(cp.stderr, opts.outputListeners?.stderr);
|
|
|
|
cp.stdout?.on('data', (chunk: Buffer) => {
|
|
// process.stdout.write(data.toString());
|
|
if (opts.outputWriters?.stdout) {
|
|
opts.outputWriters.stdout.write(chunk);
|
|
return;
|
|
}
|
|
|
|
const len = Buffer.byteLength(chunk, encoding);
|
|
stdoutLen += len;
|
|
if (stdoutLen > opts.maxBuffer) {
|
|
cp.emit('error', new Error('stdout maxBuffer exceeded'));
|
|
} else {
|
|
stdout.push(chunk);
|
|
}
|
|
});
|
|
|
|
cp.stderr?.on('data', (chunk: Buffer) => {
|
|
// process.stderr.write(data.toString());
|
|
if (opts.outputWriters?.stderr) {
|
|
opts.outputWriters.stderr.write(chunk);
|
|
return;
|
|
}
|
|
|
|
const len = Buffer.byteLength(chunk, encoding);
|
|
stderrLen += len;
|
|
if (stderrLen > opts.maxBuffer) {
|
|
cp.emit('error', new Error('stderr maxBuffer exceeded'));
|
|
} else {
|
|
stderr.push(chunk);
|
|
}
|
|
});
|
|
return [stdout, stderr];
|
|
}
|
|
|
|
function registerDataListeners(
|
|
readable: Readable | null,
|
|
dataListeners: DataListener[] | undefined,
|
|
): void {
|
|
if (isNullOrUndefined(readable) || isNullOrUndefined(dataListeners)) {
|
|
return;
|
|
}
|
|
|
|
for (const listener of dataListeners) {
|
|
readable.on('data', listener);
|
|
}
|
|
}
|
|
|
|
export function exec(
|
|
commandArgument: string | CommandWithOptions,
|
|
opts: RawExecOptions,
|
|
): Promise<ExecResult> {
|
|
let theCmd = commandArgument;
|
|
let ignoreFailure = false;
|
|
if (isCommandWithOptions(commandArgument)) {
|
|
theCmd = join(commandArgument.command);
|
|
if (commandArgument.ignoreFailure !== undefined) {
|
|
ignoreFailure = commandArgument.ignoreFailure;
|
|
}
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let cmd = asRawCommand(theCmd);
|
|
let args: string[] = [];
|
|
const maxBuffer = opts.maxBuffer ?? 10 * 1024 * 1024; // Set default max buffer size to 10MB
|
|
|
|
// don't use shell by default, as it leads to potential security issues
|
|
let shell = opts.shell ?? false;
|
|
if (
|
|
isCommandWithOptions(commandArgument) &&
|
|
commandArgument.shell !== undefined
|
|
) {
|
|
shell = commandArgument.shell;
|
|
}
|
|
|
|
// if we're not in shell mode, we need to provide the command and arguments
|
|
if (shell === false) {
|
|
const parts = split(cmd);
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (parts) {
|
|
cmd = parts[0];
|
|
args = parts.slice(1);
|
|
}
|
|
}
|
|
|
|
const subprocess = execa(cmd, args, {
|
|
...opts,
|
|
// force detached on non WIN platforms
|
|
// https://github.com/nodejs/node/issues/21825#issuecomment-611328888
|
|
detached: process.platform !== 'win32',
|
|
shell,
|
|
extendEnv: false,
|
|
// Suppress execa's internal promise rejection (e.g., from timeout).
|
|
// We handle all exit scenarios via 'exit' and 'error' event listeners below,
|
|
// so the promise rejection would otherwise surface as an unhandledRejection.
|
|
// TODO: Refactor to await execa result (#45650)
|
|
reject: false,
|
|
});
|
|
const cp = subprocess.nodeChildProcess;
|
|
|
|
// handle streams
|
|
const [stdout, stderr] = initStreamListeners(cp, {
|
|
...opts,
|
|
maxBuffer,
|
|
});
|
|
|
|
// handle process events
|
|
void cp.on('error', (error) => {
|
|
kill(cp, 'SIGTERM');
|
|
// rethrowing, use originally emitted error message
|
|
reject(new ExecError(error.message, rejectInfo(), error));
|
|
});
|
|
|
|
void cp.on('exit', (code: number, signal: NodeJS.Signals) => {
|
|
if (NONTERM.includes(signal)) {
|
|
return;
|
|
}
|
|
if (signal) {
|
|
kill(cp, signal);
|
|
reject(
|
|
new ExecError(
|
|
`Command failed: ${cp.spawnargs.join(' ')}\nInterrupted by ${signal}`,
|
|
{
|
|
...rejectInfo(),
|
|
signal,
|
|
},
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
if (code !== 0) {
|
|
if (ignoreFailure === undefined || ignoreFailure === false) {
|
|
reject(
|
|
new ExecError(
|
|
`Command failed: ${cp.spawnargs.join(' ')}\n${stringify(stderr, opts.outputWriters?.stderr)}`,
|
|
{
|
|
...rejectInfo(),
|
|
exitCode: code,
|
|
},
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
logger.once.debug(
|
|
{
|
|
command: cp.spawnargs.join(' '),
|
|
stdout: stringify(stdout, opts.outputWriters?.stdout),
|
|
stderr: stringify(stderr, opts.outputWriters?.stderr),
|
|
exitCode: code,
|
|
},
|
|
`Ignoring failure to execute comamnd \`${cp.spawnargs.join(' ')}\`, as ignoreFailure=true is set`,
|
|
);
|
|
|
|
resolve({
|
|
stderr: stringify(stderr, opts.outputWriters?.stderr),
|
|
stdout: stringify(stdout, opts.outputWriters?.stdout),
|
|
exitCode: code,
|
|
});
|
|
return;
|
|
}
|
|
resolve({
|
|
stderr: stringify(stderr, opts.outputWriters?.stderr),
|
|
stdout: stringify(stdout, opts.outputWriters?.stdout),
|
|
});
|
|
});
|
|
|
|
function rejectInfo(): ExecErrorData {
|
|
return {
|
|
cmd: cp.spawnargs.join(' '),
|
|
options: opts,
|
|
stdout: stringify(stdout, opts.outputWriters?.stdout),
|
|
stderr: stringify(stderr, opts.outputWriters?.stderr),
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
function kill(cp: ChildProcess, signal: NodeJS.Signals): boolean {
|
|
try {
|
|
if (cp.pid && getEnv().RENOVATE_X_EXEC_GPID_HANDLE) {
|
|
/**
|
|
* If `pid` is negative, but not `-1`, signal shall be sent to all processes
|
|
* (excluding an unspecified set of system processes),
|
|
* whose process group ID (pgid) is equal to the absolute value of pid,
|
|
* and for which the process has permission to send a signal.
|
|
*/
|
|
return process.kill(-cp.pid, signal);
|
|
}
|
|
// destroying stdio is needed for unref to work
|
|
// https://nodejs.org/api/child_process.html#subprocessunref
|
|
// https://github.com/nodejs/node/blob/4d5ff25a813fd18939c9f76b17e36291e3ea15c3/lib/child_process.js#L412-L426
|
|
cp.stderr?.destroy();
|
|
cp.stdout?.destroy();
|
|
cp.unref();
|
|
return cp.kill(signal);
|
|
} catch {
|
|
// cp is a single node tree, therefore -pid is invalid as there is no such pgid,
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export function rawExec(
|
|
cmd: string | CommandWithOptions,
|
|
opts: RawExecOptions,
|
|
): Promise<ExecResult> {
|
|
return instrument(`rawExec: ${sanitize(asRawCommand(cmd))}`, () =>
|
|
exec(cmd, opts),
|
|
);
|
|
}
|