diff --git a/src/desktop/deeplink-ipc.js b/src/desktop/deeplink-ipc.js index a6f42a3..c6b4e2a 100644 --- a/src/desktop/deeplink-ipc.js +++ b/src/desktop/deeplink-ipc.js @@ -11,7 +11,10 @@ import { join } from 'node:path' export const PROTOCOL_PREFIX = 'farmcontrol://' export const WINDOWS_PIPE_NAME = '\\\\.\\pipe\\com.tombutcher.farmcontrol.instance' export const SIGNAL_FILE = 'instance-signal.json' +export const LOCK_FILE = 'primary.lock' const FORWARD_TIMEOUT_MS = 750 +const PIPE_RETRY_ATTEMPTS = 3 +const PIPE_RETRY_DELAY_MS = 100 export function getInstanceDir() { if (process.platform === 'win32') { @@ -28,6 +31,37 @@ export function getSignalPath() { return join(getInstanceDir(), SIGNAL_FILE) } +export function getLockPath() { + return join(getInstanceDir(), LOCK_FILE) +} + +export function isProcessAlive(pid) { + if (!Number.isInteger(pid) || pid <= 0) { + return false + } + + try { + process.kill(pid, 0) + return true + } catch { + return false + } +} + +export function isPrimaryInstanceRunning() { + const lockPath = getLockPath() + if (!existsSync(lockPath)) { + return false + } + + try { + const existingPid = Number.parseInt(readFileSync(lockPath, 'utf8').trim(), 10) + return isProcessAlive(existingPid) + } catch { + return false + } +} + export function findProtocolUrl(args) { const directMatch = args.find( (arg) => typeof arg === 'string' && arg.startsWith(PROTOCOL_PREFIX) @@ -117,7 +151,7 @@ export function writeDeeplinkSignal(payload) { unlinkSync(tempPath) } -export function forwardDeeplinkToRunningInstance(payload) { +function tryForwardViaPipe(payload) { return new Promise((resolve) => { let settled = false const finish = (forwarded) => { @@ -126,7 +160,7 @@ export function forwardDeeplinkToRunningInstance(payload) { resolve(forwarded) } - const client = net.connect(WINDOWS_PIPE_NAME) + const client = net.connect({ path: WINDOWS_PIPE_NAME }) const message = JSON.stringify(payload) client.on('connect', () => { @@ -145,3 +179,38 @@ export function forwardDeeplinkToRunningInstance(payload) { }) }) } + +function delay(ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +async function tryForwardViaPipeWithRetries(payload) { + for (let attempt = 0; attempt < PIPE_RETRY_ATTEMPTS; attempt += 1) { + if (await tryForwardViaPipe(payload)) { + return true + } + + if (attempt < PIPE_RETRY_ATTEMPTS - 1) { + await delay(PIPE_RETRY_DELAY_MS) + } + } + + return false +} + +export async function forwardDeeplinkToRunningInstance(payload) { + if (process.platform === 'win32') { + if (await tryForwardViaPipeWithRetries(payload)) { + return true + } + } + + if (isPrimaryInstanceRunning()) { + writeDeeplinkSignal(payload) + return true + } + + return false +} diff --git a/src/desktop/single-instance.js b/src/desktop/single-instance.js index 6751467..398c0e0 100644 --- a/src/desktop/single-instance.js +++ b/src/desktop/single-instance.js @@ -9,20 +9,19 @@ import { writeFileSync } from 'node:fs' import net from 'node:net' -import { join } from 'node:path' import { buildDeeplinkPayload, findProtocolUrl, forwardDeeplinkToRunningInstance, getInstanceDir, + getLockPath, getSignalPath, + isProcessAlive, SIGNAL_FILE, WINDOWS_PIPE_NAME, writeDeeplinkSignal } from './deeplink-ipc.js' -const LOCK_FILE = 'primary.lock' - let lockFd = null let pollInterval = null let fsWatcher = null @@ -33,23 +32,6 @@ let handlers = { } const pendingMessages = [] -function getLockPath() { - return join(getInstanceDir(), LOCK_FILE) -} - -function isProcessAlive(pid) { - if (!Number.isInteger(pid) || pid <= 0) { - return false - } - - try { - process.kill(pid, 0) - return true - } catch { - return false - } -} - function tryAcquirePrimaryLock() { mkdirSync(getInstanceDir(), { recursive: true }) const lockPath = getLockPath() @@ -247,7 +229,7 @@ function startWindowsPipeServer() { } }) - server.listen(WINDOWS_PIPE_NAME) + server.listen({ path: WINDOWS_PIPE_NAME }) pipeServer = server } @@ -274,6 +256,7 @@ export async function ensureSingleInstanceLock({ launchUrl } = {}) { } startWindowsPipeServer() + startInstanceSignalWatcher() return true }