mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
refactor(runtime-environments): one exhaustive host-kind classifier for connect, remove, call readiness, hint and phone relay (#29500)
* test(runtime-environments): pin connect, remove, call readiness, hint and phone relay per host kind * refactor(runtime-environments): dispatch on one exhaustive host-kind classifier * test: avoid type assertions in host-kind characterization fakes * refactor(runtime-environments): keep service-owned connect/remove in one module; tighten characterization tests * style: break, not bare return, in host service ops switches
This commit is contained in:
@@ -6,10 +6,13 @@ import {
|
||||
resolveEnvironment
|
||||
} from '../../shared/runtime-environment-store'
|
||||
import {
|
||||
isWslHostRuntimeEnvironment,
|
||||
redactRuntimeEnvironment,
|
||||
type PublicKnownRuntimeEnvironment
|
||||
} from '../../shared/runtime-environments'
|
||||
import {
|
||||
connectThroughOwningService,
|
||||
removeThroughOwningService
|
||||
} from './runtime-environment-host-service-ops'
|
||||
import { RemoteRuntimeClientError } from '../../shared/remote-runtime-client-error'
|
||||
import { RuntimeRpcCallQueueOverloadError } from '../../shared/runtime-rpc-call-queue'
|
||||
import type { RuntimeRpcFailure, RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope'
|
||||
@@ -41,7 +44,6 @@ import {
|
||||
observeWslOrcadHostResponse,
|
||||
wslOrcadHostSupports
|
||||
} from '../wsl/wsl-orcad-host-environments'
|
||||
import { getWslHostService } from '../wsl/wsl-host-service-instance'
|
||||
import { assertWslHostReachable, isGatedOffWslHost } from '../wsl/wsl-host-gate'
|
||||
|
||||
function manuallyDisconnectedResponse(
|
||||
@@ -127,12 +129,9 @@ export function registerRuntimeEnvironmentConnectivityHandlers({
|
||||
}
|
||||
// A closed gate leaves persisted WSL hosts untouched on disk.
|
||||
assertWslHostReachable(environment)
|
||||
const wsl = isWslHostRuntimeEnvironment(environment) ? getWslHostService() : null
|
||||
if (wsl) {
|
||||
// Also releases the distro's holder and drops the WSL record.
|
||||
return wsl
|
||||
.remove(environment.id)
|
||||
.then(() => ({ removed: redactRuntimeEnvironment(environment) }))
|
||||
const ownedRemoval = removeThroughOwningService(environment)
|
||||
if (ownedRemoval) {
|
||||
return ownedRemoval.then(() => ({ removed: redactRuntimeEnvironment(environment) }))
|
||||
}
|
||||
const removed = removeEnvironment(getUserDataPath(), args.selector)
|
||||
void retireRemovedRuntimeEnvironment(removed.id, invalidateTransport, store)
|
||||
@@ -162,10 +161,7 @@ export function registerRuntimeEnvironmentConnectivityHandlers({
|
||||
assertWslHostReachable(environment)
|
||||
clearRuntimeEnvironmentManualDisconnect(environment.id)
|
||||
clearSshCredentialDeclinedForEnvironment(environment)
|
||||
if (isWslHostRuntimeEnvironment(environment)) {
|
||||
// An explicit connect may boot the distro.
|
||||
await getWslHostService()?.connect(environment.id)
|
||||
}
|
||||
await connectThroughOwningService(environment)
|
||||
return getRuntimeEnvironmentStatus(getUserDataPath(), environment.id, args.timeoutMs, {
|
||||
reconnect: true
|
||||
})
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Pins what connect and remove do for each kind of host, so host-kind dispatch stays exhaustive.
|
||||
type WslServiceFake = { connect: ReturnType<typeof vi.fn>; remove: ReturnType<typeof vi.fn> }
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const order: string[] = []
|
||||
const wsl: { service: WslServiceFake | null } = { service: null }
|
||||
return {
|
||||
order,
|
||||
handle: vi.fn(),
|
||||
resolveEnvironment: vi.fn(),
|
||||
removeEnvironment: vi.fn(),
|
||||
retire: vi.fn(),
|
||||
closeRequest: vi.fn(),
|
||||
getStatus: vi.fn(),
|
||||
clearDecline: vi.fn(),
|
||||
wsl
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('electron', () => ({ ipcMain: { handle: mocks.handle } }))
|
||||
vi.mock('../../shared/runtime-environment-store', () => ({
|
||||
addEnvironmentFromPairingCode: vi.fn(),
|
||||
listEnvironments: vi.fn(() => []),
|
||||
removeEnvironment: mocks.removeEnvironment,
|
||||
resolveEnvironment: mocks.resolveEnvironment
|
||||
}))
|
||||
vi.mock('./runtime-environment-removal-cleanup', () => ({
|
||||
retireRemovedRuntimeEnvironment: mocks.retire
|
||||
}))
|
||||
vi.mock('./runtime-environment-request-connections', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
closeRemoteRuntimeRequestConnection: mocks.closeRequest
|
||||
}))
|
||||
vi.mock('./runtime-environment-transport-routing', () => ({
|
||||
callRuntimeEnvironment: vi.fn(),
|
||||
getRuntimeEnvironmentStatus: mocks.getStatus
|
||||
}))
|
||||
vi.mock('../ssh/ssh-credential-decline', () => ({
|
||||
clearSshCredentialDeclinedForEnvironment: mocks.clearDecline
|
||||
}))
|
||||
vi.mock('../wsl/wsl-host-service-instance', () => ({ getWslHostService: () => mocks.wsl.service }))
|
||||
|
||||
import { registerRuntimeEnvironmentConnectivityHandlers } from './runtime-environment-connectivity-handlers'
|
||||
import { WSL_HOSTS_GATED_OFF_MESSAGE } from '../wsl/wsl-host-gate'
|
||||
import { WSL_HOSTS_INTERNAL_GATE_ENV } from '../../shared/wsl-host-internal-gate'
|
||||
|
||||
const endpoints = [
|
||||
{
|
||||
id: 'ws-1',
|
||||
kind: 'websocket',
|
||||
label: 'WebSocket',
|
||||
endpoint: 'ws://h',
|
||||
deviceToken: 't',
|
||||
publicKeyB64: 'k'
|
||||
}
|
||||
]
|
||||
const HOSTS = {
|
||||
paired: { id: 'paired-1', name: 'Box', runtimeId: 'r', endpoints },
|
||||
sshManagedLegacy: {
|
||||
id: 'ssh-legacy',
|
||||
name: 'Legacy',
|
||||
runtimeId: 'r',
|
||||
endpoints,
|
||||
orcadDeployment: {
|
||||
sshTargetId: 't1',
|
||||
sshTargetGeneration: 1,
|
||||
localPort: 4100,
|
||||
remotePort: 6768
|
||||
}
|
||||
},
|
||||
sshManaged: {
|
||||
id: 'ssh-1',
|
||||
name: 'Managed',
|
||||
runtimeId: 'r',
|
||||
endpoints,
|
||||
orcadDeployment: {
|
||||
kind: 'ssh',
|
||||
sshTargetId: 't1',
|
||||
sshTargetGeneration: 1,
|
||||
localPort: 4100,
|
||||
remotePort: 6768
|
||||
}
|
||||
},
|
||||
sshAccess: {
|
||||
id: 'access-1',
|
||||
name: 'Access',
|
||||
runtimeId: 'r',
|
||||
endpoints,
|
||||
sshAccess: {
|
||||
sshTargetId: 't2',
|
||||
sshTargetGeneration: 1,
|
||||
localPort: 4200,
|
||||
remotePort: 6768,
|
||||
endpointId: 'ws-1',
|
||||
previousPreferredEndpointId: 'ws-0'
|
||||
}
|
||||
},
|
||||
wsl: {
|
||||
id: 'wsl:ubuntu',
|
||||
name: 'Ubuntu (WSL)',
|
||||
runtimeId: 'r',
|
||||
endpoints,
|
||||
orcadDeployment: { kind: 'wsl', distro: 'Ubuntu', localPort: 42000, remotePort: 6768 }
|
||||
}
|
||||
} as const
|
||||
type HostName = keyof typeof HOSTS
|
||||
const NETWORK_HOSTS: HostName[] = ['paired', 'sshManagedLegacy', 'sshManaged', 'sshAccess']
|
||||
|
||||
const redacted = (name: HostName) => ({
|
||||
...HOSTS[name],
|
||||
endpoints: [{ id: 'ws-1', kind: 'websocket', label: 'WebSocket', endpoint: 'ws://h' }]
|
||||
})
|
||||
|
||||
function handler(channel: string): (event: unknown, args: Record<string, unknown>) => unknown {
|
||||
const call = mocks.handle.mock.calls.find(([name]) => name === channel)
|
||||
expect(call).toBeTruthy()
|
||||
return call![1]
|
||||
}
|
||||
|
||||
function register(activeRuntimeEnvironmentId?: string): void {
|
||||
registerRuntimeEnvironmentConnectivityHandlers({
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: connect and remove read only settings.
|
||||
store: { getSettings: () => ({ activeRuntimeEnvironmentId }) } as never,
|
||||
getUserDataPath: () => '/u',
|
||||
invalidateTransport: vi.fn()
|
||||
})
|
||||
}
|
||||
|
||||
function useHost(name: HostName): void {
|
||||
mocks.resolveEnvironment.mockReturnValue(HOSTS[name])
|
||||
mocks.removeEnvironment.mockReturnValue(HOSTS[name])
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.order.length = 0
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
|
||||
mocks.wsl.service = {
|
||||
// Why a macrotask: a dropped await would let getStatus run first and reorder the log.
|
||||
connect: vi.fn(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
mocks.order.push('wsl.connect')
|
||||
}),
|
||||
remove: vi.fn(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
mocks.order.push('wsl.remove')
|
||||
})
|
||||
}
|
||||
mocks.clearDecline.mockImplementation(() => mocks.order.push('clearDecline'))
|
||||
mocks.getStatus.mockImplementation(async () => {
|
||||
mocks.order.push('getStatus')
|
||||
return { id: 'status.get', ok: true, result: {}, _meta: { runtimeId: 'r' } }
|
||||
})
|
||||
register()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
describe('runtimeEnvironments:connect by host kind', () => {
|
||||
it('connects a WSL host through its service before asking for status', async () => {
|
||||
useHost('wsl')
|
||||
await handler('runtimeEnvironments:connect')(null, { selector: 'wsl:ubuntu', timeoutMs: 5 })
|
||||
expect(mocks.wsl.service!.connect).toHaveBeenCalledWith('wsl:ubuntu')
|
||||
expect(mocks.order).toEqual(['clearDecline', 'wsl.connect', 'getStatus'])
|
||||
expect(mocks.getStatus).toHaveBeenCalledWith('/u', 'wsl:ubuntu', 5, { reconnect: true })
|
||||
})
|
||||
|
||||
it('still asks a WSL host for status when no service exists', async () => {
|
||||
useHost('wsl')
|
||||
mocks.wsl.service = null
|
||||
await handler('runtimeEnvironments:connect')(null, { selector: 'wsl:ubuntu' })
|
||||
expect(mocks.order).toEqual(['clearDecline', 'getStatus'])
|
||||
})
|
||||
|
||||
it('refuses a WSL host with the gate closed before touching anything', async () => {
|
||||
useHost('wsl')
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
|
||||
await expect(
|
||||
handler('runtimeEnvironments:connect')(null, { selector: 'wsl:ubuntu' })
|
||||
).rejects.toThrow(WSL_HOSTS_GATED_OFF_MESSAGE)
|
||||
expect(mocks.order).toEqual([])
|
||||
})
|
||||
|
||||
it.each(NETWORK_HOSTS)('only asks a %s host for status', async (name) => {
|
||||
useHost(name)
|
||||
await handler('runtimeEnvironments:connect')(null, { selector: HOSTS[name].id })
|
||||
expect(mocks.wsl.service!.connect).not.toHaveBeenCalled()
|
||||
expect(mocks.clearDecline).toHaveBeenCalledWith(HOSTS[name])
|
||||
expect(mocks.order).toEqual(['clearDecline', 'getStatus'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('runtimeEnvironments:remove by host kind', () => {
|
||||
it('hands a WSL host to its service and reports the resolved record', async () => {
|
||||
useHost('wsl')
|
||||
const result = handler('runtimeEnvironments:remove')(null, { selector: 'wsl:ubuntu' })
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
await expect(result).resolves.toEqual({ removed: redacted('wsl') })
|
||||
expect(mocks.wsl.service!.remove).toHaveBeenCalledWith('wsl:ubuntu')
|
||||
expect(mocks.removeEnvironment).not.toHaveBeenCalled()
|
||||
expect(mocks.retire).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('removes a WSL host generically when no service exists', () => {
|
||||
useHost('wsl')
|
||||
mocks.wsl.service = null
|
||||
expect(handler('runtimeEnvironments:remove')(null, { selector: 'Ubuntu (WSL)' })).toEqual({
|
||||
removed: redacted('wsl')
|
||||
})
|
||||
expect(mocks.removeEnvironment).toHaveBeenCalledWith('/u', 'Ubuntu (WSL)')
|
||||
expect(mocks.closeRequest).toHaveBeenCalledWith('Ubuntu (WSL)')
|
||||
})
|
||||
|
||||
it('refuses a WSL host with the gate closed', () => {
|
||||
useHost('wsl')
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
|
||||
expect(() => handler('runtimeEnvironments:remove')(null, { selector: 'wsl:ubuntu' })).toThrow(
|
||||
WSL_HOSTS_GATED_OFF_MESSAGE
|
||||
)
|
||||
expect(mocks.wsl.service!.remove).not.toHaveBeenCalled()
|
||||
expect(mocks.removeEnvironment).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses the default host before the gate check', () => {
|
||||
mocks.handle.mockClear()
|
||||
register('wsl:ubuntu')
|
||||
useHost('wsl')
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
|
||||
expect(() => handler('runtimeEnvironments:remove')(null, { selector: 'wsl:ubuntu' })).toThrow(
|
||||
'This server is the default host for new projects. Choose another one in Advanced before removing it.'
|
||||
)
|
||||
})
|
||||
|
||||
it.each(NETWORK_HOSTS)('removes a %s host generically and synchronously', (name) => {
|
||||
useHost(name)
|
||||
const id = HOSTS[name].id
|
||||
expect(handler('runtimeEnvironments:remove')(null, { selector: id })).toEqual({
|
||||
removed: redacted(name)
|
||||
})
|
||||
expect(mocks.wsl.service!.remove).not.toHaveBeenCalled()
|
||||
expect(mocks.removeEnvironment).toHaveBeenCalledWith('/u', id)
|
||||
expect(mocks.retire).toHaveBeenCalledWith(id, expect.any(Function), expect.anything())
|
||||
// Removed by its own id, so no legacy-selector transport is closed.
|
||||
expect(mocks.closeRequest).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
/** Connect and remove for hosts a desktop service owns (WSL); network hosts need neither. */
|
||||
import { classifyRuntimeEnvironmentHost } from '../../shared/runtime-environment-host-kind'
|
||||
import type { KnownRuntimeEnvironment } from '../../shared/runtime-environments'
|
||||
import { getWslHostService } from '../wsl/wsl-host-service-instance'
|
||||
|
||||
/** An explicit user connect; may boot a WSL host's distro. */
|
||||
export async function connectThroughOwningService(
|
||||
environment: KnownRuntimeEnvironment
|
||||
): Promise<void> {
|
||||
const host = classifyRuntimeEnvironmentHost(environment)
|
||||
switch (host.kind) {
|
||||
case 'wsl':
|
||||
await getWslHostService()?.connect(environment.id)
|
||||
break
|
||||
case 'network':
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/** Null when no service owns the host, which leaves removal to the generic store path. */
|
||||
export function removeThroughOwningService(
|
||||
environment: KnownRuntimeEnvironment
|
||||
): Promise<unknown> | null {
|
||||
const host = classifyRuntimeEnvironmentHost(environment)
|
||||
switch (host.kind) {
|
||||
case 'wsl':
|
||||
// Also releases the distro's holder and drops the WSL record.
|
||||
return getWslHostService()?.remove(environment.id) ?? null
|
||||
case 'network':
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,35 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
ensureTunnel: vi.fn(async () => {}),
|
||||
rebuildTunnel: vi.fn(async () => {}),
|
||||
verify: vi.fn(),
|
||||
restoreUpdate: vi.fn(() => null),
|
||||
redeploy: vi.fn(async () => true)
|
||||
}))
|
||||
const mocks = vi.hoisted(() => {
|
||||
const environments: Record<string, Record<string, unknown>> = {}
|
||||
const reads: Record<string, number> = {}
|
||||
return {
|
||||
environments,
|
||||
reads,
|
||||
ensureForRuntimeCall: vi.fn(async () => {}),
|
||||
hasWslService: true,
|
||||
ensureTunnel: vi.fn(async () => {}),
|
||||
rebuildTunnel: vi.fn(async () => {}),
|
||||
verify: vi.fn(),
|
||||
restoreUpdate: vi.fn(() => null),
|
||||
redeploy: vi.fn(async () => true)
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../shared/runtime-environment-store', () => ({
|
||||
resolveEnvironment: (_userData: string, id: string) => ({
|
||||
id,
|
||||
orcadDeployment: { sshTargetId: 'ssh-1' }
|
||||
})
|
||||
resolveEnvironment: (_userData: string, id: string) => {
|
||||
const environment = mocks.environments[id]
|
||||
if (!environment) {
|
||||
return { id, orcadDeployment: { sshTargetId: 'ssh-1' } }
|
||||
}
|
||||
mocks.reads[id] = (mocks.reads[id] ?? 0) + 1
|
||||
// A fresh object per read, so a test can tell the re-read after bring-up from the first.
|
||||
return { ...environment, read: mocks.reads[id] }
|
||||
}
|
||||
}))
|
||||
vi.mock('../wsl/wsl-host-service-instance', () => ({
|
||||
getWslHostService: () =>
|
||||
mocks.hasWslService ? { ensureForRuntimeCall: mocks.ensureForRuntimeCall } : null
|
||||
}))
|
||||
vi.mock('../ssh/managed-server-update-deps', () => ({ managedServerUpdateDeps: () => ({}) }))
|
||||
vi.mock('../ssh/orcad-managed-tunnel', () => ({
|
||||
@@ -33,6 +50,8 @@ vi.mock('./ssh-renderer-broadcast', () => ({ broadcastSshState: vi.fn() }))
|
||||
|
||||
import { MANAGED_ORCAD_NOT_ACTIVATED_DETAIL } from '../ssh/orcad-managed-serving'
|
||||
import { resolveManagedRuntimeEnvironment } from './runtime-environment-managed-tunnel'
|
||||
import { WSL_HOSTS_GATED_OFF_MESSAGE } from '../wsl/wsl-host-gate'
|
||||
import { WSL_HOSTS_INTERNAL_GATE_ENV } from '../../shared/wsl-host-internal-gate'
|
||||
|
||||
describe('resolving a managed runtime for a call', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
@@ -57,3 +76,61 @@ describe('resolving a managed runtime for a call', () => {
|
||||
expect(mocks.restoreUpdate).toHaveBeenCalledWith('env-1', expect.any(Function))
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolving a runtime for a call, by host kind', () => {
|
||||
const wsl = {
|
||||
id: 'wsl:ubuntu',
|
||||
orcadDeployment: { kind: 'wsl', distro: 'Ubuntu', localPort: 42000, remotePort: 6768 }
|
||||
}
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.hasWslService = true
|
||||
for (const id of Object.keys(mocks.reads)) {
|
||||
delete mocks.reads[id]
|
||||
}
|
||||
mocks.environments = { 'wsl:ubuntu': wsl, 'paired-1': { id: 'paired-1' } }
|
||||
mocks.verify.mockResolvedValue({ state: 'serving' })
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '1')
|
||||
})
|
||||
afterEach(() => vi.unstubAllEnvs())
|
||||
|
||||
it('brings a WSL host up through its service, never through an SSH tunnel', async () => {
|
||||
// The record re-read after bring-up, which may have moved the bridge port.
|
||||
await expect(resolveManagedRuntimeEnvironment('/u', 'wsl:ubuntu')).resolves.toEqual({
|
||||
...wsl,
|
||||
read: 2
|
||||
})
|
||||
expect(mocks.ensureForRuntimeCall).toHaveBeenCalledWith('wsl:ubuntu')
|
||||
expect(mocks.ensureTunnel).not.toHaveBeenCalled()
|
||||
expect(mocks.verify).not.toHaveBeenCalled()
|
||||
expect(mocks.restoreUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resolves a WSL host with no service without bringing anything up', async () => {
|
||||
mocks.hasWslService = false
|
||||
await expect(resolveManagedRuntimeEnvironment('/u', 'wsl:ubuntu')).resolves.toEqual({
|
||||
...wsl,
|
||||
read: 2
|
||||
})
|
||||
expect(mocks.ensureTunnel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses a WSL host with the gate closed', async () => {
|
||||
vi.stubEnv(WSL_HOSTS_INTERNAL_GATE_ENV, '')
|
||||
await expect(resolveManagedRuntimeEnvironment('/u', 'wsl:ubuntu')).rejects.toThrow(
|
||||
WSL_HOSTS_GATED_OFF_MESSAGE
|
||||
)
|
||||
expect(mocks.ensureForRuntimeCall).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('runs the managed tunnel path for a paired server too', async () => {
|
||||
await expect(resolveManagedRuntimeEnvironment('/u', 'paired-1')).resolves.toEqual({
|
||||
id: 'paired-1',
|
||||
read: 2
|
||||
})
|
||||
expect(mocks.ensureTunnel).toHaveBeenCalledWith('/u', 'paired-1')
|
||||
expect(mocks.verify).toHaveBeenCalledWith('/u', 'paired-1')
|
||||
expect(mocks.restoreUpdate).toHaveBeenCalledWith('paired-1', expect.any(Function))
|
||||
expect(mocks.ensureForRuntimeCall).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { resolveEnvironment } from '../../shared/runtime-environment-store'
|
||||
import {
|
||||
getOrcadSshDeployment,
|
||||
isWslHostRuntimeEnvironment
|
||||
type KnownRuntimeEnvironment
|
||||
} from '../../shared/runtime-environments'
|
||||
import { classifyRuntimeEnvironmentHost } from '../../shared/runtime-environment-host-kind'
|
||||
import type {
|
||||
SshManagedServerStatus,
|
||||
SshManagedServerUpdateNote,
|
||||
@@ -34,11 +35,23 @@ export async function resolveManagedRuntimeEnvironment(
|
||||
const environment = resolveEnvironment(userDataPath, selector)
|
||||
// Every call, subscription and status probe passes here, so a gated-off WSL host is never reached.
|
||||
assertWslHostReachable(environment)
|
||||
if (isWslHostRuntimeEnvironment(environment)) {
|
||||
// Reached over the wsl.exe bridge; connects only when an open project or terminal needs it.
|
||||
await getWslHostService()?.ensureForRuntimeCall(environment.id)
|
||||
return resolveEnvironment(userDataPath, environment.id)
|
||||
const host = classifyRuntimeEnvironmentHost(environment)
|
||||
switch (host.kind) {
|
||||
case 'wsl':
|
||||
// Reached over the wsl.exe bridge; connects only when an open project or terminal needs it.
|
||||
await getWslHostService()?.ensureForRuntimeCall(environment.id)
|
||||
break
|
||||
case 'network':
|
||||
await prepareNetworkServerForCall(userDataPath, environment)
|
||||
break
|
||||
}
|
||||
return resolveEnvironment(userDataPath, environment.id)
|
||||
}
|
||||
|
||||
async function prepareNetworkServerForCall(
|
||||
userDataPath: string,
|
||||
environment: KnownRuntimeEnvironment
|
||||
): Promise<void> {
|
||||
await ensureOrcadManagedTunnel(userDataPath, environment.id)
|
||||
// Why: a server that stopped (idle, killed, host rebooted) starts before the call that needs it.
|
||||
const serving = await verifyOrcadManagedServing(userDataPath, environment.id)
|
||||
@@ -71,7 +84,6 @@ export async function resolveManagedRuntimeEnvironment(
|
||||
.catch(() => {})
|
||||
}
|
||||
}
|
||||
return resolveEnvironment(userDataPath, environment.id)
|
||||
}
|
||||
|
||||
function isStoppedElsewhere(serving: OrcadManagedServing): boolean {
|
||||
|
||||
@@ -68,6 +68,27 @@ describe('runtime environment mobile relay hosts', () => {
|
||||
expect(mocks.resolveManaged).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('relays SSH-managed servers, with and without the persisted deployment kind', async () => {
|
||||
const tunnel = { sshTargetId: 't', sshTargetGeneration: 1, localPort: 1, remotePort: 2 }
|
||||
mocks.environments = [
|
||||
{ id: 'legacy', name: 'A', createdAt: 1, runtimeId: null, orcadDeployment: tunnel },
|
||||
{
|
||||
id: 'managed',
|
||||
name: 'B',
|
||||
createdAt: 2,
|
||||
runtimeId: null,
|
||||
orcadDeployment: { kind: 'ssh', ...tunnel }
|
||||
}
|
||||
]
|
||||
const hosts = createRuntimeEnvironmentMobileRelayHosts()
|
||||
expect(hosts.list().environments.map((environment) => environment.id)).toEqual([
|
||||
'legacy',
|
||||
'managed'
|
||||
])
|
||||
await hosts.resolve('managed')
|
||||
expect(mocks.resolveManaged).toHaveBeenCalledWith('/user-data', 'managed')
|
||||
})
|
||||
|
||||
it("lists configured servers with the desktop's own status, ignoring another pairing's snapshot", () => {
|
||||
mocks.environments = [
|
||||
{ id: 'env-1', name: 'VM', createdAt: 1, pairingRevision: 7, runtimeId: 'runtime-a' },
|
||||
|
||||
@@ -3,9 +3,9 @@ import { pairedRuntimeBrowserClientHostsPage } from '../browser/paired-runtime-b
|
||||
import { listEnvironments } from '../../shared/runtime-environment-store'
|
||||
import {
|
||||
getPreferredPairingOffer,
|
||||
isWslHostRuntimeEnvironment,
|
||||
type KnownRuntimeEnvironment
|
||||
} from '../../shared/runtime-environments'
|
||||
import { classifyRuntimeEnvironmentHost } from '../../shared/runtime-environment-host-kind'
|
||||
import {
|
||||
runtimeEnvironmentStatusFromSnapshot,
|
||||
type RuntimeEnvironmentStatus
|
||||
@@ -31,9 +31,17 @@ export function retireMobileDesktopRelayEnvironment(environmentId: string): void
|
||||
* distros, connected only for work open here, so a phone must not wake or route to them.
|
||||
*/
|
||||
function listRelayableEnvironments(userDataPath: string) {
|
||||
return listEnvironments(userDataPath).filter(
|
||||
(environment) => !isWslHostRuntimeEnvironment(environment)
|
||||
)
|
||||
return listEnvironments(userDataPath).filter(isRelayableToPhones)
|
||||
}
|
||||
|
||||
function isRelayableToPhones(environment: KnownRuntimeEnvironment): boolean {
|
||||
const host = classifyRuntimeEnvironmentHost(environment)
|
||||
switch (host.kind) {
|
||||
case 'network':
|
||||
return true
|
||||
case 'wsl':
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** The desktop's configured servers, addressed only by exact environment id. */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { withRuntimeUnreachableHint } from './runtime-environment-unreachable-hint'
|
||||
import { withRemoteRuntimeTailscaleHint } from '../../shared/remote-runtime-tailscale-hint'
|
||||
|
||||
const UNREACHABLE = 'Could not connect to the remote Orca runtime at ws://127.0.0.1:42000.'
|
||||
|
||||
@@ -26,4 +27,40 @@ describe('withRuntimeUnreachableHint', () => {
|
||||
}
|
||||
expect(withRuntimeUnreachableHint(wsl, 'Unauthorized', null)).toBe('Unauthorized')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a paired server', {}],
|
||||
[
|
||||
'a legacy SSH-managed server',
|
||||
{ orcadDeployment: { sshTargetId: 't', sshTargetGeneration: 1, localPort: 1, remotePort: 2 } }
|
||||
],
|
||||
[
|
||||
'an SSH-managed server',
|
||||
{
|
||||
orcadDeployment: {
|
||||
kind: 'ssh' as const,
|
||||
sshTargetId: 't',
|
||||
sshTargetGeneration: 1,
|
||||
localPort: 1,
|
||||
remotePort: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
])('gives %s exactly the Tailscale hint', (_name, environment) => {
|
||||
expect(withRuntimeUnreachableHint(environment, UNREACHABLE, 'ws://203.0.113.4:6768')).toBe(
|
||||
withRemoteRuntimeTailscaleHint(UNREACHABLE, 'ws://203.0.113.4:6768')
|
||||
)
|
||||
})
|
||||
|
||||
it('pins the full WSL hint text', () => {
|
||||
expect(
|
||||
withRuntimeUnreachableHint(
|
||||
{ orcadDeployment: { kind: 'wsl', distro: 'Debian', localPort: 1, remotePort: 2 } },
|
||||
'Timed out waiting for the remote Orca runtime.',
|
||||
null
|
||||
)
|
||||
).toBe(
|
||||
'Timed out waiting for the remote Orca runtime. The Orca server in the WSL distro Debian isn’t answering; connect it again in Settings → Remote Orca Servers → WSL hosts.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
/** What to suggest when a server cannot be reached, chosen by the kind of host that owns it. */
|
||||
import { withRemoteRuntimeTailscaleHint } from '../../shared/remote-runtime-tailscale-hint'
|
||||
import {
|
||||
isWslHostRuntimeEnvironment,
|
||||
type KnownRuntimeEnvironment
|
||||
} from '../../shared/runtime-environments'
|
||||
import { classifyRuntimeEnvironmentHost } from '../../shared/runtime-environment-host-kind'
|
||||
import type { KnownRuntimeEnvironment } from '../../shared/runtime-environments'
|
||||
|
||||
const UNREACHABLE_RE =
|
||||
/could not connect to the remote orca runtime|remote orca runtime closed the connection|timed out (?:waiting for|while connecting to) the remote orca runtime/i
|
||||
@@ -13,11 +11,14 @@ export function withRuntimeUnreachableHint(
|
||||
message: string,
|
||||
endpoint: string | null | undefined
|
||||
): string {
|
||||
if (!isWslHostRuntimeEnvironment(environment)) {
|
||||
return withRemoteRuntimeTailscaleHint(message, endpoint)
|
||||
const host = classifyRuntimeEnvironmentHost(environment)
|
||||
switch (host.kind) {
|
||||
case 'network':
|
||||
return withRemoteRuntimeTailscaleHint(message, endpoint)
|
||||
case 'wsl':
|
||||
// Why not Tailscale: a WSL host is a distro on this computer, reached over wsl.exe.
|
||||
return UNREACHABLE_RE.test(message)
|
||||
? `${message} The Orca server in the WSL distro ${host.deployment.distro} isn’t answering; connect it again in Settings → Remote Orca Servers → WSL hosts.`
|
||||
: message
|
||||
}
|
||||
// Why not Tailscale: a WSL host is a distro on this computer, reached over wsl.exe.
|
||||
return UNREACHABLE_RE.test(message)
|
||||
? `${message} The Orca server in the WSL distro ${environment.orcadDeployment.distro} isn’t answering; connect it again in Settings → Remote Orca Servers → WSL hosts.`
|
||||
: message
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { classifyRuntimeEnvironmentHost } from './runtime-environment-host-kind'
|
||||
|
||||
const tunnel = { sshTargetId: 't', sshTargetGeneration: 1, localPort: 1, remotePort: 2 }
|
||||
const wsl = { kind: 'wsl' as const, distro: 'Ubuntu', localPort: 1, remotePort: 2 }
|
||||
|
||||
describe('classifyRuntimeEnvironmentHost', () => {
|
||||
it.each([
|
||||
['no deployment', {}],
|
||||
['a null deployment', { orcadDeployment: null }],
|
||||
['an SSH link saved without its kind', { orcadDeployment: tunnel }],
|
||||
['an SSH link', { orcadDeployment: { kind: 'ssh' as const, ...tunnel } }]
|
||||
])('treats %s as a network host', (_name, environment) => {
|
||||
expect(classifyRuntimeEnvironmentHost(environment)).toEqual({ kind: 'network', environment })
|
||||
})
|
||||
|
||||
it('treats a WSL deployment as a WSL host and hands back its link', () => {
|
||||
const environment = { orcadDeployment: wsl }
|
||||
const host = classifyRuntimeEnvironmentHost(environment)
|
||||
expect(host).toEqual({ kind: 'wsl', environment, deployment: wsl })
|
||||
expect(host.environment).toBe(environment)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { OrcadDeploymentLink } from './runtime-environments'
|
||||
import type { OrcadWslDeploymentLink } from './wsl-host-deployment-link'
|
||||
|
||||
/**
|
||||
* What kind of host serves a runtime environment. Switch on `kind` (lint requires the switch to be
|
||||
* exhaustive), so a new kind fails the build at every switch on it.
|
||||
* `network`: reached over the network or an SSH tunnel (paired, SSH access, SSH-managed).
|
||||
* `wsl`: a distro on this computer, reached over the wsl.exe bridge.
|
||||
*/
|
||||
export type RuntimeEnvironmentHost<T> =
|
||||
| { kind: 'network'; environment: T }
|
||||
| { kind: 'wsl'; environment: T; deployment: OrcadWslDeploymentLink }
|
||||
|
||||
export function classifyRuntimeEnvironmentHost<
|
||||
T extends { orcadDeployment?: OrcadDeploymentLink | null }
|
||||
>(environment: T): RuntimeEnvironmentHost<T> {
|
||||
const deployment = environment.orcadDeployment
|
||||
if (!deployment) {
|
||||
return { kind: 'network', environment }
|
||||
}
|
||||
switch (deployment.kind) {
|
||||
// Why undefined: SSH deployment links are persisted without their kind.
|
||||
case undefined:
|
||||
case 'ssh':
|
||||
return { kind: 'network', environment }
|
||||
case 'wsl':
|
||||
return { kind: 'wsl', environment, deployment }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user