All files / server/services/sandbox/execution-phases cleanup-phase.ts

97.72% Statements 43/44
95% Branches 38/40
100% Functions 6/6
100% Lines 41/41

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                                      15x 10x     5x 5x   5x 10x 3x 7x 3x 4x 3x                 11x 9x   11x 9x                     30x 2x     28x 28x       25x         8x     8x 7x 7x     18x 18x   3x 3x             31x     29x 29x 22x 22x 22x 22x 22x 22x 14x     22x   22x 22x    
// execution-phases/cleanup-phase.ts
// Verantwortlichkeit: Cleanup nach Ausführung
// - Batchers flushen und stoppen
// - Message Queue leeren
// - Docker-Container cleanup
 
import type { Logger } from "@shared/logger";
import type { ProcessExecution } from "../../process-execution-port";
import type { ExecutionState } from "../execution-manager";
 
export interface CleanupDependencies {
  processExecutor: ProcessExecution;
  logger: Logger;
}
 
/**
 * Flush die Message Queue und sendet alle queued Messages an Callbacks
 */
export function flushMessageQueue(state: ExecutionState): void {
  if (state.messageQueue.length === 0) {
    return;
  }
 
  const queue = state.messageQueue;
  state.messageQueue = [];
 
  for (const msg of queue) {
    if (msg.type === "pinState" && state.pinStateCallback) {
      state.pinStateCallback(msg.data.pin, msg.data.stateType, msg.data.value);
    } else if (msg.type === "output" && state.onOutputCallback) {
      state.onOutputCallback(msg.data.line, msg.data.isComplete);
    } else if (msg.type === "error" && state.errorCallback) {
      state.errorCallback(msg.data.line);
    }
  }
}
 
/**
 * Stoppt Batchers (SerialOutputBatcher und PinStateBatcher)
 */
export function flushBatchers(state: ExecutionState): void {
  if (state.serialOutputBatcher) {
    state.serialOutputBatcher.stop();
  }
  if (state.pinStateBatcher) {
    state.pinStateBatcher.stop();
  }
}
 
/**
 * Räumt Docker-Container auf
 */
export async function cleanupDockerContainer(
  containerName: string | undefined,
  deps: CleanupDependencies,
): Promise<boolean> {
  if (!containerName) {
    return true;
  }
 
  try {
    const result = await deps.processExecutor.execute("docker", ["rm", "-f", containerName], {
      timeout: 5000,
      stdio: "pipe",
    });
    if (result.code !== 0 || result.error) {
      // docker run uses --rm. A failed rm can mean auto-removal, but only a
      // successful daemon listing can establish absence (not an error string).
      // A name filter can conservatively match more than the exact name;
      // an empty successful listing still proves this container is absent.
      const remaining = await deps.processExecutor.execute("docker", [
        "container", "ls", "--all", "--filter", `name=${containerName}`, "--quiet",
      ], { timeout: 5000, stdio: "pipe" });
      if (remaining.code !== 0 || remaining.error || remaining.stdout?.trim() !== "") {
        deps.logger.warn(`Docker cleanup failed for ${containerName} (code ${result.code})`);
        return false;
      }
    }
    deps.logger.info(`Docker container cleanup: ${containerName}`);
    return true;
  } catch (error) {
    deps.logger.debug(`Docker cleanup failed for ${containerName}: ${error}`);
    return false;
  }
}
 
 
// Natural close, deadline and explicit Stop must join the same removal. The
// weak key retains no completed sessions; each runner owns at most one flight.
const cleanupFlights = new WeakMap<ExecutionState, { name: string; completion: Promise<void> }>();
 
export function cleanupExecutionContainer(state: ExecutionState, deps: CleanupDependencies): Promise<void> {
  const name = state.currentContainerName;
  if (!name) return Promise.resolve();
  const pending = cleanupFlights.get(state);
  Iif (pending?.name === name) return pending.completion;
  const generation = state.runGeneration;
  const flight = { name, completion: Promise.resolve() };
  flight.completion = cleanupDockerContainer(name, deps).then((removed) => {
    if (removed && state.runGeneration === generation && state.currentContainerName === name) {
      state.currentContainerName = undefined;
    }
  }).finally(() => {
    Eif (cleanupFlights.get(state) === flight) cleanupFlights.delete(state);
  });
  cleanupFlights.set(state, flight);
  return flight.completion;
}