All files / core/engine process-runner.service.ts

100% Statements 36/36
92.85% Branches 13/14
100% Functions 8/8
100% Lines 34/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 1154x 4x                   4x             10x     10x           10x                     10x   1x             10x 9x     10x 10x 10x   10x 9x 4x       10x 9x 2x       10x 2x   2x 2x 1x   1x 1x 1x             2x               10x 7x 7x               10x 1x 1x                      
import { Injectable } from '@nestjs/common';
import { spawn, ChildProcess } from 'child_process';
 
export interface IProcessResult {
  exitCode: number | null;
  stdout: string;
  stderr: string;
  timedOut: boolean;
}
 
@Injectable()
export class ProcessRunnerService {
  async runProcess(
    command: string,
    args: string[],
    cwd: string,
    timeoutMs: number
  ): Promise<IProcessResult> {
    return new Promise((resolve) => {
      // With shell: true, pass the full command as a single string
      // If args are provided, join them; otherwise use command as-is
      const shellCommand = args.length > 0
        ? `${command} ${args.join(' ')}`
        : command;
 
      // Explicitly wrap in sh -c to ensure shell interpretation
      // This ensures pipes and other shell operators are properly interpreted
      const childProcess: ChildProcess = spawn(
        'sh',
        [ '-c', shellCommand ],
        {
        cwd,
        // We're explicitly invoking sh, so no need for shell: true
        shell: false,
        detached: false
        }
      );
 
      if (process.env.PROCESS_RUNNER_DEBUG === 'true') {
        // eslint-disable-next-line no-console
        console.log(
          `[ProcessRunner] spawn command="${shellCommand}" cwd="${cwd}"`
        );
      }
 
      // Close stdin so commands expecting EOF when run without a TTY
      // (like cursor-agent in non-interactive mode) can exit gracefully.
      if (childProcess.stdin) {
        childProcess.stdin.end();
      }
 
      let stdout = '';
      let stderr = '';
      let timedOut = false;
 
      if (childProcess.stdout) {
        childProcess.stdout.on('data', (data) => {
          stdout += data.toString();
        });
      }
 
      if (childProcess.stderr) {
        childProcess.stderr.on('data', (data) => {
          stderr += data.toString();
        });
      }
 
      const timeout = setTimeout(() => {
        timedOut = true;
        // Kill the process group to ensure all children are terminated
        try {
          if (childProcess.pid) {
            childProcess.kill('SIGTERM');
            // Give it a moment, then force kill
            setTimeout(() => {
              Eif (!childProcess.killed) {
                childProcess.kill('SIGKILL');
              }
            }, 1000);
          }
        } catch {
          // Ignore kill errors
        }
        resolve({
          exitCode: null,
          stdout,
          stderr,
          timedOut: true
        });
      }, timeoutMs);
 
      childProcess.on('close', (code) => {
        clearTimeout(timeout);
        resolve({
          exitCode: code,
          stdout,
          stderr,
          timedOut
        });
      });
 
      childProcess.on('error', () => {
        clearTimeout(timeout);
        resolve({
          exitCode: null,
          stdout,
          stderr,
          timedOut
        });
      });
    });
  }
}