test(cli): reproduce updater executable orphaning
This commit is contained in:
parent
b47cfbee7c
commit
22263df332
1 changed files with 90 additions and 0 deletions
90
packages/cli/test/updater-orphan.test.ts
Normal file
90
packages/cli/test/updater-orphan.test.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
|
||||
const describeUnix = process.platform === "win32" ? describe.skip : describe
|
||||
const children: Bun.Subprocess[] = []
|
||||
|
||||
afterEach(() => children.splice(0).forEach((child) => child.kill()))
|
||||
|
||||
describeUnix("package-manager updater executable lifetime", () => {
|
||||
test(
|
||||
"replacing a globally installed native package orphans its running executable",
|
||||
async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-updater-orphan-"))
|
||||
const prefix = path.join(root, "prefix")
|
||||
const env = {
|
||||
...process.env,
|
||||
BUN_INSTALL_GLOBAL_DIR: path.join(prefix, "install", "global"),
|
||||
BUN_INSTALL_BIN: path.join(prefix, "bin"),
|
||||
}
|
||||
|
||||
try {
|
||||
const archives = await Promise.all(["1.0.0", "1.0.1"].map((version) => fixture(root, version)))
|
||||
await run(["bun", "install", "--global", archives[0]], undefined, env)
|
||||
|
||||
const executable = path.join(prefix, "bin", "opencode-updater-orphan-fixture")
|
||||
const child = Bun.spawn([executable], { stdout: "ignore", stderr: "ignore" })
|
||||
children.push(child)
|
||||
await Bun.sleep(100)
|
||||
|
||||
const before = await mappedExecutable(child.pid)
|
||||
await run(["bun", "remove", "--global", "opencode-updater-orphan-fixture"], undefined, env)
|
||||
await run(["bun", "install", "--global", archives[1]], undefined, env)
|
||||
const after = await mappedExecutable(child.pid)
|
||||
|
||||
console.log(JSON.stringify({ pid: child.pid, before, after }))
|
||||
expect(before).toContain("/node_modules/opencode-updater-orphan-fixture/bin/opencode-updater-orphan-fixture")
|
||||
expect(after).toBe(`${before} (deleted)`)
|
||||
expect(child.exitCode).toBeNull()
|
||||
} finally {
|
||||
await fs.rm(root, { recursive: true, force: true })
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
)
|
||||
})
|
||||
|
||||
async function fixture(root: string, version: string) {
|
||||
const directory = path.join(root, version)
|
||||
const source = path.join(directory, "fixture.c")
|
||||
const executable = path.join(directory, "bin", "opencode-updater-orphan-fixture")
|
||||
await fs.mkdir(path.dirname(executable), { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(directory, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "opencode-updater-orphan-fixture",
|
||||
version,
|
||||
bin: { "opencode-updater-orphan-fixture": "bin/opencode-updater-orphan-fixture" },
|
||||
}),
|
||||
)
|
||||
await fs.writeFile(
|
||||
source,
|
||||
"#include <unistd.h>\nint main(void) { for (;;) { access(\".\", F_OK); usleep(10000); } }\n",
|
||||
)
|
||||
await run(["cc", source, "-o", executable])
|
||||
await fs.chmod(executable, 0o755)
|
||||
await run(["bun", "pm", "pack", "--destination", root], directory)
|
||||
return path.join(root, `opencode-updater-orphan-fixture-${version}.tgz`)
|
||||
}
|
||||
|
||||
async function mappedExecutable(pid: number) {
|
||||
if (process.platform === "linux") return fs.readlink(`/proc/${pid}/exe`)
|
||||
const result = await run(["lsof", "-a", "-p", String(pid), "-d", "txt", "-Fn"])
|
||||
return result
|
||||
.split("\n")
|
||||
.find((line) => line.startsWith("n"))
|
||||
?.slice(1)
|
||||
}
|
||||
|
||||
async function run(command: string[], cwd?: string, env?: Record<string, string | undefined>) {
|
||||
const result = Bun.spawn(command, { cwd, env, stdout: "pipe", stderr: "pipe" })
|
||||
const [code, stdout, stderr] = await Promise.all([
|
||||
result.exited,
|
||||
new Response(result.stdout).text(),
|
||||
new Response(result.stderr).text(),
|
||||
])
|
||||
if (code === 0) return stdout.trim()
|
||||
throw new Error(`${command.join(" ")} failed (${code}): ${stderr.trim()}`)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue