use super::BashJob; use std::fs::File; use std::io::Read; const HEAD_BYTES: usize = 8 * 1024; const HEAD_LINES: i32 = 100; const TAIL_BYTES: usize = 32 * 1024; const PROGRESS_TAIL_LINES: i32 = 4; const FINAL_TAIL_LINES: i32 = 20; pub(super) fn observation(job: &mut BashJob) -> Vec { job.poll(); let first = !job.observed_once; let (bytes, display_lines, last_nl) = file_stats(&job.path); let elapsed = job.start.elapsed().as_secs_f64(); let mut out = if job.running { format!( "bash job={} pid={} status=running elapsed_sec={elapsed:.1} timeout_sec={:.0}\n", job.id, job.pid, job.timeout_sec ) } else { format!( "bash job={} pid={} status=done elapsed_sec={elapsed:.1} timed_out={}\nexit_status={}\n", job.id, job.pid, i32::from(job.timed_out), job.exit_status ) } .into_bytes(); if bytes == 0 { out.extend_from_slice(b"\n\n"); } else if first { let (head, shown, byte_limited) = read_head(&job.path); let truncated = byte_limited || display_lines > shown; if !job.running && !truncated { out.extend_from_slice(b"\n"); out.extend_from_slice(&head); if !head.is_empty() && !head.ends_with(b"\n") { out.push(b'\n'); } out.extend_from_slice(b"\n"); } else { append_path_head(&mut out, &job.path, bytes, display_lines, &head); } } else { let tail_lines = if job.running { PROGRESS_TAIL_LINES } else { FINAL_TAIL_LINES }; let tail = read_tail(&job.path, tail_lines); let path = job.path.display(); out.extend_from_slice( format!("output_path={path} ({bytes} bytes, {display_lines} lines)\n").as_bytes(), ); out.extend_from_slice(format!("\n").as_bytes()); out.extend_from_slice(&tail); if !tail.is_empty() && !tail.ends_with(b"\n") { out.push(b'\n'); } out.extend_from_slice(b"\n"); } if job.running { out.extend_from_slice( format!( "\nUse bash_status job={} to get info before refresh time; use bash_stop job={} to stop execution\n", job.id, job.id ) .as_bytes(), ); } job.observed_once = true; let _ = (last_nl,); out } fn file_stats(path: &std::path::Path) -> (usize, i32, bool) { let Ok(data) = std::fs::read(path) else { return (0, 0, true); }; if data.is_empty() { return (0, 0, true); } let newlines = data.iter().filter(|byte| **byte == b'\n').count() as i32; let last_nl = data.last() == Some(&b'\n'); (data.len(), newlines + i32::from(!last_nl), last_nl) } fn read_head(path: &std::path::Path) -> (Vec, i32, bool) { let Ok(mut file) = File::open(path) else { return (b"\n".to_vec(), 0, false); }; let mut out = Vec::new(); let mut buf = [0u8; 1]; let mut lines = 0i32; while lines < HEAD_LINES && out.len() < HEAD_BYTES { match file.read(&mut buf) { Ok(0) => break, Ok(_) => { out.push(buf[0]); if buf[0] == b'\n' { lines += 1; } } Err(_) => break, } } let byte_limited = out.len() >= HEAD_BYTES; let shown = lines + i32::from(!out.is_empty() && !out.ends_with(b"\n")); (out, shown, byte_limited) } fn read_tail(path: &std::path::Path, max_lines: i32) -> Vec { let Ok(data) = std::fs::read(path) else { return b"\n".to_vec(); }; if data.is_empty() { return Vec::new(); } let start = data.len().saturating_sub(TAIL_BYTES); let window = &data[start..]; let mut newlines = 0; let mut cut = 0; for (index, byte) in window.iter().enumerate().rev() { if *byte == b'\n' { newlines += 1; if newlines > max_lines { cut = index + 1; break; } } } window[cut..].to_vec() } fn append_path_head( out: &mut Vec, path: &std::path::Path, bytes: usize, lines: i32, head: &[u8], ) { let shown = path.display(); out.extend_from_slice( format!("output_path={shown} ({bytes} bytes, {lines} lines)\n").as_bytes(), ); out.extend_from_slice(format!("\n").as_bytes()); out.extend_from_slice(head); if !head.is_empty() && !head.ends_with(b"\n") { out.push(b'\n'); } out.extend_from_slice(b"\n"); }