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
5 changes: 5 additions & 0 deletions apps/desktop/src/main/runtime-host-desktop-candidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
connectOrSpawnRuntimeHost,
connectRuntimeHostProfile,
type RuntimeHostPeerClient,
type RuntimeHostConnectionPhase,
type RuntimeHostSshInteraction,
type RuntimeHostSshTunnel,
type RuntimeHostSshTunnelInput,
Expand Down Expand Up @@ -202,6 +203,7 @@ export interface DesktopRuntimeHostCandidateStartInput
readonly onExit?: (details: CandidateExitDetails) => void;
readonly candidateLaunchBarrier?: RuntimeHostCandidateLaunchBarrier;
readonly peerClient?: RuntimeHostPeerClient;
readonly onConnectionPhase?: (phase: RuntimeHostConnectionPhase) => void;
readonly profileTarget?: {
readonly profile: PersistedRuntimeHostProfile;
readonly credential?: string;
Expand Down Expand Up @@ -435,6 +437,9 @@ async function startProfileDesktopRuntimeHostCandidate(
: { handshakeTimeoutMs: input.handshakeTimeoutMs }),
readyTimeoutMs: input.electionDeadlineMs ?? 45_000,
...(input.peerClient === undefined ? {} : { peerClient: input.peerClient }),
...(input.onConnectionPhase === undefined
? {}
: { onConnectionPhase: input.onConnectionPhase }),
...(profileTarget.sshInteraction === undefined
? {}
: { sshInteraction: profileTarget.sshInteraction }),
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/main/runtime-host-desktop-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -872,6 +872,9 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
ipcMain: this.#ipcMain.createTarget(target.epoch),
isTargetActive: () => this.#ipcMain.isActive(target.epoch),
isTargetValid: () => target.valid,
onConnectionPhase: (phase) => {
target.input.onConnectionPhase?.(phase);
},
signal,
...(takeoverHostEpoch === undefined ? {} : { takeoverHostEpoch }),
},
Expand Down Expand Up @@ -1154,6 +1157,7 @@ class RuntimeHostDesktopManagerImpl implements RuntimeHostDesktopManager {
);
}
}

}

function trackOwnedProcess(
Expand Down
6 changes: 5 additions & 1 deletion packages/runtime-host/src/__tests__/peer-native.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,14 @@ module.exports = {
},
});
const native = await import(nativePath);
const phases: string[] = [];
const abort = new AbortController();
const pending = client.connect(peerConnectInput('pending'), abort.signal);
const pending = client.connect(peerConnectInput('pending'), abort.signal, (phase) => {
phases.push(phase);
});
await waitForRequestCount(native.default.stats, 1);
assert.equal(routesPrepared, true);
assert.deepEqual(phases, ['discovering', 'connecting']);
abort.abort();
await assert.rejects(pending, /aborted/u);

Expand Down
30 changes: 29 additions & 1 deletion packages/runtime-host/src/client/host-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import {
readRuntimeHostPeerAuthenticationResult,
writeRuntimeHostPeerAuthentication,
} from '../transport/peer-native.js';
import type { RuntimeHostPeerClient } from './peer-client.js';
import type { RuntimeHostPeerClient, RuntimeHostPeerConnectionPhase } from './peer-client.js';
import { RuntimeHostPermanentReconnectError } from './reconnect-lifecycle.js';
import { RuntimeHostRemoteCompatibilityError } from './remote-compatibility-error.js';
import {
Expand Down Expand Up @@ -156,6 +156,12 @@ export interface ResolvedRuntimeHostProfile {
readonly credential?: string;
}

export type RuntimeHostConnectionPhase =
| RuntimeHostPeerConnectionPhase
| 'authenticating'
| 'handshaking'
| 'waiting_for_ready';

export function sameResolvedRuntimeHostProfileTarget(
left: ResolvedRuntimeHostProfile,
right: ResolvedRuntimeHostProfile,
Expand Down Expand Up @@ -272,6 +278,7 @@ export async function connectRuntimeHostProfile(
readonly readyTimeoutMs?: number;
readonly sshInteraction?: RuntimeHostSshInteraction;
readonly peerClient?: RuntimeHostPeerClient;
readonly onConnectionPhase?: (phase: RuntimeHostConnectionPhase) => void;
},
overrides: {
connect?: typeof connectRemoteRuntimeHost;
Expand Down Expand Up @@ -326,6 +333,7 @@ export async function connectRemoteRuntimeHostProfile(
readonly readyTimeoutMs?: number;
readonly sshInteraction?: RuntimeHostSshInteraction;
readonly peerClient?: RuntimeHostPeerClient;
readonly onConnectionPhase?: (phase: RuntimeHostConnectionPhase) => void;
},
overrides: {
connect?: typeof connectRemoteRuntimeHost;
Expand All @@ -351,8 +359,12 @@ export async function connectRemoteRuntimeHostProfile(
...(input.handshakeTimeoutMs === undefined
? {}
: { handshakeTimeoutMs: input.handshakeTimeoutMs }),
...(input.onConnectionPhase === undefined
? {}
: { onConnectionPhase: input.onConnectionPhase }),
});
} else {
notifyConnectionPhase(input.onConnectionPhase, 'connecting');
const activation =
transport.kind === 'ssh' && transport.activation
? await (overrides.activateSshOperator ?? activateRuntimeHostSshOperator)({
Expand Down Expand Up @@ -420,6 +432,7 @@ export async function connectRemoteRuntimeHostProfile(
}
try {
input.signal?.throwIfAborted();
notifyConnectionPhase(input.onConnectionPhase, 'waiting_for_ready');
await (overrides.waitForReady ?? waitForRuntimeHostReady)(
connection,
input.readyTimeoutMs ?? 45_000,
Expand Down Expand Up @@ -454,6 +467,7 @@ export async function connectPeerRuntimeHost(input: {
readonly signal?: AbortSignal;
readonly connectTimeoutMs?: number;
readonly handshakeTimeoutMs?: number;
readonly onConnectionPhase?: (phase: RuntimeHostConnectionPhase) => void;
}): Promise<RuntimeHostConnection> {
input.signal?.throwIfAborted();
const stream = await input.peerClient.connect(
Expand All @@ -464,13 +478,15 @@ export async function connectPeerRuntimeHost(input: {
directDeadlineMs: Math.min(input.connectTimeoutMs ?? 40_000, 120_000),
},
input.signal,
input.onConnectionPhase,
);
const abort = () => stream.abort();
input.signal?.addEventListener('abort', abort, { once: true });
if (input.signal?.aborted) abort();
let transferred = false;
try {
input.signal?.throwIfAborted();
notifyConnectionPhase(input.onConnectionPhase, 'authenticating');
await writeRuntimeHostPeerAuthentication(stream, input.credential);
const authentication = await readRuntimeHostPeerAuthenticationResult(
stream,
Expand All @@ -481,6 +497,7 @@ export async function connectPeerRuntimeHost(input: {
`Runtime Host profile ${input.profileId} rejected its access credential`,
);
}
notifyConnectionPhase(input.onConnectionPhase, 'handshaking');
const result = await connectRuntimeHostMessageTransport({
transport: new FramedByteStreamTransport(
new RuntimeHostPeerByteStream(stream, authentication.remainder),
Expand Down Expand Up @@ -512,6 +529,17 @@ export async function connectPeerRuntimeHost(input: {
}
}

function notifyConnectionPhase(
observer: ((phase: RuntimeHostConnectionPhase) => void) | undefined,
phase: RuntimeHostConnectionPhase,
): void {
try {
observer?.(phase);
} catch {
// Connection progress is diagnostic state and cannot control the connection.
}
}

function requireRuntimeHostPeerClient(
peerClient: RuntimeHostPeerClient | undefined,
): RuntimeHostPeerClient {
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-host/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export {
type RuntimeHostProfile,
type RuntimeHostProfileAccess,
type RuntimeHostProfileCatalog,
type RuntimeHostConnectionPhase,
type RuntimeHostProfileDocument,
} from './host-profile.js';
export {
Expand Down
17 changes: 17 additions & 0 deletions packages/runtime-host/src/client/peer-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export interface RuntimeHostPeerConnectInput {
readonly directDeadlineMs: number;
}

export type RuntimeHostPeerConnectionPhase = 'discovering' | 'connecting';

export interface RuntimeHostPeerRouteResolver {
resolveRoutes(peerId: string):
| {
Expand Down Expand Up @@ -67,6 +69,7 @@ export interface RuntimeHostPeerClient {
connect(
input: RuntimeHostPeerConnectInput,
signal?: AbortSignal,
onPhase?: (phase: RuntimeHostPeerConnectionPhase) => void,
): Promise<RuntimeHostPeerNativeStream>;
connectMeshControl(
input: RuntimeHostPeerConnectInput,
Expand Down Expand Up @@ -204,8 +207,11 @@ class RuntimeHostPeerClientImpl implements RuntimeHostPeerClient {
async connect(
input: RuntimeHostPeerConnectInput,
signal?: AbortSignal,
onPhase?: (phase: RuntimeHostPeerConnectionPhase) => void,
): Promise<RuntimeHostPeerNativeStream> {
notifyPhase(onPhase, 'discovering');
await this.#prepareRoutes(input, signal);
notifyPhase(onPhase, 'connecting');
return this.#connect(input, signal, 'application');
}

Expand Down Expand Up @@ -473,6 +479,17 @@ interface InboundConsumer {
readonly reject: (error: Error) => void;
}

function notifyPhase(
observer: ((phase: RuntimeHostPeerConnectionPhase) => void) | undefined,
phase: RuntimeHostPeerConnectionPhase,
): void {
try {
observer?.(phase);
} catch {
// Connection progress is diagnostic state and cannot control the connection.
}
}

function waitForPeerConnectTurn(previous: Promise<void>, signal?: AbortSignal): Promise<void> {
if (!signal) return previous;
if (signal.aborted) return Promise.reject(signal.reason);
Expand Down
Loading