SmartEDT/frontend/main/main.cjs

309 lines
8.6 KiB
JavaScript
Raw Normal View History

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;
}
});