import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs' import net from 'node:net' import os from 'node:os' 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') { return join( os.homedir(), 'AppData', 'Local', 'com.tombutcher.farmcontrol', 'instance' ) } return join(process.env.TMPDIR || '/tmp', 'com.tombutcher.farmcontrol', 'instance') } 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) ) if (directMatch) { return directMatch } const sources = args.filter((arg) => typeof arg === 'string') for (const arg of sources) { const trimmed = arg.trim().replace(/^['"]+|['"]+$/g, '') const match = trimmed.match(/farmcontrol:\/\/\S+/i) if (match) { const rawUrl = match[0].replace(/['"]+$/g, '') try { return decodeURI(rawUrl) } catch { return rawUrl } } } const combinedMatch = sources.join(' ').match(/farmcontrol:\/\/\S+/i) if (combinedMatch) { const rawUrl = combinedMatch[0].replace(/['"]+$/g, '') try { return decodeURI(rawUrl) } catch { return rawUrl } } return undefined } export function buildDeeplinkPayload(url, argv = process.argv) { const commandLine = [...argv] return url ? { type: 'deeplink', url, argv: commandLine } : { type: 'focus', argv: commandLine } } function readSignalQueue({ clear = true } = {}) { const signalPath = getSignalPath() if (!existsSync(signalPath)) { return [] } try { const raw = readFileSync(signalPath, 'utf8') const parsed = JSON.parse(raw) if (clear) { unlinkSync(signalPath) } if (Array.isArray(parsed)) { return parsed } return parsed ? [parsed] : [] } catch { if (clear) { try { unlinkSync(signalPath) } catch { // Ignore cleanup failures. } } return [] } } export function writeDeeplinkSignal(payload) { const signalPath = getSignalPath() mkdirSync(getInstanceDir(), { recursive: true }) const queue = readSignalQueue({ clear: false }) queue.push({ ...payload, timestamp: Date.now() }) const tempPath = `${signalPath}.${process.pid}.${Date.now()}.tmp` writeFileSync(tempPath, JSON.stringify(queue), 'utf8') writeFileSync(signalPath, readFileSync(tempPath)) unlinkSync(tempPath) } function tryForwardViaPipe(payload) { return new Promise((resolve) => { let settled = false const finish = (forwarded) => { if (settled) return settled = true resolve(forwarded) } const client = net.connect({ path: WINDOWS_PIPE_NAME }) const message = JSON.stringify(payload) client.on('connect', () => { client.write(message) client.end() finish(true) }) client.on('error', () => { finish(false) }) client.setTimeout(FORWARD_TIMEOUT_MS, () => { client.destroy() finish(false) }) }) } 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 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 (allowSignalFallback && isPrimaryInstanceRunning()) { writeDeeplinkSignal(payload) return true } return false }