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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,9 +115,7 @@ export default function ProgressScreen({ bootstrap }: ProgressProps) {
|
|||
key={idx}
|
||||
className={clsx(
|
||||
'whitespace-pre-wrap',
|
||||
entry.line.startsWith('stderr:')
|
||||
? 'text-destructive'
|
||||
: 'text-foreground/70'
|
||||
entry.stream === 'stderr' ? 'text-foreground/45' : 'text-foreground/70'
|
||||
)}
|
||||
>
|
||||
{entry.line}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export interface BootstrapStateModel {
|
|||
currentStage: string | null
|
||||
installRoot: string | null
|
||||
error: string | null
|
||||
logs: Array<{ stage?: string; line: string }>
|
||||
logs: Array<{ stage?: string; line: string; stream?: 'stdout' | 'stderr' }>
|
||||
}
|
||||
|
||||
const INITIAL: BootstrapStateModel = {
|
||||
|
|
@ -106,6 +106,7 @@ interface BootstrapLogEvent {
|
|||
type: 'log'
|
||||
stage?: string
|
||||
line: string
|
||||
stream?: 'stdout' | 'stderr'
|
||||
}
|
||||
|
||||
interface BootstrapCompleteEvent {
|
||||
|
|
@ -192,7 +193,7 @@ export async function initialize(): Promise<void> {
|
|||
break
|
||||
}
|
||||
case 'log': {
|
||||
const logs = [...cur.logs, { stage: payload.stage, line: payload.line }]
|
||||
const logs = [...cur.logs, { stage: payload.stage, line: payload.line, stream: payload.stream }]
|
||||
// Keep the rolling buffer bounded so the UI doesn't get OOM'd
|
||||
// during a long install (playwright chromium download is ~10k lines).
|
||||
const trimmed = logs.length > 2000 ? logs.slice(-2000) : logs
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue