refactor: rename sockPath to listen in pid lock, add startup timing instrumentation
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useSyncExternalStore, useState } from 'react'
|
import { useEffect, useRef, useSyncExternalStore, useState } from 'react'
|
||||||
import { usePathname, useRouter } from 'expo-router'
|
import { usePathname, useRouter } from 'expo-router'
|
||||||
import { useDaemonRegistry } from '@/contexts/daemon-registry-context'
|
import { useDaemonRegistry } from '@/contexts/daemon-registry-context'
|
||||||
import { shouldUseManagedDesktopDaemon } from '@/desktop/managed-runtime/managed-runtime'
|
import { shouldUseManagedDesktopDaemon } from '@/desktop/managed-runtime/managed-runtime'
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from './index-startup'
|
} from './index-startup'
|
||||||
|
|
||||||
const STARTUP_TIMEOUT_MS = 30_000
|
const STARTUP_TIMEOUT_MS = 30_000
|
||||||
|
const APP_START_TIME = performance.now()
|
||||||
|
|
||||||
function useAnyHostOnline(serverIds: string[]): string | null {
|
function useAnyHostOnline(serverIds: string[]): string | null {
|
||||||
const runtime = getHostRuntimeStore()
|
const runtime = getHostRuntimeStore()
|
||||||
@@ -60,6 +61,17 @@ export default function Index() {
|
|||||||
const [hasTimedOut, setHasTimedOut] = useState(false)
|
const [hasTimedOut, setHasTimedOut] = useState(false)
|
||||||
const isDesktopStartupRace = shouldUseManagedDesktopDaemon()
|
const isDesktopStartupRace = shouldUseManagedDesktopDaemon()
|
||||||
const onlineServerId = useAnyHostOnline(daemons.map((daemon) => daemon.serverId))
|
const onlineServerId = useAnyHostOnline(daemons.map((daemon) => daemon.serverId))
|
||||||
|
const startupLogged = useRef(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (onlineServerId && !startupLogged.current) {
|
||||||
|
startupLogged.current = true
|
||||||
|
console.info('[Startup] host online', {
|
||||||
|
serverId: onlineServerId,
|
||||||
|
elapsedMs: Math.round(performance.now() - APP_START_TIME),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [onlineServerId])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -80,6 +92,10 @@ export default function Index() {
|
|||||||
if (pathname !== '/' && pathname !== '') {
|
if (pathname !== '/' && pathname !== '') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
console.info('[Startup] navigating to host route', {
|
||||||
|
serverId: onlineServerId,
|
||||||
|
elapsedMs: Math.round(performance.now() - APP_START_TIME),
|
||||||
|
})
|
||||||
router.replace(buildHostRootRoute(onlineServerId) as any)
|
router.replace(buildHostRootRoute(onlineServerId) as any)
|
||||||
}, [onlineServerId, pathname, registryLoading, router])
|
}, [onlineServerId, pathname, registryLoading, router])
|
||||||
|
|
||||||
|
|||||||
@@ -483,15 +483,20 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
|||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
const bootstrapDesktopStartup = async () => {
|
const bootstrapDesktopStartup = async () => {
|
||||||
|
const t0 = performance.now()
|
||||||
|
const elapsed = () => Math.round(performance.now() - t0)
|
||||||
try {
|
try {
|
||||||
const isE2E = await AsyncStorage.getItem(E2E_STORAGE_KEY)
|
const isE2E = await AsyncStorage.getItem(E2E_STORAGE_KEY)
|
||||||
if (cancelled || isE2E) {
|
if (cancelled || isE2E) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.info('[DaemonRegistry] desktop bootstrap: starting', { elapsedMs: elapsed() })
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
(async () => {
|
(async () => {
|
||||||
|
console.info('[DaemonRegistry] desktop bootstrap: startManagedDaemon', { elapsedMs: elapsed() })
|
||||||
const daemon = await startManagedDaemon()
|
const daemon = await startManagedDaemon()
|
||||||
|
console.info('[DaemonRegistry] desktop bootstrap: managed daemon ready', { elapsedMs: elapsed(), serverId: daemon.serverId })
|
||||||
if (cancelled || !daemon.serverId) {
|
if (cancelled || !daemon.serverId) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -506,15 +511,18 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
|||||||
label: daemon.hostname ?? undefined,
|
label: daemon.hostname ?? undefined,
|
||||||
connection,
|
connection,
|
||||||
})
|
})
|
||||||
|
console.info('[DaemonRegistry] desktop bootstrap: host connection upserted', { elapsedMs: elapsed() })
|
||||||
})().catch((managedBootstrapError) => {
|
})().catch((managedBootstrapError) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'[DaemonRegistry] Failed to bootstrap desktop daemon connection',
|
'[DaemonRegistry] Failed to bootstrap desktop daemon connection',
|
||||||
|
{ elapsedMs: elapsed() },
|
||||||
managedBootstrapError
|
managedBootstrapError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
(async () => {
|
(async () => {
|
||||||
|
console.info('[DaemonRegistry] desktop bootstrap: probing localhost', { elapsedMs: elapsed() })
|
||||||
const { serverId, hostname } = await probeConnection(
|
const { serverId, hostname } = await probeConnection(
|
||||||
{
|
{
|
||||||
id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`,
|
id: `bootstrap:${DEFAULT_LOCALHOST_ENDPOINT}`,
|
||||||
@@ -523,6 +531,7 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
|||||||
},
|
},
|
||||||
{ timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS }
|
{ timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS }
|
||||||
)
|
)
|
||||||
|
console.info('[DaemonRegistry] desktop bootstrap: localhost probe succeeded', { elapsedMs: elapsed(), serverId })
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -532,8 +541,9 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) {
|
|||||||
endpoint: DEFAULT_LOCALHOST_ENDPOINT,
|
endpoint: DEFAULT_LOCALHOST_ENDPOINT,
|
||||||
label: hostname ?? undefined,
|
label: hostname ?? undefined,
|
||||||
})
|
})
|
||||||
|
console.info('[DaemonRegistry] desktop bootstrap: direct connection upserted', { elapsedMs: elapsed() })
|
||||||
})().catch(() => {
|
})().catch(() => {
|
||||||
// Best-effort startup race only; ignore if localhost isn't reachable.
|
console.info('[DaemonRegistry] desktop bootstrap: localhost probe failed', { elapsedMs: elapsed() })
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
} catch (bootstrapError) {
|
} catch (bootstrapError) {
|
||||||
|
|||||||
@@ -143,7 +143,9 @@ export type HostRuntimeStartOptions = {
|
|||||||
autoProbe?: boolean;
|
autoProbe?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PROBE_INTERVAL_MS = 10_000;
|
const PROBE_TICK_MS = 2_000;
|
||||||
|
const PROBE_STEADY_MS = 10_000;
|
||||||
|
const PROBE_MAX_BACKOFF_MS = 30_000;
|
||||||
const ADAPTIVE_SWITCH_THRESHOLD_MS = 40;
|
const ADAPTIVE_SWITCH_THRESHOLD_MS = 40;
|
||||||
const ADAPTIVE_SWITCH_CONSECUTIVE_PROBES = 3;
|
const ADAPTIVE_SWITCH_CONSECUTIVE_PROBES = 3;
|
||||||
const DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT = 200;
|
const DEFAULT_AGENT_DIRECTORY_PAGE_LIMIT = 200;
|
||||||
@@ -419,19 +421,19 @@ function findConnectionById(
|
|||||||
return host.connections.find((connection) => connection.id === connectionId) ?? null;
|
return host.connections.find((connection) => connection.id === connectionId) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectInitialConnectionId(host: HostProfile): string | null {
|
function probeIntervalForConnection(
|
||||||
if (host.connections.length === 0) {
|
firstSeenAt: number,
|
||||||
return null;
|
isActiveOnline: boolean,
|
||||||
|
now: number
|
||||||
|
): number {
|
||||||
|
if (isActiveOnline) {
|
||||||
|
return PROBE_STEADY_MS;
|
||||||
}
|
}
|
||||||
if (host.preferredConnectionId) {
|
const age = now - firstSeenAt;
|
||||||
const preferred = host.connections.find(
|
if (age < 10_000) return 2_000;
|
||||||
(connection) => connection.id === host.preferredConnectionId
|
if (age < 30_000) return 5_000;
|
||||||
);
|
if (age < 60_000) return PROBE_STEADY_MS;
|
||||||
if (preferred) {
|
return PROBE_MAX_BACKOFF_MS;
|
||||||
return preferred.id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return host.connections[0]?.id ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDefaultDeps(): HostRuntimeControllerDeps {
|
function createDefaultDeps(): HostRuntimeControllerDeps {
|
||||||
@@ -507,6 +509,8 @@ export class HostRuntimeController {
|
|||||||
private unsubscribeClientStatus: (() => void) | null = null;
|
private unsubscribeClientStatus: (() => void) | null = null;
|
||||||
private probeIntervalHandle: ReturnType<typeof setInterval> | null = null;
|
private probeIntervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||||
private started = false;
|
private started = false;
|
||||||
|
private connectionFirstSeenAt = new Map<string, number>();
|
||||||
|
private connectionLastProbedAt = new Map<string, number>();
|
||||||
private switchCandidateConnectionId: string | null = null;
|
private switchCandidateConnectionId: string | null = null;
|
||||||
private switchCandidateHitCount = 0;
|
private switchCandidateHitCount = 0;
|
||||||
private clientIdPromise: Promise<string> | null = null;
|
private clientIdPromise: Promise<string> | null = null;
|
||||||
@@ -555,19 +559,12 @@ export class HostRuntimeController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.started = true;
|
this.started = true;
|
||||||
|
this.trackConnectionFirstSeen();
|
||||||
await this.runProbeCycleNow();
|
await this.runProbeCycleNow();
|
||||||
|
if (options?.autoProbe !== false) {
|
||||||
if (!this.snapshot.activeConnectionId) {
|
|
||||||
const fallbackConnectionId = selectInitialConnectionId(this.host);
|
|
||||||
if (fallbackConnectionId) {
|
|
||||||
await this.switchToConnection({ connectionId: fallbackConnectionId });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.autoProbe !== false && PROBE_INTERVAL_MS > 0) {
|
|
||||||
this.probeIntervalHandle = setInterval(() => {
|
this.probeIntervalHandle = setInterval(() => {
|
||||||
void this.runProbeCycleNow();
|
void this.runProbeCycleNow();
|
||||||
}, PROBE_INTERVAL_MS);
|
}, PROBE_TICK_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -597,20 +594,8 @@ export class HostRuntimeController {
|
|||||||
|
|
||||||
async updateHost(host: HostProfile): Promise<void> {
|
async updateHost(host: HostProfile): Promise<void> {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
const activeConnectionId = this.snapshot.activeConnectionId;
|
this.trackConnectionFirstSeen();
|
||||||
const activeConnection = findConnectionById(host, activeConnectionId);
|
await this.runProbeCycleNow();
|
||||||
if (!activeConnection) {
|
|
||||||
await this.runProbeCycleNow();
|
|
||||||
if (this.snapshot.activeConnectionId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fallbackConnectionId = selectInitialConnectionId(host);
|
|
||||||
if (!fallbackConnectionId) {
|
|
||||||
await this.stop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this.switchToConnection({ connectionId: fallbackConnectionId });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureConnected(): void {
|
ensureConnected(): void {
|
||||||
@@ -674,9 +659,29 @@ export class HostRuntimeController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const probeByConnectionId = new Map<string, ConnectionProbeState>();
|
const now = performance.now();
|
||||||
|
const isOnline = this.snapshot.connectionStatus === "online";
|
||||||
|
const activeConnectionId = this.snapshot.activeConnectionId;
|
||||||
|
|
||||||
|
const connectionsToProbe = this.host.connections.filter((connection) => {
|
||||||
|
const lastProbed = this.connectionLastProbedAt.get(connection.id);
|
||||||
|
if (lastProbed == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const firstSeen = this.connectionFirstSeenAt.get(connection.id) ?? now;
|
||||||
|
const isActiveOnline = isOnline && connection.id === activeConnectionId;
|
||||||
|
const interval = probeIntervalForConnection(firstSeen, isActiveOnline, now);
|
||||||
|
return now - lastProbed >= interval;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (connectionsToProbe.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const probeByConnectionId = new Map(this.snapshot.probeByConnectionId);
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this.host.connections.map(async (connection) => {
|
connectionsToProbe.map(async (connection) => {
|
||||||
|
this.connectionLastProbedAt.set(connection.id, performance.now());
|
||||||
try {
|
try {
|
||||||
const latencyMs = await this.deps.measureLatency({
|
const latencyMs = await this.deps.measureLatency({
|
||||||
host: this.host,
|
host: this.host,
|
||||||
@@ -700,12 +705,12 @@ export class HostRuntimeController {
|
|||||||
}
|
}
|
||||||
this.updateSnapshot({ probeByConnectionId });
|
this.updateSnapshot({ probeByConnectionId });
|
||||||
|
|
||||||
const activeConnectionId = this.snapshot.activeConnectionId;
|
const currentActiveConnectionId = this.snapshot.activeConnectionId;
|
||||||
const activeProbe = activeConnectionId
|
const activeProbe = currentActiveConnectionId
|
||||||
? probeByConnectionId.get(activeConnectionId)
|
? probeByConnectionId.get(currentActiveConnectionId)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!activeConnectionId || !findConnectionById(this.host, activeConnectionId)) {
|
if (!currentActiveConnectionId || !findConnectionById(this.host, currentActiveConnectionId)) {
|
||||||
const nextConnectionId = selectBestConnection({
|
const nextConnectionId = selectBestConnection({
|
||||||
candidates: buildConnectionCandidates(this.host),
|
candidates: buildConnectionCandidates(this.host),
|
||||||
probeByConnectionId,
|
probeByConnectionId,
|
||||||
@@ -724,7 +729,7 @@ export class HostRuntimeController {
|
|||||||
candidates: buildConnectionCandidates(this.host),
|
candidates: buildConnectionCandidates(this.host),
|
||||||
probeByConnectionId,
|
probeByConnectionId,
|
||||||
});
|
});
|
||||||
if (nextConnectionId && nextConnectionId !== activeConnectionId) {
|
if (nextConnectionId && nextConnectionId !== currentActiveConnectionId) {
|
||||||
await this.switchToConnection({
|
await this.switchToConnection({
|
||||||
connectionId: nextConnectionId,
|
connectionId: nextConnectionId,
|
||||||
expectedProbeVersion: requestVersion,
|
expectedProbeVersion: requestVersion,
|
||||||
@@ -745,7 +750,7 @@ export class HostRuntimeController {
|
|||||||
.sort((left, right) => left.latencyMs - right.latencyMs);
|
.sort((left, right) => left.latencyMs - right.latencyMs);
|
||||||
|
|
||||||
const fastest = available[0] ?? null;
|
const fastest = available[0] ?? null;
|
||||||
if (!fastest || fastest.connectionId === activeConnectionId) {
|
if (!fastest || fastest.connectionId === currentActiveConnectionId) {
|
||||||
this.switchCandidateConnectionId = null;
|
this.switchCandidateConnectionId = null;
|
||||||
this.switchCandidateHitCount = 0;
|
this.switchCandidateHitCount = 0;
|
||||||
return;
|
return;
|
||||||
@@ -839,6 +844,22 @@ export class HostRuntimeController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private trackConnectionFirstSeen(): void {
|
||||||
|
const now = performance.now();
|
||||||
|
const currentIds = new Set(this.host.connections.map((c) => c.id));
|
||||||
|
for (const id of this.connectionFirstSeenAt.keys()) {
|
||||||
|
if (!currentIds.has(id)) {
|
||||||
|
this.connectionFirstSeenAt.delete(id);
|
||||||
|
this.connectionLastProbedAt.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const connection of this.host.connections) {
|
||||||
|
if (!this.connectionFirstSeenAt.has(connection.id)) {
|
||||||
|
this.connectionFirstSeenAt.set(connection.id, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private isCurrentSwitchRequest(version: number): boolean {
|
private isCurrentSwitchRequest(version: number): boolean {
|
||||||
return version === this.switchRequestVersion;
|
return version === this.switchRequestVersion;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface LocalDaemonPidInfo {
|
|||||||
startedAt?: string
|
startedAt?: string
|
||||||
hostname?: string
|
hostname?: string
|
||||||
uid?: number
|
uid?: number
|
||||||
sockPath?: string
|
listen?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LocalDaemonState {
|
export interface LocalDaemonState {
|
||||||
@@ -164,7 +164,7 @@ function readPidFile(pidPath: string): LocalDaemonPidInfo | null {
|
|||||||
startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : undefined,
|
startedAt: typeof parsed.startedAt === 'string' ? parsed.startedAt : undefined,
|
||||||
hostname: typeof parsed.hostname === 'string' ? parsed.hostname : undefined,
|
hostname: typeof parsed.hostname === 'string' ? parsed.hostname : undefined,
|
||||||
uid: typeof parsed.uid === 'number' ? parsed.uid : undefined,
|
uid: typeof parsed.uid === 'number' ? parsed.uid : undefined,
|
||||||
sockPath: typeof parsed.sockPath === 'string' ? parsed.sockPath : undefined,
|
listen: typeof parsed.listen === 'string' ? parsed.listen : typeof parsed.sockPath === 'string' ? parsed.sockPath : undefined,
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
@@ -322,7 +322,7 @@ export function resolveLocalDaemonState(options: { home?: string } = {}): LocalD
|
|||||||
const logPath = path.join(home, DAEMON_LOG_FILENAME)
|
const logPath = path.join(home, DAEMON_LOG_FILENAME)
|
||||||
const pidInfo = existsSync(pidPath) ? readPidFile(pidPath) : null
|
const pidInfo = existsSync(pidPath) ? readPidFile(pidPath) : null
|
||||||
const running = pidInfo ? isProcessRunning(pidInfo.pid) : false
|
const running = pidInfo ? isProcessRunning(pidInfo.pid) : false
|
||||||
const listen = pidInfo?.sockPath ?? config.listen
|
const listen = pidInfo?.listen ?? config.listen
|
||||||
|
|
||||||
return {
|
return {
|
||||||
home,
|
home,
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ function readPidSocketTarget(paseoHome: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(readFileSync(pidPath, 'utf-8')) as { sockPath?: unknown }
|
const parsed = JSON.parse(readFileSync(pidPath, 'utf-8')) as { listen?: unknown; sockPath?: unknown }
|
||||||
return typeof parsed.sockPath === 'string' ? parsed.sockPath : null
|
return typeof parsed.listen === 'string' ? parsed.listen : typeof parsed.sockPath === 'string' ? parsed.sockPath : null
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ console.log('=== CLI IPC Target Helpers ===\n')
|
|||||||
mkdirSync(paseoHome, { recursive: true })
|
mkdirSync(paseoHome, { recursive: true })
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
path.join(paseoHome, 'paseo.pid'),
|
path.join(paseoHome, 'paseo.pid'),
|
||||||
JSON.stringify({ pid: process.pid, sockPath: '/tmp/paseo-from-pid.sock' })
|
JSON.stringify({ pid: process.pid, listen: '/tmp/paseo-from-pid.sock' })
|
||||||
)
|
)
|
||||||
assert.deepStrictEqual(resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome }), [
|
assert.deepStrictEqual(resolveDefaultDaemonHosts({ PASEO_HOME: paseoHome }), [
|
||||||
'unix:///tmp/paseo-from-pid.sock',
|
'unix:///tmp/paseo-from-pid.sock',
|
||||||
@@ -93,7 +93,7 @@ console.log('=== CLI IPC Target Helpers ===\n')
|
|||||||
mkdirSync(paseoHome, { recursive: true })
|
mkdirSync(paseoHome, { recursive: true })
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
path.join(paseoHome, 'paseo.pid'),
|
path.join(paseoHome, 'paseo.pid'),
|
||||||
JSON.stringify({ pid: process.pid, sockPath: '/tmp/paseo-priority.sock' })
|
JSON.stringify({ pid: process.pid, listen: '/tmp/paseo-priority.sock' })
|
||||||
)
|
)
|
||||||
assert.deepStrictEqual(
|
assert.deepStrictEqual(
|
||||||
resolveDefaultDaemonHosts({
|
resolveDefaultDaemonHosts({
|
||||||
|
|||||||
@@ -536,8 +536,9 @@ pub fn run() {
|
|||||||
garbage_collect_attachment_files
|
garbage_collect_attachment_files
|
||||||
])
|
])
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
let setup_start = std::time::Instant::now();
|
||||||
log::info!(
|
log::info!(
|
||||||
"[app] Paseo Desktop v{} starting",
|
"[app] Paseo Desktop v{} setup starting",
|
||||||
app.package_info().version
|
app.package_info().version
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -645,6 +646,7 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.set_menu(menu)?;
|
app.set_menu(menu)?;
|
||||||
|
log::info!("[app] setup complete ({}ms)", setup_start.elapsed().as_millis());
|
||||||
|
|
||||||
let window = app.get_webview_window("main").unwrap();
|
let window = app.get_webview_window("main").unwrap();
|
||||||
let window_clone = window.clone();
|
let window_clone = window.clone();
|
||||||
|
|||||||
@@ -574,8 +574,10 @@ fn managed_daemon_status_internal(app: &AppHandle) -> Result<ManagedDaemonStatus
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
|
||||||
|
let t0 = std::time::Instant::now();
|
||||||
log::info!("[daemon] starting managed daemon");
|
log::info!("[daemon] starting managed daemon");
|
||||||
let status = ensure_runtime_ready_internal(app)?;
|
let status = ensure_runtime_ready_internal(app)?;
|
||||||
|
log::info!("[daemon] runtime ready ({}ms)", t0.elapsed().as_millis());
|
||||||
let runtime_root = PathBuf::from(&status.runtime_root);
|
let runtime_root = PathBuf::from(&status.runtime_root);
|
||||||
let manifest = load_runtime_manifest(&runtime_root)?;
|
let manifest = load_runtime_manifest(&runtime_root)?;
|
||||||
let output = cli_command(&runtime_root, &manifest, &["start"])?
|
let output = cli_command(&runtime_root, &manifest, &["start"])?
|
||||||
@@ -599,20 +601,21 @@ fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus,
|
|||||||
stderr
|
stderr
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
log::info!("[daemon] start command succeeded, waiting for daemon to be ready");
|
log::info!("[daemon] start command succeeded ({}ms), waiting for daemon to be ready", t0.elapsed().as_millis());
|
||||||
for attempt in 0..150 {
|
for attempt in 0..150 {
|
||||||
let daemon_status = managed_daemon_status_internal(app)?;
|
let daemon_status = managed_daemon_status_internal(app)?;
|
||||||
if daemon_status.status == "running" && !daemon_status.server_id.trim().is_empty() {
|
if daemon_status.status == "running" && !daemon_status.server_id.trim().is_empty() {
|
||||||
log::info!(
|
log::info!(
|
||||||
"[daemon] ready after {} attempts (pid={:?})",
|
"[daemon] ready after {} attempts, {}ms (pid={:?})",
|
||||||
attempt + 1,
|
attempt + 1,
|
||||||
|
t0.elapsed().as_millis(),
|
||||||
daemon_status.pid
|
daemon_status.pid
|
||||||
);
|
);
|
||||||
return Ok(daemon_status);
|
return Ok(daemon_status);
|
||||||
}
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||||
}
|
}
|
||||||
log::warn!("[daemon] timed out waiting for daemon to become ready");
|
log::warn!("[daemon] timed out waiting for daemon to become ready ({}ms)", t0.elapsed().as_millis());
|
||||||
managed_daemon_status_internal(app)
|
managed_daemon_status_internal(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ export async function createPaseoDaemon(
|
|||||||
rootLogger: Logger
|
rootLogger: Logger
|
||||||
): Promise<PaseoDaemon> {
|
): Promise<PaseoDaemon> {
|
||||||
const logger = rootLogger.child({ module: "bootstrap" });
|
const logger = rootLogger.child({ module: "bootstrap" });
|
||||||
|
const bootstrapStart = performance.now();
|
||||||
|
const elapsed = () => `${(performance.now() - bootstrapStart).toFixed(0)}ms`;
|
||||||
const daemonVersion = resolveDaemonVersion(import.meta.url);
|
const daemonVersion = resolveDaemonVersion(import.meta.url);
|
||||||
const pidLockMode = config.pidLock?.mode ?? "self";
|
const pidLockMode = config.pidLock?.mode ?? "self";
|
||||||
const pidLockOwnerPid = config.pidLock?.ownerPid;
|
const pidLockOwnerPid = config.pidLock?.ownerPid;
|
||||||
@@ -177,6 +179,7 @@ export async function createPaseoDaemon(
|
|||||||
await acquirePidLock(config.paseoHome, config.listen, {
|
await acquirePidLock(config.paseoHome, config.listen, {
|
||||||
ownerPid: pidLockOwnerPid,
|
ownerPid: pidLockOwnerPid,
|
||||||
});
|
});
|
||||||
|
logger.info({ elapsed: elapsed() }, "PID lock acquired");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -328,6 +331,7 @@ export async function createPaseoDaemon(
|
|||||||
agentStorage
|
agentStorage
|
||||||
);
|
);
|
||||||
await agentStorage.initialize();
|
await agentStorage.initialize();
|
||||||
|
logger.info({ elapsed: elapsed() }, "Agent storage initialized");
|
||||||
await bootstrapWorkspaceRegistries({
|
await bootstrapWorkspaceRegistries({
|
||||||
paseoHome: config.paseoHome,
|
paseoHome: config.paseoHome,
|
||||||
agentStorage,
|
agentStorage,
|
||||||
@@ -335,8 +339,10 @@ export async function createPaseoDaemon(
|
|||||||
workspaceRegistry,
|
workspaceRegistry,
|
||||||
logger,
|
logger,
|
||||||
});
|
});
|
||||||
|
logger.info({ elapsed: elapsed() }, "Workspace registries bootstrapped");
|
||||||
const persistedRecords = await agentStorage.list();
|
const persistedRecords = await agentStorage.list();
|
||||||
logger.info(
|
logger.info(
|
||||||
|
{ elapsed: elapsed() },
|
||||||
`Agent registry loaded (${persistedRecords.length} record${persistedRecords.length === 1 ? "" : "s"}); agents will initialize on demand`
|
`Agent registry loaded (${persistedRecords.length} record${persistedRecords.length === 1 ? "" : "s"}); agents will initialize on demand`
|
||||||
);
|
);
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -518,6 +524,7 @@ export async function createPaseoDaemon(
|
|||||||
openaiConfig: config.openai,
|
openaiConfig: config.openai,
|
||||||
speechConfig: config.speech,
|
speechConfig: config.speech,
|
||||||
});
|
});
|
||||||
|
logger.info({ elapsed: elapsed() }, "Speech runtime initialized");
|
||||||
|
|
||||||
wsServer = new VoiceAssistantWebSocketServer(
|
wsServer = new VoiceAssistantWebSocketServer(
|
||||||
httpServer,
|
httpServer,
|
||||||
@@ -567,6 +574,8 @@ export async function createPaseoDaemon(
|
|||||||
wsServer?.publishSpeechReadiness(snapshot);
|
wsServer?.publishSpeechReadiness(snapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
logger.info({ elapsed: elapsed() }, "Bootstrap complete, ready to start listening");
|
||||||
|
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
// Start main HTTP server
|
// Start main HTTP server
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -584,11 +593,11 @@ export async function createPaseoDaemon(
|
|||||||
|
|
||||||
if (listenTarget.type === "tcp") {
|
if (listenTarget.type === "tcp") {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ host: listenTarget.host, port: listenTarget.port },
|
{ host: listenTarget.host, port: listenTarget.port, elapsed: elapsed() },
|
||||||
`Server listening on http://${listenTarget.host}:${listenTarget.port}`
|
`Server listening on http://${listenTarget.host}:${listenTarget.port}`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.info({ path: listenTarget.path }, `Server listening on ${listenTarget.path}`);
|
logger.info({ path: listenTarget.path, elapsed: elapsed() }, `Server listening on ${listenTarget.path}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relayEnabled) {
|
if (relayEnabled) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface PidLockInfo {
|
|||||||
startedAt: string;
|
startedAt: string;
|
||||||
hostname: string;
|
hostname: string;
|
||||||
uid: number;
|
uid: number;
|
||||||
sockPath: string;
|
listen: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PidLockError extends Error {
|
export class PidLockError extends Error {
|
||||||
@@ -43,7 +43,7 @@ function resolveOwnerPid(ownerPid?: number): number {
|
|||||||
|
|
||||||
export async function acquirePidLock(
|
export async function acquirePidLock(
|
||||||
paseoHome: string,
|
paseoHome: string,
|
||||||
sockPath: string,
|
listen: string,
|
||||||
options?: { ownerPid?: number }
|
options?: { ownerPid?: number }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const pidPath = getPidFilePath(paseoHome);
|
const pidPath = getPidFilePath(paseoHome);
|
||||||
@@ -85,7 +85,7 @@ export async function acquirePidLock(
|
|||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
hostname: hostname(),
|
hostname: hostname(),
|
||||||
uid: process.getuid?.() ?? 0,
|
uid: process.getuid?.() ?? 0,
|
||||||
sockPath,
|
listen,
|
||||||
};
|
};
|
||||||
|
|
||||||
let fd;
|
let fd;
|
||||||
|
|||||||
Reference in New Issue
Block a user