@mytec: iter2.2 start

This commit is contained in:
2026-01-31 15:25:18 +02:00
parent b62e893abe
commit 013cb155a9
7 changed files with 4655 additions and 65 deletions

View File

@@ -1,25 +1,46 @@
"""Entry point for PyInstaller bundle""" """Entry point for PyInstaller bundle"""
print("[RFCP] run_server.py starting...", flush=True)
import os import os
import sys import sys
# Set base path for PyInstaller # Set base path for PyInstaller
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
os.chdir(os.path.dirname(sys.executable)) base_dir = os.path.dirname(sys.executable)
# Fix uvicorn TTY detection — sys.stdin/stdout/stderr are None when frozen os.chdir(base_dir)
print(f"[RFCP] Frozen mode, base dir: {base_dir}", flush=True)
# Fix uvicorn TTY detection — redirect None streams to a log file
log_path = os.path.join(base_dir, 'rfcp-server.log')
log_file = open(log_path, 'w')
if sys.stdout is None:
sys.stdout = log_file
if sys.stderr is None:
sys.stderr = log_file
if sys.stdin is None: if sys.stdin is None:
sys.stdin = open(os.devnull, 'r') sys.stdin = open(os.devnull, 'r')
if sys.stdout is None: print(f"[RFCP] Log file: {log_path}", flush=True)
sys.stdout = open(os.devnull, 'w')
if sys.stderr is None:
sys.stderr = open(os.devnull, 'w')
print("[RFCP] Importing uvicorn...", flush=True)
import uvicorn import uvicorn
print("[RFCP] Importing app.main...", flush=True)
try:
from app.main import app from app.main import app
print("[RFCP] App imported successfully", flush=True)
except Exception as e:
print(f"[RFCP] FATAL: Failed to import app: {e}", flush=True)
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__': if __name__ == '__main__':
host = os.environ.get('RFCP_HOST', '127.0.0.1') host = os.environ.get('RFCP_HOST', '127.0.0.1')
port = int(os.environ.get('RFCP_PORT', '8888')) port = int(os.environ.get('RFCP_PORT', '8888'))
print(f"[RFCP] Starting uvicorn on {host}:{port}", flush=True)
try:
uvicorn.run( uvicorn.run(
app, app,
host=host, host=host,
@@ -27,3 +48,8 @@ if __name__ == '__main__':
log_level='warning', log_level='warning',
access_log=False, access_log=False,
) )
except Exception as e:
print(f"[RFCP] FATAL: uvicorn.run failed: {e}", flush=True)
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -8,36 +8,84 @@ const store = new Store();
let mainWindow; let mainWindow;
let splashWindow; let splashWindow;
let backendProcess; let backendProcess;
let backendLogStream;
// ── Paths ──────────────────────────────────────────────────────────
// Paths
const isDev = process.env.NODE_ENV === 'development'; const isDev = process.env.NODE_ENV === 'development';
const getResourcePath = (relativePath) => { /**
* Get path to a bundled resource (extraResources).
* In production, extraResources live under process.resourcesPath.
* In dev, they live in the repo root.
*/
const getResourcePath = (...segments) => {
if (isDev) { if (isDev) {
return path.join(__dirname, '..', relativePath); return path.join(__dirname, '..', ...segments);
} }
return path.join(process.resourcesPath, relativePath); return path.join(process.resourcesPath, ...segments);
}; };
/**
* User data directory — writable, persists across updates.
* Windows: %APPDATA%\RFCP\data\
* Linux: ~/.config/RFCP/data/
* macOS: ~/Library/Application Support/RFCP/data/
*/
const getDataPath = () => { const getDataPath = () => {
return path.join(app.getPath('userData'), 'data'); return path.join(app.getPath('userData'), 'data');
}; };
// Get backend executable name based on platform /**
const getBackendExeName = () => { * Log directory for backend output and crash logs.
switch (process.platform) { * Windows: %APPDATA%\RFCP\logs\
case 'win32': * Linux: ~/.config/RFCP/logs/
return 'rfcp-server.exe'; * macOS: ~/Library/Logs/RFCP/
case 'darwin': */
return 'rfcp-server'; const getLogPath = () => {
case 'linux': return app.getPath('logs');
return 'rfcp-server';
default:
return 'rfcp-server';
}
}; };
// Ensure data directories exist /** Backend executable path */
const getBackendExePath = () => {
const exeName = process.platform === 'win32' ? 'rfcp-server.exe' : 'rfcp-server';
if (isDev) {
return path.join(__dirname, '..', 'backend', exeName);
}
return getResourcePath('backend', exeName);
};
/** Frontend index.html path (production only) */
const getFrontendPath = () => {
return getResourcePath('frontend', 'index.html');
};
// ── Logging ────────────────────────────────────────────────────────
function log(msg) {
const line = `[${new Date().toISOString()}] ${msg}`;
console.log(line);
if (backendLogStream) {
backendLogStream.write(line + '\n');
}
}
function initLogFile() {
const logDir = getLogPath();
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, 'rfcp-main.log');
backendLogStream = fs.createWriteStream(logFile, { flags: 'w' });
log(`Log file: ${logFile}`);
log(`Platform: ${process.platform}, Electron: ${process.versions.electron}`);
log(`isDev: ${isDev}`);
log(`userData: ${app.getPath('userData')}`);
log(`resourcesPath: ${isDev ? '(dev mode)' : process.resourcesPath}`);
}
// ── Data directories ───────────────────────────────────────────────
function ensureDataDirs() { function ensureDataDirs() {
const dirs = ['terrain', 'osm', 'projects', 'cache']; const dirs = ['terrain', 'osm', 'projects', 'cache'];
const dataPath = getDataPath(); const dataPath = getDataPath();
@@ -49,10 +97,12 @@ function ensureDataDirs() {
} }
}); });
log(`Data path: ${dataPath}`);
return dataPath; return dataPath;
} }
// Create splash window // ── Splash window ──────────────────────────────────────────────────
function createSplashWindow() { function createSplashWindow() {
splashWindow = new BrowserWindow({ splashWindow = new BrowserWindow({
width: 400, width: 400,
@@ -71,21 +121,29 @@ function createSplashWindow() {
splashWindow.center(); splashWindow.center();
} }
// Start Python backend // ── Backend lifecycle ──────────────────────────────────────────────
async function startBackend() { async function startBackend() {
const dataPath = ensureDataDirs(); const dataPath = ensureDataDirs();
const logDir = getLogPath();
// SQLite URL — normalize to forward slashes for cross-platform compatibility
const dbPath = path.join(dataPath, 'rfcp.db').replace(/\\/g, '/');
const env = { const env = {
...process.env, ...process.env,
RFCP_DATA_PATH: dataPath, RFCP_DATA_PATH: dataPath,
RFCP_DATABASE_URL: `sqlite:///${path.join(dataPath, 'rfcp.db')}`, RFCP_DATABASE_URL: `sqlite:///${dbPath}`,
RFCP_HOST: '127.0.0.1', RFCP_HOST: '127.0.0.1',
RFCP_PORT: '8888', RFCP_PORT: '8888',
RFCP_LOG_PATH: logDir,
}; };
if (isDev) { if (isDev) {
// Development: run uvicorn
const pythonCmd = process.platform === 'win32' ? 'python' : 'python3'; const pythonCmd = process.platform === 'win32' ? 'python' : 'python3';
const backendCwd = path.join(__dirname, '..', 'backend');
log(`Starting dev backend: ${pythonCmd} -m uvicorn ... (cwd: ${backendCwd})`);
backendProcess = spawn(pythonCmd, [ backendProcess = spawn(pythonCmd, [
'-m', 'uvicorn', '-m', 'uvicorn',
'app.main:app', 'app.main:app',
@@ -93,46 +151,63 @@ async function startBackend() {
'--port', '8888', '--port', '8888',
'--reload' '--reload'
], { ], {
cwd: path.join(__dirname, '..', 'backend'), cwd: backendCwd,
env, env,
stdio: ['ignore', 'pipe', 'pipe'] stdio: ['ignore', 'pipe', 'pipe']
}); });
} else { } else {
// Production: run PyInstaller bundle const backendExe = getBackendExePath();
const backendExe = path.join( const backendDir = path.dirname(backendExe);
getResourcePath('backend'), log(`Starting production backend: ${backendExe}`);
getBackendExeName() log(`Backend cwd: ${backendDir}`);
);
// Verify the exe exists
if (!fs.existsSync(backendExe)) {
log(`FATAL: Backend exe not found at ${backendExe}`);
return false;
}
// Make executable on Unix // Make executable on Unix
if (process.platform !== 'win32') { if (process.platform !== 'win32') {
try { try {
fs.chmodSync(backendExe, '755'); fs.chmodSync(backendExe, '755');
} catch (e) { } catch (e) {
console.log('chmod failed:', e); log(`chmod warning: ${e.message}`);
} }
} }
backendProcess = spawn(backendExe, [], { backendProcess = spawn(backendExe, [], {
cwd: backendDir,
env, env,
stdio: ['ignore', 'pipe', 'pipe'] stdio: ['ignore', 'pipe', 'pipe']
}); });
} }
// Log backend output // Pipe backend output to log
const backendLogFile = path.join(logDir, 'rfcp-backend.log');
const backendLog = fs.createWriteStream(backendLogFile, { flags: 'w' });
backendProcess.stdout?.on('data', (data) => { backendProcess.stdout?.on('data', (data) => {
console.log(`[Backend] ${data}`); const text = data.toString().trim();
log(`[Backend] ${text}`);
backendLog.write(text + '\n');
}); });
backendProcess.stderr?.on('data', (data) => { backendProcess.stderr?.on('data', (data) => {
console.error(`[Backend Error] ${data}`); const text = data.toString().trim();
log(`[Backend:err] ${text}`);
backendLog.write(`[err] ${text}\n`);
}); });
backendProcess.on('error', (err) => { backendProcess.on('error', (err) => {
console.error('Failed to start backend:', err); log(`Failed to start backend: ${err.message}`);
dialog.showErrorBox('Backend Error', `Failed to start backend: ${err.message}`); dialog.showErrorBox('Backend Error', `Failed to start backend: ${err.message}`);
}); });
backendProcess.on('exit', (code, signal) => {
log(`Backend exited: code=${code}, signal=${signal}`);
});
// Wait for backend to be ready // Wait for backend to be ready
return new Promise((resolve) => { return new Promise((resolve) => {
const maxAttempts = 60; // 30 seconds const maxAttempts = 60; // 30 seconds
@@ -145,7 +220,7 @@ async function startBackend() {
const response = await fetch('http://127.0.0.1:8888/api/health/'); const response = await fetch('http://127.0.0.1:8888/api/health/');
if (response.ok) { if (response.ok) {
clearInterval(checkBackend); clearInterval(checkBackend);
console.log('Backend ready!'); log(`Backend ready after ${attempts * 0.5}s`);
resolve(true); resolve(true);
} }
} catch (_e) { } catch (_e) {
@@ -154,14 +229,15 @@ async function startBackend() {
if (attempts >= maxAttempts) { if (attempts >= maxAttempts) {
clearInterval(checkBackend); clearInterval(checkBackend);
console.error('Backend failed to start in time'); log('Backend failed to start within 30s');
resolve(false); resolve(false);
} }
}, 500); }, 500);
}); });
} }
// Create main window // ── Main window ────────────────────────────────────────────────────
function createMainWindow() { function createMainWindow() {
const windowState = store.get('windowState', { const windowState = store.get('windowState', {
width: 1400, width: 1400,
@@ -181,9 +257,6 @@ function createMainWindow() {
contextIsolation: true, contextIsolation: true,
preload: path.join(__dirname, 'preload.js') preload: path.join(__dirname, 'preload.js')
}, },
icon: path.join(__dirname, 'assets',
process.platform === 'win32' ? 'icon.ico' : 'icon.png'
),
title: 'RFCP - RF Coverage Planner', title: 'RFCP - RF Coverage Planner',
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default', titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
}); });
@@ -196,10 +269,21 @@ function createMainWindow() {
// Load frontend // Load frontend
if (isDev) { if (isDev) {
log('Loading frontend from dev server: http://localhost:5173');
mainWindow.loadURL('http://localhost:5173'); mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools(); mainWindow.webContents.openDevTools();
} else { } else {
mainWindow.loadFile(path.join(getResourcePath('frontend'), 'index.html')); const frontendIndex = getFrontendPath();
log(`Loading frontend from: ${frontendIndex}`);
if (!fs.existsSync(frontendIndex)) {
log(`FATAL: Frontend not found at ${frontendIndex}`);
dialog.showErrorBox('Startup Error', `Frontend not found at:\n${frontendIndex}`);
app.quit();
return;
}
mainWindow.loadFile(frontendIndex);
} }
mainWindow.once('ready-to-show', () => { mainWindow.once('ready-to-show', () => {
@@ -221,23 +305,26 @@ function createMainWindow() {
}); });
} }
// App lifecycle // ── App lifecycle ──────────────────────────────────────────────────
app.whenReady().then(async () => { app.whenReady().then(async () => {
initLogFile();
createSplashWindow(); createSplashWindow();
const backendStarted = await startBackend(); const backendStarted = await startBackend();
if (!backendStarted) { if (!backendStarted) {
const logDir = getLogPath();
const result = await dialog.showMessageBox({ const result = await dialog.showMessageBox({
type: 'error', type: 'error',
title: 'Startup Error', title: 'Startup Error',
message: 'Failed to start backend server.', message: 'Failed to start backend server.',
detail: 'Please check logs and try again. Click "Show Logs" to open the log folder.', detail: `Check logs at:\n${logDir}\n\nClick "Show Logs" to open the folder.`,
buttons: ['Quit', 'Show Logs'] buttons: ['Quit', 'Show Logs']
}); });
if (result.response === 1) { if (result.response === 1) {
shell.openPath(app.getPath('logs')); shell.openPath(logDir);
} }
app.quit(); app.quit();
@@ -268,10 +355,15 @@ app.on('before-quit', () => {
if (backendProcess) { if (backendProcess) {
backendProcess.kill(); backendProcess.kill();
} }
if (backendLogStream) {
backendLogStream.end();
}
}); });
// IPC Handlers // ── IPC Handlers ───────────────────────────────────────────────────
ipcMain.handle('get-data-path', () => getDataPath()); ipcMain.handle('get-data-path', () => getDataPath());
ipcMain.handle('get-log-path', () => getLogPath());
ipcMain.handle('get-app-version', () => app.getVersion()); ipcMain.handle('get-app-version', () => app.getVersion());
ipcMain.handle('get-platform', () => process.platform); ipcMain.handle('get-platform', () => process.platform);

4419
desktop/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@ const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('rfcp', { contextBridge.exposeInMainWorld('rfcp', {
// System info // System info
getDataPath: () => ipcRenderer.invoke('get-data-path'), getDataPath: () => ipcRenderer.invoke('get-data-path'),
getLogPath: () => ipcRenderer.invoke('get-log-path'),
getAppVersion: () => ipcRenderer.invoke('get-app-version'), getAppVersion: () => ipcRenderer.invoke('get-app-version'),
getPlatform: () => ipcRenderer.invoke('get-platform'), getPlatform: () => ipcRenderer.invoke('get-platform'),
getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'), getGpuInfo: () => ipcRenderer.invoke('get-gpu-info'),

View File

@@ -27,8 +27,8 @@ python3 -m pip install -r requirements.txt
python3 -m pip install pyinstaller python3 -m pip install pyinstaller
cd ../installer cd ../installer
python3 -m PyInstaller rfcp-server.spec --clean --noconfirm python3 -m PyInstaller rfcp-server.spec --clean --noconfirm
mkdir -p ../desktop/backend-dist/darwin mkdir -p ../desktop/backend-dist/mac
cp dist/rfcp-server ../desktop/backend-dist/darwin/ cp dist/rfcp-server ../desktop/backend-dist/mac/
cd .. cd ..
# 3. Create .icns icon if not exists # 3. Create .icns icon if not exists

View File

@@ -21,8 +21,8 @@ python -m pip install -r requirements.txt
python -m pip install pyinstaller python -m pip install pyinstaller
cd ../installer cd ../installer
python -m PyInstaller rfcp-server.spec --clean --noconfirm python -m PyInstaller rfcp-server.spec --clean --noconfirm
mkdir -p ../desktop/backend-dist/win32 mkdir -p ../desktop/backend-dist/win
cp dist/rfcp-server.exe ../desktop/backend-dist/win32/ cp dist/rfcp-server.exe ../desktop/backend-dist/win/
cd .. cd ..
# 3. Build Electron app # 3. Build Electron app

View File

@@ -13,24 +13,76 @@ a = Analysis(
(str(backend_path / 'app'), 'app'), (str(backend_path / 'app'), 'app'),
], ],
hiddenimports=[ hiddenimports=[
# Uvicorn internals
'uvicorn.logging', 'uvicorn.logging',
'uvicorn.loops',
'uvicorn.loops.auto',
'uvicorn.loops.asyncio',
'uvicorn.protocols',
'uvicorn.protocols.http', 'uvicorn.protocols.http',
'uvicorn.protocols.http.auto', 'uvicorn.protocols.http.auto',
'uvicorn.protocols.http.h11_impl', 'uvicorn.protocols.http.h11_impl',
'uvicorn.protocols.http.httptools_impl',
'uvicorn.protocols.websockets', 'uvicorn.protocols.websockets',
'uvicorn.protocols.websockets.auto', 'uvicorn.protocols.websockets.auto',
'uvicorn.protocols.websockets.wsproto_impl',
'uvicorn.lifespan', 'uvicorn.lifespan',
'uvicorn.lifespan.on', 'uvicorn.lifespan.on',
'uvicorn.lifespan.off', 'uvicorn.lifespan.off',
# FastAPI / Starlette
'fastapi',
'fastapi.middleware',
'fastapi.middleware.cors',
'fastapi.routing',
'fastapi.responses',
'fastapi.exceptions',
'starlette',
'starlette.routing',
'starlette.middleware',
'starlette.middleware.cors',
'starlette.responses',
'starlette.requests',
'starlette.concurrency',
'starlette.formparsers',
'starlette.staticfiles',
# Pydantic
'pydantic',
'pydantic.fields',
'pydantic_settings',
'pydantic_core',
# HTTP / networking
'httpx', 'httpx',
'httpcore',
'h11', 'h11',
'httptools',
'anyio',
'anyio._backends',
'anyio._backends._asyncio',
'sniffio',
# MongoDB (motor/pymongo)
'motor',
'motor.motor_asyncio',
'pymongo',
'pymongo.errors',
'pymongo.collection',
'pymongo.database',
'pymongo.mongo_client',
# Async I/O
'aiofiles',
'aiofiles.os',
'aiofiles.ospath',
# Scientific
'numpy', 'numpy',
'numpy.core',
'scipy', 'scipy',
'scipy.special', 'scipy.special',
'scipy.interpolate', 'scipy.interpolate',
'aiosqlite', # Multipart
'sqlalchemy', 'multipart',
'sqlalchemy.dialects.sqlite', 'python_multipart',
# Encoding
'email.mime',
'email.mime.multipart',
], ],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
@@ -58,7 +110,7 @@ exe = EXE(
upx=True, upx=True,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=False, # No console window console=True, # Show console for debugging — set to False for release
disable_windowed_traceback=False, disable_windowed_traceback=False,
target_arch=None, target_arch=None,
codesign_identity=None, codesign_identity=None,