use crate::docker_cleanup::docker_command;
use crate::execution_control::CommandRunOutput;
use crate::execution_store::ExecutionRecord;
use regex::Regex;
use std::cell::RefCell;
use std::collections::HashMap;
use std::process::Command;
use std::sync::OnceLock;
pub const BATCH_FIELDS: [&str; 8] = [
".Name",
".Id",
".State.Pid",
".State.Running",
".State.ExitCode",
".State.OOMKilled",
".State.StartedAt",
".State.FinishedAt",
];
pub type BatchRow = HashMap<String, String>;
pub type BatchAnswers = HashMap<String, Option<BatchRow>>;
thread_local! {
static PREFETCHED: RefCell<Option<BatchAnswers>> = const { RefCell::new(None) };
}
pub fn batch_format() -> String {
BATCH_FIELDS
.iter()
.map(|field| format!("{{{{{}}}}}", field))
.collect::<Vec<_>>()
.join(" ")
}
fn not_found_line() -> &'static Regex {
static PATTERN: OnceLock<Regex> = OnceLock::new();
PATTERN.get_or_init(|| Regex::new(r"(?i)no such (?:object|container):\s*(\S+)").unwrap())
}
fn placeholder() -> &'static Regex {
static PATTERN: OnceLock<Regex> = OnceLock::new();
PATTERN.get_or_init(|| Regex::new(r"\{\{\s*(\.[A-Za-z.]+)\s*\}\}").unwrap())
}
pub fn docker_probe_targets(records: &[ExecutionRecord]) -> Vec<String> {
let mut targets: Vec<String> = Vec::new();
for record in records {
let isolated = record.options.get("isolated").and_then(|v| v.as_str());
if isolated != Some("docker")
|| record.container_state.as_deref()
== Some(crate::container_removal::CONTAINER_STATE_REMOVED)
{
continue;
}
for key in ["sessionName", "containerId"] {
if let Some(target) = record.options.get(key).and_then(|v| v.as_str()) {
if !target.is_empty() && !targets.iter().any(|t| t == target) {
targets.push(target.to_string());
}
}
}
}
targets
}
pub fn parse_batch_inspect(targets: &[String], stdout: &str, stderr: &str) -> BatchAnswers {
let rows: Vec<BatchRow> = stdout
.lines()
.map(|line| line.split_whitespace().collect::<Vec<_>>())
.filter(|parts| parts.len() == BATCH_FIELDS.len())
.map(|parts| {
BATCH_FIELDS
.iter()
.zip(parts)
.map(|(field, value)| (field.to_string(), value.to_string()))
.collect()
})
.collect();
let missing: Vec<&str> = not_found_line()
.captures_iter(stderr)
.filter_map(|caps| caps.get(1).map(|m| m.as_str()))
.collect();
let mut answers = BatchAnswers::new();
for target in targets {
if missing.contains(&target.as_str()) {
answers.insert(target.clone(), None);
continue;
}
let name = format!("/{}", target);
let row = rows
.iter()
.find(|row| row[".Name"] == name)
.or_else(|| rows.iter().find(|row| row[".Id"].starts_with(target)));
if let Some(row) = row {
answers.insert(target.clone(), Some(row.clone()));
}
}
answers
}
fn run_batch_inspect(targets: &[String]) -> BatchAnswers {
if targets.is_empty() {
return BatchAnswers::new();
}
let output = Command::new(docker_command())
.args(["inspect", "--type", "container", "-f", &batch_format()])
.args(targets)
.output();
match output {
Ok(output) => parse_batch_inspect(
targets,
&String::from_utf8_lossy(&output.stdout),
&String::from_utf8_lossy(&output.stderr),
),
Err(_) => BatchAnswers::new(),
}
}
struct PrefetchGuard;
impl Drop for PrefetchGuard {
fn drop(&mut self) {
PREFETCHED.with(|cell| cell.borrow_mut().take());
}
}
pub fn with_docker_inspect_batch<T>(records: &[ExecutionRecord], query: impl FnOnce() -> T) -> T {
if PREFETCHED.with(|cell| cell.borrow().is_some()) {
return query();
}
let answers = run_batch_inspect(&docker_probe_targets(records));
PREFETCHED.with(|cell| *cell.borrow_mut() = Some(answers));
let _guard = PrefetchGuard;
query()
}
pub fn prefetched_inspect(format: &str, target: &str) -> Option<CommandRunOutput> {
let row = PREFETCHED.with(|cell| cell.borrow().as_ref()?.get(target).cloned())?;
let Some(row) = row else {
return Some(CommandRunOutput {
success: false,
stdout: String::new(),
stderr: format!(
"Error response from daemon: No such container: {}\n",
target
),
status: Some(1),
error: None,
});
};
let mut known = true;
let stdout =
placeholder().replace_all(format, |caps: ®ex::Captures| match row.get(&caps[1]) {
Some(value) => value.clone(),
None => {
known = false;
String::new()
}
});
if !known || stdout.contains("{{") {
return None;
}
Some(CommandRunOutput {
success: true,
stdout: format!("{}\n", stdout),
stderr: String::new(),
status: Some(0),
error: None,
})
}