Skip to content
Merged
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
8 changes: 2 additions & 6 deletions src/cli/runtime/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,8 @@ import {
import { RuntimeClientError } from './types'
import { SERVE_RUNTIME_ELECTRON, SERVE_RUNTIME_ENV } from '../../shared/orcad-local-serve-selection'
import { findBundledWebClientRoot } from '../../shared/bundled-web-client-root'
import {
resolveLocalServeRuntime,
serveOptionArgs,
serveWithOrcad,
type ServeOrcaAppArgs
} from './serve-orcad-launch'
import { resolveLocalServeRuntime, serveWithOrcad } from './serve-orcad-launch'
import { serveOptionArgs, type ServeOrcaAppArgs } from '../../shared/orcad-serve-argv'
import { waitForRecipeJson } from './serve-recipe-json'
import {
preflightElectronServe,
Expand Down
3 changes: 2 additions & 1 deletion src/cli/runtime/serve-orcad-launch.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, expect, it, vi } from 'vitest'
import { formatServeRuntimeSelection } from '../../shared/orcad-local-serve-selection'
import type { runProcess } from '@orca/process-host'
import { orcadServeArgs, resolveLocalServeRuntime, serveOptionArgs } from './serve-orcad-launch'
import { resolveLocalServeRuntime } from './serve-orcad-launch'
import { orcadServeArgs, serveOptionArgs } from '../../shared/orcad-serve-argv'

type RunProcess = typeof runProcess

Expand Down
39 changes: 1 addition & 38 deletions src/cli/runtime/serve-orcad-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,7 @@ import { waitForRecipeJson } from './serve-recipe-json'
import { superviseForegroundServe } from './serve-update-supervisor'
import { isSupervisedSpawnHeld } from './serve-supervised-stop'
import { redactingSpawn, shouldRedactSupervisedStdout } from './serve-pairing-redaction'

export type ServeOrcaAppArgs = {
json?: boolean
port?: string | null
pairingAddress?: string | null
noPairing?: boolean
mobilePairing?: boolean
grantDesktopControl?: boolean
recipeJson?: boolean
projectRoot?: string | null
/** Starts Electron serve despite a remembered macOS startup abort in this environment. */
force?: boolean
/** Restart the server under a service manager instead of exiting with it; never inferred. */
supervise?: boolean
}
import { orcadServeArgs, type ServeOrcaAppArgs } from '../../shared/orcad-serve-argv'

/** A first run may download and verify the pinned Node; bound it well past that. */
const SELECTION_TIMEOUT_MS = 10 * 60_000
Expand Down Expand Up @@ -119,26 +105,3 @@ export function serveWithOrcad(
expectedHandoff: null
})
}

export function serveOptionArgs(args: ServeOrcaAppArgs, prefix: '--' | '--serve-'): string[] {
return [
...(args.json ? [`${prefix}json`] : []),
...(args.port ? [`${prefix}port`, args.port] : []),
...(args.pairingAddress ? [`${prefix}pairing-address`, args.pairingAddress] : []),
...(args.noPairing ? [`${prefix}no-pairing`] : []),
...(args.mobilePairing ? [`${prefix}mobile-pairing`] : []),
...(args.grantDesktopControl ? [`${prefix}grant-desktop-control`] : []),
...(args.recipeJson && args.projectRoot
? [`${prefix}recipe-json`, `${prefix}project-root`, args.projectRoot]
: [])
]
}

