- Replace fork() with spawn() + ELECTRON_RUN_AS_NODE=1 so the standalone server runs correctly inside the packaged app - Set cwd to standalone directory for proper dependency resolution - Show error dialog on startup failure instead of silent quit - Add Chinese README (README_CN.md) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
185 lines
4.7 KiB
TypeScript
185 lines
4.7 KiB
TypeScript
import { app, BrowserWindow, nativeImage, dialog } from 'electron';
|
|
import path from 'path';
|
|
import { spawn, ChildProcess } from 'child_process';
|
|
import net from 'net';
|
|
|
|
let mainWindow: BrowserWindow | null = null;
|
|
let serverProcess: ChildProcess | null = null;
|
|
let serverPort: number | null = null;
|
|
|
|
const isDev = !app.isPackaged;
|
|
|
|
function getPort(): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
const server = net.createServer();
|
|
server.unref();
|
|
server.on('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
const addr = server.address();
|
|
if (addr && typeof addr === 'object') {
|
|
const port = addr.port;
|
|
server.close(() => resolve(port));
|
|
} else {
|
|
server.close(() => reject(new Error('Failed to get port')));
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function waitForServer(port: number, timeout = 30000): Promise<void> {
|
|
const start = Date.now();
|
|
while (Date.now() - start < timeout) {
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const req = require('http').get(`http://127.0.0.1:${port}/api/health`, (res: { statusCode?: number }) => {
|
|
if (res.statusCode === 200) resolve();
|
|
else reject(new Error(`Status ${res.statusCode}`));
|
|
});
|
|
req.on('error', reject);
|
|
req.setTimeout(1000, () => {
|
|
req.destroy();
|
|
reject(new Error('timeout'));
|
|
});
|
|
});
|
|
return;
|
|
} catch {
|
|
await new Promise(r => setTimeout(r, 200));
|
|
}
|
|
}
|
|
throw new Error('Server startup timeout');
|
|
}
|
|
|
|
function startServer(port: number): ChildProcess {
|
|
const serverPath = path.join(process.resourcesPath, 'standalone', 'server.js');
|
|
|
|
// Use the bundled Node.js from Electron's helper
|
|
const nodePath = process.execPath;
|
|
|
|
const child = spawn(nodePath, [serverPath], {
|
|
env: {
|
|
...process.env,
|
|
PORT: String(port),
|
|
HOSTNAME: '127.0.0.1',
|
|
CLAUDE_GUI_DATA_DIR: app.getPath('userData'),
|
|
// Tell Electron's Node not to load the Electron module
|
|
ELECTRON_RUN_AS_NODE: '1',
|
|
},
|
|
stdio: 'pipe',
|
|
cwd: path.join(process.resourcesPath, 'standalone'),
|
|
});
|
|
|
|
child.stdout?.on('data', (data: Buffer) => {
|
|
console.log(`[server] ${data.toString().trim()}`);
|
|
});
|
|
|
|
child.stderr?.on('data', (data: Buffer) => {
|
|
console.error(`[server] ${data.toString().trim()}`);
|
|
});
|
|
|
|
child.on('exit', (code) => {
|
|
console.log(`Server process exited with code ${code}`);
|
|
serverProcess = null;
|
|
});
|
|
|
|
return child;
|
|
}
|
|
|
|
function getIconPath(): string {
|
|
if (isDev) {
|
|
return path.join(process.cwd(), 'build', 'icon.png');
|
|
}
|
|
return path.join(process.resourcesPath, 'icon.icns');
|
|
}
|
|
|
|
function createWindow(port: number) {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1280,
|
|
height: 860,
|
|
minWidth: 800,
|
|
minHeight: 600,
|
|
icon: getIconPath(),
|
|
titleBarStyle: 'hiddenInset',
|
|
webPreferences: {
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
},
|
|
});
|
|
|
|
mainWindow.loadURL(`http://127.0.0.1:${port}`);
|
|
|
|
if (isDev) {
|
|
mainWindow.webContents.openDevTools();
|
|
}
|
|
|
|
mainWindow.on('closed', () => {
|
|
mainWindow = null;
|
|
});
|
|
}
|
|
|
|
app.whenReady().then(async () => {
|
|
// Set macOS Dock icon
|
|
if (process.platform === 'darwin' && app.dock) {
|
|
const iconPath = getIconPath();
|
|
app.dock.setIcon(nativeImage.createFromPath(iconPath));
|
|
}
|
|
|
|
try {
|
|
let port: number;
|
|
|
|
if (isDev) {
|
|
port = 3000;
|
|
console.log(`Dev mode: connecting to http://127.0.0.1:${port}`);
|
|
} else {
|
|
port = await getPort();
|
|
console.log(`Starting server on port ${port}...`);
|
|
serverProcess = startServer(port);
|
|
await waitForServer(port);
|
|
console.log('Server is ready');
|
|
}
|
|
|
|
serverPort = port;
|
|
createWindow(port);
|
|
} catch (err) {
|
|
console.error('Failed to start:', err);
|
|
dialog.showErrorBox(
|
|
'CodePilot - Failed to Start',
|
|
`The internal server could not start.\n\n${err instanceof Error ? err.message : String(err)}\n\nPlease try restarting the application.`
|
|
);
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (serverProcess) {
|
|
serverProcess.kill();
|
|
serverProcess = null;
|
|
}
|
|
if (process.platform !== 'darwin') {
|
|
app.quit();
|
|
}
|
|
});
|
|
|
|
app.on('activate', async () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
try {
|
|
if (!isDev && !serverProcess) {
|
|
const port = await getPort();
|
|
serverProcess = startServer(port);
|
|
await waitForServer(port);
|
|
serverPort = port;
|
|
}
|
|
createWindow(serverPort || 3000);
|
|
} catch (err) {
|
|
console.error('Failed to restart server:', err);
|
|
}
|
|
}
|
|
});
|
|
|
|
app.on('before-quit', () => {
|
|
if (serverProcess) {
|
|
serverProcess.kill();
|
|
serverProcess = null;
|
|
}
|
|
});
|