Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/scripts/bun-profile-test-paths.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function bunProfileTestPaths({ artifact = false } = {}) {
'src/main/daemon/headless-emulator-fidelity.fuzz.test.ts',
...(artifact
? [
'src/relay/windows-detached-launch.integration.test.ts',
'src/main/daemon/pty-subprocess/bun-pty-process.integration.test.ts',
'src/main/pty/posix-pty-process-groups.integration.test.ts',
'src/main/daemon/pty-subprocess/bun-pty-job-control.integration.test.ts',
Expand Down
30 changes: 17 additions & 13 deletions src/main/ssh/ssh-relay-deploy-windows-pipe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,15 +159,15 @@ describe('deployAndLaunchRelay on Windows remotes', () => {
it('launches Windows remotes via a named pipe endpoint', async () => {
const conn = makeMockConnection()
const mockExecCommand = vi.mocked(execCommand)
vi.mocked(ensureRemoteRelayBunRuntime).mockResolvedValue('C:/Program Files/nodejs/node.exe')
vi.mocked(ensureRemoteRelayBunRuntime).mockResolvedValue('C:/Program Files/Orca/bun.exe')
mockExecCommand
.mockRejectedValueOnce(new Error('uname not found')) // tagged POSIX platform probe
.mockResolvedValueOnce('__ORCA_REMOTE_PLATFORM__ Windows X64') // tagged PowerShell platform probe
.mockResolvedValueOnce('C:\\Users\\me user') // remote home
.mockResolvedValueOnce('ORCA-NATIVE-DEPS-OK') // native deps probe
.mockResolvedValueOnce('') // no persisted active pipe
.mockResolvedValueOnce('WAITING') // named pipe probe
.mockResolvedValueOnce('') // WMI relay launch
.mockResolvedValueOnce('') // detached relay launch
.mockResolvedValueOnce('READY') // named pipe poll
.mockResolvedValueOnce('') // persist active pipe marker

Expand All @@ -185,16 +185,20 @@ describe('deployAndLaunchRelay on Windows remotes', () => {
const nativeProbe = decodedScripts.find((script) => script.includes('ORCA-NATIVE-DEPS-OK'))
expect(nativeProbe).toContain('./parcel-watcher.node')
expect(nativeProbe).not.toContain('require("@parcel/watcher")')
const launchScript = decodedScripts.find((script) => script.includes('Invoke-CimMethod')) ?? ''
const launchScript = decodedScripts.find((script) => script.includes('--spawn-detached')) ?? ''
expect(launchScript).toContain(
'"C:/Users/me user/.orca-remote/relay-0.1.0+abcdef012345/relay.js"'
"'C:/Users/me user/.orca-remote/relay-0.1.0+abcdef012345/relay.js'"
)
expect(launchScript).toContain(
'"C:/Users/me user/.orca-remote/relay-0.1.0+abcdef012345/agent-hooks/orca-relay-'
"'C:/Users/me user/.orca-remote/relay-0.1.0+abcdef012345/agent-hooks/orca-relay-"
)
expect(launchScript).toContain('--endpoint-dir')
expect(launchScript).toContain("& 'C:/Program Files/Orca/bun.exe'")
expect(launchScript).toContain('--launch-error-file')
expect(launchScript).not.toMatch(/Invoke-CimMethod|cmd\.exe|\$env:PATH/)
expect(launchScript).toContain('Set-Location')
expect(launchScript).toContain(
'--ripgrep-path "C:/Users/me user/.orca-remote/ripgrep/c0ffee0123456789-win32-x64/rg.exe"'
"'--ripgrep-path' 'C:/Users/me user/.orca-remote/ripgrep/c0ffee0123456789-win32-x64/rg.exe'"
)
expect(launchScript).not.toContain('--pty-source-credit-v1')
expect(launchScript).not.toContain('.pty-source-credit-policy')
Expand All @@ -205,7 +209,7 @@ describe('deployAndLaunchRelay on Windows remotes', () => {
const script = decodePowerShellCommand(command)
return (
script?.includes('.windows-active-pipe') ||
script?.includes('Invoke-CimMethod') ||
script?.includes('--spawn-detached') ||
script?.includes('deadline=Date.now()')
)
})
Expand All @@ -220,7 +224,7 @@ describe('deployAndLaunchRelay on Windows remotes', () => {
it('relaunches Windows remotes on a fallback pipe when reconnecting the occupied pipe fails', async () => {
const conn = makeMockConnection()
const mockExecCommand = vi.mocked(execCommand)
vi.mocked(ensureRemoteRelayBunRuntime).mockResolvedValue('C:/Program Files/nodejs/node.exe')
vi.mocked(ensureRemoteRelayBunRuntime).mockResolvedValue('C:/Program Files/Orca/bun.exe')
vi.mocked(waitForSentinel)
.mockRejectedValueOnce(new Error('stale daemon handshake failed'))
.mockResolvedValueOnce({
Expand All @@ -236,7 +240,7 @@ describe('deployAndLaunchRelay on Windows remotes', () => {
.mockResolvedValueOnce('') // no persisted active pipe yet
.mockResolvedValueOnce('READY') // existing named pipe probe
.mockResolvedValueOnce('WAITING') // deterministic fallback pipe is not already running
.mockResolvedValueOnce('') // WMI relay launch on fallback pipe
.mockResolvedValueOnce('') // detached relay launch on fallback pipe
.mockResolvedValueOnce('READY') // fallback pipe poll
.mockResolvedValueOnce('') // persist fallback active pipe marker

Expand All @@ -256,7 +260,7 @@ describe('deployAndLaunchRelay on Windows remotes', () => {
const launchScript =
mockExecCommand.mock.calls
.map(([, command]) => decodePowerShellCommand(command))
.find((script) => script?.includes('Invoke-CimMethod')) ?? ''
.find((script) => script?.includes('--spawn-detached')) ?? ''
expect(launchScript).toContain(fallbackPipe)
expect(launchScript).not.toContain(primaryPipe)

Expand All @@ -274,7 +278,7 @@ describe('deployAndLaunchRelay on Windows remotes', () => {
const conn = makeMockConnection()
const mockExecCommand = vi.mocked(execCommand)
const persistedPipe = '\\\\.\\pipe\\orca-relay-1234567890abcdef1234'
vi.mocked(ensureRemoteRelayBunRuntime).mockResolvedValue('C:/Program Files/nodejs/node.exe')
vi.mocked(ensureRemoteRelayBunRuntime).mockResolvedValue('C:/Program Files/Orca/bun.exe')
mockExecCommand
.mockRejectedValueOnce(new Error('uname not found')) // tagged POSIX platform probe
.mockResolvedValueOnce('__ORCA_REMOTE_PLATFORM__ Windows X64') // tagged PowerShell platform probe
Expand All @@ -295,14 +299,14 @@ describe('deployAndLaunchRelay on Windows remotes', () => {
const decodedExecScripts = mockExecCommand.mock.calls
.map(([, command]) => decodePowerShellCommand(command))
.filter((script): script is string => script !== null)
expect(decodedExecScripts.some((script) => script.includes('Invoke-CimMethod'))).toBe(false)
expect(decodedExecScripts.some((script) => script.includes('--spawn-detached'))).toBe(false)
})

it('scopes persisted Windows active pipe markers by relay target', async () => {
const connA = makeMockConnection()
const connB = makeMockConnection()
const mockExecCommand = vi.mocked(execCommand)
vi.mocked(ensureRemoteRelayBunRuntime).mockResolvedValue('C:/Program Files/nodejs/node.exe')
vi.mocked(ensureRemoteRelayBunRuntime).mockResolvedValue('C:/Program Files/Orca/bun.exe')
mockExecCommand
.mockRejectedValueOnce(new Error('uname not found')) // tagged POSIX platform probe A
.mockResolvedValueOnce('__ORCA_REMOTE_PLATFORM__ Windows X64')
Expand Down
41 changes: 18 additions & 23 deletions src/main/ssh/ssh-relay-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ import {
RELAY_DEPLOY_TIMEOUT_MS
} from './ssh-relay-deploy-timing'
import { createSshOperationAbortError, shellEscape } from './ssh-connection-utils'
import { commandWithNodePath, readRemoteHomeCommand } from './ssh-remote-commands'
import {
commandInRemoteDirectory,
commandWithNodePath,
readRemoteHomeCommand
} from './ssh-remote-commands'
import {
cleanupOwnedRelayUploadStageCommand,
parseReservedRelayUploadStage,
Expand Down Expand Up @@ -1481,39 +1485,30 @@ function windowsRelayLaunchCommand(
ripgrepPath?: string
): string {
const relayScript = joinRemotePath(hostPlatform, remoteDir, 'relay.js')
// Why: Windows sshd kills the exec channel's process tree on close; WMI re-parents the detached relay to survive.
const quoted = (value: string): string => `"${value.replace(/"/g, '\\"')}"`
const relayCommandLine = [
quoted(nodePath),
const args = [
'--no-env-file',
'--config=NUL',
'--no-install',
quoted(relayScript),
'--detached',
relayScript,
'--spawn-detached',
'--grace-time',
String(graceTime),
'--sock-path',
quoted(sockPath),
sockPath,
'--credential-file',
quoted(credentialFile),
credentialFile,
'--endpoint-dir',
quoted(endpointDir),
// Why: --log-file owns rotation; shell redirects still capture pre-JS boot/crash output.
endpointDir,
'--log-file',
quoted(logFile),
...(ripgrepPath ? ['--ripgrep-path', quoted(ripgrepPath)] : []),
`1>${quoted(logFile)}`,
`2>${quoted(errFile)}`
].join(' ')
const wmiCommandLine = `cmd.exe /d /s /c "${relayCommandLine}"`
return commandWithNodePath(
logFile,
'--launch-error-file',
errFile,
...(ripgrepPath ? ['--ripgrep-path', ripgrepPath] : [])
]
return commandInRemoteDirectory(
hostPlatform,
nodePath,
remoteDir,
[
`$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ${powerShellLiteral(wmiCommandLine)}; CurrentDirectory = ${powerShellLiteral(remoteDir)} }`,
`if ($result.ReturnValue -ne 0) { throw "Win32_Process.Create failed with $($result.ReturnValue)" }`
].join('; ')
`& ${powerShellLiteral(nodePath)} ${args.map(powerShellLiteral).join(' ')}; if ($LASTEXITCODE -ne 0) { throw "Bun detached launch failed with exit $LASTEXITCODE" }`
)
}

Expand Down
76 changes: 75 additions & 1 deletion src/relay/relay-launch-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { parseRelayLaunchOptions, readRelayEndpointCredential } from './relay-launch-options'
import {
detachedRelayArguments,
parseRelayLaunchOptions,
readRelayEndpointCredential
} from './relay-launch-options'

describe('relay launch options', () => {
const temporaryDirectories: string[] = []
Expand Down Expand Up @@ -39,13 +43,83 @@ describe('relay launch options', () => {
connectMode: true,
detached: true,
cliMode: false,
spawnDetachedMode: false,
launchErrorFile: undefined,
ripgrepPath: undefined,
sockPath: 'relay-endpoint',
endpointDir: 'hooks',
logFile: 'relay.log',
credentialFile: 'relay.credential'
})
})

it('forwards launch values as argv without recursive launch or shell expansion', () => {
const options = parseRelayLaunchOptions([
'bun',
'relay.js',
'--spawn-detached',
'--grace-time',
'0',
'--sock-path',
'--spawn-detached',
'--credential-file',
'C:\\a & %USER%\\credential',
'--log-file',
'C:\\logs with spaces\\out.log',
'--launch-error-file',
'err.log'
])
const args = detachedRelayArguments('C:\\relay root\\relay.js', options)
expect(args).toEqual([
'--no-env-file',
'--config=NUL',
'--no-install',
'C:\\relay root\\relay.js',
'--detached',
'--grace-time',
'0',
'--sock-path',
'--spawn-detached',
'--log-file',
'C:\\logs with spaces\\out.log',
'--credential-file',
'C:\\a & %USER%\\credential'
])
const reparsed = parseRelayLaunchOptions(['bun', ...args.slice(3)])
expect(reparsed.spawnDetachedMode).toBe(false)
expect(reparsed.sockPath).toBe('--spawn-detached')
expect(reparsed.launchErrorFile).toBeUndefined()
})

it('preserves every daemon option across the detached launcher', () => {
const options = parseRelayLaunchOptions([
'bun',
'relay.js',
'--spawn-detached',
'--grace-time',
'123',
'--sock-path',
'pipe-name',
'--endpoint-dir',
'C:/hooks & tools',
'--credential-file',
'C:/秘密/credential',
'--log-file',
'C:/logs/relay.log',
'--launch-error-file',
'C:/logs/relay.err.log',
'--ripgrep-path',
'C:/tools/rg.exe'
])
const args = detachedRelayArguments('C:/relay/relay.js', options)
expect(parseRelayLaunchOptions(['bun', ...args.slice(3)])).toEqual({
...options,
detached: true,
spawnDetachedMode: false,
launchErrorFile: undefined
})
})

it('reads the bundled ripgrep path, which older launch commands omit', () => {
expect(
parseRelayLaunchOptions([
Expand Down
28 changes: 28 additions & 0 deletions src/relay/relay-launch-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export type RelayLaunchOptions = {
connectMode: boolean
detached: boolean
cliMode: boolean
spawnDetachedMode?: boolean
launchErrorFile?: string
sockPath: string
endpointDir?: string
logFile?: string
Expand All @@ -31,6 +33,8 @@ export function parseRelayLaunchOptions(argv: string[]): RelayLaunchOptions {
let connectMode = false
let detached = false
let cliMode = false
let spawnDetachedMode = false
let launchErrorFile: string | undefined
let sockPath = ''
let endpointDir: string | undefined
let logFile: string | undefined
Expand All @@ -48,6 +52,10 @@ export function parseRelayLaunchOptions(argv: string[]): RelayLaunchOptions {
connectMode = true
} else if (argv[i] === '--orca-cli') {
cliMode = true
} else if (argv[i] === '--spawn-detached') {
spawnDetachedMode = true
} else if (argv[i] === '--launch-error-file' && argv[i + 1]) {
launchErrorFile = argv[++i]
} else if (argv[i] === '--detached') {
detached = true
} else if (argv[i] === '--sock-path' && argv[i + 1]) {
Expand Down Expand Up @@ -75,6 +83,8 @@ export function parseRelayLaunchOptions(argv: string[]): RelayLaunchOptions {
connectMode,
detached,
cliMode,
spawnDetachedMode,
launchErrorFile,
sockPath,
endpointDir,
logFile,
Expand All @@ -83,6 +93,24 @@ export function parseRelayLaunchOptions(argv: string[]): RelayLaunchOptions {
}
}

export function detachedRelayArguments(entrypoint: string, options: RelayLaunchOptions): string[] {
return [
'--no-env-file',
'--config=NUL',
'--no-install',
entrypoint,
'--detached',
'--grace-time',
String(options.graceTimeMs / 1000),
'--sock-path',
options.sockPath,
...(options.endpointDir ? ['--endpoint-dir', options.endpointDir] : []),
...(options.logFile ? ['--log-file', options.logFile] : []),
...(options.credentialFile ? ['--credential-file', options.credentialFile] : []),
...(options.ripgrepPath ? ['--ripgrep-path', options.ripgrepPath] : [])
]
}

export function readRelayEndpointCredential(
credentialFile: string | undefined
): string | undefined {
Expand Down
22 changes: 20 additions & 2 deletions src/relay/relay.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@

// Orca Relay — remote-host daemon and reconnect bridge entry point.

import { parseRelayLaunchOptions, readRelayEndpointCredential } from './relay-launch-options'
import {
detachedRelayArguments,
parseRelayLaunchOptions,
readRelayEndpointCredential
} from './relay-launch-options'
import { runRelayConnectChannel } from './relay-connect-channel'
import { runRelayOrcaCliChannel } from './relay-orca-cli-channel'
import { runRelayDaemon } from './relay-daemon'
import { relayLogLine } from './relay-diagnostic-log'
import { configureRelayBundledRipgrep } from './relay-bundled-ripgrep'
import { launchDetachedWindowsRelay } from './windows-detached-launch'

async function main(): Promise<void> {
const options = parseRelayLaunchOptions(process.argv)
Expand All @@ -23,6 +27,20 @@ async function main(): Promise<void> {
)
return
}
if (options.spawnDetachedMode) {
if (!options.logFile || !options.launchErrorFile) {
throw new Error('Detached relay launch requires stdout and stderr paths')
}
const pid = launchDetachedWindowsRelay({
executable: process.execPath,
args: detachedRelayArguments(process.argv[1], options),
cwd: process.cwd(),
stdoutPath: options.logFile,
stderrPath: options.launchErrorFile
})
process.stdout.write(`launched ${pid}\n`)
return
}
configureRelayBundledRipgrep(options.ripgrepPath)
// Why no read here: the daemon publishes its credential itself, after it owns the socket.
await runRelayDaemon(options)
Expand Down
Loading
Loading