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.
232 lines
4.9 KiB
JavaScript
232 lines
4.9 KiB
JavaScript
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
|
|
}
|