Fix dev override host bootstrap (#789)

This commit is contained in:
Mohamed Boudra
2026-05-07 13:08:10 +08:00
committed by GitHub
parent 16944da45b
commit 7558bd7947
4 changed files with 212 additions and 59 deletions

View File

@@ -65,6 +65,7 @@ import { polyfillCrypto } from "@/polyfills/crypto";
import { queryClient } from "@/query/query-client";
import {
getHostRuntimeStore,
hasConfiguredLocalDaemonOverride,
useHostMutations,
useHostRuntimeClient,
useHosts,
@@ -326,6 +327,8 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
const anyOnlineHostServerId = useEarliestOnlineHostServerId();
const daemonStartError = useDaemonStartLastError();
const daemonStartIsRunning = useDaemonStartIsRunning();
const waitForConfiguredLocalDaemon =
hasConfiguredLocalDaemonOverride() && !shouldUseDesktopDaemon();
const [hasGivenUpWaitingForHost, setHasGivenUpWaitingForHost] = useState(false);
useEffect(() => {
@@ -333,6 +336,7 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
anyOnlineHostServerId ||
daemonStartError ||
daemonStartIsRunning ||
waitForConfiguredLocalDaemon ||
hasGivenUpWaitingForHost
) {
return;
@@ -343,7 +347,13 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
return () => {
clearTimeout(handle);
};
}, [anyOnlineHostServerId, daemonStartError, daemonStartIsRunning, hasGivenUpWaitingForHost]);
}, [
anyOnlineHostServerId,
daemonStartError,
daemonStartIsRunning,
waitForConfiguredLocalDaemon,
hasGivenUpWaitingForHost,
]);
const retry = useCallback(() => {
const daemonStartService = getDaemonStartService({ store: getHostRuntimeStore() });

View File

@@ -10,7 +10,7 @@ import {
serializeConnectionUri,
serializeConnectionUriForStorage,
} from "@/utils/daemon-endpoints";
import { DaemonConnectionTestError, connectToDaemon } from "@/utils/test-daemon-connection";
import { DaemonConnectionTestError } from "@/utils/test-daemon-connection";
import { AdaptiveModalSheet, AdaptiveTextInput } from "./adaptive-modal-sheet";
import { Button } from "@/components/ui/button";
@@ -267,7 +267,7 @@ export interface AddHostModalProps {
export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostModalProps) {
const { theme } = useUnistyles();
const daemons = useHosts();
const { upsertDirectConnection } = useHostMutations();
const { probeAndUpsertDirectConnection } = useHostMutations();
const isMobile = useIsCompactFormFactor();
const [isSaving, setIsSaving] = useState(false);
@@ -336,22 +336,12 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod
setIsSaving(true);
setErrorMessage("");
const { client, serverId, hostname } = await connectToDaemon({
id: "probe",
type: "directTcp",
const { profile, serverId, hostname } = await probeAndUpsertDirectConnection({
endpoint: connection.endpoint,
useTls: connection.useTls,
...(connection.password ? { password: connection.password } : {}),
});
await client.close().catch(() => undefined);
const isNewHost = !daemons.some((daemon) => daemon.serverId === serverId);
const profile = await upsertDirectConnection({
serverId,
endpoint: connection.endpoint,
useTls: connection.useTls,
...(connection.password ? { password: connection.password } : {}),
label: hostname ?? undefined,
});
onSaved?.({ profile, serverId, hostname, isNewHost });
handleClose();
@@ -381,7 +371,7 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod
onSaved,
password,
port,
upsertDirectConnection,
probeAndUpsertDirectConnection,
useTls,
]);

View File

@@ -1596,6 +1596,85 @@ describe("HostRuntimeStore", () => {
store.syncHosts([]);
});
it("probeAndUpsertConnection learns the real server id before storing a direct host", async () => {
const connection: HostConnection = {
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
};
const probeClient = makeConnectedProbeClient(5);
const seenProbeHosts: string[] = [];
const store = new HostRuntimeStore({
deps: {
createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
connectToDaemon: async ({ host, connection: probedConnection }) => {
seenProbeHosts.push(host.serverId);
expect(probedConnection).toEqual(connection);
return {
client: probeClient as unknown as DaemonClient,
serverId: "srv_real_direct",
hostname: "mbp",
};
},
getClientId: async () => "cid_test_runtime",
},
});
const result = await store.probeAndUpsertConnection({ connection });
expect(result.serverId).toBe("srv_real_direct");
expect(result.hostname).toBe("mbp");
expect(seenProbeHosts).toEqual([""]);
expect(probeClient.closeCalls).toBe(0);
expect(store.getHosts()).toMatchObject([
{
serverId: "srv_real_direct",
label: "mbp",
connections: [connection],
},
]);
store.syncHosts([]);
});
it("probeAndUpsertConnection replaces a matching placeholder host with the real server id", async () => {
const connection: HostConnection = {
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
};
const store = new HostRuntimeStore({
deps: {
createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
connectToDaemon: async () => ({
client: makeConnectedProbeClient(5) as unknown as DaemonClient,
serverId: "srv_real_direct",
hostname: "mbp",
}),
getClientId: async () => "cid_test_runtime",
},
});
(
store as unknown as {
hosts: HostProfile[];
}
).hosts = [
makeHost({
serverId: "local:lan:6767",
label: "local:lan:6767",
connections: [connection],
preferredConnectionId: connection.id,
}),
];
await store.probeAndUpsertConnection({ connection });
expect(store.getHosts().map((host) => host.serverId)).toEqual(["srv_real_direct"]);
expect(store.getHosts()[0]?.label).toBe("mbp");
store.syncHosts([]);
});
it("uses the advertised hostname when adding a relay host from a pairing offer", async () => {
const store = new HostRuntimeStore({
deps: {

View File

@@ -9,7 +9,6 @@ import {
} from "@server/client/daemon-client";
import {
connectionFromListen,
hostHasConnection,
normalizeStoredHostProfile,
upsertHostConnectionInProfiles,
registryHasConnection,
@@ -120,7 +119,11 @@ export interface HostRuntimeControllerDeps {
clientId: string;
runtimeGeneration: number;
}) => DaemonClient;
connectToDaemon: (input: { host: HostProfile; connection: HostConnection }) => Promise<{
connectToDaemon: (input: {
host: HostProfile;
connection: HostConnection;
timeoutMs?: number;
}) => Promise<{
client: DaemonClient;
serverId: string;
hostname: string | null;
@@ -142,6 +145,7 @@ const PROBE_MAX_BACKOFF_MS = 30_000;
const ADAPTIVE_SWITCH_THRESHOLD_MS = 40;
const ADAPTIVE_SWITCH_CONSECUTIVE_PROBES = 3;
const DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT = 200;
const CONFIGURED_OVERRIDE_BOOTSTRAP_RETRY_MS = 1_000;
const DEFAULT_AGENT_DIRECTORY_SORT: NonNullable<FetchAgentsOptions["sort"]> = [
{ key: "updated_at", direction: "desc" },
@@ -485,8 +489,11 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
},
});
},
connectToDaemon: ({ host, connection }) =>
connectToDaemon(connection, { serverId: host.serverId }),
connectToDaemon: ({ host, connection, timeoutMs }) =>
connectToDaemon(connection, {
...(host.serverId ? { serverId: host.serverId } : {}),
...(timeoutMs !== undefined ? { timeoutMs } : {}),
}),
getClientId: () => getOrCreateClientId(),
};
}
@@ -1179,14 +1186,18 @@ function readConfiguredLocalDaemonOverride(): string | null {
return value && value.length > 0 ? value : null;
}
function placeholderServerIdForEndpoint(endpoint: string): string {
return `local:${normalizeHostPort(endpoint)}`;
export function hasConfiguredLocalDaemonOverride(): boolean {
return readConfiguredLocalDaemonOverride() !== null;
}
function isPlaceholderServerId(serverId: string): boolean {
return serverId.startsWith("local:");
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function rekeyMap<V>(map: Map<string, V>, oldKey: string, newKey: string): void {
const value = map.get(oldKey);
if (value === undefined) {
@@ -1207,6 +1218,7 @@ export class HostRuntimeStore {
private deps: HostRuntimeControllerDeps;
private lastConnectionStatusByServer = new Map<string, HostRuntimeConnectionStatus>();
private agentDirectoryBootstrapInFlight = new Map<string, Promise<void>>();
private configuredOverrideBootstrapInFlight: Promise<void> | null = null;
private bootStarted = false;
constructor(input?: { deps?: HostRuntimeControllerDeps }) {
@@ -1239,12 +1251,8 @@ export class HostRuntimeStore {
}
private async runBoot(): Promise<void> {
await this.loadFromStorage();
const override = readConfiguredLocalDaemonOverride();
if (override) {
this.prunePlaceholderHostsForOtherEndpoints(override);
}
await this.loadFromStorage();
let isE2E: string | null = null;
try {
@@ -1277,12 +1285,16 @@ export class HostRuntimeStore {
if (!Array.isArray(parsed)) {
return;
}
const profiles = parsed
const normalizedProfiles = parsed
.map((entry) => normalizeStoredHostProfile(entry))
.filter((entry): entry is HostProfile => entry !== null);
const profiles = normalizedProfiles.filter((entry) => !isPlaceholderServerId(entry.serverId));
this.hosts = profiles;
this.syncHosts(profiles);
this.emitHostList();
if (profiles.length !== normalizedProfiles.length) {
void this.persistHosts();
}
} catch (error) {
console.error("[HostRuntime] Failed to load host registry from storage", error);
}
@@ -1295,15 +1307,9 @@ export class HostRuntimeStore {
}
try {
const { client, serverId, hostname } = await connectToDaemon(connection, {
timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS,
});
await this.upsertHostConnection({
serverId,
label: hostname ?? undefined,
await this.probeAndUpsertConnection({
connection,
existingClient: client,
timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS,
});
} catch (error) {
console.warn("[HostRuntime] bootstrap probe failed", {
@@ -1318,35 +1324,45 @@ export class HostRuntimeStore {
if (!connection) {
return;
}
const placeholderId = placeholderServerIdForEndpoint(endpoint);
const staleHosts = this.hosts.filter(
(host) => host.serverId !== placeholderId && hostHasConnection(host, connection),
);
for (const staleHost of staleHosts) {
void this.removeConnection(staleHost.serverId, connection.id);
}
if (registryHasConnection(this.hosts, connection)) {
return;
}
void this.upsertHostConnection({
serverId: placeholderId,
connection,
});
}
private prunePlaceholderHostsForOtherEndpoints(currentEndpoint: string): void {
const expectedServerId = placeholderServerIdForEndpoint(currentEndpoint);
const remaining = this.hosts.filter((host) => {
if (!isPlaceholderServerId(host.serverId)) {
return true;
}
return host.serverId === expectedServerId;
});
if (remaining.length === this.hosts.length) {
if (this.configuredOverrideBootstrapInFlight) {
return;
}
this.setHostsAndSync(remaining);
void this.persistHosts();
const bootstrap = this.runConfiguredOverrideBootstrap(endpoint, connection).finally(() => {
if (this.configuredOverrideBootstrapInFlight === bootstrap) {
this.configuredOverrideBootstrapInFlight = null;
}
});
this.configuredOverrideBootstrapInFlight = bootstrap;
}
private async runConfiguredOverrideBootstrap(
endpoint: string,
connection: HostConnection,
): Promise<void> {
let attempt = 0;
while (!registryHasConnection(this.hosts, connection)) {
attempt += 1;
try {
await this.probeAndUpsertConnection({
connection,
timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS,
});
return;
} catch (error) {
if (attempt === 1 || attempt % 10 === 0) {
console.warn("[HostRuntime] configured bootstrap probe failed", {
endpoint,
attempt,
error,
});
}
await delay(CONFIGURED_OVERRIDE_BOOTSTRAP_RETRY_MS);
}
}
}
reconcileServerId(oldServerId: string, newServerId: string): void {
@@ -1411,6 +1427,57 @@ export class HostRuntimeStore {
});
}
async probeAndUpsertConnection(input: {
connection: HostConnection;
label?: string;
timeoutMs?: number;
}): Promise<{ profile: HostProfile; serverId: string; hostname: string | null }> {
if (input.connection.type === "relay") {
throw new Error("Cannot probe a relay connection without a server id.");
}
const probeHost: HostProfile = {
serverId: "",
label: input.label ?? input.connection.id,
lifecycle: {},
connections: [input.connection],
preferredConnectionId: input.connection.id,
createdAt: new Date(0).toISOString(),
updatedAt: new Date(0).toISOString(),
};
const { client, serverId, hostname } = await this.deps.connectToDaemon({
host: probeHost,
connection: input.connection,
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
});
const profile = await this.upsertHostConnection({
serverId,
label: input.label ?? hostname ?? undefined,
connection: input.connection,
existingClient: client,
});
return { profile, serverId, hostname };
}
async probeAndUpsertDirectConnection(input: {
endpoint: string;
useTls?: boolean;
password?: string;
label?: string;
}): Promise<{ profile: HostProfile; serverId: string; hostname: string | null }> {
const endpoint = normalizeHostPort(input.endpoint);
const password = input.password?.trim();
return this.probeAndUpsertConnection({
label: input.label,
connection: {
id: `direct:${endpoint}`,
type: "directTcp",
endpoint,
useTls: input.useTls ?? false,
...(password ? { password } : {}),
},
});
}
async upsertRelayConnection(input: {
serverId: string;
relayEndpoint: string;
@@ -1985,6 +2052,12 @@ export interface HostMutations {
password?: string;
label?: string;
}) => Promise<HostProfile>;
probeAndUpsertDirectConnection: (input: {
endpoint: string;
useTls?: boolean;
password?: string;
label?: string;
}) => Promise<{ profile: HostProfile; serverId: string; hostname: string | null }>;
upsertRelayConnection: (input: {
serverId: string;
relayEndpoint: string;
@@ -2007,6 +2080,7 @@ export function useHostMutations(): HostMutations {
return useMemo(
() => ({
upsertDirectConnection: (input) => store.upsertDirectConnection(input),
probeAndUpsertDirectConnection: (input) => store.probeAndUpsertDirectConnection(input),
upsertRelayConnection: (input) => store.upsertRelayConnection(input),
upsertConnectionFromOffer: (offer, label) => store.upsertConnectionFromOffer(offer, label),
upsertConnectionFromOfferUrl: (url, label) => store.upsertConnectionFromOfferUrl(url, label),