| 209 | } |
| 210 | |
| 211 | async function freeStaleAppPorts(log) { |
| 212 | // If CDP is not serving but the dev ports are held, a previous run left a |
| 213 | // half-dead app behind (e.g. Electron without its devtools listener). |
| 214 | // Clear them so the fresh spawn does not lose the bind race. |
| 215 | for (const port of [9823, 5173]) { |
| 216 | try { |
| 217 | const { stdout } = await execFileAsync("lsof", ["-nP", "-ti", `tcp:${port}`]); |
| 218 | const pids = stdout.split("\n").map((line) => line.trim()).filter(Boolean); |
| 219 | for (const pid of pids) { |
| 220 | try { |
| 221 | process.kill(Number(pid), "SIGKILL"); |
| 222 | log(`Cleared stale process ${pid} holding :${port}`); |
| 223 | } catch { |
| 224 | // Already gone. |
| 225 | } |
| 226 | } |
| 227 | } catch { |
| 228 | // Port free — nothing to do. |
| 229 | } |
| 230 | } |
| 231 | await sleep(1_500); |
| 232 | } |
| 233 | |
| 234 | async function ensureApp(log, cdpCandidates) { |
| 235 | for (const candidate of cdpCandidates) { |