@@ -12,9 +12,18 @@ import path from 'path';
1212import { app , BrowserWindow , shell , ipcMain } from 'electron' ;
1313import { autoUpdater } from 'electron-updater' ;
1414import log from 'electron-log' ;
15+ import { spawn , ChildProcess } from 'child_process' ;
16+ import fs from 'fs' ;
1517import MenuBuilder from './menu' ;
1618import { 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+
1827class AppUpdater {
1928 constructor ( ) {
2029 log . transports . file . level = 'info' ;
@@ -24,13 +33,231 @@ class AppUpdater {
2433}
2534
2635let 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
28209ipcMain . 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+
34261if ( process . env . NODE_ENV === 'production' ) {
35262 const sourceMapSupport = require ( 'source-map-support' ) ;
36263 sourceMapSupport . install ( ) ;
@@ -117,17 +344,26 @@ const createWindow = async () => {
117344 */
118345
119346app . 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+
127361app
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.
0 commit comments