129 lines
4.6 KiB
JavaScript
129 lines
4.6 KiB
JavaScript
'use strict';
|
|
|
|
const { exec, spawn } = require('child_process');
|
|
const path = require('path');
|
|
const { getContainerContext, resolveContainerPath, buildEnvArgs } = require('./container_bridge');
|
|
|
|
function resolvePath(workspace, p) {
|
|
if (!p) return workspace;
|
|
if (path.isAbsolute(p)) return p;
|
|
return path.join(workspace, p);
|
|
}
|
|
|
|
function runCommandTool(workspace, args, abortSignal) {
|
|
return new Promise((resolve) => {
|
|
const cmd = args.command;
|
|
const timeoutSec = Number(args.timeout || 0);
|
|
const cwd = resolvePath(workspace, args.working_dir || '.');
|
|
const ctx = getContainerContext();
|
|
if (!cmd) return resolve({ success: false, error: 'command 不能为空' });
|
|
if (abortSignal && abortSignal.aborted) {
|
|
return resolve({ success: false, error: '任务被用户取消', cancelled: true });
|
|
}
|
|
const timeoutMs = Math.max(0, timeoutSec) * 1000;
|
|
let finished = false;
|
|
|
|
if (ctx) {
|
|
const resolved = resolveContainerPath(workspace, args.working_dir || '.', ctx);
|
|
if (resolved.error) {
|
|
return resolve({ success: false, error: resolved.error });
|
|
}
|
|
const workdir = resolved.containerPath || ctx.mountPath;
|
|
const execArgs = [ 'exec', '-i' ];
|
|
if (workdir) execArgs.push('-w', workdir);
|
|
execArgs.push(...buildEnvArgs(ctx));
|
|
const venv = ctx.containerVenv || '/opt/agent-venv';
|
|
const basePath = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
|
const exportPrefix = `export VIRTUAL_ENV=${venv}; export PATH=${venv}/bin:${basePath}; `;
|
|
const wrappedCmd = `${exportPrefix}${cmd}`;
|
|
execArgs.push(ctx.containerName, 'sh', '-c', wrappedCmd);
|
|
const child = spawn(ctx.dockerBin, execArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
let stdout = '';
|
|
let stderr = '';
|
|
const onData = (chunk, buffer) => buffer.push(chunk);
|
|
const stdoutBuf = [];
|
|
const stderrBuf = [];
|
|
child.stdout.on('data', (chunk) => onData(chunk, stdoutBuf));
|
|
child.stderr.on('data', (chunk) => onData(chunk, stderrBuf));
|
|
const done = (result) => {
|
|
if (finished) return;
|
|
finished = true;
|
|
if (abortSignal) abortSignal.removeEventListener('abort', onAbort);
|
|
return resolve(result);
|
|
};
|
|
const onAbort = () => {
|
|
if (finished) return;
|
|
finished = true;
|
|
try {
|
|
child.kill('SIGTERM');
|
|
} catch (_) {}
|
|
return resolve({ success: false, error: '任务被用户取消', cancelled: true });
|
|
};
|
|
if (abortSignal) abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
let timer = null;
|
|
if (timeoutMs) {
|
|
timer = setTimeout(() => {
|
|
try {
|
|
child.kill('SIGTERM');
|
|
} catch (_) {}
|
|
const output = Buffer.concat(stdoutBuf).toString() + Buffer.concat(stderrBuf).toString();
|
|
done({
|
|
success: false,
|
|
status: 'timeout',
|
|
error: '命令超时',
|
|
return_code: null,
|
|
output,
|
|
timeout: timeoutSec,
|
|
});
|
|
}, timeoutMs);
|
|
}
|
|
child.on('close', (code) => {
|
|
if (timer) clearTimeout(timer);
|
|
const output = Buffer.concat(stdoutBuf).toString() + Buffer.concat(stderrBuf).toString();
|
|
if (finished) return;
|
|
if (code && code !== 0) {
|
|
return done({
|
|
success: false,
|
|
status: 'error',
|
|
error: `exit ${code}`,
|
|
return_code: code,
|
|
output,
|
|
});
|
|
}
|
|
return done({ success: true, status: 'ok', output });
|
|
});
|
|
return;
|
|
}
|
|
|
|
const child = exec(cmd, { cwd, timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, shell: true }, (err, stdout, stderr) => {
|
|
if (finished) return;
|
|
finished = true;
|
|
if (abortSignal) abortSignal.removeEventListener('abort', onAbort);
|
|
const output = [stdout, stderr].filter(Boolean).join('');
|
|
if (err) {
|
|
const isTimeout = err.killed && err.signal === 'SIGTERM';
|
|
return resolve({
|
|
success: false,
|
|
status: isTimeout ? 'timeout' : 'error',
|
|
error: err.message,
|
|
return_code: err.code,
|
|
output,
|
|
timeout: timeoutSec,
|
|
});
|
|
}
|
|
resolve({ success: true, status: 'ok', output });
|
|
});
|
|
const onAbort = () => {
|
|
if (finished) return;
|
|
finished = true;
|
|
try {
|
|
child.kill('SIGTERM');
|
|
} catch (_) {}
|
|
return resolve({ success: false, error: '任务被用户取消', cancelled: true });
|
|
};
|
|
if (abortSignal) abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
});
|
|
}
|
|
|
|
module.exports = { runCommandTool };
|