Skip to content

Commit 3bbba26

Browse files
committed
refact: add backendError.js with hints
1 parent adc8515 commit 3bbba26

File tree

4 files changed

+99
-29
lines changed

4 files changed

+99
-29
lines changed

backend/pkg/logger/trace/trace.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ func InitTrace(traceLevel string) *os.File {
6969
// Human-friendly console writer that prints logs to stdout.
7070
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
7171

72-
// Console writer that prints warn/error/fatal logs to stderr so the
72+
// Console writer that prints fatal logs to stderr so the
7373
// parent process (Electron) can capture them via the stderr pipe.
7474
stderrConsoleWriter := &levelFilterWriter{
7575
w: zerolog.ConsoleWriter{Out: os.Stderr},
76-
minLevel: zerolog.WarnLevel,
76+
minLevel: zerolog.FatalLevel,
7777
}
7878

7979
// Try to create/open the file for writing logs. On failure, fall back to console only and exit.

electron-app/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ When running in development mode (unpackaged), the application creates temporary
3232

3333
- **Configuration and Logs**: Stored in `{UserConfigDir}/hyperloop-control-station/` (using Go's `os.UserConfigDir()`)
3434
- Config files and backups: `{UserConfigDir}/hyperloop-control-station/configs/`
35-
- Trace/log files: `{UserConfigDir}/hyperloop-control-station/trace-*.json`
35+
- Trace/log files: `{UserConfigDir}/hyperloop-control-station/configs/trace-*.json`
3636

3737
- **ADJ Module**: Stored in `{UserCacheDir}/hyperloop-control-station/adj/` (using Go's `os.UserCacheDir()`)
3838

electron-app/src/processes/backend.js

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getBinaryPath,
1616
getUserConfigPath,
1717
} from "../utils/paths.js";
18+
import { formatBackendError, getHint } from "./backendError.js";
1819

1920
// Create ANSI to HTML converter
2021
const convert = new AnsiToHtml();
@@ -31,27 +32,6 @@ let storedLogWindow = null;
3132
// Store error messages accumulated from the current process run
3233
let lastBackendError = null;
3334

34-
const ERROR_HINTS = [
35-
{
36-
pattern: /bind: The requested address is not valid/,
37-
message: "Network address unavailable",
38-
advice:
39-
"The configured IP address doesn't exist on this machine. Check your network adapter or ADJ.",
40-
},
41-
{
42-
pattern: /failed to start UDP server/,
43-
message: "UDP server failed to start",
44-
advice: "Another process may already be using this port.",
45-
},
46-
];
47-
48-
function getHint(errorText) {
49-
const match = ERROR_HINTS.find(({ pattern }) => pattern.test(errorText));
50-
return match
51-
? `${match.message}\n\n${match.advice}\n\n${errorText}`
52-
: errorText;
53-
}
54-
5535
/**
5636
* Starts the backend process by spawning the backend binary with the user configuration.
5737
* @returns {void}
@@ -94,9 +74,6 @@ async function startBackend(logWindow = null) {
9474
cwd: workingDir,
9575
});
9676

97-
console.log("[DEBUG] backendProcess.stderr:", backendProcess.stderr);
98-
console.log("[DEBUG] backendProcess.stdout:", backendProcess.stdout);
99-
10077
// Log stdout output from backend
10178
backendProcess.stdout.on("data", (data) => {
10279
const text = data.toString().trim();
@@ -125,7 +102,6 @@ async function startBackend(logWindow = null) {
125102
backendProcess.stderr.on("data", (data) => {
126103
const errorMsg = data.toString().trim();
127104
logger.backend.error(errorMsg);
128-
console.log("[DEBUG stderr chunk]", JSON.stringify(errorMsg));
129105
lastBackendError = errorMsg;
130106

131107
// Send error message to log window
@@ -155,7 +131,8 @@ async function startBackend(logWindow = null) {
155131

156132
if (lastBackendError) {
157133
const stripped = lastBackendError.replace(/\x1b\[[0-9;]*m/g, "");
158-
errorMessage += `\n\n${getHint(stripped)}`;
134+
const formatted = formatBackendError(stripped);
135+
errorMessage += `\n\n${getHint(stripped, formatted)}`;
159136
} else {
160137
errorMessage += "\n\n(No error output captured)";
161138
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @module processes
3+
* @description Error formatting and hint utilities for backend crash diagnostics.
4+
* Parses zerolog console output, strips ANSI codes, and maps known error patterns
5+
* to actionable user-facing messages shown in the crash dialog.
6+
*/
7+
8+
/**
9+
* List of known error patterns with human-readable messages and fix advice.
10+
* Each entry is matched against the raw stripped stderr output.
11+
*/
12+
const ERROR_HINTS = [
13+
{
14+
pattern: /bind: The requested address is not valid/,
15+
message: "Network address unavailable",
16+
advice:
17+
"The configured IP address doesn't exist on this machine. Check your network adapter or ADJ.",
18+
},
19+
{
20+
pattern: /failed to start UDP server/,
21+
message: "UDP server failed to start",
22+
advice: "Another process may already be using this port.",
23+
},
24+
{
25+
pattern: /jsonschema/,
26+
message: "ADJ Validator dependency missing",
27+
advice:
28+
"Install the required Python package by running: pip install jsonschema==4.25.0",
29+
},
30+
{
31+
pattern: /No Python interpreter found/,
32+
message: "Python not found",
33+
advice:
34+
"Install Python 3 and make sure it is accessible via 'python3', 'python', or 'py' in your PATH.",
35+
},
36+
{
37+
pattern: /ADJ Validator failed with error/,
38+
message: "ADJ validation failed",
39+
advice:
40+
"Your ADJ files contain schema errors. Check the ADJ validator log file in the logs folder for details.",
41+
},
42+
];
43+
44+
/**
45+
* Reformats a single stripped zerolog console line into a readable block.
46+
* Zerolog console format: "TIME LEVEL FILE > message key=value ..."
47+
* @param {string} line - A single log line with ANSI codes already stripped.
48+
* @returns {string} A formatted multi-line string with level, file, and key-value pairs on separate lines.
49+
* @example
50+
* formatLine("11:43AM FTL setup_transport.go:143 > failed to start UDP server error=\"some error\"");
51+
* // "[FTL] at setup_transport.go:143\n failed to start UDP server\n error: \"some error\""
52+
*/
53+
function formatLine(line) {
54+
const m = line.match(/^\S+\s+(\S+)\s+(\S+)\s+>\s+(.*)/);
55+
if (!m) return line;
56+
const [, level, file, rest] = m;
57+
const body = rest.replace(
58+
/\s+(\w+)=("(?:[^"\\]|\\.)*"|\S+)/g,
59+
"\n $1: $2",
60+
);
61+
return `[${level}] at ${file}\n ${body.trim()}`;
62+
}
63+
64+
/**
65+
* Formats a full multi-line stderr output by reformatting each zerolog line.
66+
* @param {string} text - Raw stderr text with ANSI codes already stripped.
67+
* @returns {string} Formatted text with each log line reformatted for readability.
68+
* @example
69+
* formatBackendError("11:43AM FTL file.go:10 > something failed error=\"bad\"");
70+
*/
71+
function formatBackendError(text) {
72+
return text.split("\n").filter(Boolean).map(formatLine).join("\n\n");
73+
}
74+
75+
/**
76+
* Returns a user-facing error message by matching the raw error against known patterns.
77+
* If a match is found, prepends a hint and advice to the formatted error.
78+
* Falls back to the formatted error text if no pattern matches.
79+
* @param {string} raw - Raw stripped stderr text used for pattern matching.
80+
* @param {string} formatted - Pre-formatted version of the error for display.
81+
* @returns {string} The final message to show in the crash dialog.
82+
* @example
83+
* getHint("failed to start UDP server ...", "[FTL] at ...");
84+
* // "UDP server failed to start\n\nAnother process may already be using this port.\n\n[FTL] at ..."
85+
*/
86+
function getHint(raw, formatted) {
87+
const match = ERROR_HINTS.find(({ pattern }) => pattern.test(raw));
88+
return match
89+
? `${match.message}\n\n${match.advice}\n\n${formatted}`
90+
: formatted;
91+
}
92+
93+
export { formatBackendError, getHint };

0 commit comments

Comments
 (0)