1use std::{
2 collections::BTreeMap,
3 fs,
4 path::{Path, PathBuf},
5};
6
7use chrono::Utc;
8
9use crate::types::{
10 ExecutionCapabilities, NamespaceCapabilities, ResourceCapabilities, ResourceCapacity,
11 RuntimeCapabilities, RuntimePlatform, SandboxCapabilities, StorageCapabilities,
12};
13
14#[derive(Debug, Clone)]
15pub struct CapabilityProbeInput {
16 pub runtime_id: String,
17 pub data_dir: PathBuf,
18 pub cgroup_root: PathBuf,
19 pub max_running_tasks: usize,
20 pub disable_linux_sandbox: bool,
21 pub disable_cgroup: bool,
22 pub capacity_memory_bytes: Option<u64>,
23 pub capacity_pids: Option<u64>,
24}
25
26pub fn probe_runtime_capabilities(input: &CapabilityProbeInput) -> RuntimeCapabilities {
27 let mut warnings = Vec::new();
28 let mut overrides = BTreeMap::new();
29
30 if input.disable_linux_sandbox {
31 overrides.insert("linux_sandbox".into(), "disabled".into());
32 }
33 if input.disable_cgroup {
34 overrides.insert("cgroup".into(), "disabled".into());
35 }
36 if let Some(value) = input.capacity_memory_bytes {
37 overrides.insert("capacity_memory_bytes".into(), value.to_string());
38 }
39 if let Some(value) = input.capacity_pids {
40 overrides.insert("capacity_pids".into(), value.to_string());
41 }
42
43 let data_dir_writable = data_dir_writable(&input.data_dir);
44 if !data_dir_writable {
45 warnings.push(format!(
46 "data-dir is not writable: {}",
47 input.data_dir.to_string_lossy()
48 ));
49 }
50
51 let platform = RuntimePlatform {
52 os: std::env::consts::OS.to_string(),
53 arch: std::env::consts::ARCH.to_string(),
54 containerized: detect_containerized(),
55 kubernetes: detect_kubernetes(),
56 };
57
58 let linux = cfg!(target_os = "linux");
59 let root_user = current_euid() == Some(0);
60 let linux_sandbox = linux && root_user && !input.disable_linux_sandbox;
61 if !linux_sandbox {
62 warnings.push(if linux {
63 "linux_sandbox is unavailable without root-equivalent namespace permissions".into()
64 } else {
65 "linux_sandbox is unavailable on this host".into()
66 });
67 }
68
69 let cgroup_v2 = linux && Path::new("/sys/fs/cgroup/cgroup.controllers").exists();
70 let cgroup_writable =
71 cgroup_v2 && !input.disable_cgroup && path_likely_writable(&input.cgroup_root);
72 if cgroup_v2 && !cgroup_writable {
73 warnings.push(format!(
74 "cgroup v2 detected but cgroup root is not writable: {}",
75 input.cgroup_root.to_string_lossy()
76 ));
77 }
78
79 let memory_capacity = input.capacity_memory_bytes.or_else(detect_memory_bytes);
80 let pids_capacity = input.capacity_pids.or_else(detect_pids_capacity);
81
82 let resources = ResourceCapabilities {
83 rlimit_cpu: cfg!(unix),
84 rlimit_memory: cfg!(unix),
85 cgroup_v2,
86 cgroup_writable,
87 memory_limit: cfg!(unix),
88 pids_limit: cgroup_writable,
89 oom_detection: cgroup_writable,
90 cpu_quota: false,
91 ledger: true,
92 capacity: ResourceCapacity {
93 task_slots: input.max_running_tasks as u64,
94 memory_bytes: memory_capacity,
95 pids: pids_capacity,
96 },
97 };
98
99 RuntimeCapabilities {
100 runtime_id: input.runtime_id.clone(),
101 snapshot_version: RuntimeCapabilities::snapshot_version().to_string(),
102 collected_at: Utc::now(),
103 platform,
104 execution: ExecutionCapabilities {
105 command: true,
106 script: true,
107 process_group: cfg!(unix),
108 },
109 sandbox: SandboxCapabilities {
110 process: true,
111 linux_sandbox,
112 chroot: linux_sandbox,
113 namespaces: NamespaceCapabilities {
114 mount: linux_sandbox,
115 pid: linux_sandbox,
116 uts: linux_sandbox,
117 ipc: linux_sandbox,
118 net: linux_sandbox,
119 },
120 },
121 storage: StorageCapabilities { data_dir_writable },
122 resources,
123 stable_semantics: stable_semantics(),
124 enhanced_semantics: enhanced_semantics(linux_sandbox, cgroup_writable),
125 degraded: !warnings.is_empty(),
126 warnings,
127 overrides,
128 }
129}
130
131fn stable_semantics() -> Vec<String> {
132 [
133 "submit",
134 "status",
135 "events",
136 "stdout_stderr",
137 "timeout",
138 "kill",
139 "artifacts",
140 "result_persistence",
141 "recovery",
142 ]
143 .into_iter()
144 .map(str::to_string)
145 .collect()
146}
147
148fn enhanced_semantics(linux_sandbox: bool, cgroup_writable: bool) -> Vec<String> {
149 let mut items = vec!["resource_ledger".to_string()];
150 if linux_sandbox {
151 items.extend([
152 "linux_sandbox".to_string(),
153 "namespaces".to_string(),
154 "chroot".to_string(),
155 ]);
156 }
157 if cgroup_writable {
158 items.extend([
159 "cgroup_memory".to_string(),
160 "cgroup_pids".to_string(),
161 "oom_detection".to_string(),
162 ]);
163 }
164 items
165}
166
167fn data_dir_writable(path: &Path) -> bool {
168 let probe_path = path.join(format!(".execgo-runtime-probe-{}", std::process::id()));
169 match fs::write(&probe_path, b"probe") {
170 Ok(()) => {
171 let _ = fs::remove_file(probe_path);
172 true
173 }
174 Err(_) => false,
175 }
176}
177
178fn path_likely_writable(path: &Path) -> bool {
179 let candidate = if path.exists() {
180 path
181 } else {
182 path.parent().unwrap_or(path)
183 };
184 fs::metadata(candidate)
185 .map(|metadata| !metadata.permissions().readonly())
186 .unwrap_or(false)
187}
188
189fn detect_containerized() -> bool {
190 Path::new("/.dockerenv").exists()
191 || Path::new("/run/.containerenv").exists()
192 || read_to_string("/proc/1/cgroup").is_some_and(|contents| {
193 contents.contains("docker")
194 || contents.contains("containerd")
195 || contents.contains("kubepods")
196 })
197}
198
199fn detect_kubernetes() -> bool {
200 std::env::var_os("KUBERNETES_SERVICE_HOST").is_some()
201 || read_to_string("/proc/1/cgroup").is_some_and(|contents| contents.contains("kubepods"))
202}
203
204fn detect_memory_bytes() -> Option<u64> {
205 #[cfg(unix)]
206 unsafe {
207 let pages = libc::sysconf(libc::_SC_PHYS_PAGES);
208 let page_size = libc::sysconf(libc::_SC_PAGESIZE);
209 if pages > 0 && page_size > 0 {
210 return Some((pages as u64).saturating_mul(page_size as u64));
211 }
212 }
213 None
214}
215
216fn detect_pids_capacity() -> Option<u64> {
217 #[cfg(target_os = "linux")]
218 {
219 read_to_string("/proc/sys/kernel/pid_max")
220 .and_then(|value| value.trim().parse::<u64>().ok())
221 }
222 #[cfg(not(target_os = "linux"))]
223 {
224 None
225 }
226}
227
228fn current_euid() -> Option<u32> {
229 #[cfg(unix)]
230 unsafe {
231 Some(libc::geteuid())
232 }
233 #[cfg(not(unix))]
234 {
235 None
236 }
237}
238
239fn read_to_string(path: &str) -> Option<String> {
240 fs::read_to_string(path).ok()
241}
242
243#[cfg(test)]
244mod tests {
245 use tempfile::TempDir;
246
247 use super::*;
248
249 #[test]
250 fn probe_returns_stable_shape() {
251 let temp = TempDir::new().expect("tempdir");
252 let capabilities = probe_runtime_capabilities(&CapabilityProbeInput {
253 runtime_id: "test-runtime".into(),
254 data_dir: temp.path().to_path_buf(),
255 cgroup_root: temp.path().join("cgroup"),
256 max_running_tasks: 3,
257 disable_linux_sandbox: true,
258 disable_cgroup: true,
259 capacity_memory_bytes: Some(1024),
260 capacity_pids: Some(64),
261 });
262
263 assert_eq!(capabilities.runtime_id, "test-runtime");
264 assert_eq!(capabilities.resources.capacity.task_slots, 3);
265 assert_eq!(capabilities.resources.capacity.memory_bytes, Some(1024));
266 assert!(!capabilities.sandbox.linux_sandbox);
267 assert_eq!(
268 capabilities
269 .overrides
270 .get("linux_sandbox")
271 .map(String::as_str),
272 Some("disabled")
273 );
274 }
275}