mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-21 05:10:58 +08:00
feat(core): add debug workspace server (#23590)
This commit is contained in:
108
packages/opencode/script/run-workspace-server
Executable file
108
packages/opencode/script/run-workspace-server
Executable file
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
// This script runs a separate OpenCode server to be used as a remote
|
||||
// workspace, simulating a remote environment but all local to make
|
||||
// debugger easier
|
||||
//
|
||||
// *Important*: make sure you add the debug workspace plugin first.
|
||||
// In `.opencode/opencode.jsonc` in the root of this project add:
|
||||
//
|
||||
// "plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"]
|
||||
//
|
||||
// Afterwards, run `./packages/opencode/script/run-workspace-server`
|
||||
|
||||
import { stat } from "node:fs/promises"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
|
||||
const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
|
||||
const RESTART_POLL_INTERVAL = 250
|
||||
|
||||
async function readData() {
|
||||
return await Bun.file(DEV_DATA_FILE).json()
|
||||
}
|
||||
|
||||
async function readDataMtime() {
|
||||
return await stat(DEV_DATA_FILE)
|
||||
.then((info) => info.mtimeMs)
|
||||
.catch((error) => {
|
||||
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
|
||||
return undefined
|
||||
}
|
||||
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
async function readSnapshot() {
|
||||
while (true) {
|
||||
try {
|
||||
const before = await readDataMtime()
|
||||
if (before === undefined) {
|
||||
await sleep(RESTART_POLL_INTERVAL)
|
||||
continue
|
||||
}
|
||||
|
||||
const data = await readData()
|
||||
const after = await readDataMtime()
|
||||
|
||||
if (before === after) {
|
||||
return { data, mtime: after }
|
||||
}
|
||||
} catch (error) {
|
||||
if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
|
||||
await sleep(RESTART_POLL_INTERVAL)
|
||||
continue
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function startDevServer(data: any) {
|
||||
const env = Object.fromEntries(
|
||||
Object.entries(data.env ?? {}).filter(([, value]) => value !== undefined),
|
||||
)
|
||||
|
||||
return Bun.spawn(["bun", "run", "dev", "serve", "--port", String(data.port), "--print-logs"], {
|
||||
env: {
|
||||
...process.env,
|
||||
...env,
|
||||
XDG_DATA_HOME: "/tmp/data",
|
||||
},
|
||||
stdin: "inherit",
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForRestartSignal(mtime: number, signal: AbortSignal) {
|
||||
while (!signal.aborted) {
|
||||
await sleep(RESTART_POLL_INTERVAL)
|
||||
if (signal.aborted) return false
|
||||
if ((await readDataMtime()) !== mtime) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { data, mtime } = await readSnapshot()
|
||||
const proc = startDevServer(data)
|
||||
const restartAbort = new AbortController()
|
||||
|
||||
const result = await Promise.race([
|
||||
proc.exited.then((code) => ({ type: "exit" as const, code })),
|
||||
waitForRestartSignal(mtime, restartAbort.signal).then((restart) => ({ type: "restart" as const, restart })),
|
||||
])
|
||||
|
||||
restartAbort.abort()
|
||||
|
||||
if (result.type === "restart" && result.restart) {
|
||||
proc.kill()
|
||||
await proc.exited
|
||||
continue
|
||||
}
|
||||
|
||||
process.exit(result.code)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { Plugin } from "@opencode-ai/plugin"
|
||||
import { rename, writeFile } from "node:fs/promises"
|
||||
import { randomInt } from "node:crypto"
|
||||
import { setTimeout as sleep } from "node:timers/promises"
|
||||
|
||||
const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
|
||||
const DEV_DATA_TEMP_FILE = `${DEV_DATA_FILE}.tmp`
|
||||
|
||||
async function waitForHealth(port: number) {
|
||||
const url = `http://127.0.0.1:${port}/global/health`
|
||||
const started = Date.now()
|
||||
|
||||
while (Date.now() - started < 30_000) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
if (response.ok) {
|
||||
return
|
||||
}
|
||||
} catch {}
|
||||
|
||||
await sleep(250)
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for debug server health check at ${url}`)
|
||||
}
|
||||
|
||||
let PORT: number | undefined
|
||||
|
||||
async function writeDebugData(port: number, id: string, env: Record<string, string | undefined>) {
|
||||
await writeFile(
|
||||
DEV_DATA_TEMP_FILE,
|
||||
JSON.stringify(
|
||||
{
|
||||
port,
|
||||
id,
|
||||
env,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
await rename(DEV_DATA_TEMP_FILE, DEV_DATA_FILE)
|
||||
}
|
||||
|
||||
export const DebugWorkspacePlugin: Plugin = async ({ experimental_workspace }) => {
|
||||
experimental_workspace.register("debug", {
|
||||
name: "Debug",
|
||||
description: "Create a debugging server",
|
||||
configure(config) {
|
||||
return config
|
||||
},
|
||||
async create(config, env) {
|
||||
const port = randomInt(5000, 9001)
|
||||
PORT = port
|
||||
|
||||
await writeDebugData(port, config.id, env)
|
||||
|
||||
await waitForHealth(port)
|
||||
},
|
||||
async remove(_config) {},
|
||||
target(_config) {
|
||||
return {
|
||||
type: "remote",
|
||||
url: `http://localhost:${PORT!}/`,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
export default DebugWorkspacePlugin
|
||||
Reference in New Issue
Block a user