byteor_runtime/
doctor.rs

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/// One doctor/preflight check.
13#[derive(Debug, Serialize)]
14pub struct Check {
15    /// Stable check name.
16    pub name: &'static str,
17    /// Whether the check passed.
18    pub ok: bool,
19    /// Machine-readable details.
20    pub details: serde_json::Value,
21}
22
23/// Doctor/preflight report.
24#[derive(Debug, Serialize)]
25pub struct Report {
26    /// Whether all checks passed.
27    pub ok: bool,
28    /// Selected tuning profile.
29    pub profile: String,
30    /// Effective tuning derived from the profile and CLI overrides.
31    pub tuning: EffectiveTuningReport,
32    /// Per-check results.
33    pub checks: Vec<Check>,
34}
35
36/// Print a human-friendly summary of checks to stderr.
37pub 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
310/// Run `doctor` / preflight checks.
311pub 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}