mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-15 04:27:20 +03:00
fix(desktop): stop freezing the main process during the login shell env probe (#63208)
This commit is contained in:
committed by
GitHub
Unverified
parent
01e577f4dd
commit
db871272af
@@ -1,8 +1,11 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import path from "node:path"
|
||||
import { NodePath } from "@effect/platform-node"
|
||||
import { Effect } from "effect"
|
||||
|
||||
import { isNushell, parseShellEnv, resolveUserShell } from "./shell-env"
|
||||
import { isNushell, loadShellEnv, parseShellEnv, resolveUserShell } from "./shell-env"
|
||||
|
||||
describe("shell env", () => {
|
||||
test("parseShellEnv reads null-delimited pairs and ignores invalid entries", () => {
|
||||
@@ -27,4 +30,28 @@ describe("shell env", () => {
|
||||
expect(check("C:\\Program Files\\nu.exe")).toBe(true)
|
||||
expect(check("/bin/zsh")).toBe(false)
|
||||
})
|
||||
|
||||
// The fake shell fails `-il` and holds `-l` until the test releases it, which a blocking probe never lets happen.
|
||||
test.skipIf(process.platform === "win32")(
|
||||
"loadShellEnv falls back to -l without blocking the event loop",
|
||||
async () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "opencode-shell-env-"))
|
||||
const shell = path.join(root, "shell")
|
||||
const release = path.join(root, "release")
|
||||
|
||||
try {
|
||||
writeFileSync(
|
||||
shell,
|
||||
`#!/bin/sh\n[ "$1" = "-il" ] && exit 1\nwhile [ ! -e "${release}" ]; do sleep 0.01; done\nFAKE_SHELL_ENV=1 exec /bin/sh -c "$3"\n`,
|
||||
)
|
||||
chmodSync(shell, 0o755)
|
||||
const env = Effect.runPromise(loadShellEnv(shell).pipe(Effect.provide(NodePath.layer)))
|
||||
|
||||
writeFileSync(release, "")
|
||||
expect((await env)?.FAKE_SHELL_ENV).toBe("1")
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawnSync } from "node:child_process"
|
||||
import { spawn } from "node:child_process"
|
||||
import { userInfo } from "node:os"
|
||||
import { Effect, Path } from "effect"
|
||||
|
||||
@@ -6,6 +6,11 @@ const TIMEOUT = 5_000
|
||||
|
||||
type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
|
||||
|
||||
type Run =
|
||||
| { type: "Exited"; status: number | null; stdout: Buffer }
|
||||
| { type: "Error"; error: Error }
|
||||
| { type: "Timeout" }
|
||||
|
||||
export function resolveUserShell(envShell: string | undefined, loginShell: string | null | undefined) {
|
||||
const resolvedLoginShell = loginShell && loginShell !== "unknown" ? loginShell : undefined
|
||||
|
||||
@@ -34,18 +39,32 @@ export function parseShellEnv(out: Buffer) {
|
||||
return env
|
||||
}
|
||||
|
||||
// Async so the main thread keeps painting the window and serving renderer assets while a slow rc file runs.
|
||||
const run = (shell: string, mode: "-il" | "-l") =>
|
||||
Effect.callback<Run>((resume) => {
|
||||
const child = spawn(shell, [mode, "-c", "env -0"], {
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
windowsHide: true,
|
||||
})
|
||||
|
||||
const chunks: Buffer[] = []
|
||||
|
||||
child.stdout.on("data", (chunk: Buffer) => chunks.push(chunk))
|
||||
child.on("error", (error) => resume(Effect.succeed({ type: "Error", error })))
|
||||
child.on("close", (status) => resume(Effect.succeed({ type: "Exited", status, stdout: Buffer.concat(chunks) })))
|
||||
|
||||
return Effect.sync(() => {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill()
|
||||
})
|
||||
}).pipe(Effect.timeoutOrElse({ duration: TIMEOUT, orElse: () => Effect.succeed<Run>({ type: "Timeout" }) }))
|
||||
|
||||
const probe = Effect.fn("ShellEnv.probe")(function* (shell: string, mode: "-il" | "-l") {
|
||||
const out = spawnSync(shell, [mode, "-c", "env -0"], {
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
timeout: TIMEOUT,
|
||||
windowsHide: true,
|
||||
})
|
||||
const out = yield* run(shell, mode)
|
||||
|
||||
const err = out.error as NodeJS.ErrnoException | undefined
|
||||
if (out.type === "Timeout") return { type: "Timeout" } satisfies Probe
|
||||
|
||||
if (err) {
|
||||
if (err.code === "ETIMEDOUT") return { type: "Timeout" } satisfies Probe
|
||||
yield* Effect.logWarning(`[server] Shell env probe failed for ${shell} ${mode}: ${err.message}`)
|
||||
if (out.type === "Error") {
|
||||
yield* Effect.logWarning(`[server] Shell env probe failed for ${shell} ${mode}: ${out.error.message}`)
|
||||
|
||||
return { type: "Unavailable" } satisfies Probe
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user