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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 | 75x 75x 75x 75x 75x 75x 8x 1x 1x 7x 8x 4x 4x 4x 8x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 4x 4x 4x 4x 4x 4x 3x 4x 1x 3x 4x 3x 3x 3x 3x 3x 4x 1x | /**
* Docker-Manager: Manages Docker container lifecycle, setup, and event handling
* Extracted from Etappe A: Docker-Lifecycle refactoring
*/
import type { IProcessController } from "../process-controller";
import type { ArduinoOutputParser, ParsedStderrOutput } from "../arduino-output-parser";
import { Logger } from "@shared/logger";
import type { SimulationTimeoutManager } from "../simulation-timeout-manager";
interface DockerManagerCallbacks {
onOutput: (line: string, isComplete?: boolean) => void;
onPinState: (pin: number, type: "mode" | "value" | "pwm", value: number) => void;
onError: (line: string) => void;
}
interface DockerProcessConfig {
flushBatchers: () => void;
flushMessageQueue: () => void;
/** Use a getter so the guard reflects the live value, preventing stale-capture bugs. */
getProcessKilled: () => boolean;
executionTimeout?: number;
onStateTransition?: (state: "running" | "stopped") => void;
}
interface DockerEventHandlers {
onCompileError?: (error: string) => void;
onCompileSuccess?: () => void;
onExit?: (code: number | null) => void;
}
interface DockerHandlerState {
isCompilePhase: { value: boolean };
compileErrorBuffer: { value: string };
compileSuccessSent: { value: boolean };
totalOutputBytes: number;
processStartTime: number | null;
stderrFallbackBuffer: string;
flushTimer: NodeJS.Timeout | null;
}
type HandleParsedLineDelegate = (parsed: ParsedStderrOutput, callbacks: DockerManagerCallbacks) => void;
export class DockerManager {
private readonly logger = new Logger("DockerManager");
private readonly SANDBOX_CONFIG = {
maxOutputBytes: 100 * 1024 * 1024, // Max 100MB output
maxExecutionTimeSec: 60, // Max 60 seconds runtime
};
constructor(
private readonly processController: IProcessController,
private readonly stderrParser: ArduinoOutputParser,
private readonly timeoutManager: SimulationTimeoutManager,
private readonly handleParsedLine: HandleParsedLineDelegate,
) {}
/**
* Setup and configure Docker process timeout
*/
setupDockerTimeout(executionTimeout: number | undefined, callbacks: DockerManagerCallbacks): void {
// executionTimeout === 0 means "infinite" (user selected ∞ in the Tools menu)
if (executionTimeout === 0) {
this.logger.debug("Infinite timeout configured – no timer scheduled");
return;
}
const timeoutSec =
executionTimeout && executionTimeout > 0 ? executionTimeout : this.SANDBOX_CONFIG.maxExecutionTimeSec;
const handleTimeout = () => {
this.processController.kill("SIGKILL");
callbacks.onOutput(`--- Simulation timeout (${timeoutSec}s) ---`, true);
this.logger.info(`Docker timeout after ${timeoutSec}s`);
};
this.timeoutManager.schedule(timeoutSec * 1000, handleTimeout);
}
/**
* Setup Docker stdout handler (detects end of compile phase, parses output)
*/
setupStdoutHandler(
callbacks: DockerManagerCallbacks,
state: Partial<DockerHandlerState>,
onCompileSuccess?: () => void,
): void {
const isCompilePhase = state.isCompilePhase as { value: boolean };
const compileSuccessSent = state.compileSuccessSent as { value: boolean };
this.processController.onStdout((data) => {
const str = data.toString();
// Detect end of compile phase
Eif (isCompilePhase.value) {
isCompilePhase.value = false;
Iif (!compileSuccessSent.value && onCompileSuccess) {
compileSuccessSent.value = true;
onCompileSuccess();
}
}
// Check output size limit
const currentBytes = state.totalOutputBytes || 0;
state.totalOutputBytes = currentBytes + str.length;
Eif (state.totalOutputBytes > this.SANDBOX_CONFIG.maxOutputBytes) {
callbacks.onError("Output size limit exceeded");
return;
}
// Parse stdout lines (safety net for direct binary output)
const lines = str.split(/\r?\n/);
lines.forEach((line) => {
// Filter the compile-phase sentinel added by buildCompileAndRunCommand.
// Its sole purpose is to trigger the isCompilePhase reset above and
// must not be forwarded to the protocol parser or the client.
if (!line || line.trim() === '[[RUNTIME_START]]') return;
const parsed = this.stderrParser.parseStderrLine(line, state.processStartTime || 0);
this.handleParsedLine(parsed, callbacks);
});
});
}
/**
* Setup Docker stderr handlers (raw + fallback + readline)
*/
setupStderrHandlers(
callbacks: DockerManagerCallbacks,
state: Partial<DockerHandlerState>,
): void {
const isCompilePhase = state.isCompilePhase as { value: boolean };
const compileErrorBuffer = state.compileErrorBuffer as { value: string };
const useFallbackParser = !this.processController.supportsStderrLineStreaming();
// Raw stderr stream for compile aggregation
this.processController.onStderr((data) => {
const chunk = data.toString();
Eif (isCompilePhase.value) {
compileErrorBuffer.value += chunk;
}
// Fallback parsing when readline is unavailable
Eif (useFallbackParser) {
state.stderrFallbackBuffer = (state.stderrFallbackBuffer || "") + chunk;
const lines = state.stderrFallbackBuffer.split(/\r?\n/);
state.stderrFallbackBuffer = lines.pop() || "";
for (const line of lines) {
Iif (!line) continue;
const parsed = this.stderrParser.parseStderrLine(line, state.processStartTime || 0);
this.handleParsedLine(parsed, callbacks);
}
}
});
// Readline-based stderr line stream (preferred when available)
this.processController.onStderrLine((line) => {
if (line.length === 0) return;
const parsed = this.stderrParser.parseStderrLine(line, state.processStartTime || 0);
this.handleParsedLine(parsed, callbacks);
});
}
/**
* Handle Docker process exit (cleanup, final parsing, callbacks)
*/
handleDockerExit(
callbacks: DockerManagerCallbacks,
state: Partial<DockerHandlerState>,
code: number | null,
config: DockerProcessConfig,
handlers: DockerEventHandlers,
): void {
const isCompilePhase = state.isCompilePhase as { value: boolean };
const compileErrorBuffer = state.compileErrorBuffer as { value: string };
const useFallbackParser = !this.processController.supportsStderrLineStreaming();
// Flush any remaining data in stderr fallback buffer
Iif (state.stderrFallbackBuffer && useFallbackParser) {
const buffered = state.stderrFallbackBuffer;
state.stderrFallbackBuffer = "";
if (buffered.trim()) {
const parsed = this.stderrParser.parseStderrLine(buffered, state.processStartTime || 0);
this.handleParsedLine(parsed, callbacks);
}
}
// Flush message queue before exit
config.flushMessageQueue();
// Flush batchers if not still in compile phase
if (!isCompilePhase.value || code === 0) {
config.flushBatchers();
}
// Report compile errors or success
if (code !== 0 && isCompilePhase.value && compileErrorBuffer.value && handlers.onCompileError) {
handlers.onCompileError(this.cleanCompilerErrors(compileErrorBuffer.value));
I} else if (code === 0 && handlers.onCompileSuccess) {
handlers.onCompileSuccess();
}
// Call exit callback (guard: only if process wasn't terminated by stop())
Eif (!config.getProcessKilled() && handlers.onExit) handlers.onExit(code);
}
/**
* Setup all Docker handlers (timeout, stdout, stderr, close)
*/
setupDockerHandlers(
callbacks: DockerManagerCallbacks,
state: Partial<DockerHandlerState>,
config: DockerProcessConfig,
handlers: DockerEventHandlers,
): void {
// Setup all handlers via dedicated functions
this.setupDockerTimeout(config.executionTimeout, callbacks);
this.processController.onError((err) => {
this.logger.error(`Docker process error: ${err.message}`);
callbacks.onError(`Docker process failed: ${err.message}`);
});
this.setupStdoutHandler(callbacks, state, handlers.onCompileSuccess);
this.setupStderrHandlers(callbacks, state);
this.processController.onClose((code) => {
this.handleDockerExit(
callbacks,
state,
code,
config,
handlers,
);
});
}
/**
* Clean up compiler error messages
*/
private cleanCompilerErrors(errors: string): string {
return errors.replaceAll("/sandbox/sketch.cpp", "sketch.ino").replaceAll(/(?:\/[^\s:/]+)+\/temp\/[a-f0-9-]+\/sketch\.cpp/gi, "sketch.ino").trim();
}
/**
* Complete Docker orchestration: spawn process and setup all handlers
* This consolidates runInDocker + setupDockerHandlers into a single delegated call
*/
async runInDockerWithHandlers(
dockerArgs: string[],
callbacks: DockerManagerCallbacks,
state: Partial<DockerHandlerState>,
config: DockerProcessConfig,
handlers: DockerEventHandlers,
): Promise<void> {
try {
// Clear listeners from previous run before spawning new process
this.processController.clearListeners();
// Spawn Docker process
await this.processController.spawn("docker", dockerArgs);
this.logger.info("🚀 Docker: Compile + Run in single container");
// Record process start time and transition to running
state.processStartTime = Date.now();
config.onStateTransition?.("running");
// Setup all handlers for Docker process
this.setupDockerHandlers(
callbacks,
state,
config,
handlers,
);
} catch (err) {
this.logger.error(`Docker process spawn failed: ${err instanceof Error ? err.message : String(err)}`);
config.onStateTransition?.("stopped");
throw err;
}
}
}
|