mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(inspector): retain bootstrap request until cancellation settles
This commit is contained in:
1 parent
ba4395d046
commit
e7436013d1
5 files changed
+23
-9
No files matched your search
@@ -18,8 +18,8 @@
|
||||
en: 6d290aabc543f878
|
||||
zh: 7ad283b159ae2c29
|
||||
/deepseek-ai-dsh-experimental-inspector/runtime-layout:
|
||||
en: 13f495af2fec307d
|
||||
zh: ec4bebf24a87ec54
|
||||
en: 7db0c435e07d5005
|
||||
zh: 1573972326c33893
|
||||
/deepseek-ai-dsh-experimental-inspector/configuration:
|
||||
en: d094842893700446
|
||||
zh: 7a7f880ba795948f
|
||||
|
||||
@@ -48,7 +48,7 @@ The frontend uses browser runtime adapters and excludes Node-only transports at
|
||||
<a id="runtime-layout"></a>
|
||||
## Runtime layout
|
||||
|
||||
The Worker never accesses live Cordis objects: the shared Host/Client collector projects them into validated snapshots before transport. Cordis owns plugin composition, `ctx.inspector` registration, bootstrap injection, and disposal. A late-loading Client fetches its bootstrap through an authenticated Host route, which the Host registers before it starts the Worker: a request waits for Worker readiness and then receives the bootstrap, or `503` when startup fails; an aborted request stops waiting. When the platform cannot run the Worker and reports Node's `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM`, such as the browser preview's Worker host, the Host logs one warning and the plugin still starts with only that route, which answers `503`; it provides no `ctx.inspector`, DevTools routes, bootstrap injection, or `devtools://` URL. Any other startup error still rejects the plugin. Bootstrap transport and source-setup failures warn and retain connection-reset recovery for both injected and fetched startup; invalid bootstrap data or service-registration failures reject startup. Source replacement joins old service-consumer cleanup before claiming another source, and unloading an active plugin cancels its pending bootstrap refresh.
|
||||
The Worker never accesses live Cordis objects: the shared Host/Client collector projects them into validated snapshots before transport. Cordis owns plugin composition, `ctx.inspector` registration, bootstrap injection, and disposal. A late-loading Client fetches its bootstrap through an authenticated Host route, which the Host registers before it starts the Worker: a request waits for Worker readiness and then receives the bootstrap, or `503` when startup fails; an aborted request stops waiting even if garbage collection runs during startup. When the platform cannot run the Worker and reports Node's `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM`, such as the browser preview's Worker host, the Host logs one warning and the plugin still starts with only that route, which answers `503`; it provides no `ctx.inspector`, DevTools routes, bootstrap injection, or `devtools://` URL. Any other startup error still rejects the plugin. Bootstrap transport and source-setup failures warn and retain connection-reset recovery for both injected and fetched startup; invalid bootstrap data or service-registration failures reject startup. Source replacement joins old service-consumer cleanup before claiming another source, and unloading an active plugin cancels its pending bootstrap refresh.
|
||||
|
||||
The Host plugin starts the Worker and connects a dedicated `MessagePort`. The Client uses injected or fetched browser-source parameters to open an authenticated WebSocket directly to the Worker; it receives no DevTools URL. Chrome DevTools connects to the Worker's CDP WebSocket. A private `node:inspector.Session` per DevTools connection attaches from the Worker to the Host main thread, so Host debugging remains available while Host JavaScript is paused.
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Host 在相对于应用根目录的 `inspector/devtools/devtools_app.html` 路
|
||||
<a id="runtime-layout"></a>
|
||||
## 运行时布局
|
||||
|
||||
Worker 不访问实时 Cordis 对象;共享 Host/Client collector 会在传输前把它们投影成已验证快照。Cordis 负责插件组合、注册 `ctx.inspector`、注入 bootstrap 和 dispose(资源释放)。后加载的 Client 通过已鉴权的 Host 路由获取 bootstrap。Host 在启动 Worker 之前注册该路由,请求会等到 Worker 就绪后返回 bootstrap,启动失败时返回 `503`;请求中止时立即停止等待。当前平台无法运行 Worker 并报告 Node 的 `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` 时(例如浏览器预览的 Worker host),Host 记录一条警告,插件仍照常启动,只保留该路由并返回 `503`,不提供 `ctx.inspector`、DevTools 路由、bootstrap 注入或 `devtools://` URL;其他任何启动错误仍使插件启动失败。初始注入与主动获取两条路径中,bootstrap 传输或 source 建立失败均记录警告并保留连接重置后的重试;bootstrap 数据非法或服务注册失败则使启动失败。替换 source 会等待旧服务消费者清理完成,再申请新 source;卸载已激活插件会取消其未完成的 bootstrap 刷新。
|
||||
Worker 不访问实时 Cordis 对象;共享 Host/Client collector 会在传输前把它们投影成已验证快照。Cordis 负责插件组合、注册 `ctx.inspector`、注入 bootstrap 和 dispose(资源释放)。后加载的 Client 通过已鉴权的 Host 路由获取 bootstrap。Host 在启动 Worker 之前注册该路由,请求会等到 Worker 就绪后返回 bootstrap,启动失败时返回 `503`;即使启动期间发生垃圾回收,请求中止时也立即停止等待。当前平台无法运行 Worker 并报告 Node 的 `ERR_FEATURE_UNAVAILABLE_ON_PLATFORM` 时(例如浏览器预览的 Worker host),Host 记录一条警告,插件仍照常启动,只保留该路由并返回 `503`,不提供 `ctx.inspector`、DevTools 路由、bootstrap 注入或 `devtools://` URL;其他任何启动错误仍使插件启动失败。初始注入与主动获取两条路径中,bootstrap 传输或 source 建立失败均记录警告并保留连接重置后的重试;bootstrap 数据非法或服务注册失败则使启动失败。替换 source 会等待旧服务消费者清理完成,再申请新 source;卸载已激活插件会取消其未完成的 bootstrap 刷新。
|
||||
|
||||
Host 插件启动 Worker 并连接专用 `MessagePort`。Client 使用注入或主动获取的浏览器 source 参数,直接向 Worker 打开带鉴权的 WebSocket,不接收 DevTools URL。Chrome DevTools 连接 Worker 的 CDP WebSocket。每条 DevTools 连接在 Worker 中独占一个连接 Host 主线程的 `node:inspector.Session`,因此 Host JavaScript 暂停时仍可调试 Host。
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export async function apply(ctx: Context, config: HostPluginConfig): Promise<voi
|
||||
try {
|
||||
disposers.push(ctx.connection.fetch.register({
|
||||
path: INSPECTOR_BOOTSTRAP_PATH, methods: ['GET'], requestBody: 'buffered',
|
||||
fetch: request => bootstrapResponse(ready.promise, request.signal),
|
||||
fetch: request => bootstrapResponse(ready.promise, request),
|
||||
}))
|
||||
const handle = await startInspector(spec).catch((error: unknown) => {
|
||||
if (!isUnavailableOnPlatform(error)) throw error
|
||||
@@ -196,10 +196,11 @@ function isUnavailableOnPlatform(error: unknown): boolean {
|
||||
/**
|
||||
* Answer one bootstrap request once Host initialization settles or the browser abandons the request.
|
||||
* @param ready - Resolves with the Client bootstrap, or with undefined after initialization failed or found the platform unavailable.
|
||||
* @param signal - Request signal; an aborted request stops waiting and receives no endpoint.
|
||||
* @param request - Retained until settlement so Node keeps forwarding the caller's abort to its signal.
|
||||
* @returns The bootstrap JSON, or 503 when the Inspector is unavailable or the request was aborted.
|
||||
*/
|
||||
async function bootstrapResponse(ready: Promise<InspectorClientBootstrap | undefined>, signal: AbortSignal): Promise<Response> {
|
||||
async function bootstrapResponse(ready: Promise<InspectorClientBootstrap | undefined>, request: Request): Promise<Response> {
|
||||
const signal = request.signal
|
||||
const headers = { 'cache-control': 'no-store' }
|
||||
const abandoned = Promise.withResolvers<undefined>()
|
||||
const abandon = () => { abandoned.resolve(undefined) }
|
||||
@@ -209,6 +210,6 @@ async function bootstrapResponse(ready: Promise<InspectorClientBootstrap | undef
|
||||
const client = await Promise.race([ready, abandoned.promise])
|
||||
return client === undefined ? new Response(null, { status: 503, headers }) : Response.json(client, { headers })
|
||||
} finally {
|
||||
signal.removeEventListener('abort', abandon)
|
||||
request.signal.removeEventListener('abort', abandon)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import type { Message } from '@deepseek-ai/cordis'
|
||||
import type { IndexInjection, WebServer } from '@deepseek-ai/dsh-host-webserver'
|
||||
import { HostConnectionService } from '@deepseek-ai/dsh-client-connection'
|
||||
import type { BrowserAuth } from '@deepseek-ai/dsh-client-connection/src/browser-auth.ts'
|
||||
import { setFlagsFromString } from 'node:v8'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import open from 'open'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { apply, Config, inject, name } from '../src/index.ts'
|
||||
@@ -154,7 +156,7 @@ describe('experimental Inspector Host bootstrap route', () => {
|
||||
expect(messages.filter(message => message.type === 'error')).toEqual([])
|
||||
})
|
||||
|
||||
it('stops waiting when a bootstrap request is aborted before the Worker is ready', async () => {
|
||||
it('stops waiting after garbage collection when a bootstrap request is aborted before the Worker is ready', async () => {
|
||||
const gate = gateStartup('start')
|
||||
const { fiber, bootstrap } = mount()
|
||||
await vi.waitFor(() => { expect(startInspector).toHaveBeenCalledOnce() })
|
||||
@@ -162,6 +164,7 @@ describe('experimental Inspector Host bootstrap route', () => {
|
||||
const abort = new AbortController()
|
||||
const abandoned = bootstrap(abort.signal)
|
||||
expect(await settledWithin(abandoned, 50)).toBe(false)
|
||||
await collectGarbage()
|
||||
abort.abort()
|
||||
expect((await abandoned).status).toBe(503)
|
||||
expect((await bootstrap(AbortSignal.abort())).status).toBe(503)
|
||||
@@ -172,6 +175,16 @@ describe('experimental Inspector Host bootstrap route', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/** Collect unreachable Request controllers before the caller aborts. */
|
||||
async function collectGarbage(): Promise<void> {
|
||||
setFlagsFromString('--expose-gc')
|
||||
const gc = runInNewContext('gc') as () => void
|
||||
for (let pass = 0; pass < 5; pass += 1) {
|
||||
gc()
|
||||
await new Promise<void>((resolve) => { setImmediate(resolve) })
|
||||
}
|
||||
}
|
||||
|
||||
/** Report whether a promise settles before a real-time deadline. */
|
||||
async function settledWithin(promise: Promise<unknown>, ms: number): Promise<boolean> {
|
||||
const timeout = new Promise<false>((resolve) => { setTimeout(() => { resolve(false) }, ms) })
|
||||
|
||||
Reference in new issue
Block a user