mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
refactor(pty): PTY controller inventory and startup waits take a host id (#26998)
The runtime PTY controller's listProcesses / supportsForegroundProcessEvidence and the local daemon-startup waits took a connectionId where null meant this machine and undefined meant every host. They now take an ExecutionHostId (omitted = every host for inventory, this machine for startup waits); id-routed callers pass the PTY's resolved host instead of re-deriving a raw SSH connection id. Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
This commit is contained in:
@@ -99,10 +99,7 @@ const mainWindow = {
|
||||
}
|
||||
|
||||
function captureController(): {
|
||||
listProcesses: (
|
||||
connectionId?: string | null,
|
||||
opts?: { deadlineMs?: number }
|
||||
) => Promise<PtyProcessInfo[]>
|
||||
listProcesses: (hostId?: string, opts?: { deadlineMs?: number }) => Promise<PtyProcessInfo[]>
|
||||
} {
|
||||
handleMock.mockReset()
|
||||
onMock.mockReset()
|
||||
@@ -177,7 +174,7 @@ describe('aggregate PTY process inventory', () => {
|
||||
const controller = captureController()
|
||||
const deadlineMs = Date.now() + 1200
|
||||
|
||||
await controller.listProcesses('conn-a', { deadlineMs })
|
||||
await controller.listProcesses('ssh:conn-a', { deadlineMs })
|
||||
|
||||
expect(remote.calls).toEqual([{ opts: { deadlineMs } }])
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { spawnMock } from './pty-ipc-mock-registry'
|
||||
import { makeDeferred } from './pty-ipc-test-constants'
|
||||
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
|
||||
import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host'
|
||||
import type { RuntimePtyController } from '../runtime/runtime-pty-controller-contract'
|
||||
import {
|
||||
registerPtyHandlers,
|
||||
registerSshPtyProvider,
|
||||
@@ -105,31 +106,28 @@ describe('registerPtyHandlers', () => {
|
||||
registerSshPtyProvider('ssh-b', { listProcesses: sshBList } as never)
|
||||
setPtyOwnership('ssh-b-pty', 'ssh:ssh-b')
|
||||
const runtime = {
|
||||
setPtyController: vi.fn(),
|
||||
setPtyController: vi.fn((_controller: RuntimePtyController) => {}),
|
||||
markPtyLivenessUnverifiable: vi.fn()
|
||||
}
|
||||
handlers.clear()
|
||||
registerPtyHandlers(mainWindow as never, runtime as never)
|
||||
const controller = runtime.setPtyController.mock.calls[0]?.[0] as {
|
||||
listProcesses(connectionId?: string | null): Promise<{ id: string }[]>
|
||||
listProcessesWithHostScope(): Promise<{ processes: { id: string }[]; hostIds: string[] }>
|
||||
}
|
||||
const controller = runtime.setPtyController.mock.calls[0]?.[0]
|
||||
|
||||
await expect(controller.listProcesses(null)).resolves.toEqual([
|
||||
await expect(controller?.listProcesses?.('local')).resolves.toEqual([
|
||||
{ id: 'local-pty', title: 'Local', cwd: '/local' }
|
||||
])
|
||||
expect(localList).toHaveBeenCalledOnce()
|
||||
expect(sshAList).not.toHaveBeenCalled()
|
||||
expect(sshBList).not.toHaveBeenCalled()
|
||||
|
||||
await expect(controller.listProcesses('ssh-a')).resolves.toEqual([{ id: 'ssh-a-pty' }])
|
||||
await expect(controller?.listProcesses?.('ssh:ssh-a')).resolves.toEqual([{ id: 'ssh-a-pty' }])
|
||||
expect(sshAList).toHaveBeenCalledOnce()
|
||||
expect(sshBList).not.toHaveBeenCalled()
|
||||
|
||||
// STA-517: the aggregate used to propagate ssh-b's failure, which cost the runtime the
|
||||
// whole liveness inventory — so no PTY was ever proven dead and mobile kept every
|
||||
// retained pane "active". One unreachable relay now drops out of the answer instead.
|
||||
await expect(controller.listProcesses()).resolves.toEqual([
|
||||
await expect(controller?.listProcesses?.()).resolves.toEqual([
|
||||
{ id: 'local-pty', title: 'Local', cwd: '/local' },
|
||||
{ id: 'ssh-a-pty' }
|
||||
])
|
||||
@@ -139,7 +137,7 @@ describe('registerPtyHandlers', () => {
|
||||
'ssh-b unavailable'
|
||||
)
|
||||
|
||||
await expect(controller.listProcessesWithHostScope()).resolves.toEqual({
|
||||
await expect(controller?.listProcessesWithHostScope?.()).resolves.toEqual({
|
||||
processes: [{ id: 'local-pty', title: 'Local', cwd: '/local' }, { id: 'ssh-a-pty' }],
|
||||
hostIds: [LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId('ssh-a')]
|
||||
})
|
||||
|
||||
@@ -63,7 +63,7 @@ describe('stopping a PTY whose SSH provider is unregistered', () => {
|
||||
function installController(): {
|
||||
controller: {
|
||||
kill: (ptyId: string) => boolean
|
||||
listProcesses: (connectionId?: string | null) => Promise<{ id: string }[]>
|
||||
listProcesses: (hostId?: string) => Promise<{ id: string }[]>
|
||||
retireRejectedPty: (ptyId: string, stopConfirmed: boolean) => void
|
||||
stopAndWait: (ptyId: string, opts?: { deadlineMs?: number }) => Promise<boolean>
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import { ptyOwnership } from '../provider/ownership-state'
|
||||
import {
|
||||
getProviderForPty,
|
||||
getProvider,
|
||||
getPtySshConnectionId,
|
||||
hasPtyProviderForInspection,
|
||||
isDispatchablePtyHost,
|
||||
registeredPtyProviders,
|
||||
resolvePtyExecutionHost,
|
||||
tryGetProviderForPty
|
||||
tryGetProviderForPty,
|
||||
type ResolvedPtyHost
|
||||
} from '../provider/registry'
|
||||
import {
|
||||
getConnectionExecutionHostId,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
} from '../pane/serializer-state'
|
||||
|
||||
export function installPtyInspectIpcHandlers(deps: {
|
||||
getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise<void> | undefined
|
||||
getLocalPtyProviderStartupPromise: (hostId?: ResolvedPtyHost) => Promise<void> | undefined
|
||||
}): void {
|
||||
const ipcMain = getPtyIpc()
|
||||
const { getLocalPtyProviderStartupPromise } = deps
|
||||
@@ -42,7 +42,7 @@ export function installPtyInspectIpcHandlers(deps: {
|
||||
// renderer-kill.ts inlines this — pty:kill's listener teardown is
|
||||
// ordering-sensitive and must not gain even a no-barrier microtask.
|
||||
const awaitSwapWindow = async (id: string): Promise<void> => {
|
||||
await getLocalPtyProviderStartupPromise(getPtySshConnectionId(id))
|
||||
await getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(id))
|
||||
}
|
||||
|
||||
ipcMain.handle(
|
||||
|
||||
@@ -9,7 +9,9 @@ import { ptyIncarnationById } from '../provider/ownership-state'
|
||||
import {
|
||||
getProviderForPty,
|
||||
getPtySshConnectionId,
|
||||
tryGetProviderForPty
|
||||
resolvePtyExecutionHost,
|
||||
tryGetProviderForPty,
|
||||
type ResolvedPtyHost
|
||||
} from '../provider/registry'
|
||||
import { finishPtyShutdown, isPtyAlreadyGoneError } from '../provider/liveness'
|
||||
import { recordUndeliveredSshPtyKill } from '../runtime/undelivered-ssh-kill'
|
||||
@@ -17,7 +19,7 @@ import { recordUndeliveredSshPtyKill } from '../runtime/undelivered-ssh-kill'
|
||||
export type PtyKillIpcDeps = {
|
||||
store?: Store
|
||||
runtime?: OrcaRuntimeService
|
||||
getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise<void> | undefined
|
||||
getLocalPtyProviderStartupPromise: (hostId?: ResolvedPtyHost) => Promise<void> | undefined
|
||||
shutdownProviderAndDetectExit: (
|
||||
provider: IPtyProvider,
|
||||
id: string,
|
||||
@@ -91,7 +93,7 @@ async function stopRendererOwnedPtyProcess(
|
||||
runtime?.markPtyStopRequested?.(args.id)
|
||||
const connectionId = getPtySshConnectionId(args.id)
|
||||
// Why: wait for daemon startup before selecting the local provider, else a fallback shutdown falsely succeeds and orphans a restored daemon PTY (#7742).
|
||||
const startupPromise = getLocalPtyProviderStartupPromise(connectionId)
|
||||
const startupPromise = getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(args.id))
|
||||
if (startupPromise) {
|
||||
await startupPromise
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import type {
|
||||
} from '../host-env/types'
|
||||
import type { CodexResumeLaunch, PreparedCodexResumeHome } from '../host-env/codex-resume'
|
||||
import type { StablePaneOwner } from '../pane/stable-owner'
|
||||
import type { ResolvedPtyHost } from '../provider/registry'
|
||||
|
||||
export type PtySpawnIpcArgs = {
|
||||
cols: number
|
||||
@@ -91,7 +92,7 @@ export type PtySpawnIpcDeps = {
|
||||
prepareCodexSessionResume?: PrepareCodexSessionResume
|
||||
onCodexHomePtySpawned?: (args: CodexHomePtySpawnedLifecycleArgs) => void
|
||||
}
|
||||
getLocalPtyStartupPromise: (connectionId?: string | null) => Promise<void> | undefined
|
||||
getLocalPtyStartupPromise: (hostId?: ResolvedPtyHost) => Promise<void> | undefined
|
||||
adoptStablePane: (args: AdoptStablePaneArgs) => Promise<AdoptStablePaneResult | null>
|
||||
assertFolderWorkspacePtyPathUsable: (worktreeId: string | undefined) => Promise<void> | void
|
||||
resolvePtySpawnStartupCwd: (
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { getPtyIpc } from '../../pty-host-bindings'
|
||||
import { runPtyIpcSpawn } from './spawn-run'
|
||||
import type { PtySpawnIpcArgs, PtySpawnIpcDeps } from './spawn-types'
|
||||
import { getConnectionExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
export function installPtySpawnIpcHandler(deps: PtySpawnIpcDeps): void {
|
||||
const ipcMain = getPtyIpc()
|
||||
const { getLocalPtyStartupPromise } = deps
|
||||
|
||||
ipcMain.handle('pty:spawn', async (_event, args: PtySpawnIpcArgs) => {
|
||||
const startupPromise = getLocalPtyStartupPromise(args.connectionId)
|
||||
const startupPromise = getLocalPtyStartupPromise(
|
||||
getConnectionExecutionHostId(args.connectionId)
|
||||
)
|
||||
if (startupPromise) {
|
||||
await startupPromise
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ export function registeredPtyProviders(): RegisteredPtyProvider[] {
|
||||
}
|
||||
|
||||
export class PtyHostNotDispatchableError extends Error {
|
||||
constructor(readonly hostId: ExecutionHostId | 'foreign') {
|
||||
constructor(readonly hostId: ResolvedPtyHost) {
|
||||
// Why: a paired runtime drives its own PTYs over runtime RPC; this process has no provider for them.
|
||||
super(`PTY host "${hostId}" is not dispatchable from this process`)
|
||||
this.name = 'PtyHostNotDispatchableError'
|
||||
@@ -93,9 +93,11 @@ export function getProvider(hostId: ExecutionHostId): IPtyProvider {
|
||||
return provider
|
||||
}
|
||||
|
||||
/** The host that runs a PTY: its recorded owner, else the host its id names. 'foreign' = off this
|
||||
* machine but unnameable. */
|
||||
export function resolvePtyExecutionHost(ptyId: string): ExecutionHostId | 'foreign' {
|
||||
/** 'foreign' = a PTY that runs off this machine on a host its id cannot name. */
|
||||
export type ResolvedPtyHost = ExecutionHostId | 'foreign'
|
||||
|
||||
/** The host that runs a PTY: its recorded owner, else the host its id names. */
|
||||
export function resolvePtyExecutionHost(ptyId: string): ResolvedPtyHost {
|
||||
const owner = ptyOwnership.get(ptyId)
|
||||
if (owner !== undefined) {
|
||||
return owner
|
||||
@@ -117,7 +119,7 @@ export function getPtySshConnectionId(ptyId: string): string | null {
|
||||
}
|
||||
|
||||
/** Whether this process can hold a provider for the host at all (this machine or an SSH relay). */
|
||||
export function isDispatchablePtyHost(hostId: ExecutionHostId | 'foreign'): boolean {
|
||||
export function isDispatchablePtyHost(hostId: ResolvedPtyHost): boolean {
|
||||
return (
|
||||
hostId === LOCAL_EXECUTION_HOST_ID ||
|
||||
(hostId !== 'foreign' && getSshTargetIdForExecutionHost(hostId) !== null)
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
getHiddenRendererPtyDeliveryDebug,
|
||||
resetRendererScopedHiddenPtyDeliveryState
|
||||
} from '../pty-hidden-delivery-gate'
|
||||
import { localProvider } from './provider/registry'
|
||||
import { localProvider, type ResolvedPtyHost } from './provider/registry'
|
||||
import { finishPtyShutdown } from './provider/liveness'
|
||||
import type { GetSelectedCodexHomePath, PrepareClaudeAuth } from './host-env/types'
|
||||
import { installPtyInspectIpcHandlers } from './ipc/inspect'
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
stripSequencedStartupResumeArgv
|
||||
} from './host-env/codex-resume'
|
||||
import { ensureLinuxTerminalOrcaCliShimDir } from '../../cli/linux-terminal-orca-cli-shim'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../shared/execution-host'
|
||||
|
||||
export function registerPtyHandlers(
|
||||
mainWindow?: PtyRendererDelivery,
|
||||
@@ -95,8 +96,11 @@ export function registerPtyHandlers(
|
||||
setRebindProviderListeners(() => {})
|
||||
registerRendererLifecycleResetHandlers(mainWindow?.webContents)
|
||||
|
||||
const getLocalPtyStartupPromise = (connectionId?: string | null): Promise<void> | undefined => {
|
||||
if (connectionId) {
|
||||
// Why only `local`: the daemon swap re-owns this machine's PTYs; no other host waits on it.
|
||||
const getLocalPtyStartupPromise = (
|
||||
hostId: ResolvedPtyHost = LOCAL_EXECUTION_HOST_ID
|
||||
): Promise<void> | undefined => {
|
||||
if (hostId !== LOCAL_EXECUTION_HOST_ID) {
|
||||
return undefined
|
||||
}
|
||||
// Why: during cold start the daemon provider swap overlaps first paint, so local spawns must wait; SSH/headless don't use the desktop daemon.
|
||||
@@ -104,9 +108,9 @@ export function registerPtyHandlers(
|
||||
}
|
||||
|
||||
const getLocalPtyProviderStartupPromise = (
|
||||
connectionId?: string | null
|
||||
hostId: ResolvedPtyHost = LOCAL_EXECUTION_HOST_ID
|
||||
): Promise<void> | undefined => {
|
||||
if (connectionId) {
|
||||
if (hostId !== LOCAL_EXECUTION_HOST_ID) {
|
||||
return undefined
|
||||
}
|
||||
return options?.awaitLocalPtyProviderStartup?.() ?? options?.awaitLocalPtyStartup?.()
|
||||
|
||||
@@ -18,13 +18,14 @@ import type { CodexResumeLaunch, PreparedCodexResumeHome } from '../host-env/cod
|
||||
import type { StablePaneOwner } from '../pane/stable-owner'
|
||||
import type { AdoptStablePaneArgs, AdoptStablePaneResult } from '../ipc/spawn-types'
|
||||
import type { finishPtyShutdown } from '../provider/liveness'
|
||||
import type { ResolvedPtyHost } from '../provider/registry'
|
||||
|
||||
export type PtyRuntimeControllerDeps = {
|
||||
runtime?: OrcaRuntimeService
|
||||
store?: Store
|
||||
adoptStablePane: (args: AdoptStablePaneArgs) => Promise<AdoptStablePaneResult | null>
|
||||
getLocalPtyStartupPromise: (connectionId?: string | null) => Promise<void> | undefined
|
||||
getLocalPtyProviderStartupPromise: (connectionId?: string | null) => Promise<void> | undefined
|
||||
getLocalPtyStartupPromise: (hostId?: ResolvedPtyHost) => Promise<void> | undefined
|
||||
getLocalPtyProviderStartupPromise: (hostId?: ResolvedPtyHost) => Promise<void> | undefined
|
||||
prepareCodexResumeHome: (args: {
|
||||
connectionId?: string | null
|
||||
launchAgent?: TuiAgent
|
||||
|
||||
@@ -73,12 +73,11 @@ export function installPtyRuntimeController(deps: PtyRuntimeControllerDeps): voi
|
||||
clearBuffer: (ptyId) => clearBufferFromRuntimeController(deps, ptyId),
|
||||
resetInputModes: (ptyId) => resetInputModesFromRuntimeController(deps, ptyId),
|
||||
hasPty: (ptyId) => hasPtyFromRuntimeController(deps, ptyId),
|
||||
listProcesses: (connectionId, opts) =>
|
||||
listProcessesFromRuntimeController(deps, connectionId, opts),
|
||||
listProcesses: (hostId, opts) => listProcessesFromRuntimeController(deps, hostId, opts),
|
||||
listProcessesWithHostScope: (opts) =>
|
||||
listProcessesWithHostScopeFromRuntimeController(deps, opts),
|
||||
supportsForegroundProcessEvidence: (connectionId) =>
|
||||
supportsForegroundProcessEvidenceFromRuntimeController(connectionId),
|
||||
supportsForegroundProcessEvidence: (hostId) =>
|
||||
supportsForegroundProcessEvidenceFromRuntimeController(hostId),
|
||||
serializeBuffer: (ptyId, opts) => {
|
||||
// Why: mobile xterm must start from the desktop's exact screen state/dimensions before live TUI chunks render correctly.
|
||||
return requestSerializedBuffer(ptyId, opts)
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { getProvider, registeredPtyProviders } from '../provider/registry'
|
||||
import {
|
||||
getConnectionExecutionHostId,
|
||||
LOCAL_EXECUTION_HOST_ID
|
||||
} from '../../../../shared/execution-host'
|
||||
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
/** Probe the owning provider before opting into the no-process-table inventory projection. */
|
||||
export async function supportsForegroundProcessEvidenceFromRuntimeController(
|
||||
connectionId?: string | null
|
||||
hostId?: ExecutionHostId
|
||||
): Promise<boolean> {
|
||||
if (connectionId === null) {
|
||||
if (hostId === LOCAL_EXECUTION_HOST_ID) {
|
||||
return true
|
||||
}
|
||||
if (connectionId === undefined) {
|
||||
const providers = registeredPtyProviders()
|
||||
if (hostId === undefined) {
|
||||
const supported = await Promise.all(
|
||||
providers.map(async ({ provider, hostId }) =>
|
||||
hostId === LOCAL_EXECUTION_HOST_ID
|
||||
registeredPtyProviders().map(async ({ provider, hostId: providerHostId }) =>
|
||||
providerHostId === LOCAL_EXECUTION_HOST_ID
|
||||
? true
|
||||
: ((await provider.supportsForegroundProcessEvidence?.()) ?? false)
|
||||
)
|
||||
@@ -23,11 +19,7 @@ export async function supportsForegroundProcessEvidenceFromRuntimeController(
|
||||
return supported.every(Boolean)
|
||||
}
|
||||
try {
|
||||
return (
|
||||
(await getProvider(
|
||||
getConnectionExecutionHostId(connectionId)
|
||||
).supportsForegroundProcessEvidence?.()) ?? false
|
||||
)
|
||||
return (await getProvider(hostId).supportsForegroundProcessEvidence?.()) ?? false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { getPtyIdsForHost } from '../provider/ownership-state'
|
||||
import { getProvider, localProvider, registeredPtyProviders } from '../provider/registry'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
getConnectionExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { PtyProcessInfo } from '../../../providers/pty-process-info'
|
||||
import type { PtyRuntimeControllerDeps } from './controller-deps'
|
||||
|
||||
@@ -49,14 +45,13 @@ export async function listProcessesWithHostScopeFromRuntimeController(
|
||||
|
||||
export async function listProcessesFromRuntimeController(
|
||||
deps: PtyRuntimeControllerDeps,
|
||||
connectionId?: string | null,
|
||||
hostId?: ExecutionHostId,
|
||||
opts?: { deadlineMs?: number; includeForegroundProcessEvidence?: boolean }
|
||||
) {
|
||||
if (connectionId === null) {
|
||||
if (hostId === LOCAL_EXECUTION_HOST_ID) {
|
||||
return localProvider.listProcesses()
|
||||
}
|
||||
if (connectionId !== undefined) {
|
||||
const hostId = getConnectionExecutionHostId(connectionId)
|
||||
if (hostId !== undefined) {
|
||||
try {
|
||||
return await getProvider(hostId).listProcesses(opts)
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { IPtyProvider } from '../../../providers/types'
|
||||
import { SSH_PROVIDER_UNREGISTERED_REASON } from '../../../../shared/pty-liveness-verdict'
|
||||
import { ptyOwnership, ptyIncarnationById } from '../provider/ownership-state'
|
||||
import { getProviderForPty, getPtySshConnectionId } from '../provider/registry'
|
||||
import {
|
||||
getProviderForPty,
|
||||
getPtySshConnectionId,
|
||||
resolvePtyExecutionHost
|
||||
} from '../provider/registry'
|
||||
import { isPtyAlreadyGoneError, delay, verifyPtyStopped } from '../provider/liveness'
|
||||
import { recordUndeliveredSshPtyKill } from './undelivered-ssh-kill'
|
||||
import type { PtyRuntimeControllerDeps } from './controller-deps'
|
||||
@@ -100,7 +104,7 @@ export function killPtyFromRuntimeController(
|
||||
})
|
||||
return true
|
||||
}
|
||||
const startupPromise = getLocalPtyProviderStartupPromise(connectionId)
|
||||
const startupPromise = getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(ptyId))
|
||||
if (startupPromise) {
|
||||
// Why: select the provider after the daemon swap; the fallback first can report success while orphaning a daemon PTY.
|
||||
void startupPromise.then(killWithCurrentProvider).catch((err) => {
|
||||
@@ -167,7 +171,7 @@ export async function stopAndWaitPtyFromRuntimeController(
|
||||
// below; each RPC leaf converts it to the remaining time when it issues, so
|
||||
// sequential RPCs share the budget and cannot overrun the sweep deadline.
|
||||
const deadlineMs = opts?.deadlineMs
|
||||
const startupPromise = getLocalPtyProviderStartupPromise(connectionId)
|
||||
const startupPromise = getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(ptyId))
|
||||
if (startupPromise) {
|
||||
// Why: exact-stop must resolve the provider after daemon startup just
|
||||
// like renderer kills, or the fallback can falsely confirm teardown.
|
||||
|
||||
@@ -5,12 +5,7 @@ import { LocalPtyProvider } from '../../../providers/local-pty-provider'
|
||||
import { ptyOwnership } from '../provider/ownership-state'
|
||||
import { ptySizes } from '../delivery/visibility-state'
|
||||
import { rendererSerializerReadiness } from '../pane/serializer-state'
|
||||
import {
|
||||
getProviderForPty,
|
||||
getPtySshConnectionId,
|
||||
localProvider,
|
||||
resolvePtyExecutionHost
|
||||
} from '../provider/registry'
|
||||
import { getProviderForPty, localProvider, resolvePtyExecutionHost } from '../provider/registry'
|
||||
import { inspectPtyProviderProcess } from '../../../providers/pty-process-inspection'
|
||||
import type { PtyRuntimeControllerDeps } from './controller-deps'
|
||||
import {
|
||||
@@ -97,10 +92,9 @@ export async function probePtyLivenessFromRuntimeController(
|
||||
if (isRemoteRuntimePtyId(ptyId)) {
|
||||
return null
|
||||
}
|
||||
const connectionId = getPtySshConnectionId(ptyId)
|
||||
// Why: during cold start the daemon swap is in flight; the pre-swap
|
||||
// fallback would answer absent for every daemon-owned id.
|
||||
const startupPromise = deps.getLocalPtyProviderStartupPromise(connectionId)
|
||||
const startupPromise = deps.getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(ptyId))
|
||||
if (startupPromise) {
|
||||
await startupPromise
|
||||
}
|
||||
@@ -244,8 +238,7 @@ export function hasPtyFromRuntimeController(
|
||||
if (isRemoteRuntimePtyId(ptyId)) {
|
||||
return null
|
||||
}
|
||||
const connectionId = getPtySshConnectionId(ptyId)
|
||||
const startupPromise = deps.getLocalPtyProviderStartupPromise(connectionId)
|
||||
const startupPromise = deps.getLocalPtyProviderStartupPromise(resolvePtyExecutionHost(ptyId))
|
||||
if (startupPromise && !settledLocalPtyProviderStartups.has(startupPromise)) {
|
||||
// Why: a sync probe cannot wait out the cold-start daemon swap the way
|
||||
// probePtyLiveness does, and the pre-swap provider's "no PTY" for a
|
||||
|
||||
@@ -5,6 +5,7 @@ import { snapshotCodexPaneHomeRoutes, codexReattachedHomeRouteField } from '../h
|
||||
import { ensureWslHookRelayForReattach } from '../../../agent-hooks/wsl-hook-relay-reattach'
|
||||
import type { CodexPaneHomeRoute } from '../../../codex/codex-pane-account-registry'
|
||||
import type { RuntimePtySpawnState } from './spawn-state'
|
||||
import { getConnectionExecutionHostId } from '../../../../shared/execution-host'
|
||||
|
||||
export function adoptMaterializedRuntimePtySpawn(
|
||||
ctx: RuntimePtySpawnState,
|
||||
@@ -28,7 +29,9 @@ export function adoptMaterializedRuntimePtySpawn(
|
||||
)
|
||||
: new Map<string, CodexPaneHomeRoute | null>()
|
||||
}
|
||||
const startupPromise = ctx.deps.getLocalPtyStartupPromise(args.connectionId)
|
||||
const startupPromise = ctx.deps.getLocalPtyStartupPromise(
|
||||
getConnectionExecutionHostId(args.connectionId)
|
||||
)
|
||||
if (startupPromise && !startupAlreadyAwaited) {
|
||||
return startupPromise.then(() => adoptMaterializedRuntimePtySpawn(ctx, true))
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { TUI_AGENT_CONFIG } from '../../shared/tui-agent-config'
|
||||
import type { RuntimePtyController } from './runtime-pty-controller-contract'
|
||||
import { judgeTerminalForeground, readTerminalProcessRows } from './terminal-foreground-group'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
||||
|
||||
/**
|
||||
* What holds a launched agent's terminal: the agent (anything but the pane's shell), the shell
|
||||
@@ -61,7 +62,7 @@ export async function readLaunchedAgentForeground(
|
||||
return (await controller.confirmShellForeground?.(ptyId)) ? 'shell' : 'unknown'
|
||||
}
|
||||
if (!host.remote) {
|
||||
const rootPid = (await controller.listProcesses?.(null))?.find(
|
||||
const rootPid = (await controller.listProcesses?.(LOCAL_EXECUTION_HOST_ID))?.find(
|
||||
(pane) => pane.id === ptyId
|
||||
)?.rootProcessId
|
||||
const rows = rootPid ? await readTerminalProcessRows(rootPid) : null
|
||||
|
||||
@@ -441,7 +441,7 @@ describe('agent-session create operation ledger', () => {
|
||||
disposition: 'replayed',
|
||||
terminal: { handle: orphanHandle, ptyId: 'ssh-1:pty2:e:1', worktreeId: 'worktree-1' }
|
||||
})
|
||||
expect(listProcesses).toHaveBeenCalledWith('ssh-1')
|
||||
expect(listProcesses).toHaveBeenCalledWith('ssh:ssh-1')
|
||||
expect(createTerminal).toHaveBeenCalledOnce()
|
||||
expect(failure.message).toBe('execution_owner_unavailable')
|
||||
})
|
||||
@@ -455,7 +455,7 @@ describe('agent-session create operation ledger', () => {
|
||||
await expect(runtime.createAgentSession(request(id), { clientId: 'device-a' })).rejects.toThrow(
|
||||
failure.message
|
||||
)
|
||||
expect(listProcesses).toHaveBeenCalledWith('ssh-1')
|
||||
expect(listProcesses).toHaveBeenCalledWith('ssh:ssh-1')
|
||||
expect(createTerminal).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
|
||||
@@ -52,7 +52,9 @@ export class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh extends OrcaRu
|
||||
if (
|
||||
targetConnectionId !== null &&
|
||||
this.ptyController.supportsForegroundProcessEvidence &&
|
||||
!(await this.ptyController.supportsForegroundProcessEvidence(targetConnectionId))
|
||||
!(await this.ptyController.supportsForegroundProcessEvidence(
|
||||
parsedTargetHost?.kind === 'ssh' ? parsedTargetHost.id : undefined
|
||||
))
|
||||
) {
|
||||
// A legacy relay ignores the optional projection and would still run its
|
||||
// expensive process-table inventory on every mobile cadence tick.
|
||||
|
||||
@@ -8,7 +8,7 @@ const SSH_SCOPE = JSON.stringify({ kind: 'ssh', targetId: 'ssh-1' })
|
||||
const PROCESS_INCARNATION = 'remote:ssh-1:pty-1:inc-1'
|
||||
|
||||
function runtimeWithInventory(
|
||||
listProcesses: (connectionId?: string | null) => Promise<PtyProcessInfo[]>
|
||||
listProcesses: (hostId?: string) => Promise<PtyProcessInfo[]>
|
||||
): OrcaRuntimeService {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
runtime.setPtyController({
|
||||
@@ -38,8 +38,8 @@ describe('terminal process incarnation liveness', () => {
|
||||
await expect(
|
||||
runtime.inspectTerminalProcessIncarnationLiveness('remote:ssh-1:pty-1:inc-old', SSH_SCOPE)
|
||||
).resolves.toBe('exited')
|
||||
expect(listProcesses).toHaveBeenNthCalledWith(1, 'ssh-1')
|
||||
expect(listProcesses).toHaveBeenNthCalledWith(2, 'ssh-1')
|
||||
expect(listProcesses).toHaveBeenNthCalledWith(1, 'ssh:ssh-1')
|
||||
expect(listProcesses).toHaveBeenNthCalledWith(2, 'ssh:ssh-1')
|
||||
})
|
||||
|
||||
it('keeps missing or malformed identity and unavailable inventory unverifiable', async () => {
|
||||
@@ -77,16 +77,16 @@ describe('terminal process incarnation liveness', () => {
|
||||
})
|
||||
|
||||
it.each([
|
||||
[{ kind: 'local', hostId: 'local' }, null],
|
||||
[{ kind: 'wsl', hostId: 'local', distro: 'Ubuntu' }, null]
|
||||
] as const)('uses the local provider inventory for %s scope', async (scope, connectionId) => {
|
||||
[{ kind: 'local', hostId: 'local' }, 'local'],
|
||||
[{ kind: 'wsl', hostId: 'local', distro: 'Ubuntu' }, 'local']
|
||||
] as const)('uses the local provider inventory for %s scope', async (scope, hostId) => {
|
||||
const listProcesses = vi.fn().mockResolvedValue([])
|
||||
const runtime = runtimeWithInventory(listProcesses)
|
||||
|
||||
await expect(
|
||||
runtime.inspectTerminalProcessIncarnationLiveness('local-pty:inc-1', JSON.stringify(scope))
|
||||
).resolves.toBe('exited')
|
||||
expect(listProcesses).toHaveBeenCalledWith(connectionId)
|
||||
expect(listProcesses).toHaveBeenCalledWith(hostId)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
+4
-9
@@ -6,8 +6,7 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
|
||||
import {
|
||||
getConnectionExecutionHostId,
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId
|
||||
parseExecutionHostId
|
||||
} from '../../shared/execution-host'
|
||||
import {
|
||||
PTY_CONTROLLER_LIST_PROVIDER_MARGIN_MS,
|
||||
@@ -77,13 +76,9 @@ export class OrcaRuntimeWithRefreshPtyWorktreeRecordsWithControllerInventory ext
|
||||
const processInventory =
|
||||
connectionId === undefined && this.ptyController.listProcessesWithHostScope
|
||||
? this.ptyController.listProcessesWithHostScope(providerListOpts)
|
||||
: this.ptyController.listProcesses(connectionId, providerListOpts).then((processes) => {
|
||||
const hostId: ExecutionHostId =
|
||||
connectionId === undefined || connectionId === null
|
||||
? LOCAL_EXECUTION_HOST_ID
|
||||
: toSshExecutionHostId(connectionId)
|
||||
return { processes, hostIds: [hostId] }
|
||||
})
|
||||
: this.ptyController
|
||||
.listProcesses(connectionId === undefined ? undefined : providerKey, providerListOpts)
|
||||
.then((processes) => ({ processes, hostIds: [providerKey] }))
|
||||
const sessionsResult = await withTimeoutResult(processInventory, listBudgetMs)
|
||||
if (!sessionsResult.ok) {
|
||||
return null
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from './orchestration/worker-terminal-process-liveness'
|
||||
import { getRepoIdFromWorktreeId } from '../../shared/worktree/id'
|
||||
import { buildOrchestrationTaskDisplayMetadata } from '../../shared/orchestration-task-display'
|
||||
import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host'
|
||||
|
||||
export class OrcaRuntimeWithSubscribeToTerminalResize extends OrcaRuntimeWithApplyMobileDisplayMode {
|
||||
subscribeToTerminalResize(
|
||||
@@ -176,7 +177,11 @@ export class OrcaRuntimeWithSubscribeToTerminalResize extends OrcaRuntimeWithApp
|
||||
return 'unverifiable'
|
||||
}
|
||||
const listed = await withTimeoutResult(
|
||||
this.ptyController.listProcesses(hostScope.kind === 'ssh' ? hostScope.targetId : null),
|
||||
this.ptyController.listProcesses(
|
||||
hostScope.kind === 'ssh'
|
||||
? toSshExecutionHostId(hostScope.targetId)
|
||||
: LOCAL_EXECUTION_HOST_ID
|
||||
),
|
||||
PTY_CONTROLLER_LIST_TIMEOUT_MS
|
||||
)
|
||||
if (!listed.ok) {
|
||||
|
||||
@@ -7,7 +7,11 @@ import { withTimeoutResult } from './runtime-async-boundaries'
|
||||
import { PTY_CONTROLLER_LIST_TIMEOUT_MS } from './orca-runtime-postlude'
|
||||
import { inferWorktreeIdFromPtyId } from './runtime-worktree-path-identity'
|
||||
import { getRegisteredSshState } from '../ssh/ssh-target-registry'
|
||||
import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host'
|
||||
import {
|
||||
getConnectionExecutionHostId,
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
toSshExecutionHostId
|
||||
} from '../../shared/execution-host'
|
||||
import { resolveWorktreeLaunchHost } from './worktree-launch-host-repo'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
|
||||
@@ -69,7 +73,9 @@ export class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithC
|
||||
throw new Error('runtime_unavailable')
|
||||
}
|
||||
const listed = await withTimeoutResult(
|
||||
this.ptyController.listProcesses(connectionId),
|
||||
this.ptyController.listProcesses(
|
||||
connectionId === undefined ? undefined : getConnectionExecutionHostId(connectionId)
|
||||
),
|
||||
PTY_CONTROLLER_LIST_TIMEOUT_MS
|
||||
)
|
||||
if (!listed.ok) {
|
||||
|
||||
@@ -259,7 +259,7 @@ describe('terminal create idempotency', () => {
|
||||
})
|
||||
|
||||
// Mirrors listProcessesFromRuntimeController: `undefined` aggregates every provider and
|
||||
// silently drops a non-answering SSH host, `null` is local-only, a string is host-scoped
|
||||
// silently drops a non-answering SSH host, `local` is local-only, an `ssh:` host is host-scoped
|
||||
// and rethrows the host's failure.
|
||||
function createHostScopedInventory(hosts: {
|
||||
local?: PtyProcessInfo[]
|
||||
@@ -267,12 +267,12 @@ function createHostScopedInventory(hosts: {
|
||||
}) {
|
||||
const local = hosts.local ?? []
|
||||
const ssh = hosts.ssh ?? {}
|
||||
return vi.fn(async (connectionId?: string | null): Promise<PtyProcessInfo[]> => {
|
||||
if (connectionId === null) {
|
||||
return vi.fn(async (hostId?: string): Promise<PtyProcessInfo[]> => {
|
||||
if (hostId === 'local') {
|
||||
return local
|
||||
}
|
||||
if (typeof connectionId === 'string') {
|
||||
const host = ssh[connectionId]
|
||||
if (typeof hostId === 'string') {
|
||||
const host = ssh[hostId.slice('ssh:'.length)]
|
||||
if (host === undefined || host === 'unreachable') {
|
||||
throw new Error('ssh relay did not answer')
|
||||
}
|
||||
@@ -310,7 +310,7 @@ describe('terminal create reconciliation scopes inventory to the owning executio
|
||||
runtime.dedupeTerminalCreate('device-a', 'id:worktree-1', 'mutation-1', true, create)
|
||||
).rejects.toThrow('runtime_unavailable')
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
expect(listProcesses).toHaveBeenCalledWith('ssh-1')
|
||||
expect(listProcesses).toHaveBeenCalledWith('ssh:ssh-1')
|
||||
})
|
||||
|
||||
it('adopts the original PTY from the owning host listing', async () => {
|
||||
@@ -343,7 +343,7 @@ describe('terminal create reconciliation scopes inventory to the owning executio
|
||||
)
|
||||
|
||||
expect(create).toHaveBeenCalledWith('id:worktree-1', result.handle)
|
||||
expect(listProcesses).toHaveBeenCalledWith('ssh-1')
|
||||
expect(listProcesses).toHaveBeenCalledWith('ssh:ssh-1')
|
||||
})
|
||||
|
||||
it('scopes the listing to the local host for a workspace with no connection', async () => {
|
||||
@@ -357,7 +357,7 @@ describe('terminal create reconciliation scopes inventory to the owning executio
|
||||
await expect(
|
||||
runtime.dedupeTerminalCreate('device-a', 'id:worktree-1', 'mutation-1', true, create)
|
||||
).resolves.toMatchObject({ handle, ptyId: 'worktree-1@@session-a' })
|
||||
expect(listProcesses).toHaveBeenCalledWith(null)
|
||||
expect(listProcesses).toHaveBeenCalledWith('local')
|
||||
expect(create).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -377,6 +377,6 @@ describe('terminal create reconciliation scopes inventory to the owning executio
|
||||
)
|
||||
|
||||
expect(create).toHaveBeenCalledWith('id:folder:folder-1', result.handle)
|
||||
expect(listProcesses).toHaveBeenCalledWith(null)
|
||||
expect(listProcesses).toHaveBeenCalledWith('local')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -198,7 +198,7 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(listWorktrees).not.toHaveBeenCalled()
|
||||
expect(listProcesses).toHaveBeenCalledOnce()
|
||||
expect(listProcesses).toHaveBeenCalledWith(
|
||||
'ssh-target',
|
||||
'ssh:ssh-target',
|
||||
expect.objectContaining({ deadlineMs: expect.any(Number) })
|
||||
)
|
||||
expect(result).toMatchObject({
|
||||
|
||||
+1
-1
@@ -212,7 +212,7 @@ describe('OrcaRuntimeService', () => {
|
||||
paneKey: makePaneKey('tab-agent', HEADLESS_LEAF_ID)
|
||||
})
|
||||
expect(listProcesses).toHaveBeenCalledTimes(inventoryCount + 1)
|
||||
expect(listProcesses).toHaveBeenLastCalledWith(null, LIST_PROVIDER_DEADLINE)
|
||||
expect(listProcesses).toHaveBeenLastCalledWith('local', LIST_PROVIDER_DEADLINE)
|
||||
expect(
|
||||
(await runtime.listTerminals()).terminals.find((terminal) => terminal.ptyId === 'pty-agent')
|
||||
).toMatchObject({
|
||||
|
||||
+9
-4
@@ -307,8 +307,8 @@ describe('OrcaRuntimeService', () => {
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const listProcesses = vi.fn(async (connectionId?: string | null) => {
|
||||
if (connectionId !== null) {
|
||||
const listProcesses = vi.fn(async (hostId?: string) => {
|
||||
if (hostId !== 'local') {
|
||||
throw new Error('unrelated SSH inventory must not run')
|
||||
}
|
||||
return [
|
||||
@@ -347,7 +347,7 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(getSession().sleepingAgentSessionsByPaneKey?.[workerPaneKey]).toBeUndefined()
|
||||
expect(getSession().sleepingAgentSessionsByPaneKey?.[secondWorkerPaneKey]).toBeUndefined()
|
||||
expect(listProcesses).toHaveBeenCalledTimes(3)
|
||||
expect(listProcesses).toHaveBeenCalledWith(null, LIST_PROVIDER_DEADLINE)
|
||||
expect(listProcesses).toHaveBeenCalledWith('local', LIST_PROVIDER_DEADLINE)
|
||||
expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([
|
||||
expect.objectContaining({ id: 'legacy-worker-two', ptyId: 'pty-exited-two' })
|
||||
])
|
||||
@@ -460,7 +460,12 @@ describe('OrcaRuntimeService', () => {
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
|
||||
expect(listProcesses).toHaveBeenCalledTimes(4)
|
||||
expect(listProcesses.mock.calls.map((call) => call[0])).toEqual([null, null, null, null])
|
||||
expect(listProcesses.mock.calls.map((call) => call[0])).toEqual([
|
||||
'local',
|
||||
'local',
|
||||
'local',
|
||||
'local'
|
||||
])
|
||||
expect(hasPty).not.toHaveBeenCalled()
|
||||
expect(getSession().tabsByWorktree[TEST_WORKTREE_ID]).toEqual([
|
||||
expect.objectContaining({ id: 'legacy-worker', ptyId: 'pty-inventory-unavailable' })
|
||||
|
||||
+3
-3
@@ -151,7 +151,7 @@ describe('OrcaRuntimeService', () => {
|
||||
deferredDispatchIds: ['dispatch-missing', 'dispatch-ambiguous']
|
||||
})
|
||||
expect(listProcesses).toHaveBeenCalledOnce()
|
||||
expect(listProcesses).toHaveBeenCalledWith(null, LIST_PROVIDER_DEADLINE)
|
||||
expect(listProcesses).toHaveBeenCalledWith('local', LIST_PROVIDER_DEADLINE)
|
||||
expect(getForegroundProcess).not.toHaveBeenCalled()
|
||||
await runtime.refreshPtyForegroundAgentFromController('pty-ambiguous')
|
||||
expect(getForegroundProcess).toHaveBeenCalledExactlyOnceWith('pty-ambiguous')
|
||||
@@ -263,7 +263,7 @@ describe('OrcaRuntimeService', () => {
|
||||
)
|
||||
expect(getSession().sleepingAgentSessionsByPaneKey?.[workerPaneKey]).toBeUndefined()
|
||||
expect(listProcesses).toHaveBeenCalledTimes(3)
|
||||
expect(listProcesses).toHaveBeenCalledWith(null, LIST_PROVIDER_DEADLINE)
|
||||
expect(listProcesses).toHaveBeenCalledWith('local', LIST_PROVIDER_DEADLINE)
|
||||
expect(revealTerminalSession).toHaveBeenCalledWith(TEST_FOLDER_WORKSPACE_KEY, {
|
||||
ptyId: 'pty-folder-legacy',
|
||||
title: 'Folder worker',
|
||||
@@ -400,7 +400,7 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(getWorkspaceSession).toHaveBeenCalledWith(`ssh:${connectionId}`)
|
||||
expect(setWorkspaceSession).toHaveBeenCalledWith(expect.any(Object), `ssh:${connectionId}`)
|
||||
expect(listProcesses).toHaveBeenCalledTimes(3)
|
||||
expect(listProcesses).toHaveBeenCalledWith(connectionId, LIST_PROVIDER_DEADLINE)
|
||||
expect(listProcesses).toHaveBeenCalledWith(`ssh:${connectionId}`, LIST_PROVIDER_DEADLINE)
|
||||
expect(sshSession.tabsByWorktree[TEST_FOLDER_WORKSPACE_KEY]).toContainEqual(
|
||||
expect.objectContaining({
|
||||
id: 'legacy-ssh-folder-worker',
|
||||
|
||||
+8
-2
@@ -167,7 +167,7 @@ describe('OrcaRuntimeService', () => {
|
||||
exitedDispatchIds: [],
|
||||
deferredDispatchIds: []
|
||||
})
|
||||
expect(listProcesses).toHaveBeenLastCalledWith(connectionId, LIST_PROVIDER_DEADLINE)
|
||||
expect(listProcesses).toHaveBeenLastCalledWith(`ssh:${connectionId}`, LIST_PROVIDER_DEADLINE)
|
||||
} finally {
|
||||
unregisterSshGitProvider(connectionId)
|
||||
}
|
||||
@@ -310,7 +310,13 @@ describe('OrcaRuntimeService', () => {
|
||||
expect(getSession().sleepingAgentSessionsByPaneKey?.[workerPaneKey]).toBeUndefined()
|
||||
expect(revealTerminalSession).toHaveBeenCalledOnce()
|
||||
expect(listProcesses).toHaveBeenCalledTimes(5)
|
||||
expect(listProcesses.mock.calls.map((call) => call[0])).toEqual([null, null, null, null, null])
|
||||
expect(listProcesses.mock.calls.map((call) => call[0])).toEqual([
|
||||
'local',
|
||||
'local',
|
||||
'local',
|
||||
'local',
|
||||
'local'
|
||||
])
|
||||
})
|
||||
|
||||
it('restores orphan pane and group topology without replacing a newer host-owned tab', async () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('lifecycle invalidation respects inventory scope and retry bounds', ()
|
||||
reply.resolve([processRow('ssh:host-b@@child')])
|
||||
expect(await pending).toBeNull()
|
||||
expect(list).toHaveBeenCalledTimes(1)
|
||||
expect(list.mock.calls[0][0]).toBe('host-b')
|
||||
expect(list.mock.calls[0][0]).toBe('ssh:host-b')
|
||||
expect(hasPty).not.toHaveBeenCalled()
|
||||
expect(runtime.capture(id).connected).toBe(true)
|
||||
}
|
||||
@@ -88,7 +88,7 @@ describe('lifecycle invalidation respects inventory scope and retry bounds', ()
|
||||
const a = deferred<PtyProcessInfo[]>()
|
||||
const b = deferred<PtyProcessInfo[]>()
|
||||
const { runtime } = createInventoryRuntime((connection) =>
|
||||
connection === 'a' ? a.promise : b.promise
|
||||
connection === 'ssh:a' ? a.promise : b.promise
|
||||
)
|
||||
const pa = runtime.read('a')
|
||||
const pb = runtime.read('b')
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
confirmRootShellAloneFromProcessTable,
|
||||
inspectionShowsShellAlone
|
||||
} from './run-terminal-shell-alone'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../shared/execution-host'
|
||||
|
||||
/**
|
||||
* Whether a client drove or is viewing a PTY's current process, for closing finished run
|
||||
@@ -47,7 +48,7 @@ export async function confirmRunTerminalShellAlone(
|
||||
(await controller?.inspectProcess?.(ptyId, { scanChildProcesses: true })) ?? null
|
||||
)
|
||||
}
|
||||
const processes = (await controller?.listProcesses?.(null)) ?? []
|
||||
const processes = (await controller?.listProcesses?.(LOCAL_EXECUTION_HOST_ID)) ?? []
|
||||
const rootPid = processes.find((entry) => entry.id === ptyId)?.rootProcessId
|
||||
return rootPid ? await confirmRootShellAloneFromProcessTable(rootPid) : false
|
||||
} catch {
|
||||
|
||||
@@ -135,8 +135,9 @@ export type RuntimePtyController = {
|
||||
resize?(ptyId: string, cols: number, rows: number): boolean
|
||||
// Why: exact-id mobile polls should not enumerate every local and SSH PTY.
|
||||
hasPty?(ptyId: string): boolean | null
|
||||
/** Omitting the host lists every registered host. */
|
||||
listProcesses?(
|
||||
connectionId?: string | null,
|
||||
hostId?: ExecutionHostId,
|
||||
opts?: { deadlineMs?: number; includeForegroundProcessEvidence?: boolean }
|
||||
): Promise<PtyProcessInfo[]>
|
||||
listProcessesWithHostScope?(opts?: {
|
||||
@@ -146,7 +147,7 @@ export type RuntimePtyController = {
|
||||
processes: PtyProcessInfo[]
|
||||
hostIds: ExecutionHostId[]
|
||||
}>
|
||||
supportsForegroundProcessEvidence?(connectionId?: string | null): Promise<boolean>
|
||||
supportsForegroundProcessEvidence?(hostId?: ExecutionHostId): Promise<boolean>
|
||||
serializeBuffer?(
|
||||
ptyId: string,
|
||||
opts?: { scrollbackRows?: number }
|
||||
|
||||
Reference in New Issue
Block a user