Enhance single instance and deep link handling
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 `captureLaunchUrl` to retrieve launch URLs for Windows, improving deep link processing. - Updated `ensureSingleInstanceLock` to accept a launch URL, enhancing instance management. - Refactored deep link handling functions to utilize the captured launch URL, ensuring better navigation and focus management. - Improved signal processing by consolidating instance signal handling functions for better clarity and efficiency.
This commit is contained in:
parent
76ec8389c2
commit
3ed7945b7c
@ -1,9 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
|
captureLaunchUrl,
|
||||||
closeSingleInstanceServer,
|
closeSingleInstanceServer,
|
||||||
ensureSingleInstanceLock
|
ensureSingleInstanceLock
|
||||||
} from '../desktop/single-instance.js'
|
} from '../desktop/single-instance.js'
|
||||||
|
|
||||||
const gotSingleInstanceLock = await ensureSingleInstanceLock()
|
const launchUrl = captureLaunchUrl()
|
||||||
|
const gotSingleInstanceLock = await ensureSingleInstanceLock({ launchUrl })
|
||||||
|
|
||||||
if (!gotSingleInstanceLock) {
|
if (!gotSingleInstanceLock) {
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
@ -22,14 +24,15 @@ const {
|
|||||||
setupWindowsDeepLinkHandling
|
setupWindowsDeepLinkHandling
|
||||||
} = await import('../desktop/window.js')
|
} = await import('../desktop/window.js')
|
||||||
|
|
||||||
|
setupWindowsDeepLinkHandling()
|
||||||
|
|
||||||
const rpc = createAppRpc()
|
const rpc = createAppRpc()
|
||||||
const mainWindow = await createMainWindow(rpc)
|
const mainWindow = await createMainWindow(rpc)
|
||||||
|
|
||||||
setupWindowsDeepLinkHandling()
|
|
||||||
setupNavigationGestures(mainWindow)
|
setupNavigationGestures(mainWindow)
|
||||||
registerGlobalShortcuts(rpc)
|
registerGlobalShortcuts(rpc)
|
||||||
setupDevAuthServer()
|
setupDevAuthServer()
|
||||||
handleDeepLinkFromArgv()
|
handleDeepLinkFromArgv(launchUrl)
|
||||||
|
|
||||||
process.on('exit', () => {
|
process.on('exit', () => {
|
||||||
unregisterGlobalShortcuts()
|
unregisterGlobalShortcuts()
|
||||||
|
|||||||
@ -9,7 +9,10 @@ import {
|
|||||||
writeFileSync
|
writeFileSync
|
||||||
} from 'node:fs'
|
} from 'node:fs'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import { getWindowsLaunchSources } from './windows-launch-args.js'
|
import {
|
||||||
|
getWindowsLaunchSources,
|
||||||
|
getWindowsLaunchUrl
|
||||||
|
} from './windows-launch-args.js'
|
||||||
|
|
||||||
const LOCK_FILE = 'primary.lock'
|
const LOCK_FILE = 'primary.lock'
|
||||||
const SIGNAL_FILE = 'instance-signal.json'
|
const SIGNAL_FILE = 'instance-signal.json'
|
||||||
@ -147,45 +150,67 @@ function collectLaunchSources() {
|
|||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLaunchPayload() {
|
export function captureLaunchUrl() {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return getWindowsLaunchUrl() || findProtocolUrl(collectLaunchSources())
|
||||||
|
}
|
||||||
|
|
||||||
|
return findProtocolUrl(collectLaunchSources())
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildLaunchPayload(launchUrl = captureLaunchUrl()) {
|
||||||
const sources = collectLaunchSources()
|
const sources = collectLaunchSources()
|
||||||
const url = findProtocolUrl(sources)
|
const url = launchUrl || findProtocolUrl(sources)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
? { type: 'deeplink', url, argv: sources }
|
? { type: 'deeplink', url, argv: sources }
|
||||||
: { type: 'focus', argv: sources }
|
: { type: 'focus', argv: sources }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function writeInstanceSignal(payload) {
|
||||||
const signalPath = getSignalPath()
|
const signalPath = getSignalPath()
|
||||||
mkdirSync(getInstanceDir(), { recursive: true })
|
mkdirSync(getInstanceDir(), { recursive: true })
|
||||||
|
|
||||||
|
const queue = readSignalQueue({ clear: false })
|
||||||
|
queue.push({ ...payload, timestamp: Date.now() })
|
||||||
|
|
||||||
const tempPath = `${signalPath}.${process.pid}.${Date.now()}.tmp`
|
const tempPath = `${signalPath}.${process.pid}.${Date.now()}.tmp`
|
||||||
writeFileSync(tempPath, JSON.stringify({ ...payload, timestamp: Date.now() }), 'utf8')
|
writeFileSync(tempPath, JSON.stringify(queue), 'utf8')
|
||||||
writeFileSync(signalPath, readFileSync(tempPath))
|
writeFileSync(signalPath, readFileSync(tempPath))
|
||||||
unlinkSync(tempPath)
|
unlinkSync(tempPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
function readAndClearInstanceSignal() {
|
|
||||||
const signalPath = getSignalPath()
|
|
||||||
if (!existsSync(signalPath)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload = JSON.parse(readFileSync(signalPath, 'utf8'))
|
|
||||||
unlinkSync(signalPath)
|
|
||||||
return payload
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
unlinkSync(signalPath)
|
|
||||||
} catch {
|
|
||||||
// Ignore cleanup failures.
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveIncomingMessage(message) {
|
function resolveIncomingMessage(message) {
|
||||||
if (!message || typeof message !== 'object') {
|
if (!message || typeof message !== 'object') {
|
||||||
return null
|
return null
|
||||||
@ -222,8 +247,10 @@ function dispatchMessage(message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processInstanceSignal() {
|
function processInstanceSignals() {
|
||||||
dispatchMessage(readAndClearInstanceSignal())
|
for (const message of readSignalQueue({ clear: true })) {
|
||||||
|
dispatchMessage(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startInstanceSignalWatcher() {
|
function startInstanceSignalWatcher() {
|
||||||
@ -232,22 +259,22 @@ function startInstanceSignalWatcher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mkdirSync(getInstanceDir(), { recursive: true })
|
mkdirSync(getInstanceDir(), { recursive: true })
|
||||||
processInstanceSignal()
|
processInstanceSignals()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fsWatcher = watch(getInstanceDir(), (_event, filename) => {
|
fsWatcher = watch(getInstanceDir(), (_event, filename) => {
|
||||||
if (!filename || filename === SIGNAL_FILE) {
|
if (!filename || filename === SIGNAL_FILE) {
|
||||||
setTimeout(processInstanceSignal, 25)
|
setTimeout(processInstanceSignals, 25)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
// Polling below covers filesystems without reliable watch support.
|
// Polling below covers filesystems without reliable watch support.
|
||||||
}
|
}
|
||||||
|
|
||||||
pollInterval = setInterval(processInstanceSignal, 100)
|
pollInterval = setInterval(processInstanceSignals, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureSingleInstanceLock() {
|
export async function ensureSingleInstanceLock({ launchUrl } = {}) {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -257,7 +284,7 @@ export async function ensureSingleInstanceLock() {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
writeInstanceSignal(buildLaunchPayload())
|
writeInstanceSignal(buildLaunchPayload(launchUrl))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,7 +295,7 @@ export function setSingleInstanceHandlers({ onDeepLink, onFocus }) {
|
|||||||
dispatchMessage(pendingMessages.shift())
|
dispatchMessage(pendingMessages.shift())
|
||||||
}
|
}
|
||||||
|
|
||||||
processInstanceSignal()
|
processInstanceSignals()
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeSingleInstanceServer() {
|
export function closeSingleInstanceServer() {
|
||||||
|
|||||||
@ -10,8 +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 { findProtocolUrl, setSingleInstanceHandlers } from './single-instance.js'
|
import { captureLaunchUrl, setSingleInstanceHandlers } from './single-instance.js'
|
||||||
import { getWindowsLaunchSources } from './windows-launch-args.js'
|
|
||||||
|
|
||||||
let mainWindow = null
|
let mainWindow = null
|
||||||
let webviewDomReady = false
|
let webviewDomReady = false
|
||||||
@ -128,16 +127,12 @@ export function handleDeepLink(url) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleDeepLinkFromArgv() {
|
export function handleDeepLinkFromArgv(launchUrl = captureLaunchUrl()) {
|
||||||
if (process.platform === 'darwin') return
|
if (process.platform === 'darwin') return
|
||||||
|
|
||||||
const sources = [...process.argv]
|
if (launchUrl) {
|
||||||
if (process.platform === 'win32') {
|
handleDeepLink(launchUrl)
|
||||||
sources.push(...getWindowsLaunchSources())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = findProtocolUrl(sources)
|
|
||||||
if (url) handleDeepLink(url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastWindowState() {
|
function broadcastWindowState() {
|
||||||
|
|||||||
@ -1,9 +1,44 @@
|
|||||||
|
function hasProtocolUrl(sources) {
|
||||||
|
return sources.some(
|
||||||
|
(source) =>
|
||||||
|
typeof source === 'string' && /farmcontrol:\/\//i.test(source)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPowerShell(script) {
|
||||||
|
try {
|
||||||
|
const proc = Bun.spawnSync({
|
||||||
|
cmd: [
|
||||||
|
'powershell.exe',
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'Bypass',
|
||||||
|
'-Command',
|
||||||
|
script
|
||||||
|
],
|
||||||
|
stdout: 'pipe',
|
||||||
|
stderr: 'pipe',
|
||||||
|
windowsHide: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (proc.exitCode !== 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return proc.stdout.toString().trim()
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function runCommand(command) {
|
function runCommand(command) {
|
||||||
try {
|
try {
|
||||||
const proc = Bun.spawnSync({
|
const proc = Bun.spawnSync({
|
||||||
cmd: ['cmd.exe', '/c', command],
|
cmd: ['cmd.exe', '/c', command],
|
||||||
stdout: 'pipe',
|
stdout: 'pipe',
|
||||||
stderr: 'pipe'
|
stderr: 'pipe',
|
||||||
|
windowsHide: true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (proc.exitCode !== 0) {
|
if (proc.exitCode !== 0) {
|
||||||
@ -36,51 +71,38 @@ function getParentProcessId(pid) {
|
|||||||
return parentPid ? Number.parseInt(parentPid, 10) : null
|
return parentPid ? Number.parseInt(parentPid, 10) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPowerShellCommandLines() {
|
function getPowerShellProcessTreeCommandLines() {
|
||||||
try {
|
const output = runPowerShell(`
|
||||||
const proc = Bun.spawnSync({
|
$pid = $PID
|
||||||
cmd: [
|
$lines = New-Object System.Collections.Generic.List[string]
|
||||||
'powershell.exe',
|
for ($i = 0; $i -lt 12; $i++) {
|
||||||
'-NoProfile',
|
|
||||||
'-NonInteractive',
|
|
||||||
'-Command',
|
|
||||||
`$pid = $PID
|
|
||||||
for ($i = 0; $i -lt 6; $i++) {
|
|
||||||
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue
|
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue
|
||||||
if (-not $proc) { break }
|
if (-not $proc) { break }
|
||||||
if ($proc.CommandLine) { $proc.CommandLine }
|
if ($proc.CommandLine) {
|
||||||
if ($proc.CommandLine -match 'farmcontrol://') { break }
|
$lines.Add([string]$proc.CommandLine)
|
||||||
if (-not $proc.ParentProcessId) { break }
|
if ($proc.CommandLine -match 'farmcontrol://') { break }
|
||||||
|
}
|
||||||
|
if (-not $proc.ParentProcessId -or $proc.ParentProcessId -eq $pid) { break }
|
||||||
$pid = $proc.ParentProcessId
|
$pid = $proc.ParentProcessId
|
||||||
}`
|
}
|
||||||
],
|
$lines -join [Environment]::NewLine
|
||||||
stdout: 'pipe',
|
`)
|
||||||
stderr: 'pipe'
|
|
||||||
})
|
|
||||||
|
|
||||||
if (proc.exitCode !== 0) {
|
if (!output) {
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return proc.stdout
|
|
||||||
.toString()
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
} catch {
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWindowsLaunchSources() {
|
function getWmicProcessTreeCommandLines() {
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const sources = []
|
const sources = []
|
||||||
let pid = process.pid
|
let pid = process.pid
|
||||||
|
|
||||||
for (let depth = 0; depth < 6; depth += 1) {
|
for (let depth = 0; depth < 12; depth += 1) {
|
||||||
const commandLine = getProcessCommandLine(pid)
|
const commandLine = getProcessCommandLine(pid)
|
||||||
if (commandLine) {
|
if (commandLine) {
|
||||||
sources.push(commandLine)
|
sources.push(commandLine)
|
||||||
@ -97,9 +119,69 @@ export function getWindowsLaunchSources() {
|
|||||||
pid = parentPid
|
pid = parentPid
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sources.length === 0) {
|
|
||||||
return getPowerShellCommandLines()
|
|
||||||
}
|
|
||||||
|
|
||||||
return sources
|
return sources
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEnvironmentLaunchSources() {
|
||||||
|
return Object.values(process.env).filter(
|
||||||
|
(value) => typeof value === 'string' && /farmcontrol:\/\//i.test(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWindowsLaunchSources() {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = new Set([
|
||||||
|
...process.argv,
|
||||||
|
...getEnvironmentLaunchSources()
|
||||||
|
])
|
||||||
|
|
||||||
|
for (const commandLine of getPowerShellProcessTreeCommandLines()) {
|
||||||
|
sources.add(commandLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasProtocolUrl([...sources])) {
|
||||||
|
for (const commandLine of getWmicProcessTreeCommandLines()) {
|
||||||
|
sources.add(commandLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...sources]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getWindowsLaunchUrl() {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const sources = getWindowsLaunchSources()
|
||||||
|
|
||||||
|
for (const source of sources) {
|
||||||
|
if (typeof source !== 'string') continue
|
||||||
|
|
||||||
|
const trimmed = source.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
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user