feat(cloakserve): on-demand close endpoint (POST /fingerprint/{seed}/close)
Adds ChromePool.terminate_seed() + a close route that tears down one seed's Chrome immediately and frees its slot, instead of waiting out the idle timeout. Profile is preserved (goes through _cleanup_process). Idempotent.
This commit is contained in:
@@ -427,6 +427,22 @@ class ChromePool:
|
||||
self._locks.pop(key, None)
|
||||
self._connections.pop(key, None)
|
||||
|
||||
async def terminate_seed(self, seed: str) -> bool:
|
||||
"""Terminate one seed's Chrome on demand, freeing its slot immediately.
|
||||
|
||||
Returns True if a process was running and got torn down, False if there
|
||||
was nothing to terminate. The persistent profile is preserved: teardown
|
||||
goes through _cleanup_process, whose _safe_rmtree the persistent pool
|
||||
overrides to a no-op, so reopening the same seed later restores identity.
|
||||
"""
|
||||
if not seed or not SAFE_SEED_RE.match(seed) or seed in RESERVED_SEEDS:
|
||||
return False
|
||||
async with self._get_lock(seed):
|
||||
if seed not in self._processes:
|
||||
return False
|
||||
await self._cleanup_process(seed)
|
||||
return True
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Terminate all Chrome processes."""
|
||||
idle_tasks = list(self._idle_tasks.values())
|
||||
@@ -527,6 +543,18 @@ def _external_host(request: web.Request) -> str:
|
||||
return fallback_host
|
||||
|
||||
|
||||
async def handle_close(request: web.Request) -> web.Response:
|
||||
"""Terminate one seed's browser on demand, freeing its slot immediately.
|
||||
|
||||
Idempotent: closing a seed that is not running returns terminated=false, 200.
|
||||
The persistent profile is kept, so reopening the seed restores its identity.
|
||||
"""
|
||||
pool: ChromePool = request.app["pool"]
|
||||
seed = request.match_info.get("seed", "")
|
||||
terminated = await pool.terminate_seed(seed)
|
||||
return web.json_response({"seed": seed, "terminated": terminated})
|
||||
|
||||
|
||||
async def handle_root(request: web.Request) -> web.Response:
|
||||
"""Health check / process status."""
|
||||
pool: ChromePool = request.app["pool"]
|
||||
@@ -860,6 +888,7 @@ def main() -> None:
|
||||
app.router.add_get("/json/", handle_json_list)
|
||||
|
||||
# WebSocket routes — seed-specific (must be before default to match first)
|
||||
app.router.add_post("/fingerprint/{seed}/close", handle_close)
|
||||
app.router.add_get("/fingerprint/{seed}/devtools/{path:.+}", handle_ws_seed)
|
||||
# WebSocket routes — default (no seed)
|
||||
app.router.add_get("/devtools/{path:.+}", handle_ws_default)
|
||||
|
||||
Reference in New Issue
Block a user