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 {
|
||||
captureLaunchUrl,
|
||||
closeSingleInstanceServer,
|
||||
ensureSingleInstanceLock
|
||||
} from '../desktop/single-instance.js'
|
||||
|
||||
const gotSingleInstanceLock = await ensureSingleInstanceLock()
|
||||
const launchUrl = captureLaunchUrl()
|
||||
const gotSingleInstanceLock = await ensureSingleInstanceLock({ launchUrl })
|
||||
|
||||
if (!gotSingleInstanceLock) {
|
||||
process.exit(0)
|
||||
@ -22,14 +24,15 @@ const {
|
||||
setupWindowsDeepLinkHandling
|
||||
} = await import('../desktop/window.js')
|
||||
|
||||
setupWindowsDeepLinkHandling()
|
||||
|
||||
const rpc = createAppRpc()
|
||||
const mainWindow = await createMainWindow(rpc)
|
||||
|
||||
setupWindowsDeepLinkHandling()
|
||||
setupNavigationGestures(mainWindow)
|
||||
registerGlobalShortcuts(rpc)
|
||||
setupDevAuthServer()
|
||||
handleDeepLinkFromArgv()
|
||||
handleDeepLinkFromArgv(launchUrl)
|
||||
|
||||
process.on('exit', () => {
|
||||
unregisterGlobalShortcuts()
|
||||
|
||||
@ -9,7 +9,10 @@ import {
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
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 SIGNAL_FILE = 'instance-signal.json'
|
||||
@ -147,45 +150,67 @@ function collectLaunchSources() {
|
||||
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 url = findProtocolUrl(sources)
|
||||
const url = launchUrl || findProtocolUrl(sources)
|
||||
|
||||
return url
|
||||
? { type: 'deeplink', url, 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) {
|
||||
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({ ...payload, timestamp: Date.now() }), 'utf8')
|
||||
writeFileSync(tempPath, JSON.stringify(queue), 'utf8')
|
||||
writeFileSync(signalPath, readFileSync(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) {
|
||||
if (!message || typeof message !== 'object') {
|
||||
return null
|
||||
@ -222,8 +247,10 @@ function dispatchMessage(message) {
|
||||
}
|
||||
}
|
||||
|
||||
function processInstanceSignal() {
|
||||
dispatchMessage(readAndClearInstanceSignal())
|
||||
function processInstanceSignals() {
|
||||
for (const message of readSignalQueue({ clear: true })) {
|
||||
dispatchMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
function startInstanceSignalWatcher() {
|
||||
@ -232,22 +259,22 @@ function startInstanceSignalWatcher() {
|
||||
}
|
||||
|
||||
mkdirSync(getInstanceDir(), { recursive: true })
|
||||
processInstanceSignal()
|
||||
processInstanceSignals()
|
||||
|
||||
try {
|
||||
fsWatcher = watch(getInstanceDir(), (_event, filename) => {
|
||||
if (!filename || filename === SIGNAL_FILE) {
|
||||
setTimeout(processInstanceSignal, 25)
|
||||
setTimeout(processInstanceSignals, 25)
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// 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') {
|
||||
return true
|
||||
}
|
||||
@ -257,7 +284,7 @@ export async function ensureSingleInstanceLock() {
|
||||
return true
|
||||
}
|
||||
|
||||
writeInstanceSignal(buildLaunchPayload())
|
||||
writeInstanceSignal(buildLaunchPayload(launchUrl))
|
||||
return false
|
||||
}
|
||||
|
||||
@ -268,7 +295,7 @@ export function setSingleInstanceHandlers({ onDeepLink, onFocus }) {
|
||||
dispatchMessage(pendingMessages.shift())
|
||||
}
|
||||
|
||||
processInstanceSignal()
|
||||
processInstanceSignals()
|
||||
}
|
||||
|
||||
export function closeSingleInstanceServer() {
|
||||
|
||||
@ -10,8 +10,7 @@ const isMacOS = process.platform === 'darwin'
|
||||
|
||||
const DEV_SERVER_PORT = 5780
|
||||
const DEV_SERVER_URL = `http://localhost:${DEV_SERVER_PORT}`
|
||||
import { findProtocolUrl, setSingleInstanceHandlers } from './single-instance.js'
|
||||
import { getWindowsLaunchSources } from './windows-launch-args.js'
|
||||
import { captureLaunchUrl, setSingleInstanceHandlers } from './single-instance.js'
|
||||
|
||||
let mainWindow = null
|
||||
let webviewDomReady = false
|
||||
@ -128,16 +127,12 @@ export function handleDeepLink(url) {
|
||||
return true
|
||||
}
|
||||
|
||||
export function handleDeepLinkFromArgv() {
|
||||
export function handleDeepLinkFromArgv(launchUrl = captureLaunchUrl()) {
|
||||
if (process.platform === 'darwin') return
|
||||
|
||||
const sources = [...process.argv]
|
||||
if (process.platform === 'win32') {
|
||||
sources.push(...getWindowsLaunchSources())
|
||||
if (launchUrl) {
|
||||
handleDeepLink(launchUrl)
|
||||
}
|
||||
|
||||
const url = findProtocolUrl(sources)
|
||||
if (url) handleDeepLink(url)
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
const proc = Bun.spawnSync({
|
||||
cmd: ['cmd.exe', '/c', command],
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe'
|
||||
stderr: 'pipe',
|
||||
windowsHide: true
|
||||
})
|
||||
|
||||
if (proc.exitCode !== 0) {
|
||||
@ -36,51 +71,38 @@ function getParentProcessId(pid) {
|
||||
return parentPid ? Number.parseInt(parentPid, 10) : null
|
||||
}
|
||||
|
||||
function getPowerShellCommandLines() {
|
||||
try {
|
||||
const proc = Bun.spawnSync({
|
||||
cmd: [
|
||||
'powershell.exe',
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
`$pid = $PID
|
||||
for ($i = 0; $i -lt 6; $i++) {
|
||||
function getPowerShellProcessTreeCommandLines() {
|
||||
const output = runPowerShell(`
|
||||
$pid = $PID
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
for ($i = 0; $i -lt 12; $i++) {
|
||||
$proc = Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue
|
||||
if (-not $proc) { break }
|
||||
if ($proc.CommandLine) { $proc.CommandLine }
|
||||
if ($proc.CommandLine -match 'farmcontrol://') { break }
|
||||
if (-not $proc.ParentProcessId) { break }
|
||||
if ($proc.CommandLine) {
|
||||
$lines.Add([string]$proc.CommandLine)
|
||||
if ($proc.CommandLine -match 'farmcontrol://') { break }
|
||||
}
|
||||
if (-not $proc.ParentProcessId -or $proc.ParentProcessId -eq $pid) { break }
|
||||
$pid = $proc.ParentProcessId
|
||||
}`
|
||||
],
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe'
|
||||
})
|
||||
}
|
||||
$lines -join [Environment]::NewLine
|
||||
`)
|
||||
|
||||
if (proc.exitCode !== 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return proc.stdout
|
||||
.toString()
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
} catch {
|
||||
if (!output) {
|
||||
return []
|
||||
}
|
||||
|
||||
return output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
export function getWindowsLaunchSources() {
|
||||
if (process.platform !== 'win32') {
|
||||
return []
|
||||
}
|
||||
|
||||
function getWmicProcessTreeCommandLines() {
|
||||
const sources = []
|
||||
let pid = process.pid
|
||||
|
||||
for (let depth = 0; depth < 6; depth += 1) {
|
||||
for (let depth = 0; depth < 12; depth += 1) {
|
||||
const commandLine = getProcessCommandLine(pid)
|
||||
if (commandLine) {
|
||||
sources.push(commandLine)
|
||||
@ -97,9 +119,69 @@ export function getWindowsLaunchSources() {
|
||||
pid = parentPid
|
||||
}
|
||||
|
||||
if (sources.length === 0) {
|
||||
return getPowerShellCommandLines()
|
||||
}
|
||||
|
||||
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