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

97.22% Statements 35/36
57.14% Branches 8/14
100% Functions 8/8
97.05% Lines 33/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 1151x 1x                   1x             65x     65x           65x                     65x   1x             65x 65x     65x 65x 65x   65x 65x 59x       65x 65x 6x       65x 3x   3x 3x 3x   3x 2x               3x               65x 64x 64x               65x 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.
      Eif (childProcess.stdin) {
        childProcess.stdin.end();
      }
 
      let stdout = '';
      let stderr = '';
      let timedOut = false;
 
      Eif (childProcess.stdout) {
        childProcess.stdout.on('data', (data) => {
          stdout += data.toString();
        });
      }
 
      Eif (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 {
          Eif (childProcess.pid) {
            childProcess.kill('SIGTERM');
            // Give it a moment, then force kill
            setTimeout(() => {
              Iif (!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
        });
      });
    });
  }
}