@@ -5,115 +5,12 @@ const readline = require('readline');
55const fs = require ( 'fs' ) ;
66const fsp = require ( 'fs/promises' ) ;
77const os = require ( 'os' ) ;
8+ const { identifier : APP_IDENTIFIER } = require ( './package.json' ) ;
89
910let mainWindow ;
10- let nodeProcess ;
11- let nodeWSPort = null ;
12- let isNodeTerminated = false ;
13-
14- // Promise that resolves when node server port is available (similar to Tauri's serverPortPromise)
15- let nodePortResolve ;
16- const nodePortPromise = new Promise ( ( resolve ) => { nodePortResolve = resolve ; } ) ;
17-
18- const NODE_COMMANDS = {
19- TERMINATE : "terminate" ,
20- PING : "ping" ,
21- GET_PORT : "getPort" ,
22- HEART_BEAT : "heartBeat"
23- } ;
24-
25- let commandId = 0 ;
26- const pendingCommands = { } ;
27-
28- function execNode ( commandCode ) {
29- return new Promise ( ( resolve , reject ) => {
30- if ( ! nodeProcess || isNodeTerminated ) {
31- reject ( new Error ( 'Node process not running' ) ) ;
32- return ;
33- }
34- const newCommandID = commandId ++ ;
35- const cmd = JSON . stringify ( { commandCode, commandId : newCommandID } ) + "\n" ;
36- nodeProcess . stdin . write ( cmd ) ;
37- pendingCommands [ newCommandID ] = { resolve, reject } ;
38- } ) ;
39- }
40-
41- function startNodeServer ( ) {
42- return new Promise ( ( resolve , reject ) => {
43- const nodeSrcPath = path . join ( __dirname , '..' , 'src-tauri' , 'node-src' , 'index.js' ) ;
44-
45- console . log ( 'Starting Node server from:' , nodeSrcPath ) ;
46-
47- nodeProcess = spawn ( 'node' , [ nodeSrcPath ] , {
48- stdio : [ 'pipe' , 'pipe' , 'pipe' ]
49- } ) ;
50-
51- const rl = readline . createInterface ( {
52- input : nodeProcess . stdout ,
53- crlfDelay : Infinity
54- } ) ;
55-
56- rl . on ( 'line' , ( line ) => {
57- if ( line && line . trim ( ) . startsWith ( "{" ) ) {
58- try {
59- const jsonMsg = JSON . parse ( line ) ;
60- if ( pendingCommands [ jsonMsg . commandId ] ) {
61- pendingCommands [ jsonMsg . commandId ] . resolve ( jsonMsg . message ) ;
62- delete pendingCommands [ jsonMsg . commandId ] ;
63- }
64- } catch ( e ) {
65- console . log ( 'Node:' , line ) ;
66- }
67- } else if ( line ) {
68- console . log ( 'Node:' , line ) ;
69- }
70- } ) ;
71-
72- nodeProcess . stderr . on ( 'data' , ( data ) => {
73- console . error ( 'Node Error:' , data . toString ( ) ) ;
74- } ) ;
75-
76- nodeProcess . on ( 'close' , ( code , signal ) => {
77- isNodeTerminated = true ;
78- console . log ( `Node process exited with code ${ code } and signal ${ signal } ` ) ;
79- } ) ;
80-
81- nodeProcess . on ( 'error' , ( err ) => {
82- console . error ( 'Failed to start Node process:' , err ) ;
83- reject ( err ) ;
84- } ) ;
85-
86- // Node-src's GET_PORT command waits for serverPortPromise internally,
87- // so no timeout needed - it will respond once the server is ready
88- execNode ( NODE_COMMANDS . GET_PORT )
89- . then ( ( result ) => {
90- nodeWSPort = result . port ;
91- nodePortResolve ( nodeWSPort ) ;
92- console . log ( 'Node WebSocket server running on port:' , nodeWSPort ) ;
93- resolve ( nodeWSPort ) ;
94- } )
95- . catch ( ( err ) => {
96- reject ( err ) ;
97- } ) ;
98- } ) ;
99- }
100-
101- // Heartbeat to keep Node server alive
102- let heartbeatInterval ;
103- function startHeartbeat ( ) {
104- heartbeatInterval = setInterval ( ( ) => {
105- if ( ! isNodeTerminated ) {
106- execNode ( NODE_COMMANDS . HEART_BEAT ) . catch ( ( ) => { } ) ;
107- }
108- } , 10000 ) ;
109- }
110-
111- function stopHeartbeat ( ) {
112- if ( heartbeatInterval ) {
113- clearInterval ( heartbeatInterval ) ;
114- heartbeatInterval = null ;
115- }
116- }
11+ let processInstanceId = 0 ;
12+ // Map of instanceId -> { process, terminated }
13+ const spawnedProcesses = new Map ( ) ;
11714
11815async function createWindow ( ) {
11916 mainWindow = new BrowserWindow ( {
@@ -139,9 +36,59 @@ async function createWindow() {
13936}
14037
14138// IPC handlers
142- ipcMain . handle ( 'get-node-ws-port' , async ( ) => {
143- // Wait for node server to be ready before returning port
144- return await nodePortPromise ;
39+
40+ // Spawn a child process and forward stdio to the calling renderer.
41+ // Returns an instanceId so the renderer can target the correct process.
42+ ipcMain . handle ( 'spawn-process' , async ( event , command , args ) => {
43+ const instanceId = ++ processInstanceId ;
44+ const sender = event . sender ;
45+ console . log ( `Spawning: ${ command } ${ args . join ( ' ' ) } (instance ${ instanceId } )` ) ;
46+
47+ const childProcess = spawn ( command , args , {
48+ stdio : [ 'pipe' , 'pipe' , 'pipe' ]
49+ } ) ;
50+
51+ const instance = { process : childProcess , terminated : false } ;
52+ spawnedProcesses . set ( instanceId , instance ) ;
53+
54+ const rl = readline . createInterface ( {
55+ input : childProcess . stdout ,
56+ crlfDelay : Infinity
57+ } ) ;
58+
59+ rl . on ( 'line' , ( line ) => {
60+ if ( ! sender . isDestroyed ( ) ) {
61+ sender . send ( 'process-stdout' , instanceId , line ) ;
62+ }
63+ } ) ;
64+
65+ childProcess . stderr . on ( 'data' , ( data ) => {
66+ if ( ! sender . isDestroyed ( ) ) {
67+ sender . send ( 'process-stderr' , instanceId , data . toString ( ) ) ;
68+ }
69+ } ) ;
70+
71+ childProcess . on ( 'close' , ( code , signal ) => {
72+ instance . terminated = true ;
73+ console . log ( `Process (instance ${ instanceId } ) exited with code ${ code } and signal ${ signal } ` ) ;
74+ if ( ! sender . isDestroyed ( ) ) {
75+ sender . send ( 'process-close' , instanceId , { code, signal } ) ;
76+ }
77+ } ) ;
78+
79+ childProcess . on ( 'error' , ( err ) => {
80+ console . error ( `Failed to start process (instance ${ instanceId } ):` , err ) ;
81+ } ) ;
82+
83+ return instanceId ;
84+ } ) ;
85+
86+ // Write data to a specific spawned process stdin
87+ ipcMain . handle ( 'write-to-process' , ( event , instanceId , data ) => {
88+ const instance = spawnedProcesses . get ( instanceId ) ;
89+ if ( instance && ! instance . terminated ) {
90+ instance . process . stdin . write ( data ) ;
91+ }
14592} ) ;
14693
14794ipcMain . handle ( 'quit-app' , ( event , exitCode ) => {
@@ -153,17 +100,30 @@ ipcMain.on('console-log', (event, message) => {
153100 console . log ( 'Renderer:' , message ) ;
154101} ) ;
155102
103+ // App path (repo root when running from source)
104+ ipcMain . handle ( 'get-app-path' , ( ) => {
105+ return app . getAppPath ( ) ;
106+ } ) ;
107+
156108// Directory APIs
157109ipcMain . handle ( 'get-documents-dir' , ( ) => {
158110 return path . join ( os . homedir ( ) , 'Documents' ) ;
159111} ) ;
160112
161113ipcMain . handle ( 'get-app-data-dir' , ( ) => {
162- // Returns app-specific data directory similar to Tauri's appLocalDataDir
163- // Linux: ~/.local/share/<app-name>/
164- // macOS: ~/Library/Application Support/<app-name>/
165- // Windows: %APPDATA%/<app-name>/
166- return app . getPath ( 'userData' ) ;
114+ // Match Tauri's appLocalDataDir which uses the bundle identifier "fs.phcode"
115+ // Linux: ~/.local/share/fs.phcode/
116+ // macOS: ~/Library/Application Support/fs.phcode/
117+ // Windows: %LOCALAPPDATA%/fs.phcode/
118+ const home = os . homedir ( ) ;
119+ switch ( process . platform ) {
120+ case 'darwin' :
121+ return path . join ( home , 'Library' , 'Application Support' , APP_IDENTIFIER ) ;
122+ case 'win32' :
123+ return path . join ( process . env . LOCALAPPDATA || path . join ( home , 'AppData' , 'Local' ) , APP_IDENTIFIER ) ;
124+ default :
125+ return path . join ( process . env . XDG_DATA_HOME || path . join ( home , '.local' , 'share' ) , APP_IDENTIFIER ) ;
126+ }
167127} ) ;
168128
169129// Dialogs
@@ -228,37 +188,23 @@ function waitForTrue(fn, timeout) {
228188async function gracefulShutdown ( exitCode = 0 ) {
229189 console . log ( 'Initiating graceful shutdown...' ) ;
230190
231- stopHeartbeat ( ) ;
232-
233- if ( ! isNodeTerminated && nodeProcess ) {
234- // Send terminate command (don't await - node exits without responding)
235- try {
236- const cmd = JSON . stringify ( { commandCode : NODE_COMMANDS . TERMINATE , commandId : - 1 } ) + "\n" ;
237- nodeProcess . stdin . write ( cmd ) ;
238- } catch ( e ) {
239- // Process may already be terminated
240- }
241-
242- // Wait for node process to terminate (like Tauri does)
243- await waitForTrue ( ( ) => isNodeTerminated , 1000 ) ;
191+ for ( const [ , instance ] of spawnedProcesses ) {
192+ if ( ! instance . terminated ) {
193+ try {
194+ instance . process . kill ( ) ;
195+ } catch ( e ) {
196+ // Process may already be terminated
197+ }
244198
245- if ( ! isNodeTerminated ) {
246- nodeProcess . kill ( ) ;
199+ await waitForTrue ( ( ) => instance . terminated , 1000 ) ;
247200 }
248201 }
249202
250203 app . exit ( exitCode ) ;
251204}
252205
253206app . whenReady ( ) . then ( async ( ) => {
254- try {
255- await startNodeServer ( ) ;
256- startHeartbeat ( ) ;
257- await createWindow ( ) ;
258- } catch ( err ) {
259- console . error ( 'Failed to start:' , err ) ;
260- app . exit ( 1 ) ;
261- }
207+ await createWindow ( ) ;
262208} ) ;
263209
264210app . on ( 'window-all-closed' , ( ) => {
0 commit comments