Skip to content

Commit f0eb252

Browse files
Added some basic IPC (#1)
Signed-off-by: Matías Insaurralde <[email protected]>
1 parent 56dd35e commit f0eb252

File tree

5 files changed

+386
-1
lines changed

5 files changed

+386
-1
lines changed

src/main/main.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,18 @@ import path from 'path';
1212
import { app, BrowserWindow, shell, ipcMain } from 'electron';
1313
import { autoUpdater } from 'electron-updater';
1414
import log from 'electron-log';
15+
import { spawn, ChildProcess } from 'child_process';
16+
import fs from 'fs';
1517
import MenuBuilder from './menu';
1618
import { resolveHtmlPath } from './util';
1719

20+
// === IPC SOCKET CONFIGURATION ===
21+
// The Electron app will create a Unix domain socket at this path for Go backend IPC.
22+
// Update your Go backend to listen on this socket:
23+
// os.Getenv("CROWDLLAMA_SOCKET")
24+
// Example: /tmp/crowdllama.sock
25+
const CROWDLLAMA_SOCKET_PATH = '/tmp/crowdllama.sock';
26+
1827
class AppUpdater {
1928
constructor() {
2029
log.transports.file.level = 'info';
@@ -24,13 +33,231 @@ class AppUpdater {
2433
}
2534

2635
let mainWindow: BrowserWindow | null = null;
36+
let goBackendProcess: ChildProcess | null = null;
37+
let pingInterval: ReturnType<typeof setInterval> | null = null;
38+
let ipcClient: any = null;
39+
let ipcClientBuffer = '';
40+
41+
// Connect to Go backend IPC socket (persistent connection)
42+
const connectIPC = () => {
43+
if (ipcClient) {
44+
return; // Already connected
45+
}
46+
const net = require('net');
47+
ipcClient = new net.Socket();
48+
ipcClient.setEncoding('utf8');
49+
50+
ipcClient.connect(CROWDLLAMA_SOCKET_PATH, () => {
51+
console.log('Connected to Go backend socket (persistent)');
52+
});
53+
54+
ipcClient.on('data', (data: string) => {
55+
ipcClientBuffer += data;
56+
let index;
57+
while ((index = ipcClientBuffer.indexOf('\n')) !== -1) {
58+
const message = ipcClientBuffer.slice(0, index);
59+
ipcClientBuffer = ipcClientBuffer.slice(index + 1);
60+
if (message.trim()) {
61+
try {
62+
const parsed = JSON.parse(message);
63+
console.log('Received from backend:', parsed);
64+
// TODO: handle parsed message (route to renderer, etc)
65+
} catch (err) {
66+
console.log('Failed to parse backend message:', message);
67+
}
68+
}
69+
}
70+
});
71+
72+
ipcClient.on('error', (err: any) => {
73+
console.log('IPC socket error:', err.message);
74+
// Optionally, try to reconnect or clean up
75+
ipcClient = null;
76+
});
77+
78+
ipcClient.on('close', () => {
79+
console.log('IPC socket closed');
80+
ipcClient = null;
81+
});
82+
};
83+
84+
// Send a message over the persistent IPC connection
85+
const sendIPCMessage = (msg: object) => {
86+
if (ipcClient && !ipcClient.destroyed) {
87+
ipcClient.write(JSON.stringify(msg) + '\n');
88+
} else {
89+
console.log('IPC client not connected, cannot send message');
90+
}
91+
};
2792

93+
// Send ping to Go backend via persistent socket
94+
const pingBackend = async () => {
95+
try {
96+
if (!ipcClient || ipcClient.destroyed) {
97+
console.log('IPC client not connected, reconnecting...');
98+
connectIPC();
99+
// Wait a moment for connection
100+
setTimeout(() => sendIPCMessage({ type: 'ping', timestamp: Date.now() }), 200);
101+
return;
102+
}
103+
sendIPCMessage({ type: 'ping', timestamp: Date.now() });
104+
} catch (error) {
105+
console.error('Error sending ping:', error);
106+
}
107+
};
108+
109+
// Start ping interval
110+
const startPingInterval = () => {
111+
// Clear any existing interval
112+
if (pingInterval) {
113+
clearInterval(pingInterval);
114+
}
115+
116+
// Start new ping interval (every 1 minute = 60000ms)
117+
pingInterval = setInterval(pingBackend, 60000);
118+
console.log('Started ping interval (every 1 minute)');
119+
120+
// Send initial ping immediately
121+
pingBackend();
122+
};
123+
124+
// Start Go backend process
125+
const startGoBackend = () => {
126+
try {
127+
const goBackendPath = '/Users/matias/go/src/github.com/crowdllama/crowdllama';
128+
const goBackendCommand = '/opt/homebrew/bin/go';
129+
const goBackendArgs = ['run', 'cmd/crowdllama/main.go', 'start'];
130+
131+
// Remove the socket file if it exists
132+
try {
133+
if (fs.existsSync(CROWDLLAMA_SOCKET_PATH)) {
134+
fs.unlinkSync(CROWDLLAMA_SOCKET_PATH);
135+
}
136+
} catch (err) {
137+
console.error('Error removing old socket file:', err);
138+
}
139+
140+
console.log('Starting Go backend process...');
141+
console.log(`Command: ${goBackendCommand} ${goBackendArgs.join(' ')}`);
142+
console.log(`Working directory: ${goBackendPath}`);
143+
console.log(`Socket path: ${CROWDLLAMA_SOCKET_PATH}`);
144+
145+
goBackendProcess = spawn(goBackendCommand, goBackendArgs, {
146+
cwd: goBackendPath,
147+
stdio: ['pipe', 'pipe', 'pipe'],
148+
detached: false,
149+
env: {
150+
...process.env,
151+
CROWDLLAMA_SOCKET: CROWDLLAMA_SOCKET_PATH,
152+
},
153+
});
154+
155+
goBackendProcess.stdout?.on('data', (data) => {
156+
console.log('Go backend stdout:', data.toString());
157+
});
158+
159+
goBackendProcess.stderr?.on('data', (data) => {
160+
console.log('Go backend stderr:', data.toString());
161+
});
162+
163+
goBackendProcess.on('error', (error) => {
164+
console.error('Failed to start Go backend process:', error);
165+
});
166+
167+
goBackendProcess.on('close', (code) => {
168+
console.log(`Go backend process exited with code ${code}`);
169+
goBackendProcess = null;
170+
// Clean up IPC client
171+
if (ipcClient) {
172+
ipcClient.destroy();
173+
ipcClient = null;
174+
}
175+
});
176+
177+
console.log('Go backend process started successfully');
178+
179+
// Start ping interval after a short delay to allow backend to initialize
180+
setTimeout(() => {
181+
connectIPC();
182+
startPingInterval();
183+
}, 2000);
184+
} catch (error) {
185+
console.error('Error starting Go backend process:', error);
186+
}
187+
};
188+
189+
// Stop Go backend process
190+
const stopGoBackend = () => {
191+
if (goBackendProcess) {
192+
console.log('Stopping Go backend process...');
193+
goBackendProcess.kill('SIGTERM');
194+
goBackendProcess = null;
195+
}
196+
197+
// Stop ping interval
198+
if (pingInterval) {
199+
clearInterval(pingInterval);
200+
pingInterval = null;
201+
}
202+
if (ipcClient) {
203+
ipcClient.destroy();
204+
ipcClient = null;
205+
}
206+
};
207+
208+
// IPC handlers
28209
ipcMain.on('ipc-example', async (event, arg) => {
29210
const msgTemplate = (pingPong: string) => `IPC test: ${pingPong}`;
30211
console.log(msgTemplate(arg));
31212
event.reply('ipc-example', msgTemplate('pong'));
32213
});
33214

215+
ipcMain.handle('start-backend', async () => {
216+
try {
217+
startGoBackend();
218+
return { success: true, message: 'Go backend started successfully' };
219+
} catch (error) {
220+
console.error('Error starting backend:', error);
221+
return {
222+
success: false,
223+
message: error instanceof Error ? error.message : 'Unknown error',
224+
};
225+
}
226+
});
227+
228+
ipcMain.handle('stop-backend', async () => {
229+
try {
230+
stopGoBackend();
231+
return { success: true, message: 'Go backend stopped successfully' };
232+
} catch (error) {
233+
console.error('Error stopping backend:', error);
234+
return {
235+
success: false,
236+
message: error instanceof Error ? error.message : 'Unknown error',
237+
};
238+
}
239+
});
240+
241+
ipcMain.handle('get-backend-status', async () => {
242+
return {
243+
isRunning: goBackendProcess !== null,
244+
pid: goBackendProcess && typeof goBackendProcess.pid === 'number' ? goBackendProcess.pid : null,
245+
};
246+
});
247+
248+
ipcMain.handle('ping-backend', async () => {
249+
try {
250+
await pingBackend();
251+
return { success: true, message: 'Ping sent successfully' };
252+
} catch (error) {
253+
console.error('Error pinging backend:', error);
254+
return {
255+
success: false,
256+
message: error instanceof Error ? error.message : 'Unknown error',
257+
};
258+
}
259+
});
260+
34261
if (process.env.NODE_ENV === 'production') {
35262
const sourceMapSupport = require('source-map-support');
36263
sourceMapSupport.install();
@@ -117,17 +344,26 @@ const createWindow = async () => {
117344
*/
118345

119346
app.on('window-all-closed', () => {
347+
// Stop Go backend process when app closes
348+
stopGoBackend();
120349
// Respect the OSX convention of having the application in memory even
121350
// after all windows have been closed
122351
if (process.platform !== 'darwin') {
123352
app.quit();
124353
}
125354
});
126355

356+
app.on('before-quit', () => {
357+
// Ensure Go backend is stopped before quitting
358+
stopGoBackend();
359+
});
360+
127361
app
128362
.whenReady()
129363
.then(() => {
130364
createWindow();
365+
// Start Go backend process when app is ready
366+
startGoBackend();
131367
app.on('activate', () => {
132368
// On macOS it's common to re-create a window in the app when the
133369
// dock icon is clicked and there are no other windows open.

src/main/preload.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
/* eslint no-unused-vars: off */
33
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
44

5-
export type Channels = 'ipc-example';
5+
export type Channels =
6+
| 'ipc-example'
7+
| 'start-backend'
8+
| 'stop-backend'
9+
| 'get-backend-status'
10+
| 'ping-backend';
611

712
const electronHandler = {
813
ipcRenderer: {
@@ -22,6 +27,20 @@ const electronHandler = {
2227
ipcRenderer.once(channel, (_event, ...args) => func(...args));
2328
},
2429
},
30+
backend: {
31+
async startBackend() {
32+
return ipcRenderer.invoke('start-backend');
33+
},
34+
async stopBackend() {
35+
return ipcRenderer.invoke('stop-backend');
36+
},
37+
async getBackendStatus() {
38+
return ipcRenderer.invoke('get-backend-status');
39+
},
40+
async pingBackend() {
41+
return ipcRenderer.invoke('ping-backend');
42+
},
43+
},
2544
};
2645

2746
contextBridge.exposeInMainWorld('electron', electronHandler);

0 commit comments

Comments
 (0)