* Start a script process
(params: StartScriptParams)
| 423 | * Start a script process |
| 424 | */ |
| 425 | async function handleStartScript(params: StartScriptParams): Promise<StartProcessResponse> { |
| 426 | const startTime = Date.now() |
| 427 | logger.info("[Process:startScript] Starting script", JSON.stringify({ |
| 428 | scriptLength: params.script.length, |
| 429 | cwd: params.cwd, |
| 430 | hasEnv: !!params.env, |
| 431 | timeoutMs: params.timeoutMs, |
| 432 | })) |
| 433 | |
| 434 | let tempScriptPath: string | undefined |
| 435 | |
| 436 | try { |
| 437 | validateCwd(params.cwd) |
| 438 | |
| 439 | // Check process limit |
| 440 | if (activeProcesses.size >= MAX_ACTIVE_PROCESSES) { |
| 441 | throw new Error(`Maximum active processes (${MAX_ACTIVE_PROCESSES}) reached`) |
| 442 | } |
| 443 | |
| 444 | const processId = generateProcessId() |
| 445 | const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS |
| 446 | |
| 447 | // Ensure temp directory exists |
| 448 | const tempDir = getTempDir() |
| 449 | if (!fs.existsSync(tempDir)) { |
| 450 | fs.mkdirSync(tempDir, { recursive: true, mode: 0o700 }) |
| 451 | } |
| 452 | |
| 453 | // Get platform-appropriate shell |
| 454 | const shellConfig = getScriptShell() |
| 455 | |
| 456 | // Write script to temp file |
| 457 | tempScriptPath = path.join(tempDir, `script-${processId}${shellConfig.extension}`) |
| 458 | let fullScript = params.script |
| 459 | // Add preamble for bash scripts that don't have a shebang |
| 460 | if (shellConfig.usePreamble && !params.script.startsWith("#!")) { |
| 461 | fullScript = BASH_SCRIPT_PREAMBLE + params.script |
| 462 | } |
| 463 | fs.writeFileSync(tempScriptPath, fullScript, { mode: 0o700 }) |
| 464 | |
| 465 | // Spawn the script in a new process group so we can kill the entire tree |
| 466 | const childProcess = spawn(shellConfig.shell, shellConfig.args(tempScriptPath), { |
| 467 | cwd: params.cwd, |
| 468 | env: { ...process.env, ...params.env }, |
| 469 | stdio: ["ignore", "pipe", "pipe"], |
| 470 | detached: process.platform !== "win32", // detached doesn't work the same on Windows |
| 471 | }) |
| 472 | |
| 473 | // Store in active processes |
| 474 | activeProcesses.set(processId, { |
| 475 | process: childProcess, |
| 476 | outputBuffer: [], |
| 477 | bufferSizeBytes: 0, |
| 478 | exitCode: null, |
| 479 | signal: null, |
| 480 | completed: false, |
| 481 | tempScriptPath, |
| 482 | }) |
no test coverage detected