const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); const Store = require('electron-store'); const store = new Store(); let mainWindow; let splashWindow; let backendProcess; // Paths const isDev = process.env.NODE_ENV === 'development'; const getResourcePath = (relativePath) => { if (isDev) { return path.join(__dirname, '..', relativePath); } return path.join(process.resourcesPath, relativePath); }; const getDataPath = () => { return path.join(app.getPath('userData'), 'data'); }; // Get backend executable name based on platform const getBackendExeName = () => { switch (process.platform) { case 'win32': return 'rfcp-server.exe'; case 'darwin': return 'rfcp-server'; case 'linux': return 'rfcp-server'; default: return 'rfcp-server'; } }; // Ensure data directories exist function ensureDataDirs() { const dirs = ['terrain', 'osm', 'projects', 'cache']; const dataPath = getDataPath(); dirs.forEach(dir => { const fullPath = path.join(dataPath, dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); } }); return dataPath; } // Create splash window function createSplashWindow() { splashWindow = new BrowserWindow({ width: 400, height: 300, frame: false, transparent: true, alwaysOnTop: true, resizable: false, webPreferences: { nodeIntegration: true, contextIsolation: false } }); splashWindow.loadFile(path.join(__dirname, 'splash.html')); splashWindow.center(); } // Start Python backend async function startBackend() { const dataPath = ensureDataDirs(); const env = { ...process.env, RFCP_DATA_PATH: dataPath, RFCP_DATABASE_URL: `sqlite:///${path.join(dataPath, 'rfcp.db')}`, RFCP_HOST: '127.0.0.1', RFCP_PORT: '8888', }; if (isDev) { // Development: run uvicorn const pythonCmd = process.platform === 'win32' ? 'python' : 'python3'; backendProcess = spawn(pythonCmd, [ '-m', 'uvicorn', 'app.main:app', '--host', '127.0.0.1', '--port', '8888', '--reload' ], { cwd: path.join(__dirname, '..', 'backend'), env, stdio: ['ignore', 'pipe', 'pipe'] }); } else { // Production: run PyInstaller bundle const backendExe = path.join( getResourcePath('backend'), getBackendExeName() ); // Make executable on Unix if (process.platform !== 'win32') { try { fs.chmodSync(backendExe, '755'); } catch (e) { console.log('chmod failed:', e); } } backendProcess = spawn(backendExe, [], { env, stdio: ['ignore', 'pipe', 'pipe'] }); } // Log backend output backendProcess.stdout?.on('data', (data) => { console.log(`[Backend] ${data}`); }); backendProcess.stderr?.on('data', (data) => { console.error(`[Backend Error] ${data}`); }); backendProcess.on('error', (err) => { console.error('Failed to start backend:', err); dialog.showErrorBox('Backend Error', `Failed to start backend: ${err.message}`); }); // Wait for backend to be ready return new Promise((resolve) => { const maxAttempts = 60; // 30 seconds let attempts = 0; const checkBackend = setInterval(async () => { attempts++; try { const response = await fetch('http://127.0.0.1:8888/api/health/'); if (response.ok) { clearInterval(checkBackend); console.log('Backend ready!'); resolve(true); } } catch (_e) { // Not ready yet } if (attempts >= maxAttempts) { clearInterval(checkBackend); console.error('Backend failed to start in time'); resolve(false); } }, 500); }); } // Create main window function createMainWindow() { const windowState = store.get('windowState', { width: 1400, height: 900 }); mainWindow = new BrowserWindow({ width: windowState.width, height: windowState.height, x: windowState.x, y: windowState.y, minWidth: 1024, minHeight: 768, show: false, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') }, icon: path.join(__dirname, 'assets', process.platform === 'win32' ? 'icon.ico' : 'icon.png' ), title: 'RFCP - RF Coverage Planner', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', }); // Save window state on close mainWindow.on('close', () => { const bounds = mainWindow.getBounds(); store.set('windowState', bounds); }); // Load frontend if (isDev) { mainWindow.loadURL('http://localhost:5173'); mainWindow.webContents.openDevTools(); } else { mainWindow.loadFile(path.join(getResourcePath('frontend'), 'index.html')); } mainWindow.once('ready-to-show', () => { if (splashWindow) { splashWindow.close(); splashWindow = null; } mainWindow.show(); }); mainWindow.on('closed', () => { mainWindow = null; }); // Handle external links mainWindow.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); } // App lifecycle app.whenReady().then(async () => { createSplashWindow(); const backendStarted = await startBackend(); if (!backendStarted) { const result = await dialog.showMessageBox({ type: 'error', title: 'Startup Error', message: 'Failed to start backend server.', detail: 'Please check logs and try again. Click "Show Logs" to open the log folder.', buttons: ['Quit', 'Show Logs'] }); if (result.response === 1) { shell.openPath(app.getPath('logs')); } app.quit(); return; } createMainWindow(); }); app.on('window-all-closed', () => { if (backendProcess) { backendProcess.kill(); backendProcess = null; } if (process.platform !== 'darwin') { app.quit(); } }); app.on('activate', () => { if (mainWindow === null) { createMainWindow(); } }); app.on('before-quit', () => { if (backendProcess) { backendProcess.kill(); } }); // IPC Handlers ipcMain.handle('get-data-path', () => getDataPath()); ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-platform', () => process.platform); ipcMain.handle('get-gpu-info', async () => { // TODO: Implement GPU detection return { available: false, name: null, cudaVersion: null }; }); ipcMain.handle('select-directory', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'] }); return result.filePaths[0] || null; }); ipcMain.handle('select-file', async (_event, options) => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: options?.filters || [] }); return result.filePaths[0] || null; }); ipcMain.handle('save-file', async (_event, options) => { const result = await dialog.showSaveDialog(mainWindow, { defaultPath: options?.defaultPath, filters: options?.filters || [] }); return result.filePath || null; }); ipcMain.handle('get-setting', (_event, key) => store.get(key)); ipcMain.handle('set-setting', (_event, key, value) => store.set(key, value)); ipcMain.handle('open-external', (_event, url) => shell.openExternal(url)); ipcMain.handle('open-path', (_event, filePath) => shell.openPath(filePath));