mirror of
https://github.com/black-ant/Ant-Browser.git
synced 2026-07-14 18:48:55 +08:00
312 lines
9.6 KiB
JavaScript
312 lines
9.6 KiB
JavaScript
import { spawnSync } from 'node:child_process'
|
|
import { dirname, resolve } from 'node:path'
|
|
import process from 'node:process'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
export const frontendDir = resolve(scriptDir, '..')
|
|
export const repoRoot = resolve(frontendDir, '..')
|
|
|
|
const frontendDirLower = frontendDir.toLowerCase()
|
|
const repoRootLower = repoRoot.toLowerCase()
|
|
const defaultPreferredPort = 5218
|
|
const maxCandidateCount = 20
|
|
const processInspectionFilter = "Name = 'node.exe' OR Name = 'cmd.exe' OR Name = 'npm.exe' OR Name = 'esbuild.exe' OR Name = 'wails.exe'"
|
|
|
|
function runPowerShell(command, cwd = repoRoot) {
|
|
return spawnSync('powershell.exe', ['-NoProfile', '-Command', command], {
|
|
cwd,
|
|
encoding: 'utf8',
|
|
timeout: 12000,
|
|
})
|
|
}
|
|
|
|
function normalizeProcessChain(proc) {
|
|
const chain = Array.isArray(proc?.chain) ? proc.chain : []
|
|
return [
|
|
String(proc?.commandLine || ''),
|
|
...chain.map((item) => String(item?.commandLine || '')),
|
|
]
|
|
.map((item) => item.trim().toLowerCase())
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function collectProcessesByPowerShell(filterCommand) {
|
|
const result = runPowerShell(filterCommand)
|
|
|
|
if (result.error) {
|
|
throw new Error(result.error.message || 'failed to inspect processes')
|
|
}
|
|
|
|
if (result.status !== 0) {
|
|
throw new Error(result.stderr?.trim() || 'failed to inspect processes')
|
|
}
|
|
|
|
const text = result.stdout.trim()
|
|
if (!text) {
|
|
return []
|
|
}
|
|
|
|
const parsed = JSON.parse(text)
|
|
return Array.isArray(parsed) ? parsed : [parsed]
|
|
}
|
|
|
|
export function resolveRequestedPort(rawPort, fallbackPort = defaultPreferredPort) {
|
|
const parsed = Number.parseInt(String(rawPort || '').trim(), 10)
|
|
if (Number.isInteger(parsed) && parsed > 0 && parsed <= 65535) {
|
|
return parsed
|
|
}
|
|
return fallbackPort
|
|
}
|
|
|
|
export function listListeners(port) {
|
|
const items = collectProcessesByPowerShell(`
|
|
$port = ${port}
|
|
$items = @()
|
|
$allProcs = Get-CimInstance Win32_Process
|
|
$procMap = @{}
|
|
foreach ($item in $allProcs) { $procMap[[string]$item.ProcessId] = $item }
|
|
$conns = Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue
|
|
foreach ($conn in $conns) {
|
|
$proc = $procMap[[string]$conn.OwningProcess]
|
|
if (-not $proc) { continue }
|
|
|
|
$chain = @()
|
|
$current = $proc
|
|
$visited = @{}
|
|
$depth = 0
|
|
while ($current -and $depth -lt 8 -and -not $visited.ContainsKey([string]$current.ProcessId)) {
|
|
$visited[[string]$current.ProcessId] = $true
|
|
$chain += [PSCustomObject]@{
|
|
pid = [int]$current.ProcessId
|
|
parentProcessId = [int]$current.ParentProcessId
|
|
name = [string]$current.Name
|
|
commandLine = [string]$current.CommandLine
|
|
}
|
|
if ($current.ParentProcessId -le 0) { break }
|
|
$current = $procMap[[string]$current.ParentProcessId]
|
|
$depth++
|
|
}
|
|
|
|
$items += [PSCustomObject]@{
|
|
pid = [int]$proc.ProcessId
|
|
parentProcessId = [int]$proc.ParentProcessId
|
|
name = [string]$proc.Name
|
|
commandLine = [string]$proc.CommandLine
|
|
chain = $chain
|
|
}
|
|
}
|
|
$items | ConvertTo-Json -Compress -Depth 6
|
|
`)
|
|
return items
|
|
}
|
|
|
|
export function summarizeProcess(proc) {
|
|
const name = proc?.name || 'unknown'
|
|
const pid = proc?.pid || 0
|
|
const commandLine = String(proc?.commandLine || '').trim()
|
|
if (!commandLine) {
|
|
return `${name} (PID ${pid})`
|
|
}
|
|
return `${name} (PID ${pid}) ${commandLine}`
|
|
}
|
|
|
|
export function isProjectDevProcess(proc) {
|
|
const lines = normalizeProcessChain(proc)
|
|
const hasProjectPath = lines.some((line) => line.includes(frontendDirLower) || line.includes(repoRootLower))
|
|
const hasDevWatcher = lines.some((line) => line.includes('scripts/dev-watcher.mjs'))
|
|
const hasRawDev = lines.some((line) => line.includes('npm run dev:raw'))
|
|
const hasVite = lines.some((line) => line.includes('vite/bin/vite.js') || line.includes('\\vite\\bin\\vite.js'))
|
|
const hasNpmDev = lines.some((line) => line.includes('npm run dev'))
|
|
const hasEnsureNative = lines.some((line) => line.includes('ensure-rollup-native.mjs'))
|
|
const hasWailsDev = lines.some((line) => line.includes('wails dev'))
|
|
const hasEsbuild = lines.some((line) => line.includes('esbuild.exe') || line.endsWith('\\esbuild'))
|
|
const hasDevMarker = hasDevWatcher || hasRawDev || hasVite || hasNpmDev || hasEnsureNative || hasWailsDev || hasEsbuild
|
|
|
|
return hasProjectPath && hasDevMarker
|
|
}
|
|
|
|
export function killProcessTree(pid) {
|
|
if (!pid || pid <= 0) {
|
|
return true
|
|
}
|
|
|
|
const killed = spawnSync('taskkill.exe', ['/F', '/T', '/PID', String(pid)], {
|
|
cwd: repoRoot,
|
|
stdio: 'ignore',
|
|
timeout: 8000,
|
|
})
|
|
if (killed.error) {
|
|
return false
|
|
}
|
|
return killed.status === 0
|
|
}
|
|
|
|
export function listProjectDevProcesses() {
|
|
return collectProcessesByPowerShell(`
|
|
$items = @()
|
|
$allProcs = Get-CimInstance Win32_Process
|
|
$procMap = @{}
|
|
foreach ($item in $allProcs) { $procMap[[string]$item.ProcessId] = $item }
|
|
$targetNames = @('node.exe', 'cmd.exe', 'npm.exe', 'esbuild.exe', 'wails.exe')
|
|
$procs = $allProcs | Where-Object { $targetNames -contains $_.Name }
|
|
foreach ($proc in $procs) {
|
|
$chain = @()
|
|
$current = $proc
|
|
$visited = @{}
|
|
$depth = 0
|
|
while ($current -and $depth -lt 8 -and -not $visited.ContainsKey([string]$current.ProcessId)) {
|
|
$visited[[string]$current.ProcessId] = $true
|
|
$chain += [PSCustomObject]@{
|
|
pid = [int]$current.ProcessId
|
|
parentProcessId = [int]$current.ParentProcessId
|
|
name = [string]$current.Name
|
|
commandLine = [string]$current.CommandLine
|
|
}
|
|
if ($current.ParentProcessId -le 0) { break }
|
|
$current = $procMap[[string]$current.ParentProcessId]
|
|
$depth++
|
|
}
|
|
|
|
$items += [PSCustomObject]@{
|
|
pid = [int]$proc.ProcessId
|
|
parentProcessId = [int]$proc.ParentProcessId
|
|
name = [string]$proc.Name
|
|
commandLine = [string]$proc.CommandLine
|
|
chain = $chain
|
|
}
|
|
}
|
|
$items | ConvertTo-Json -Compress -Depth 6
|
|
`)
|
|
}
|
|
|
|
function selectRootProcesses(processes) {
|
|
const pidSet = new Set(processes.map((proc) => proc.pid))
|
|
return processes.filter((proc) => !pidSet.has(proc.parentProcessId))
|
|
}
|
|
|
|
function waitForPortToClear(port, timeoutMs = 2500) {
|
|
const startedAt = Date.now()
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
if (listListeners(port).length === 0) {
|
|
return true
|
|
}
|
|
runPowerShell('Start-Sleep -Milliseconds 150')
|
|
}
|
|
return listListeners(port).length === 0
|
|
}
|
|
|
|
export function cleanupProjectListeners(port, logger = console.error) {
|
|
const listeners = listListeners(port)
|
|
let cleaned = false
|
|
|
|
for (const proc of listeners) {
|
|
if (!isProjectDevProcess(proc)) {
|
|
continue
|
|
}
|
|
|
|
cleaned = true
|
|
logger(`[dev] cleaning stale project dev process on ${port}: ${summarizeProcess(proc)}`)
|
|
if (!killProcessTree(proc.pid)) {
|
|
throw new Error(`failed to kill stale dev process ${proc.pid} on port ${port}`)
|
|
}
|
|
}
|
|
|
|
if (cleaned && !waitForPortToClear(port)) {
|
|
throw new Error(`port ${port} is still occupied after cleaning stale project dev processes`)
|
|
}
|
|
|
|
return cleaned
|
|
}
|
|
|
|
export function cleanupProjectDevProcesses(logger = console.error) {
|
|
const processes = listProjectDevProcesses().filter(isProjectDevProcess)
|
|
const roots = selectRootProcesses(processes)
|
|
|
|
for (const proc of roots) {
|
|
logger(`[dev] cleaning stale project dev process: ${summarizeProcess(proc)}`)
|
|
if (!killProcessTree(proc.pid)) {
|
|
throw new Error(`failed to kill stale project dev process ${proc.pid}`)
|
|
}
|
|
}
|
|
|
|
return roots.length > 0
|
|
}
|
|
|
|
export function resolveFrontendDevPort(preferredPort = defaultPreferredPort, logger = console.error, allowFallback = true) {
|
|
const requestedPort = resolveRequestedPort(preferredPort)
|
|
|
|
cleanupProjectListeners(requestedPort, logger)
|
|
let listeners = listListeners(requestedPort)
|
|
if (listeners.length === 0) {
|
|
return {
|
|
port: requestedPort,
|
|
preferredPort: requestedPort,
|
|
reusedPreferredPort: true,
|
|
reason: 'preferred-port-available',
|
|
}
|
|
}
|
|
|
|
logger(`[dev] preferred frontend port ${requestedPort} is occupied by: ${listeners.map(summarizeProcess).join('; ')}`)
|
|
if (!allowFallback) {
|
|
throw new Error(`preferred frontend port ${requestedPort} is occupied`)
|
|
}
|
|
|
|
for (let offset = 1; offset <= maxCandidateCount; offset++) {
|
|
const candidatePort = requestedPort + offset
|
|
cleanupProjectListeners(candidatePort, logger)
|
|
listeners = listListeners(candidatePort)
|
|
if (listeners.length === 0) {
|
|
logger(`[dev] switching frontend dev server from ${requestedPort} to ${candidatePort}`)
|
|
return {
|
|
port: candidatePort,
|
|
preferredPort: requestedPort,
|
|
reusedPreferredPort: false,
|
|
reason: 'fallback-port-selected',
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error(`failed to find a free frontend dev port in range ${requestedPort}-${requestedPort + maxCandidateCount}`)
|
|
}
|
|
|
|
function parseCliArgs(argv) {
|
|
const args = argv.slice(2)
|
|
const command = args[0] || 'resolve'
|
|
let preferredPort = defaultPreferredPort
|
|
|
|
for (let index = 1; index < args.length; index++) {
|
|
const item = args[index]
|
|
if (item === '--preferred' && index + 1 < args.length) {
|
|
preferredPort = resolveRequestedPort(args[index + 1], defaultPreferredPort)
|
|
index++
|
|
}
|
|
}
|
|
|
|
return { command, preferredPort }
|
|
}
|
|
|
|
function runCli() {
|
|
const { command, preferredPort } = parseCliArgs(process.argv)
|
|
if (command === 'cleanup') {
|
|
cleanupProjectDevProcesses()
|
|
return
|
|
}
|
|
if (command !== 'resolve') {
|
|
throw new Error(`unsupported command: ${command}`)
|
|
}
|
|
|
|
const result = resolveFrontendDevPort(preferredPort)
|
|
process.stdout.write(`${result.port}\n`)
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
try {
|
|
runCli()
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error)
|
|
process.stderr.write(`${message}\n`)
|
|
process.exit(1)
|
|
}
|
|
}
|