Skip to content

Commit e2fc74e

Browse files
committed
feat: add backend logs window to electron
1 parent ef5bd72 commit e2fc74e

7 files changed

Lines changed: 100 additions & 11 deletions

File tree

electron-app/main.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@
44
* Handles application lifecycle, initialization, and cleanup of processes and windows.
55
*/
66

7-
import { app, BrowserWindow, dialog } from "electron";
7+
import { app, BrowserWindow, dialog, screen } from "electron";
88
import pkg from "electron-updater";
99
import { getConfigManager } from "./src/config/configInstance.js";
1010
import { setupIpcHandlers } from "./src/ipc/handlers.js";
1111
import { startBackend, stopBackend } from "./src/processes/backend.js";
1212
import { stopPacketSender } from "./src/processes/packetSender.js";
1313
import { logger } from "./src/utils/logger.js";
14+
import { createLogWindow } from "./src/windows/logWindow.js";
1415
import { createWindow } from "./src/windows/mainWindow.js";
1516

1617
const { autoUpdater } = pkg;
@@ -38,23 +39,32 @@ app.setName("hyperloop-control-station");
3839

3940
// App lifecycle: wait for Electron to be ready
4041
app.whenReady().then(async () => {
42+
// Get the screen width and height
43+
// Only can be used inside app.whenReady()
44+
const { width: screenWidth, height: screenHeight } =
45+
screen.getPrimaryDisplay().workAreaSize;
46+
4147
// Initialize ConfigManager and ensure config exists BEFORE starting backend
4248
logger.electron.header("Initializing configuration...");
4349
// Get ConfigManager instance (creates config from template if needed)
4450
await getConfigManager();
4551
logger.electron.header("Configuration ready");
4652

53+
const logWindow = createLogWindow(screenWidth, screenHeight);
54+
4755
// Start backend process
4856
try {
49-
await startBackend();
57+
await startBackend(logWindow);
5058
logger.electron.header("Backend process spawned");
5159
} catch (error) {
5260
// Start backend already shows these errors
5361
return;
5462
}
5563

5664
// Create main application window
57-
createWindow();
65+
const mainWindow = createWindow(screenWidth, screenHeight);
66+
mainWindow.maximize();
67+
5868
logger.electron.header("Main application window created");
5969

6070
// Updater setup

electron-app/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
},
3939
"dependencies": {
4040
"@iarna/toml": "^2.2.5",
41+
"ansi-to-html": "^0.7.2",
4142
"electron-store": "^11.0.2",
4243
"electron-updater": "^6.7.3",
4344
"picocolors": "^1.1.1"

electron-app/preload.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
3636
importConfig: () => ipcRenderer.invoke("import-config"),
3737
// Open folder selection dialog
3838
selectFolder: () => ipcRenderer.invoke("select-folder"),
39+
// Receive log message from backend
40+
onLog: (callback) =>
41+
ipcRenderer.on("log", (_event, value) => callback(value)),
3942
});

electron-app/src/processes/backend.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Handles starting, stopping, and restarting the backend process with proper error handling and logging.
55
*/
66

7+
import AnsiToHtml from "ansi-to-html";
78
import { spawn } from "child_process";
89
import { app, dialog } from "electron";
910
import fs from "fs";
@@ -15,6 +16,9 @@ import {
1516
getUserConfigPath,
1617
} from "../utils/paths.js";
1718

19+
// Create ANSI to HTML converter
20+
const convert = new AnsiToHtml();
21+
1822
// Get the application root path
1923
const appPath = getAppPath();
2024

@@ -30,7 +34,7 @@ let lastBackendError = null;
3034
* @example
3135
* startBackend();
3236
*/
33-
function startBackend() {
37+
function startBackend(logWindow = null) {
3438
return new Promise((resolve, reject) => {
3539
// Get paths for binary and config
3640
const backendBin = getBinaryPath("backend");
@@ -63,6 +67,12 @@ function startBackend() {
6367
// Log stdout output from backend
6468
backendProcess.stdout.on("data", (data) => {
6569
logger.backend.info(`${data.toString().trim()}`);
70+
71+
// Send log message to log window
72+
if (logWindow) {
73+
const htmlData = convert.toHtml(data.toString().trim());
74+
logWindow.webContents.send("log", htmlData);
75+
}
6676
});
6777

6878
// Capture stderr output (where Go errors/panics are written)
@@ -71,6 +81,12 @@ function startBackend() {
7181
logger.backend.error(errorMsg);
7282
// Store the last error message
7383
lastBackendError = errorMsg;
84+
85+
// Send error message to log window
86+
if (logWindow) {
87+
const htmlError = convert.toHtml(errorMsg);
88+
logWindow.webContents.send("log", htmlError);
89+
}
7490
});
7591

7692
// Handle spawn errors
@@ -120,8 +136,20 @@ function stopBackend() {
120136
// Only stop if process exists and is still running
121137
if (backendProcess && !backendProcess.killed) {
122138
logger.backend.info("Stopping backend...");
123-
// Send termination signal
124-
backendProcess.kill("SIGTERM");
139+
140+
backendProcess.stdin.end();
141+
142+
const fallbackTimer = setTimeout(() => {
143+
if (backendProcess && !backendProcess.killed) {
144+
logger.backend.warning(
145+
"Backend did not exit gracefully, force killing..."
146+
);
147+
backendProcess.kill("SIGKILL");
148+
}
149+
}, 2000);
150+
151+
fallbackTimer.unref();
152+
125153
// Clear the process reference
126154
backendProcess = null;
127155
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { BrowserWindow } from "electron";
2+
import path from "path";
3+
4+
import { getAppPath } from "../utils/paths.js";
5+
6+
// Get the application root path
7+
const appPath = getAppPath();
8+
9+
export const createLogWindow = (screenWidth, screenHeight) => {
10+
const logWindow = new BrowserWindow({
11+
x: Math.floor(screenWidth * 0.65),
12+
y: 0,
13+
width: Math.floor(screenWidth * 0.35),
14+
height: screenHeight,
15+
title: "Backend Logs",
16+
webPreferences: {
17+
preload: path.join(appPath, "preload.js"),
18+
contextIsolation: true,
19+
nodeIntegration: false,
20+
},
21+
});
22+
23+
logWindow.loadFile(path.join(appPath, "src/logs/logs.html"));
24+
25+
return logWindow;
26+
};

electron-app/src/windows/mainWindow.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ let currentView = "testing-view";
2424
* @example
2525
* createWindow();
2626
*/
27-
function createWindow() {
27+
function createWindow(screenWidth, screenHeight) {
2828
// Create new browser window with configuration
2929
mainWindow = new BrowserWindow({
30-
width: 1920,
31-
height: 1080,
32-
minWidth: 1280,
33-
minHeight: 720,
30+
x: 0,
31+
y: 0,
32+
width: screenWidth,
33+
height: screenHeight,
34+
minWidth: 800,
35+
minHeight: 600,
3436
webPreferences: {
3537
// Path to preload script for secure IPC
3638
preload: path.join(appPath, "preload.js"),
@@ -60,6 +62,8 @@ function createWindow() {
6062
mainWindow.on("closed", () => {
6163
mainWindow = null;
6264
});
65+
66+
return mainWindow;
6367
}
6468

6569
/**

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)