Skip to content

Commit debcff2

Browse files
authored
feat(core): add debug workspace server (#23590)
1 parent 8b33237 commit debcff2

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env bun
2+
3+
// This script runs a separate OpenCode server to be used as a remote
4+
// workspace, simulating a remote environment but all local to make
5+
// debugger easier
6+
//
7+
// *Important*: make sure you add the debug workspace plugin first.
8+
// In `.opencode/opencode.jsonc` in the root of this project add:
9+
//
10+
// "plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"]
11+
//
12+
// Afterwards, run `./packages/opencode/script/run-workspace-server`
13+
14+
import { stat } from "node:fs/promises"
15+
import { setTimeout as sleep } from "node:timers/promises"
16+
17+
const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
18+
const RESTART_POLL_INTERVAL = 250
19+
20+
async function readData() {
21+
return await Bun.file(DEV_DATA_FILE).json()
22+
}
23+
24+
async function readDataMtime() {
25+
return await stat(DEV_DATA_FILE)
26+
.then((info) => info.mtimeMs)
27+
.catch((error) => {
28+
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
29+
return undefined
30+
}
31+
32+
throw error
33+
})
34+
}
35+
36+
async function readSnapshot() {
37+
while (true) {
38+
try {
39+
const before = await readDataMtime()
40+
if (before === undefined) {
41+
await sleep(RESTART_POLL_INTERVAL)
42+
continue
43+
}
44+
45+
const data = await readData()
46+
const after = await readDataMtime()
47+
48+
if (before === after) {
49+
return { data, mtime: after }
50+
}
51+
} catch (error) {
52+
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
53+
await sleep(RESTART_POLL_INTERVAL)
54+
continue
55+
}
56+
57+
throw error
58+
}
59+
}
60+
}
61+
62+
function startDevServer(data: any) {
63+
const env = Object.fromEntries(
64+
Object.entries(data.env ?? {}).filter(([, value]) => value !== undefined),
65+
)
66+
67+
return Bun.spawn(["bun", "run", "dev", "serve", "--port", String(data.port), "--print-logs"], {
68+
env: {
69+
...process.env,
70+
...env,
71+
XDG_DATA_HOME: "/tmp/data",
72+
},
73+
stdin: "inherit",
74+
stdout: "inherit",
75+
stderr: "inherit",
76+
})
77+
}
78+
79+
async function waitForRestartSignal(mtime: number, signal: AbortSignal) {
80+
while (!signal.aborted) {
81+
await sleep(RESTART_POLL_INTERVAL)
82+
if (signal.aborted) return false
83+
if ((await readDataMtime()) !== mtime) return true
84+
}
85+
86+
return false
87+
}
88+
89+
while (true) {
90+
const { data, mtime } = await readSnapshot()
91+
const proc = startDevServer(data)
92+
const restartAbort = new AbortController()
93+
94+
const result = await Promise.race([
95+
proc.exited.then((code) => ({ type: "exit" as const, code })),
96+
waitForRestartSignal(mtime, restartAbort.signal).then((restart) => ({ type: "restart" as const, restart })),
97+
])
98+
99+
restartAbort.abort()
100+
101+
if (result.type === "restart" && result.restart) {
102+
proc.kill()
103+
await proc.exited
104+
continue
105+
}
106+
107+
process.exit(result.code)
108+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { Plugin } from "@opencode-ai/plugin"
2+
import { rename, writeFile } from "node:fs/promises"
3+
import { randomInt } from "node:crypto"
4+
import { setTimeout as sleep } from "node:timers/promises"
5+
6+
const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
7+
const DEV_DATA_TEMP_FILE = `${DEV_DATA_FILE}.tmp`
8+
9+
async function waitForHealth(port: number) {
10+
const url = `http://127.0.0.1:${port}/global/health`
11+
const started = Date.now()
12+
13+
while (Date.now() - started < 30_000) {
14+
try {
15+
const response = await fetch(url)
16+
if (response.ok) {
17+
return
18+
}
19+
} catch {}
20+
21+
await sleep(250)
22+
}
23+
24+
throw new Error(`Timed out waiting for debug server health check at ${url}`)
25+
}
26+
27+
let PORT: number | undefined
28+
29+
async function writeDebugData(port: number, id: string, env: Record<string, string | undefined>) {
30+
await writeFile(
31+
DEV_DATA_TEMP_FILE,
32+
JSON.stringify(
33+
{
34+
port,
35+
id,
36+
env,
37+
},
38+
null,
39+
2,
40+
),
41+
)
42+
43+
await rename(DEV_DATA_TEMP_FILE, DEV_DATA_FILE)
44+
}
45+
46+
export const DebugWorkspacePlugin: Plugin = async ({ experimental_workspace }) => {
47+
experimental_workspace.register("debug", {
48+
name: "Debug",
49+
description: "Create a debugging server",
50+
configure(config) {
51+
return config
52+
},
53+
async create(config, env) {
54+
const port = randomInt(5000, 9001)
55+
PORT = port
56+
57+
await writeDebugData(port, config.id, env)
58+
59+
await waitForHealth(port)
60+
},
61+
async remove(_config) {},
62+
target(_config) {
63+
return {
64+
type: "remote",
65+
url: `http://localhost:${PORT!}/`,
66+
}
67+
},
68+
})
69+
70+
return {}
71+
}
72+
73+
export default DebugWorkspacePlugin

0 commit comments

Comments
 (0)