export function orcadServeArgs(args: ServeOrcaAppArgs, webClientRoot?: string): string[] {
return [
'--bind',
'0.0.0.0',
...serveOptionArgs(args, '--'),
...(webClientRoot ? ['--web-client-root', webClientRoot] : [])
]
}
57 changes: 47 additions & 10 deletions src/main/orcad/orcad-serve-parity.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
/**
* `orca serve` on orcad, against the packaged slot: the native preflight the launcher asks
* first, desktop-serve flag parity on stdout (the readiness contract), and the shared-profile
* refusal while the desktop app holds the profile.
* first, desktop-serve flag parity on stdout (the readiness contract), the browser client the
* CLI hands orcad, and the shared-profile refusal while the desktop app holds the profile.
*/
import { existsSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync } from 'node:fs'
import { tmpdir, userInfo } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { runProcess, spawnProcess } from '@orca/process-host'
import { ORCAD_NODE_RUNTIME_MARKER_FILENAME } from '../../shared/orcad-artifacts'
import {
findBundledWebClientRoot,
WEB_CLIENT_INDEX_FILENAME
} from '../../shared/bundled-web-client-root'
import { orcadServeArgs } from '../../shared/orcad-serve-argv'
import {
ORCAD_NATIVE_PREFLIGHT_FLAG,
parseOrcadNativePreflightReport
Expand Down Expand Up @@ -189,6 +194,32 @@ describe.skipIf(skip)('orca serve on orcad', () => {
expect(result.stderr).toContain('The Orca desktop app')
}, 90_000)

// #30120: the CLI never handed orcad the app's web client, so no URL was printed and GETs hung.
it('serves the web client the CLI hands it, and answers other paths with 404', async () => {
const userData = realpathSync(
mkdtempSync(join(process.platform === 'win32' ? tmpdir() : '/tmp', 'orca-serve-web-'))
)
roots.push(userData)
const appRoot = profile()
mkdirSync(join(appRoot, 'out', 'web'), { recursive: true })
writeFileSync(join(appRoot, 'out', 'web', WEB_CLIENT_INDEX_FILENAME), '<title>orca web</title>')
// The argv `orca serve` builds, but on loopback: a test must not listen on every interface.
const args = orcadServeArgs({ json: true, port: '0' }, findBundledWebClientRoot(appRoot))
args.splice(args.indexOf('0.0.0.0'), 1, '127.0.0.1')
try {
const readiness = await serveOnce(userData, args)
const webClientUrl: unknown = readiness.pairing?.webClientUrl
expect(webClientUrl).toEqual(expect.stringMatching(/^http:\/\/127\.0\.0\.1:\d+\//u))
const index = await fetch(new URL(`/${WEB_CLIENT_INDEX_FILENAME}`, String(webClientUrl)))
expect(index.status).toBe(200)
expect(await index.text()).toBe('<title>orca web</title>')
const unknown = await fetch(new URL('/no-such-page', String(webClientUrl)))
expect(unknown.status).toBe(404)
} finally {
await stopServeProfile(userData, null)
}
}, 150_000)

// A short /tmp root keeps the POSIX daemon socket path legal; Windows uses a named pipe.
it('keeps a live terminal across a serve restart on the shared profile (D7)', async () => {
const userData = realpathSync(
Expand All @@ -207,15 +238,15 @@ describe.skipIf(skip)('orca serve on orcad', () => {
expect(result.code, result.stderr).toBe(0)
return JSON.parse(result.stdout.trim().split('\n').at(-1) ?? '{}')
}
const first = await serveOnce(userData)
const first = await serveDaemonPid(userData)
let shellPid: number | null = null
try {
const created = await session('create', 'BEFORE')
shellPid = created.pid
expect(created).toMatchObject({ isReattach: false, output: true })
await stopServe(userData)
// The same daemon, in the same \`<userData>/daemon\` Electron serve uses, is adopted.
expect(await serveOnce(userData)).toBe(first)
expect(await serveDaemonPid(userData)).toBe(first)
expect(await session('attach', 'AFTER')).toEqual({
pid: created.pid,
isReattach: true,
Expand All @@ -230,10 +261,18 @@ describe.skipIf(skip)('orca serve on orcad', () => {
const running = new Map<string, ReturnType<typeof spawnProcess>>()

/** Starts `orca serve --json` on orcad and returns the terminal daemon pid its readiness names. */
async function serveOnce(userData: string): Promise<number> {
async function serveDaemonPid(userData: string): Promise<number> {
const readiness = await serveOnce(userData, ['--bind', '127.0.0.1', '--port', '0', '--json'])
const daemon = readiness.health?.terminalDaemon
expect(daemon?.state).toBe('live')
return daemon.pid
}

/** Starts orcad with `args` and returns its parsed readiness line. */
async function serveOnce(userData: string, args: string[]) {
const child = spawnProcess({
program: runtime!,
args: [join(slotDir, 'orcad.js'), '--bind', '127.0.0.1', '--port', '0', '--json'],
args: [join(slotDir, 'orcad.js'), ...args],
env: serveEnv(userData),
timeoutMs: null
})
Expand All @@ -252,9 +291,7 @@ async function serveOnce(userData: string): Promise<number> {
}
})
})
const daemon = JSON.parse(line).health?.terminalDaemon
expect(daemon?.state, stderr).toBe('live')
return daemon.pid
return JSON.parse(line)
}

/** SIGTERM is serve's graceful stop on POSIX; it leaves the daemon running by design. */
Expand Down
41 changes: 41 additions & 0 deletions src/shared/orcad-serve-argv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* The argv `orca serve` hands a serve host: Electron as `--serve-*`, orcad as plain flags. Shared so
* orcad's own tests run the exact command line the CLI builds (#30120 dropped one across it).
*/
export type ServeOrcaAppArgs = {
json?: boolean
port?: string | null
pairingAddress?: string | null
noPairing?: boolean
mobilePairing?: boolean
grantDesktopControl?: boolean
recipeJson?: boolean
projectRoot?: string | null
/** Starts Electron serve despite a remembered macOS startup abort in this environment. */
force?: boolean
/** Restart the server under a service manager instead of exiting with it; never inferred. */
supervise?: boolean
}

export function serveOptionArgs(args: ServeOrcaAppArgs, prefix: '--' | '--serve-'): string[] {
return [
...(args.json ? [`${prefix}json`] : []),
...(args.port ? [`${prefix}port`, args.port] : []),
...(args.pairingAddress ? [`${prefix}pairing-address`, args.pairingAddress] : []),
...(args.noPairing ? [`${prefix}no-pairing`] : []),
...(args.mobilePairing ? [`${prefix}mobile-pairing`] : []),
...(args.grantDesktopControl ? [`${prefix}grant-desktop-control`] : []),
...(args.recipeJson && args.projectRoot
? [`${prefix}recipe-json`, `${prefix}project-root`, args.projectRoot]
: [])
]
}

export function orcadServeArgs(args: ServeOrcaAppArgs, webClientRoot?: string): string[] {
return [
'--bind',
'0.0.0.0',
...serveOptionArgs(args, '--'),
...(webClientRoot ? ['--web-client-root', webClientRoot] : [])
]
}
Loading