Skip to content

Commit 99d8ba1

Browse files
committed
Add option to kill run
1 parent 74bcf80 commit 99d8ba1

File tree

8 files changed

+201
-34
lines changed

8 files changed

+201
-34
lines changed

evals/apps/web/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"next": "15.2.2",
3333
"next-themes": "^0.4.6",
3434
"p-map": "^7.0.3",
35+
"ps-tree": "^1.2.0",
3536
"react": "^19.0.0",
3637
"react-dom": "^19.0.0",
3738
"react-hook-form": "^7.54.2",
@@ -46,6 +47,7 @@
4647
"@evals/eslint-config": "workspace:^",
4748
"@evals/typescript-config": "workspace:^",
4849
"@tailwindcss/postcss": "^4",
50+
"@types/ps-tree": "^1.1.6",
4951
"@types/react": "^19",
5052
"@types/react-dom": "^19",
5153
"tailwindcss": "^4"

evals/apps/web/src/app/api/runs/[id]/stream/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
1616
const client = new IpcClient(run.socketPath, () => {})
1717

1818
const write = async (data: string | object) => {
19-
console.log(`[stream#${requestId}] write`, data)
19+
// console.log(`[stream#${requestId}] write`, data)
2020
const success = await stream.write(data)
2121

2222
if (!success) {

evals/apps/web/src/app/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,5 @@ export const dynamic = "force-dynamic"
66

77
export default async function Page() {
88
const runs = await getRuns()
9-
console.log(runs)
10-
119
return <Home runs={runs} />
1210
}
Lines changed: 59 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,69 @@
1+
"use client"
2+
3+
import { useCallback } from "react"
4+
import { Skull } from "lucide-react"
5+
6+
import { killProcessTree } from "@/lib/server/processes"
17
import { EventSourceStatus } from "@/hooks/use-event-source"
8+
import { useProcessList } from "@/hooks/use-process-tree"
29
import { cn } from "@/lib/utils"
10+
import { Button } from "@/components/ui"
311

412
type ConnectionStatusProps = {
513
status: EventSourceStatus
614
pid: number | null
715
}
816

9-
export const ConnectionStatus = ({ status, pid }: ConnectionStatusProps) => (
10-
<div className="flex items-center">
11-
<div className="flex flex-col items-end gap-1 font-mono text-xs border-r border-dotted pr-4 mr-4">
12-
<div>
13-
Status: <span className="capitalize">{status}</span>
17+
export const ConnectionStatus = (connectionStatus: ConnectionStatusProps) => {
18+
const { data: pids, isLoading } = useProcessList(connectionStatus.pid)
19+
const status = isLoading ? "loading" : pids === null ? "dead" : connectionStatus.status
20+
21+
const onKill = useCallback(async () => {
22+
if (connectionStatus.pid) {
23+
await killProcessTree(connectionStatus.pid)
24+
window.location.reload()
25+
}
26+
}, [connectionStatus.pid])
27+
28+
return (
29+
<div>
30+
<div className="flex items-center gap-2">
31+
<div className="flex items-center gap-2">
32+
<div>Status:</div>
33+
<div className="capitalize">{status}</div>
34+
</div>
35+
<div className="relative">
36+
<div
37+
className={cn("absolute size-2.5 rounded-full opacity-50 animate-ping", {
38+
"bg-gray-500": status === "loading",
39+
"bg-green-500": status === "connected",
40+
"bg-amber-500": status === "waiting",
41+
"bg-rose-500": status === "error" || status === "dead",
42+
})}
43+
/>
44+
<div
45+
className={cn("size-2.5 rounded-full", {
46+
"bg-gray-500": status === "loading",
47+
"bg-green-500": status === "connected",
48+
"bg-amber-500": status === "waiting",
49+
"bg-rose-500": status === "error" || status === "dead",
50+
})}
51+
/>
52+
</div>
53+
</div>
54+
<div className="flex items-center gap-2">
55+
<div>PIDs:</div>
56+
<div className="font-mono text-sm">{connectionStatus.pid}</div>
57+
{status === "connected" && (
58+
<>
59+
<div className="font-mono text-sm text-muted-foreground">{pids?.join(" ")}</div>
60+
<Button variant="ghost" size="sm" onClick={onKill}>
61+
Kill
62+
<Skull />
63+
</Button>
64+
</>
65+
)}
1466
</div>
15-
<div>PID: {pid}</div>
16-
</div>
17-
<div className="relative">
18-
<div
19-
className={cn("absolute size-2.5 rounded-full opacity-50 animate-ping", {
20-
"bg-green-500": status === "connected",
21-
"bg-amber-500": status === "waiting",
22-
"bg-rose-500": status === "error",
23-
})}
24-
/>
25-
<div
26-
className={cn("size-2.5 rounded-full", {
27-
"bg-green-500": status === "connected",
28-
"bg-amber-500": status === "waiting",
29-
"bg-rose-500": status === "error",
30-
})}
31-
/>
3267
</div>
33-
</div>
34-
)
68+
)
69+
}

evals/apps/web/src/app/runs/[id]/run.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DrawerHeader,
1414
DrawerTitle,
1515
ScrollArea,
16+
Separator,
1617
Table,
1718
TableBody,
1819
TableCell,
@@ -44,10 +45,13 @@ export function Run({ run }: { run: db.Run }) {
4445

4546
return (
4647
<>
47-
<div className="flex flex-col gap-2">
48-
<div>
49-
<div>{run.model}</div>
50-
{run.description && <div className="text-sm text-muted-foreground">{run.description}</div>}
48+
<div>
49+
<div className="mb-2">
50+
<div>
51+
<div>{run.model}</div>
52+
{run.description && <div className="text-sm text-muted-foreground">{run.description}</div>}
53+
</div>
54+
<ConnectionStatus status={status} pid={run.pid} />
5155
</div>
5256
{!tasks ? (
5357
<LoaderCircle className="size-4 animate-spin" />
@@ -110,9 +114,6 @@ export function Run({ run }: { run: db.Run }) {
110114
</Table>
111115
)}
112116
</div>
113-
<div className="absolute top-5 right-5">
114-
<ConnectionStatus status={status} pid={run.pid} />
115-
</div>
116117
<Drawer open={!!selectedTask} onOpenChange={() => setSelectedTask(undefined)}>
117118
<DrawerContent>
118119
<div className="mx-auto w-full max-w-2xl">
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useQuery } from "@tanstack/react-query"
2+
3+
import { getProcessList } from "@/lib/server/processes"
4+
5+
export const useProcessList = (pid: number | null) =>
6+
useQuery({
7+
queryKey: ["process-tree", pid],
8+
queryFn: () => (pid ? getProcessList(pid) : []),
9+
enabled: !!pid,
10+
})
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use server"
2+
3+
import psTree from "ps-tree"
4+
import { exec } from "child_process"
5+
6+
export const getProcessList = async (pid: number) => {
7+
const promise = new Promise<string>((resolve, reject) => {
8+
exec(`ps -p ${pid} -o pid=`, (err, stdout, stderr) => {
9+
if (err) {
10+
reject(stderr)
11+
}
12+
13+
resolve(stdout)
14+
})
15+
})
16+
17+
try {
18+
await promise
19+
} catch (_) {
20+
return null
21+
}
22+
23+
return new Promise<number[]>((resolve, reject) => {
24+
psTree(pid, (err, children) => {
25+
if (err) {
26+
reject(err)
27+
}
28+
29+
resolve(children.map((p) => parseInt(p.PID)))
30+
})
31+
})
32+
}
33+
34+
export const killProcessTree = async (pid: number) => {
35+
const descendants = await getProcessList(pid)
36+
37+
if (descendants === null) {
38+
return
39+
}
40+
41+
if (descendants.length > 0) {
42+
await exec(`kill -9 ${descendants.join(" ")}`)
43+
}
44+
45+
await exec(`kill -9 ${pid}`)
46+
}

0 commit comments

Comments
 (0)