(instancePath, options = {})
| 247 | // running — it then just removes a leftover directory. All side effects are |
| 248 | // injectable so the orchestration is unit-testable without real processes. |
| 249 | export async function stopInstance(instancePath, options = {}) { |
| 250 | const { |
| 251 | dryRun = false, |
| 252 | kill = process.kill.bind(process), |
| 253 | runPs = defaultRunPs, |
| 254 | removeDir = (dir) => fs.rm(dir, { recursive: true, force: true }), |
| 255 | selfPid = process.pid, |
| 256 | graceMs = TERM_GRACE_MS, |
| 257 | pollMs = TERM_POLL_MS, |
| 258 | profileRoot, |
| 259 | } = options; |
| 260 | |
| 261 | assertSafeInstancePath(instancePath, profileRoot); |
| 262 | |
| 263 | const processes = parsePsOutput(await runPs()); |
| 264 | const pids = collectInstancePids(processes, instancePath, { selfPid }); |
| 265 | |
| 266 | if (dryRun) { |
| 267 | return { instancePath, pids, terminated: [], killed: [], removed: false }; |
| 268 | } |
| 269 | |
| 270 | signalPids(pids, "SIGTERM", kill); |
| 271 | |
| 272 | let survivors = pids; |
| 273 | const deadline = monotonicNow() + graceMs; |
| 274 | while (survivors.length > 0 && monotonicNow() < deadline) { |
| 275 | await sleep(pollMs); |
| 276 | survivors = survivors.filter((pid) => pidAlive(pid, kill)); |
| 277 | } |
| 278 | const killed = |
| 279 | survivors.length > 0 ? signalPids(survivors, "SIGKILL", kill) : []; |
| 280 | |
| 281 | await removeDir(instancePath); |
| 282 | |
| 283 | return { instancePath, pids, terminated: pids, killed, removed: true }; |
| 284 | } |
| 285 | |
| 286 | // Reads the vault paths an instance registered in its private obsidian.json. |
| 287 | // Returns null when the registration is unreadable (so callers can stay |
no test coverage detected