Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 14 additions & 24 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
getStartupConfigurationEntries,
} from "./startup-access";
import { apiRateLimitKey, shouldSkipApiRateLimit } from "./rate-limit-policy";
import {
safeErrorLogMetadata,
safeErrorStackFrames,
} from "./services/safe-error-log-metadata";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

Expand Down Expand Up @@ -152,40 +156,29 @@ app.use(
app.use((req, res, next) => {
const start = Date.now();
const reqPath = req.path;
let capturedJsonResponse: Record<string, any> | undefined = undefined;

const originalResJson = res.json;
res.json = function (bodyJson, ...args) {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};

res.on("finish", () => {
const duration = Date.now() - start;
if (reqPath.startsWith("/api") && reqPath !== "/api/health") {
let logLine = `${req.method} ${reqPath} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}

if (logLine.length > 80) {
logLine = logLine.slice(0, 79) + "…";
}

log(logLine);
log(`${req.method} ${reqPath} ${res.statusCode} in ${duration}ms`);
}
});

next();
});

// Global error handlers to prevent server crashes
process.on("unhandledRejection", (reason, promise) => {
console.error(`[ERROR] Unhandled Promise Rejection:`, promise, reason);
function safeStackSuffix(error: unknown): string {
const stackFrames = safeErrorStackFrames(error);
return stackFrames ? "\n" + stackFrames : "";
}

process.on("unhandledRejection", (reason) => {
console.error("[ERROR] Unhandled Promise Rejection (" + safeErrorLogMetadata(reason) + ")" + safeStackSuffix(reason));
});

process.on("uncaughtException", (error) => {
console.error(`[ERROR] Uncaught Exception:`, error);
console.error("[ERROR] Uncaught Exception (" + safeErrorLogMetadata(error) + ")" + safeStackSuffix(error));
// In development, keep running; in production may want to restart
if (config.serverMode === "docker") {
console.error("Shutting down due to uncaught exception");
Expand All @@ -211,10 +204,7 @@ let cleanupTimer: NodeJS.Timeout | null = null;

// Logging für Debugging (Server-seitig)
if (status >= 500) {
console.error(
`[ERROR] ${status}: ${err.message}`,
isProduction ? "" : err.stack,
);
console.error(`[ERROR] ${status} (` + safeErrorLogMetadata(err) + ")" + safeStackSuffix(err));
}

res.status(status).json({ message });
Expand Down
3 changes: 2 additions & 1 deletion server/routes/compiler.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { RequestIdentity } from "../security/access-control";
import type { RateLimitResult } from "../services/rate-limiter";
import { operationError, SYSTEM_BUSY_MESSAGE } from "@shared/operation-errors";
import { CompileCapacityError } from "../services/compilation-worker-pool";
import { safeErrorLogMetadata } from "../services/safe-error-log-metadata";

type CompilerHeader = { name: string; content: string };

Expand Down Expand Up @@ -208,7 +209,7 @@ export function registerCompilerRoutes(app: Express, deps: CompilerDeps) {
return res.status(503).json({ error: operationError("SYSTEM_BUSY", SYSTEM_BUSY_MESSAGE, retryAfter) });
}
recordCompileErrorIfNeeded(compiler, compileStartTime, error);
logger.error(`[Compiler Route] Error during /api/compile: ${error instanceof Error ? error.message : String(error)}`);
logger.error(`[Compiler Route] Error during /api/compile (${safeErrorLogMetadata(error)})`);
res.status(500).json({ error: "Compilation failed" });
}
});
Expand Down
7 changes: 4 additions & 3 deletions server/routes/simulation.ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import { operationError, SYSTEM_BUSY_MESSAGE } from "@shared/operation-errors";
import { config } from "../config";
import { InboundMessageLimiter } from "./simulation/ws-inbound-limiter";
import { safeErrorLogMetadata } from "../services/safe-error-log-metadata";

function sendStartError(
ws: WebSocket,
Expand Down Expand Up @@ -252,7 +253,7 @@ export function registerSimulationWebSocket(
};

const onError = (err: string) => {
logger.warn(`[Client WS][ERR]: ${err}`);
logger.warn(`[Client WS][ERR] diagnostic received (${Buffer.byteLength(err)} bytes)`);
outputBuffer.flushSerialOutputBuffer(ws);
sendMessageToClient(ws, {
type: WSMessageType.SERIAL_OUTPUT,
Expand Down Expand Up @@ -323,7 +324,7 @@ export function registerSimulationWebSocket(
);
});
}
logger.error(`[Client Compile Error]: ${compileErr}`);
logger.error(`[Client Compile Error] diagnostic received (${Buffer.byteLength(compileErr)} bytes)`);
};

const onCompileSuccess = () => {
Expand Down Expand Up @@ -644,7 +645,7 @@ export function registerSimulationWebSocket(
});
if (!processReady) return;
} catch (error) {
logger.error(`[Simulation] runSketch failed: ${error}`);
logger.error(`[Simulation] runSketch failed (${safeErrorLogMetadata(error)})`);
await sessionManager.safeReleaseRunner(
clientState,
"runSketch-error",
Expand Down
2 changes: 1 addition & 1 deletion server/services/arduino-output-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export class ArduinoOutputParser {
/^[A-Za-z0-9+/=:]+\]\]$/.test(line) || // timestamp:base64 tail + ]]
/^\d+:[A-Za-z0-9+/=]+/.test(line) // timestamp:base64 (no brackets)
) {
logger.debug(`Ignoring protocol fragment: ${line.slice(0, 80)}...`);
logger.debug("Ignoring partial simulator protocol fragment");
return { type: "ignored" };
}

Expand Down
2 changes: 1 addition & 1 deletion server/services/compilation-worker-pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export class CompilationWorkerPool {

worker.on("error", (err) => {
if (!isCurrent()) return;
this.logger.error(`[Worker ${workerId}] Error: ${err.message}`);
this.logger.error(`[Worker ${workerId}] Error (${err.name})`);
this.handleWorkerFailure(workerId, err);
});

Expand Down
6 changes: 5 additions & 1 deletion server/services/compiler/cli-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ export async function compileWithArduinoCli(
} catch (error) {
const errorText = error instanceof Error ? error.message : String(error);
const errorMessage = withPathHint(`Failed to execute arduino-cli: ${errorText}.`, error);
logger.error(errorMessage);
const errorType = error instanceof Error ? error.name : typeof error;
const errorCode = error && typeof error === "object" && "code" in error && typeof error.code === "string"
? `, ${error.code}`
: "";
logger.error(`Failed to execute arduino-cli (${errorType}${errorCode}, ${Buffer.byteLength(errorMessage)} diagnostic bytes)`);
return {
success: false,
output: "",
Expand Down
5 changes: 2 additions & 3 deletions server/services/local-compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class LocalCompiler {
break;
}
if (attempt < 2) {
this.logger.warn(`Compilation attempt ${attempt} failed, retrying... (${lastError.message})`);
this.logger.warn(`Compilation attempt ${attempt} failed (${lastError.name}); retrying`);
await new Promise<void>(r => setTimeout(r, 500));
}
}
Expand Down Expand Up @@ -198,8 +198,7 @@ export class LocalCompiler {

if (result.error || result.code !== 0) {
const cleanedError = this.cleanCompilerErrors(result.stderr || "");
const errorMsg = `Compiler error (Code ${result.code}, attempt ${attempt}): ${cleanedError}`;
this.logger.error(errorMsg);
this.logger.error(`Compiler failed (code ${result.code}, attempt ${attempt}, ${Buffer.byteLength(cleanedError)} diagnostic bytes)`);
throw new CompilerError(cleanedError);
}

Expand Down
2 changes: 1 addition & 1 deletion server/services/process-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class ProcessController implements IProcessController {
if (process.env.NODE_ENV === "test") {
// convert low-level wrapper events into buffered debug logs
try {
logger.debug(`wrapper stderr handler invoked with: ${d.toString()}`);
logger.debug(`wrapper stderr handler received ${d.length} bytes`);
} catch {}
}
this.stderrListeners.forEach((cb) => cb(d));
Expand Down
7 changes: 4 additions & 3 deletions server/services/process-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ export class ProcessExecutor {
result.error = new Error(`Process timeout after ${timeout}ms`);
this.logger.warn(`${command} timed out: ${result.error.message}`);
} else if (code !== 0) {
result.error = new Error(`${command} exit code ${code}: ${stderr}`);
this.logger.warn(`${command} failed: ${result.error.message}`);
result.error = new Error(`${command} exit code ${code}`);
this.logger.warn(`${command} failed with exit code ${code} (${Buffer.byteLength(stderr)} stderr bytes)`);
}

resolve(result);
Expand All @@ -223,7 +223,8 @@ export class ProcessExecutor {
this.activeTimeout = null;
}
this.activeProcess = null;
this.logger.error(`${command} error: ${err.message}`);
const errorCode = "code" in err && typeof err.code === "string" ? `, ${err.code}` : "";
this.logger.error(`${command} process error (${err.name}${errorCode})`);
resolve({
code: -1,
error: err,
Expand Down
19 changes: 19 additions & 0 deletions server/services/safe-error-log-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function safeErrorLogMetadata(error: unknown): string {
const errorType = error instanceof Error ? error.name : typeof error;
let messageBytes = 0;
if (error instanceof Error) {
messageBytes = Buffer.byteLength(error.message);
} else if (typeof error === "string") {
messageBytes = Buffer.byteLength(error);
}
const errorCode = error instanceof Error && "code" in error && typeof error.code === "string"
? " code=" + error.code
: "";

return errorType + errorCode + ", " + messageBytes + " diagnostic bytes";
}

export function safeErrorStackFrames(error: unknown): string {
if (!(error instanceof Error) || !error.stack) return "";
return error.stack.split("\n").slice(1, 6).join("\n");
}
3 changes: 2 additions & 1 deletion server/services/sandbox/execution-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ export class ExecutionManager {
return;
}
const errorMessage = err instanceof Error ? err.message : String(err);
this.logger.error(`Kompilierfehler oder Timeout: ${errorMessage}`);
const errorType = err instanceof Error ? err.name : typeof err;
this.logger.error(`Kompilierfehler oder Timeout (${errorType}, ${Buffer.byteLength(errorMessage)} diagnostic bytes)`);
options.onCompileError?.(errorMessage);
options.onExit?.(-1);
state.processController.destroySockets();
Expand Down
2 changes: 1 addition & 1 deletion server/services/sandbox/stream-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class StreamHandler {

case "text":
if (callbacks.onError) {
this.logger.warn(`[STDERR]: ${parsed.line}`);
this.logger.warn(`[STDERR] diagnostic received (${Buffer.byteLength(parsed.line)} bytes)`);
callbacks.onError(parsed.line);
}
break;
Expand Down
4 changes: 2 additions & 2 deletions server/services/workers/compile-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import { parentPort, workerData } from "node:worker_threads";
import { Logger } from "../../../shared/logger.ts";
import { safeErrorLogMetadata } from "../safe-error-log-metadata.ts";
import { getFastTmpBaseDir } from "../../../shared/utils/temp-paths.ts";
import {
type CompileRequestPayload,
Expand Down Expand Up @@ -271,8 +272,7 @@ async function processCompileRequest(task: CompileRequestPayload) {
}

} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
logger.error(`[Worker] Compilation failed: ${errorMsg}`);
logger.error(`[Worker] Compilation failed (${safeErrorLogMetadata(err)})`);
throw err;
}
}
Expand Down
7 changes: 3 additions & 4 deletions shared/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,12 @@ export function initializeGlobalErrorHandlers(): void {

process.on("uncaughtException", (error: Error) => {
// note: processError variable removed – we no longer track it separately
flushDebugOnFailure(`Uncaught Exception: ${error.message}`);
flushDebugOnFailure(`Uncaught Exception (${error.name})`);
});

process.on("unhandledRejection", (reason: unknown) => {
flushDebugOnFailure(`Unhandled Rejection: ${String(reason)}`);
const reasonType = reason instanceof Error ? reason.name : typeof reason;
flushDebugOnFailure(`Unhandled Rejection (${reasonType})`);
});
}

Expand All @@ -256,5 +257,3 @@ export function markTestAsFailed(testName?: string): void {
export function setLogLevel(level: LogLevel): void {
globalLogLevel = level;
}


27 changes: 27 additions & 0 deletions tests/server/dev-entrypoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,4 +191,31 @@ describe("local development entrypoint", () => {
expect(output()).toContain("[Shutdown] Received SIGTERM");
expect(child.exitCode).toBe(0);
}, 20_000);

it("does not log sketch source included in a REST response", async () => {
const port = await reservePort();
const child = spawnLocalDevelopment(createLocalServerEnv(port));
const output = collectOutput(child);
const marker = "F01_REST_SOURCE_SENTINEL_2c0f6a";
const markerPrefix = "F01_REST_SOURCE";
try {
await waitForReadiness(child, port, output);
const response = await fetch(`http://127.0.0.1:${port}/api/sketches`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "", content: marker }),
});

expect(response.status).toBe(201);
expect(await response.json()).toMatchObject({ content: marker });
const deadline = Date.now() + 1_000;
while (!output().includes("POST /api/sketches 201") && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
expect(output()).toContain("POST /api/sketches 201");
expect(output()).not.toContain(markerPrefix);
} finally {
await stopProcess(child);
}
}, 20_000);
});
4 changes: 3 additions & 1 deletion tests/server/routes/compiler.routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,10 +341,12 @@ describe("compiler.routes - /api/compile", () => {
});

it("handles compiler exceptions", async () => {
deps.compiler.compile.mockRejectedValueOnce(new Error("Compiler crashed"));
const diagnostic = "F01_COMPILER_DIAGNOSTIC_SENTINEL";
deps.compiler.compile.mockRejectedValueOnce(new Error(diagnostic));
const res = await post(baseUrl, "/api/compile", { code: "crash_code" });
expect(res.status).toBe(500);
expect(res.body.error).toBe("Compilation failed");
expect(deps.logger.error.mock.calls.flat().join(" ")).not.toContain(diagnostic);
});

it("passes headers in compilation request", async () => {
Expand Down
7 changes: 5 additions & 2 deletions tests/server/services/local-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,19 @@ describe("LocalCompiler public compile behavior", () => {

it("normalizes compiler stderr and rejects failed compilation", async () => {
const workspace = await createSketchWorkspace();
const diagnostic = "F01_COMPILER_DIAGNOSTIC_SENTINEL";
vi.spyOn(ProcessExecutor.prototype, "execute").mockResolvedValue({
code: 1,
stdout: "",
stderr: "/tmp/temp/abc123/sketch.cpp:4: error: invalid syntax",
stderr: `/tmp/temp/abc123/sketch.cpp:4: error: ${diagnostic}`,
error: new Error("g++ failed"),
});
const errorLog = vi.spyOn(Logger.prototype, "error");

await expect(
new LocalCompiler().compile(workspace.sketchFile, workspace.executableFile),
).rejects.toThrow("sketch.ino:4: error: invalid syntax");
).rejects.toThrow(`sketch.ino:4: error: ${diagnostic}`);
expect(errorLog.mock.calls.flat().join(" ")).not.toContain(diagnostic);
});

it("retries a transient compiler failure and succeeds on the second attempt", async () => {
Expand Down
23 changes: 22 additions & 1 deletion tests/server/services/process-controller.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { ProcessController } from "../../../server/services/process-controller";
import { Logger } from "../../../shared/logger";

describe("ProcessController — unit", () => {
it("forwards stderr without logging its diagnostic contents", async () => {
const pc = new ProcessController();
const diagnostic = "F01_COMPILER_DIAGNOSTIC_SENTINEL";
let stderr = "";
const debug = vi.spyOn(Logger.prototype, "debug");
pc.onStderr((chunk) => { stderr += chunk.toString(); });
const closed = new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("timed out waiting for stderr close")), 2_000);
pc.onClose(() => {
clearTimeout(timeout);
resolve();
});
});
await pc.spawn("node", ["-e", "process.stderr.write('F01_COMPILER_' + 'DIAGNOSTIC_SENTINEL')"]);
await closed;

expect(stderr).toBe(diagnostic);
expect(debug.mock.calls.flat().join(" ")).not.toContain(diagnostic);
});

it("forwards stdout data to registered listeners (pre/post-spawn)", async () => {
const pc = new ProcessController();

Expand Down
Loading
Loading