perf(wsl): pooled wsl.exe command loops and orcad compile cache on the WSL wake path (C4 follow-up) (#27923)

* perf(wsl): run stdin-free probes over pooled wsl.exe command loops; cache orcad's compiled bundle

The WSL wake path made ~37 sequential wsl.exe starts at ~180 ms each. Stdin-free
execCommand calls now go through a few long-lived wsl.exe sh loops (one command at a
time per loop, own session, stdin at EOF, base64-framed output). SSH has no such
method, so its commands and options are unchanged.

orcad enables Node's compile cache inside a client-managed slot, and hands it to the
profile preflight child, which compiles the same bundle.

* fix(orcad): share the slot compile cache with the preflight only in a managed slot

An unmanaged orcad started with its own NODE_COMPILE_CACHE kept that
location for its preflight before; never redirect it into the install dir.
This commit is contained in:
Neil
2026-10-13 04:27:48 -07:00
committed by GitHub
parent 121386908b
commit e09d732d17
17 changed files with 848 additions and 26 deletions
+5 -1
View File
@@ -5,6 +5,7 @@ import { dirname, join } from 'node:path'
import { ORCAD_SERVER_ENTRY_FILENAME } from '../../shared/orcad-artifacts'
import { assertOrcadServerRuntime, handoffToBundledOrcad } from './orcad-bundled-runtime'
import { ORCAD_EXIT_CONFIGURATION } from './orcad-configuration-exit'
import { enableManagedOrcadCompileCache } from './orcad-compile-cache'
declare const ORCAD_SERVER_SHA256: string
@@ -18,7 +19,10 @@ try {
if (createHash('sha256').update(readFileSync(server)).digest('hex') !== ORCAD_SERVER_SHA256) {
throw new Error('The Orca server does not match its launcher')
}
loadServer = () => createRequire(entry)(server)
loadServer = () => {
enableManagedOrcadCompileCache(dirname(entry))
createRequire(entry)(server)
}
}
} catch (error) {
console.error('orcad: failed to launch:', error)
@@ -0,0 +1,53 @@
import { join } from 'node:path'
import { describe, expect, it, vi } from 'vitest'
import { ORCAD_MANAGED_ACTIVATION_ROOT_ENV } from '../../shared/orcad-managed-launch-env'
import {
ORCAD_COMPILE_CACHE_DIRNAME,
enableManagedOrcadCompileCache,
orcadCompileCacheEnv
} from './orcad-compile-cache'
const managed = {
[ORCAD_MANAGED_ACTIVATION_ROOT_ENV]: '/home/u/.orca-remote/.fence'
}
describe('managed orcad compile cache', () => {
it('caches inside a managed slot, where slot GC removes it with the slot', () => {
const enableCompileCache = vi.fn()
enableManagedOrcadCompileCache('/home/u/.orca-remote/orcad-1+ab', managed, {
enableCompileCache
})
expect(enableCompileCache).toHaveBeenCalledWith(
join('/home/u/.orca-remote/orcad-1+ab', ORCAD_COMPILE_CACHE_DIRNAME)
)
})
it('never writes into a slot no client manages, such as a packaged app', () => {
const enableCompileCache = vi.fn()
enableManagedOrcadCompileCache('/Applications/Orca.app/orcad', {}, { enableCompileCache })
expect(enableCompileCache).not.toHaveBeenCalled()
})
it('starts as before on a runtime without the API or with an unwritable slot', () => {
expect(() => enableManagedOrcadCompileCache('/slot', managed, {})).not.toThrow()
const enableCompileCache = vi.fn(() => {
throw new Error('EACCES')
})
expect(() =>
enableManagedOrcadCompileCache('/slot', managed, { enableCompileCache })
).not.toThrow()
})
it('hands the preflight child the slot cache only when this process enabled one', () => {
const enabled = { getCompileCacheDir: () => '/slot/x/v24' }
expect(orcadCompileCacheEnv('/slot', enabled, managed)).toEqual({
NODE_COMPILE_CACHE: join('/slot', ORCAD_COMPILE_CACHE_DIRNAME)
})
expect(orcadCompileCacheEnv('/slot', { getCompileCacheDir: () => undefined }, managed)).toEqual(
{}
)
expect(orcadCompileCacheEnv('/slot', {}, managed)).toEqual({})
// Regression: an unmanaged orcad's own NODE_COMPILE_CACHE stays where the operator put it.
expect(orcadCompileCacheEnv('/slot', enabled, {})).toEqual({})
})
})
+51
View File
@@ -0,0 +1,51 @@
/**
* V8 code cache for a managed slot's bundles. MEASURED on a low-spec WSL host: compiling
* orcad-server.js took ~600 ms per start, twice (server and its profile preflight); cached ~290 ms.
*/
import module from 'node:module'
import { join } from 'node:path'
import { ORCAD_MANAGED_ACTIVATION_ROOT_ENV } from '../../shared/orcad-managed-launch-env'
export const ORCAD_COMPILE_CACHE_DIRNAME = '.orcad-compile-cache'
type CompileCacheModule = {
enableCompileCache?: (directory: string) => unknown
getCompileCacheDir?: () => string | undefined
}
/**
* Only a client-managed slot (SSH or WSL), which slot GC deletes with its cache; never a packaged
* app directory, where a new file would break the bundle's signature. Never throws: an unwritable
* slot or a runtime without the API just compiles as before.
*/
export function enableManagedOrcadCompileCache(
slotDirectory: string,
env: NodeJS.ProcessEnv = process.env,
api: CompileCacheModule = module
): void {
if (!env[ORCAD_MANAGED_ACTIVATION_ROOT_ENV] || typeof api.enableCompileCache !== 'function') {
return
}
try {
api.enableCompileCache(join(slotDirectory, ORCAD_COMPILE_CACHE_DIRNAME))
} catch {
// Best effort by design.
}
}
/**
* Env that gives a disposable orcad child the same cache. Not for children that spawn user
* processes: an inherited NODE_COMPILE_CACHE would reach every Node program in a terminal.
*/
export function orcadCompileCacheEnv(
slotDirectory: string,
api: CompileCacheModule = module,
env: NodeJS.ProcessEnv = process.env
): Record<string, string> {
// An unmanaged orcad may have an operator's NODE_COMPILE_CACHE; never redirect it into the slot.
const enabled =
Boolean(env[ORCAD_MANAGED_ACTIVATION_ROOT_ENV]) &&
typeof api.getCompileCacheDir === 'function' &&
Boolean(api.getCompileCacheDir())
return enabled ? { NODE_COMPILE_CACHE: join(slotDirectory, ORCAD_COMPILE_CACHE_DIRNAME) } : {}
}
@@ -73,6 +73,7 @@ it('keeps the server and profile-state graph outside the compatibility launcher'
'src/main/orcad/orcad-app-paths.ts',
'src/main/orcad/orcad-configuration-exit.ts',
'src/main/orcad/orcad-bundled-runtime.ts',
'src/main/orcad/orcad-compile-cache.ts',
'src/main/orcad/launcher.ts'
])
expect(inputs.some((path) => path.startsWith('node_modules/'))).toBe(false)
+3 -1
View File
@@ -16,6 +16,7 @@ import { ORCAD_SERVER_ENTRY_FILENAME, ORCAD_VERSION_FILENAME } from '../../share
import { ORCAD_NODE_RUNTIME_IDENTITY } from '../../shared/orcad-node-runtime-identity'
import { runProcess } from '@orca/process-host'
import { preflightOrcadNativeRuntime } from './orcad-runtime-native-preflight'
import { orcadCompileCacheEnv } from './orcad-compile-cache'
import {
isRunningAsBundledOrcadRuntime,
OrcadBundledRuntimeError,
@@ -34,7 +35,8 @@ export async function preflightBundledOrcadStartup(): Promise<void> {
const result = await runProcess({
program: process.execPath,
args: [join(directory, ORCAD_SERVER_ENTRY_FILENAME), ORCAD_STARTUP_PREFLIGHT_FLAG, nonce],
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
// The preflight compiles the same bundle again, so it shares the server's code cache.
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1', ...orcadCompileCacheEnv(directory) },
timeoutMs: ORCAD_PROFILE_PREFLIGHT_TIMEOUT_MS,
maxOutputBytes: 64 * 1024,
terminationBarrier: true
+5
View File
@@ -17,6 +17,11 @@ export type RemoteCommandChannel = Duplex & {
/** What remote install, lock, GC and host-probe code uses from a host connection. */
export type RemoteInstallTransport = {
exec(command: string, options?: SshExecOptions): Promise<RemoteCommandChannel>
/**
* Optional: `exec` for a command whose stdin the caller never writes, which may run with stdin
* at EOF. Lets a transport whose every exec is a process start reuse one (WSL); SSH has none.
*/
execWithClosedStdin?(command: string, options?: SshExecOptions): Promise<RemoteCommandChannel>
usesSystemSshTransport(): boolean
/** Changes whenever the underlying transport is replaced; scopes per-transport verdicts. */
getConnectGeneration(): number
@@ -0,0 +1,60 @@
import { Duplex, PassThrough, Readable } from 'node:stream'
import { describe, expect, it, vi } from 'vitest'
import type { RemoteCommandChannel } from './remote-install-transport'
import { execCommand } from './ssh-relay-exec-command'
function answering(output: string): RemoteCommandChannel {
const channel = new Duplex({
emitClose: false,
read() {},
write: (_c, _e, done) => done()
})
Object.assign(channel, {
stdin: new PassThrough(),
stderr: new PassThrough(),
close() {}
})
setImmediate(() => {
channel.push(output)
channel.push(null)
channel.emit('close', 0)
})
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: stdin, stderr and close were assigned above.
return channel as unknown as RemoteCommandChannel
}
describe('execCommand transport routing', () => {
it('sends SSH the same command and options as before, through exec', async () => {
const exec = vi.fn(async (_command: string, _options?: unknown) => answering('ok'))
const conn = { exec, usesSystemSshTransport: () => false }
const signal = new AbortController().signal
await execCommand(conn, 'uname -sm', { signal })
expect(exec).toHaveBeenCalledTimes(1)
const [command, options] = exec.mock.calls[0] ?? ['', null]
expect(options).toEqual({ signal })
expect(command.split('\n').slice(1, -3)).toEqual(['uname -sm'])
await execCommand(conn, 'true', { wrapCommand: false })
expect(exec).toHaveBeenLastCalledWith('true', { wrapCommand: false })
})
it('uses a closed-stdin exec only when the caller has no stdin to send', async () => {
const exec = vi.fn(async () => answering('direct'))
const execWithClosedStdin = vi.fn(async () => answering('pooled'))
const conn = {
exec,
execWithClosedStdin,
usesSystemSshTransport: () => false
}
await expect(execCommand(conn, 'probe', { wrapCommand: false })).resolves.toBe('pooled')
expect(execWithClosedStdin).toHaveBeenCalledWith('probe', {
wrapCommand: false
})
await expect(
execCommand(conn, 'cat', {
wrapCommand: false,
stdin: Readable.from(['x'])
})
).resolves.toBe('direct')
expect(exec).toHaveBeenCalledWith('cat', { wrapCommand: false })
})
})
+6 -2
View File
@@ -73,7 +73,7 @@ export function sshCommandExitError(
}
export async function execCommand(
conn: Pick<RemoteInstallTransport, 'exec' | 'usesSystemSshTransport'>,
conn: Pick<RemoteInstallTransport, 'exec' | 'execWithClosedStdin' | 'usesSystemSshTransport'>,
command: string,
options?: ExecCommandOptions
): Promise<string> {
@@ -91,7 +91,11 @@ export async function execCommand(
const readPayload = (stdout: string): string => fence?.readStdout(stdout) ?? stdout
let channel: RemoteCommandChannel
try {
channel = await conn.exec(fence?.command ?? command, execOptions)
// Why only without `stdin`: nothing below writes or ends the channel's stdin otherwise.
channel =
!stdin && conn.execWithClosedStdin
? await conn.execWithClosedStdin(fence?.command ?? command, execOptions)
: await conn.exec(fence?.command ?? command, execOptions)
} catch (error) {
// Preserve identity/classifier fields while removing install-owner tokens.
redactRelayInstallMarkerError(error)
+143
View File
@@ -0,0 +1,143 @@
import type { PipedChildProcess } from '@orca/process-host/process-spec'
import type { RemoteCommandChannel } from '../ssh/remote-install-transport'
import { createSshOperationAbortError } from '../ssh/ssh-connection-utils'
import { startWslCommandLoop, wslCommandLoopMarker, type WslCommandLoop } from './wsl-command-loop'
export type WslCommandLoopPool = {
/** A channel on an idle loop, or null when the caller should start its own wsl.exe. */
run(command: string, signal?: AbortSignal): Promise<RemoteCommandChannel | null>
dispose(): void
}
type WslCommandLoopPoolOptions = {
/** Starts `wsl.exe --exec sh -c <loop script> <argv0> <marker>`. */
spawnLoop: (marker: string) => PipedChildProcess
/** Concurrent commands beyond this run on their own wsl.exe, as before. */
maxLoops?: number
idleMs?: number
/** A guest that cannot answer the self-test in time falls back to one wsl.exe per command. */
readyTimeoutMs?: number
}
export function createWslCommandLoopPool(options: WslCommandLoopPoolOptions): WslCommandLoopPool {
const maxLoops = options.maxLoops ?? 4
const idleMs = options.idleMs ?? 5_000
const readyTimeoutMs = options.readyTimeoutMs ?? 15_000
const loops = new Set<WslCommandLoop>()
const reserved = new Set<WslCommandLoop>()
const idleTimers = new Map<WslCommandLoop, ReturnType<typeof setTimeout>>()
// Sticky: a guest without mktemp/base64, or a wsl.exe that cannot keep a loop, stays direct.
let unavailable = false
let disposed = false
const retire = (loop: WslCommandLoop): void => {
clearTimeout(idleTimers.get(loop))
idleTimers.delete(loop)
loops.delete(loop)
reserved.delete(loop)
loop.end()
}
const create = (): WslCommandLoop => {
const marker = wslCommandLoopMarker()
const loop = startWslCommandLoop(options.spawnLoop(marker), marker)
loops.add(loop)
loop.onIdle(() => {
if (disposed) {
retire(loop)
return
}
const timer = setTimeout(() => retire(loop), idleMs)
timer.unref?.()
idleTimers.set(loop, timer)
})
return loop
}
const reserve = (): WslCommandLoop | null => {
for (const loop of loops) {
if (!loop.alive) {
retire(loop)
} else if (!loop.busy && !reserved.has(loop)) {
clearTimeout(idleTimers.get(loop))
idleTimers.delete(loop)
reserved.add(loop)
return loop
}
}
if (loops.size >= maxLoops) {
return null
}
let loop: WslCommandLoop
try {
loop = create()
} catch {
// The caller's own wsl.exe start reports the real failure.
unavailable = true
return null
}
reserved.add(loop)
return loop
}
return {
async run(command, signal) {
if (unavailable || disposed) {
return null
}
if (signal?.aborted) {
throw createSshOperationAbortError()
}
const loop = reserve()
if (!loop) {
return null
}
try {
await waitReady(loop, readyTimeoutMs, signal)
} catch {
retire(loop)
loop.kill()
if (signal?.aborted) {
throw createSshOperationAbortError()
}
unavailable = true
return null
}
reserved.delete(loop)
if (!loop.alive || loop.busy) {
retire(loop)
return null
}
return loop.run(command)
},
dispose() {
disposed = true
for (const loop of loops) {
if (!loop.busy) {
retire(loop)
}
}
}
}
}
function waitReady(loop: WslCommandLoop, timeoutMs: number, signal?: AbortSignal): Promise<void> {
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(
() => finish(new Error('The WSL command loop did not start')),
timeoutMs
)
const onAbort = (): void => finish(createSshOperationAbortError())
const finish = (error?: Error): void => {
clearTimeout(timer)
signal?.removeEventListener('abort', onAbort)
if (error) {
reject(error)
} else {
resolve()
}
}
signal?.addEventListener('abort', onAbort, { once: true })
loop.ready.then(() => finish(), finish)
})
}
+135
View File
@@ -0,0 +1,135 @@
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { spawnProcess } from '@orca/process-host'
import type { PipedChildProcess } from '@orca/process-host/process-spec'
import { execCommand } from '../ssh/ssh-relay-exec-command'
import { WSL_COMMAND_LOOP_ARGV0, WSL_COMMAND_LOOP_SCRIPT } from './wsl-command-loop'
import { createWslCommandLoopPool, type WslCommandLoopPool } from './wsl-command-loop-pool'
// The guest side is plain POSIX sh, so a local sh stands in for `wsl.exe --exec sh`.
const posix = process.platform !== 'win32'
const spawned: PipedChildProcess[] = []
const pools: WslCommandLoopPool[] = []
function pool(options: { maxLoops?: number; script?: string } = {}) {
const created = createWslCommandLoopPool({
maxLoops: options.maxLoops,
spawnLoop: (marker) => {
const child = spawnProcess({
program: 'sh',
args: ['-c', options.script ?? WSL_COMMAND_LOOP_SCRIPT, WSL_COMMAND_LOOP_ARGV0, marker]
})
spawned.push(child)
return child
}
})
pools.push(created)
const conn = {
exec: () => Promise.reject(new Error('direct exec')),
execWithClosedStdin: async (command: string, options?: { signal?: AbortSignal }) => {
const channel = await created.run(command, options?.signal)
if (!channel) {
throw new Error('loop unavailable')
}
return channel
},
usesSystemSshTransport: () => false
}
return { pool: created, conn }
}
afterEach(() => {
pools.splice(0).forEach((entry) => entry.dispose())
spawned.splice(0).forEach((child) => child.kill())
})
describe.runIf(posix)('WSL command loop', () => {
it('runs many commands over one process with their own stdout, stderr and status', async () => {
const { conn } = pool()
expect(await execCommand(conn, 'echo one')).toBe('one\n')
expect(await execCommand(conn, "printf 'a\\nb'")).toBe('a\nb')
let stderr = ''
expect(
await execCommand(conn, 'echo out; echo err >&2', {
onStderr: (text) => (stderr = text)
})
).toBe('out\n')
expect(stderr).toBe('err\n')
await expect(execCommand(conn, 'echo partial; exit 7')).rejects.toMatchObject({
exitCode: 7,
stdout: 'partial\n'
})
expect(spawned).toHaveLength(1)
})
it('keeps binary output, quoting and multi-line scripts intact', async () => {
const { conn } = pool()
const script = "x='it''s'\nfor i in 1 2; do printf '%s-%s\\n' \"$x\" $i; done"
expect(await execCommand(conn, script)).toBe('its-1\nits-2\n')
expect(await execCommand(conn, 'printf "\\0\\377"')).toBe(
Buffer.from([0, 0xff]).toString('utf-8')
)
})
it('gives each command EOF on stdin, its own session and no loop variables', async () => {
const { conn } = pool()
expect(await execCommand(conn, 'cat; echo eof')).toBe('eof\n')
expect(await execCommand(conn, 'echo "[${m:-}${d:-}${l:-}]"')).toBe('[]\n')
const sessionLeads = await execCommand(
conn,
'if command -v setsid >/dev/null 2>&1; then [ "$(ps -o sid= -p $$ | tr -d " ")" = "$$" ] && echo own || echo shared; else echo own; fi'
)
expect(sessionLeads).toBe('own\n')
})
it('runs concurrent commands on separate loops up to the cap, then declines', async () => {
const { pool: loops, conn } = pool({ maxLoops: 2 })
const slow = [execCommand(conn, 'sleep 0.3; echo a'), execCommand(conn, 'sleep 0.3; echo b')]
await new Promise((resolve) => setTimeout(resolve, 50))
await expect(loops.run('echo c')).resolves.toBeNull()
expect(await Promise.all(slow)).toEqual(['a\n', 'b\n'])
expect(await execCommand(conn, 'echo d')).toBe('d\n')
expect(spawned).toHaveLength(2)
})
it('does not let a backgrounded writer leak into the next command', async () => {
const dir = mkdtempSync(join(tmpdir(), 'orca-wsl-loop-'))
try {
const { conn } = pool()
await execCommand(conn, `(sleep 0.2; echo late; echo done > '${dir}/f') &`)
await new Promise((resolve) => setTimeout(resolve, 400))
expect(readFileSync(join(dir, 'f'), 'utf8')).toBe('done\n')
expect(await execCommand(conn, 'echo next')).toBe('next\n')
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
it('kills the loop when a command is aborted, and later commands start a new one', async () => {
const { conn } = pool()
const controller = new AbortController()
const running = execCommand(conn, 'sleep 30', {
signal: controller.signal
})
await new Promise((resolve) => setTimeout(resolve, 100))
controller.abort()
await expect(running).rejects.toMatchObject({ name: 'AbortError' })
expect(await execCommand(conn, 'echo again')).toBe('again\n')
expect(spawned).toHaveLength(2)
})
it('declines for good when the guest loop cannot start', async () => {
const { pool: loops } = pool({ script: 'exit 98' })
await expect(loops.run('echo x')).resolves.toBeNull()
await expect(loops.run('echo y')).resolves.toBeNull()
expect(spawned).toHaveLength(1)
})
it('reports a loop that dies mid-command as a failed command', async () => {
const { conn } = pool()
await expect(execCommand(conn, 'kill -9 $PPID 2>/dev/null; sleep 5')).rejects.toThrow()
expect(await execCommand(conn, 'echo recovered')).toBe('recovered\n')
})
})
+233
View File
@@ -0,0 +1,233 @@
/**
* One long-lived `wsl.exe --exec sh` that runs stdin-free commands one after another, so a run of
* short probes pays one wsl.exe start instead of one each (MEASURED ~180 ms each on win-lowspec).
* Each command runs as `sh -c` in its own session with stdin at /dev/null; its stdout, stderr and
* exit status come back base64-framed between per-loop markers.
*/
import { randomUUID } from 'node:crypto'
import { Duplex, PassThrough, Writable } from 'node:stream'
import type { PipedChildProcess } from '@orca/process-host/process-spec'
import type { RemoteCommandChannel } from '../ssh/remote-install-transport'
// Why setsid: a `wsl.exe --exec` command leads its own session; without it every command
// would share the loop's process group. Why fresh files per command: a backgrounded writer
// keeps the unlinked one, never the next command's output.
export const WSL_COMMAND_LOOP_SCRIPT = [
'm="$1"',
'd=$(mktemp -d "${TMPDIR:-/tmp}/orca-wsl-exec.XXXXXX") || exit 97',
'trap \'rm -rf "$d"\' EXIT',
"trap 'exit 129' HUP INT TERM",
'[ "$(printf eA== | base64 -d 2>/dev/null)" = x ] || exit 98',
'if command -v setsid >/dev/null 2>&1; then s=setsid; else s=; fi',
'printf \'%s R\\n\' "$m"',
'while IFS= read -r l; do',
' c=$(printf %s "$l" | base64 -d) || exit 99',
' $s sh -c "$c" </dev/null >"$d/o" 2>"$d/e"',
' r=$?',
' printf \'%s O\\n\' "$m"; base64 <"$d/o"',
' printf \'%s E\\n\' "$m"; base64 <"$d/e"',
' printf \'%s X %s\\n\' "$m" "$r"',
' rm -f "$d/o" "$d/e"',
'done'
].join('\n')
export const WSL_COMMAND_LOOP_ARGV0 = 'orca-wsl-exec-loop'
const BASE64_LINE = /^[A-Za-z0-9+/=]*$/u
type PendingCommand = {
channel: Duplex
stderr: PassThrough
stdout: string[]
errors: string[]
section: 'none' | 'out' | 'err'
settled: boolean
}
export type WslCommandLoop = {
/** Resolves once the guest loop answered its self-test; rejects if it never can. */
readonly ready: Promise<void>
readonly busy: boolean
readonly alive: boolean
run(command: string): RemoteCommandChannel
/** Ends the loop at its next read; a running command finishes first. */
end(): void
kill(): void
onIdle(listener: () => void): void
}
export function wslCommandLoopMarker(): string {
return `__ORCA_WSL_EXEC_${randomUUID().replaceAll('-', '')}__`
}
export function startWslCommandLoop(child: PipedChildProcess, marker: string): WslCommandLoop {
let pending: PendingCommand | null = null
let alive = true
let buffered = ''
let idleListener: () => void = () => {}
let markReady: () => void = () => {}
let failReady: (error: Error) => void = () => {}
let isReady = false
const ready = new Promise<void>((resolve, reject) => {
markReady = resolve
failReady = reject
})
// Callers that never await `ready` must not see an unhandled rejection.
ready.catch(() => {})
const settle = (code: number | null, signal: NodeJS.Signals | null, error?: Error): void => {
const command = pending
if (!command || command.settled) {
return
}
command.settled = true
pending = null
if (error) {
command.channel.destroy(error)
} else {
const out = Buffer.from(command.stdout.join(''), 'base64')
if (out.length > 0) {
command.channel.push(out)
}
command.channel.push(null)
const err = Buffer.from(command.errors.join(''), 'base64')
command.stderr.end(err.length > 0 ? err : undefined)
}
command.channel.emit('exit', code, signal)
// Like a wsl.exe child: `close` after its streams, so readers have their data first.
setImmediate(() => command.channel.emit('close', code, signal))
if (alive && !error) {
idleListener()
}
}
const fail = (reason: string): void => {
if (!isReady) {
failReady(new Error(reason))
}
settle(null, null, new Error(reason))
if (alive) {
kill()
}
}
const onLine = (line: string): void => {
if (line.startsWith(`${marker} `)) {
const control = line.slice(marker.length + 1)
if (control === 'R' && !isReady) {
isReady = true
markReady()
} else if (control === 'O' && pending) {
pending.section = 'out'
} else if (control === 'E' && pending) {
pending.section = 'err'
} else if (control.startsWith('X ') && pending) {
const code = Number.parseInt(control.slice(2), 10)
settle(Number.isSafeInteger(code) ? code : null, null)
} else {
fail('The WSL command loop sent an unexpected frame')
}
return
}
if (!pending || pending.section === 'none') {
// Outside a frame: nothing the protocol writes, so ignore stray guest output.
return
}
if (!BASE64_LINE.test(line)) {
fail('The WSL command loop sent a malformed frame')
return
}
;(pending.section === 'out' ? pending.stdout : pending.errors).push(line)
}
const kill = (): void => {
if (child.exitCode === null && child.signalCode === null) {
child.kill()
}
}
child.stdout.setEncoding('utf8')
child.stdout.on('data', (chunk: string) => {
buffered += chunk
let newline = buffered.indexOf('\n')
while (newline >= 0) {
onLine(buffered.slice(0, newline).replace(/\r$/u, ''))
buffered = buffered.slice(newline + 1)
newline = buffered.indexOf('\n')
}
})
// The loop's own stderr is never a command's; drain it so wsl.exe cannot block on it.
child.stderr.resume()
const ignore = (): void => {}
child.stdin.on('error', ignore)
child.stdout.on('error', ignore)
child.stderr.on('error', ignore)
child.on('error', (error) => {
alive = false
fail(error.message)
})
child.on('close', (code, signal) => {
alive = false
if (!isReady) {
failReady(new Error(`The WSL command loop exited (${code ?? signal ?? 'unknown'})`))
}
// A command cut off by the loop's exit reads like a killed wsl.exe child.
settle(null, signal ?? 'SIGTERM')
})
return {
ready,
get busy() {
return pending !== null
},
get alive() {
return alive
},
run(command) {
if (pending || !alive || !isReady) {
throw new Error('The WSL command loop is not idle')
}
const stderr = new PassThrough()
const channel = new Duplex({
emitClose: false,
read() {},
write: refuseStdin
})
const entry: PendingCommand = {
channel,
stderr,
stdout: [],
errors: [],
section: 'none',
settled: false
}
pending = entry
Object.assign(channel, {
stdin: new Writable({ write: refuseStdin }),
stderr,
close: () => {
// The command runs inside the loop, so stopping it means stopping the loop.
if (!entry.settled) {
kill()
}
}
})
child.stdin.write(`${Buffer.from(command, 'utf8').toString('base64')}\n`)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `stdin`, `stderr` and `close` were assigned above and `exit`/`close` are emitted by `settle`.
return channel as unknown as RemoteCommandChannel
},
end() {
if (alive) {
child.stdin.end()
}
},
kill,
onIdle(listener) {
idleListener = listener
}
}
}
function refuseStdin(_chunk: unknown, _encoding: unknown, done: (error?: Error) => void): void {
done(new Error('This WSL command runs with stdin closed'))
}
+16 -3
View File
@@ -32,7 +32,7 @@ import { createWslDistroHolder, type WslDistroHolder } from './wsl-distro-holder
import { wslDistroHolderKey } from './wsl-distro-holder-record'
import { readWslDistroRegistryId } from './wsl-distro-registry-id'
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
import { createWslOrcadHostTransport } from './wsl-orcad-host-transport'
import { createWslOrcadHostTransport, type WslOrcadHostTransport } from './wsl-orcad-host-transport'
import { adoptUnrecordedWslOrcad } from './wsl-orcad-instance-adoption'
export type WslOrcadLaunch =
@@ -67,7 +67,7 @@ export type WslOrcadDeployOptions = {
signal?: AbortSignal
/** Defaults to one created over `userData`; the app should pass its shared instance. */
holder?: WslDistroHolder
createTransport?: (target: WslOrcadHostTarget) => OrcadHostTransport
createTransport?: (target: WslOrcadHostTarget) => OrcadHostTransport & { dispose?(): void }
readRegistryId?: (distro: string) => Promise<string | null>
}
@@ -91,6 +91,19 @@ export async function deployAndLaunchWslOrcad(
label: options.label ?? name
}
const conn = (options.createTransport ?? defaultTransport)(target)
try {
return await launchOverTransport(name, target, conn, options)
} finally {
conn.dispose?.()
}
}
async function launchOverTransport(
name: string,
target: WslOrcadHostTarget,
conn: OrcadHostTransport,
options: WslOrcadDeployOptions
): Promise<WslOrcadLaunch> {
const { signal } = options
const kernel = await execCommand(conn, 'uname -r', { signal })
if (isWsl1KernelRelease(kernel)) {
@@ -184,7 +197,7 @@ export async function deployAndLaunchWslOrcad(
}
}
function defaultTransport(target: WslOrcadHostTarget): OrcadHostTransport {
function defaultTransport(target: WslOrcadHostTarget): WslOrcadHostTransport {
return createWslOrcadHostTransport({ target })
}
@@ -0,0 +1,93 @@
import { Readable } from 'node:stream'
import { afterEach, describe, expect, it } from 'vitest'
import { spawnProcess } from '@orca/process-host'
import type { PipedChildProcess, PipedProcessSpec } from '@orca/process-host/process-spec'
import { execCommand } from '../ssh/ssh-relay-exec-command'
import { createWslOrcadHostTransport, type WslOrcadHostTransport } from './wsl-orcad-host-transport'
const posix = process.platform !== 'win32'
const children: PipedChildProcess[] = []
const transports: WslOrcadHostTransport[] = []
/** Runs what follows `--exec` with the local sh, as wsl.exe would inside the distro. */
function localTransport() {
const specs: PipedProcessSpec[] = []
const transport = createWslOrcadHostTransport({
target: {
kind: 'wsl',
id: 'env-1',
distro: 'Ubuntu-24.04',
label: 'Ubuntu'
},
spawn: (spec) => {
specs.push(spec)
const args = spec.args ?? []
const child = spawnProcess({
program: 'sh',
args: args.slice(args.indexOf('--exec') + 2)
})
children.push(child)
return child
}
})
transports.push(transport)
return { transport, specs }
}
afterEach(() => {
transports.splice(0).forEach((transport) => transport.dispose())
children.splice(0).forEach((child) => child.kill())
})
describe.runIf(posix)('WSL transport command loops', () => {
it('runs a sequence of stdin-free probes over one wsl.exe', async () => {
const { transport, specs } = localTransport()
for (const word of ['uname', 'home', 'record', 'fence']) {
expect(await execCommand(transport, `echo ${word}`)).toBe(`${word}\n`)
}
expect(specs).toHaveLength(1)
expect(specs[0]?.args?.slice(0, 5)).toEqual(['-d', 'Ubuntu-24.04', '--exec', 'sh', '-c'])
expect(specs[0]?.args?.[6]).toBe('orca-wsl-exec-loop')
})
it('still gives a command that takes stdin its own wsl.exe', async () => {
const { transport, specs } = localTransport()
await execCommand(transport, 'echo first')
expect(
await execCommand(transport, 'cat', {
stdin: Readable.from([Buffer.from('piped')])
})
).toBe('piped')
expect(specs).toHaveLength(2)
expect(specs[1]?.args).toEqual(['-d', 'Ubuntu-24.04', '--exec', 'sh', '-c', expect.any(String)])
expect(specs[1]?.args?.[5]).toContain('cat')
})
it('falls back to one wsl.exe per command when the loop cannot start', async () => {
const specs: PipedProcessSpec[] = []
const transport = createWslOrcadHostTransport({
target: {
kind: 'wsl',
id: 'env-1',
distro: 'Ubuntu-24.04',
label: 'Ubuntu'
},
spawn: (spec) => {
specs.push(spec)
const args = spec.args ?? []
const shellArgs = args.slice(args.indexOf('--exec') + 2)
// A guest without base64: the loop's self-test fails and it exits.
const child = spawnProcess({
program: 'sh',
args: shellArgs[2] === 'orca-wsl-exec-loop' ? ['-c', 'exit 98'] : shellArgs
})
children.push(child)
return child
}
})
transports.push(transport)
expect(await execCommand(transport, 'echo a')).toBe('a\n')
expect(await execCommand(transport, 'echo b')).toBe('b\n')
expect(specs).toHaveLength(3)
})
})
@@ -52,6 +52,7 @@ function transportWith(children: FakeChild[]) {
const specs: PipedProcessSpec[] = []
const transport = createWslOrcadHostTransport({
target: { kind: 'wsl', id: 'env-1', distro: 'Ubuntu-24.04', label: 'My Ubuntu' },
commandLoops: false,
spawn: (spec) => {
specs.push(spec)
const next = children.shift()
+36 -13
View File
@@ -9,12 +9,16 @@ import { createSshOperationAbortError, type SshExecOptions } from '../ssh/ssh-co
import { writeStringViaExecStdin } from '../ssh/ssh-exec-stdin-file-transfer'
import { resolveWslInteropSpawnCwd } from '../wsl-interop-spawn-directory'
import { wrapWslCommandProcess } from './wsl-command-channel'
import { WSL_COMMAND_LOOP_ARGV0, WSL_COMMAND_LOOP_SCRIPT } from './wsl-command-loop'
import { createWslCommandLoopPool } from './wsl-command-loop-pool'
import { uploadDirectoryIntoWsl, wslGuestSourcePath } from './wsl-directory-upload'
import { resolveWslExecutablePath } from './wsl-executable-path'
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
export type WslOrcadHostTransport = OrcadHostTransport & {
getTarget(): WslOrcadHostTarget
/** Ends the idle command loops now instead of after their idle timeout. */
dispose(): void
}
type WslOrcadHostTransportOptions = {
@@ -26,10 +30,13 @@ type WslOrcadHostTransportOptions = {
* 20 MB). `guest-mount` copies from `/mnt/<drive>` when the guest can see the source.
*/
uploadSource?: 'tar' | 'guest-mount'
/** Test seam: false runs every command on its own wsl.exe. */
commandLoops?: boolean
}
/**
* orcad's host connection to a WSL distro: every exec is its own `wsl.exe --exec sh -c` child.
* orcad's host connection to a WSL distro: an exec that may write stdin is its own
* `wsl.exe --exec sh -c` child; a stdin-free one reuses a pooled command loop when it can.
* The distro is never a Windows host and has no SFTP, so every transfer runs over exec.
*/
export function createWslOrcadHostTransport(
@@ -37,24 +44,34 @@ export function createWslOrcadHostTransport(
): WslOrcadHostTransport {
const spawn = options.spawn ?? spawnProcess
const { target } = options
const spawnWsl = (shellArgs: string[]) => {
const program = resolveWslExecutablePath()
const args = buildWslExecArgs(target.distro, shellArgs)
if (commandLineLength([program, ...args]) > MAX_COMMAND_LINE_CHARS) {
throw new Error('WSL command exceeds the Windows command-line limit')
}
return spawn({
program,
args,
cwd: resolveWslInteropSpawnCwd(),
// Why WSL_UTF8: otherwise wsl.exe writes its own errors as UTF-16LE.
env: { ...process.env, WSL_UTF8: '1' }
})
}
const loops =
options.commandLoops === false
? null
: createWslCommandLoopPool({
spawnLoop: (marker) =>
spawnWsl(['sh', '-c', WSL_COMMAND_LOOP_SCRIPT, WSL_COMMAND_LOOP_ARGV0, marker])
})
const transport: WslOrcadHostTransport = {
// `wrapCommand` is SSH-only: there is no login shell between wsl.exe and `sh -c`.
async exec(command: string, execOptions?: SshExecOptions): Promise<RemoteCommandChannel> {
if (execOptions?.signal?.aborted) {
throw createSshOperationAbortError()
}
const program = resolveWslExecutablePath()
const args = buildWslExecArgs(target.distro, ['sh', '-c', command])
if (commandLineLength([program, ...args]) > MAX_COMMAND_LINE_CHARS) {
throw new Error('WSL command exceeds the Windows command-line limit')
}
const child = spawn({
program,
args,
cwd: resolveWslInteropSpawnCwd(),
// Why WSL_UTF8: otherwise wsl.exe writes its own errors as UTF-16LE.
env: { ...process.env, WSL_UTF8: '1' }
})
const child = spawnWsl(['sh', '-c', command])
const channel = wrapWslCommandProcess(child)
// Like an SSH channel open: a child that never started rejects here, not as a stream error.
const spawnError = (error: Error): void => {
@@ -68,6 +85,12 @@ export function createWslOrcadHostTransport(
}
return channel
},
async execWithClosedStdin(command, execOptions) {
return (
(await loops?.run(command, execOptions?.signal)) ?? transport.exec(command, execOptions)
)
},
dispose: () => loops?.dispose(),
// MEASURED: `nohup … &` dies when its `wsl.exe --exec` exits on a distro without systemd.
orcadLaunchDetach: 'setsid',
usesSystemSshTransport: () => false,
+1 -6
View File
@@ -4,12 +4,7 @@
*/
import { z } from 'zod'
/**
* Set only by a client's managed launch. Enables idle exit and names the host's activation
* fence, so an update or rollback in flight keeps the server up. User-started servers never
* carry it.
*/
export const ORCAD_MANAGED_ACTIVATION_ROOT_ENV = 'ORCA_ORCAD_MANAGED_ACTIVATION_ROOT'
export { ORCAD_MANAGED_ACTIVATION_ROOT_ENV } from './orcad-managed-launch-env'
/** Test-only quiet period; a client forwards it to the servers it launches. */
export const ORCAD_E2E_IDLE_TIMEOUT_ENV = 'ORCA_E2E_ORCAD_IDLE_TIMEOUT_MS'
export const ORCAD_IDLE_EXIT_TIMEOUT_MS = 15 * 60_000
+6
View File
@@ -0,0 +1,6 @@
/**
* Set only by a client's managed launch. Enables idle exit and names the host's activation
* fence, so an update or rollback in flight keeps the server up. User-started servers never
* carry it. Dependency-free so the orcad launcher can read it.
*/
export const ORCAD_MANAGED_ACTIVATION_ROOT_ENV = 'ORCA_ORCAD_MANAGED_ACTIVATION_ROOT'