const { app, BrowserWindow, screen, Menu } = require("electron"); const { spawn } = require("child_process"); const path = require("path"); const fs = require("fs"); const os = require("os"); const http = require("http"); // 简单的日志记录函数 function log(message) { const logPath = path.join(app.getPath("userData"), "smartedt.log"); const timestamp = new Date().toISOString(); const logMessage = `[${timestamp}] ${message}\n`; try { fs.appendFileSync(logPath, logMessage); } catch (e) { console.error("Failed to write log:", e); } } let backendProcess = null; let staticServer = null; let backendLogStream = null; function resolveRepoRoot() { return path.resolve(__dirname, "..", ".."); } function getPythonPath(repoRoot) { if (process.env.SMARTEDT_PYTHON) { return process.env.SMARTEDT_PYTHON; } // Check for venv in backend (Windows) const venvPythonWin = path.join(repoRoot, "backend", "venv", "Scripts", "python.exe"); if (fs.existsSync(venvPythonWin)) { return venvPythonWin; } // Check for venv in backend (Linux/Mac) const venvPythonUnix = path.join(repoRoot, "backend", "venv", "bin", "python"); if (fs.existsSync(venvPythonUnix)) { return venvPythonUnix; } // Fallback to system python return "python"; } function startBackend() { log("Starting backend process..."); if (backendLogStream) { try { backendLogStream.end(); } catch (_) {} backendLogStream = null; } const repoRoot = resolveRepoRoot(); let backendCmd; let args; let cwd; const safeTimestamp = new Date().toISOString().replace(/[:.]/g, "-"); const backendLogPath = path.join(app.getPath("userData"), `backend-${safeTimestamp}.log`); try { backendLogStream = fs.createWriteStream(backendLogPath, { flags: "a" }); backendLogStream.write(`[${new Date().toISOString()}] Backend log start\n`); log(`Backend log file: ${backendLogPath}`); } catch (e) { backendLogStream = null; log(`Failed to open backend log file: ${e.message}`); } if (app.isPackaged) { // In production, the backend is in resources/backend const backendDir = path.join(process.resourcesPath, "backend"); backendCmd = path.join(backendDir, "smartedt_backend.exe"); args = ["--host", "127.0.0.1", "--port", "5000"]; cwd = backendDir; log(`Production mode. Backend cmd: ${backendCmd}`); log(`Backend cwd: ${cwd}`); } else { // In development const backendMain = path.join(repoRoot, "backend", "main.py"); backendCmd = getPythonPath(repoRoot); args = [backendMain, "--host", "127.0.0.1", "--port", "5000"]; cwd = repoRoot; log(`Development mode. Backend cmd: ${backendCmd}`); } // 设置 PYTHONPATH 环境变量,确保能找到 backend 模块 (only for dev) const env = { ...process.env }; if (!app.isPackaged) { env.PYTHONPATH = repoRoot; } try { if (!fs.existsSync(backendCmd) && app.isPackaged) { log(`ERROR: Backend executable not found at ${backendCmd}`); } backendProcess = spawn(backendCmd, args, { cwd: cwd, env: env, stdio: "pipe", // Capture stdio windowsHide: true }); backendProcess.stdout.on("data", (data) => { const text = data.toString(); log(`[Backend stdout] ${text.trim()}`); if (backendLogStream) { backendLogStream.write(text.endsWith("\n") ? text : `${text}\n`); } }); backendProcess.stderr.on("data", (data) => { const text = data.toString(); log(`[Backend stderr] ${text.trim()}`); if (backendLogStream) { backendLogStream.write(text.endsWith("\n") ? text : `${text}\n`); } }); backendProcess.on("error", (err) => { log(`Backend failed to start: ${err.message}`); if (backendLogStream) { backendLogStream.write(`[${new Date().toISOString()}] Backend failed to start: ${err.message}\n`); } }); backendProcess.on("exit", (code) => { log(`Backend exited with code ${code}`); backendProcess = null; if (backendLogStream) { backendLogStream.write(`[${new Date().toISOString()}] Backend exited with code ${code}\n`); try { backendLogStream.end(); } catch (_) {} backendLogStream = null; } }); } catch (e) { log(`Exception starting backend: ${e.message}`); if (backendLogStream) { backendLogStream.write(`[${new Date().toISOString()}] Exception starting backend: ${e.message}\n`); try { backendLogStream.end(); } catch (_) {} backendLogStream = null; } } } function getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); const mimeTypes = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.woff': 'application/font-woff', '.woff2': 'font/woff2', '.ttf': 'application/font-ttf' }; return mimeTypes[ext] || 'application/octet-stream'; } function startLocalServer(callback) { const distPath = path.join(__dirname, "..", "dist"); log(`Starting local static server serving: ${distPath}`); staticServer = http.createServer((req, res) => { try { const url = new URL(req.url, `http://localhost`); let filePath = path.join(distPath, url.pathname); // Security check if (!filePath.startsWith(distPath)) { res.statusCode = 403; res.end('Forbidden'); return; } // Default to index.html for directories if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) { filePath = path.join(filePath, 'index.html'); } // SPA Fallback: if file not found and no extension, serve index.html if (!fs.existsSync(filePath)) { if (path.extname(filePath) === '') { filePath = path.join(distPath, 'index.html'); } else { res.statusCode = 404; res.end('Not Found'); return; } } const data = fs.readFileSync(filePath); const mimeType = getMimeType(filePath); res.setHeader('Content-Type', mimeType); res.end(data); } catch (err) { log(`Server error: ${err.message}`); res.statusCode = 500; res.end(`Internal Server Error`); } }); // Listen on a random available port staticServer.listen(0, '127.0.0.1', () => { const port = staticServer.address().port; const url = `http://127.0.0.1:${port}`; log(`Local static server running at ${url}`); callback(url); }); staticServer.on('error', (err) => { log(`Static server error: ${err.message}`); }); } function createWindowForDisplay(display, routePath, baseUrl) { const bounds = display.bounds; const win = new BrowserWindow({ x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, autoHideMenuBar: true, webPreferences: { contextIsolation: true } }); win.setMenuBarVisibility(false); if (process.platform !== "darwin") { win.removeMenu(); } // Combine baseUrl with route hash // e.g. http://127.0.0.1:5173/#/control/config // or http://127.0.0.1:xxxxx/#/control/config const fullUrl = `${baseUrl}/#${routePath}`; log(`Loading window URL: ${fullUrl}`); win.loadURL(fullUrl).catch(e => { log(`Failed to load URL ${fullUrl}: ${e.message}`); }); return win; } function createWindows(baseUrl) { log(`Creating windows with base URL: ${baseUrl}`); const displays = screen.getAllDisplays(); const primary = screen.getPrimaryDisplay(); const others = displays.filter((d) => d.id !== primary.id); createWindowForDisplay(primary, "/control/config", baseUrl); if (others[0]) { createWindowForDisplay(others[0], "/big/dashboard", baseUrl); } if (others[1]) { createWindowForDisplay(others[1], "/car/sim", baseUrl); } } app.whenReady().then(() => { log("App ready"); if (process.platform !== "darwin") { Menu.setApplicationMenu(null); } startBackend(); if (app.isPackaged) { // Production: Start local static server startLocalServer((serverUrl) => { createWindows(serverUrl); }); } else { // Development: Use Vite dev server const devServerUrl = process.env.VITE_DEV_SERVER_URL || "http://127.0.0.1:5173"; createWindows(devServerUrl); } }); app.on("before-quit", () => { if (backendProcess) { backendProcess.kill(); backendProcess = null; } if (backendLogStream) { try { backendLogStream.end(); } catch (_) {} backendLogStream = null; } if (staticServer) { staticServer.close(); staticServer = null; } });