From 2721ce331a21392a4dc0f36545fcce546619d0e1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 8 Mar 2026 20:48:13 +0700 Subject: [PATCH] Simplify managed runtime to execute in place from app bundle --- RUNTIME_SIMPLIFICATION_PLAN.md | 81 ++++ .../contexts/daemon-registry-context.test.ts | 81 ++++ .../src/contexts/daemon-registry-context.tsx | 30 +- .../components/desktop-updates-section.tsx | 189 ++++++-- .../managed-runtime/managed-runtime.ts | 12 +- packages/app/src/hooks/use-settings.test.ts | 53 +++ packages/app/src/hooks/use-settings.ts | 11 +- .../desktop/scripts/managed-daemon-smoke.mjs | 213 ++++----- packages/desktop/src-tauri/src/lib.rs | 12 +- .../desktop/src-tauri/src/runtime_manager.rs | 406 +++++------------- 10 files changed, 613 insertions(+), 475 deletions(-) create mode 100644 RUNTIME_SIMPLIFICATION_PLAN.md create mode 100644 packages/app/src/hooks/use-settings.test.ts diff --git a/RUNTIME_SIMPLIFICATION_PLAN.md b/RUNTIME_SIMPLIFICATION_PLAN.md new file mode 100644 index 000000000..2d0e591c6 --- /dev/null +++ b/RUNTIME_SIMPLIFICATION_PLAN.md @@ -0,0 +1,81 @@ +# Runtime Simplification Plan + +## Goal + +Remove the managed runtime install/copy layer. The desktop app must run the bundled runtime in place from the application resources/install directory on macOS, Windows, and Linux. + +This is a simplification task. If a change preserves the old install-manager shape under a new name, it fails the goal. + +## Hard Requirements + +1. The bundled runtime is read-only. + The desktop app must not copy the bundled runtime to app data, temp, cache, or any other writable directory. + +2. The app must execute Node/CLI/server entrypoints directly from the bundled runtime root. + On macOS this means inside `Paseo.app/Contents/Resources/...`. + On Windows and Linux this means inside the installed app resources directory. + +3. There is exactly one runtime per installed app. + Remove versioned installed runtime directories like `runtime/` from the runtime execution path. + +4. The CLI shim may point into the installed app bundle/directory. + Do not keep an extra installed runtime tree just to preserve a stable shim target across updates. + +5. All mutable state remains outside the bundled runtime. + Logs, sockets/pipes, PID files, daemon state, `PASEO_HOME`, and any other writable files must continue to live in managed home / app data locations. + +6. Runtime discovery must stay cross-platform. + The implementation must resolve the bundled runtime/resources path on macOS, Windows, and Linux using the app install/resources directory, not hardcoded `.app` assumptions. + +7. Remove dead machinery, do not leave adapters behind. + If install/copy/versioned-runtime code becomes unused, delete it instead of keeping fallback paths "just in case". + +## Non-Goals + +1. Do not change how the runtime is built into the desktop app bundle in this task. + This task is about runtime execution and path management, not bundling format. + +2. Do not reintroduce a second runtime location for migration compatibility. + It is acceptable if older installed clients do not migrate cleanly. + +3. Do not add feature flags, env-guarded fallback paths, or compatibility shims unless absolutely required by a real platform constraint proven in code. + +## Concrete Implementation Direction + +1. Treat `bundled_runtime_root(app)` plus `current-runtime.json` as the runtime source of truth. + +2. Replace `paths.runtime_root` usage for runtime execution with the bundled runtime root selected by `current-runtime.json`. + +3. Delete `install_runtime_if_needed(...)` and related copy/install staging logic if nothing else still needs it. + +4. Rework CLI shim generation so the inner launcher points at the bundled runtime's Node + CLI entrypoint directly, while still keeping mutable state (`PASEO_HOME`) outside the runtime. + +5. Simplify `ManagedPaths` and related structs if `runtime_root` and `stable_runtime_root` no longer need separate installed-runtime semantics. + +6. Keep diagnostics/status reporting accurate. + If the app reports bundled vs installed runtime roots today, update that output to reflect the new single-runtime model. + +## Acceptance Criteria + +1. No code path copies the bundled runtime tree into app data before launching the daemon or CLI. + +2. No code path depends on a versioned installed runtime directory for execution. + +3. The CLI shim and daemon launch path both resolve to bundled runtime executables/resources. + +4. Typecheck passes. + +5. Relevant desktop/runtime tests pass, updated to reflect the new direct-from-bundle model. + +6. The resulting implementation is materially simpler: + fewer runtime path concepts, fewer staging/install branches, fewer indirections. + +## Review Bar + +The implementation should be rejected if: + +- it still copies the runtime anywhere before execution +- it keeps runtime version directories in the execution path +- it preserves the stable installed runtime launcher concept without a hard platform reason +- it adds migration complexity for old installs +- it introduces new fallback branches instead of removing obsolete ones diff --git a/packages/app/src/contexts/daemon-registry-context.test.ts b/packages/app/src/contexts/daemon-registry-context.test.ts index d44a9b6cd..13e2ca891 100644 --- a/packages/app/src/contexts/daemon-registry-context.test.ts +++ b/packages/app/src/contexts/daemon-registry-context.test.ts @@ -3,6 +3,7 @@ import { hostHasDirectEndpoint, registryHasDirectEndpoint, reconcileDesktopStartupRegistry, + resolveManagedDesktopStartupStatus, type HostProfile, } from './daemon-registry-context' @@ -240,3 +241,83 @@ describe('reconcileDesktopStartupRegistry', () => { expect(second).toEqual(first) }) }) + +describe('resolveManagedDesktopStartupStatus', () => { + it('starts the managed daemon when management is enabled', async () => { + const managedStatus = { + runtimeId: 'runtime_1', + runtimeVersion: '1.2.3', + runtimeRoot: '/runtime', + managedHome: '/home', + transportType: 'socket', + transportPath: '/tmp/paseo.sock', + daemonPid: 123, + daemonRunning: true, + daemonStatus: 'running', + logPath: '/tmp/daemon.log', + serverId: 'srv_managed', + hostname: 'managed-host', + relayEnabled: true, + tcpEnabled: false, + tcpListen: null, + cliShimPath: null, + } + let startCalls = 0 + let statusCalls = 0 + + const result = await resolveManagedDesktopStartupStatus({ + settings: { manageBuiltInDaemon: true }, + startManagedDaemonFn: async () => { + startCalls += 1 + return managedStatus + }, + getManagedDaemonStatusFn: async () => { + statusCalls += 1 + return managedStatus + }, + }) + + expect(result).toEqual(managedStatus) + expect(startCalls).toBe(1) + expect(statusCalls).toBe(0) + }) + + it('only reads managed daemon status when management is paused', async () => { + const managedStatus = { + runtimeId: 'runtime_1', + runtimeVersion: '1.2.3', + runtimeRoot: '/runtime', + managedHome: '/home', + transportType: 'socket', + transportPath: '/tmp/paseo.sock', + daemonPid: null, + daemonRunning: false, + daemonStatus: 'stopped', + logPath: '/tmp/daemon.log', + serverId: null, + hostname: null, + relayEnabled: true, + tcpEnabled: false, + tcpListen: null, + cliShimPath: null, + } + let startCalls = 0 + let statusCalls = 0 + + const result = await resolveManagedDesktopStartupStatus({ + settings: { manageBuiltInDaemon: false }, + startManagedDaemonFn: async () => { + startCalls += 1 + return managedStatus + }, + getManagedDaemonStatusFn: async () => { + statusCalls += 1 + return managedStatus + }, + }) + + expect(result).toEqual(managedStatus) + expect(startCalls).toBe(0) + expect(statusCalls).toBe(1) + }) +}) diff --git a/packages/app/src/contexts/daemon-registry-context.tsx b/packages/app/src/contexts/daemon-registry-context.tsx index b510c4c92..9649b89da 100644 --- a/packages/app/src/contexts/daemon-registry-context.tsx +++ b/packages/app/src/contexts/daemon-registry-context.tsx @@ -4,8 +4,10 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { useQuery, useQueryClient } from '@tanstack/react-query' import { decodeOfferFragmentPayload, normalizeHostPort } from '@/utils/daemon-endpoints' import { probeConnection } from '@/utils/test-daemon-connection' +import { useAppSettings, type AppSettings } from '@/hooks/use-settings' import { ConnectionOfferSchema, type ConnectionOffer } from '@server/shared/connection-offer' import { + getManagedDaemonStatus, type ManagedDaemonStatus, shouldUseManagedDesktopDaemon, startManagedDaemon, @@ -460,6 +462,18 @@ export function reconcileDesktopStartupRegistry( return next } +export async function resolveManagedDesktopStartupStatus(input: { + settings: Pick + startManagedDaemonFn?: () => Promise + getManagedDaemonStatusFn?: () => Promise +}): Promise { + if (input.settings.manageBuiltInDaemon) { + return await (input.startManagedDaemonFn ?? startManagedDaemon)() + } + + return await (input.getManagedDaemonStatusFn ?? getManagedDaemonStatus)() +} + async function probeManagedStartupTarget(input: { managedDaemon: ManagedDaemonStatus cancelled?: () => boolean @@ -551,6 +565,7 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) { const queryClient = useQueryClient() const desktopStartupReconciledRef = useRef(false) const localhostBootstrapAttemptedRef = useRef(false) + const { settings, isLoading: settingsLoading } = useAppSettings() const { data: daemons = [], isPending, @@ -667,6 +682,7 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) { useEffect(() => { if (isPending) return + if (settingsLoading) return if (!shouldUseManagedDesktopDaemon()) return if (desktopStartupReconciledRef.current) return desktopStartupReconciledRef.current = true @@ -682,11 +698,15 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) { let managed: ManagedHostReconciliationInput | null = null try { - const managedDaemon = await startManagedDaemon() - managed = await probeManagedConnectionUntilReady({ - managedDaemon, - cancelled: () => cancelled, + const managedDaemon = await resolveManagedDesktopStartupStatus({ + settings, }) + if (managedDaemon.daemonRunning) { + managed = await probeManagedConnectionUntilReady({ + managedDaemon, + cancelled: () => cancelled, + }) + } } catch (managedBootstrapError) { if (!cancelled) { console.warn( @@ -750,6 +770,8 @@ export function DaemonRegistryProvider({ children }: { children: ReactNode }) { isPending, persist, readDaemons, + settings.manageBuiltInDaemon, + settingsLoading, ]) useEffect(() => { diff --git a/packages/app/src/desktop/components/desktop-updates-section.tsx b/packages/app/src/desktop/components/desktop-updates-section.tsx index a2ca81a84..6515dc193 100644 --- a/packages/app/src/desktop/components/desktop-updates-section.tsx +++ b/packages/app/src/desktop/components/desktop-updates-section.tsx @@ -7,6 +7,7 @@ import { StyleSheet } from "react-native-unistyles"; import { ArrowUpRight } from "lucide-react-native"; import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet"; import { Button } from "@/components/ui/button"; +import { useAppSettings } from "@/hooks/use-settings"; import { confirmDialog } from "@/utils/confirm-dialog"; import { openExternalUrl } from "@/utils/open-external-url"; import { @@ -20,6 +21,8 @@ import { installManagedCliShim, restartManagedDaemon, shouldUseManagedDesktopDaemon, + startManagedDaemon, + stopManagedDaemon, uninstallManagedCliShim, type ManagedDaemonLogs, type ManagedPairingOffer, @@ -33,9 +36,11 @@ export interface LocalDaemonSectionProps { export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { const showSection = shouldUseManagedDesktopDaemon(); + const { settings, updateSettings } = useAppSettings(); const [managedStatus, setManagedStatus] = useState(null); const [statusError, setStatusError] = useState(null); const [isRestartingDaemon, setIsRestartingDaemon] = useState(false); + const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false); const [isInstallingCli, setIsInstallingCli] = useState(false); const [statusMessage, setStatusMessage] = useState(null); const [cliStatusMessage, setCliStatusMessage] = useState(null); @@ -78,8 +83,19 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { const localDaemonVersionText = formatVersionWithPrefix(managedStatus?.runtimeVersion ?? null); const daemonVersionMismatch = isVersionMismatch(appVersion, managedStatus?.runtimeVersion ?? null); - const daemonVersionHint = - statusError ?? (managedStatus?.daemonRunning ? "Running." : "Not running."); + const daemonStatusStateText = + statusError ?? + (managedStatus?.daemonRunning + ? managedStatus?.daemonStatus ?? "running" + : "not running"); + const daemonStatusDetailText = `PID ${managedStatus?.daemonPid ? managedStatus.daemonPid : "—"}`; + const isDaemonManagementPaused = !settings.manageBuiltInDaemon; + const daemonActionLabel = managedStatus?.daemonRunning ? "Restart daemon" : "Start daemon"; + const daemonActionMessage = managedStatus?.daemonRunning + ? "Restarts the built-in daemon." + : isDaemonManagementPaused + ? "Starts the built-in daemon manually. Paseo will not auto-start it while paused." + : "Starts the built-in daemon."; const handleUpdateLocalDaemon = useCallback(() => { if (!showSection) { @@ -90,10 +106,11 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { } void confirmDialog({ - title: "Restart daemon", - message: - "This will restart the built-in daemon. The app will reconnect automatically.", - confirmLabel: "Restart", + title: daemonActionLabel, + message: managedStatus?.daemonRunning + ? "This will restart the built-in daemon. The app will reconnect automatically." + : "This will start the built-in daemon.", + confirmLabel: daemonActionLabel, cancelLabel: "Cancel", }) .then((confirmed) => { @@ -104,26 +121,100 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { setIsRestartingDaemon(true); setStatusMessage(null); - void restartManagedDaemon() + const action = managedStatus?.daemonRunning ? restartManagedDaemon : startManagedDaemon; + + void action() .then((status) => { setManagedStatus(status); - setStatusMessage("Daemon restarted."); + setStatusMessage( + managedStatus?.daemonRunning ? "Daemon restarted." : "Daemon started." + ); return loadManagedStatus(); }) .catch((error) => { - console.error("[Settings] Failed to restart managed daemon", error); + console.error("[Settings] Failed to change managed daemon state", error); const message = error instanceof Error ? error.message : String(error); - setStatusMessage(`Restart failed: ${message}`); + setStatusMessage(`${daemonActionLabel} failed: ${message}`); }) .finally(() => { setIsRestartingDaemon(false); }); }) .catch((error) => { - console.error("[Settings] Failed to open managed daemon restart confirmation", error); - Alert.alert("Error", "Unable to open the restart confirmation dialog."); + console.error("[Settings] Failed to open managed daemon action confirmation", error); + Alert.alert("Error", "Unable to open the daemon confirmation dialog."); }); - }, [isRestartingDaemon, loadManagedStatus, showSection]); + }, [daemonActionLabel, isRestartingDaemon, loadManagedStatus, managedStatus?.daemonRunning, showSection]); + + const handleToggleDaemonManagement = useCallback(() => { + if (isUpdatingDaemonManagement) { + return; + } + + if (!settings.manageBuiltInDaemon) { + setIsUpdatingDaemonManagement(true); + setStatusMessage(null); + void updateSettings({ manageBuiltInDaemon: true }) + .then(() => { + setStatusMessage("Paseo will resume managing the built-in daemon on startup."); + }) + .catch((error) => { + console.error("[Settings] Failed to update built-in daemon management", error); + Alert.alert("Error", "Unable to update built-in daemon management."); + }) + .finally(() => { + setIsUpdatingDaemonManagement(false); + }); + return; + } + + void confirmDialog({ + title: "Pause built-in daemon", + message: + "This will stop the built-in daemon immediately and prevent Paseo from auto-starting it on launch. Running agents and terminals connected to the built-in daemon will be stopped.", + confirmLabel: "Pause and stop", + cancelLabel: "Cancel", + destructive: true, + }) + .then((confirmed) => { + if (!confirmed) { + return; + } + + setIsUpdatingDaemonManagement(true); + setStatusMessage(null); + + const stopPromise = managedStatus?.daemonRunning + ? stopManagedDaemon() + : Promise.resolve(managedStatus ?? null); + + void stopPromise + .then(() => updateSettings({ manageBuiltInDaemon: false })) + .then(() => loadManagedStatus()) + .then(() => { + setStatusMessage( + "Paseo paused the built-in daemon and will no longer auto-start it on launch." + ); + }) + .catch((error) => { + console.error("[Settings] Failed to pause built-in daemon management", error); + Alert.alert("Error", "Unable to pause built-in daemon management."); + }) + .finally(() => { + setIsUpdatingDaemonManagement(false); + }); + }) + .catch((error) => { + console.error("[Settings] Failed to open built-in daemon pause confirmation", error); + Alert.alert("Error", "Unable to open the daemon confirmation dialog."); + }); + }, [ + isUpdatingDaemonManagement, + loadManagedStatus, + managedStatus, + settings.manageBuiltInDaemon, + updateSettings, + ]); const handleToggleCliShim = useCallback(() => { if (!showSection || isInstallingCli) { @@ -255,26 +346,59 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { - Version - {daemonVersionHint} + Status + Only the built-in managed daemon is shown here. + + + {daemonStatusStateText} + {daemonStatusDetailText} - {localDaemonVersionText} - Restart daemon - Restarts the built-in daemon. + Daemon management + + {isDaemonManagementPaused + ? "Paused. Paseo will not auto-start the built-in daemon on app launch." + : "Enabled. Paseo will start the built-in daemon automatically when needed."} + + + + + + + {daemonActionLabel} + {daemonActionMessage} {statusMessage ? ( {statusMessage} ) : null} @@ -286,8 +410,9 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) { {cliStatusMessage ? {cliStatusMessage} : null} ) : null} @@ -560,6 +685,18 @@ const styles = StyleSheet.create((theme) => ({ actionGroup: { flexDirection: "row", gap: theme.spacing[2], + flexWrap: "wrap", + justifyContent: "flex-end", + }, + statusValueGroup: { + alignItems: "flex-end", + gap: 2, + }, + primaryActionButton: { + minWidth: 124, + }, + secondaryActionButton: { + minWidth: 112, }, rowTitle: { color: theme.colors.foreground, @@ -570,6 +707,10 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.normal, }, + valueSubtext: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + }, hintText: { color: theme.colors.foregroundMuted, fontSize: theme.fontSize.xs, diff --git a/packages/app/src/desktop/managed-runtime/managed-runtime.ts b/packages/app/src/desktop/managed-runtime/managed-runtime.ts index ef57185b7..ef402d2c0 100644 --- a/packages/app/src/desktop/managed-runtime/managed-runtime.ts +++ b/packages/app/src/desktop/managed-runtime/managed-runtime.ts @@ -4,9 +4,7 @@ import { getTauri, isTauriEnvironment } from "@/utils/tauri"; export type ManagedRuntimeStatus = { runtimeId: string; runtimeVersion: string; - bundledRuntimeRoot: string; - installedRuntimeRoot: string; - installed: boolean; + runtimeRoot: string; managedHome: string; transportType: string; transportPath: string; @@ -103,9 +101,7 @@ function parseManagedRuntimeStatus(raw: unknown): ManagedRuntimeStatus { return { runtimeId: toStringOrNull(raw.runtimeId) ?? "", runtimeVersion: toStringOrNull(raw.runtimeVersion) ?? "", - bundledRuntimeRoot: toStringOrNull(raw.bundledRuntimeRoot) ?? "", - installedRuntimeRoot: toStringOrNull(raw.installedRuntimeRoot) ?? "", - installed: raw.installed === true, + runtimeRoot: toStringOrNull(raw.runtimeRoot) ?? "", managedHome: toStringOrNull(raw.managedHome) ?? "", transportType: toStringOrNull(raw.transportType) ?? "socket", transportPath: toStringOrNull(raw.transportPath) ?? "", @@ -189,10 +185,6 @@ export function shouldUseManagedDesktopDaemon(): boolean { return isTauriEnvironment() && getTauri() !== null; } -export async function ensureManagedRuntime(): Promise { - return parseManagedRuntimeStatus(await invokeDesktopCommand("ensure_managed_runtime")); -} - export async function getManagedRuntimeStatus(): Promise { return parseManagedRuntimeStatus(await invokeDesktopCommand("managed_runtime_status")); } diff --git a/packages/app/src/hooks/use-settings.test.ts b/packages/app/src/hooks/use-settings.test.ts new file mode 100644 index 000000000..18370ae19 --- /dev/null +++ b/packages/app/src/hooks/use-settings.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const asyncStorageMock = vi.hoisted(() => ({ + getItem: vi.fn<(_: string) => Promise>(), + setItem: vi.fn<(_: string, __: string) => Promise>(), +})); + +vi.mock("@react-native-async-storage/async-storage", () => ({ + default: asyncStorageMock, +})); + +describe("use-settings", () => { + beforeEach(() => { + vi.resetModules(); + asyncStorageMock.getItem.mockReset(); + asyncStorageMock.setItem.mockReset(); + }); + + it("defaults built-in daemon management to enabled when storage is empty", async () => { + asyncStorageMock.getItem.mockResolvedValue(null); + asyncStorageMock.setItem.mockResolvedValue(); + + const mod = await import("./use-settings"); + const result = await mod.loadSettingsFromStorage(); + + expect(result).toEqual(mod.DEFAULT_APP_SETTINGS); + expect(asyncStorageMock.setItem).toHaveBeenCalledWith( + mod.APP_SETTINGS_KEY, + JSON.stringify(mod.DEFAULT_APP_SETTINGS) + ); + }); + + it("loads persisted built-in daemon management state", async () => { + asyncStorageMock.getItem.mockImplementation(async (key: string) => { + if (key === "@paseo:app-settings") { + return JSON.stringify({ + theme: "light", + manageBuiltInDaemon: false, + }); + } + return null; + }); + + const mod = await import("./use-settings"); + const result = await mod.loadSettingsFromStorage(); + + expect(result).toEqual({ + theme: "light", + manageBuiltInDaemon: false, + }); + expect(asyncStorageMock.setItem).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/app/src/hooks/use-settings.ts b/packages/app/src/hooks/use-settings.ts index bbe28fe4d..6bb837c99 100644 --- a/packages/app/src/hooks/use-settings.ts +++ b/packages/app/src/hooks/use-settings.ts @@ -2,16 +2,18 @@ import { useCallback } from "react"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -const APP_SETTINGS_KEY = "@paseo:app-settings"; +export const APP_SETTINGS_KEY = "@paseo:app-settings"; const LEGACY_SETTINGS_KEY = "@paseo:settings"; const APP_SETTINGS_QUERY_KEY = ["app-settings"]; export interface AppSettings { theme: "dark" | "light" | "auto"; + manageBuiltInDaemon: boolean; } -const DEFAULT_APP_SETTINGS: AppSettings = { +export const DEFAULT_APP_SETTINGS: AppSettings = { theme: "dark", + manageBuiltInDaemon: true, }; export interface UseAppSettingsReturn { @@ -66,7 +68,7 @@ export function useAppSettings(): UseAppSettingsReturn { }; } -async function loadSettingsFromStorage(): Promise { +export async function loadSettingsFromStorage(): Promise { try { const stored = await AsyncStorage.getItem(APP_SETTINGS_KEY); if (stored) { @@ -98,6 +100,9 @@ function pickAppSettingsFromLegacy(legacy: Record): Partial Result { json!(managed_runtime_status(app_handle.clone()).await?) } ManagedHeadlessCommand::Bootstrap => { - ensure_managed_runtime(app_handle.clone()).await?; json!(start_managed_daemon(app_handle.clone()).await?) } ManagedHeadlessCommand::DaemonStatus => { @@ -603,7 +602,6 @@ pub fn run() { .plugin(tauri_plugin_websocket::init()) .invoke_handler(tauri::generate_handler![ managed_runtime_status, - ensure_managed_runtime, managed_daemon_status, start_managed_daemon, stop_managed_daemon, diff --git a/packages/desktop/src-tauri/src/runtime_manager.rs b/packages/desktop/src-tauri/src/runtime_manager.rs index d0f947a39..e5e28ae44 100644 --- a/packages/desktop/src-tauri/src/runtime_manager.rs +++ b/packages/desktop/src-tauri/src/runtime_manager.rs @@ -6,11 +6,9 @@ use std::collections::HashMap; use std::env; use std::fs; use std::io; -use std::iter; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::OnceLock; use std::sync::{Arc, Mutex}; use tauri::{AppHandle, Emitter, Manager, State}; use tokio::sync::mpsc; @@ -33,13 +31,6 @@ const DEFAULT_MANAGED_TCP_PORT: u16 = 7771; const CLI_SHIM_NAME: &str = "paseo"; #[cfg(windows)] const CLI_SHIM_WINDOWS_NAME: &str = "paseo.cmd"; -const CLI_INNER_DIR: &str = "bin"; -const CLI_INNER_UNIX_NAME: &str = "paseo-managed"; -#[cfg(windows)] -const CLI_INNER_WINDOWS_NAME: &str = "paseo-managed.cmd"; - -static RUNTIME_INSTALL_LOCK: OnceLock> = OnceLock::new(); - #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ManagedRuntimeManifest { @@ -84,8 +75,6 @@ struct PidFile { #[derive(Debug, Clone)] struct ManagedPaths { - runtime_root: PathBuf, - stable_runtime_root: PathBuf, managed_home: PathBuf, transport_path: PathBuf, logs_path: PathBuf, @@ -112,9 +101,7 @@ pub struct ManagedTcpSettings { pub struct ManagedRuntimeStatus { pub runtime_id: String, pub runtime_version: String, - pub bundled_runtime_root: String, - pub installed_runtime_root: String, - pub installed: bool, + pub runtime_root: String, pub managed_home: String, pub transport_type: String, pub transport_path: String, @@ -353,43 +340,39 @@ mod tests { } #[test] - fn outer_cli_shim_contents_only_forwards_to_inner_launcher() { - let contents = outer_cli_shim_contents(Path::new( - "/Applications/Paseo.app/Contents/MacOS/paseo-managed", - )); - assert!(contents - .contains("exec \"/Applications/Paseo.app/Contents/MacOS/paseo-managed\" \"$@\"")); + fn cli_shim_contents_runs_bundled_runtime_in_place() { + let runtime_root = + PathBuf::from("/Applications/Paseo.app/Contents/Resources/managed-runtime/runtime-1"); + let paths = ManagedPaths { + managed_home: PathBuf::from("/Users/me/.paseo"), + transport_path: PathBuf::from("/Users/me/.paseo/paseo.sock"), + logs_path: PathBuf::from("/Users/me/.paseo/daemon.log"), + state_file_path: PathBuf::from( + "/Users/me/Library/Application Support/Paseo/managed-state.json", + ), + diagnostics_root: PathBuf::from("/Users/me/Library/Application Support/Paseo"), + }; + let manifest = ManagedRuntimeManifest { + runtime_id: "runtime-1".to_string(), + runtime_version: "0.1.0".to_string(), + platform: "darwin".to_string(), + arch: "arm64".to_string(), + created_at: "2025-01-01T00:00:00.000Z".to_string(), + node_relative_path: "node/node".to_string(), + cli_entrypoint_relative_path: "node_modules/@getpaseo/cli/dist/index.js".to_string(), + cli_shim_relative_path: "node_modules/@getpaseo/cli/bin/paseo".to_string(), + server_runner_relative_path: + "node_modules/@getpaseo/server/dist/scripts/daemon-runner.js".to_string(), + }; + + let contents = cli_shim_contents(&runtime_root, &manifest, &paths); + + assert!(contents.contains("export PASEO_HOME='/Users/me/.paseo'")); + assert!(contents.contains("Contents/Resources/managed-runtime/runtime-1/node/node")); + assert!(contents.contains("node_modules/@getpaseo/cli/dist/index.js")); assert!(!contents.contains("--paseo-cli-shim")); } - #[test] - fn inner_cli_launcher_path_lives_inside_managed_runtime_tree() { - let runtime_root = PathBuf::from("/tmp/paseo-desktop/runtime"); - let inner_launcher = managed_cli_inner_launcher_path(&runtime_root); - - assert_eq!( - inner_launcher, - PathBuf::from("/tmp/paseo-desktop/runtime/bin/paseo-managed") - ); - } - - #[test] - fn outer_shim_target_remains_stable_across_runtime_updates() { - let runtime_root = PathBuf::from("/tmp/paseo-desktop/runtime"); - let first_runtime_root = PathBuf::from("/tmp/paseo-desktop/runtime/runtime-a"); - let second_runtime_root = PathBuf::from("/tmp/paseo-desktop/runtime/runtime-b"); - let first_target = managed_cli_inner_launcher_path(&runtime_root); - let second_target = managed_cli_inner_launcher_path(&runtime_root); - let first_contents = outer_cli_shim_contents(&first_target); - let second_contents = outer_cli_shim_contents(&second_target); - - assert_ne!(first_runtime_root, second_runtime_root); - assert_eq!(first_target, second_target); - assert_eq!(first_contents, second_contents); - assert!(!first_contents.contains(first_runtime_root.to_string_lossy().as_ref())); - assert!(!first_contents.contains(second_runtime_root.to_string_lossy().as_ref())); - } - #[cfg(unix)] #[test] #[ignore = "requires a running local daemon socket"] @@ -426,89 +409,6 @@ mod tests { } } -fn copy_dir_recursive(source: &Path, target: &Path) -> Result<(), String> { - if !source.exists() { - return Err(format!( - "Managed runtime resource is missing at {}", - source.display() - )); - } - fs::create_dir_all(target) - .map_err(|error| format!("Failed to create {}: {error}", target.display()))?; - for entry in fs::read_dir(source) - .map_err(|error| format!("Failed to read {}: {error}", source.display()))? - { - let entry = entry.map_err(|error| format!("Failed to read directory entry: {error}"))?; - let source_path = entry.path(); - let target_path = target.join(entry.file_name()); - let file_type = entry - .file_type() - .map_err(|error| format!("Failed to inspect {}: {error}", source_path.display()))?; - if file_type.is_symlink() { - let resolved = fs::canonicalize(&source_path).map_err(|error| { - format!( - "Failed to resolve symlink {}: {error}", - source_path.display() - ) - })?; - let resolved_type = fs::metadata(&resolved).map_err(|error| { - format!( - "Failed to inspect resolved symlink {}: {error}", - resolved.display() - ) - })?; - if resolved_type.is_dir() { - copy_dir_recursive(&resolved, &target_path)?; - continue; - } - if let Some(parent) = target_path.parent() { - fs::create_dir_all(parent) - .map_err(|error| format!("Failed to create {}: {error}", parent.display()))?; - } - fs::copy(&resolved, &target_path).map_err(|error| { - format!( - "Failed to copy {} to {}: {error}", - resolved.display(), - target_path.display() - ) - })?; - continue; - } - if file_type.is_dir() { - copy_dir_recursive(&source_path, &target_path)?; - continue; - } - if let Some(parent) = target_path.parent() { - fs::create_dir_all(parent) - .map_err(|error| format!("Failed to create {}: {error}", parent.display()))?; - } - fs::copy(&source_path, &target_path).map_err(|error| { - format!( - "Failed to copy {} to {}: {error}", - source_path.display(), - target_path.display() - ) - })?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let permissions = fs::metadata(&source_path) - .map_err(|error| format!("Failed to read {}: {error}", source_path.display()))? - .permissions() - .mode(); - fs::set_permissions(&target_path, fs::Permissions::from_mode(permissions)).map_err( - |error| { - format!( - "Failed to preserve mode on {}: {error}", - target_path.display() - ) - }, - )?; - } - } - Ok(()) -} - fn read_json_file Deserialize<'de>>(path: &Path) -> Result { let raw = fs::read_to_string(path) .map_err(|error| format!("Failed to read {}: {error}", path.display()))?; @@ -616,17 +516,13 @@ fn default_transport_path(managed_home: &Path, diagnostics_root: &Path) -> PathB } } -fn resolve_paths(app: &AppHandle, runtime_id: &str) -> Result { +fn resolve_paths(app: &AppHandle) -> Result { if let Some(test_root) = resolve_test_root() { - let runtime_root = resolve_override_path("PASEO_DESKTOP_MANAGED_RUNTIME_ROOT") - .unwrap_or_else(|| test_root.join("runtime").join(runtime_id)); let managed_home = resolve_override_path("PASEO_DESKTOP_MANAGED_HOME") .unwrap_or_else(|| test_root.join(DEFAULT_MANAGED_HOME_DIRNAME)); let transport_path = resolve_override_path("PASEO_DESKTOP_MANAGED_SOCKET_PATH") .unwrap_or_else(|| default_transport_path(&managed_home, &test_root)); return Ok(ManagedPaths { - runtime_root, - stable_runtime_root: test_root.join("runtime"), managed_home: managed_home.clone(), transport_path, logs_path: managed_home.join("daemon.log"), @@ -636,8 +532,6 @@ fn resolve_paths(app: &AppHandle, runtime_id: &str) -> Result Result String { value.replace('`', "``").replace('"', "`\"") } -fn managed_cli_inner_launcher_path(launcher_root: &Path) -> PathBuf { - #[cfg(windows)] - { - return launcher_root - .join(CLI_INNER_DIR) - .join(CLI_INNER_WINDOWS_NAME); - } - #[cfg(not(windows))] - { - launcher_root.join(CLI_INNER_DIR).join(CLI_INNER_UNIX_NAME) - } -} - fn outer_cli_shim_path() -> Result { #[cfg(target_os = "macos")] { @@ -889,11 +768,13 @@ fn outer_cli_shim_path() -> Result { } } -fn inner_cli_launcher_contents(paths: &ManagedPaths, manifest: &ManagedRuntimeManifest) -> String { - let node = paths.runtime_root.join(&manifest.node_relative_path); - let cli = paths - .runtime_root - .join(&manifest.cli_entrypoint_relative_path); +fn cli_shim_contents( + runtime_root: &Path, + manifest: &ManagedRuntimeManifest, + paths: &ManagedPaths, +) -> String { + let node = runtime_root.join(&manifest.node_relative_path); + let cli = runtime_root.join(&manifest.cli_entrypoint_relative_path); #[cfg(windows)] { return format!( @@ -914,17 +795,6 @@ fn inner_cli_launcher_contents(paths: &ManagedPaths, manifest: &ManagedRuntimeMa } } -fn outer_cli_shim_contents(inner_launcher: &Path) -> String { - #[cfg(windows)] - { - return format!("@echo off\r\n\"{}\" %*\r\n", inner_launcher.display()); - } - #[cfg(not(windows))] - { - format!("#!/bin/sh\nexec \"{}\" \"$@\"\n", inner_launcher.display()) - } -} - fn write_cli_launcher(path: &Path, contents: &str) -> Result<(), String> { if let Some(parent) = path.parent() { fs::create_dir_all(parent) @@ -941,30 +811,18 @@ fn write_cli_launcher(path: &Path, contents: &str) -> Result<(), String> { Ok(()) } -fn ensure_inner_cli_launcher( - paths: &ManagedPaths, - manifest: &ManagedRuntimeManifest, -) -> Result { - let inner_launcher = managed_cli_inner_launcher_path(&paths.stable_runtime_root); - write_cli_launcher( - &inner_launcher, - &inner_cli_launcher_contents(paths, manifest), - )?; - Ok(inner_launcher) -} - fn cli_manual_instructions( + runtime_root: &Path, paths: &ManagedPaths, manifest: &ManagedRuntimeManifest, ) -> Result { - let inner_launcher = ensure_inner_cli_launcher(paths, manifest)?; let outer_shim = outer_cli_shim_path()?; + let contents = cli_shim_contents(runtime_root, manifest, paths); #[cfg(windows)] { let target_dir = outer_shim .parent() .ok_or_else(|| "CLI shim target is missing a parent directory.".to_string())?; - let contents = outer_cli_shim_contents(&inner_launcher); return Ok(CliManualInstructions { title: "Install the Paseo CLI from PowerShell".to_string(), detail: "If the automatic install does not complete, run these commands in PowerShell to write the PATH shim manually.".to_string(), @@ -991,12 +849,9 @@ fn cli_manual_instructions( .as_ref() ), target_path = shell_single_quote(outer_shim.to_string_lossy().as_ref()), - lines = iter::once("#!/bin/sh".to_string()) - .chain(iter::once(format!( - "exec \"{}\" \"$@\"", - inner_launcher.to_string_lossy() - ))) - .map(|line| shell_single_quote(&line)) + lines = contents + .lines() + .map(|line| shell_single_quote(line)) .collect::>() .join(" ") ), @@ -1004,10 +859,13 @@ fn cli_manual_instructions( } } -fn detect_installed_cli_shim_path(paths: &ManagedPaths) -> Option { +fn detect_installed_cli_shim_path( + runtime_root: &Path, + paths: &ManagedPaths, + manifest: &ManagedRuntimeManifest, +) -> Option { let outer_shim = outer_cli_shim_path().ok()?; - let inner_launcher = managed_cli_inner_launcher_path(&paths.stable_runtime_root); - let expected = outer_cli_shim_contents(&inner_launcher); + let expected = cli_shim_contents(runtime_root, manifest, paths); let actual = fs::read_to_string(&outer_shim).ok()?; (actual == expected).then(|| outer_shim.to_string_lossy().into_owned()) } @@ -1032,79 +890,22 @@ fn write_state_file(path: &Path, value: &ManagedStateFile) -> Result<(), String> write_json_file(path, value) } -fn runtime_install_lock() -> &'static Mutex<()> { - RUNTIME_INSTALL_LOCK.get_or_init(|| Mutex::new(())) -} - -fn install_runtime_if_needed(source_root: &Path, target_root: &Path) -> Result<(), String> { - let _guard = runtime_install_lock() - .lock() - .map_err(|_| "Managed runtime install lock was poisoned.".to_string())?; - - let target_manifest = target_root.join("runtime-manifest.json"); - if target_manifest.exists() { - return Ok(()); - } - - if target_root.exists() { - fs::remove_dir_all(target_root).map_err(|error| { - format!( - "Failed to remove incomplete runtime {}: {error}", - target_root.display() - ) - })?; - } - - let staging_root = target_root.with_extension(format!("installing-{}", std::process::id())); - if staging_root.exists() { - fs::remove_dir_all(&staging_root).map_err(|error| { - format!( - "Failed to remove stale runtime staging dir {}: {error}", - staging_root.display() - ) - })?; - } - - copy_dir_recursive(source_root, &staging_root)?; - let staging_manifest = staging_root.join("runtime-manifest.json"); - if !staging_manifest.exists() { - let _ = fs::remove_dir_all(&staging_root); - return Err(format!( - "Managed runtime staging manifest is missing at {}", - staging_manifest.display() - )); - } - - fs::rename(&staging_root, target_root).map_err(|error| { - let _ = fs::remove_dir_all(&staging_root); - format!( - "Failed to finalize managed runtime install from {} to {}: {error}", - staging_root.display(), - target_root.display() - ) - })?; - - Ok(()) -} - -fn ensure_runtime_installed_internal(app: &AppHandle) -> Result { +fn ensure_runtime_ready_internal(app: &AppHandle) -> Result { let (bundled_root, pointer) = load_bundled_runtime_pointer(app)?; - let source_root = bundled_root.join(&pointer.relative_root); - let paths = resolve_paths(app, &pointer.runtime_id)?; - install_runtime_if_needed(&source_root, &paths.runtime_root)?; - let manifest = load_runtime_manifest(&paths.runtime_root)?; + let runtime_root = bundled_root.join(&pointer.relative_root); + let paths = resolve_paths(app)?; + let manifest = load_runtime_manifest(&runtime_root)?; if let Some(parent) = paths.transport_path.parent() { fs::create_dir_all(parent) .map_err(|error| format!("Failed to create {}: {error}", parent.display()))?; } fs::create_dir_all(&paths.managed_home) .map_err(|error| format!("Failed to create {}: {error}", paths.managed_home.display()))?; - ensure_inner_cli_launcher(&paths, &manifest)?; let existing_state = read_state_file(&paths.state_file_path); let target = managed_transport_target(&paths, existing_state.as_ref())?; let state = ManagedStateFile { runtime_id: manifest.runtime_id.clone(), - runtime_root: paths.runtime_root.to_string_lossy().into_owned(), + runtime_root: runtime_root.to_string_lossy().into_owned(), managed_home: paths.managed_home.to_string_lossy().into_owned(), transport_type: target.transport_type.clone(), transport_path: target.transport_path.clone(), @@ -1122,9 +923,7 @@ fn ensure_runtime_installed_internal(app: &AppHandle) -> Result Result { - let status = ensure_runtime_installed_internal(app)?; - let paths = resolve_paths(app, &status.runtime_id)?; - let manifest = load_runtime_manifest(&paths.runtime_root)?; + let status = ensure_runtime_ready_internal(app)?; + let paths = resolve_paths(app)?; + let runtime_root = PathBuf::from(&status.runtime_root); + let manifest = load_runtime_manifest(&runtime_root)?; + let cli_shim_path = detect_installed_cli_shim_path(&runtime_root, &paths, &manifest); let state = read_state_file(&paths.state_file_path); let target = managed_transport_target(&paths, state.as_ref())?; let tcp_settings = resolve_tcp_settings_from_state(state.as_ref()); let cli_status = run_cli_json_command( - &paths.runtime_root, + &runtime_root, &manifest, &[ "daemon", @@ -1208,7 +1009,7 @@ fn managed_daemon_status_internal(app: &AppHandle) -> Result Result Result<(), String> { } fn install_cli_shim_internal(app: &AppHandle) -> Result { - let status = ensure_runtime_installed_internal(app)?; - let paths = resolve_paths(app, &status.runtime_id)?; - let manifest = load_runtime_manifest(&paths.runtime_root)?; - let inner_launcher = ensure_inner_cli_launcher(&paths, &manifest)?; + let status = ensure_runtime_ready_internal(app)?; + let paths = resolve_paths(app)?; + let runtime_root = PathBuf::from(&status.runtime_root); + let manifest = load_runtime_manifest(&runtime_root)?; let shim_path = outer_cli_shim_path()?; - let shim_contents = outer_cli_shim_contents(&inner_launcher); - let manual_instructions = cli_manual_instructions(&paths, &manifest)?; + let shim_contents = cli_shim_contents(&runtime_root, &manifest, &paths); + let manual_instructions = cli_manual_instructions(&runtime_root, &paths, &manifest)?; #[cfg(target_os = "macos")] let install_result = install_cli_shim_via_macos_prompt(&shim_path, &shim_contents); @@ -1317,7 +1118,7 @@ fn install_cli_shim_internal(app: &AppHandle) -> Result { Ok(()) => { let mut state = read_state_file(&paths.state_file_path).unwrap_or(ManagedStateFile { runtime_id: status.runtime_id.clone(), - runtime_root: paths.runtime_root.to_string_lossy().into_owned(), + runtime_root: runtime_root.to_string_lossy().into_owned(), managed_home: paths.managed_home.to_string_lossy().into_owned(), transport_type: status.transport_type.clone(), transport_path: status.transport_path.clone(), @@ -1362,9 +1163,11 @@ fn install_cli_shim_internal(app: &AppHandle) -> Result { } fn uninstall_cli_shim_internal(app: &AppHandle) -> Result { - let status = ensure_runtime_installed_internal(app)?; - let paths = resolve_paths(app, &status.runtime_id)?; - let shim_path = detect_installed_cli_shim_path(&paths) + let status = ensure_runtime_ready_internal(app)?; + let paths = resolve_paths(app)?; + let runtime_root = PathBuf::from(&status.runtime_root); + let manifest = load_runtime_manifest(&runtime_root)?; + let shim_path = detect_installed_cli_shim_path(&runtime_root, &paths, &manifest) .map(PathBuf::from) .or_else(|| { read_state_file(&paths.state_file_path) @@ -1383,7 +1186,7 @@ fn uninstall_cli_shim_internal(app: &AppHandle) -> Result } let mut state = read_state_file(&paths.state_file_path).unwrap_or(ManagedStateFile { runtime_id: status.runtime_id.clone(), - runtime_root: paths.runtime_root.to_string_lossy().into_owned(), + runtime_root: runtime_root.to_string_lossy().into_owned(), managed_home: paths.managed_home.to_string_lossy().into_owned(), transport_type: status.transport_type.clone(), transport_path: status.transport_path.clone(), @@ -1404,8 +1207,8 @@ fn uninstall_cli_shim_internal(app: &AppHandle) -> Result } fn start_managed_daemon_internal(app: &AppHandle) -> Result { - let status = ensure_runtime_installed_internal(app)?; - let paths = resolve_paths(app, &status.runtime_id)?; + let status = ensure_runtime_ready_internal(app)?; + let paths = resolve_paths(app)?; let existing_status = managed_daemon_status_internal(app)?; if existing_status.daemon_running { return Ok(existing_status); @@ -1414,11 +1217,12 @@ fn start_managed_daemon_internal(app: &AppHandle) -> Result Result Result { - let status = ensure_runtime_installed_internal(app)?; - let paths = resolve_paths(app, &status.runtime_id)?; - let manifest = load_runtime_manifest(&paths.runtime_root)?; + let status = ensure_runtime_ready_internal(app)?; + let paths = resolve_paths(app)?; + let runtime_root = PathBuf::from(&status.runtime_root); + let manifest = load_runtime_manifest(&runtime_root)?; let output = cli_command( - &paths.runtime_root, + &runtime_root, &manifest, &[ "daemon", @@ -1481,13 +1286,14 @@ fn stop_managed_daemon_internal(app: &AppHandle) -> Result Result { - let status = ensure_runtime_installed_internal(app)?; - let paths = resolve_paths(app, &status.runtime_id)?; - let manifest = load_runtime_manifest(&paths.runtime_root)?; + let status = ensure_runtime_ready_internal(app)?; + let paths = resolve_paths(app)?; + let runtime_root = PathBuf::from(&status.runtime_root); + let manifest = load_runtime_manifest(&runtime_root)?; let state = read_state_file(&paths.state_file_path); let target = managed_transport_target(&paths, state.as_ref())?; let output = cli_command( - &paths.runtime_root, + &runtime_root, &manifest, &[ "daemon", @@ -1521,11 +1327,11 @@ fn update_managed_tcp_settings_internal( if settings.enabled { parse_tcp_listen(&format!("{}:{}", settings.host.trim(), settings.port))?; } - let status = ensure_runtime_installed_internal(app)?; - let paths = resolve_paths(app, &status.runtime_id)?; + let status = ensure_runtime_ready_internal(app)?; + let paths = resolve_paths(app)?; let mut state = read_state_file(&paths.state_file_path).unwrap_or(ManagedStateFile { runtime_id: status.runtime_id.clone(), - runtime_root: paths.runtime_root.to_string_lossy().into_owned(), + runtime_root: status.runtime_root.clone(), managed_home: paths.managed_home.to_string_lossy().into_owned(), transport_type: status.transport_type.clone(), transport_path: status.transport_path.clone(), @@ -1548,18 +1354,11 @@ fn update_managed_tcp_settings_internal( #[tauri::command] pub async fn managed_runtime_status(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || ensure_runtime_installed_internal(&app)) + tauri::async_runtime::spawn_blocking(move || ensure_runtime_ready_internal(&app)) .await .map_err(|error| format!("Managed runtime status task failed: {error}"))? } -#[tauri::command] -pub async fn ensure_managed_runtime(app: AppHandle) -> Result { - tauri::async_runtime::spawn_blocking(move || ensure_runtime_installed_internal(&app)) - .await - .map_err(|error| format!("Managed runtime install task failed: {error}"))? -} - #[tauri::command] pub async fn managed_daemon_status(app: AppHandle) -> Result { tauri::async_runtime::spawn_blocking(move || managed_daemon_status_internal(&app)) @@ -1591,8 +1390,8 @@ pub async fn restart_managed_daemon(app: AppHandle) -> Result Result { tauri::async_runtime::spawn_blocking(move || { - let status = ensure_runtime_installed_internal(&app)?; - let paths = resolve_paths(&app, &status.runtime_id)?; + ensure_runtime_ready_internal(&app)?; + let paths = resolve_paths(&app)?; Ok(ManagedDaemonLogs { log_path: paths.logs_path.to_string_lossy().into_owned(), contents: tail_log(&paths.logs_path, 400), @@ -1605,11 +1404,12 @@ pub async fn managed_daemon_logs(app: AppHandle) -> Result Result { tauri::async_runtime::spawn_blocking(move || { - let status = ensure_runtime_installed_internal(&app)?; - let paths = resolve_paths(&app, &status.runtime_id)?; - let manifest = load_runtime_manifest(&paths.runtime_root)?; + let status = ensure_runtime_ready_internal(&app)?; + let paths = resolve_paths(&app)?; + let runtime_root = PathBuf::from(&status.runtime_root); + let manifest = load_runtime_manifest(&runtime_root)?; let value = run_cli_json_command( - &paths.runtime_root, + &runtime_root, &manifest, &[ "daemon",