246 lines
6.8 KiB
JavaScript
Executable file
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)
|
|
})
|