openpetswithchatandmcp/website/scripts/dev-process-clean.mjs

246 lines
6.8 KiB
JavaScript
Executable file

import { spawnSync } from 'node:child_process'
import { promises as fs } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const uiRoot = path.resolve(__dirname, '..')
const repoRoot = path.resolve(uiRoot, '..')
const lockPath = path.join(uiRoot, 'node_modules', '.cache', 'familiaros-website-dev-server.lock.json')
const dryRun = process.argv.includes('--dry-run')
const actionVerb = dryRun ? 'matched' : 'stopped'
function processIsAlive(pid) {
if (!Number.isInteger(pid) || pid <= 0) return false
try {
process.kill(pid, 0)
return true
} catch {
return false
}
}
function normalizeForSearch(value) {
return String(value ?? '').replaceAll('\\', '/').toLowerCase()
}
async function readLock() {
try {
const raw = await fs.readFile(lockPath, 'utf8')
return JSON.parse(raw)
} catch {
return null
}
}
async function clearLock() {
try {
await fs.unlink(lockPath)
} catch {
// Ignore missing lock file.
}
}
function killPid(pid) {
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) return false
if (dryRun) {
console.log(`[dev:clean] dry-run: would stop PID ${pid}`)
return true
}
if (process.platform === 'win32') {
const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
stdio: 'ignore',
})
return result.status === 0
}
try {
process.kill(pid, 'SIGTERM')
return true
} catch {
return false
}
}
function parsePowerShellJson(stdout) {
const text = String(stdout ?? '').trim()
if (!text) return []
try {
const parsed = JSON.parse(text)
return Array.isArray(parsed) ? parsed : [parsed]
} catch {
return []
}
}
function queryProcessMetadata(pid) {
if (!Number.isInteger(pid) || pid <= 0) return null
if (process.platform === 'win32') {
const script = [
"$ErrorActionPreference = 'SilentlyContinue'",
`$proc = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}" | Select-Object ProcessId, Name, CommandLine, ExecutablePath`,
'$proc | ConvertTo-Json -Compress',
].join('; ')
const result = spawnSync(
'powershell',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script],
{ encoding: 'utf8' }
)
return parsePowerShellJson(result.stdout)[0] ?? null
}
const result = spawnSync('ps', ['-p', String(pid), '-o', 'comm=', '-o', 'args='], {
encoding: 'utf8',
})
const output = String(result.stdout ?? '').trim()
if (!output) return null
const [firstLine] = output.split(/\r?\n/)
const trimmed = firstLine.trim()
if (!trimmed) return null
const [name = '', ...rest] = trimmed.split(/\s+/)
return {
ProcessId: pid,
Name: path.basename(name),
CommandLine: rest.join(' '),
ExecutablePath: name,
}
}
function isExpectedDevProcessName(name) {
const normalized = String(name ?? '').trim().toLowerCase()
return [
'node',
'node.exe',
'cmd.exe',
'npm',
'npm.cmd',
'vite',
'vite.cmd',
'esbuild',
'esbuild.exe',
'sh',
'bash',
'powershell',
'powershell.exe',
].includes(normalized)
}
function isOwnedDevProcess(pid) {
if (!processIsAlive(pid)) return false
const metadata = queryProcessMetadata(pid)
if (!metadata) return false
const name = String(metadata.Name ?? '')
if (!isExpectedDevProcessName(name)) return false
const haystack = normalizeForSearch([
metadata.CommandLine,
metadata.ExecutablePath,
name,
].filter(Boolean).join(' '))
const uiRootToken = normalizeForSearch(uiRoot)
const repoRootToken = normalizeForSearch(repoRoot)
const pathMatches = haystack.includes(uiRootToken) || haystack.includes(repoRootToken)
const commandMatches =
haystack.includes('vite') ||
haystack.includes('dev:raw') ||
haystack.includes('dev-singleton.mjs') ||
haystack.includes('familiaros-website')
return pathMatches || commandMatches
}
function queryWindowsDevProcesses() {
const escapedUiRoot = uiRoot.replace(/'/g, "''")
const escapedRepoRoot = repoRoot.replace(/'/g, "''")
const script = [
"$ErrorActionPreference = 'SilentlyContinue'",
`$targets = @('${escapedUiRoot}', '${escapedRepoRoot}') | ForEach-Object { $_.ToLowerInvariant() }`,
'$procs = Get-CimInstance Win32_Process | Where-Object {',
' if (-not $_.CommandLine) { return $false }',
" if ($_.Name -ne 'node.exe' -and $_.Name -ne 'esbuild.exe') { return $false }",
' $cmd = $_.CommandLine.ToLowerInvariant()',
' return ($targets | Where-Object { $_.Length -gt 0 -and $cmd.Contains($_) }).Count -gt 0',
'} | Select-Object ProcessId, Name, CommandLine',
'$procs | ConvertTo-Json -Compress',
].join('; ')
const result = spawnSync(
'powershell',
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', script],
{ encoding: 'utf8' }
)
return parsePowerShellJson(result.stdout)
}
async function main() {
const lock = await readLock()
const lockPidCandidates = [lock?.childPid, lock?.wrapperPid].filter((pid) =>
Number.isInteger(pid)
)
const reported = new Set()
let stoppedCount = 0
for (const pid of lockPidCandidates) {
if (!isOwnedDevProcess(pid) || reported.has(pid)) continue
reported.add(pid)
const stopped = killPid(pid)
if (stopped) {
console.log(`[dev:clean] ${actionVerb} lock PID ${pid}`)
stoppedCount += 1
}
}
if (process.platform === 'win32') {
const processes = queryWindowsDevProcesses()
for (const proc of processes) {
const pid = Number(proc?.ProcessId)
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid || reported.has(pid)) continue
reported.add(pid)
const name = String(proc?.Name ?? 'process')
const stopped = killPid(pid)
if (stopped) {
console.log(`[dev:clean] ${actionVerb} ${name} PID ${pid}`)
stoppedCount += 1
}
}
} else {
const escapedUi = uiRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const escapedRepo = repoRoot.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const pattern = `(${escapedUi}|${escapedRepo}).*(vite|esbuild|node)`
const pkill = spawnSync('pkill', ['-f', pattern], {
stdio: 'ignore',
})
if (pkill.status === 0) {
stoppedCount += 1
if (dryRun) {
console.log('[dev:clean] dry-run: would stop matching vite/esbuild processes')
} else {
console.log('[dev:clean] stopped matching vite/esbuild processes')
}
}
}
await clearLock()
if (stoppedCount === 0) {
console.log('[dev:clean] no matching stale dev processes found.')
} else {
console.log(`[dev:clean] completed. Processes handled: ${stoppedCount}`)
}
}
main().catch(async (error) => {
console.error(`[dev:clean] ${error.message}`)
await clearLock()
process.exit(1)
})