mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-18 14:52:04 +00:00
fix(installer): stop mislabeling stdout-style progress as stderr
Both installers (Electron bootstrap-runner + Tauri) hardcoded a literal `stderr: ` prefix onto every line that arrived on fd 2. Tools like uv/pip/git/npm write normal progress to stderr by design, so routine install output showed up tagged as "stderr" (and rendered red in the Tauri progress UI), making a healthy install look like it was erroring. Carry the stream as structured metadata (`stream: 'stdout' | 'stderr'`) on the log event instead of mangling the line text. The UI now styles stderr subtly (dimmed) rather than alarmingly, and the persistent forensic logs keep their stdout/stderr distinction.
This commit is contained in:
parent
ecac659d7d
commit
810e5864db
9 changed files with 50 additions and 29 deletions
|
|
@ -21,7 +21,7 @@ use serde::{Deserialize, Serialize};
|
|||
use tauri::{AppHandle, Emitter, State};
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
use crate::events::{BootstrapEvent, Manifest, StageState};
|
||||
use crate::events::{BootstrapEvent, LogStream, Manifest, StageState};
|
||||
use crate::install_script::{self, Pin, ScriptKind, ScriptSource};
|
||||
use crate::powershell::{self, StreamSink};
|
||||
use crate::AppState;
|
||||
|
|
@ -291,6 +291,7 @@ async fn run_bootstrap(
|
|||
BootstrapEvent::Log {
|
||||
stage: None,
|
||||
line: line.to_string(),
|
||||
stream: LogStream::Stdout,
|
||||
},
|
||||
);
|
||||
// Bump to info-level so the line shows in bootstrap-installer.log
|
||||
|
|
@ -625,6 +626,7 @@ async fn run_install_script(
|
|||
BootstrapEvent::Log {
|
||||
stage: stage_for_stdout.clone(),
|
||||
line: line.to_string(),
|
||||
stream: LogStream::Stdout,
|
||||
},
|
||||
);
|
||||
// Tee to the rolling installer log so we have a persistent
|
||||
|
|
@ -643,7 +645,8 @@ async fn run_install_script(
|
|||
&app_for_stderr,
|
||||
BootstrapEvent::Log {
|
||||
stage: stage_for_stderr.clone(),
|
||||
line: format!("stderr: {line}"),
|
||||
line: line.to_string(),
|
||||
stream: LogStream::Stderr,
|
||||
},
|
||||
);
|
||||
// stderr-level lines get warn! so they're visually distinct
|
||||
|
|
|
|||
|
|
@ -51,6 +51,16 @@ pub enum StageState {
|
|||
Failed,
|
||||
}
|
||||
|
||||
/// Which pipe a raw log line came from. Reported as structured metadata so
|
||||
/// the UI can style stderr subtly rather than mislabeling it as an error:
|
||||
/// uv/pip/git/npm write normal progress to stderr by design.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum LogStream {
|
||||
Stdout,
|
||||
Stderr,
|
||||
}
|
||||
|
||||
/// The single event channel `bootstrap` emits these. `type` discriminates.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "lowercase")]
|
||||
|
|
@ -72,11 +82,14 @@ pub enum BootstrapEvent {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
error: Option<String>,
|
||||
},
|
||||
/// Raw stdout/stderr line from install.ps1 (or our wrapper).
|
||||
/// Raw stdout/stderr line from install.ps1 (or our wrapper). `stream`
|
||||
/// tells the UI which pipe it came from so stderr can be styled subtly
|
||||
/// instead of being mislabeled as an error.
|
||||
Log {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stage: Option<String>,
|
||||
line: String,
|
||||
stream: LogStream,
|
||||
},
|
||||
/// Sent once when all stages complete successfully.
|
||||
Complete {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use tauri::{AppHandle, Emitter};
|
|||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio::process::Command;
|
||||
|
||||
use crate::events::{BootstrapEvent, StageInfo, StageState};
|
||||
use crate::events::{BootstrapEvent, LogStream, StageInfo, StageState};
|
||||
|
||||
/// `hermes update` exit code meaning "another hermes process is holding the
|
||||
/// venv shim open / dirty precondition" — see _cmd_update_impl in
|
||||
|
|
@ -237,6 +237,7 @@ async fn run_update(app: AppHandle) -> Result<()> {
|
|||
emit_log(
|
||||
&app,
|
||||
None,
|
||||
LogStream::Stdout,
|
||||
&format!("[update] could not auto-launch desktop: {err}. Launch Hermes manually."),
|
||||
);
|
||||
}
|
||||
|
|
@ -251,7 +252,7 @@ async fn wait_for_venv_free(install_root: &Path, app: &AppHandle) {
|
|||
let shim = venv_hermes(install_root);
|
||||
let deadline = Instant::now() + DESKTOP_EXIT_WAIT;
|
||||
|
||||
emit_log(app, Some("update"), "[update] waiting for Hermes to exit…");
|
||||
emit_log(app, Some("update"), LogStream::Stdout, "[update] waiting for Hermes to exit…");
|
||||
|
||||
loop {
|
||||
if !is_locked(&shim) {
|
||||
|
|
@ -261,6 +262,7 @@ async fn wait_for_venv_free(install_root: &Path, app: &AppHandle) {
|
|||
emit_log(
|
||||
app,
|
||||
Some("update"),
|
||||
LogStream::Stdout,
|
||||
"[update] timed out waiting for Hermes to exit; proceeding anyway",
|
||||
);
|
||||
return;
|
||||
|
|
@ -320,22 +322,22 @@ async fn run_streamed(
|
|||
loop {
|
||||
tokio::select! {
|
||||
line = out.next_line() => match line {
|
||||
Ok(Some(l)) => emit_log(app, stage_owned.as_deref(), &l),
|
||||
Ok(Some(l)) => emit_log(app, stage_owned.as_deref(), LogStream::Stdout, &l),
|
||||
Ok(None) => break,
|
||||
Err(e) => { tracing::warn!("stdout read error: {e}"); break; }
|
||||
},
|
||||
line = err.next_line() => match line {
|
||||
Ok(Some(l)) => emit_log(app, stage_owned.as_deref(), &format!("stderr: {l}")),
|
||||
Ok(Some(l)) => emit_log(app, stage_owned.as_deref(), LogStream::Stderr, &l),
|
||||
Ok(None) => {}
|
||||
Err(e) => { tracing::warn!("stderr read error: {e}"); }
|
||||
},
|
||||
}
|
||||
}
|
||||
while let Ok(Some(l)) = out.next_line().await {
|
||||
emit_log(app, stage_owned.as_deref(), &l);
|
||||
emit_log(app, stage_owned.as_deref(), LogStream::Stdout, &l);
|
||||
}
|
||||
while let Ok(Some(l)) = err.next_line().await {
|
||||
emit_log(app, stage_owned.as_deref(), &format!("stderr: {l}"));
|
||||
emit_log(app, stage_owned.as_deref(), LogStream::Stderr, &l);
|
||||
}
|
||||
|
||||
let status = child.wait().await.map_err(|e| anyhow!("waiting for child: {e}"))?;
|
||||
|
|
@ -429,7 +431,7 @@ fn emit_stage(
|
|||
);
|
||||
}
|
||||
|
||||
fn emit_log(app: &AppHandle, stage: Option<&str>, line: &str) {
|
||||
fn emit_log(app: &AppHandle, stage: Option<&str>, stream: LogStream, line: &str) {
|
||||
match stage {
|
||||
Some(s) => tracing::info!(target: "bootstrap.log", stage = %s, "{line}"),
|
||||
None => tracing::info!(target: "bootstrap.log", "{line}"),
|
||||
|
|
@ -439,6 +441,7 @@ fn emit_log(app: &AppHandle, stage: Option<&str>, line: &str) {
|
|||
BootstrapEvent::Log {
|
||||
stage: stage.map(|s| s.to_string()),
|
||||
line: line.to_string(),
|
||||
stream,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue