Enhance Windows instance management and path resolution
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit

- 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.
This commit is contained in:
Tom Butcher 2026-08-03 00:21:04 +01:00
parent e1051499f9
commit de93658df1
4 changed files with 116 additions and 25 deletions

View File

@ -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];

View File

@ -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
}

View File

@ -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
}

View File

@ -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.
}
}