start_command/
docker_inspect_batch.rs1use crate::docker_cleanup::docker_command;
17use crate::execution_control::CommandRunOutput;
18use crate::execution_store::ExecutionRecord;
19use regex::Regex;
20use std::cell::RefCell;
21use std::collections::HashMap;
22use std::process::Command;
23use std::sync::OnceLock;
24
25pub const BATCH_FIELDS: [&str; 8] = [
27 ".Name",
28 ".Id",
29 ".State.Pid",
30 ".State.Running",
31 ".State.ExitCode",
32 ".State.OOMKilled",
33 ".State.StartedAt",
34 ".State.FinishedAt",
35];
36
37pub type BatchRow = HashMap<String, String>;
39
40pub type BatchAnswers = HashMap<String, Option<BatchRow>>;
42
43thread_local! {
44 static PREFETCHED: RefCell<Option<BatchAnswers>> = const { RefCell::new(None) };
46}
47
48pub fn batch_format() -> String {
50 BATCH_FIELDS
51 .iter()
52 .map(|field| format!("{{{{{}}}}}", field))
53 .collect::<Vec<_>>()
54 .join(" ")
55}
56
57fn not_found_line() -> &'static Regex {
58 static PATTERN: OnceLock<Regex> = OnceLock::new();
59 PATTERN.get_or_init(|| Regex::new(r"(?i)no such (?:object|container):\s*(\S+)").unwrap())
60}
61
62fn placeholder() -> &'static Regex {
63 static PATTERN: OnceLock<Regex> = OnceLock::new();
64 PATTERN.get_or_init(|| Regex::new(r"\{\{\s*(\.[A-Za-z.]+)\s*\}\}").unwrap())
65}
66
67pub fn docker_probe_targets(records: &[ExecutionRecord]) -> Vec<String> {
70 let mut targets: Vec<String> = Vec::new();
71 for record in records {
72 let isolated = record.options.get("isolated").and_then(|v| v.as_str());
73 if isolated != Some("docker")
74 || record.container_state.as_deref()
75 == Some(crate::container_removal::CONTAINER_STATE_REMOVED)
76 {
77 continue;
78 }
79 for key in ["sessionName", "containerId"] {
80 if let Some(target) = record.options.get(key).and_then(|v| v.as_str()) {
81 if !target.is_empty() && !targets.iter().any(|t| t == target) {
82 targets.push(target.to_string());
83 }
84 }
85 }
86 }
87 targets
88}
89
90pub fn parse_batch_inspect(targets: &[String], stdout: &str, stderr: &str) -> BatchAnswers {
93 let rows: Vec<BatchRow> = stdout
94 .lines()
95 .map(|line| line.split_whitespace().collect::<Vec<_>>())
96 .filter(|parts| parts.len() == BATCH_FIELDS.len())
97 .map(|parts| {
98 BATCH_FIELDS
99 .iter()
100 .zip(parts)
101 .map(|(field, value)| (field.to_string(), value.to_string()))
102 .collect()
103 })
104 .collect();
105 let missing: Vec<&str> = not_found_line()
106 .captures_iter(stderr)
107 .filter_map(|caps| caps.get(1).map(|m| m.as_str()))
108 .collect();
109 let mut answers = BatchAnswers::new();
110 for target in targets {
111 if missing.contains(&target.as_str()) {
114 answers.insert(target.clone(), None);
115 continue;
116 }
117 let name = format!("/{}", target);
118 let row = rows
119 .iter()
120 .find(|row| row[".Name"] == name)
121 .or_else(|| rows.iter().find(|row| row[".Id"].starts_with(target)));
122 if let Some(row) = row {
123 answers.insert(target.clone(), Some(row.clone()));
124 }
125 }
126 answers
127}
128
129fn run_batch_inspect(targets: &[String]) -> BatchAnswers {
130 if targets.is_empty() {
131 return BatchAnswers::new();
132 }
133 let output = Command::new(docker_command())
134 .args(["inspect", "--type", "container", "-f", &batch_format()])
135 .args(targets)
136 .output();
137 match output {
138 Ok(output) => parse_batch_inspect(
139 targets,
140 &String::from_utf8_lossy(&output.stdout),
141 &String::from_utf8_lossy(&output.stderr),
142 ),
143 Err(_) => BatchAnswers::new(),
144 }
145}
146
147struct PrefetchGuard;
149
150impl Drop for PrefetchGuard {
151 fn drop(&mut self) {
152 PREFETCHED.with(|cell| cell.borrow_mut().take());
153 }
154}
155
156pub fn with_docker_inspect_batch<T>(records: &[ExecutionRecord], query: impl FnOnce() -> T) -> T {
159 if PREFETCHED.with(|cell| cell.borrow().is_some()) {
160 return query();
161 }
162 let answers = run_batch_inspect(&docker_probe_targets(records));
163 PREFETCHED.with(|cell| *cell.borrow_mut() = Some(answers));
164 let _guard = PrefetchGuard;
165 query()
166}
167
168pub fn prefetched_inspect(format: &str, target: &str) -> Option<CommandRunOutput> {
171 let row = PREFETCHED.with(|cell| cell.borrow().as_ref()?.get(target).cloned())?;
172 let Some(row) = row else {
173 return Some(CommandRunOutput {
174 success: false,
175 stdout: String::new(),
176 stderr: format!(
177 "Error response from daemon: No such container: {}\n",
178 target
179 ),
180 status: Some(1),
181 error: None,
182 });
183 };
184 let mut known = true;
185 let stdout =
186 placeholder().replace_all(format, |caps: ®ex::Captures| match row.get(&caps[1]) {
187 Some(value) => value.clone(),
188 None => {
189 known = false;
190 String::new()
191 }
192 });
193 if !known || stdout.contains("{{") {
194 return None;
195 }
196 Some(CommandRunOutput {
197 success: true,
198 stdout: format!("{}\n", stdout),
199 stderr: String::new(),
200 status: Some(0),
201 error: None,
202 })
203}