fix(desktop): inherit WSL shell environment
This commit is contained in:
parent
c9db6e9a1f
commit
467028a52d
3 changed files with 72 additions and 1 deletions
|
|
@ -3,6 +3,7 @@ import { existsSync } from "node:fs"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
import * as pty from "@lydell/node-pty"
|
import * as pty from "@lydell/node-pty"
|
||||||
import type { WslDistroProbe, WslInstalledDistro, WslOnlineDistro, WslRuntimeCheck } from "../../preload/types"
|
import type { WslDistroProbe, WslInstalledDistro, WslOnlineDistro, WslRuntimeCheck } from "../../preload/types"
|
||||||
|
import { parseShellEnv } from "../shell-env"
|
||||||
import { wslTerminalArgs } from "./policy"
|
import { wslTerminalArgs } from "./policy"
|
||||||
|
|
||||||
export type WslCommandLine = {
|
export type WslCommandLine = {
|
||||||
|
|
@ -32,6 +33,7 @@ export type RunWslOptions = {
|
||||||
|
|
||||||
const DEFAULT_WSL_TIMEOUT_MS = 20_000
|
const DEFAULT_WSL_TIMEOUT_MS = 20_000
|
||||||
const DEFAULT_WSL_INSTALL_TIMEOUT_MS = 15 * 60_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) {
|
export function wslArgs(args: string[], distro?: string | null, user?: string | null) {
|
||||||
return [...(distro ? ["-d", distro] : []), ...(user ? ["--user", user] : []), "--", ...args]
|
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)
|
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> {
|
export async function probeWslRuntime(opts?: RunWslOptions): Promise<WslRuntimeCheck> {
|
||||||
const version = await runWsl(["--version"], opts).catch((error) => ({
|
const version = await runWsl(["--version"], opts).catch((error) => ({
|
||||||
code: 1,
|
code: 1,
|
||||||
|
|
|
||||||
35
packages/desktop/src/main/wsl/sidecar.test.ts
Normal file
35
packages/desktop/src/main/wsl/sidecar.test.ts
Normal file
|
|
@ -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 })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -3,7 +3,14 @@ import { randomUUID } from "node:crypto"
|
||||||
import { createServer } from "node:net"
|
import { createServer } from "node:net"
|
||||||
import { app } from "electron"
|
import { app } from "electron"
|
||||||
import { checkHealth } from "../server"
|
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"
|
import { pollWslHealth } from "./startup"
|
||||||
|
|
||||||
export type WslSidecar = {
|
export type WslSidecar = {
|
||||||
|
|
@ -23,7 +30,9 @@ export async function spawnWslSidecar(
|
||||||
const port = await allocatePort()
|
const port = await allocatePort()
|
||||||
const password = randomUUID()
|
const password = randomUUID()
|
||||||
const username = "opencode"
|
const username = "opencode"
|
||||||
|
const env = await loadWslShellEnv(distro)
|
||||||
const script = [
|
const script = [
|
||||||
|
...wslShellEnvExports(env),
|
||||||
"set -euo pipefail",
|
"set -euo pipefail",
|
||||||
'cd "$HOME" || cd /',
|
'cd "$HOME" || cd /',
|
||||||
'PATH=$(awk -v RS=: -v ORS=: \'$0 !~ /^\\/mnt\\//\' <<<"$PATH" | sed "s/:$//")',
|
'PATH=$(awk -v RS=: -v ORS=: \'$0 !~ /^\\/mnt\\//\' <<<"$PATH" | sed "s/:$//")',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue