feat: add Screeps console monitoring with configurable error handling and shard support #84

Merged
Philipp merged 9 commits from feature/console-monitor into main 2026-05-16 19:44:39 +02:00
4 changed files with 66 additions and 6 deletions
Showing only changes of commit fe7c14540e - Show all commits
+22
View File
@@ -482,4 +482,26 @@ describe("monitorConsole", () => {
// We expect it to have called api.time fewer than 10 times (excluding the startTick call) // We expect it to have called api.time fewer than 10 times (excluding the startTick call)
expect(api.time.mock.calls.length).toBeLessThan(10); expect(api.time.mock.calls.length).toBeLessThan(10);
}); });
it("detects encoded tracebacks in log lines", async () => {
const api = buildMockApi({ ticks: [100, 101, 102] });
setTimeout(() => {
api._fireConsole({
messages: {
log: [
"Error: ReferenceError: a is not defined%0A at eval (eval at <anonymous> (_console1778948572008_0:1:46), <anonymous>:1:1)%0A at _console1778948572008_0:1:46",
],
results: [],
},
error: null,
});
}, 50);
const result = await monitorConsole(api, {
...BASE_OPTS,
onTraceback: "fail",
});
expect(result.sawTraceback).toBe(true);
expect(result.sawErrorLog).toBe(true);
});
}); });
+1 -1
View File
File diff suppressed because one or more lines are too long
+31 -5
View File
@@ -51,7 +51,8 @@ export function buildSubscribePath(hostname, shard) {
*/ */
export function detectTraceback(errorText) { export function detectTraceback(errorText) {
if (!errorText) return false; if (!errorText) return false;
return /^\s{4}at /m.test(errorText); const text = safeDecode(errorText);
return /^\s{4}at /m.test(text);
} }
/** /**
@@ -67,6 +68,22 @@ export function detectWarning(logLines) {
return logLines.some((line) => warnPattern.test(line)); return logLines.some((line) => warnPattern.test(line));
} }
/**
* Safely decodes a URI-encoded string from the Screeps console.
* Returns the original string if decoding fails or if no '%' is present.
*
* @param {string} text
* @returns {string}
*/
export function safeDecode(text) {
if (typeof text !== "string" || !text.includes("%")) return text;
try {
return decodeURIComponent(text);
} catch (err) {
return text;
}
}
// ──────────────────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────────────────
// Output helpers // Output helpers
// ──────────────────────────────────────────────────────────────────────────── // ────────────────────────────────────────────────────────────────────────────
@@ -166,11 +183,19 @@ export function handleConsoleEvent(event, opts, stdoutBuffer, state) {
if (detectWarning(logLines)) { if (detectWarning(logLines)) {
state.sawWarningLog = true; state.sawWarningLog = true;
const warnPattern = /<font\s+color=['"](?:orange|yellow)['"]/i; const warnPattern = /<font\s+color=['"](?:orange|yellow)['"]/i;
logLines.filter((l) => warnPattern.test(l)).forEach((l) => core.warning(l)); logLines
.filter((l) => warnPattern.test(l))
.forEach((l) => core.warning(safeDecode(l)));
}
// ── Traceback detection in log lines (Screeps sometimes sends errors here) ─
if (logLines.some((l) => detectTraceback(l))) {
state.sawTraceback = true;
state.sawErrorLog = true;
} }
// ── Stdout lines ────────────────────────────────────────────────────────── // ── Stdout lines ──────────────────────────────────────────────────────────
const allStdout = [...logLines, ...results]; const allStdout = [...logLines, ...results].map(safeDecode);
if (allStdout.length > 0) { if (allStdout.length > 0) {
if (logToFile) { if (logToFile) {
stdoutBuffer.push(...allStdout); stdoutBuffer.push(...allStdout);
@@ -182,8 +207,9 @@ export function handleConsoleEvent(event, opts, stdoutBuffer, state) {
// ── Error field (always live) ───────────────────────────────────────────── // ── Error field (always live) ─────────────────────────────────────────────
if (errorText) { if (errorText) {
state.sawErrorLog = true; state.sawErrorLog = true;
core.error(errorText); const decodedError = safeDecode(errorText);
if (detectTraceback(errorText)) { core.error(decodedError);
if (detectTraceback(decodedError)) {
state.sawTraceback = true; state.sawTraceback = true;
} }
} }
+12
View File
@@ -0,0 +1,12 @@
function detectTraceback(errorText) {
if (!errorText) return false;
return /^\s{4}at /m.test(errorText);
}
const encodedTraceback =
"Error: ReferenceError: a is not defined%0A at eval (eval at <anonymous> (_console1778948572008_0:1:46), <anonymous>:1:1)%0A at _console1778948572008_0:1:46%0A at _console1778948572008_0:1:60%0A at exports.evalCode (<runtime>:15347:63)%0A at exports.run (<runtime>:20876:41)%0A";
console.log("Encoded match:", detectTraceback(encodedTraceback));
const decodedTraceback = decodeURIComponent(encodedTraceback);
console.log("Decoded match:", detectTraceback(decodedTraceback));