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,
|
watch,
|
||||||
writeFileSync
|
writeFileSync
|
||||||
} from 'node:fs'
|
} from 'node:fs'
|
||||||
|
import net from 'node:net'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import {
|
import {
|
||||||
getWindowsLaunchSources,
|
getWindowsLaunchSources,
|
||||||
getWindowsLaunchUrl
|
getWindowsLaunchUrl
|
||||||
} from './windows-launch-args.js'
|
} from './windows-launch-args.js'
|
||||||
|
|
||||||
|
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||||
const LOCK_FILE = 'primary.lock'
|
const LOCK_FILE = 'primary.lock'
|
||||||
const SIGNAL_FILE = 'instance-signal.json'
|
const SIGNAL_FILE = 'instance-signal.json'
|
||||||
|
const WINDOWS_PIPE_NAME = '\\\\.\\pipe\\com.tombutcher.farmcontrol.instance'
|
||||||
|
const FORWARD_TIMEOUT_MS = 750
|
||||||
|
|
||||||
let lockFd = null
|
let lockFd = null
|
||||||
let pollInterval = null
|
let pollInterval = null
|
||||||
let fsWatcher = null
|
let fsWatcher = null
|
||||||
|
let pipeServer = null
|
||||||
let handlers = {
|
let handlers = {
|
||||||
onDeepLink: null,
|
onDeepLink: null,
|
||||||
onFocus: null
|
onFocus: null
|
||||||
@ -111,6 +116,14 @@ function releasePrimaryLock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function findProtocolUrl(args) {
|
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')
|
const sources = args.filter((arg) => typeof arg === 'string')
|
||||||
|
|
||||||
for (const arg of sources) {
|
for (const arg of sources) {
|
||||||
@ -126,8 +139,7 @@ export function findProtocolUrl(args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const combined = sources.join(' ')
|
const combinedMatch = sources.join(' ').match(/farmcontrol:\/\/\S+/i)
|
||||||
const combinedMatch = combined.match(/farmcontrol:\/\/\S+/i)
|
|
||||||
if (combinedMatch) {
|
if (combinedMatch) {
|
||||||
const rawUrl = combinedMatch[0].replace(/['"]+$/g, '')
|
const rawUrl = combinedMatch[0].replace(/['"]+$/g, '')
|
||||||
try {
|
try {
|
||||||
@ -151,6 +163,11 @@ function collectLaunchSources() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function captureLaunchUrl() {
|
export function captureLaunchUrl() {
|
||||||
|
const argvUrl = findProtocolUrl(process.argv)
|
||||||
|
if (argvUrl) {
|
||||||
|
return argvUrl
|
||||||
|
}
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
return getWindowsLaunchUrl() || findProtocolUrl(collectLaunchSources())
|
return getWindowsLaunchUrl() || findProtocolUrl(collectLaunchSources())
|
||||||
}
|
}
|
||||||
@ -159,12 +176,12 @@ export function captureLaunchUrl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildLaunchPayload(launchUrl = captureLaunchUrl()) {
|
function buildLaunchPayload(launchUrl = captureLaunchUrl()) {
|
||||||
const sources = collectLaunchSources()
|
const commandLine = collectLaunchSources()
|
||||||
const url = launchUrl || findProtocolUrl(sources)
|
const url = launchUrl || findProtocolUrl(commandLine)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
? { type: 'deeplink', url, argv: sources }
|
? { type: 'deeplink', url, argv: commandLine }
|
||||||
: { type: 'focus', argv: sources }
|
: { type: 'focus', argv: commandLine }
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSignalQueue({ clear = true } = {}) {
|
function readSignalQueue({ clear = true } = {}) {
|
||||||
@ -274,17 +291,103 @@ function startInstanceSignalWatcher() {
|
|||||||
pollInterval = setInterval(processInstanceSignals, 100)
|
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 } = {}) {
|
export async function ensureSingleInstanceLock({ launchUrl } = {}) {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
return true
|
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()) {
|
if (tryAcquirePrimaryLock()) {
|
||||||
startInstanceSignalWatcher()
|
startInstanceSignalWatcher()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
writeInstanceSignal(buildLaunchPayload(launchUrl))
|
writeInstanceSignal(payload)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,6 +402,15 @@ export function setSingleInstanceHandlers({ onDeepLink, onFocus }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function closeSingleInstanceServer() {
|
export function closeSingleInstanceServer() {
|
||||||
|
if (pipeServer) {
|
||||||
|
try {
|
||||||
|
pipeServer.close()
|
||||||
|
} catch {
|
||||||
|
// Ignore close failures.
|
||||||
|
}
|
||||||
|
pipeServer = null
|
||||||
|
}
|
||||||
|
|
||||||
if (pollInterval) {
|
if (pollInterval) {
|
||||||
clearInterval(pollInterval)
|
clearInterval(pollInterval)
|
||||||
pollInterval = null
|
pollInterval = null
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const isMacOS = process.platform === 'darwin'
|
|||||||
|
|
||||||
const DEV_SERVER_PORT = 5780
|
const DEV_SERVER_PORT = 5780
|
||||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
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 mainWindow = null
|
||||||
let webviewDomReady = false
|
let webviewDomReady = false
|
||||||
@ -90,6 +90,8 @@ export function openInternalUrl(url) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||||
|
|
||||||
function parseDeepLinkPath(url) {
|
function parseDeepLinkPath(url) {
|
||||||
if (!url || typeof url !== 'string') {
|
if (!url || typeof url !== 'string') {
|
||||||
return null
|
return null
|
||||||
@ -99,12 +101,11 @@ function parseDeepLinkPath(url) {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = url.match(/^farmcontrol:\/\/app(.*)$/i)
|
if (!url.startsWith(`${PROTOCOL_PREFIX}app`)) {
|
||||||
if (!match) {
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectPath = match[1] || '/'
|
const redirectPath = url.slice(`${PROTOCOL_PREFIX}app`.length) || '/'
|
||||||
const normalizedPath = redirectPath.startsWith('/')
|
const normalizedPath = redirectPath.startsWith('/')
|
||||||
? redirectPath
|
? redirectPath
|
||||||
: `/${redirectPath}`
|
: `/${redirectPath}`
|
||||||
@ -127,11 +128,12 @@ export function handleDeepLink(url) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDeepLinkFromArgv(launchUrl = captureLaunchUrl()) {
|
export function handleDeepLinkFromArgv(launchUrl) {
|
||||||
if (process.platform === 'darwin') return
|
if (process.platform === 'darwin') return
|
||||||
|
|
||||||
if (launchUrl) {
|
const url = launchUrl || findProtocolUrl(process.argv)
|
||||||
handleDeepLink(launchUrl)
|
if (url) {
|
||||||
|
handleDeepLink(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user