-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathbackend.js
More file actions
228 lines (193 loc) · 6.82 KB
/
backend.js
File metadata and controls
228 lines (193 loc) · 6.82 KB
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
/**
* @module processes
* @description Backend process management for spawning and controlling the backend binary.
* Handles starting, stopping, and restarting the backend process with proper error handling and logging.
*/
import AnsiToHtml from "ansi-to-html";
import { spawn } from "child_process";
import { app, dialog } from "electron";
import fs from "fs";
import path from "path";
import { logger } from "../utils/logger.js";
import {
getAppPath,
getBinaryPath,
getUserConfigPath,
} from "../utils/paths.js";
import { formatBackendError, getHint } from "./backendError.js";
// Create ANSI to HTML converter
const convert = new AnsiToHtml();
// Get the application root path
const appPath = getAppPath();
// Store the backend process instance
let backendProcess = null;
// Common log window instance for all backend processes
let storedLogWindow = null;
// Store error messages accumulated from the current process run
let lastBackendError = null;
/**
* Starts the backend process by spawning the backend binary with the user configuration.
* @returns {void}
* @example
* startBackend();
*/
async function startBackend(logWindow = null) {
if (logWindow) {
storedLogWindow = logWindow;
}
const currentLogWindow = logWindow || storedLogWindow;
return new Promise((resolve, reject) => {
// Get paths for binary and config
const backendBin = getBinaryPath("backend");
const configPath = getUserConfigPath();
// Check if binary exists before attempting to start
if (!fs.existsSync(backendBin)) {
logger.backend.error(`Backend binary not found: ${backendBin}`);
dialog.showErrorBox(
"Error",
`Backend binary not found at: ${backendBin}`,
);
return reject(new Error(`Backend binary not found: ${backendBin}`));
}
logger.backend.info(
`Starting backend: ${backendBin}, config: ${configPath}`,
);
// Set working directory to backend/cmd in development, or resources in production
const workingDir = !app.isPackaged
? path.join(appPath, "..", "backend", "cmd")
: path.dirname(configPath);
// Spawn the backend process with config argument
backendProcess = spawn(backendBin, ["--config", configPath], {
cwd: workingDir,
});
// Log stdout output from backend
backendProcess.stdout.on("data", (data) => {
const text = data.toString().trim();
logger.backend.info(text);
// Send log message to log window
if (
currentLogWindow &&
!currentLogWindow.isDestroyed() &&
currentLogWindow.webContents
) {
const htmlData = convert.toHtml(text);
currentLogWindow.webContents.send("log", htmlData);
}
// Resolve as soon as the HTTP server confirms it is listening.
// Matches: "INF ... > http server listening localAddr=..."
if (text.includes("http server listening")) {
logger.backend.info("Backend ready (HTTP server listening)");
clearTimeout(startupTimer);
resolve(backendProcess);
}
});
// Capture stderr output (where Go errors/panics are written)
backendProcess.stderr.on("data", (data) => {
const errorMsg = data.toString().trim();
logger.backend.error(errorMsg);
lastBackendError = errorMsg;
// Send error message to log window
if (currentLogWindow && !currentLogWindow.isDestroyed()) {
const htmlError = convert.toHtml(errorMsg);
currentLogWindow.webContents.send("log", htmlError);
}
});
// Handle spawn errors
backendProcess.on("error", (error) => {
logger.backend.error(`Failed to start backend: ${error.message}`);
dialog.showErrorBox(
"Backend Error",
`Failed to start backend: ${error.message}`,
);
return reject(new Error(`Failed to start backend: ${error.message}`));
});
// Handle process exit
backendProcess.on("close", (code) => {
logger.backend.info(`Backend process exited with code ${code}`);
clearTimeout(startupTimer);
if (code !== 0 && code !== null) {
let errorMessage = `Backend exited with code ${code}`;
if (lastBackendError) {
const stripped = lastBackendError.replace(/\x1b\[[0-9;]*m/g, "");
const formatted = formatBackendError(stripped);
errorMessage += `\n\n${getHint(stripped, formatted)}`;
} else {
errorMessage += "\n\n(No error output captured)";
}
dialog.showErrorBox("Backend Crashed", errorMessage);
lastBackendError = null;
backendProcess = null;
return reject(new Error(errorMessage));
}
backendProcess = null;
});
// Fallback: if the ready message never appears, resolve anyway after timeout
const startupTimer = setTimeout(() => {
logger.backend.warning(
"Backend ready signal not received - resolving after timeout",
);
resolve(backendProcess);
}, 5000);
});
}
/**
* Stops the backend process by sending a SIGTERM and std.in.end() signal.
* If the process does not exit gracefully after defined time, it will be force killed.
* @returns {void}
* @example
* stopBackend();
*/
async function stopBackend() {
return new Promise((resolve, reject) => {
const localBackendProcess = backendProcess;
// Only stop if process exists and is still running
if (localBackendProcess && !localBackendProcess.killed) {
logger.backend.info("Stopping backend...");
localBackendProcess.once("close", () => {
// Clear the process reference
if (localBackendProcess === backendProcess) {
backendProcess = null;
}
resolve();
});
localBackendProcess.kill("SIGTERM");
localBackendProcess.stdin.end();
const fallbackTimer = setTimeout(() => {
if (localBackendProcess && !localBackendProcess.killed) {
logger.backend.warning(
"Backend did not exit gracefully, force killing...",
);
localBackendProcess.kill("SIGKILL");
}
}, 2000);
fallbackTimer.unref();
} else {
logger.backend.warning("Backend process not found, skipping stop...");
resolve();
}
});
}
/**
* Restarts the backend process by stopping the current process and starting a new one.
* @returns {void}
* @example
* restartBackend();
*/
async function restartBackend() {
// Stop current process first
await stopBackend();
// Start a new process
try {
await startBackend();
logger.electron.info("Backend restarted successfully");
} catch (error) {
logger.electron.error("Failed to restart backend:", error);
throw error; // Let the IPC handler know it failed
}
}
function getBackendWorkingDir() {
return !app.isPackaged
? path.join(appPath, "..", "backend", "cmd")
: path.dirname(getUserConfigPath());
}
export { getBackendWorkingDir, restartBackend, startBackend, stopBackend };