Simplify managed runtime to execute in place from app bundle

This commit is contained in:
Mohamed Boudra
2026-03-08 20:48:13 +07:00
parent ca787271b3
commit 2721ce331a
10 changed files with 613 additions and 475 deletions
+81
View File
@@ -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/<runtime-id>` 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
@@ -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)
})
})
@@ -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<AppSettings, 'manageBuiltInDaemon'>
startManagedDaemonFn?: () => Promise<ManagedDaemonStatus>
getManagedDaemonStatusFn?: () => Promise<ManagedDaemonStatus>
}): Promise<ManagedDaemonStatus> {
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(() => {
@@ -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<ManagedDaemonStatus | null>(null);
const [statusError, setStatusError] = useState<string | null>(null);
const [isRestartingDaemon, setIsRestartingDaemon] = useState(false);
const [isUpdatingDaemonManagement, setIsUpdatingDaemonManagement] = useState(false);
const [isInstallingCli, setIsInstallingCli] = useState(false);
const [statusMessage, setStatusMessage] = useState<string | null>(null);
const [cliStatusMessage, setCliStatusMessage] = useState<string | null>(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) {
<View style={styles.card}>
<View style={styles.row}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>Version</Text>
<Text style={styles.hintText}>{daemonVersionHint}</Text>
<Text style={styles.rowTitle}>Status</Text>
<Text style={styles.hintText}>Only the built-in managed daemon is shown here.</Text>
</View>
<View style={styles.statusValueGroup}>
<Text style={styles.valueText}>{daemonStatusStateText}</Text>
<Text style={styles.valueSubtext}>{daemonStatusDetailText}</Text>
</View>
<Text style={styles.valueText}>{localDaemonVersionText}</Text>
</View>
<View style={[styles.row, styles.rowBorder]}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>Restart daemon</Text>
<Text style={styles.hintText}>Restarts the built-in daemon.</Text>
<Text style={styles.rowTitle}>Daemon management</Text>
<Text style={styles.hintText}>
{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."}
</Text>
</View>
<Button
variant="outline"
size="sm"
style={styles.primaryActionButton}
onPress={handleToggleDaemonManagement}
disabled={isUpdatingDaemonManagement}
>
{isUpdatingDaemonManagement
? isDaemonManagementPaused
? "Resuming..."
: "Pausing..."
: isDaemonManagementPaused
? "Resume"
: "Pause"}
</Button>
</View>
<View style={[styles.row, styles.rowBorder]}>
<View style={styles.rowContent}>
<Text style={styles.rowTitle}>{daemonActionLabel}</Text>
<Text style={styles.hintText}>{daemonActionMessage}</Text>
{statusMessage ? (
<Text style={styles.statusText}>{statusMessage}</Text>
) : null}
</View>
<Button
variant="secondary"
variant="outline"
size="sm"
style={styles.primaryActionButton}
onPress={handleUpdateLocalDaemon}
disabled={isRestartingDaemon}
>
{isRestartingDaemon ? "Restarting..." : "Restart daemon"}
{isRestartingDaemon
? managedStatus?.daemonRunning
? "Restarting..."
: "Starting..."
: daemonActionLabel}
</Button>
</View>
<View style={[styles.row, styles.rowBorder]}>
@@ -286,8 +410,9 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
{cliStatusMessage ? <Text style={styles.statusText}>{cliStatusMessage}</Text> : null}
</View>
<Button
variant="secondary"
variant="outline"
size="sm"
style={styles.secondaryActionButton}
onPress={handleToggleCliShim}
disabled={isInstallingCli}
>
@@ -309,12 +434,12 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
</View>
<View style={styles.actionGroup}>
{(managedLogs?.logPath ?? managedStatus?.logPath) ? (
<Button variant="secondary" size="sm" onPress={handleCopyLogPath}>
<Button variant="outline" size="sm" onPress={handleCopyLogPath}>
Copy path
</Button>
) : null}
<Button
variant="secondary"
variant="outline"
size="sm"
onPress={handleOpenLogs}
disabled={!managedLogs}
@@ -330,7 +455,7 @@ export function LocalDaemonSection({ appVersion }: LocalDaemonSectionProps) {
Connect your phone to this computer.
</Text>
</View>
<Button variant="secondary" size="sm" onPress={handleOpenPairingModal}>
<Button variant="outline" size="sm" style={styles.secondaryActionButton} onPress={handleOpenPairingModal}>
Pair device
</Button>
</View>
@@ -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,
@@ -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<ManagedRuntimeStatus> {
return parseManagedRuntimeStatus(await invokeDesktopCommand("ensure_managed_runtime"));
}
export async function getManagedRuntimeStatus(): Promise<ManagedRuntimeStatus> {
return parseManagedRuntimeStatus(await invokeDesktopCommand("managed_runtime_status"));
}
@@ -0,0 +1,53 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const asyncStorageMock = vi.hoisted(() => ({
getItem: vi.fn<(_: string) => Promise<string | null>>(),
setItem: vi.fn<(_: string, __: string) => Promise<void>>(),
}));
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();
});
});
+8 -3
View File
@@ -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<AppSettings> {
export async function loadSettingsFromStorage(): Promise<AppSettings> {
try {
const stored = await AsyncStorage.getItem(APP_SETTINGS_KEY);
if (stored) {
@@ -98,6 +100,9 @@ function pickAppSettingsFromLegacy(legacy: Record<string, unknown>): Partial<App
if (legacy.theme === "dark" || legacy.theme === "light" || legacy.theme === "auto") {
result.theme = legacy.theme;
}
if (typeof legacy.manageBuiltInDaemon === "boolean") {
result.manageBuiltInDaemon = legacy.manageBuiltInDaemon;
}
return result;
}
+89 -124
View File
@@ -25,10 +25,6 @@ const currentRuntimePointer = JSON.parse(
);
const currentRuntimeId = currentRuntimePointer.runtimeId;
function buildRuntimeId(version) {
return currentRuntimeId.replace(currentRuntimeVersion, version);
}
function resolvePackagedBinary() {
if (process.platform === "darwin") {
return path.join(
@@ -103,87 +99,6 @@ function escapeForRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function derivePreviousVersion(version) {
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!match) {
return `${version}-previous`;
}
const [, major, minor, patch] = match;
const numericPatch = Number.parseInt(patch, 10);
if (numericPatch <= 0) {
return `${major}.${minor}.0-previous`;
}
return `${major}.${minor}.${numericPatch - 1}`;
}
function buildSeededTransportPath(testRoot) {
if (process.platform === "win32") {
return String.raw`\\.\pipe\paseo-managed-seeded-upgrade`;
}
return path.join(testRoot, "managed-home", "run", "paseo.sock");
}
async function seedPreviousManagedRuntime(testRoot) {
const previousRuntimeVersion = derivePreviousVersion(currentRuntimeVersion);
const previousRuntimeId = buildRuntimeId(previousRuntimeVersion);
const previousRuntimeRoot = path.join(testRoot, "runtime", previousRuntimeId);
const stateFilePath = path.join(testRoot, "managed-state.json");
const managedHome = path.join(testRoot, "managed-home");
const transportType = process.platform === "win32" ? "pipe" : "socket";
const transportPath = buildSeededTransportPath(testRoot);
await fs.mkdir(previousRuntimeRoot, { recursive: true });
await fs.writeFile(
path.join(previousRuntimeRoot, "runtime-manifest.json"),
JSON.stringify(
{
runtimeId: previousRuntimeId,
runtimeVersion: previousRuntimeVersion,
platform: process.platform,
arch: process.arch,
createdAt: "2000-01-01T00:00:00.000Z",
nodeRelativePath: process.platform === "win32" ? path.join("node", "node.exe") : path.join("node", "node"),
cliEntrypointRelativePath: path.join("node_modules", "@getpaseo", "cli", "dist", "index.js"),
cliShimRelativePath: path.join("node_modules", "@getpaseo", "cli", "bin", "paseo"),
serverRunnerRelativePath: path.join(
"node_modules",
"@getpaseo",
"server",
"dist",
"scripts",
"daemon-runner.js"
),
},
null,
2
) + "\n",
"utf8"
);
await fs.writeFile(path.join(previousRuntimeRoot, "seed-marker.txt"), "version-n\n", "utf8");
await fs.writeFile(
stateFilePath,
JSON.stringify(
{
runtimeId: previousRuntimeId,
runtimeRoot: previousRuntimeRoot,
managedHome,
transportType,
transportPath,
tcpEnabled: false,
tcpListen: null,
cliShimPath: null,
},
null,
2
) + "\n",
"utf8"
);
return {
previousRuntimeId,
previousRuntimeVersion,
previousRuntimeRoot,
stateFilePath,
};
}
async function runBinary(binaryPath, args, env) {
const { stdout, stderr } = await execFileAsync(binaryPath, args, {
@@ -223,6 +138,26 @@ async function runWorkspaceCli(args, env) {
return { stdout, stderr, json };
}
async function runBundledRuntimeCli(runtimeRoot, managedHome, args, env) {
const manifest = JSON.parse(
await fs.readFile(path.join(runtimeRoot, "runtime-manifest.json"), "utf8")
);
const { stdout, stderr } = await execFileAsync(
path.join(runtimeRoot, manifest.nodeRelativePath),
[path.join(runtimeRoot, manifest.cliEntrypointRelativePath), ...args],
{
cwd: repoRoot,
env: {
...process.env,
...env,
PASEO_HOME: managedHome,
},
maxBuffer: 10 * 1024 * 1024,
}
);
return { stdout, stderr };
}
async function readDaemonStatus(home, env) {
const result = await runWorkspaceCli(["daemon", "status", "--home", home, "--json"], env);
return result.json ?? {};
@@ -466,6 +401,7 @@ const externalPort = await getAvailablePort();
const externalEndpoint = `127.0.0.1:${externalPort}`;
const relayPort = await getAvailablePort();
const relayEndpoint = `127.0.0.1:${relayPort}`;
const managedRuntimeDir = path.join(testRoot, "runtime");
await fs.mkdir(testRoot, { recursive: true });
await fs.mkdir(fakePaseoHome, { recursive: true });
await fs.mkdir(cliScratchHome, { recursive: true });
@@ -473,7 +409,6 @@ await fs.mkdir(externalHome, { recursive: true });
await fs.writeFile(path.join(fakePaseoHome, "sentinel.txt"), "do not touch\n", "utf8");
const fakePaseoSnapshotBefore = await snapshotTree(fakePaseoHome);
const seededUpgradeState = await seedPreviousManagedRuntime(testRoot);
const managedEnv = {
...process.env,
HOME: fakeHome,
@@ -492,7 +427,7 @@ const managedEnv = {
let externalPid = null;
let startedTemporaryExternalDaemon = false;
let relayProcess = null;
const forbiddenManagedReferences = ["127.0.0.1:6767", fakePaseoHome];
const forbiddenManagedReferences = ["127.0.0.1:6767", fakePaseoHome, managedRuntimeDir];
try {
logStep(`Starting isolated local relay on ${relayEndpoint}`);
@@ -544,39 +479,40 @@ try {
logStep("Bootstrapping managed runtime from packaged desktop binary");
const runtimeStatus = await runBinary(packagedBinary, ["--managed-headless", "runtime-status"], managedEnv);
assert.equal(await pathExists(seededUpgradeState.previousRuntimeRoot), true);
assert.equal(runtimeStatus.json.runtimeId, currentRuntimeId);
assert.equal(runtimeStatus.json.runtimeVersion, currentRuntimeVersion);
assert.match(runtimeStatus.json.managedHome, new RegExp(`^${escapeForRegExp(testRoot)}`));
assert.equal(
runtimeStatus.json.installedRuntimeRoot,
path.join(testRoot, "runtime", currentRuntimeId),
"managed runtime should install under the current bundled runtime id"
assert.match(
runtimeStatus.json.runtimeRoot,
new RegExp(escapeForRegExp(currentRuntimeId)),
"runtime status should point into the bundled runtime selected by current-runtime.json"
);
assert.notEqual(
runtimeStatus.json.installedRuntimeRoot,
seededUpgradeState.previousRuntimeRoot,
"packaged app should adopt the bundled runtime instead of the seeded previous runtime"
assert.equal(
runtimeStatus.json.runtimeRoot.startsWith(testRoot),
false,
"runtime status should not resolve into managed app data"
);
assert.ok(runtimeStatus.json.transportType === "socket" || runtimeStatus.json.transportType === "pipe");
assert.notEqual(runtimeStatus.json.transportPath, "127.0.0.1:6767");
assertNoForbiddenPathsOrPorts(runtimeStatus.json, forbiddenManagedReferences);
const stateAfterRuntimeInstall = JSON.parse(
await fs.readFile(seededUpgradeState.stateFilePath, "utf8")
assert.equal(await pathExists(managedRuntimeDir), false, "managed app data should not gain a runtime tree");
const stateAfterRuntimeStatus = JSON.parse(
await fs.readFile(path.join(testRoot, "managed-state.json"), "utf8")
);
assert.equal(stateAfterRuntimeInstall.runtimeId, currentRuntimeId);
assert.equal(stateAfterRuntimeInstall.runtimeRoot, runtimeStatus.json.installedRuntimeRoot);
assert.equal(await pathExists(seededUpgradeState.previousRuntimeRoot), true);
assert.equal(stateAfterRuntimeStatus.runtimeId, currentRuntimeId);
assert.equal(stateAfterRuntimeStatus.runtimeRoot, runtimeStatus.json.runtimeRoot);
const managedStart = await runBinary(packagedBinary, ["--managed-headless", "bootstrap"], managedEnv);
assert.equal(managedStart.json.daemonRunning, true);
assert.ok(managedStart.json.daemonPid, "managed daemon pid should exist");
assert.equal(managedStart.json.runtimeRoot, runtimeStatus.json.runtimeRoot);
assert.ok(
managedStart.json.transportType === "socket" || managedStart.json.transportType === "pipe",
"managed daemon should default to private IPC transport"
);
assert.notEqual(managedStart.json.transportPath, "127.0.0.1:6767");
assertNoForbiddenPathsOrPorts(managedStart.json, forbiddenManagedReferences);
assert.equal(await pathExists(managedRuntimeDir), false, "starting the daemon should not install a runtime copy");
const managedPid = managedStart.json.daemonPid;
const stateFile = path.join(testRoot, "managed-state.json");
@@ -594,7 +530,7 @@ try {
assert.equal(persistedManagedStatus.json.relayEnabled, true);
assertNoForbiddenPathsOrPorts(persistedManagedStatus.json, forbiddenManagedReferences);
logStep("Installing CLI shim and verifying it targets the managed daemon");
logStep("Installing CLI shim and verifying the bundled CLI target");
const cliInstall = await runBinary(
packagedBinary,
["--managed-headless", "install-cli-shim"],
@@ -602,32 +538,62 @@ try {
);
const cliShimPath = cliInstall.json.path;
assert.ok(cliShimPath, "CLI shim path should be returned");
assert.equal(await pathExists(cliShimPath), true, "CLI shim should exist");
const cliVersion = await execFileAsync(cliShimPath, ["--version"], {
env: managedEnv,
cwd: repoRoot,
maxBuffer: 1024 * 1024,
});
const cliShimInstalled = cliInstall.json.installed === true && (await pathExists(cliShimPath));
if (!cliShimInstalled) {
assert.ok(cliInstall.json.manualInstructions, "manual CLI install instructions should be returned");
assert.match(
cliInstall.json.manualInstructions.commands,
new RegExp(escapeForRegExp(runtimeStatus.json.runtimeRoot)),
"manual CLI install instructions should point at the bundled runtime"
);
assertNoForbiddenPathsOrPorts(cliInstall.json.manualInstructions, forbiddenManagedReferences);
}
const cliVersion = cliShimInstalled
? await execFileAsync(cliShimPath, ["--version"], {
env: managedEnv,
cwd: repoRoot,
maxBuffer: 1024 * 1024,
})
: await runBundledRuntimeCli(
runtimeStatus.json.runtimeRoot,
managedStart.json.managedHome,
["--version"],
managedEnv
);
assert.match(cliVersion.stdout.trim(), /^0\./);
const shimStatus = await execFileAsync(cliShimPath, ["daemon", "status", "--json"], {
env: managedEnv,
cwd: repoRoot,
maxBuffer: 1024 * 1024,
});
const shimStatus = cliShimInstalled
? await execFileAsync(cliShimPath, ["daemon", "status", "--json"], {
env: managedEnv,
cwd: repoRoot,
maxBuffer: 1024 * 1024,
})
: await runBundledRuntimeCli(
runtimeStatus.json.runtimeRoot,
managedStart.json.managedHome,
["daemon", "status", "--json"],
managedEnv
);
const shimDaemonStatus = JSON.parse(shimStatus.stdout.trim());
assert.equal(shimDaemonStatus.pid, managedPid);
assertNoForbiddenPathsOrPorts(shimDaemonStatus, forbiddenManagedReferences);
logStep("Verifying relay connectivity still works after the desktop command has exited");
const relayPairing = await execFileAsync(
cliShimPath,
["daemon", "pair", "--home", managedStart.json.managedHome],
{
env: managedEnv,
cwd: repoRoot,
maxBuffer: 4 * 1024 * 1024,
}
);
const relayPairing = cliShimInstalled
? await execFileAsync(
cliShimPath,
["daemon", "pair", "--home", managedStart.json.managedHome],
{
env: managedEnv,
cwd: repoRoot,
maxBuffer: 4 * 1024 * 1024,
}
)
: await runBundledRuntimeCli(
runtimeStatus.json.runtimeRoot,
managedStart.json.managedHome,
["daemon", "pair", "--home", managedStart.json.managedHome],
managedEnv
);
const relayOfferUrl = parseOfferUrlFromCommandOutput(relayPairing.stdout);
const relayOffer = decodeOfferFromFragmentUrl(relayOfferUrl);
assert.equal(relayOffer.relay?.endpoint, relayEndpoint);
@@ -705,8 +671,7 @@ try {
{
runtimeStatus: runtimeStatus.json,
managedStart: managedStart.json,
seededUpgradeState,
stateAfterRuntimeInstall,
stateAfterRuntimeStatus,
persistedManagedStatus: persistedManagedStatus.json,
managedRestartless: managedRestartless.json,
cliInstall: cliInstall.json,
+5 -7
View File
@@ -14,11 +14,11 @@ use tauri_plugin_updater::UpdaterExt;
mod runtime_manager;
use runtime_manager::{
close_local_daemon_transport, ensure_managed_runtime, install_cli_shim, managed_daemon_logs,
managed_daemon_pairing, managed_daemon_status, managed_runtime_status,
open_local_daemon_transport, restart_managed_daemon, send_local_daemon_transport_message,
start_managed_daemon, stop_managed_daemon, uninstall_cli_shim,
update_managed_daemon_tcp_settings, LocalTransportState, ManagedTcpSettings,
close_local_daemon_transport, install_cli_shim, managed_daemon_logs, managed_daemon_pairing,
managed_daemon_status, managed_runtime_status, open_local_daemon_transport,
restart_managed_daemon, send_local_daemon_transport_message, start_managed_daemon,
stop_managed_daemon, uninstall_cli_shim, update_managed_daemon_tcp_settings,
LocalTransportState, ManagedTcpSettings,
};
// Store zoom as u64 bits (f64 * 100 as integer for atomic ops)
@@ -169,7 +169,6 @@ fn maybe_run_managed_headless_command(app: &AppHandle) -> Result<bool, String> {
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,
+103 -303
View File
@@ -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<Mutex<()>> = 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<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
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<ManagedPaths, String> {
fn resolve_paths(app: &AppHandle) -> Result<ManagedPaths, String> {
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<ManagedPaths, Stri
}
let root = app_data_root(app)?;
let runtime_root = resolve_override_path("PASEO_DESKTOP_MANAGED_RUNTIME_ROOT")
.unwrap_or_else(|| root.join("runtime").join(runtime_id));
let managed_home = resolve_override_path("PASEO_DESKTOP_MANAGED_HOME").unwrap_or_else(|| {
#[cfg(target_os = "macos")]
{
@@ -651,8 +545,6 @@ fn resolve_paths(app: &AppHandle, runtime_id: &str) -> Result<ManagedPaths, Stri
let transport_path = resolve_override_path("PASEO_DESKTOP_MANAGED_SOCKET_PATH")
.unwrap_or_else(|| default_transport_path(&managed_home, &root));
Ok(ManagedPaths {
runtime_root,
stable_runtime_root: root.join("runtime"),
managed_home: managed_home.clone(),
transport_path,
logs_path: managed_home.join("daemon.log"),
@@ -854,19 +746,6 @@ fn powershell_double_quote(value: &str) -> 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<PathBuf, String> {
#[cfg(target_os = "macos")]
{
@@ -889,11 +768,13 @@ fn outer_cli_shim_path() -> Result<PathBuf, String> {
}
}
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<PathBuf, String> {
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<CliManualInstructions, String> {
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::<Vec<_>>()
.join(" ")
),
@@ -1004,10 +859,13 @@ fn cli_manual_instructions(
}
}
fn detect_installed_cli_shim_path(paths: &ManagedPaths) -> Option<String> {
fn detect_installed_cli_shim_path(
runtime_root: &Path,
paths: &ManagedPaths,
manifest: &ManagedRuntimeManifest,
) -> Option<String> {
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<ManagedRuntimeStatus, String> {
fn ensure_runtime_ready_internal(app: &AppHandle) -> Result<ManagedRuntimeStatus, String> {
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<ManagedRuntimeSt
Ok(ManagedRuntimeStatus {
runtime_id: manifest.runtime_id,
runtime_version: manifest.runtime_version,
bundled_runtime_root: source_root.to_string_lossy().into_owned(),
installed_runtime_root: paths.runtime_root.to_string_lossy().into_owned(),
installed: true,
runtime_root: runtime_root.to_string_lossy().into_owned(),
managed_home: paths.managed_home.to_string_lossy().into_owned(),
transport_type: state.transport_type,
transport_path: state.transport_path,
@@ -1161,14 +960,16 @@ fn run_cli_json_command(
}
fn managed_daemon_status_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
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<ManagedDaemonStatus
Ok(ManagedDaemonStatus {
runtime_id: manifest.runtime_id,
runtime_version: manifest.runtime_version,
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,
transport_path: target.transport_path,
@@ -1223,7 +1024,7 @@ fn managed_daemon_status_internal(app: &AppHandle) -> Result<ManagedDaemonStatus
tcp_listen: tcp_settings
.enabled
.then(|| format!("{}:{}", tcp_settings.host, tcp_settings.port)),
cli_shim_path: detect_installed_cli_shim_path(&paths),
cli_shim_path,
})
}
@@ -1298,13 +1099,13 @@ fn remove_cli_shim_via_macos_prompt(shim_path: &Path) -> Result<(), String> {
}
fn install_cli_shim_internal(app: &AppHandle) -> Result<CliShimResult, String> {
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<CliShimResult, String> {
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<CliShimResult, String> {
}
fn uninstall_cli_shim_internal(app: &AppHandle) -> Result<CliShimResult, String> {
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<CliShimResult, String>
}
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<CliShimResult, String>
}
fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
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<ManagedDaemonStatus,
fs::create_dir_all(parent)
.map_err(|error| format!("Failed to create {}: {error}", parent.display()))?;
}
let manifest = load_runtime_manifest(&paths.runtime_root)?;
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,
&[
"start",
@@ -1451,11 +1255,12 @@ fn start_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus,
}
fn stop_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
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<ManagedDaemonStatus,
}
fn restart_managed_daemon_internal(app: &AppHandle) -> Result<ManagedDaemonStatus, String> {
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<ManagedRuntimeStatus, String> {
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<ManagedRuntimeStatus, String> {
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<ManagedDaemonStatus, String> {
tauri::async_runtime::spawn_blocking(move || managed_daemon_status_internal(&app))
@@ -1591,8 +1390,8 @@ pub async fn restart_managed_daemon(app: AppHandle) -> Result<ManagedDaemonStatu
#[tauri::command]
pub async fn managed_daemon_logs(app: AppHandle) -> Result<ManagedDaemonLogs, String> {
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<ManagedDaemonLogs, St
#[tauri::command]
pub async fn managed_daemon_pairing(app: AppHandle) -> Result<ManagedPairingOffer, String> {
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",