Skip to content

Commit 3b047a4

Browse files
committed
feat: electron support initial scaffolding for linux
1 parent 39d42d1 commit 3b047a4

4 files changed

Lines changed: 1042 additions & 0 deletions

File tree

src-electron/main.js

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
const { app, BrowserWindow, ipcMain } = require('electron');
2+
const { spawn } = require('child_process');
3+
const path = require('path');
4+
const readline = require('readline');
5+
6+
let mainWindow;
7+
let nodeProcess;
8+
let nodeWSPort = null;
9+
let isNodeTerminated = false;
10+
11+
// Promise that resolves when node server port is available (similar to Tauri's serverPortPromise)
12+
let nodePortResolve;
13+
const nodePortPromise = new Promise((resolve) => { nodePortResolve = resolve; });
14+
15+
const NODE_COMMANDS = {
16+
TERMINATE: "terminate",
17+
PING: "ping",
18+
GET_PORT: "getPort",
19+
HEART_BEAT: "heartBeat"
20+
};
21+
22+
let commandId = 0;
23+
const pendingCommands = {};
24+
25+
function execNode(commandCode) {
26+
return new Promise((resolve, reject) => {
27+
if (!nodeProcess || isNodeTerminated) {
28+
reject(new Error('Node process not running'));
29+
return;
30+
}
31+
const newCommandID = commandId++;
32+
const cmd = JSON.stringify({ commandCode, commandId: newCommandID }) + "\n";
33+
nodeProcess.stdin.write(cmd);
34+
pendingCommands[newCommandID] = { resolve, reject };
35+
});
36+
}
37+
38+
function startNodeServer() {
39+
return new Promise((resolve, reject) => {
40+
const nodeSrcPath = path.join(__dirname, '..', 'src-tauri', 'node-src', 'index.js');
41+
42+
console.log('Starting Node server from:', nodeSrcPath);
43+
44+
nodeProcess = spawn('node', [nodeSrcPath], {
45+
stdio: ['pipe', 'pipe', 'pipe']
46+
});
47+
48+
const rl = readline.createInterface({
49+
input: nodeProcess.stdout,
50+
crlfDelay: Infinity
51+
});
52+
53+
rl.on('line', (line) => {
54+
if (line && line.trim().startsWith("{")) {
55+
try {
56+
const jsonMsg = JSON.parse(line);
57+
if (pendingCommands[jsonMsg.commandId]) {
58+
pendingCommands[jsonMsg.commandId].resolve(jsonMsg.message);
59+
delete pendingCommands[jsonMsg.commandId];
60+
}
61+
} catch (e) {
62+
console.log('Node:', line);
63+
}
64+
} else if (line) {
65+
console.log('Node:', line);
66+
}
67+
});
68+
69+
nodeProcess.stderr.on('data', (data) => {
70+
console.error('Node Error:', data.toString());
71+
});
72+
73+
nodeProcess.on('close', (code, signal) => {
74+
isNodeTerminated = true;
75+
console.log(`Node process exited with code ${code} and signal ${signal}`);
76+
});
77+
78+
nodeProcess.on('error', (err) => {
79+
console.error('Failed to start Node process:', err);
80+
reject(err);
81+
});
82+
83+
// Node-src's GET_PORT command waits for serverPortPromise internally,
84+
// so no timeout needed - it will respond once the server is ready
85+
execNode(NODE_COMMANDS.GET_PORT)
86+
.then((result) => {
87+
nodeWSPort = result.port;
88+
nodePortResolve(nodeWSPort);
89+
console.log('Node WebSocket server running on port:', nodeWSPort);
90+
resolve(nodeWSPort);
91+
})
92+
.catch((err) => {
93+
reject(err);
94+
});
95+
});
96+
}
97+
98+
// Heartbeat to keep Node server alive
99+
let heartbeatInterval;
100+
function startHeartbeat() {
101+
heartbeatInterval = setInterval(() => {
102+
if (!isNodeTerminated) {
103+
execNode(NODE_COMMANDS.HEART_BEAT).catch(() => {});
104+
}
105+
}, 10000);
106+
}
107+
108+
function stopHeartbeat() {
109+
if (heartbeatInterval) {
110+
clearInterval(heartbeatInterval);
111+
heartbeatInterval = null;
112+
}
113+
}
114+
115+
async function createWindow() {
116+
mainWindow = new BrowserWindow({
117+
width: 1200,
118+
height: 800,
119+
webPreferences: {
120+
preload: path.join(__dirname, 'preload.js'),
121+
contextIsolation: true,
122+
nodeIntegration: false
123+
},
124+
icon: path.join(__dirname, '..', 'src-tauri', 'icons', 'icon.png')
125+
});
126+
127+
// Load the test page from the http-server
128+
mainWindow.loadURL('http://localhost:8081/test/index.html');
129+
130+
// Open DevTools for debugging
131+
mainWindow.webContents.openDevTools();
132+
133+
mainWindow.on('closed', () => {
134+
mainWindow = null;
135+
});
136+
}
137+
138+
// IPC handlers
139+
ipcMain.handle('get-node-ws-port', async () => {
140+
// Wait for node server to be ready before returning port
141+
return await nodePortPromise;
142+
});
143+
144+
ipcMain.handle('quit-app', (event, exitCode) => {
145+
console.log('Quit requested with exit code:', exitCode);
146+
gracefulShutdown(exitCode);
147+
});
148+
149+
ipcMain.on('console-log', (event, message) => {
150+
console.log('Renderer:', message);
151+
});
152+
153+
function waitForTrue(fn, timeout) {
154+
return new Promise((resolve) => {
155+
const startTime = Date.now();
156+
function check() {
157+
if (fn()) {
158+
resolve(true);
159+
} else if (Date.now() - startTime > timeout) {
160+
resolve(false);
161+
} else {
162+
setTimeout(check, 50);
163+
}
164+
}
165+
check();
166+
});
167+
}
168+
169+
async function gracefulShutdown(exitCode = 0) {
170+
console.log('Initiating graceful shutdown...');
171+
172+
stopHeartbeat();
173+
174+
if (!isNodeTerminated && nodeProcess) {
175+
// Send terminate command (don't await - node exits without responding)
176+
try {
177+
const cmd = JSON.stringify({ commandCode: NODE_COMMANDS.TERMINATE, commandId: -1 }) + "\n";
178+
nodeProcess.stdin.write(cmd);
179+
} catch (e) {
180+
// Process may already be terminated
181+
}
182+
183+
// Wait for node process to terminate (like Tauri does)
184+
await waitForTrue(() => isNodeTerminated, 1000);
185+
186+
if (!isNodeTerminated) {
187+
nodeProcess.kill();
188+
}
189+
}
190+
191+
app.exit(exitCode);
192+
}
193+
194+
app.whenReady().then(async () => {
195+
try {
196+
await startNodeServer();
197+
startHeartbeat();
198+
await createWindow();
199+
} catch (err) {
200+
console.error('Failed to start:', err);
201+
app.exit(1);
202+
}
203+
});
204+
205+
app.on('window-all-closed', () => {
206+
gracefulShutdown(0);
207+
});
208+
209+
app.on('activate', () => {
210+
if (BrowserWindow.getAllWindows().length === 0) {
211+
createWindow();
212+
}
213+
});
214+
215+
// Handle process termination signals
216+
process.on('SIGINT', () => gracefulShutdown(0));
217+
process.on('SIGTERM', () => gracefulShutdown(0));

0 commit comments

Comments
 (0)