From 5cf0c49c77456ecfb2a6662dd91a15faf7cd9b98 Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Tue, 16 Jun 2026 22:29:42 +0800 Subject: [PATCH 1/2] fix: stop libopentui.so leaking to /tmp on every opencode launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under `bun build --compile`, @opentui/core's libopentui.so lives in bunfs and dlopen() cannot load the virtual path, so Bun extracted it to a random /tmp/.{hash}-00000000.so on EVERY process launch. That temp file is only unlinked by an exit handler, which is skipped on crash/SIGKILL — orphaning ~4.5MB per crashed launch. Observed in production: 55,232 files / ~248GB filling the disk and taking down co-located services. Patch @opentui/core-linux-x64 to materialize the embedded lib ONCE into a stable, content-addressed cache file (~/.cache/opentui/) and dlopen that real path. Reused across launches, so a crash can only ever leave the single cache file (which the next launch reuses). Non-bunfs (dev) paths pass through unchanged; every error path falls back to Bun's default behavior. Verified end-to-end on the compiled opencode-linux-x64 binary: 0 /tmp/.so across repeated launches; cache holds exactly one reused file; dev mode unchanged. Same one-file change applies identically to the other platform packages (core-darwin-arm64/x64, core-linux-arm64, core-win32-arm64/x64) — only linux-x64 is patched here as it's the verified target. --- bun.lock | 1 + package.json | 3 +- patches/@opentui%2Fcore-linux-x64@0.2.2.patch | 76 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 patches/@opentui%2Fcore-linux-x64@0.2.2.patch diff --git a/bun.lock b/bun.lock index 4e70576306..048cf9da11 100644 --- a/bun.lock +++ b/bun.lock @@ -647,6 +647,7 @@ "patchedDependencies": { "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", + "@opentui/core-linux-x64@0.2.2": "patches/@opentui%2Fcore-linux-x64@0.2.2.patch", "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", }, "overrides": { diff --git a/package.json b/package.json index 9d9207c5ea..da7c2f8364 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "patchedDependencies": { "@npmcli/agent@4.0.0": "patches/@npmcli%2Fagent@4.0.0.patch", "@standard-community/standard-openapi@0.2.9": "patches/@standard-community%2Fstandard-openapi@0.2.9.patch", - "solid-js@1.9.10": "patches/solid-js@1.9.10.patch" + "solid-js@1.9.10": "patches/solid-js@1.9.10.patch", + "@opentui/core-linux-x64@0.2.2": "patches/@opentui%2Fcore-linux-x64@0.2.2.patch" } } diff --git a/patches/@opentui%2Fcore-linux-x64@0.2.2.patch b/patches/@opentui%2Fcore-linux-x64@0.2.2.patch new file mode 100644 index 0000000000..df6412bf7a --- /dev/null +++ b/patches/@opentui%2Fcore-linux-x64@0.2.2.patch @@ -0,0 +1,76 @@ +diff --git a/index.ts b/index.ts +index 0a413ac9e1f520d655c28ef7a8877786ca79c7c6..bd983b42ff94644f41e69da9feec9088544c4102 100644 +--- a/index.ts ++++ b/index.ts +@@ -1,3 +1,69 @@ ++import { existsSync, mkdirSync, writeFileSync, renameSync, readdirSync, unlinkSync } from "node:fs" ++import { join } from "node:path" ++import { tmpdir } from "node:os" ++import { createHash } from "node:crypto" ++ + const module = await import("./libopentui.so", { with: { type: "file" } }) +-const path = module.default +-export default path; ++const embeddedPath = module.default ++ ++// Under `bun build --compile`, this asset lives in bunfs (a virtual path like ++// "/$bunfs/root/libopentui.so"). dlopen() can't load a virtual path, so Bun ++// extracts it to a random "/tmp/.{hash}-00000000.so" on EVERY launch. That temp ++// file is only unlinked by an exit handler, which is skipped on crash/SIGKILL — ++// so every crashed launch orphans ~4.5MB (observed: 55k files / 248GB). ++// ++// Fix: materialize the embedded bytes once to a stable, content-addressed cache ++// file and dlopen that real path. Reused across launches, so a crash can only ++// ever leave the single cache file (which the next launch reuses). Dev (non- ++// bunfs) paths pass through unchanged. ++function isBunfsPath(p: unknown): p is string { ++ return typeof p === "string" && (p.includes("$bunfs") || /^B:[\\/]~BUN/i.test(p)) ++} ++ ++function cacheDir(): string { ++ if (process.platform === "win32") { ++ return join(process.env.LOCALAPPDATA || process.env.APPDATA || tmpdir(), "opentui") ++ } ++ return join(process.env.XDG_CACHE_HOME || join(process.env.HOME || tmpdir(), ".cache"), "opentui") ++} ++ ++async function materialize(bunfsPath: string): Promise { ++ let bytes: Uint8Array ++ try { ++ bytes = new Uint8Array(await Bun.file(bunfsPath).arrayBuffer()) ++ } catch { ++ return bunfsPath ++ } ++ ++ const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 16) ++ const dir = cacheDir() ++ try { ++ mkdirSync(dir, { recursive: true }) ++ } catch {} ++ ++ const ext = bunfsPath.match(/\.(so|dylib|dll)$/i)?.[0]?.toLowerCase() || ".so" ++ const dest = join(dir, `libopentui-${process.platform}-${process.arch}-${hash}${ext}`) ++ ++ if (!existsSync(dest)) { ++ const tmp = `${dest}.${process.pid}.tmp` ++ try { ++ writeFileSync(tmp, bytes) ++ renameSync(tmp, dest) ++ } catch { ++ return bunfsPath ++ } ++ try { ++ for (const f of readdirSync(dir)) { ++ if (!/^libopentui-.*\.(so|dylib|dll)$/i.test(f)) continue ++ const fp = join(dir, f) ++ if (fp !== dest) { ++ try { unlinkSync(fp) } catch {} ++ } ++ } ++ } catch {} ++ } ++ ++ return dest ++} ++ ++export default isBunfsPath(embeddedPath) ? await materialize(embeddedPath) : embeddedPath From f483e2eaf37a66d09c40cd4a3b829308cc2116c7 Mon Sep 17 00:00:00 2001 From: ranxianglei Date: Tue, 16 Jun 2026 23:22:50 +0800 Subject: [PATCH 2/2] chore: retrigger CI (pr-standards now resilient on master via #2)