-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcleanup.ts
More file actions
119 lines (109 loc) · 3.97 KB
/
Copy pathcleanup.ts
File metadata and controls
119 lines (109 loc) · 3.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
// ─────────────────────────────────────────────────────────────────────────
// session cleanup
//
// when a websocket closes we don't immediately wipe the session — wifi
// blips are common, so we want a brief grace period during which the user
// can reconnect and resume. if no new socket attaches within the grace
// window, we sweep:
//
// • all containers labelled session=<id>
// • all images labelled session=<id>
// • the user's workspace directory on disk
// • the per-session port reservation slice
//
// the user keeps the same session id (derived from username), so a quick
// disconnect/reconnect just hits the cancel path and nothing happens.
// ─────────────────────────────────────────────────────────────────────────
import type { Session } from "./session"
import { releaseAllPorts } from "./workspace"
const GRACE_MS = 90_000 // 90 seconds — survives network blips, kills idle tabs
const pendingCleanups = new Map<string, ReturnType<typeof setTimeout>>()
function log(...args: unknown[]): void {
console.log("[cleanup]", ...args)
}
async function runDocker(args: string[], timeoutMs = 30_000): Promise<string> {
const proc = Bun.spawn(["docker", ...args], {
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
})
const timer = setTimeout(() => {
try {
proc.kill()
} catch {
/* noop */
}
}, timeoutMs)
const text = await new Response(proc.stdout).text()
clearTimeout(timer)
await proc.exited
return text
}
/** Force-remove every container + image labelled with this session. */
async function sweepDockerArtifacts(sessionId: string): Promise<void> {
const label = `label=session=${sessionId}`
try {
// containers (running or not)
const containerIds = (
await runDocker(["ps", "-aq", "--filter", label])
)
.split("\n")
.map((s) => s.trim())
.filter(Boolean)
if (containerIds.length > 0) {
await runDocker(["rm", "-f", ...containerIds])
log(`session ${sessionId}: removed ${containerIds.length} container(s)`)
}
// images
const imageIds = (await runDocker(["images", "-q", "--filter", label]))
.split("\n")
.map((s) => s.trim())
.filter(Boolean)
if (imageIds.length > 0) {
await runDocker(["rmi", "-f", ...imageIds])
log(`session ${sessionId}: removed ${imageIds.length} image(s)`)
}
} catch (err) {
log(`session ${sessionId}: sweep failed`, err)
}
}
/**
* Schedule a cleanup for a session whose socket just disconnected. If the
* user reconnects within `GRACE_MS`, call `cancelCleanup(sessionId)` to
* abort.
*/
export function scheduleCleanup(
session: Session,
onWipe: () => void
): void {
// cancel any existing pending cleanup so we always get the latest grace
const existing = pendingCleanups.get(session.id)
if (existing) clearTimeout(existing)
const timer = setTimeout(async () => {
pendingCleanups.delete(session.id)
log(`session ${session.id}: grace period expired, sweeping`)
await sweepDockerArtifacts(session.id)
releaseAllPorts(session.id)
onWipe()
}, GRACE_MS)
pendingCleanups.set(session.id, timer)
log(
`session ${session.id}: cleanup scheduled in ${Math.round(GRACE_MS / 1000)}s`
)
}
export function cancelCleanup(sessionId: string): void {
const existing = pendingCleanups.get(sessionId)
if (existing) {
clearTimeout(existing)
pendingCleanups.delete(sessionId)
log(`session ${sessionId}: cleanup cancelled (reconnected)`)
}
}
/**
* Sweep everything for a session right now. Used by tests and shutdown.
*/
export async function sweepNow(sessionId: string): Promise<void> {
cancelCleanup(sessionId)
await sweepDockerArtifacts(sessionId)
releaseAllPorts(sessionId)
}