Compare commits
2 Commits
d0345fd7f3
...
611cd4aad5
| Author | SHA1 | Date | |
|---|---|---|---|
| 611cd4aad5 | |||
| 1b77428a8a |
@ -1,15 +1,30 @@
|
||||
import { createAppRpc } from '../desktop/rpc.js'
|
||||
import {
|
||||
closeSingleInstanceServer,
|
||||
ensureSingleInstanceLock
|
||||
} from '../desktop/single-instance.js'
|
||||
import {
|
||||
registerGlobalShortcuts,
|
||||
unregisterGlobalShortcuts
|
||||
} from '../desktop/spotlight.js'
|
||||
import {
|
||||
createMainWindow,
|
||||
handleDeepLink,
|
||||
handleDeepLinkFromArgv,
|
||||
setupDevAuthServer,
|
||||
setupNavigationGestures
|
||||
setupNavigationGestures,
|
||||
showMainWindow
|
||||
} from '../desktop/window.js'
|
||||
|
||||
const gotSingleInstanceLock = await ensureSingleInstanceLock({
|
||||
onDeepLink: handleDeepLink,
|
||||
onFocus: showMainWindow
|
||||
})
|
||||
|
||||
if (!gotSingleInstanceLock) {
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const rpc = createAppRpc()
|
||||
const mainWindow = await createMainWindow(rpc)
|
||||
|
||||
@ -20,4 +35,5 @@ handleDeepLinkFromArgv()
|
||||
|
||||
process.on('exit', () => {
|
||||
unregisterGlobalShortcuts()
|
||||
closeSingleInstanceServer()
|
||||
})
|
||||
|
||||
@ -54,8 +54,8 @@ const DashboardWindowButtons = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{maximizeButton}
|
||||
{minimizeButton}
|
||||
{maximizeButton}
|
||||
{closeButton}
|
||||
</>
|
||||
)}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export function showMainWindow() {
|
||||
mainWindow?.show?.()
|
||||
mainWindow?.activate?.()
|
||||
}
|
||||
|
||||
export async function getMainViewUrl() {
|
||||
const channel = await Updater.localInfo.channel()
|
||||
if (channel === 'dev' || process.env.NODE_ENV === 'development') {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user