use crate::timestamps::{until_server_bound, server_ts};
use crate::Args;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::process::{Child, ChildStdout, Command, Stdio, exit};
pub(crate) enum Source {
Lines(Box<dyn BufRead>),
Rows(csv::Reader<ChildStdout>),
}
pub(crate) fn spawn_fetcher(tool: &'static str, argv: Vec<String>) -> (ChildStdout, Child) {
let spawn = |program: &str| {
let mut cmd = Command::new(program);
cmd.args(&argv).stdout(Stdio::piped()).stderr(Stdio::inherit());
#[cfg(unix)]
std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
cmd.spawn()
};
let mut child = match spawn(tool) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
#[cfg(windows)]
let retried = [".cmd", ".bat"].iter().find_map(|ext| spawn(&format!("{tool}{ext}")).ok());
#[cfg(not(windows))]
let retried: Option<Child> = None;
let Some(child) = retried else {
eprintln!("Failed to execute {}: {}. Is the {} CLI installed?", tool, e, tool);
exit(1);
};
child
}
Err(e) => {
eprintln!("Failed to execute {}: {}. Is the {} CLI installed?", tool, e, tool);
exit(1);
}
};
let stdout: ChildStdout = child.stdout.take().expect("child stdout");
(stdout, child)
}
pub(crate) fn kill_fetcher(child: &mut Child) {
let pid = child.id();
#[cfg(unix)]
let _ = Command::new("kill").arg("-9").arg(format!("-{pid}")).status();
let _ = child.kill();
}
pub(crate) fn spawn_watchdog(child: &std::sync::Arc<std::sync::Mutex<Child>>, secs: u64) {
if secs == 0 {
return;
}
let watchdog = std::sync::Arc::clone(child);
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(secs));
let Ok(mut c) = watchdog.lock() else { return };
match c.try_wait() {
Ok(Some(_)) | Err(_) => {}
Ok(None) => {
kill_fetcher(&mut c);
eprintln!("Error: fetcher killed after --timeout {}s (its job id is visible in the output above).", secs);
}
}
});
}
pub(crate) fn collect_fetcher(
child: &std::sync::Arc<std::sync::Mutex<Child>>,
tool: Option<&'static str>,
early_cut: bool,
) -> Result<(), crate::errors::Fail> {
if early_cut {
let mut g = child.lock().unwrap();
kill_fetcher(&mut g);
}
let status;
loop {
let probed = { child.lock().unwrap().try_wait() };
match probed {
Ok(Some(s)) => { status = Some(s); break; }
Ok(None) => std::thread::sleep(std::time::Duration::from_millis(50)),
Err(e) => {
eprintln!("Warning: failed to wait for {}: {}", tool.unwrap_or("fetcher"), e);
status = None;
break;
}
}
}
let Some(status) = status else { return Ok(()) };
if !status.success() && !early_cut {
return Err(crate::errors::Fail::Fetch(format!(
"{} exited with {}", tool.unwrap_or("fetcher"), status)));
}
Ok(())
}
pub(crate) fn make_reader(
args: &Args,
since_valid: &Option<String>,
until_valid: &Option<String>,
) -> (Source, Option<(&'static str, Child)>) {
if let Some(filter) = &args.gcloud {
if since_valid.is_none() {
eprintln!("Warning: no --since — this gcloud call streams the WHOLE matching range, oldest first. Add --since unless you truly want everything.");
}
let mut gc_filter = format!("({})", filter);
if let Some(s) = since_valid {
gc_filter.push_str(&format!(" AND timestamp>=\"{}\"", server_ts(s)));
}
if let Some(next) = until_valid.as_ref().and_then(|u| until_server_bound(u)) {
gc_filter.push_str(&format!(" AND timestamp<\"{}\"", next));
}
let gc_args = vec![
"logging".to_string(), "read".to_string(), gc_filter,
"--format".into(), "value(timestamp,textPayload,jsonPayload.message)".into(),
"--order=asc".into(),
];
let (stdout, child) = spawn_fetcher("gcloud", gc_args);
(Source::Lines(Box::new(BufReader::new(stdout))), Some(("gcloud", child)))
} else if let Some(sql) = &args.bq {
let mut bq_args: Vec<String> = Vec::new();
if let Some(p) = &args.bq_project {
bq_args.push(format!("--project_id={p}"));
}
bq_args.push("--quiet".to_string());
bq_args.push("query".to_string());
bq_args.push("--use_legacy_sql=false".to_string());
bq_args.push("--format=csv".to_string());
bq_args.push("--max_rows=10000000".to_string());
if args.dry_run {
bq_args.push("--dry_run".to_string());
}
if let Some(mb) = args.max_bytes_billed {
bq_args.push(format!("--maximum_bytes_billed={mb}"));
}
if let Some(extra) = &args.bq_args {
bq_args.extend(extra.split_whitespace().map(str::to_string));
}
bq_args.push(sql.clone());
let (stdout, child) = spawn_fetcher("bq", bq_args);
let mut rows = csv::ReaderBuilder::new().flexible(true).from_reader(stdout);
if let Ok(h) = rows.headers() {
eprintln!("Note: bq columns: {}", h.iter().collect::<Vec<_>>().join(" | "));
}
(Source::Rows(rows), Some(("bq", child)))
} else if let Some(selector) = &args.k8s {
if since_valid.is_none() {
eprintln!("Warning: no --since — this kubectl logs call streams from pod creation. Add --since unless you want everything.");
}
let mut k8s_args: Vec<String> = vec!["logs".into(), selector.clone()];
k8s_args.push("--timestamps".into());
if let Some(c) = &args.k8s_container {
k8s_args.push("-c".into());
k8s_args.push(c.clone());
}
if let Some(s) = since_valid {
k8s_args.push(format!("--since-time={}Z", server_ts(s)));
}
let (stdout, child) = spawn_fetcher("kubectl", k8s_args);
(Source::Lines(Box::new(BufReader::new(stdout))), Some(("kubectl", child)))
} else if let Some(path) = &args.file_path {
let file = File::open(path).unwrap_or_else(|e| {
eprintln!("Error opening file {}: {}", path, e);
exit(1);
});
(Source::Lines(Box::new(BufReader::new(file))), None)
} else {
(Source::Lines(Box::new(BufReader::new(io::stdin()))), None)
}
}