feat(runtime): bundle node.exe for Windows to prevent access violations with native modules

This commit is contained in:
Sparsh 2026-05-11 12:50:03 +05:30
parent 16c383bbc9
commit 28ba9bb61d
3 changed files with 34 additions and 2 deletions

View file

@ -32,6 +32,12 @@ extraResources:
- '**/*'
- from: '${env.GITNEXUS_DESKTOP_GITNEXUS_PACKAGE_JSON}'
to: gitnexus/package.json
- from: '${env.GITNEXUS_DESKTOP_NODE_BINARY}'
to: runtime/node.exe
filter:
- '**/*'
platform:
- win
asar: false
npmRebuild: false
win:

View file

@ -155,6 +155,12 @@ const builderEnvironment = {
GITNEXUS_DESKTOP_GITNEXUS_SKILLS: toBuilderRelativePath(path.join(gitnexusRoot, 'skills')),
GITNEXUS_DESKTOP_GITNEXUS_VENDOR: toBuilderRelativePath(path.join(gitnexusRoot, 'vendor')),
GITNEXUS_DESKTOP_WEB_DIST: toBuilderRelativePath(path.join(gitnexusWebRoot, 'dist')),
// On Windows, lbugjs.node PE-imports node.exe by name, which fails under Electron's binary.
// electron-builder copies this into resources/runtime/node.exe so it lands in both
// win-unpacked (smoke test) and the NSIS installer (end-user install).
// The extraResources entry in electron-builder.yml is platform: [win] so this value is
// ignored on macOS/Linux (empty string prevents electron-builder from touching it there).
GITNEXUS_DESKTOP_NODE_BINARY: process.platform === 'win32' ? process.execPath : '',
};
const builderCliArgs = [

View file

@ -38,6 +38,10 @@ const GITNEXUS_WEB_DEV_ROOT = path.resolve(__dirname, '../../gitnexus-web');
const GITNEXUS_WEB_DEV_ROOT_FALLBACK = path.resolve(__dirname, '../../../gitnexus-web');
const GITNEXUS_PACKAGED_RUNTIME_DIR = path.join(process.resourcesPath, 'gitnexus');
const GITNEXUS_WEB_PACKAGED_DIR = path.join(process.resourcesPath, 'gitnexus-web');
// On Windows, native modules (e.g. lbugjs.node) PE-import node.exe by name.
// Electron's binary is not node.exe, so LoadLibrary fails with an access violation.
// Bundling a real node.exe and using it as the subprocess host avoids the crash.
const GITNEXUS_PACKAGED_NODE_BINARY = path.join(process.resourcesPath, 'runtime', 'node.exe');
const GITNEXUS_WEB_EXPECTED_MARKERS = ['<title>GitNexus</title>', '<div id="root"></div>'];
const GITNEXUS_SERVER_READY_TIMEOUT_MS = 30_000;
const GITNEXUS_WEB_READY_TIMEOUT_MS = 60_000;
@ -240,6 +244,10 @@ const getGitNexusWebViteCliEntry = (): string => {
const getNodeCommand = (): string => {
if (app.isPackaged) {
if (process.platform === 'win32' && existsSync(GITNEXUS_PACKAGED_NODE_BINARY)) {
return GITNEXUS_PACKAGED_NODE_BINARY;
}
return process.execPath;
}
@ -257,8 +265,14 @@ const getNodeProcessEnvironment = (overrides: NodeJS.ProcessEnv = {}): NodeJS.Pr
};
if (app.isPackaged) {
// Packaged mode reuses Electron's embedded Node runtime via process.execPath.
environment.ELECTRON_RUN_AS_NODE = '1';
// Only set ELECTRON_RUN_AS_NODE when falling back to the Electron binary as the node host.
// When using the bundled node.exe, the host is already a real node process — no flag needed.
const usingBundledNode =
process.platform === 'win32' && existsSync(GITNEXUS_PACKAGED_NODE_BINARY);
if (!usingBundledNode) {
environment.ELECTRON_RUN_AS_NODE = '1';
}
}
return environment;
@ -657,6 +671,12 @@ const registerWindowIpcHandlers = (): void => {
const showStartupError = (error: unknown): void => {
console.error('[gitnexus-desktop] Startup failed.', error);
// In CI smoke tests, dialog.showErrorBox() blocks synchronously forever (no display).
// Log the error above and let process.exit() handle the failure signal instead.
if (IS_DESKTOP_SMOKE_TEST) {
return;
}
if (isAddressInUseError(error)) {
dialog.showErrorBox(
'GitNexus Desktop Startup Failed',