Compare commits

...

1 Commits

Author SHA1 Message Date
Jack 467028a52d fix(desktop): inherit WSL shell environment 2026-07-22 04:03:31 +00:00
3 changed files with 72 additions and 1 deletions
+27
View File
@@ -3,6 +3,7 @@ import { existsSync } from "node:fs"
import { join } from "node:path"
import * as pty from "@lydell/node-pty"
import type { WslDistroProbe, WslInstalledDistro, WslOnlineDistro, WslRuntimeCheck } from "../../preload/types"
import { parseShellEnv } from "../shell-env"
import { wslTerminalArgs } from "./policy"
export type WslCommandLine = {
@@ -32,6 +33,7 @@ export type RunWslOptions = {
const DEFAULT_WSL_TIMEOUT_MS = 20_000
const DEFAULT_WSL_INSTALL_TIMEOUT_MS = 15 * 60_000
const WSL_SHELL_ENV_TIMEOUT_MS = 5_000
export function wslArgs(args: string[], distro?: string | null, user?: string | null) {
return [...(distro ? ["-d", distro] : []), ...(user ? ["--user", user] : []), "--", ...args]
@@ -202,6 +204,31 @@ export function runWslSh(script: string, distro?: string | null, opts?: RunWslOp
return runWslInDistro(["sh", "-lc", script], distro, opts)
}
export function wslShellEnvProbeArgs(mode: "-il" | "-l") {
return ["bash", mode, "-c", "env -0"]
}
export function wslShellEnvExports(env: Record<string, string>) {
return Object.entries(env)
.filter(([key]) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
.map(([key, value]) => `export ${key}=${shellEscape(value)}`)
}
export async function loadWslShellEnv(distro: string) {
// Probe separately so the long-running server stays non-interactive and does not emit shell prompts or job-control warnings.
const probe = async (mode: "-il" | "-l") => {
const result = await runWslInDistro(wslShellEnvProbeArgs(mode), distro, {
timeoutMs: WSL_SHELL_ENV_TIMEOUT_MS,
}).catch(() => undefined)
if (!result || result.code !== 0) return undefined
const env = parseShellEnv(Buffer.from(result.stdout))
if (Object.keys(env).length === 0) return undefined
return env
}
return (await probe("-il")) ?? (await probe("-l")) ?? {}
}
export async function probeWslRuntime(opts?: RunWslOptions): Promise<WslRuntimeCheck> {
const version = await runWsl(["--version"], opts).catch((error) => ({
code: 1,
@@ -0,0 +1,35 @@
import { expect, test } from "bun:test"
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { spawnSync } from "node:child_process"
import { parseShellEnv } from "../shell-env"
import { wslShellEnvExports, wslShellEnvProbeArgs } from "./runtime"
test("probes the interactive login environment used by a WSL terminal", () => {
expect(wslShellEnvProbeArgs("-il")).toEqual(["bash", "-il", "-c", "env -0"])
})
test.skipIf(process.platform === "win32")("loads the login environment before starting the server", () => {
const home = mkdtempSync(join(tmpdir(), "opencode-wsl-env-"))
try {
writeFileSync(join(home, ".bash_profile"), '[ -f "$HOME/.bashrc" ] && . "$HOME/.bashrc"\n')
writeFileSync(join(home, ".bashrc"), "export OPENCODE_WSL_ENV_TEST='https://company.test/v1?x=$HOME&y=one two'\n")
const command = wslShellEnvProbeArgs("-il")
const probe = spawnSync(command[0], command.slice(1), {
encoding: "utf8",
env: { ...process.env, HOME: home },
})
const env = parseShellEnv(Buffer.from(probe.stdout))
const result = spawnSync("bash", ["-se"], {
input: [...wslShellEnvExports(env), 'printf "%s" "${OPENCODE_WSL_ENV_TEST:-missing}"'].join("\n"),
encoding: "utf8",
})
expect(probe.status).toBe(0)
expect(result.status).toBe(0)
expect(result.stdout).toBe("https://company.test/v1?x=$HOME&y=one two")
} finally {
rmSync(home, { recursive: true, force: true })
}
})
+10 -1
View File
@@ -3,7 +3,14 @@ import { randomUUID } from "node:crypto"
import { createServer } from "node:net"
import { app } from "electron"
import { checkHealth } from "../server"
import { type WslCommandLine, resolveWslOpencode, shellEscape, wslArgs } from "./runtime"
import {
loadWslShellEnv,
type WslCommandLine,
resolveWslOpencode,
shellEscape,
wslArgs,
wslShellEnvExports,
} from "./runtime"
import { pollWslHealth } from "./startup"
export type WslSidecar = {
@@ -23,7 +30,9 @@ export async function spawnWslSidecar(
const port = await allocatePort()
const password = randomUUID()
const username = "opencode"
const env = await loadWslShellEnv(distro)
const script = [
...wslShellEnvExports(env),
"set -euo pipefail",
'cd "$HOME" || cd /',
'PATH=$(awk -v RS=: -v ORS=: \'$0 !~ /^\\/mnt\\//\' <<<"$PATH" | sed "s/:$//")',