electron: implement server env probe for in-process server (#21196)

pull/17803/head
Brendan Allan 2026-04-07 11:37:57 +08:00 committed by Brendan Allan
parent eb30104732
commit 14042e2a56
No known key found for this signature in database
GPG Key ID: 41E835AEA046A32E
6 changed files with 40 additions and 17 deletions

View File

@ -518,6 +518,7 @@ export const Terminal = (props: TerminalProps) => {
const next = new URL(url + `/pty/${id}/connect`) const next = new URL(url + `/pty/${id}/connect`)
next.searchParams.set("directory", directory) next.searchParams.set("directory", directory)
next.searchParams.set("cursor", String(seek)) next.searchParams.set("cursor", String(seek))
next.searchParams.set("token", btoa(`${username}:${password}`))
next.protocol = next.protocol === "https:" ? "wss:" : "ws:" next.protocol = next.protocol === "https:" ? "wss:" : "ws:"
next.username = username next.username = username
next.password = password next.password = password

View File

@ -43,8 +43,8 @@ export default defineConfig({
enforce: "post", enforce: "post",
async closeBundle() { async closeBundle() {
for (const l of await fs.readdir(OPENCODE_SERVER_DIST)) { for (const l of await fs.readdir(OPENCODE_SERVER_DIST)) {
if (l.endsWith(".js")) continue if (!l.endsWith(".wasm")) continue
await fs.writeFile(`./out/main/${l}`, await fs.readFile(`${OPENCODE_SERVER_DIST}/${l}`)) await fs.writeFile(`./out/main/chunks/${l}`, await fs.readFile(`${OPENCODE_SERVER_DIST}/${l}`))
} }
}, },
}, },

View File

@ -36,7 +36,7 @@ import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu" import { createMenu } from "./menu"
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server"
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows" import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
import { Server } from "virtual:opencode-server" import type { Server } from "virtual:opencode-server"
const initEmitter = new EventEmitter() const initEmitter = new EventEmitter()
let initStep: InitStep = { phase: "server_waiting" } let initStep: InitStep = { phase: "server_waiting" }

View File

@ -1,5 +1,6 @@
import { Server, Log } from "virtual:opencode-server" import { app } from "electron"
import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants" import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants"
import { getUserShell, loadShellEnv } from "./shell-env"
import { store } from "./store" import { store } from "./store"
export type WslConfig = { enabled: boolean } export type WslConfig = { enabled: boolean }
@ -30,6 +31,8 @@ export function setWslConfig(config: WslConfig) {
} }
export async function spawnLocalServer(hostname: string, port: number, password: string) { export async function spawnLocalServer(hostname: string, port: number, password: string) {
prepareServerEnv(password)
const { Log, Server } = await import("virtual:opencode-server")
await Log.init({ level: "WARN" }) await Log.init({ level: "WARN" })
const listener = await Server.listen({ const listener = await Server.listen({
port, port,
@ -54,6 +57,22 @@ export async function spawnLocalServer(hostname: string, port: number, password:
return { listener, health: { wait } } return { listener, health: { wait } }
} }
function prepareServerEnv(password: string) {
const shell = process.platform === "win32" ? null : getUserShell()
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
const env = {
...process.env,
...shellEnv,
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
OPENCODE_CLIENT: "desktop",
OPENCODE_SERVER_USERNAME: "opencode",
OPENCODE_SERVER_PASSWORD: password,
XDG_STATE_HOME: app.getPath("userData"),
}
Object.assign(process.env, env)
}
export async function checkHealth(url: string, password?: string | null): Promise<boolean> { export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
let healthUrl: URL let healthUrl: URL
try { try {

View File

@ -1,7 +1,7 @@
import { spawnSync } from "node:child_process" import { spawnSync } from "node:child_process"
import { basename } from "node:path" import { basename } from "node:path"
const SHELL_ENV_TIMEOUT = 5_000 const TIMEOUT = 5_000
type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" } type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
@ -20,28 +20,28 @@ export function parseShellEnv(out: Buffer) {
return env return env
} }
function probeShellEnv(shell: string, mode: "-il" | "-l"): Probe { function probe(shell: string, mode: "-il" | "-l"): Probe {
const out = spawnSync(shell, [mode, "-c", "env -0"], { const out = spawnSync(shell, [mode, "-c", "env -0"], {
stdio: ["ignore", "pipe", "ignore"], stdio: ["ignore", "pipe", "ignore"],
timeout: SHELL_ENV_TIMEOUT, timeout: TIMEOUT,
windowsHide: true, windowsHide: true,
}) })
const err = out.error as NodeJS.ErrnoException | undefined const err = out.error as NodeJS.ErrnoException | undefined
if (err) { if (err) {
if (err.code === "ETIMEDOUT") return { type: "Timeout" } if (err.code === "ETIMEDOUT") return { type: "Timeout" }
console.log(`[cli] Shell env probe failed for ${shell} ${mode}: ${err.message}`) console.log(`[server] Shell env probe failed for ${shell} ${mode}: ${err.message}`)
return { type: "Unavailable" } return { type: "Unavailable" }
} }
if (out.status !== 0) { if (out.status !== 0) {
console.log(`[cli] Shell env probe exited with non-zero status for ${shell} ${mode}`) console.log(`[server] Shell env probe exited with non-zero status for ${shell} ${mode}`)
return { type: "Unavailable" } return { type: "Unavailable" }
} }
const env = parseShellEnv(out.stdout) const env = parseShellEnv(out.stdout)
if (Object.keys(env).length === 0) { if (Object.keys(env).length === 0) {
console.log(`[cli] Shell env probe returned empty env for ${shell} ${mode}`) console.log(`[server] Shell env probe returned empty env for ${shell} ${mode}`)
return { type: "Unavailable" } return { type: "Unavailable" }
} }
@ -56,27 +56,27 @@ export function isNushell(shell: string) {
export function loadShellEnv(shell: string) { export function loadShellEnv(shell: string) {
if (isNushell(shell)) { if (isNushell(shell)) {
console.log(`[cli] Skipping shell env probe for nushell: ${shell}`) console.log(`[server] Skipping shell env probe for nushell: ${shell}`)
return null return null
} }
const interactive = probeShellEnv(shell, "-il") const interactive = probe(shell, "-il")
if (interactive.type === "Loaded") { if (interactive.type === "Loaded") {
console.log(`[cli] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`) console.log(`[server] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`)
return interactive.value return interactive.value
} }
if (interactive.type === "Timeout") { if (interactive.type === "Timeout") {
console.warn(`[cli] Interactive shell env probe timed out: ${shell}`) console.warn(`[server] Interactive shell env probe timed out: ${shell}`)
return null return null
} }
const login = probeShellEnv(shell, "-l") const login = probe(shell, "-l")
if (login.type === "Loaded") { if (login.type === "Loaded") {
console.log(`[cli] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`) console.log(`[server] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`)
return login.value return login.value
} }
console.warn(`[cli] Falling back to app environment: ${shell}`) console.warn(`[server] Falling back to app environment: ${shell}`)
return null return null
} }

View File

@ -54,6 +54,9 @@ export namespace Server {
const password = Flag.OPENCODE_SERVER_PASSWORD const password = Flag.OPENCODE_SERVER_PASSWORD
if (!password) return next() if (!password) return next()
const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
if (c.req.query("token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("token")}`)
return basicAuth({ username, password })(c, next) return basicAuth({ username, password })(c, next)
}) })
.use(async (c, next) => { .use(async (c, next) => {