fix(desktop): fix Claude agent spawn from managed runtime and rotate logs on restart
- Always override spawnClaudeCodeProcess to use process.execPath instead of SDK's PATH-based "node" lookup, which fails in the managed runtime bundle - Fix append-mode arg ordering in resolveClaudeSpawnCommand so extra CLI args (e.g. --chrome) go after cli.js, not before it (Node exit code 9) - Rotate daemon.log on every daemon restart for clean startup logs - Remove dead dev_resource_root fallback from runtime_manager.rs - Canonicalize current_exe path in CLI runner
This commit is contained in:
@@ -312,10 +312,6 @@ fn read_json_file<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String
|
||||
.map_err(|error| format!("Failed to parse {}: {error}", path.display()))
|
||||
}
|
||||
|
||||
fn dev_resource_root() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources")
|
||||
}
|
||||
|
||||
fn bundled_runtime_root_from_resource_dir(resource_dir: &Path) -> Option<PathBuf> {
|
||||
for candidate in [
|
||||
resource_dir.join("resources").join("managed-runtime"),
|
||||
@@ -337,12 +333,7 @@ fn bundled_runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
return Ok(candidate);
|
||||
}
|
||||
} else {
|
||||
log::info!("[runtime] resource_dir() unavailable, checking dev path");
|
||||
}
|
||||
let dev = dev_resource_root().join("managed-runtime");
|
||||
if dev.exists() {
|
||||
log::info!("[runtime] using dev runtime at {}", dev.display());
|
||||
return Ok(dev);
|
||||
log::info!("[runtime] resource_dir() unavailable");
|
||||
}
|
||||
log::error!("[runtime] no managed runtime found");
|
||||
Err("Managed runtime resources are not bundled with this desktop build.".to_string())
|
||||
@@ -786,6 +777,7 @@ pub async fn update_managed_daemon_tcp_settings(
|
||||
|
||||
pub fn run_managed_cli_from_current_process(args: Vec<String>) -> Result<i32, String> {
|
||||
let current_exe = std::env::current_exe()
|
||||
.and_then(|p| p.canonicalize())
|
||||
.map_err(|error| format!("Failed to resolve desktop executable path: {error}"))?;
|
||||
let current_exe = dunce::simplified(¤t_exe).to_path_buf();
|
||||
let exe_dir = current_exe
|
||||
@@ -799,10 +791,6 @@ pub fn run_managed_cli_from_current_process(args: Vec<String>) -> Result<i32, St
|
||||
let bundled_root = resource_dirs
|
||||
.into_iter()
|
||||
.find_map(|resource_dir| bundled_runtime_root_from_resource_dir(&resource_dir))
|
||||
.or_else(|| {
|
||||
let dev = dev_resource_root().join("managed-runtime");
|
||||
dev.exists().then_some(dev)
|
||||
})
|
||||
.ok_or_else(|| {
|
||||
"Managed runtime resources are not bundled with this desktop build.".to_string()
|
||||
})?;
|
||||
|
||||
@@ -376,7 +376,7 @@ function resolveClaudeSpawnCommand(
|
||||
if (commandConfig.mode === "append") {
|
||||
return {
|
||||
command: spawnOptions.command,
|
||||
args: [...(commandConfig.args ?? []), ...spawnOptions.args],
|
||||
args: [...spawnOptions.args, ...(commandConfig.args ?? [])],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -390,15 +390,6 @@ function applyRuntimeSettingsToClaudeOptions(
|
||||
options: ClaudeOptions,
|
||||
runtimeSettings?: ProviderRuntimeSettings
|
||||
): ClaudeOptions {
|
||||
const hasEnvOverrides = Object.keys(runtimeSettings?.env ?? {}).length > 0;
|
||||
const commandMode = runtimeSettings?.command?.mode;
|
||||
const needsCustomSpawn =
|
||||
hasEnvOverrides || commandMode === "append" || commandMode === "replace";
|
||||
|
||||
if (!needsCustomSpawn) {
|
||||
return options;
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
spawnClaudeCodeProcess: (spawnOptions) => {
|
||||
@@ -406,7 +397,13 @@ function applyRuntimeSettingsToClaudeOptions(
|
||||
spawnOptions,
|
||||
runtimeSettings
|
||||
);
|
||||
return spawn(resolved.command, resolved.args, {
|
||||
// The SDK defaults to spawning "node" via PATH lookup, which fails when
|
||||
// running from the managed runtime bundle where node isn't in PATH.
|
||||
// Always use process.execPath — the actual node binary running the daemon.
|
||||
const command = resolved.command === spawnOptions.command
|
||||
? process.execPath
|
||||
: resolved.command;
|
||||
return spawn(command, resolved.args, {
|
||||
cwd: spawnOptions.cwd,
|
||||
env: applyProviderEnv(spawnOptions.env, runtimeSettings),
|
||||
signal: spawnOptions.signal,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readdirSync, renameSync, unlinkSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import pretty from "pino-pretty";
|
||||
@@ -138,6 +138,37 @@ function normalizeLoggerConfigInput(config: LoggerConfigInput): PersistedConfig
|
||||
return config as PersistedConfig;
|
||||
}
|
||||
|
||||
function rotateOnRestart(filePath: string, maxFiles: number): void {
|
||||
if (!existsSync(filePath)) return;
|
||||
|
||||
const dir = path.dirname(filePath);
|
||||
const base = path.basename(filePath);
|
||||
const now = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
const ts = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
|
||||
|
||||
try {
|
||||
renameSync(filePath, path.join(dir, `${ts}-00-${base}`));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean up old rotated logs beyond maxFiles.
|
||||
// Both our restart-rotated files (YYYYMMDD-HHMM-00-daemon.log) and
|
||||
// rotating-file-stream's size-rotated files (YYYYMMDD-HHMM-NN-daemon.log)
|
||||
// end with -${base} and sort chronologically by name.
|
||||
const rotatedFiles = readdirSync(dir)
|
||||
.filter((f) => f.endsWith(`-${base}`) && f !== base)
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
for (const file of rotatedFiles.slice(maxFiles)) {
|
||||
try {
|
||||
unlinkSync(path.join(dir, file));
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
function toRotatingFileStreamSize(size: string): string {
|
||||
const trimmed = size.trim();
|
||||
const match = trimmed.match(/^(\d+)\s*([bBkKmMgG])?$/);
|
||||
@@ -230,11 +261,12 @@ export function createRootLogger(
|
||||
})
|
||||
: pino.destination({ dest: 1, sync: false });
|
||||
|
||||
rotateOnRestart(config.file.path, config.file.rotate.maxFiles);
|
||||
|
||||
const fileStream = createRotatingFileStream(path.basename(config.file.path), {
|
||||
path: path.dirname(config.file.path),
|
||||
size: toRotatingFileStreamSize(config.file.rotate.maxSize),
|
||||
maxFiles: config.file.rotate.maxFiles,
|
||||
initialRotation: true,
|
||||
});
|
||||
|
||||
return pino(
|
||||
|
||||
Reference in New Issue
Block a user