All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Added functions to manage lock files and check if the primary instance is running, improving instance handling. - Implemented retry logic for forwarding deeplinks via Windows pipes, enhancing reliability in communication between instances. - Refactored existing functions to streamline the deeplink forwarding process and improve code clarity.
308 lines
6.0 KiB
JavaScript
308 lines
6.0 KiB
JavaScript
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()
|
|
}
|