309 lines
8.6 KiB
JavaScript
309 lines
8.6 KiB
JavaScript
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;
|
|
}
|
|
});
|