From de93658df1e6747239700a906599bbf2732a9324 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 3 Aug 2026 00:21:04 +0100 Subject: [PATCH] Enhance Windows instance management and path resolution - Introduced a new function `isPrimaryInstanceReachable` to check the reachability of the primary instance, improving instance handling on Windows. - Updated `forwardDeeplinkToRunningInstance` to include an option for signal fallback, enhancing flexibility in deeplink forwarding. - Implemented `tryRecoverUnresponsivePrimaryLock` to handle unresponsive primary instances by removing stale lock files, improving application reliability. - Refactored `resolveWindowsBinDir` to utilize unique path resolution, ensuring accurate directory handling for Windows applications. - Enhanced the `ensureWindowsWorkingDirectory` function to streamline directory changes, improving application behavior. --- scripts/patch-electrobun-src.mjs | 19 ++++++++ src/desktop/deeplink-ipc.js | 15 ++++++- src/desktop/single-instance.js | 33 +++++++++++++- src/desktop/windows-app-paths.js | 74 ++++++++++++++++++++++---------- 4 files changed, 116 insertions(+), 25 deletions(-) diff --git a/scripts/patch-electrobun-src.mjs b/scripts/patch-electrobun-src.mjs index 0656823..f341c36 100644 --- a/scripts/patch-electrobun-src.mjs +++ b/scripts/patch-electrobun-src.mjs @@ -285,6 +285,24 @@ function patchNativeWrapperPath(nativeTsPath) { let source = readFileSync(nativeTsPath, 'utf8') if (source.includes('function resolveNativeWrapperPath()')) { + source = source.replace( + /function resolveNativeWrapperPath\(\) \{[\s\S]*?\n\}/m, + `function resolveNativeWrapperPath() { +\tconst fileName = \`libNativeWrapper.\${suffix}\`; +\tconst candidates = [ +\t\tjoin(dirname(process.argv0), fileName), +\t\tjoin(dirname(process.execPath), fileName), +\t\tjoin(process.cwd(), fileName), +\t]; +\tfor (const candidate of candidates) { +\t\tif (existsSync(candidate)) { +\t\t\treturn candidate; +\t\t} +\t} +\treturn candidates[0]!; +}` + ) + writeFileSync(nativeTsPath, source) return } @@ -311,6 +329,7 @@ function patchNativeWrapperPath(nativeTsPath) { function resolveNativeWrapperPath() { \tconst fileName = \`libNativeWrapper.\${suffix}\`; \tconst candidates = [ +\t\tjoin(dirname(process.argv0), fileName), \t\tjoin(dirname(process.execPath), fileName), \t\tjoin(process.cwd(), fileName), \t]; diff --git a/src/desktop/deeplink-ipc.js b/src/desktop/deeplink-ipc.js index d037192..2eb9976 100644 --- a/src/desktop/deeplink-ipc.js +++ b/src/desktop/deeplink-ipc.js @@ -204,14 +204,25 @@ async function tryForwardViaPipeWithRetries(payload) { return false } -export async function forwardDeeplinkToRunningInstance(payload) { +export async function isPrimaryInstanceReachable() { + if (process.platform === 'win32') { + return tryForwardViaPipeWithRetries({ type: 'focus' }) + } + + return isPrimaryInstanceRunning() +} + +export async function forwardDeeplinkToRunningInstance( + payload, + { allowSignalFallback = true } = {} +) { if (process.platform === 'win32') { if (await tryForwardViaPipeWithRetries(payload)) { return true } } - if (isPrimaryInstanceRunning()) { + if (allowSignalFallback && isPrimaryInstanceRunning()) { writeDeeplinkSignal(payload) return true } diff --git a/src/desktop/single-instance.js b/src/desktop/single-instance.js index 398c0e0..3ce5d13 100644 --- a/src/desktop/single-instance.js +++ b/src/desktop/single-instance.js @@ -16,6 +16,7 @@ import { getInstanceDir, getLockPath, getSignalPath, + isPrimaryInstanceReachable, isProcessAlive, SIGNAL_FILE, WINDOWS_PIPE_NAME, @@ -233,6 +234,23 @@ function startWindowsPipeServer() { pipeServer = server } +async function tryRecoverUnresponsivePrimaryLock() { + if (!isPrimaryInstanceRunning()) { + return false + } + + if (await isPrimaryInstanceReachable()) { + return false + } + + try { + unlinkSync(getLockPath()) + return true + } catch { + return false + } +} + export async function ensureSingleInstanceLock({ launchUrl } = {}) { if (process.platform === 'darwin') { return true @@ -241,7 +259,11 @@ export async function ensureSingleInstanceLock({ launchUrl } = {}) { const payload = buildLaunchPayload(launchUrl) if (process.platform === 'win32') { - if (await forwardDeeplinkToRunningInstance(payload)) { + if ( + await forwardDeeplinkToRunningInstance(payload, { + allowSignalFallback: false + }) + ) { console.log('[deeplink] forwarded duplicate launcher instance to running app') return false } @@ -251,6 +273,15 @@ export async function ensureSingleInstanceLock({ launchUrl } = {}) { return false } + if ( + (await tryRecoverUnresponsivePrimaryLock()) && + tryAcquirePrimaryLock() + ) { + startWindowsPipeServer() + startInstanceSignalWatcher() + return true + } + writeInstanceSignal(payload) return false } diff --git a/src/desktop/windows-app-paths.js b/src/desktop/windows-app-paths.js index e3511e6..f645be0 100644 --- a/src/desktop/windows-app-paths.js +++ b/src/desktop/windows-app-paths.js @@ -1,26 +1,57 @@ import { chdirSync, existsSync } from 'node:fs' -import { dirname, join } from 'node:path' +import { dirname, join, resolve } from 'node:path' const NATIVE_WRAPPER_DLL = 'libNativeWrapper.dll' -export function resolveWindowsBinDir() { - const candidates = [ - dirname(process.execPath), - process.cwd(), - join(dirname(process.execPath), 'bin'), - join(process.cwd(), 'bin') - ] +function uniquePaths(paths) { + const seen = new Set() + const result = [] - for (const candidate of candidates) { - if ( - existsSync(join(candidate, 'launcher.exe')) || - existsSync(join(candidate, NATIVE_WRAPPER_DLL)) - ) { + for (const candidate of paths) { + if (!candidate) { + continue + } + + const normalized = resolve(candidate) + if (seen.has(normalized)) { + continue + } + + seen.add(normalized) + result.push(normalized) + } + + return result +} + +function hasAppBinMarker(dir) { + return ( + existsSync(join(dir, 'launcher.exe')) || + existsSync(join(dir, NATIVE_WRAPPER_DLL)) || + existsSync(join(dir, 'bun.exe')) || + existsSync(join(dir, '..', 'Resources', 'version.json')) + ) +} + +export function resolveWindowsBinDir() { + const roots = uniquePaths([ + dirname(process.argv0), + dirname(process.execPath), + process.cwd() + ]) + + const candidates = [] + for (const root of roots) { + candidates.push(root, join(root, 'bin')) + } + + for (const candidate of uniquePaths(candidates)) { + if (hasAppBinMarker(candidate)) { return candidate } } - return dirname(process.execPath) + return dirname(process.argv0 || process.execPath) } export function ensureWindowsWorkingDirectory() { @@ -29,14 +60,13 @@ export function ensureWindowsWorkingDirectory() { } const binDir = resolveWindowsBinDir() - const wrapperInCwd = existsSync(join(process.cwd(), NATIVE_WRAPPER_DLL)) - const wrapperInBinDir = existsSync(join(binDir, NATIVE_WRAPPER_DLL)) + if (resolve(process.cwd()) === resolve(binDir)) { + return + } - if (!wrapperInCwd && wrapperInBinDir) { - try { - chdirSync(binDir) - } catch { - // Ignore if we cannot change directory. - } + try { + chdirSync(binDir) + } catch { + // Ignore if we cannot change directory. } }