| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- const { spawn } = require('child_process');
- const path = require('path');
- const fs = require('fs');
- const http = require('http');
- const { app } = require('electron');
- interface ServiceProcess {
- process: ReturnType<typeof spawn> | null;
- name: string;
- port: number;
- ready: boolean;
- restartCount: number;
- lastRestartTime: number;
- }
- const NODE_PORT = 14890;
- const LOCAL_NODE_URL = `http://127.0.0.1:${NODE_PORT}`;
- const MAX_RESTARTS = 4;
- const RESTART_WINDOW_MS = 2 * 60 * 1000;
- const services: { node: ServiceProcess } = {
- node: {
- process: null,
- name: 'Node Server',
- port: NODE_PORT,
- ready: false,
- restartCount: 0,
- lastRestartTime: 0,
- },
- };
- let logFilePath = '';
- function getLogPath(): string {
- if (!logFilePath) {
- try {
- logFilePath = path.join(app.getPath('userData'), 'local-services.log');
- } catch {
- logFilePath = path.join(process.cwd(), 'local-services.log');
- }
- }
- return logFilePath;
- }
- function log(level: string, ...args: unknown[]): void {
- const line = `[${new Date().toISOString()}] [${level}] ${args.map((item) => typeof item === 'string' ? item : JSON.stringify(item)).join(' ')}`;
- if (level === 'ERROR') {
- console.error(line);
- } else {
- console.log(line);
- }
- try {
- fs.appendFileSync(getLogPath(), line + '\n', 'utf8');
- } catch {
- // ignore logging failures
- }
- }
- function isPackaged(): boolean {
- return app.isPackaged;
- }
- function getServerDir(): string {
- if (isPackaged()) {
- return path.join((process as any).resourcesPath, 'server');
- }
- return path.resolve(__dirname, '..', '..', 'server');
- }
- function getPlaywrightBrowsersPath(): string | null {
- if (!isPackaged()) return null;
- const bundled = path.join((process as any).resourcesPath, 'playwright');
- return fs.existsSync(bundled) ? bundled : null;
- }
- function findNodeRunner(): { cmd: string; args: string[]; useElectronAsNode?: boolean } {
- const serverDir = getServerDir();
- const distEntry = path.join(serverDir, 'dist', 'app.js');
- const srcEntry = path.join(serverDir, 'src', 'app.ts');
- const isWin = process.platform === 'win32';
- if (isPackaged()) {
- return { cmd: process.execPath, args: [distEntry], useElectronAsNode: true };
- }
- if (fs.existsSync(distEntry)) {
- return { cmd: 'node', args: [distEntry] };
- }
- const tsxBin = path.join(serverDir, 'node_modules', '.bin', isWin ? 'tsx.cmd' : 'tsx');
- if (fs.existsSync(tsxBin)) {
- return { cmd: tsxBin, args: [srcEntry] };
- }
- return { cmd: isWin ? 'npx.cmd' : 'npx', args: ['tsx', srcEntry] };
- }
- function checkHealth(port: number): Promise<boolean> {
- return new Promise((resolve) => {
- const req = http.get(`http://127.0.0.1:${port}/api/health`, { timeout: 2500 }, (res: any) => {
- res.resume();
- resolve(res.statusCode >= 200 && res.statusCode < 500);
- });
- req.on('error', () => resolve(false));
- req.on('timeout', () => {
- req.destroy();
- resolve(false);
- });
- });
- }
- async function waitForService(port: number, timeoutMs = 45000): Promise<boolean> {
- const startedAt = Date.now();
- while (Date.now() - startedAt < timeoutMs) {
- if (await checkHealth(port)) {
- return true;
- }
- await new Promise((resolve) => setTimeout(resolve, 1000));
- }
- return false;
- }
- function shouldRestart(service: ServiceProcess): boolean {
- const now = Date.now();
- if (now - service.lastRestartTime > RESTART_WINDOW_MS) {
- service.restartCount = 0;
- }
- service.lastRestartTime = now;
- service.restartCount += 1;
- return service.restartCount <= MAX_RESTARTS;
- }
- function startNodeServer(): void {
- const service = services.node;
- const serverDir = getServerDir();
- const { cmd, args, useElectronAsNode } = findNodeRunner();
- const env: Record<string, string> = {
- ...(process.env as Record<string, string>),
- PORT: String(NODE_PORT),
- HOST: '127.0.0.1',
- USE_REDIS_QUEUE: 'false',
- LOCAL_NODE_ONLY_AUTOMATION: 'true',
- NODE_OPTIONS: '--max-old-space-size=512',
- };
- if (useElectronAsNode) {
- env.ELECTRON_RUN_AS_NODE = '1';
- }
- if (isPackaged()) {
- const uploadsDir = path.join(app.getPath('userData'), 'uploads');
- fs.mkdirSync(uploadsDir, { recursive: true });
- env.UPLOAD_PATH = uploadsDir;
- }
- const playwrightPath = getPlaywrightBrowsersPath();
- if (playwrightPath) {
- env.PLAYWRIGHT_BROWSERS_PATH = playwrightPath;
- }
- log('INFO', 'Starting node service', cmd, args.join(' '));
- const child = spawn(cmd, args, {
- cwd: serverDir,
- env,
- stdio: ['ignore', 'pipe', 'pipe'],
- shell: false,
- windowsHide: true,
- });
- service.process = child;
- service.ready = false;
- child.stdout?.on('data', (chunk: Buffer) => {
- const message = chunk.toString().trim();
- if (!message) return;
- log('NODE', message);
- if (message.includes('Server running') || message.includes('listening')) {
- service.ready = true;
- }
- });
- child.stderr?.on('data', (chunk: Buffer) => {
- const message = chunk.toString().trim();
- if (message) log('NODE-ERR', message);
- });
- child.on('error', (error: Error) => {
- log('ERROR', 'Node spawn error', error.message);
- });
- child.on('exit', (code: number | null, signal: string | null) => {
- log('WARN', `Node service exited code=${code} signal=${signal}`);
- service.process = null;
- service.ready = false;
- if (signal === 'SIGTERM' || signal === 'SIGINT') {
- return;
- }
- if (!shouldRestart(service)) {
- log('ERROR', 'Node service restart budget exhausted');
- return;
- }
- const backoffMs = Math.min(service.restartCount * 4000, 15000);
- log('INFO', `Restarting node service in ${backoffMs}ms`);
- setTimeout(() => startNodeServer(), backoffMs);
- });
- }
- async function startLocalServices(): Promise<{ nodeOk: boolean }> {
- if (!services.node.process) {
- startNodeServer();
- }
- const nodeOk = await waitForService(NODE_PORT);
- services.node.ready = nodeOk;
- return { nodeOk };
- }
- function stopLocalServices(): void {
- const service = services.node;
- service.ready = false;
- if (!service.process) return;
- try {
- service.process.kill('SIGTERM');
- } catch (error) {
- log('ERROR', 'Failed to stop node service', error);
- }
- service.process = null;
- }
- function getServiceStatus() {
- return {
- node: {
- ready: services.node.ready,
- port: services.node.port,
- restartCount: services.node.restartCount,
- },
- };
- }
- export {
- NODE_PORT,
- LOCAL_NODE_URL,
- startLocalServices,
- stopLocalServices,
- getServiceStatus,
- getLogPath,
- };
|