use crate::error::Result;
use crate::local;
use crate::registry::Registry;
use crate::runtime::RuntimeCtx;
use console::style;
use std::path::PathBuf;
use super::{job_path_from_workspace, status::resolve_job};
#[derive(Debug, Default)]
pub struct ShowLogsOptions {
pub follow: bool,
pub only_stdout: bool,
pub only_stderr: bool,
pub tail: Option<usize>,
pub raw: bool,
pub ctx: RuntimeCtx,
}
pub async fn show_logs(
job_id: Option<&str>,
tags: &[(String, String)],
note_filter: Option<&str>,
opts: ShowLogsOptions,
) -> Result<()> {
let registry = Registry::open()?;
let job = resolve_job(®istry, job_id, tags, note_filter)?;
let show_stdout = !opts.only_stderr || opts.only_stdout;
let show_stderr = !opts.only_stdout || opts.only_stderr;
let show_both = show_stdout && show_stderr;
let strip_ansi = opts.raw || !std::io::IsTerminal::is_terminal(&std::io::stdout());
if job.remote_host == "local" {
let project_path = PathBuf::from(&job.project_path);
if opts.follow {
println!(
"{}",
style("Following output (Ctrl+C to disconnect)...").yellow()
);
local::follow_local_logs(&project_path, &job.id)?;
} else if show_both {
println!("{}", style("=== STDOUT ===").bold());
match local::read_local_logs(
&project_path,
&job.id,
local::LogStream::Stdout,
opts.tail,
) {
Ok(content) => print!("{}", maybe_strip_ansi(&content, strip_ansi)),
Err(e) => eprintln!("Error reading stdout: {e}"),
}
println!();
println!("{}", style("=== STDERR ===").bold());
match local::read_local_logs(
&project_path,
&job.id,
local::LogStream::Stderr,
opts.tail,
) {
Ok(content) => print!("{}", maybe_strip_ansi(&content, strip_ansi)),
Err(e) => eprintln!("Error reading stderr: {e}"),
}
} else {
let stream = if show_stderr {
local::LogStream::Stderr
} else {
local::LogStream::Stdout
};
let content = local::read_local_logs(&project_path, &job.id, stream, opts.tail)?;
print!("{}", maybe_strip_ansi(&content, strip_ansi));
}
return Ok(());
}
let ssh = opts.ctx.ssh(&job.remote_host);
let log_base = job_path_from_workspace(&job.remote_path, &job.id);
if opts.follow {
let paths = if opts.only_stderr {
vec![format!("{log_base}/job.err")]
} else if opts.only_stdout {
vec![format!("{log_base}/job.out")]
} else {
vec![format!("{log_base}/job.out"), format!("{log_base}/job.err")]
};
let path_refs: Vec<&str> = paths.iter().map(String::as_str).collect();
if job.slurm_id.is_some() {
println!(
"{}",
style("Following output (Ctrl+C to disconnect)...").yellow()
);
}
let mut child = ssh.tail_follow(&path_refs)?;
let _ = child.wait().await;
} else if show_both {
println!("{}", style("=== STDOUT ===").bold());
let stdout_path = format!("{log_base}/job.out");
match ssh.cat_tail(&stdout_path, opts.tail).await {
Ok(content) => print!("{}", maybe_strip_ansi(&content, strip_ansi)),
Err(e) => eprintln!("Error reading stdout: {e}"),
}
println!();
println!("{}", style("=== STDERR ===").bold());
let stderr_path = format!("{log_base}/job.err");
match ssh.cat_tail(&stderr_path, opts.tail).await {
Ok(content) => print!("{}", maybe_strip_ansi(&content, strip_ansi)),
Err(e) => eprintln!("Error reading stderr: {e}"),
}
} else {
let log_file = if show_stderr { "job.err" } else { "job.out" };
let log_path = format!("{log_base}/{log_file}");
let content = ssh.cat_tail(&log_path, opts.tail).await?;
print!("{}", maybe_strip_ansi(&content, strip_ansi));
}
Ok(())
}
fn maybe_strip_ansi(s: &str, strip: bool) -> std::borrow::Cow<'_, str> {
if strip {
strip_ansi_codes(s).into()
} else {
s.into()
}
}
fn strip_ansi_codes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
match chars.peek() {
Some('[') => {
chars.next(); while let Some(&nc) = chars.peek() {
chars.next();
if nc.is_ascii_alphabetic() || ('@'..='~').contains(&nc) {
break;
}
}
}
Some(']') => {
chars.next(); while let Some(&nc) = chars.peek() {
chars.next();
if nc == '\x07' {
break;
}
if nc == '\x1b' {
if chars.peek() == Some(&'\\') {
chars.next();
}
break;
}
}
}
Some('(' | ')') => {
chars.next();
chars.next(); }
_ => {
chars.next();
}
}
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_ansi_codes_csi_colors() {
assert_eq!(strip_ansi_codes("\x1b[31mred\x1b[0m"), "red");
assert_eq!(
strip_ansi_codes("\x1b[1;32mbold green\x1b[0m"),
"bold green"
);
assert_eq!(
strip_ansi_codes("\x1b[38;5;196mextended\x1b[0m"),
"extended"
);
}
#[test]
fn test_strip_ansi_codes_csi_cursor() {
assert_eq!(strip_ansi_codes("\x1b[2Jclear"), "clear");
assert_eq!(strip_ansi_codes("\x1b[10;20Hposition"), "position");
}
#[test]
fn test_strip_ansi_codes_osc() {
assert_eq!(strip_ansi_codes("\x1b]0;title\x07text"), "text");
assert_eq!(strip_ansi_codes("\x1b]9;notification\x07text"), "text");
}
#[test]
fn test_strip_ansi_codes_preserves_text() {
assert_eq!(strip_ansi_codes("plain text"), "plain text");
assert_eq!(strip_ansi_codes("line1\nline2"), "line1\nline2");
assert_eq!(strip_ansi_codes(""), "");
}
#[test]
fn test_strip_ansi_codes_complex() {
let input = "\x1b[1mBold\x1b[0m and \x1b[31mred\x1b[0m text";
assert_eq!(strip_ansi_codes(input), "Bold and red text");
}
#[test]
fn test_strip_ansi_codes_progress_bar() {
let input = "Progress: \x1b[32m50%\x1b[0m \x1b[K";
assert_eq!(strip_ansi_codes(input), "Progress: 50% ");
}
}