fix(desktop): stop freezing the main process during the login shell env probe (#63208)

This commit is contained in:
Aiden Cline
2026-11-10 17:19:24 -06:00
committed by GitHub
Unverified
parent 01e577f4dd
commit db871272af
2 changed files with 57 additions and 11 deletions
@@ -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 })
}
},
)
})
+29 -10
View File
@@ -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
}