Phase 2.2: performance optimizations, debug tools, app close fix
This commit is contained in:
116
desktop/main.js
116
desktop/main.js
@@ -1,5 +1,5 @@
|
||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
|
||||
const { spawn } = require('child_process');
|
||||
const { spawn, execSync } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const Store = require('electron-store');
|
||||
@@ -179,7 +179,8 @@ async function startBackend() {
|
||||
backendProcess = spawn(backendExe, [], {
|
||||
cwd: backendDir,
|
||||
env,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: process.platform !== 'win32' // Unix: create process group for clean kill
|
||||
});
|
||||
}
|
||||
|
||||
@@ -305,6 +306,35 @@ function createMainWindow() {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Backend cleanup ───────────────────────────────────────────────
|
||||
|
||||
function killBackend() {
|
||||
if (!backendProcess) return;
|
||||
|
||||
const pid = backendProcess.pid;
|
||||
log(`Killing backend (PID ${pid})...`);
|
||||
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
// Windows: taskkill with /T (tree) to kill child processes too
|
||||
execSync(`taskkill /f /t /pid ${pid}`, { stdio: 'ignore' });
|
||||
} else {
|
||||
// Unix: kill process group
|
||||
process.kill(-pid, 'SIGTERM');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: try normal kill
|
||||
try {
|
||||
backendProcess.kill('SIGKILL');
|
||||
} catch (_e2) {
|
||||
// Already dead
|
||||
}
|
||||
}
|
||||
|
||||
backendProcess = null;
|
||||
log('Backend killed');
|
||||
}
|
||||
|
||||
// ── App lifecycle ──────────────────────────────────────────────────
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
@@ -335,10 +365,7 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (backendProcess) {
|
||||
backendProcess.kill();
|
||||
backendProcess = null;
|
||||
}
|
||||
killBackend();
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
@@ -352,11 +379,11 @@ app.on('activate', () => {
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (backendProcess) {
|
||||
backendProcess.kill();
|
||||
}
|
||||
killBackend();
|
||||
|
||||
if (backendLogStream) {
|
||||
backendLogStream.end();
|
||||
backendLogStream = null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -404,3 +431,74 @@ 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));
|
||||
|
||||
// ── Import Region Data ────────────────────────────────────────────
|
||||
|
||||
ipcMain.handle('import-region-data', async () => {
|
||||
const result = await dialog.showOpenDialog(mainWindow, {
|
||||
title: 'Select folder with region data',
|
||||
properties: ['openDirectory']
|
||||
});
|
||||
|
||||
const srcDir = result.filePaths[0];
|
||||
if (!srcDir) return { success: false, message: 'Cancelled' };
|
||||
|
||||
const dataPath = getDataPath();
|
||||
let terrainCount = 0;
|
||||
let osmCount = 0;
|
||||
|
||||
try {
|
||||
// Copy terrain/*.hgt files
|
||||
const terrainSrc = path.join(srcDir, 'terrain');
|
||||
if (fs.existsSync(terrainSrc)) {
|
||||
const terrainDest = path.join(dataPath, 'terrain');
|
||||
if (!fs.existsSync(terrainDest)) {
|
||||
fs.mkdirSync(terrainDest, { recursive: true });
|
||||
}
|
||||
const hgtFiles = fs.readdirSync(terrainSrc).filter(f => f.endsWith('.hgt'));
|
||||
for (const file of hgtFiles) {
|
||||
fs.copyFileSync(path.join(terrainSrc, file), path.join(terrainDest, file));
|
||||
terrainCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy osm/**/*.json files
|
||||
const osmSrc = path.join(srcDir, 'osm');
|
||||
if (fs.existsSync(osmSrc)) {
|
||||
const osmDest = path.join(dataPath, 'osm');
|
||||
const subdirs = fs.readdirSync(osmSrc).filter(d =>
|
||||
fs.statSync(path.join(osmSrc, d)).isDirectory()
|
||||
);
|
||||
for (const subdir of subdirs) {
|
||||
const subSrc = path.join(osmSrc, subdir);
|
||||
const subDest = path.join(osmDest, subdir);
|
||||
if (!fs.existsSync(subDest)) {
|
||||
fs.mkdirSync(subDest, { recursive: true });
|
||||
}
|
||||
const jsonFiles = fs.readdirSync(subSrc).filter(f => f.endsWith('.json'));
|
||||
for (const file of jsonFiles) {
|
||||
fs.copyFileSync(path.join(subSrc, file), path.join(subDest, file));
|
||||
osmCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (terrainCount === 0 && osmCount === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No data files found. Expected terrain/*.hgt or osm/**/*.json'
|
||||
};
|
||||
}
|
||||
|
||||
log(`Imported ${terrainCount} terrain tiles, ${osmCount} OSM files from ${srcDir}`);
|
||||
return {
|
||||
success: true,
|
||||
terrainCount,
|
||||
osmCount,
|
||||
message: `Imported ${terrainCount} terrain tiles and ${osmCount} OSM cache files`
|
||||
};
|
||||
} catch (e) {
|
||||
log(`Import error: ${e.message}`);
|
||||
return { success: false, message: `Import failed: ${e.message}` };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,6 +21,9 @@ contextBridge.exposeInMainWorld('rfcp', {
|
||||
openExternal: (url) => ipcRenderer.invoke('open-external', url),
|
||||
openPath: (path) => ipcRenderer.invoke('open-path', path),
|
||||
|
||||
// Region data import
|
||||
importRegionData: () => ipcRenderer.invoke('import-region-data'),
|
||||
|
||||
// Platform info
|
||||
platform: process.platform,
|
||||
isDesktop: true,
|
||||
|
||||
Reference in New Issue
Block a user