feat(desktop): move server to utilityProcess (#25962)

Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
Brendan Allan
2026-05-07 13:48:56 +08:00
committed by GitHub
co-authored by LukeParkerDev
parent 9b30ee2db2
commit 54a78c9224
7 changed files with 441 additions and 93 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ export default defineConfig({
}, },
build: { build: {
rollupOptions: { rollupOptions: {
input: { index: "src/main/index.ts" }, input: { index: "src/main/index.ts", sidecar: "src/main/sidecar.ts" },
}, },
externalizeDeps: { include: [nodePtyPkg] }, externalizeDeps: { include: [nodePtyPkg] },
}, },
+31 -24
View File
@@ -1,14 +1,22 @@
import { execFileSync } from "node:child_process" import { execFile, execFileSync } from "node:child_process"
import { existsSync, readFileSync, readdirSync } from "node:fs" import { access, readFile, readdir } from "node:fs/promises"
import { dirname, extname, join } from "node:path" import { dirname, extname, join } from "node:path"
import util from "node:util"
export function checkAppExists(appName: string): boolean { const execFilePromise = util.promisify(execFile)
const exists = (path: string) =>
access(path)
.then(() => true)
.catch(() => false)
export function checkAppExists(appName: string) {
if (process.platform === "win32") return true if (process.platform === "win32") return true
if (process.platform === "linux") return true if (process.platform === "linux") return true
return checkMacosApp(appName) return checkMacosApp(appName)
} }
export function resolveAppPath(appName: string): string | null { export function resolveAppPath(appName: string) {
if (process.platform !== "win32") return appName if (process.platform !== "win32") return appName
return resolveWindowsAppPath(appName) return resolveWindowsAppPath(appName)
} }
@@ -32,26 +40,25 @@ export function wslPath(path: string, mode: "windows" | "linux" | null): string
} }
} }
function checkMacosApp(appName: string) { async function checkMacosApp(appName: string) {
const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`] const locations = [`/Applications/${appName}.app`, `/System/Applications/${appName}.app`]
const home = process.env.HOME const home = process.env.HOME
if (home) locations.push(`${home}/Applications/${appName}.app`) if (home) locations.push(`${home}/Applications/${appName}.app`)
if (locations.some((location) => existsSync(location))) return true for (const location of locations) {
if (await exists(location)) return true
try {
execFileSync("which", [appName])
return true
} catch {
return false
} }
return execFilePromise("which", [appName])
.then(() => true)
.catch(() => false)
} }
function resolveWindowsAppPath(appName: string): string | null { async function resolveWindowsAppPath(appName: string): Promise<string | null> {
let output: string let output: string
try { try {
output = execFileSync("where", [appName]).toString() output = execFilePromise("where", [appName]).toString()
} catch { } catch {
return null return null
} }
@@ -66,8 +73,8 @@ function resolveWindowsAppPath(appName: string): string | null {
const exe = paths.find((path) => hasExt(path, "exe")) const exe = paths.find((path) => hasExt(path, "exe"))
if (exe) return exe if (exe) return exe
const resolveCmd = (path: string) => { const resolveCmd = async (path: string) => {
const content = readFileSync(path, "utf8") const content = await readFile(path, "utf8")
for (const token of content.split('"').map((value: string) => value.trim())) { for (const token of content.split('"').map((value: string) => value.trim())) {
const lower = token.toLowerCase() const lower = token.toLowerCase()
if (!lower.includes(".exe")) continue if (!lower.includes(".exe")) continue
@@ -85,10 +92,10 @@ function resolveWindowsAppPath(appName: string): string | null {
return join(current, part) return join(current, part)
}, base) }, base)
if (existsSync(resolved)) return resolved if (await exists(resolved)) return resolved
} }
if (existsSync(token)) return token if (await exists(token)) return token
} }
return null return null
@@ -96,20 +103,20 @@ function resolveWindowsAppPath(appName: string): string | null {
for (const path of paths) { for (const path of paths) {
if (hasExt(path, "cmd") || hasExt(path, "bat")) { if (hasExt(path, "cmd") || hasExt(path, "bat")) {
const resolved = resolveCmd(path) const resolved = await resolveCmd(path)
if (resolved) return resolved if (resolved) return resolved
} }
if (!extname(path)) { if (!extname(path)) {
const cmd = `${path}.cmd` const cmd = `${path}.cmd`
if (existsSync(cmd)) { if (await exists(cmd)) {
const resolved = resolveCmd(cmd) const resolved = await resolveCmd(cmd)
if (resolved) return resolved if (resolved) return resolved
} }
const bat = `${path}.bat` const bat = `${path}.bat`
if (existsSync(bat)) { if (await exists(bat)) {
const resolved = resolveCmd(bat) const resolved = await resolveCmd(bat)
if (resolved) return resolved if (resolved) return resolved
} }
} }
@@ -126,7 +133,7 @@ function resolveWindowsAppPath(appName: string): string | null {
const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))] const dirs = [dirname(path), dirname(dirname(path)), dirname(dirname(dirname(path)))]
for (const dir of dirs) { for (const dir of dirs) {
try { try {
for (const entry of readdirSync(dir)) { for (const entry of await readdir(dir)) {
const candidate = join(dir, entry) const candidate = join(dir, entry)
if (!hasExt(candidate, "exe")) continue if (!hasExt(candidate, "exe")) continue
const stem = entry.replace(/\.exe$/i, "") const stem = entry.replace(/\.exe$/i, "")
+1
View File
@@ -5,6 +5,7 @@ interface ImportMetaEnv {
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv
} }
declare module "virtual:opencode-server" { declare module "virtual:opencode-server" {
export namespace Server { export namespace Server {
export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen export const listen: typeof import("../../../opencode/dist/types/src/node").Server.listen
+41 -38
View File
@@ -47,7 +47,15 @@ import { registerIpcHandlers, sendDeepLinks, sendMenuCommand, sendSqliteMigratio
import { initLogging } from "./logging" import { initLogging } from "./logging"
import { parseMarkdown } from "./markdown" import { parseMarkdown } from "./markdown"
import { createMenu } from "./menu" import { createMenu } from "./menu"
import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" import {
getDefaultServerUrl,
getWslConfig,
preferAppEnv,
setDefaultServerUrl,
setWslConfig,
spawnLocalServer,
type SidecarListener,
} from "./server"
import { import {
createLoadingWindow, createLoadingWindow,
createMainWindow, createMainWindow,
@@ -55,15 +63,13 @@ import {
setBackgroundColor, setBackgroundColor,
setDockIcon, setDockIcon,
} from "./windows" } from "./windows"
import { drizzle } from "drizzle-orm/node-sqlite/driver"
import type { Server } from "virtual:opencode-server"
import { migrate } from "./migrate" import { migrate } from "./migrate"
const initEmitter = new EventEmitter() const initEmitter = new EventEmitter()
let initStep: InitStep = { phase: "server_waiting" } let initStep: InitStep = { phase: "server_waiting" }
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
let server: Server.Listener | null = null let server: SidecarListener | null = null
const loadingComplete = defer<void>() const loadingComplete = defer<void>()
const pendingDeepLinks: string[] = [] const pendingDeepLinks: string[] = []
@@ -107,6 +113,8 @@ function setupApp() {
return return
} }
preferAppEnv(app.getPath("userData"))
app.on("second-instance", (_event: Event, argv: string[]) => { app.on("second-instance", (_event: Event, argv: string[]) => {
const urls = argv.filter((arg: string) => arg.startsWith("opencode://")) const urls = argv.filter((arg: string) => arg.startsWith("opencode://"))
if (urls.length) { if (urls.length) {
@@ -123,17 +131,16 @@ function setupApp() {
}) })
app.on("before-quit", () => { app.on("before-quit", () => {
killSidecar() void killSidecar()
}) })
app.on("will-quit", () => { app.on("will-quit", () => {
killSidecar() void killSidecar()
}) })
for (const signal of ["SIGINT", "SIGTERM"] as const) { for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.on(signal, () => { process.on(signal, () => {
killSidecar() void killSidecar().finally(() => app.exit(0))
app.exit(0)
}) })
} }
@@ -184,7 +191,6 @@ function setInitStep(step: InitStep) {
async function initialize() { async function initialize() {
const needsMigration = !sqliteFileExists() const needsMigration = !sqliteFileExists()
const sqliteDone = needsMigration ? defer<void>() : undefined
let overlay: BrowserWindow | null = null let overlay: BrowserWindow | null = null
const port = await getSidecarPort() const port = await getSidecarPort()
@@ -199,31 +205,26 @@ async function initialize() {
setInitStep({ phase: "sqlite_waiting" }) setInitStep({ phase: "sqlite_waiting" })
if (overlay) sendSqliteMigrationProgress(overlay, progress) if (overlay) sendSqliteMigrationProgress(overlay, progress)
if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress) if (mainWindow) sendSqliteMigrationProgress(mainWindow, progress)
if (progress.type === "Done") sqliteDone?.resolve()
}) })
if (needsMigration) {
const { Database, JsonMigration } = await import("virtual:opencode-server")
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
progress: (event: { current: number; total: number }) => {
const percent = Math.round(event.current / event.total) * 100
initEmitter.emit("sqlite", { type: "InProgress", value: percent })
},
})
initEmitter.emit("sqlite", { type: "Done" })
sqliteDone?.resolve()
}
if (needsMigration) {
await sqliteDone?.promise
}
logger.log("spawning sidecar", { url }) logger.log("spawning sidecar", { url })
const { listener, health } = await spawnLocalServer(hostname, port, password, () => { const { listener, health } = await spawnLocalServer(
ensureLoopbackNoProxy() hostname,
useEnvProxy() port,
}) password,
() => {
ensureLoopbackNoProxy()
useEnvProxy()
},
{
needsMigration,
userDataPath: app.getPath("userData"),
onSqliteProgress: (progress) => initEmitter.emit("sqlite", progress),
onStdout: (message) => logger.log("sidecar stdout", { message }),
onStderr: (message) => logger.warn("sidecar stderr", { message }),
onExit: (code) => logger.warn("sidecar exited", { code }),
},
)
server = listener server = listener
serverReady.resolve({ serverReady.resolve({
url, url,
@@ -273,9 +274,10 @@ function wireMenu() {
}, },
reload: () => mainWindow?.reload(), reload: () => mainWindow?.reload(),
relaunch: () => { relaunch: () => {
killSidecar() void killSidecar().finally(() => {
app.relaunch() app.relaunch()
app.exit(0) app.exit(0)
})
}, },
}) })
} }
@@ -304,7 +306,7 @@ registerIpcHandlers({
getDisplayBackend: async () => null, getDisplayBackend: async () => null,
setDisplayBackend: async () => undefined, setDisplayBackend: async () => undefined,
parseMarkdown: async (markdown) => parseMarkdown(markdown), parseMarkdown: async (markdown) => parseMarkdown(markdown),
checkAppExists: async (appName) => checkAppExists(appName), checkAppExists: (appName) => checkAppExists(appName),
wslPath: async (path, mode) => wslPath(path, mode), wslPath: async (path, mode) => wslPath(path, mode),
resolveAppPath: async (appName) => resolveAppPath(appName), resolveAppPath: async (appName) => resolveAppPath(appName),
loadingWindowComplete: () => loadingComplete.resolve(), loadingWindowComplete: () => loadingComplete.resolve(),
@@ -314,10 +316,11 @@ registerIpcHandlers({
setBackgroundColor: (color) => setBackgroundColor(color), setBackgroundColor: (color) => setBackgroundColor(color),
}) })
function killSidecar() { async function killSidecar() {
if (!server) return if (!server) return
server.stop() const current = server
server = null server = null
await current.stop()
} }
function ensureLoopbackNoProxy() { function ensureLoopbackNoProxy() {
@@ -440,7 +443,7 @@ async function installUpdate() {
logger.log("installing downloaded update", { logger.log("installing downloaded update", {
version: downloadedUpdateVersion, version: downloadedUpdateVersion,
}) })
killSidecar() await killSidecar()
autoUpdater.quitAndInstall() autoUpdater.quitAndInstall()
} }
+1 -1
View File
@@ -19,7 +19,7 @@ const pickerFilters = (ext?: string[]) => {
} }
type Deps = { type Deps = {
killSidecar: () => void killSidecar: () => Promise<void> | void
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData> awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
getWindowConfig: () => Promise<WindowConfig> | WindowConfig getWindowConfig: () => Promise<WindowConfig> | WindowConfig
consumeInitialDeepLinks: () => Promise<string[]> | string[] consumeInitialDeepLinks: () => Promise<string[]> | string[]
+188 -29
View File
@@ -1,12 +1,37 @@
import { app } from "electron" import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import { app, utilityProcess } from "electron"
import type { Details } 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 { getUserShell, loadShellEnv, mergeShellEnv } from "./shell-env"
import { getStore } from "./store" import { getStore } from "./store"
import type { SqliteMigrationProgress } from "../preload/types"
export type WslConfig = { enabled: boolean } export type WslConfig = { enabled: boolean }
export type HealthCheck = { wait: Promise<void> } export type HealthCheck = { wait: Promise<void> }
type SidecarMessage =
| { type: "sqlite"; progress: SqliteMigrationProgress }
| { type: "ready" }
| { type: "stopped" }
| { type: "error"; error: { message: string; stack?: string } }
export type SidecarListener = { stop: () => Promise<void> }
const SIDECAR_SERVICE_NAME = "opencode server"
const SIDECAR_START_STALL_TIMEOUT = 60_000
const SIDECAR_STOP_TIMEOUT = 6_000
type SpawnLocalServerOptions = {
needsMigration: boolean
userDataPath: string
onSqliteProgress?: (progress: SqliteMigrationProgress) => void
onStdout?: (message: string) => void
onStderr?: (message: string) => void
onExit?: (code: number) => void
}
export function getDefaultServerUrl(): string | null { export function getDefaultServerUrl(): string | null {
const value = getStore().get(DEFAULT_SERVER_URL_KEY) const value = getStore().get(DEFAULT_SERVER_URL_KEY)
return typeof value === "string" ? value : null return typeof value === "string" ? value : null
@@ -30,49 +55,155 @@ export function setWslConfig(config: WslConfig) {
getStore().set(WSL_ENABLED_KEY, config.enabled) getStore().set(WSL_ENABLED_KEY, config.enabled)
} }
export async function spawnLocalServer(hostname: string, port: number, password: string, configureEnv?: () => void) { export function preferAppEnv(userDataPath: string) {
prepareServerEnv(password) const shell = process.platform === "win32" ? null : getUserShell()
Object.assign(
process.env,
mergeShellEnv(shell ? loadShellEnv(shell) : null, {
...process.env,
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
OPENCODE_CLIENT: "desktop",
XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath,
}),
)
}
export async function spawnLocalServer(
hostname: string,
port: number,
password: string,
configureEnv: () => void,
options: SpawnLocalServerOptions,
) {
configureEnv?.() configureEnv?.()
const { Log, Server } = await import("virtual:opencode-server") const sidecar = join(dirname(fileURLToPath(import.meta.url)), "sidecar.js")
await Log.init({ level: "WARN" }) const child = utilityProcess.fork(sidecar, [], {
const listener = await Server.listen({ cwd: process.cwd(),
port, env: createSidecarEnv(),
hostname, serviceName: SIDECAR_SERVICE_NAME,
username: "opencode", stdio: "pipe",
password, })
cors: ["oc://renderer"], let exited = false
const exit = defer<number>()
const onProcessGone = (_event: unknown, details: Details) => {
if (details.type !== "Utility" || details.name !== SIDECAR_SERVICE_NAME) return
options.onStderr?.(`utility process gone reason=${details.reason} exitCode=${details.exitCode}`)
}
app.on("child-process-gone", onProcessGone)
child.once("exit", (code) => {
exited = true
app.off("child-process-gone", onProcessGone)
options.onExit?.(code)
exit.resolve(code)
})
child.on("error", (error) => options.onStderr?.(`utility process error: ${serializeError(error).message}`))
child.stdout?.on("data", (chunk: Buffer) => options.onStdout?.(chunk.toString("utf8").trimEnd()))
child.stderr?.on("data", (chunk: Buffer) => options.onStderr?.(chunk.toString("utf8").trimEnd()))
await new Promise<void>((resolve, reject) => {
let done = false
let timeout: NodeJS.Timeout
const fail = (error: Error) => {
if (done) return
done = true
cleanup()
reject(error)
}
const refreshTimeout = () => {
clearTimeout(timeout)
timeout = setTimeout(() => {
fail(new Error(`Sidecar did not become ready within ${SIDECAR_START_STALL_TIMEOUT}ms: ${sidecar}`))
}, SIDECAR_START_STALL_TIMEOUT)
}
const onMessage = (message: SidecarMessage) => {
if (message.type === "sqlite") {
refreshTimeout()
options.onSqliteProgress?.(message.progress)
return
}
if (message.type === "ready") {
if (done) return
done = true
cleanup()
resolve()
return
}
if (message.type === "error") {
fail(Object.assign(new Error(message.error.message), { stack: message.error.stack }))
}
}
const onExit = (code: number) => {
fail(new Error(`Sidecar exited before ready with code ${code}`))
}
const cleanup = () => {
clearTimeout(timeout)
child.off("message", onMessage)
child.off("exit", onExit)
}
child.on("message", onMessage)
child.on("exit", onExit)
refreshTimeout()
child.postMessage({
type: "start",
hostname,
port,
password,
userDataPath: options.userDataPath,
needsMigration: options.needsMigration,
})
}).catch((error) => {
if (!exited) child.kill()
throw error
}) })
const wait = (async () => { const wait = (async () => {
const url = `http://${hostname}:${port}` const url = `http://${hostname}:${port}`
let healthy = false
const gone = exit.promise.then((code) => {
if (healthy) return
throw new Error(`Sidecar exited before health check passed with code ${code}`)
})
const ready = async () => { const ready = async () => {
while (true) { while (true) {
await new Promise((resolve) => setTimeout(resolve, 100)) await new Promise((resolve) => setTimeout(resolve, 100))
if (await checkHealth(url, password)) return if (await checkHealth(url, password)) {
healthy = true
return
}
} }
} }
await ready() await Promise.race([ready(), gone])
})() })()
return { listener, health: { wait } } let stopping: Promise<void> | undefined
}
function prepareServerEnv(password: string) { return {
const shell = process.platform === "win32" ? null : getUserShell() listener: {
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {} stop: () => {
const env = { if (stopping) return stopping
...process.env, if (exited) return Promise.resolve()
...shellEnv, child.postMessage({ type: "stop" })
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", stopping = Promise.race([
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", exit.promise.then(() => undefined),
OPENCODE_CLIENT: "desktop", delay(SIDECAR_STOP_TIMEOUT).then(() => {
OPENCODE_SERVER_USERNAME: "opencode", if (!exited) child.kill()
OPENCODE_SERVER_PASSWORD: password, }),
XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? app.getPath("userData"), ])
return stopping
},
},
health: { wait },
} }
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> {
@@ -100,3 +231,31 @@ export async function checkHealth(url: string, password?: string | null): Promis
return false return false
} }
} }
function createSidecarEnv(): Record<string, string> {
const env = Object.fromEntries(
Object.entries(process.env).flatMap(([key, value]) => (value === undefined ? [] : [[key, String(value)]])),
)
delete env.DEBUG
if (process.platform === "linux") delete env.LD_PRELOAD
return env
}
function delay(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms))
}
function serializeError(error: unknown) {
if (error instanceof Error) return { message: error.message, stack: error.stack }
return { message: String(error) }
}
function defer<T>() {
let resolve!: (value: T) => void
let reject!: (error: Error) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
+178
View File
@@ -0,0 +1,178 @@
import { drizzle } from "drizzle-orm/node-sqlite/driver"
import * as http from "node:http"
import * as tls from "node:tls"
type NodeHttpWithEnvProxy = typeof http & {
setGlobalProxyFromEnv: () => void
}
type NodeTlsWithSystemCertificates = typeof tls & {
getCACertificates: (type: "default" | "system") => string[]
setDefaultCACertificates: (certificates: string[]) => void
}
type StartCommand = {
type: "start"
hostname: string
port: number
password: string
userDataPath: string
needsMigration: boolean
}
type StopCommand = { type: "stop" }
type SidecarCommand = StartCommand | StopCommand
type SidecarMessage =
| { type: "sqlite"; progress: { type: "InProgress"; value: number } | { type: "Done" } }
| { type: "ready" }
| { type: "stopped" }
| { type: "error"; error: { message: string; stack?: string } }
type ParentPort = {
postMessage(message: SidecarMessage): void
on(event: "message", listener: (event: { data: unknown }) => void): void
}
type Listener = {
stop(close?: boolean): void | Promise<void>
}
const parentPort = getParentPort()
let listener: Listener | undefined
parentPort.on("message", (event) => {
const command = parseCommand(event.data)
if (!command) return
if (command.type === "stop") {
void stop()
return
}
void start(command)
})
async function start(command: StartCommand) {
try {
prepareSidecarEnv(command.password, command.userDataPath)
ensureLoopbackNoProxy()
useSystemCertificates()
useEnvProxy()
const { Database, JsonMigration, Log, Server } = await import("virtual:opencode-server")
await Log.init({ level: "WARN" })
if (command.needsMigration) {
await JsonMigration.run(drizzle({ client: Database.Client().$client }), {
progress: (event: { current: number; total: number }) => {
parentPort.postMessage({
type: "sqlite",
progress: {
type: "InProgress",
value: event.total === 0 ? 100 : Math.round((event.current / event.total) * 100),
},
})
},
})
parentPort.postMessage({ type: "sqlite", progress: { type: "Done" } })
}
listener = await Server.listen({
port: command.port,
hostname: command.hostname,
username: "opencode",
password: command.password,
cors: ["oc://renderer"],
})
parentPort.postMessage({ type: "ready" })
} catch (error) {
parentPort.postMessage({ type: "error", error: serializeError(error) })
setImmediate(() => process.exit(1))
}
}
async function stop() {
try {
await listener?.stop()
} finally {
listener = undefined
parentPort.postMessage({ type: "stopped" })
setImmediate(() => process.exit(0))
}
}
function prepareSidecarEnv(password: string, userDataPath: string) {
Object.assign(process.env, {
OPENCODE_SERVER_USERNAME: "opencode",
OPENCODE_SERVER_PASSWORD: password,
XDG_STATE_HOME: process.env.XDG_STATE_HOME ?? userDataPath,
})
}
function ensureLoopbackNoProxy() {
const loopback = ["127.0.0.1", "localhost", "::1"]
const upsert = (key: string) => {
const items = (process.env[key] ?? "")
.split(",")
.map((value: string) => value.trim())
.filter((value: string) => Boolean(value))
for (const host of loopback) {
if (items.some((value: string) => value.toLowerCase() === host)) continue
items.push(host)
}
process.env[key] = items.join(",")
}
upsert("NO_PROXY")
upsert("no_proxy")
}
function useSystemCertificates() {
try {
const nodeTls = tls as NodeTlsWithSystemCertificates
nodeTls.setDefaultCACertificates([
...new Set([...nodeTls.getCACertificates("default"), ...nodeTls.getCACertificates("system")]),
])
} catch (error) {
console.warn("failed to load system certificates", error)
}
}
function useEnvProxy() {
try {
;(http as NodeHttpWithEnvProxy).setGlobalProxyFromEnv()
} catch (error) {
console.warn("failed to load proxy environment", error)
}
}
function parseCommand(value: unknown): SidecarCommand | undefined {
if (!value || typeof value !== "object") return
const command = value as Partial<StartCommand | StopCommand>
if (command.type === "stop") return { type: "stop" }
if (command.type !== "start") return
if (typeof command.hostname !== "string") return
if (typeof command.port !== "number") return
if (typeof command.password !== "string") return
if (typeof command.userDataPath !== "string") return
if (typeof command.needsMigration !== "boolean") return
return {
type: "start",
hostname: command.hostname,
port: command.port,
password: command.password,
userDataPath: command.userDataPath,
needsMigration: command.needsMigration,
}
}
function serializeError(error: unknown) {
if (error instanceof Error) return { message: error.message, stack: error.stack }
return { message: String(error) }
}
function getParentPort() {
const port = process.parentPort as ParentPort | undefined
if (!port) throw new Error("Sidecar parent port unavailable")
return port
}