Enhance single instance and deep link functionality for Windows
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Introduced a Windows pipe server to facilitate communication between instances, improving single instance management. - Updated `ensureSingleInstanceLock` to handle payload forwarding and signal writing more effectively. - Refactored deep link handling to utilize the new protocol detection logic, enhancing URL parsing and navigation. - Improved error handling and connection management for the Windows-specific implementation.
This commit is contained in:
parent
3ed7945b7c
commit
93df523b7c
@ -8,18 +8,23 @@ import {
|
||||
watch,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import net from 'node:net'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
getWindowsLaunchSources,
|
||||
getWindowsLaunchUrl
|
||||
} from './windows-launch-args.js'
|
||||
|
||||
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||
const LOCK_FILE = 'primary.lock'
|
||||
const SIGNAL_FILE = 'instance-signal.json'
|
||||
const WINDOWS_PIPE_NAME = '\\\\.\\pipe\\com.tombutcher.farmcontrol.instance'
|
||||
const FORWARD_TIMEOUT_MS = 750
|
||||
|
||||
let lockFd = null
|
||||
let pollInterval = null
|
||||
let fsWatcher = null
|
||||
let pipeServer = null
|
||||
let handlers = {
|
||||
onDeepLink: null,
|
||||
onFocus: null
|
||||
@ -111,6 +116,14 @@ function releasePrimaryLock() {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -126,8 +139,7 @@ export function findProtocolUrl(args) {
|
||||
}
|
||||
}
|
||||
|
||||
const combined = sources.join(' ')
|
||||
const combinedMatch = combined.match(/farmcontrol:\/\/\S+/i)
|
||||
const combinedMatch = sources.join(' ').match(/farmcontrol:\/\/\S+/i)
|
||||
if (combinedMatch) {
|
||||
const rawUrl = combinedMatch[0].replace(/['"]+$/g, '')
|
||||
try {
|
||||
@ -151,6 +163,11 @@ function collectLaunchSources() {
|
||||
}
|
||||
|
||||
export function captureLaunchUrl() {
|
||||
const argvUrl = findProtocolUrl(process.argv)
|
||||
if (argvUrl) {
|
||||
return argvUrl
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return getWindowsLaunchUrl() || findProtocolUrl(collectLaunchSources())
|
||||
}
|
||||
@ -159,12 +176,12 @@ export function captureLaunchUrl() {
|
||||
}
|
||||
|
||||
function buildLaunchPayload(launchUrl = captureLaunchUrl()) {
|
||||
const sources = collectLaunchSources()
|
||||
const url = launchUrl || findProtocolUrl(sources)
|
||||
const commandLine = collectLaunchSources()
|
||||
const url = launchUrl || findProtocolUrl(commandLine)
|
||||
|
||||
return url
|
||||
? { type: 'deeplink', url, argv: sources }
|
||||
: { type: 'focus', argv: sources }
|
||||
? { type: 'deeplink', url, argv: commandLine }
|
||||
: { type: 'focus', argv: commandLine }
|
||||
}
|
||||
|
||||
function readSignalQueue({ clear = true } = {}) {
|
||||
@ -274,17 +291,103 @@ function startInstanceSignalWatcher() {
|
||||
pollInterval = setInterval(processInstanceSignals, 100)
|
||||
}
|
||||
|
||||
function forwardToRunningInstance(payload) {
|
||||
if (process.platform !== 'win32') {
|
||||
return Promise.resolve(false)
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false
|
||||
const finish = (forwarded) => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
resolve(forwarded)
|
||||
}
|
||||
|
||||
const client = net.connect(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 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(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 forwardToRunningInstance(payload)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!tryAcquirePrimaryLock()) {
|
||||
if (await forwardToRunningInstance(payload)) {
|
||||
return false
|
||||
}
|
||||
|
||||
writeInstanceSignal(payload)
|
||||
return false
|
||||
}
|
||||
|
||||
startWindowsPipeServer()
|
||||
return true
|
||||
}
|
||||
|
||||
if (tryAcquirePrimaryLock()) {
|
||||
startInstanceSignalWatcher()
|
||||
return true
|
||||
}
|
||||
|
||||
writeInstanceSignal(buildLaunchPayload(launchUrl))
|
||||
writeInstanceSignal(payload)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -299,6 +402,15 @@ export function setSingleInstanceHandlers({ onDeepLink, onFocus }) {
|
||||
}
|
||||
|
||||
export function closeSingleInstanceServer() {
|
||||
if (pipeServer) {
|
||||
try {
|
||||
pipeServer.close()
|
||||
} catch {
|
||||
// Ignore close failures.
|
||||
}
|
||||
pipeServer = null
|
||||
}
|
||||
|
||||
if (pollInterval) {
|
||||
clearInterval(pollInterval)
|
||||
pollInterval = null
|
||||
|
||||
@ -10,7 +10,7 @@ const isMacOS = process.platform === 'darwin'
|
||||
|
||||
const DEV_SERVER_PORT = 5780
|
||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
||||
import { captureLaunchUrl, setSingleInstanceHandlers } from './single-instance.js'
|
||||
import { findProtocolUrl, setSingleInstanceHandlers } from './single-instance.js'
|
||||
|
||||
let mainWindow = null
|
||||
let webviewDomReady = false
|
||||
@ -90,6 +90,8 @@ export function openInternalUrl(url) {
|
||||
return true
|
||||
}
|
||||
|
||||
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||
|
||||
function parseDeepLinkPath(url) {
|
||||
if (!url || typeof url !== 'string') {
|
||||
return null
|
||||
@ -99,12 +101,11 @@ function parseDeepLinkPath(url) {
|
||||
return url
|
||||
}
|
||||
|
||||
const match = url.match(/^farmcontrol:\/\/app(.*)$/i)
|
||||
if (!match) {
|
||||
if (!url.startsWith(`${PROTOCOL_PREFIX}app`)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const redirectPath = match[1] || '/'
|
||||
const redirectPath = url.slice(`${PROTOCOL_PREFIX}app`.length) || '/'
|
||||
const normalizedPath = redirectPath.startsWith('/')
|
||||
? redirectPath
|
||||
: `/${redirectPath}`
|
||||
@ -127,11 +128,12 @@ export function handleDeepLink(url) {
|
||||
return true
|
||||
}
|
||||
|
||||
export function handleDeepLinkFromArgv(launchUrl = captureLaunchUrl()) {
|
||||
export function handleDeepLinkFromArgv(launchUrl) {
|
||||
if (process.platform === 'darwin') return
|
||||
|
||||
if (launchUrl) {
|
||||
handleDeepLink(launchUrl)
|
||||
const url = launchUrl || findProtocolUrl(process.argv)
|
||||
if (url) {
|
||||
handleDeepLink(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user