Implement single instance management and enhance 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
- Added single instance server functionality to prevent multiple app instances. - Introduced `ensureSingleInstanceLock` and `closeSingleInstanceServer` methods for managing instance locks. - Updated main application entry point to handle deep links and focus events. - Enhanced window management by showing the main window on focus and handling deep links appropriately.
This commit is contained in:
parent
1b77428a8a
commit
611cd4aad5
@ -1,15 +1,30 @@
|
|||||||
import { createAppRpc } from '../desktop/rpc.js'
|
import { createAppRpc } from '../desktop/rpc.js'
|
||||||
|
import {
|
||||||
|
closeSingleInstanceServer,
|
||||||
|
ensureSingleInstanceLock
|
||||||
|
} from '../desktop/single-instance.js'
|
||||||
import {
|
import {
|
||||||
registerGlobalShortcuts,
|
registerGlobalShortcuts,
|
||||||
unregisterGlobalShortcuts
|
unregisterGlobalShortcuts
|
||||||
} from '../desktop/spotlight.js'
|
} from '../desktop/spotlight.js'
|
||||||
import {
|
import {
|
||||||
createMainWindow,
|
createMainWindow,
|
||||||
|
handleDeepLink,
|
||||||
handleDeepLinkFromArgv,
|
handleDeepLinkFromArgv,
|
||||||
setupDevAuthServer,
|
setupDevAuthServer,
|
||||||
setupNavigationGestures
|
setupNavigationGestures,
|
||||||
|
showMainWindow
|
||||||
} from '../desktop/window.js'
|
} from '../desktop/window.js'
|
||||||
|
|
||||||
|
const gotSingleInstanceLock = await ensureSingleInstanceLock({
|
||||||
|
onDeepLink: handleDeepLink,
|
||||||
|
onFocus: showMainWindow
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!gotSingleInstanceLock) {
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
const rpc = createAppRpc()
|
const rpc = createAppRpc()
|
||||||
const mainWindow = await createMainWindow(rpc)
|
const mainWindow = await createMainWindow(rpc)
|
||||||
|
|
||||||
@ -20,4 +35,5 @@ handleDeepLinkFromArgv()
|
|||||||
|
|
||||||
process.on('exit', () => {
|
process.on('exit', () => {
|
||||||
unregisterGlobalShortcuts()
|
unregisterGlobalShortcuts()
|
||||||
|
closeSingleInstanceServer()
|
||||||
})
|
})
|
||||||
|
|||||||
111
src/desktop/single-instance.js
Normal file
111
src/desktop/single-instance.js
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||||
|
const SINGLE_INSTANCE_HOST = '127.0.0.1'
|
||||||
|
|
||||||
|
// Stable port derived from the app identifier so multiple apps don't collide.
|
||||||
|
function getSingleInstancePort() {
|
||||||
|
const identifier = 'com.tombutcher.farmcontrol'
|
||||||
|
let hash = 0
|
||||||
|
|
||||||
|
for (const char of identifier) {
|
||||||
|
hash = (hash * 31 + char.charCodeAt(0)) | 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 49152 + (Math.abs(hash) % 16383)
|
||||||
|
}
|
||||||
|
|
||||||
|
function findProtocolUrl(args) {
|
||||||
|
return args.find(
|
||||||
|
(arg) => typeof arg === 'string' && arg.startsWith(PROTOCOL_PREFIX)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSecondInstanceMessage() {
|
||||||
|
const url = findProtocolUrl(process.argv)
|
||||||
|
return url ? { type: 'deeplink', url } : { type: 'focus' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIncomingMessage(data) {
|
||||||
|
const text = data.toString().trim()
|
||||||
|
if (!text) return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forwardToPrimaryInstance(message) {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
Bun.connect({
|
||||||
|
hostname: SINGLE_INSTANCE_HOST,
|
||||||
|
port: getSingleInstancePort(),
|
||||||
|
socket: {
|
||||||
|
open(socket) {
|
||||||
|
socket.write(`${JSON.stringify(message)}\n`)
|
||||||
|
socket.end()
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
data() {},
|
||||||
|
error(_socket, error) {
|
||||||
|
reject(error)
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).catch(reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let server = null
|
||||||
|
|
||||||
|
function startPrimaryInstanceServer({ onDeepLink, onFocus }) {
|
||||||
|
server = Bun.listen({
|
||||||
|
hostname: SINGLE_INSTANCE_HOST,
|
||||||
|
port: getSingleInstancePort(),
|
||||||
|
socket: {
|
||||||
|
data(socket, data) {
|
||||||
|
const message = parseIncomingMessage(data)
|
||||||
|
socket.end()
|
||||||
|
|
||||||
|
if (!message) return
|
||||||
|
|
||||||
|
if (message.type === 'deeplink' && message.url) {
|
||||||
|
onDeepLink?.(message.url)
|
||||||
|
} else if (message.type === 'focus') {
|
||||||
|
onFocus?.()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureSingleInstanceLock({ onDeepLink, onFocus }) {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
startPrimaryInstanceServer({ onDeepLink, onFocus })
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
if (error?.code !== 'EADDRINUSE') {
|
||||||
|
console.warn('Single instance lock failed:', error)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await forwardToPrimaryInstance(buildSecondInstanceMessage())
|
||||||
|
} catch (forwardError) {
|
||||||
|
console.warn('Failed to forward to running instance:', forwardError)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeSingleInstanceServer() {
|
||||||
|
server?.stop?.()
|
||||||
|
server = null
|
||||||
|
}
|
||||||
@ -20,6 +20,11 @@ export function getMainWindow() {
|
|||||||
return mainWindow
|
return mainWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function showMainWindow() {
|
||||||
|
mainWindow?.show?.()
|
||||||
|
mainWindow?.activate?.()
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMainViewUrl() {
|
export async function getMainViewUrl() {
|
||||||
const channel = await Updater.localInfo.channel()
|
const channel = await Updater.localInfo.channel()
|
||||||
if (channel === 'dev' || process.env.NODE_ENV === 'development') {
|
if (channel === 'dev' || process.env.NODE_ENV === 'development') {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user