electron: implement server env probe for in-process server (#21196)
parent
eb30104732
commit
14042e2a56
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}`))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue