Implement deeplink functionality and update installer for Windows
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
- Introduced a new script to build the Windows deeplink executable, enhancing deep link handling. - Updated the installer script to replace references from `launcher.exe` to `deeplink.exe` for better integration with the new deeplink functionality. - Added a new `deeplink.js` file to manage deeplink processing and communication with running instances. - Refactored single instance management to utilize the new deeplink capabilities, improving instance handling and navigation. - Removed deprecated Windows launch argument handling to streamline the codebase.
This commit is contained in:
parent
37593d73ce
commit
5450473f74
@ -1,4 +1,4 @@
|
||||
!macro quitFarmControl
|
||||
!macro quitFarmControl
|
||||
DetailPrint "Stopping running Farm Control processes..."
|
||||
ExecWait 'taskkill /F /IM launcher.exe /T' $R0
|
||||
!macroend
|
||||
@ -51,10 +51,10 @@
|
||||
DeleteRegKey HKCR "farmcontrol"
|
||||
WriteRegStr HKCR "farmcontrol" "" "URL:farmcontrol"
|
||||
WriteRegStr HKCR "farmcontrol" "URL Protocol" ""
|
||||
WriteRegStr HKCR "farmcontrol\DefaultIcon" "" "$INSTDIR\bin\launcher.exe"
|
||||
WriteRegStr HKCR "farmcontrol\DefaultIcon" "" "$INSTDIR\bin\deeplink.exe"
|
||||
WriteRegStr HKCR "farmcontrol\shell" "" ""
|
||||
WriteRegStr HKCR "farmcontrol\shell\Open" "" ""
|
||||
WriteRegStr HKCR "farmcontrol\shell\Open\command" "" '"$INSTDIR\bin\launcher.exe" "%1"'
|
||||
WriteRegStr HKCR "farmcontrol\shell\Open\command" "" '"$INSTDIR\bin\deeplink.exe" "%1"'
|
||||
!macroend
|
||||
|
||||
!macro customUnInstall
|
||||
|
||||
60
scripts/build-windows-deeplink.mjs
Normal file
60
scripts/build-windows-deeplink.mjs
Normal file
@ -0,0 +1,60 @@
|
||||
import { existsSync, mkdirSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const entrypoint = path.join(rootDir, 'src/bun/deeplink.js')
|
||||
|
||||
export function buildWindowsDeeplinkExe(outputPath) {
|
||||
if (!existsSync(entrypoint)) {
|
||||
throw new Error(`build-windows-deeplink: entrypoint not found: ${entrypoint}`)
|
||||
}
|
||||
|
||||
mkdirSync(path.dirname(outputPath), { recursive: true })
|
||||
|
||||
const target = process.env.ELECTROBUN_ARCH === 'arm64'
|
||||
? 'bun-windows-x64'
|
||||
: 'bun-windows-x64'
|
||||
|
||||
const result = spawnSync(
|
||||
'bun',
|
||||
[
|
||||
'build',
|
||||
'--compile',
|
||||
'--minify',
|
||||
`--target=${target}`,
|
||||
entrypoint,
|
||||
'--outfile',
|
||||
outputPath
|
||||
],
|
||||
{
|
||||
cwd: rootDir,
|
||||
stdio: 'inherit',
|
||||
env: process.env
|
||||
}
|
||||
)
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(
|
||||
`build-windows-deeplink: bun build failed with exit code ${result.status ?? 1}`
|
||||
)
|
||||
}
|
||||
|
||||
if (!existsSync(outputPath)) {
|
||||
throw new Error(`build-windows-deeplink: output not created: ${outputPath}`)
|
||||
}
|
||||
|
||||
console.log(`build-windows-deeplink: created ${outputPath}`)
|
||||
return outputPath
|
||||
}
|
||||
|
||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
||||
const outputArg = process.argv[2]
|
||||
if (!outputArg) {
|
||||
console.error('Usage: bun scripts/build-windows-deeplink.mjs <output-path>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
buildWindowsDeeplinkExe(path.resolve(outputArg))
|
||||
}
|
||||
@ -68,9 +68,13 @@ if ($stagedAppBytes -lt 5242880) {
|
||||
}
|
||||
|
||||
$requiredExe = Join-Path $stagingAppDir "bin\launcher.exe"
|
||||
$requiredDeeplinkExe = Join-Path $stagingAppDir "bin\deeplink.exe"
|
||||
if (-not (Test-Path $requiredExe)) {
|
||||
throw "Staged application is missing bin\launcher.exe"
|
||||
}
|
||||
if (-not (Test-Path $requiredDeeplinkExe)) {
|
||||
throw "Staged application is missing bin\deeplink.exe"
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $nsiPath -Destination (Join-Path $workDir "farmcontrol.nsi")
|
||||
Copy-Item -LiteralPath $installerInclude -Destination (Join-Path $workDir "installer.nsh")
|
||||
|
||||
@ -19,7 +19,7 @@ import {
|
||||
cleanExpandedWindowsApp,
|
||||
expandWindowsAppFromArchive
|
||||
} from './expand-windows-installer.mjs'
|
||||
import { codesignMacAppBundle } from './codesign-macos-app.mjs'
|
||||
import { buildWindowsDeeplinkExe } from './build-windows-deeplink.mjs'
|
||||
|
||||
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const packageJson = JSON.parse(
|
||||
@ -428,6 +428,12 @@ function buildMacPkg(appBundlePath, arch) {
|
||||
return pkgPath
|
||||
}
|
||||
|
||||
function stageWindowsDeeplinkExe(appDir) {
|
||||
const deeplinkPath = path.join(appDir, 'bin', 'deeplink.exe')
|
||||
buildWindowsDeeplinkExe(deeplinkPath)
|
||||
return deeplinkPath
|
||||
}
|
||||
|
||||
function cleanWinBuildDir(arch) {
|
||||
const platformDir = path.join(getBuildRoot(), `stable-win-${arch}`)
|
||||
if (existsSync(platformDir)) {
|
||||
@ -577,6 +583,7 @@ async function main() {
|
||||
installerFiles.setupArchive,
|
||||
platformDir
|
||||
)
|
||||
stageWindowsDeeplinkExe(appDir)
|
||||
|
||||
let published
|
||||
try {
|
||||
|
||||
45
src/bun/deeplink.js
Normal file
45
src/bun/deeplink.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import {
|
||||
buildDeeplinkPayload,
|
||||
findProtocolUrl,
|
||||
forwardDeeplinkToRunningInstance,
|
||||
writeDeeplinkSignal
|
||||
} from '../desktop/deeplink-ipc.js'
|
||||
|
||||
const url = findProtocolUrl(process.argv)
|
||||
|
||||
console.log('[deeplink] argv:', process.argv)
|
||||
console.log('[deeplink] url:', url ?? null)
|
||||
|
||||
if (!url) {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const payload = buildDeeplinkPayload(url, process.argv)
|
||||
const forwarded = await forwardDeeplinkToRunningInstance(payload)
|
||||
|
||||
if (forwarded) {
|
||||
console.log('[deeplink] forwarded to running Farm Control instance')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log('[deeplink] no running instance found, launching Farm Control')
|
||||
|
||||
writeDeeplinkSignal(payload)
|
||||
|
||||
const launcherPath = join(dirname(process.execPath), 'launcher.exe')
|
||||
if (!existsSync(launcherPath)) {
|
||||
console.error('[deeplink] launcher.exe not found at', launcherPath)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const child = spawn(launcherPath, [], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
windowsHide: true
|
||||
})
|
||||
|
||||
child.unref()
|
||||
process.exit(0)
|
||||
@ -1,28 +1,16 @@
|
||||
import {
|
||||
captureLaunchUrl,
|
||||
closeSingleInstanceServer,
|
||||
ensureSingleInstanceLock,
|
||||
logWindowsLaunchArgs
|
||||
ensureSingleInstanceLock
|
||||
} from '../desktop/single-instance.js'
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
logWindowsLaunchArgs('index-before-lock')
|
||||
}
|
||||
|
||||
const launchUrl = captureLaunchUrl()
|
||||
const gotSingleInstanceLock = await ensureSingleInstanceLock({ launchUrl })
|
||||
|
||||
if (!gotSingleInstanceLock) {
|
||||
if (process.platform === 'win32') {
|
||||
console.log('[launch-args][index] exiting secondary instance')
|
||||
}
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log('[launch-args][index] acquired single instance lock')
|
||||
}
|
||||
|
||||
const { createAppRpc } = await import('../desktop/rpc.js')
|
||||
const {
|
||||
registerGlobalShortcuts,
|
||||
|
||||
147
src/desktop/deeplink-ipc.js
Normal file
147
src/desktop/deeplink-ipc.js
Normal file
@ -0,0 +1,147 @@
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
unlinkSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import net from 'node:net'
|
||||
import { join } from 'node:path'
|
||||
|
||||
export const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||
export const WINDOWS_PIPE_NAME = '\\\\.\\pipe\\com.tombutcher.farmcontrol.instance'
|
||||
export const SIGNAL_FILE = 'instance-signal.json'
|
||||
const FORWARD_TIMEOUT_MS = 750
|
||||
|
||||
export function getInstanceDir() {
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA
|
||||
if (localAppData) {
|
||||
return join(localAppData, 'com.tombutcher.farmcontrol', 'instance')
|
||||
}
|
||||
}
|
||||
|
||||
return join(process.env.TMPDIR || '/tmp', 'com.tombutcher.farmcontrol', 'instance')
|
||||
}
|
||||
|
||||
export function getSignalPath() {
|
||||
return join(getInstanceDir(), SIGNAL_FILE)
|
||||
}
|
||||
|
||||
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')
|
||||
|
||||
for (const arg of sources) {
|
||||
const trimmed = arg.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
|
||||
}
|
||||
|
||||
export function buildDeeplinkPayload(url, argv = process.argv) {
|
||||
const commandLine = [...argv]
|
||||
|
||||
return url
|
||||
? { type: 'deeplink', url, argv: commandLine }
|
||||
: { type: 'focus', argv: commandLine }
|
||||
}
|
||||
|
||||
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 []
|
||||
}
|
||||
}
|
||||
|
||||
export function writeDeeplinkSignal(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(queue), 'utf8')
|
||||
writeFileSync(signalPath, readFileSync(tempPath))
|
||||
unlinkSync(tempPath)
|
||||
}
|
||||
|
||||
export function forwardDeeplinkToRunningInstance(payload) {
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -11,15 +11,17 @@ import {
|
||||
import net from 'node:net'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
getWindowsLaunchSources,
|
||||
getWindowsLaunchUrl
|
||||
} from './windows-launch-args.js'
|
||||
buildDeeplinkPayload,
|
||||
findProtocolUrl,
|
||||
forwardDeeplinkToRunningInstance,
|
||||
getInstanceDir,
|
||||
getSignalPath,
|
||||
SIGNAL_FILE,
|
||||
WINDOWS_PIPE_NAME,
|
||||
writeDeeplinkSignal
|
||||
} from './deeplink-ipc.js'
|
||||
|
||||
const PROTOCOL_PREFIX = 'farmcontrol://'
|
||||
const LOCK_FILE = 'primary.lock'
|
||||
const SIGNAL_FILE = 'instance-signal.json'
|
||||
const WINDOWS_PIPE_NAME = '\\\\.\\pipe\\com.tombutcher.farmcontrol.instance'
|
||||
const FORWARD_TIMEOUT_MS = 750
|
||||
|
||||
let lockFd = null
|
||||
let pollInterval = null
|
||||
@ -31,25 +33,10 @@ let handlers = {
|
||||
}
|
||||
const pendingMessages = []
|
||||
|
||||
function getInstanceDir() {
|
||||
if (process.platform === 'win32') {
|
||||
const localAppData = process.env.LOCALAPPDATA || process.env.APPDATA
|
||||
if (localAppData) {
|
||||
return join(localAppData, 'com.tombutcher.farmcontrol', 'instance')
|
||||
}
|
||||
}
|
||||
|
||||
return join(process.env.TMPDIR || '/tmp', 'com.tombutcher.farmcontrol', 'instance')
|
||||
}
|
||||
|
||||
function getLockPath() {
|
||||
return join(getInstanceDir(), LOCK_FILE)
|
||||
}
|
||||
|
||||
function getSignalPath() {
|
||||
return join(getInstanceDir(), SIGNAL_FILE)
|
||||
}
|
||||
|
||||
function isProcessAlive(pid) {
|
||||
if (!Number.isInteger(pid) || pid <= 0) {
|
||||
return false
|
||||
@ -115,86 +102,17 @@ function releasePrimaryLock() {
|
||||
}
|
||||
}
|
||||
|
||||
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')
|
||||
|
||||
for (const arg of sources) {
|
||||
const trimmed = arg.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
|
||||
}
|
||||
|
||||
function collectLaunchSources() {
|
||||
const sources = [...process.argv]
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
sources.push(...getWindowsLaunchSources())
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
export function logWindowsLaunchArgs(context = 'startup') {
|
||||
if (process.platform !== 'win32') {
|
||||
return
|
||||
}
|
||||
|
||||
const launchSources = getWindowsLaunchSources()
|
||||
const launchUrl = captureLaunchUrl()
|
||||
|
||||
console.log(`[launch-args][${context}] process.argv:`, process.argv)
|
||||
console.log(`[launch-args][${context}] launch sources:`, launchSources)
|
||||
console.log(`[launch-args][${context}] captured url:`, launchUrl ?? null)
|
||||
}
|
||||
export { findProtocolUrl }
|
||||
|
||||
export function captureLaunchUrl() {
|
||||
const argvUrl = findProtocolUrl(process.argv)
|
||||
if (argvUrl) {
|
||||
return argvUrl
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return getWindowsLaunchUrl() || findProtocolUrl(collectLaunchSources())
|
||||
}
|
||||
|
||||
return findProtocolUrl(collectLaunchSources())
|
||||
return findProtocolUrl(process.argv)
|
||||
}
|
||||
|
||||
function buildLaunchPayload(launchUrl = captureLaunchUrl()) {
|
||||
const commandLine = collectLaunchSources()
|
||||
const commandLine = [...process.argv]
|
||||
const url = launchUrl || findProtocolUrl(commandLine)
|
||||
|
||||
return url
|
||||
? { type: 'deeplink', url, argv: commandLine }
|
||||
: { type: 'focus', argv: commandLine }
|
||||
return buildDeeplinkPayload(url, commandLine)
|
||||
}
|
||||
|
||||
function readSignalQueue({ clear = true } = {}) {
|
||||
@ -229,16 +147,7 @@ function readSignalQueue({ clear = true } = {}) {
|
||||
}
|
||||
|
||||
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(queue), 'utf8')
|
||||
writeFileSync(signalPath, readFileSync(tempPath))
|
||||
unlinkSync(tempPath)
|
||||
writeDeeplinkSignal(payload)
|
||||
}
|
||||
|
||||
function resolveIncomingMessage(message) {
|
||||
@ -263,8 +172,8 @@ function dispatchMessage(message) {
|
||||
if (!resolved) return
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
console.log('[launch-args][dispatch]', message)
|
||||
console.log('[launch-args][dispatch] resolved:', resolved)
|
||||
console.log('[deeplink][dispatch]', message)
|
||||
console.log('[deeplink][dispatch] resolved:', resolved)
|
||||
}
|
||||
|
||||
if (!handlers.onDeepLink && !handlers.onFocus) {
|
||||
@ -309,39 +218,6 @@ function startInstanceSignalWatcher() {
|
||||
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
|
||||
@ -383,18 +259,13 @@ export async function ensureSingleInstanceLock({ launchUrl } = {}) {
|
||||
const payload = buildLaunchPayload(launchUrl)
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
logWindowsLaunchArgs('ensure-single-instance-lock')
|
||||
console.log('[launch-args][single-instance] payload:', payload)
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
if (await forwardToRunningInstance(payload)) {
|
||||
console.log('[launch-args][single-instance] forwarded to running instance')
|
||||
if (await forwardDeeplinkToRunningInstance(payload)) {
|
||||
console.log('[deeplink] forwarded duplicate launcher instance to running app')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!tryAcquirePrimaryLock()) {
|
||||
if (await forwardToRunningInstance(payload)) {
|
||||
if (await forwardDeeplinkToRunningInstance(payload)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@ -1,187 +0,0 @@
|
||||
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',
|
||||
windowsHide: true
|
||||
})
|
||||
|
||||
if (proc.exitCode !== 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return proc.stdout.toString()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getWmicValue(output, key) {
|
||||
const match = output.match(new RegExp(`${key}=(.+?)(?:\\r?\\n|$)`, 'i'))
|
||||
return match?.[1]?.trim() ?? ''
|
||||
}
|
||||
|
||||
function getProcessCommandLine(pid) {
|
||||
const output = runCommand(
|
||||
`wmic process where "ProcessId=${pid}" get CommandLine /value`
|
||||
)
|
||||
return getWmicValue(output, 'CommandLine')
|
||||
}
|
||||
|
||||
function getParentProcessId(pid) {
|
||||
const output = runCommand(
|
||||
`wmic process where "ProcessId=${pid}" get ParentProcessId /value`
|
||||
)
|
||||
const parentPid = getWmicValue(output, 'ParentProcessId')
|
||||
return parentPid ? Number.parseInt(parentPid, 10) : null
|
||||
}
|
||||
|
||||
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) {
|
||||
$lines.Add([string]$proc.CommandLine)
|
||||
if ($proc.CommandLine -match 'farmcontrol://') { break }
|
||||
}
|
||||
if (-not $proc.ParentProcessId -or $proc.ParentProcessId -eq $pid) { break }
|
||||
$pid = $proc.ParentProcessId
|
||||
}
|
||||
$lines -join [Environment]::NewLine
|
||||
`)
|
||||
|
||||
if (!output) {
|
||||
return []
|
||||
}
|
||||
|
||||
return output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
function getWmicProcessTreeCommandLines() {
|
||||
const sources = []
|
||||
let pid = process.pid
|
||||
|
||||
for (let depth = 0; depth < 12; depth += 1) {
|
||||
const commandLine = getProcessCommandLine(pid)
|
||||
if (commandLine) {
|
||||
sources.push(commandLine)
|
||||
if (/farmcontrol:\/\//i.test(commandLine)) {
|
||||
return sources
|
||||
}
|
||||
}
|
||||
|
||||
const parentPid = getParentProcessId(pid)
|
||||
if (!parentPid || parentPid === pid) {
|
||||
break
|
||||
}
|
||||
|
||||
pid = parentPid
|
||||
}
|
||||
|
||||
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