1use std::path::{Path, PathBuf};
2
3use serde::Serialize;
4
5use crate::{
6 effective_tuning_report, profile_defaults, EffectiveTuningReport, MlockallPreset,
7 ResolvedTuning, TuningProfile,
8};
9use crate::host::{probe_shm_dir_writable, shm_runtime_context};
10use crate::tuning::refresh_effective_tuning_rollup;
11
12#[derive(Debug, Serialize)]
14pub struct Check {
15 pub name: &'static str,
17 pub ok: bool,
19 pub details: serde_json::Value,
21}
22
23#[derive(Debug, Serialize)]
25pub struct Report {
26 pub ok: bool,
28 pub profile: String,
30 pub tuning: EffectiveTuningReport,
32 pub checks: Vec<Check>,
34}
35
36pub fn eprint_human(checks: &[Check]) {
38 for c in checks {
39 if c.ok {
40 eprintln!("ok {}", c.name);
41 } else {
42 eprintln!("FAIL {}", c.name);
43 }
44 }
45}
46
47fn read_proc_status_field(name: &str) -> Option<String> {
48 let s = std::fs::read_to_string("/proc/self/status").ok()?;
49 for line in s.lines() {
50 if let Some(rest) = line.strip_prefix(&format!("{name}:")) {
51 return Some(rest.trim().to_string());
52 }
53 }
54 None
55}
56
57fn cpu_pinning_check() -> Check {
58 let allowed = read_proc_status_field("Cpus_allowed_list");
59 let ok = allowed.as_deref().map(|s| !s.is_empty()).unwrap_or(false);
60 Check {
61 name: "cpu_pinning",
62 ok,
63 details: serde_json::json!({
64 "cpus_allowed_list": allowed,
65 }),
66 }
67}
68
69fn parse_cap_eff_hex(s: &str) -> Option<u64> {
70 u64::from_str_radix(s.trim(), 16).ok()
71}
72
73fn has_cap_ipc_lock() -> Option<bool> {
74 let cap_eff = read_proc_status_field("CapEff")?;
75 let n = parse_cap_eff_hex(&cap_eff)?;
76 Some((n & (1u64 << 14)) != 0)
77}
78
79fn has_cap_sys_nice() -> Option<bool> {
80 let cap_eff = read_proc_status_field("CapEff")?;
81 let n = parse_cap_eff_hex(&cap_eff)?;
82 Some((n & (1u64 << 23)) != 0)
83}
84
85fn proc_limits_soft_value_bytes(name_prefix: &str) -> Option<u64> {
86 let limits = std::fs::read_to_string("/proc/self/limits").ok()?;
87 for line in limits.lines() {
88 if !line.starts_with(name_prefix) {
89 continue;
90 }
91 let parts: Vec<&str> = line.split_whitespace().collect();
92 if parts.len() < 4 {
93 return None;
94 }
95 let soft = parts[parts.len().saturating_sub(3)];
96 if soft == "unlimited" {
97 return None;
98 }
99 let n = soft.parse::<u64>().ok()?;
100 let units = parts[parts.len().saturating_sub(1)];
101 match units {
102 "bytes" => return Some(n),
103 "kbytes" => return Some(n.saturating_mul(1024)),
104 _ => return None,
105 }
106 }
107 None
108}
109
110fn proc_limits_soft_value_u64(name_prefix: &str) -> Option<u64> {
111 let limits = std::fs::read_to_string("/proc/self/limits").ok()?;
112 for line in limits.lines() {
113 if !line.starts_with(name_prefix) {
114 continue;
115 }
116 let parts: Vec<&str> = line.split_whitespace().collect();
117 if parts.len() < 4 {
118 return None;
119 }
120 let soft = parts[parts.len().saturating_sub(3)];
121 if soft == "unlimited" {
122 return None;
123 }
124 return soft.parse::<u64>().ok();
125 }
126 None
127}
128
129fn mlockall_availability_check(mlockall_required: bool) -> Check {
130 mlockall_availability_check_from_state(
131 mlockall_required,
132 has_cap_ipc_lock(),
133 proc_limits_soft_value_bytes("Max locked memory"),
134 )
135}
136
137pub(crate) fn mlockall_availability_check_from_state(
138 mlockall_required: bool,
139 cap_ipc_lock: Option<bool>,
140 max_locked_bytes: Option<u64>,
141) -> Check {
142 let ok = if !mlockall_required {
143 true
144 } else {
145 let cap_ok = cap_ipc_lock.unwrap_or(false);
146 let rlimit_ok = max_locked_bytes.unwrap_or(0) > 0;
147 cap_ok || rlimit_ok
148 };
149
150 Check {
151 name: "mlockall",
152 ok,
153 details: serde_json::json!({
154 "required": mlockall_required,
155 "cap_ipc_lock": cap_ipc_lock,
156 "max_locked_bytes": max_locked_bytes,
157 }),
158 }
159}
160
161fn rt_scheduling_capability_check() -> Check {
162 rt_scheduling_capability_check_from_state(
163 has_cap_sys_nice(),
164 proc_limits_soft_value_u64("Max realtime priority"),
165 )
166}
167
168pub(crate) fn rt_scheduling_capability_check_from_state(
169 cap_sys_nice: Option<bool>,
170 max_rt_prio: Option<u64>,
171) -> Check {
172 let ok = cap_sys_nice.unwrap_or(false) || max_rt_prio.unwrap_or(0) > 0;
173
174 Check {
175 name: "rt_scheduling",
176 ok,
177 details: serde_json::json!({
178 "cap_sys_nice": cap_sys_nice,
179 "max_realtime_priority": max_rt_prio,
180 }),
181 }
182}
183
184pub(crate) fn annotate_tuning_report_from_checks(
185 mut tuning: EffectiveTuningReport,
186 checks: &[Check],
187) -> EffectiveTuningReport {
188 for check in checks {
189 if let Some(observations) = tuning.host_observations.as_mut() {
190 match check.name {
191 "cpu_pinning" => {
192 observations.cpu_pinning_supported = Some(check.ok);
193 observations.observed_cpu_set = check
194 .details
195 .get("cpus_allowed_list")
196 .and_then(serde_json::Value::as_str)
197 .map(ToOwned::to_owned);
198 }
199 "mlockall" => {
200 observations.mlockall_supported = Some(check.ok);
201 }
202 "rt_scheduling" => {
203 observations.realtime_scheduler_supported = Some(check.ok);
204 }
205 _ => {}
206 }
207 }
208
209 if check.ok {
210 continue;
211 }
212
213 match check.name {
214 "cpu_pinning" if tuning.requested.pinning_mode.is_some() => {
215 tuning.applied.pinning_mode = None;
216 tuning.degraded.pinning_mode = tuning.requested.pinning_mode.clone();
217 push_failure_reason(
218 &mut tuning.failure_reasons,
219 "pinning requested but host readiness checks did not confirm a usable CPU affinity mask"
220 .to_string(),
221 );
222 }
223 "mlockall" if tuning.requested.mlockall == Some(true) => {
224 tuning.applied.mlockall = None;
225 tuning.degraded.mlockall = Some(true);
226 push_failure_reason(
227 &mut tuning.failure_reasons,
228 "mlockall requested but host readiness checks did not confirm CAP_IPC_LOCK or a sufficient memlock rlimit"
229 .to_string(),
230 );
231 }
232 "shm_dir" => {
233 tuning.applied.shm_parent = None;
234 tuning.degraded.shm_parent = tuning.requested.shm_parent.clone();
235 let detail = check
236 .details
237 .get("write_err")
238 .and_then(serde_json::Value::as_str)
239 .or_else(|| check.details.get("path").and_then(serde_json::Value::as_str))
240 .unwrap_or("shm_dir check failed");
241 push_failure_reason(
242 &mut tuning.failure_reasons,
243 format!("shm_parent requested but host readiness checks failed: {detail}"),
244 );
245 }
246 "rt_scheduling" if tuning.requested.sched_preset.is_some() => {
247 tuning.applied.sched_preset = None;
248 tuning.applied.rt_prio = None;
249 tuning.degraded.sched_preset = tuning.requested.sched_preset.clone();
250 tuning.degraded.rt_prio = tuning.requested.rt_prio;
251 push_failure_reason(
252 &mut tuning.failure_reasons,
253 "realtime scheduling requested but host readiness checks did not confirm CAP_SYS_NICE or a sufficient realtime priority rlimit"
254 .to_string(),
255 );
256 }
257 _ => {}
258 }
259 }
260
261 refresh_effective_tuning_rollup(&mut tuning);
262
263 tuning
264}
265
266fn push_failure_reason(reasons: &mut Vec<String>, reason: String) {
267 if !reasons.iter().any(|existing| existing == &reason) {
268 reasons.push(reason);
269 }
270}
271
272fn shm_dir_check(shm_parent: &Path) -> Check {
273 let mut ok = true;
274 let context = shm_runtime_context(shm_parent);
275 let mut detail = serde_json::json!({
276 "path": context.path,
277 "fstype": context.mount_fstype,
278 "hugetlbfs": context.hugetlbfs,
279 });
280
281 if !shm_parent.exists() {
282 ok = false;
283 detail["exists"] = serde_json::json!(false);
284 } else {
285 detail["exists"] = serde_json::json!(true);
286 detail["is_dir"] = serde_json::json!(shm_parent.is_dir());
287 if !shm_parent.is_dir() {
288 ok = false;
289 }
290
291 match probe_shm_dir_writable(shm_parent) {
292 Ok(()) => {
293 detail["writable"] = serde_json::json!(true);
294 }
295 Err(e) => {
296 ok = false;
297 detail["writable"] = serde_json::json!(false);
298 detail["write_err"] = serde_json::json!(e.to_string());
299 }
300 }
301 }
302
303 Check {
304 name: "shm_dir",
305 ok,
306 details: detail,
307 }
308}
309
310pub fn doctor(mlockall_required: bool, shm_parent: PathBuf) -> Report {
312 let mut tuning = profile_defaults(TuningProfile::Default, shm_parent);
313 tuning.mlockall = if mlockall_required {
314 MlockallPreset::On
315 } else {
316 MlockallPreset::None
317 };
318 doctor_with_tuning(&tuning)
319}
320
321pub fn doctor_with_tuning(tuning: &ResolvedTuning) -> Report {
322 let mut checks = Vec::new();
323 checks.push(cpu_pinning_check());
324 checks.push(mlockall_availability_check(
325 tuning.mlockall == MlockallPreset::On,
326 ));
327 checks.push(shm_dir_check(&tuning.shm_parent));
328
329 #[cfg(target_os = "linux")]
330 {
331 let preflight =
332 byteor_ops::preflight_linux_minimal(&tuning.shm_parent, tuning.mlockall == MlockallPreset::On);
333 match preflight {
334 Ok(r) => {
335 checks.push(Check {
336 name: "linux_preflight",
337 ok: r.is_ok(),
338 details: serde_json::json!({
339 "observations": r.observations,
340 "warnings": r.warnings,
341 "errors": r.errors,
342 "strict": tuning.mlockall == MlockallPreset::On,
343 }),
344 });
345 }
346 Err(e) => {
347 checks.push(Check {
348 name: "linux_preflight",
349 ok: false,
350 details: serde_json::json!({
351 "error": e.to_string(),
352 "strict": tuning.mlockall == MlockallPreset::On,
353 }),
354 });
355 }
356 }
357
358 checks.push(rt_scheduling_capability_check());
359 }
360
361 let ok = checks.iter().all(|c| c.ok);
362 let tuning = annotate_tuning_report_from_checks(effective_tuning_report(tuning.profile, tuning), &checks);
363 Report {
364 ok,
365 profile: tuning.profile.as_str().to_string(),
366 tuning,
367 checks,
368 }
369}