import { closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, watch, writeFileSync } from 'node:fs' import net from 'node:net' import { buildDeeplinkPayload, findProtocolUrl, forwardDeeplinkToRunningInstance, getInstanceDir, getLockPath, getSignalPath, isProcessAlive, SIGNAL_FILE, WINDOWS_PIPE_NAME, writeDeeplinkSignal } from './deeplink-ipc.js' let lockFd = null let pollInterval = null let fsWatcher = null let pipeServer = null let handlers = { onDeepLink: null, onFocus: null } const pendingMessages = [] function tryAcquirePrimaryLock() { mkdirSync(getInstanceDir(), { recursive: true }) const lockPath = getLockPath() if (existsSync(lockPath)) { try { const existingPid = Number.parseInt(readFileSync(lockPath, 'utf8').trim(), 10) if (existingPid === process.pid) { return true } if (isProcessAlive(existingPid)) { return false } unlinkSync(lockPath) } catch { try { unlinkSync(lockPath) } catch { // Ignore cleanup failures. } } } try { lockFd = openSync(lockPath, 'wx') writeFileSync(lockFd, String(process.pid)) return true } catch { return false } } function releasePrimaryLock() { if (lockFd !== null) { try { closeSync(lockFd) } catch { // Ignore close failures. } lockFd = null } try { unlinkSync(getLockPath()) } catch { // Ignore cleanup failures. } } export { findProtocolUrl } export function captureLaunchUrl() { return findProtocolUrl(process.argv) } function buildLaunchPayload(launchUrl = captureLaunchUrl()) { const commandLine = [...process.argv] const url = launchUrl || findProtocolUrl(commandLine) return buildDeeplinkPayload(url, 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 [] } } function writeInstanceSignal(payload) { writeDeeplinkSignal(payload) } function resolveIncomingMessage(message) { if (!message || typeof message !== 'object') { return null } if (message.type === 'deeplink') { const url = message.url || findProtocolUrl(message.argv || []) return url ? { type: 'deeplink', url } : { type: 'focus' } } if (message.type === 'focus') { return { type: 'focus' } } return null } function dispatchMessage(message) { const resolved = resolveIncomingMessage(message) if (!resolved) return if (process.platform === 'win32') { console.log('[deeplink][dispatch]', message) console.log('[deeplink][dispatch] resolved:', resolved) } if (!handlers.onDeepLink && !handlers.onFocus) { pendingMessages.push(resolved) return } if (resolved.type === 'deeplink' && resolved.url) { handlers.onDeepLink?.(resolved.url) return } if (resolved.type === 'focus') { handlers.onFocus?.() } } function processInstanceSignals() { for (const message of readSignalQueue({ clear: true })) { dispatchMessage(message) } } function startInstanceSignalWatcher() { if (process.platform === 'darwin' || pollInterval) { return } mkdirSync(getInstanceDir(), { recursive: true }) processInstanceSignals() try { fsWatcher = watch(getInstanceDir(), (_event, filename) => { if (!filename || filename === SIGNAL_FILE) { setTimeout(processInstanceSignals, 25) } }) } catch { // Polling below covers filesystems without reliable watch support. } pollInterval = setInterval(processInstanceSignals, 100) } function startWindowsPipeServer() { if (process.platform !== 'win32' || pipeServer) { return } const server = net.createServer((socket) => { let buffer = '' socket.on('data', (chunk) => { buffer += chunk.toString() }) socket.on('end', () => { if (!buffer) return try { dispatchMessage(JSON.parse(buffer)) } catch { // Ignore malformed payloads. } }) }) server.on('error', (error) => { if (error?.code !== 'EADDRINUSE') { console.warn('[single-instance] pipe server error:', error) } }) server.listen({ path: WINDOWS_PIPE_NAME }) pipeServer = server } export async function ensureSingleInstanceLock({ launchUrl } = {}) { if (process.platform === 'darwin') { return true } const payload = buildLaunchPayload(launchUrl) if (process.platform === 'win32') { if (await forwardDeeplinkToRunningInstance(payload)) { console.log('[deeplink] forwarded duplicate launcher instance to running app') return false } if (!tryAcquirePrimaryLock()) { if (await forwardDeeplinkToRunningInstance(payload)) { return false } writeInstanceSignal(payload) return false } startWindowsPipeServer() startInstanceSignalWatcher() return true } if (tryAcquirePrimaryLock()) { startInstanceSignalWatcher() return true } writeInstanceSignal(payload) return false } export function setSingleInstanceHandlers({ onDeepLink, onFocus }) { handlers = { onDeepLink, onFocus } while (pendingMessages.length > 0) { dispatchMessage(pendingMessages.shift()) } processInstanceSignals() } export function closeSingleInstanceServer() { if (pipeServer) { try { pipeServer.close() } catch { // Ignore close failures. } pipeServer = null } if (pollInterval) { clearInterval(pollInterval) pollInterval = null } if (fsWatcher) { try { fsWatcher.close() } catch { // Ignore close failures. } fsWatcher = null } releasePrimaryLock() }