Add Windows support and improve cross-platform shell execution
This commit is contained in:
@@ -215,23 +215,38 @@ fn maybe_run_managed_headless_command(app: &AppHandle) -> Result<bool, String> {
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_login_shell() -> String {
|
fn shell_command(script: &str) -> Command {
|
||||||
std::env::var("SHELL")
|
#[cfg(unix)]
|
||||||
.ok()
|
{
|
||||||
.map(|value| value.trim().to_string())
|
let shell = std::env::var("SHELL")
|
||||||
.filter(|value| !value.is_empty())
|
.ok()
|
||||||
.unwrap_or_else(|| "/bin/zsh".to_string())
|
.map(|value| value.trim().to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.unwrap_or_else(|| "/bin/zsh".to_string());
|
||||||
|
let mut cmd = Command::new(shell);
|
||||||
|
cmd.arg("-lc").arg(script);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let mut cmd = Command::new("cmd.exe");
|
||||||
|
cmd.arg("/C").arg(script);
|
||||||
|
cmd
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_local_daemon_version(shell: &str) -> LocalDaemonVersionResult {
|
fn execute_local_daemon_version() -> LocalDaemonVersionResult {
|
||||||
|
#[cfg(unix)]
|
||||||
let script = r#"if command -v paseo >/dev/null 2>&1; then
|
let script = r#"if command -v paseo >/dev/null 2>&1; then
|
||||||
paseo --version
|
paseo --version
|
||||||
else
|
else
|
||||||
echo "paseo command not found in PATH" >&2
|
echo "paseo command not found in PATH" >&2
|
||||||
exit 127
|
exit 127
|
||||||
fi"#;
|
fi"#;
|
||||||
|
#[cfg(windows)]
|
||||||
|
let script = r#"where paseo >nul 2>&1 && paseo --version || (echo paseo command not found in PATH >&2 & exit /b 127)"#;
|
||||||
|
|
||||||
match Command::new(shell).arg("-lc").arg(script).output() {
|
match shell_command(script).output() {
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
@@ -268,15 +283,18 @@ fi"#;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_local_daemon_update(shell: &str) -> DaemonUpdateCommandResult {
|
fn execute_local_daemon_update() -> DaemonUpdateCommandResult {
|
||||||
|
#[cfg(unix)]
|
||||||
let script = r#"if command -v paseo >/dev/null 2>&1; then
|
let script = r#"if command -v paseo >/dev/null 2>&1; then
|
||||||
paseo daemon update
|
paseo daemon update
|
||||||
else
|
else
|
||||||
echo "paseo command not found in PATH. Ensure Paseo CLI is installed for this user." >&2
|
echo "paseo command not found in PATH. Ensure Paseo CLI is installed for this user." >&2
|
||||||
exit 127
|
exit 127
|
||||||
fi"#;
|
fi"#;
|
||||||
|
#[cfg(windows)]
|
||||||
|
let script = r#"where paseo >nul 2>&1 && paseo daemon update || (echo paseo command not found in PATH. Ensure Paseo CLI is installed for this user. >&2 & exit /b 127)"#;
|
||||||
|
|
||||||
match Command::new(shell).arg("-lc").arg(script).output() {
|
match shell_command(script).output() {
|
||||||
Ok(output) => DaemonUpdateCommandResult {
|
Ok(output) => DaemonUpdateCommandResult {
|
||||||
exit_code: output.status.code().unwrap_or(1),
|
exit_code: output.status.code().unwrap_or(1),
|
||||||
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
|
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
|
||||||
@@ -292,8 +310,7 @@ fi"#;
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_local_daemon_version() -> LocalDaemonVersionResult {
|
async fn get_local_daemon_version() -> LocalDaemonVersionResult {
|
||||||
let shell = resolve_login_shell();
|
tauri::async_runtime::spawn_blocking(execute_local_daemon_version)
|
||||||
tauri::async_runtime::spawn_blocking(move || execute_local_daemon_version(&shell))
|
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|error| LocalDaemonVersionResult {
|
.unwrap_or_else(|error| LocalDaemonVersionResult {
|
||||||
version: None,
|
version: None,
|
||||||
@@ -303,8 +320,7 @@ async fn get_local_daemon_version() -> LocalDaemonVersionResult {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn run_local_daemon_update() -> DaemonUpdateCommandResult {
|
async fn run_local_daemon_update() -> DaemonUpdateCommandResult {
|
||||||
let shell = resolve_login_shell();
|
tauri::async_runtime::spawn_blocking(execute_local_daemon_update)
|
||||||
tauri::async_runtime::spawn_blocking(move || execute_local_daemon_update(&shell))
|
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|error| DaemonUpdateCommandResult {
|
.unwrap_or_else(|error| DaemonUpdateCommandResult {
|
||||||
exit_code: -1,
|
exit_code: -1,
|
||||||
|
|||||||
@@ -561,15 +561,25 @@ fn dev_resource_root() -> PathBuf {
|
|||||||
|
|
||||||
fn bundled_runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
|
fn bundled_runtime_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||||
let candidate = resource_dir.join("managed-runtime");
|
// Tauri's bundle.resources preserves the source directory structure, so
|
||||||
if candidate.exists() {
|
// "resources/**/*" in tauri.conf.json places files at
|
||||||
log::info!("[runtime] found bundled runtime at {}", candidate.display());
|
// $RESOURCE/resources/managed-runtime/. Try the nested path first (installed
|
||||||
return Ok(candidate);
|
// builds), then the flat path for any future config changes.
|
||||||
|
for candidate in [
|
||||||
|
resource_dir.join("resources").join("managed-runtime"),
|
||||||
|
resource_dir.join("managed-runtime"),
|
||||||
|
] {
|
||||||
|
if candidate.exists() {
|
||||||
|
log::info!("[runtime] found bundled runtime at {}", candidate.display());
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
log::info!(
|
||||||
|
"[runtime] no bundled runtime at {}",
|
||||||
|
candidate.display()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
log::info!(
|
} else {
|
||||||
"[runtime] no bundled runtime at {}, checking dev path",
|
log::info!("[runtime] resource_dir() unavailable, checking dev path");
|
||||||
candidate.display()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
let dev = dev_resource_root().join("managed-runtime");
|
let dev = dev_resource_root().join("managed-runtime");
|
||||||
if dev.exists() {
|
if dev.exists() {
|
||||||
@@ -624,10 +634,19 @@ fn is_pid_running(pid: i32) -> bool {
|
|||||||
.status();
|
.status();
|
||||||
return output.map(|status| status.success()).unwrap_or(false);
|
return output.map(|status| status.success()).unwrap_or(false);
|
||||||
}
|
}
|
||||||
#[cfg(not(unix))]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let _ = pid;
|
let output = Command::new("tasklist")
|
||||||
false
|
.args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.output();
|
||||||
|
return output
|
||||||
|
.map(|out| {
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
stdout.contains(&pid.to_string())
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3269,25 +3269,35 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.logger.trace("interruptActiveTurn: calling query.interrupt()...");
|
this.logger.trace("interruptActiveTurn: calling query.interrupt()...");
|
||||||
const t0 = Date.now();
|
await this.awaitWithTimeout(
|
||||||
await queryToInterrupt.interrupt();
|
queryToInterrupt.interrupt(),
|
||||||
this.logger.trace({ durationMs: Date.now() - t0 }, "interruptActiveTurn: query.interrupt() returned");
|
"interruptActiveTurn query.interrupt()"
|
||||||
// After interrupt(), the query iterator is done (returns done: true).
|
);
|
||||||
// Clear it so ensureQuery() creates a fresh query for the next turn.
|
|
||||||
// Also end the input stream and call return() to clean up the SDK process.
|
|
||||||
this.input?.end();
|
this.input?.end();
|
||||||
this.logger.trace("interruptActiveTurn: calling query.return()...");
|
this.logger.trace("interruptActiveTurn: calling query.return()...");
|
||||||
const t1 = Date.now();
|
await this.awaitWithTimeout(
|
||||||
await queryToInterrupt.return?.();
|
queryToInterrupt.return?.(),
|
||||||
this.logger.trace({ durationMs: Date.now() - t1 }, "interruptActiveTurn: query.return() returned");
|
"interruptActiveTurn query.return()"
|
||||||
|
);
|
||||||
this.query = null;
|
this.query = null;
|
||||||
this.input = null;
|
this.input = null;
|
||||||
this.queryRestartNeeded = false;
|
this.queryRestartNeeded = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn({ err: error }, "Failed to interrupt active turn");
|
this.logger.warn({ err: error }, "Failed to interrupt active turn");
|
||||||
// If interrupt fails, the SDK iterator may remain in an indeterminate state.
|
this.input?.end();
|
||||||
// Force a teardown/recreate path so the next turn cannot reuse stale query state.
|
if (this.query === queryToInterrupt) {
|
||||||
this.queryRestartNeeded = true;
|
this.query = null;
|
||||||
|
this.input = null;
|
||||||
|
}
|
||||||
|
// Try to force-close the iterator to unblock the pump's q.next() call.
|
||||||
|
this.awaitWithTimeout(
|
||||||
|
queryToInterrupt.return?.(),
|
||||||
|
"interruptActiveTurn force return after failure"
|
||||||
|
).catch(() => {});
|
||||||
|
// Disown the current pump and start a fresh one immediately so
|
||||||
|
// autonomous wakes are not lost while waiting for the next user turn.
|
||||||
|
this.queryPumpPromise = null;
|
||||||
|
this.startQueryPump();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user