All files / server/services docker-command-builder.ts

100% Statements 6/6
50% Branches 2/4
100% Functions 2/2
100% Lines 5/5

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 8814x                                                                 3x 3x 3x                                                                             3x                          
import { realpathSync } from "node:fs";
 
/**
 * Docker Command Builder
 * 
 * Handles the construction of secure Docker run commands with all necessary
 * security constraints and resource limits for Arduino sketch execution.
 */
 
interface DockerRunOptions {
  sketchDir: string;
  memoryMB: number;
  cpuLimit: string;
  pidsLimit: number;
  imageName: string;
  command: string[];
  containerName?: string;
  /** Host path for the Arduino compiler cache. When set, the directory is
   *  bind-mounted into the container at the same path and ARDUINO_CACHE_DIR
   *  is forwarded as an environment variable so the compiler inside the
   *  container writes artefacts to the persisted host location. */
  arduinoCacheDir?: string;
}
 
export class DockerCommandBuilder {
  /**
   * Builds a secure Docker run command with all security constraints
   * 
   * @param options - Docker run configuration
   * @returns Array of command arguments for spawn
   */
  static buildSecureRunCommand(options: DockerRunOptions): string[] {
    // Resolve symlinks so Docker Desktop on macOS gets the real path (e.g. /private/tmp not /tmp)
    let realSketchDir = options.sketchDir;
    try { realSketchDir = realpathSync(options.sketchDir); } catch { /* keep original */ }
    return [
      "run",
      "--rm", // Remove container after exit
      ...(options.containerName ? ["--name", options.containerName] : []),
      "-i", // Interactive mode for stdin
      "--network",
      "none", // No network access
      "--memory",
      `${options.memoryMB}m`, // Memory limit
      "--memory-swap",
      `${options.memoryMB}m`, // Disable swap
      "--cpus",
      options.cpuLimit, // CPU limit (e.g., "0.5" for 50%)
      "--pids-limit",
      String(options.pidsLimit), // Limit number of processes
      "--security-opt",
      "no-new-privileges", // Prevent privilege escalation
      "--cap-drop",
      "ALL", // Drop all Linux capabilities
      "-v",
      `${realSketchDir}:/sandbox:rw`, // Mount sketch directory (realpath resolves macOS /tmp symlink)
      // Cache volume: only added when a host cache dir is configured
      ...(options.arduinoCacheDir
        ? [
            "-v",
            `${options.arduinoCacheDir}:${options.arduinoCacheDir}`,
            "-e",
            `ARDUINO_CACHE_DIR=${options.arduinoCacheDir}`,
          ]
        : []),
      options.imageName,
      ...options.command, // Execution command
    ];
  }
 
  /**
   * Builds the compile and run command for Docker
   */
  static buildCompileAndRunCommand(): string[] {
    return [
      "sh",
      "-c",
      // The echo marker ensures that a successful silent compilation (no
      // warnings, no output) still triggers an onStdout event so that
      // isCompilePhase is reset before the sketch starts writing to stderr.
      // Without this, isCompilePhase would stay true and all runtime stderr
      // (SERIAL_EVENT, IO_REGISTRY, …) would accumulate in compileErrorBuffer,
      // causing a spurious 'compilation_error' message on process exit.
      "g++ /sandbox/sketch.cpp -o /tmp/sketch -pthread 2>&1 && echo '[[RUNTIME_START]]' && /tmp/sketch",
    ];
  }
}