mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
fix(tui): don't crash when fs.watch throws (e.g. ENOSPC) (#51210)
Co-authored-by: Alireza Haghdoost <haghdoost@uber.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,7 @@ function segment(value: string) {
|
||||
return value
|
||||
}
|
||||
|
||||
function createStorage(root: string, channel: string) {
|
||||
export function createStorage(root: string, channel: string) {
|
||||
const entries = new Map<string, { readonly value: Entry<object>; readonly reload: () => void }>()
|
||||
const memories = new Map<string, MemoryEntry<object>>()
|
||||
const pending = new Set<Promise<void>>()
|
||||
@@ -111,17 +111,25 @@ function createStorage(root: string, channel: string) {
|
||||
}
|
||||
|
||||
let reload: ReturnType<typeof setTimeout> | undefined
|
||||
const watcher = watch(directory, () => {
|
||||
clearTimeout(reload)
|
||||
// Atomic writes notify for the temporary file before its final rename, and some
|
||||
// platforms coalesce the rename event. Reload after the event burst has settled.
|
||||
reload = setTimeout(() => entries.forEach((entry) => entry.reload()), 50)
|
||||
})
|
||||
let watcher: ReturnType<typeof watch> | undefined
|
||||
try {
|
||||
watcher = watch(directory, () => {
|
||||
clearTimeout(reload)
|
||||
// Atomic writes notify for the temporary file before its final rename, and some
|
||||
// platforms coalesce the rename event. Reload after the event burst has settled.
|
||||
reload = setTimeout(() => entries.forEach((entry) => entry.reload()), 50)
|
||||
})
|
||||
} catch (error) {
|
||||
// fs.watch throws synchronously (e.g. ENOSPC when the inotify watch limit is
|
||||
// exhausted). Losing cross-process live-reload is recoverable; crashing the
|
||||
// whole TUI over it is not, so degrade instead of propagating.
|
||||
console.error("Failed to watch storage directory, live-reload disabled", { directory, error })
|
||||
}
|
||||
return {
|
||||
storage,
|
||||
close: () => {
|
||||
clearTimeout(reload)
|
||||
watcher.close()
|
||||
watcher?.close()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { afterEach, expect, spyOn, test } from "bun:test"
|
||||
import * as fs from "fs"
|
||||
import { mkdtempSync, rmSync } from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import path from "path"
|
||||
import { createStorage } from "../../src/context/storage"
|
||||
|
||||
afterEach(() => {
|
||||
spyOn(fs, "watch").mockRestore()
|
||||
})
|
||||
|
||||
test("createStorage degrades gracefully when fs.watch throws (e.g. ENOSPC)", async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "storage-test-"))
|
||||
// fs.watch throws synchronously when inotify_add_watch fails (e.g. the watch
|
||||
// limit is exhausted). Simulate that scoped to this test only.
|
||||
spyOn(fs, "watch").mockImplementation(() => {
|
||||
throw Object.assign(new Error("ENOSPC: no space left on device, watch '/some/dir'"), { code: "ENOSPC" })
|
||||
})
|
||||
|
||||
try {
|
||||
let result: ReturnType<typeof createStorage> | undefined
|
||||
expect(() => {
|
||||
result = createStorage(dir, "next")
|
||||
}).not.toThrow()
|
||||
|
||||
// storage should still be usable even though the live-reload watcher failed to attach
|
||||
const [store, update] = result!.storage.store("kv", { initial: { count: 0 } })
|
||||
expect(store.count).toBe(0)
|
||||
await update((draft) => {
|
||||
draft.count = 1
|
||||
})
|
||||
expect(store.count).toBe(1)
|
||||
expect(() => result!.close()).not.toThrow()
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user