1#[cfg(target_os = "linux")]
4use std::{
5 collections::{BTreeMap, HashMap},
6 fs, io,
7 num::NonZeroUsize,
8 path::{Path, PathBuf},
9 thread::available_parallelism,
10};
11
12#[cfg(target_os = "linux")]
13use color_eyre::eyre::{eyre, Report};
14#[cfg(target_os = "linux")]
15use nix::unistd::{access, AccessFlags};
16#[cfg(target_os = "linux")]
17use std::os::unix::fs::MetadataExt;
18#[cfg(target_os = "linux")]
19use tracing::warn;
20#[cfg(target_os = "linux")]
21use zebra_chain::parameters::{Network, NetworkKind};
22
23#[cfg(target_os = "linux")]
24use super::{
25 datadir::resolve_zcashd_conf_path,
26 effective_zcashd_datadir, effective_zcashd_source, ensure_zcashd_datadir,
27 is_command_resolvable,
28 managed::{cached_managed_zcashd_binary_is_present, managed_zcashd_binary_path},
29 resolve_zcashd_datadir_path, ZcashdBinarySource,
30};
31use crate::config::ZebradConfig;
32
33#[cfg(target_os = "linux")]
34const GIB: u64 = 1024 * 1024 * 1024;
35#[cfg(target_os = "linux")]
36const TIB: u64 = 1024 * GIB;
37
38#[cfg(target_os = "linux")]
39const MIN_CPU_LOGICAL: usize = 4;
40#[cfg(target_os = "linux")]
41const RECOMMENDED_CPU_LOGICAL: usize = 8;
42
43#[cfg(target_os = "linux")]
44const MIN_RAM_BYTES: u64 = 16 * GIB;
45#[cfg(target_os = "linux")]
46const RECOMMENDED_RAM_BYTES: u64 = 32 * GIB;
47
48#[cfg(target_os = "linux")]
49const MAINNET_MIN_ZEBRA_PROVISIONED_BYTES: u64 = 275 * GIB;
50#[cfg(target_os = "linux")]
51const MAINNET_MIN_ZCASHD_PROVISIONED_BYTES: u64 = 275 * GIB;
52#[cfg(target_os = "linux")]
53const MAINNET_RECOMMENDED_COMBINED_TOTAL_BYTES: u64 = TIB;
54
55#[cfg(target_os = "linux")]
56const TESTNET_MIN_ZEBRA_PROVISIONED_BYTES: u64 = 30 * GIB;
57#[cfg(target_os = "linux")]
58const TESTNET_MIN_ZCASHD_PROVISIONED_BYTES: u64 = 30 * GIB;
59#[cfg(target_os = "linux")]
60const TESTNET_RECOMMENDED_COMBINED_TOTAL_BYTES: u64 = 100 * GIB;
61
62pub fn run_preflight(
67 config: &ZebradConfig,
68 unsafe_low_specs: bool,
69) -> Result<(), color_eyre::eyre::Report> {
70 #[cfg(target_os = "linux")]
71 {
72 run_linux_preflight(config, unsafe_low_specs)
73 }
74
75 #[cfg(not(target_os = "linux"))]
76 {
77 let _ = (config, unsafe_low_specs);
78 let message = "zcashd-compat mode is supported on Linux only";
79
80 if unsafe_low_specs {
81 tracing::warn!(
82 "{message}. continuing because --unsafe-low-specs was explicitly provided"
83 );
84 Ok(())
85 } else {
86 Err(color_eyre::eyre::eyre!(message))
87 }
88 }
89}
90
91#[cfg(target_os = "linux")]
92#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
93enum DiskRole {
94 ZebraState,
95 ZcashdData,
96}
97
98#[cfg(target_os = "linux")]
99impl DiskRole {
100 fn label(self) -> &'static str {
101 match self {
102 DiskRole::ZebraState => "zebra state",
103 DiskRole::ZcashdData => "zcashd datadir",
104 }
105 }
106}
107
108#[cfg(target_os = "linux")]
109#[derive(Clone, Debug)]
110struct PathRequirement {
111 role: DiskRole,
112 target_path: PathBuf,
113 min_provisioned_bytes: u64,
114}
115
116#[cfg(target_os = "linux")]
117#[derive(Copy, Clone, Debug, Eq, PartialEq)]
118struct DiskThresholds {
119 min_zebra_bytes: u64,
120 min_zcashd_bytes: u64,
121 recommended_combined_bytes: u64,
122}
123
124#[cfg(target_os = "linux")]
125#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
126enum PermissionRole {
127 ZebraState,
128 ZcashdDatadir,
129 ZcashdConf,
130 ManagedZcashdCache,
131}
132
133#[cfg(target_os = "linux")]
134impl PermissionRole {
135 fn label(self) -> &'static str {
136 match self {
137 PermissionRole::ZebraState => "zebra state directory",
138 PermissionRole::ZcashdDatadir => "zcashd datadir",
139 PermissionRole::ZcashdConf => "zcashd config directory",
140 PermissionRole::ManagedZcashdCache => "embedded zcashd cache directory",
141 }
142 }
143}
144
145#[cfg(target_os = "linux")]
146#[derive(Clone, Debug)]
147struct WriteRequirement {
148 role: PermissionRole,
149 target_path: PathBuf,
150}
151
152#[cfg(target_os = "linux")]
153#[derive(Clone, Debug, Default)]
154struct PermissionRequirements {
155 roles: Vec<PermissionRole>,
156 target_paths: Vec<PathBuf>,
157}
158
159#[cfg(target_os = "linux")]
160#[derive(Clone, Debug, Default)]
161struct FilesystemRequirements {
162 roles: Vec<DiskRole>,
163 target_paths: Vec<PathBuf>,
164 min_provisioned_sum_bytes: u64,
165 provisioned_bytes: u64,
166}
167
168#[cfg(target_os = "linux")]
169#[derive(Clone, Debug, Default)]
170struct PreflightSummary {
171 errors: Vec<String>,
172 warnings: Vec<String>,
173}
174
175#[cfg(target_os = "linux")]
176fn run_linux_preflight(config: &ZebradConfig, unsafe_low_specs: bool) -> Result<(), Report> {
177 let mut zcashd_datadir =
178 effective_zcashd_datadir(&config.zcashd_compat, &config.state.cache_dir);
179
180 if config.zcashd_compat.manage_zcashd {
181 zcashd_datadir =
182 resolve_zcashd_datadir_path(&zcashd_datadir, &config.zcashd_compat.zcashd_extra_args);
183 }
184
185 let zcashd_datadir_for_disk_check = (config.zcashd_compat.manage_zcashd
190 || config.zcashd_compat.zcashd_datadir.is_some())
191 .then_some(zcashd_datadir.as_path());
192
193 let mut summary = PreflightSummary::default();
194
195 let check_results = [
201 check_permissions(&mut summary, config, &zcashd_datadir),
202 check_cpu(&mut summary),
203 check_memory(&mut summary),
204 check_disk(
205 &mut summary,
206 &config.network.network,
207 &config.state.cache_dir,
208 zcashd_datadir_for_disk_check,
209 ),
210 ];
211 for error in check_results.into_iter().filter_map(Result::err) {
212 summary
213 .errors
214 .push(format!("preflight check could not run: {error}"));
215 }
216
217 for warning in finalize_preflight(summary, unsafe_low_specs)? {
218 warn!("{warning}");
219 }
220
221 if config.zcashd_compat.manage_zcashd {
224 ensure_zcashd_datadir(&zcashd_datadir, &config.zcashd_compat.zcashd_extra_args)?;
225 }
226
227 Ok(())
228}
229
230#[cfg(target_os = "linux")]
231fn disk_thresholds(network: &Network) -> DiskThresholds {
232 match network.kind() {
233 NetworkKind::Mainnet => DiskThresholds {
234 min_zebra_bytes: MAINNET_MIN_ZEBRA_PROVISIONED_BYTES,
235 min_zcashd_bytes: MAINNET_MIN_ZCASHD_PROVISIONED_BYTES,
236 recommended_combined_bytes: MAINNET_RECOMMENDED_COMBINED_TOTAL_BYTES,
237 },
238 NetworkKind::Testnet | NetworkKind::Regtest => DiskThresholds {
239 min_zebra_bytes: TESTNET_MIN_ZEBRA_PROVISIONED_BYTES,
240 min_zcashd_bytes: TESTNET_MIN_ZCASHD_PROVISIONED_BYTES,
241 recommended_combined_bytes: TESTNET_RECOMMENDED_COMBINED_TOTAL_BYTES,
242 },
243 }
244}
245
246#[cfg(target_os = "linux")]
247fn finalize_preflight(
248 mut summary: PreflightSummary,
249 unsafe_low_specs: bool,
250) -> Result<Vec<String>, Report> {
251 if !summary.errors.is_empty() {
252 if unsafe_low_specs {
253 summary
254 .warnings
255 .extend(summary.errors.into_iter().map(|error| {
256 format!(
257 "{error}. continuing because --unsafe-low-specs was explicitly provided"
258 )
259 }));
260 } else {
261 return Err(eyre!(
262 "zcashd-compat preflight failed:\n- {}",
263 summary.errors.join("\n- ")
264 ));
265 }
266 }
267
268 Ok(summary.warnings)
269}
270
271#[cfg(target_os = "linux")]
272fn check_permissions(
273 summary: &mut PreflightSummary,
274 config: &ZebradConfig,
275 zcashd_datadir: &Path,
276) -> Result<(), Report> {
277 let mut requirements = vec![WriteRequirement {
278 role: PermissionRole::ZebraState,
279 target_path: config.state.cache_dir.clone(),
280 }];
281
282 if config.zcashd_compat.manage_zcashd {
283 requirements.push(WriteRequirement {
284 role: PermissionRole::ZcashdDatadir,
285 target_path: zcashd_datadir.to_path_buf(),
286 });
287
288 let conf_path =
289 resolve_zcashd_conf_path(zcashd_datadir, &config.zcashd_compat.zcashd_extra_args);
290 check_conf_access(summary, &mut requirements, &conf_path)?;
291 check_zcashd_binary(
292 summary,
293 &mut requirements,
294 &config.zcashd_compat,
295 &config.state.cache_dir,
296 );
297 }
298
299 check_write_requirements(summary, &requirements)
300}
301
302#[cfg(target_os = "linux")]
303fn check_conf_access(
304 summary: &mut PreflightSummary,
305 requirements: &mut Vec<WriteRequirement>,
306 conf_path: &Path,
307) -> Result<(), Report> {
308 let metadata = match fs::symlink_metadata(conf_path) {
309 Ok(metadata) => {
310 if metadata.file_type().is_symlink() {
311 match fs::metadata(conf_path) {
312 Ok(target_metadata) => target_metadata,
313 Err(error) if error.kind() == io::ErrorKind::NotFound => {
314 summary.errors.push(format!(
315 "zcashd config {} is a symlink to a missing target",
316 conf_path.display()
317 ));
318 return Ok(());
319 }
320 Err(error) => {
321 summary.errors.push(format!(
322 "failed to inspect zcashd config symlink target {}: {error}",
323 conf_path.display()
324 ));
325 return Ok(());
326 }
327 }
328 } else {
329 metadata
330 }
331 }
332 Err(error) if error.kind() == io::ErrorKind::NotFound => {
333 let parent = conf_path.parent().ok_or_else(|| {
334 eyre!(
335 "zcashd config path has no parent directory: {}",
336 conf_path.display()
337 )
338 })?;
339
340 requirements.push(WriteRequirement {
341 role: PermissionRole::ZcashdConf,
342 target_path: parent.to_path_buf(),
343 });
344
345 return Ok(());
346 }
347 Err(error) => {
348 summary.errors.push(format!(
349 "failed to inspect zcashd config {}: {error}",
350 conf_path.display()
351 ));
352 return Ok(());
353 }
354 };
355
356 if metadata.is_dir() {
357 summary.errors.push(format!(
358 "zcashd config path {} is a directory, expected a file",
359 conf_path.display()
360 ));
361 return Ok(());
362 }
363
364 if access(conf_path, AccessFlags::R_OK).is_err() {
365 summary.errors.push(format!(
366 "zcashd config {} exists but is not readable by the current user",
367 conf_path.display()
368 ));
369 }
370
371 Ok(())
372}
373
374#[cfg(target_os = "linux")]
375fn check_zcashd_binary(
376 summary: &mut PreflightSummary,
377 requirements: &mut Vec<WriteRequirement>,
378 zcashd_compat_config: &super::Config,
379 state_cache_dir: &Path,
380) {
381 match effective_zcashd_source(zcashd_compat_config) {
382 Ok(ZcashdBinarySource::Path(path)) => {
383 if !is_command_resolvable(&path) {
384 summary.errors.push(format!(
385 "zcashd binary {} does not exist or is not executable by the current user",
386 path.display()
387 ));
388 }
389 }
390 Ok(ZcashdBinarySource::Embedded) => {
391 let Some(binary_path) = managed_zcashd_binary_path(state_cache_dir) else {
392 return;
393 };
394 let cache_is_current =
395 cached_managed_zcashd_binary_is_present(state_cache_dir).unwrap_or(false);
396
397 if binary_path.exists() && !is_command_resolvable(&binary_path) {
398 summary.errors.push(format!(
399 "zcashd binary {} does not exist or is not executable by the current user",
400 binary_path.display()
401 ));
402 }
403
404 if !cache_is_current {
405 let Some(parent) = binary_path.parent() else {
406 return;
407 };
408
409 requirements.push(WriteRequirement {
410 role: PermissionRole::ManagedZcashdCache,
411 target_path: parent.to_path_buf(),
412 });
413 }
414 }
415 Err(error) => summary.errors.push(error.to_string()),
416 }
417}
418
419#[cfg(target_os = "linux")]
420fn check_write_requirements(
421 summary: &mut PreflightSummary,
422 requirements: &[WriteRequirement],
423) -> Result<(), Report> {
424 let mut grouped = BTreeMap::<PathBuf, PermissionRequirements>::new();
425
426 for requirement in requirements {
427 let probed_path = nearest_existing_ancestor(&requirement.target_path)?;
428 let entry = grouped.entry(probed_path).or_default();
429
430 if !entry.roles.contains(&requirement.role) {
431 entry.roles.push(requirement.role);
432 }
433
434 if !entry.target_paths.contains(&requirement.target_path) {
435 entry.target_paths.push(requirement.target_path.clone());
436 }
437 }
438
439 for (probed_path, requirements) in grouped {
440 let metadata = fs::metadata(&probed_path).map_err(|error| {
441 eyre!(
442 "failed to read metadata for {}: {error}",
443 probed_path.display()
444 )
445 })?;
446
447 if !metadata.is_dir() {
448 summary.errors.push(format!(
449 "{} (paths: {}) requires a directory at {}, which exists but is not a directory",
450 permission_role_labels(&requirements.roles),
451 display_paths(&requirements.target_paths),
452 probed_path.display()
453 ));
454 continue;
455 }
456
457 if path_is_writable_dir(&probed_path) {
458 continue;
459 }
460
461 if requirements
462 .target_paths
463 .iter()
464 .all(|target_path| target_path == &probed_path)
465 {
466 summary.errors.push(format!(
467 "{} (paths: {}) is not writable by the current user",
468 permission_role_labels(&requirements.roles),
469 display_paths(&requirements.target_paths)
470 ));
471 } else {
472 summary.errors.push(format!(
473 "{} (paths: {}) cannot be created: nearest existing ancestor {} is not writable by the current user",
474 permission_role_labels(&requirements.roles),
475 display_paths(&requirements.target_paths),
476 probed_path.display()
477 ));
478 }
479 }
480
481 Ok(())
482}
483
484#[cfg(target_os = "linux")]
485fn path_is_writable_dir(path: &Path) -> bool {
486 access(path, AccessFlags::W_OK | AccessFlags::X_OK).is_ok()
487}
488
489#[cfg(target_os = "linux")]
490fn check_cpu(summary: &mut PreflightSummary) -> Result<(), Report> {
491 let cpu_count = available_parallelism()
492 .map(NonZeroUsize::get)
493 .map_err(|error| eyre!("failed to read available logical CPU count: {error}"))?;
494
495 if cpu_count < MIN_CPU_LOGICAL {
496 summary.errors.push(format!(
497 "detected {cpu_count} logical CPUs, minimum required is {MIN_CPU_LOGICAL}"
498 ));
499 } else if cpu_count < RECOMMENDED_CPU_LOGICAL {
500 summary.warnings.push(format!(
501 "detected {cpu_count} logical CPUs, recommended is {RECOMMENDED_CPU_LOGICAL}"
502 ));
503 }
504
505 Ok(())
506}
507
508#[cfg(target_os = "linux")]
509fn check_memory(summary: &mut PreflightSummary) -> Result<(), Report> {
510 let mem_total = meminfo_total_bytes()?;
511 let cgroup_limit = cgroup_memory_limit_bytes()?;
512 let effective_memory = cgroup_limit.map_or(mem_total, |limit| limit.min(mem_total));
513
514 if effective_memory < MIN_RAM_BYTES {
515 summary.errors.push(format!(
516 "detected effective memory {}, minimum required is {}",
517 human_gib(effective_memory),
518 human_gib(MIN_RAM_BYTES)
519 ));
520 } else if effective_memory < RECOMMENDED_RAM_BYTES {
521 summary.warnings.push(format!(
522 "detected effective memory {}, recommended is {}",
523 human_gib(effective_memory),
524 human_gib(RECOMMENDED_RAM_BYTES)
525 ));
526 }
527
528 Ok(())
529}
530
531#[cfg(target_os = "linux")]
532fn check_disk(
533 summary: &mut PreflightSummary,
534 network: &Network,
535 zebra_cache_dir: &Path,
536 zcashd_datadir: Option<&Path>,
537) -> Result<(), Report> {
538 let thresholds = disk_thresholds(network);
539 let mut requirements = vec![PathRequirement {
540 role: DiskRole::ZebraState,
541 target_path: zebra_cache_dir.to_path_buf(),
542 min_provisioned_bytes: thresholds.min_zebra_bytes,
543 }];
544
545 if let Some(zcashd_datadir) = zcashd_datadir {
549 requirements.push(PathRequirement {
550 role: DiskRole::ZcashdData,
551 target_path: zcashd_datadir.to_path_buf(),
552 min_provisioned_bytes: thresholds.min_zcashd_bytes,
553 });
554 }
555
556 let grouped_filesystems = grouped_requirements_by_filesystem(&requirements)?;
557 evaluate_disk_thresholds(
558 summary,
559 &grouped_filesystems,
560 thresholds.recommended_combined_bytes,
561 );
562
563 Ok(())
564}
565
566#[cfg(target_os = "linux")]
567fn evaluate_disk_thresholds(
568 summary: &mut PreflightSummary,
569 grouped_filesystems: &HashMap<u64, FilesystemRequirements>,
570 recommended_combined_bytes: u64,
571) {
572 let combined_provisioned_capacity = grouped_filesystems
576 .values()
577 .map(|filesystem| filesystem.provisioned_bytes)
578 .fold(0u64, u64::saturating_add);
579
580 for filesystem in grouped_filesystems.values() {
581 if filesystem.provisioned_bytes < filesystem.min_provisioned_sum_bytes {
582 summary.errors.push(format!(
583 "{} mount (paths: {}) has provisioned capacity {}, minimum required is {}",
584 role_labels(&filesystem.roles),
585 display_paths(&filesystem.target_paths),
586 human_gib(filesystem.provisioned_bytes),
587 human_gib(filesystem.min_provisioned_sum_bytes),
588 ));
589 }
590 }
591
592 if combined_provisioned_capacity < recommended_combined_bytes {
593 summary.warnings.push(format!(
594 "combined zcashd-compat filesystem capacity is {}, recommended is {}",
595 human_gib(combined_provisioned_capacity),
596 human_gib(recommended_combined_bytes)
597 ));
598 }
599}
600
601#[cfg(target_os = "linux")]
602fn grouped_requirements_by_filesystem(
603 requirements: &[PathRequirement],
604) -> Result<HashMap<u64, FilesystemRequirements>, Report> {
605 let mut grouped = HashMap::new();
606
607 for requirement in requirements {
608 let probed_path = nearest_existing_ancestor(&requirement.target_path)?;
609 let metadata = fs::metadata(&probed_path).map_err(|error| {
610 eyre!(
611 "failed to read metadata for {}: {error}",
612 probed_path.display()
613 )
614 })?;
615 let device_id = metadata.dev();
616 let provisioned_bytes = statvfs_provisioned_bytes(&probed_path)?;
617
618 let entry = grouped
619 .entry(device_id)
620 .or_insert_with(|| FilesystemRequirements {
621 provisioned_bytes,
622 ..FilesystemRequirements::default()
623 });
624
625 if !entry.roles.contains(&requirement.role) {
626 entry.roles.push(requirement.role);
627 }
628 entry.target_paths.push(requirement.target_path.clone());
629 entry.min_provisioned_sum_bytes = entry
630 .min_provisioned_sum_bytes
631 .saturating_add(requirement.min_provisioned_bytes);
632 }
633
634 Ok(grouped)
635}
636
637#[cfg(target_os = "linux")]
638fn nearest_existing_ancestor(path: &Path) -> Result<PathBuf, Report> {
639 let mut current = path.to_path_buf();
640
641 loop {
642 if current.exists() {
643 return Ok(current);
644 }
645
646 if let Some(parent) = current.parent() {
647 current = parent.to_path_buf();
648 continue;
649 }
650
651 return Err(eyre!(
652 "no existing ancestor path found for {}",
653 path.display()
654 ));
655 }
656}
657
658#[cfg(target_os = "linux")]
659fn statvfs_provisioned_bytes(path: &Path) -> Result<u64, Report> {
660 let stats = nix::sys::statvfs::statvfs(path).map_err(|error| {
661 eyre!(
662 "failed to get filesystem stats for {}: {error}",
663 path.display()
664 )
665 })?;
666
667 let fragment_size = stats.fragment_size();
668
669 Ok(stats.blocks().saturating_mul(fragment_size))
670}
671
672#[cfg(target_os = "linux")]
673fn meminfo_total_bytes() -> Result<u64, Report> {
674 let meminfo = fs::read_to_string("/proc/meminfo")
675 .map_err(|error| eyre!("failed to read /proc/meminfo: {error}"))?;
676 let mem_total_kib = meminfo
677 .lines()
678 .find_map(|line| line.strip_prefix("MemTotal:"))
679 .and_then(|line| line.split_whitespace().next())
680 .ok_or_else(|| eyre!("MemTotal field missing in /proc/meminfo"))?
681 .parse::<u64>()
682 .map_err(|error| eyre!("failed to parse MemTotal from /proc/meminfo: {error}"))?;
683
684 Ok(mem_total_kib.saturating_mul(1024))
685}
686
687#[cfg(target_os = "linux")]
688fn cgroup_memory_limit_bytes() -> Result<Option<u64>, Report> {
705 let self_cgroup = fs::read_to_string("/proc/self/cgroup")
706 .map_err(|error| eyre!("failed to read /proc/self/cgroup: {error}"))?;
707 let (v2_relative_path, v1_memory_relative_path) = parse_self_cgroup_paths(&self_cgroup);
708
709 let v2_limit = parse_first_cgroup_limit(
710 &v2_relative_path
711 .iter()
712 .map(|relative_path| {
713 cgroup_limit_path(Path::new("/sys/fs/cgroup"), relative_path, "memory.max")
714 })
715 .chain(std::iter::once(PathBuf::from("/sys/fs/cgroup/memory.max")))
716 .collect::<Vec<_>>(),
717 )?;
718
719 let v1_limit = parse_first_cgroup_limit(
720 &v1_memory_relative_path
721 .iter()
722 .map(|relative_path| {
723 cgroup_limit_path(
724 Path::new("/sys/fs/cgroup/memory"),
725 relative_path,
726 "memory.limit_in_bytes",
727 )
728 })
729 .chain(std::iter::once(PathBuf::from(
730 "/sys/fs/cgroup/memory/memory.limit_in_bytes",
731 )))
732 .collect::<Vec<_>>(),
733 )?;
734
735 Ok(select_cgroup_memory_limit(v2_limit, v1_limit))
736}
737
738#[cfg(target_os = "linux")]
739fn parse_self_cgroup_paths(self_cgroup: &str) -> (Option<String>, Option<String>) {
740 let mut v2_relative_path = None;
741 let mut v1_memory_relative_path = None;
742
743 for line in self_cgroup.lines() {
744 let mut fields = line.splitn(3, ':');
745 let _hierarchy_id = fields.next();
746 let controllers = fields.next();
747 let relative_path = fields.next();
748
749 let (Some(controllers), Some(relative_path)) = (controllers, relative_path) else {
750 continue;
751 };
752
753 if controllers.is_empty() {
754 v2_relative_path = Some(relative_path.to_string());
755 } else if controllers
756 .split(',')
757 .any(|controller| controller == "memory")
758 {
759 v1_memory_relative_path = Some(relative_path.to_string());
760 }
761 }
762
763 (v2_relative_path, v1_memory_relative_path)
764}
765
766#[cfg(target_os = "linux")]
767fn cgroup_limit_path(base_path: &Path, relative_path: &str, file_name: &str) -> PathBuf {
768 let normalized_relative_path = relative_path.trim_start_matches('/');
769
770 if normalized_relative_path.is_empty() {
771 base_path.join(file_name)
772 } else {
773 base_path.join(normalized_relative_path).join(file_name)
774 }
775}
776
777#[cfg(target_os = "linux")]
778fn parse_first_cgroup_limit(candidate_paths: &[PathBuf]) -> Result<Option<u64>, Report> {
779 for path in candidate_paths {
780 if let Some(limit) = parse_cgroup_limit(path)? {
781 return Ok(Some(limit));
782 }
783 }
784
785 Ok(None)
786}
787
788#[cfg(target_os = "linux")]
789fn parse_cgroup_limit(path: &Path) -> Result<Option<u64>, Report> {
790 let raw_limit = match fs::read_to_string(path) {
791 Ok(raw_limit) => raw_limit,
792 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
793 Err(error) => return Err(eyre!("failed to read {}: {error}", path.display())),
794 };
795
796 let trimmed = raw_limit.trim();
797 if trimmed.eq_ignore_ascii_case("max") {
798 return Ok(None);
799 }
800
801 let parsed_limit = trimmed.parse::<u64>().map_err(|error| {
802 eyre!(
803 "failed to parse cgroup memory limit from {}: {error}",
804 path.display()
805 )
806 })?;
807
808 if parsed_limit >= 0x7fff_ffff_ffff_f000 {
810 return Ok(None);
811 }
812
813 Ok(Some(parsed_limit))
814}
815
816#[cfg(target_os = "linux")]
817fn select_cgroup_memory_limit(v2_limit: Option<u64>, v1_limit: Option<u64>) -> Option<u64> {
818 match (v2_limit, v1_limit) {
819 (Some(v2), Some(v1)) => Some(v2.min(v1)),
820 (Some(v2), None) => Some(v2),
821 (None, Some(v1)) => Some(v1),
822 (None, None) => None,
823 }
824}
825
826#[cfg(target_os = "linux")]
827fn role_labels(roles: &[DiskRole]) -> String {
828 roles
829 .iter()
830 .map(|role| role.label())
831 .collect::<Vec<_>>()
832 .join(" + ")
833}
834
835#[cfg(target_os = "linux")]
836fn permission_role_labels(roles: &[PermissionRole]) -> String {
837 roles
838 .iter()
839 .map(|role| role.label())
840 .collect::<Vec<_>>()
841 .join(" + ")
842}
843
844#[cfg(target_os = "linux")]
845fn display_paths(paths: &[PathBuf]) -> String {
846 paths
847 .iter()
848 .map(|path| path.display().to_string())
849 .collect::<Vec<_>>()
850 .join(", ")
851}
852
853#[cfg(target_os = "linux")]
854fn human_gib(bytes: u64) -> String {
855 format!("{:.1} GiB", bytes as f64 / GIB as f64)
859}
860
861#[cfg(test)]
862mod tests {
863 #[cfg(target_os = "linux")]
864 use super::*;
865 #[cfg(target_os = "linux")]
866 use std::{
867 fs as std_fs,
868 os::unix::fs::{symlink, PermissionsExt},
869 path::Path,
870 process::Command,
871 };
872 #[cfg(target_os = "linux")]
873 use tempfile::TempDir;
874
875 #[cfg(target_os = "linux")]
876 use crate::components::zcashd_compat::{zcashd_target_triple, ConfigZcashdBinarySource};
877
878 #[cfg(target_os = "linux")]
879 #[test]
880 fn disk_thresholds_mainnet_returns_existing_limits() {
881 assert_eq!(
882 disk_thresholds(&Network::Mainnet),
883 DiskThresholds {
884 min_zebra_bytes: 275 * GIB,
885 min_zcashd_bytes: 275 * GIB,
886 recommended_combined_bytes: TIB,
887 }
888 );
889 }
890
891 #[cfg(target_os = "linux")]
892 #[test]
893 fn disk_thresholds_testnet_returns_lower_limits() {
894 assert_eq!(
895 disk_thresholds(&Network::new_default_testnet()),
896 DiskThresholds {
897 min_zebra_bytes: 30 * GIB,
898 min_zcashd_bytes: 30 * GIB,
899 recommended_combined_bytes: 100 * GIB,
900 }
901 );
902 }
903
904 #[cfg(target_os = "linux")]
905 #[test]
906 fn merges_provisioned_requirement_when_paths_share_filesystem() {
907 let requirements = vec![
908 PathRequirement {
909 role: DiskRole::ZebraState,
910 target_path: PathBuf::from("/tmp"),
911 min_provisioned_bytes: MAINNET_MIN_ZEBRA_PROVISIONED_BYTES,
912 },
913 PathRequirement {
914 role: DiskRole::ZcashdData,
915 target_path: PathBuf::from("/tmp"),
916 min_provisioned_bytes: MAINNET_MIN_ZCASHD_PROVISIONED_BYTES,
917 },
918 ];
919
920 let grouped = grouped_requirements_by_filesystem(&requirements)
921 .expect("filesystem grouping should succeed");
922 let filesystem = grouped.values().next().expect("group should not be empty");
923
924 assert_eq!(
925 filesystem.min_provisioned_sum_bytes,
926 MAINNET_MIN_ZEBRA_PROVISIONED_BYTES + MAINNET_MIN_ZCASHD_PROVISIONED_BYTES
927 );
928 }
929
930 #[cfg(target_os = "linux")]
931 #[test]
932 fn parses_cgroup_max_as_unlimited() {
933 assert_eq!(parse_cgroup_value("max").expect("valid cgroup value"), None);
934 }
935
936 #[cfg(target_os = "linux")]
937 #[test]
938 fn parses_cgroup_numeric_value() {
939 assert_eq!(
940 parse_cgroup_value("17179869184").expect("valid cgroup value"),
941 Some(17_179_869_184)
942 );
943 }
944
945 #[cfg(target_os = "linux")]
946 #[test]
947 fn prefers_v1_when_v2_is_unavailable() {
948 assert_eq!(select_cgroup_memory_limit(None, Some(16)), Some(16));
949 }
950
951 #[cfg(target_os = "linux")]
952 #[test]
953 fn chooses_tighter_limit_when_both_are_available() {
954 assert_eq!(select_cgroup_memory_limit(Some(32), Some(16)), Some(16));
955 }
956
957 #[cfg(target_os = "linux")]
958 #[test]
959 fn parses_v2_and_v1_process_cgroup_paths() {
960 let cgroup = "0::/user.slice/user-1000.slice/session-2.scope\n2:memory:/docker/abcdef";
961 let (v2_path, v1_path) = parse_self_cgroup_paths(cgroup);
962
963 assert_eq!(
964 v2_path,
965 Some("/user.slice/user-1000.slice/session-2.scope".to_string())
966 );
967 assert_eq!(v1_path, Some("/docker/abcdef".to_string()));
968 }
969
970 #[cfg(target_os = "linux")]
971 #[test]
972 fn builds_cgroup_limit_paths_with_root_and_nested_relative_paths() {
973 assert_eq!(
974 cgroup_limit_path(Path::new("/sys/fs/cgroup"), "/", "memory.max"),
975 PathBuf::from("/sys/fs/cgroup/memory.max")
976 );
977 assert_eq!(
978 cgroup_limit_path(
979 Path::new("/sys/fs/cgroup/memory"),
980 "/docker/abcdef",
981 "memory.limit_in_bytes"
982 ),
983 PathBuf::from("/sys/fs/cgroup/memory/docker/abcdef/memory.limit_in_bytes")
984 );
985 }
986
987 #[cfg(target_os = "linux")]
988 #[test]
989 fn reports_disk_failure_when_below_minimum_provisioned_capacity() {
990 let mut summary = PreflightSummary::default();
991 let mut grouped = HashMap::new();
992 grouped.insert(
993 1,
994 FilesystemRequirements {
995 roles: vec![DiskRole::ZebraState, DiskRole::ZcashdData],
996 target_paths: vec!["/zebra".into(), "/zcashd".into()],
997 min_provisioned_sum_bytes: MAINNET_MIN_ZEBRA_PROVISIONED_BYTES
998 + MAINNET_MIN_ZCASHD_PROVISIONED_BYTES,
999 provisioned_bytes: 400 * GIB,
1000 },
1001 );
1002
1003 evaluate_disk_thresholds(
1004 &mut summary,
1005 &grouped,
1006 MAINNET_RECOMMENDED_COMBINED_TOTAL_BYTES,
1007 );
1008
1009 assert_eq!(summary.errors.len(), 1);
1010 assert!(
1011 summary
1012 .errors
1013 .iter()
1014 .any(|error| error.contains("provisioned capacity")),
1015 "expected provisioned capacity error: {:?}",
1016 summary.errors
1017 );
1018 }
1019
1020 #[cfg(target_os = "linux")]
1021 #[test]
1022 fn evaluate_disk_thresholds_testnet_fails_below_60_gib_combined() {
1023 let mut summary = PreflightSummary::default();
1024 let mut grouped = HashMap::new();
1025 grouped.insert(
1026 1,
1027 FilesystemRequirements {
1028 roles: vec![DiskRole::ZebraState, DiskRole::ZcashdData],
1029 target_paths: vec!["/zebra".into(), "/zcashd".into()],
1030 min_provisioned_sum_bytes: TESTNET_MIN_ZEBRA_PROVISIONED_BYTES
1031 + TESTNET_MIN_ZCASHD_PROVISIONED_BYTES,
1032 provisioned_bytes: 50 * GIB,
1033 },
1034 );
1035
1036 evaluate_disk_thresholds(
1037 &mut summary,
1038 &grouped,
1039 TESTNET_RECOMMENDED_COMBINED_TOTAL_BYTES,
1040 );
1041
1042 assert_eq!(summary.errors.len(), 1);
1043 assert!(
1044 summary
1045 .errors
1046 .iter()
1047 .any(|error| error.contains("minimum required is 60.0 GiB")),
1048 "expected testnet minimum error: {:?}",
1049 summary.errors
1050 );
1051 }
1052
1053 #[cfg(target_os = "linux")]
1054 #[test]
1055 fn evaluate_disk_thresholds_testnet_warns_between_60_and_100_gib() {
1056 let mut summary = PreflightSummary::default();
1057 let mut grouped = HashMap::new();
1058 grouped.insert(
1059 1,
1060 FilesystemRequirements {
1061 roles: vec![DiskRole::ZebraState, DiskRole::ZcashdData],
1062 target_paths: vec!["/zebra".into(), "/zcashd".into()],
1063 min_provisioned_sum_bytes: TESTNET_MIN_ZEBRA_PROVISIONED_BYTES
1064 + TESTNET_MIN_ZCASHD_PROVISIONED_BYTES,
1065 provisioned_bytes: 80 * GIB,
1066 },
1067 );
1068
1069 evaluate_disk_thresholds(
1070 &mut summary,
1071 &grouped,
1072 TESTNET_RECOMMENDED_COMBINED_TOTAL_BYTES,
1073 );
1074
1075 assert_eq!(summary.errors, Vec::<String>::new());
1076 assert_eq!(summary.warnings.len(), 1);
1077 assert!(
1078 summary
1079 .warnings
1080 .iter()
1081 .any(|warning| warning.contains("recommended is 100.0 GiB")),
1082 "expected testnet recommendation warning: {:?}",
1083 summary.warnings
1084 );
1085 }
1086
1087 #[cfg(target_os = "linux")]
1088 #[test]
1089 fn bypass_turns_failures_into_warnings() {
1090 let summary = PreflightSummary {
1091 errors: vec!["cpu below minimum".to_string()],
1092 warnings: vec!["disk below recommendation".to_string()],
1093 };
1094
1095 let warnings = finalize_preflight(summary, true).expect("unsafe bypass should continue");
1096
1097 assert_eq!(warnings.len(), 2);
1098 assert!(
1099 warnings
1100 .iter()
1101 .any(|warning| warning.contains("--unsafe-low-specs")),
1102 "expected unsafe bypass warning message: {warnings:?}"
1103 );
1104 }
1105
1106 #[cfg(target_os = "linux")]
1107 #[test]
1108 fn fails_when_below_minimum_without_bypass() {
1109 let summary = PreflightSummary {
1110 errors: vec!["ram below minimum".to_string()],
1111 warnings: Vec::new(),
1112 };
1113
1114 let error = finalize_preflight(summary, false)
1115 .expect_err("preflight should fail without unsafe bypass");
1116 assert!(
1117 error.to_string().contains("preflight failed"),
1118 "unexpected error: {error}"
1119 );
1120 }
1121
1122 #[cfg(target_os = "linux")]
1123 #[test]
1124 fn permission_checks_pass_in_writable_tempdir() {
1125 let temp_dir = TempDir::new().expect("tempdir should be created");
1126 let config = permission_test_config(&temp_dir);
1127 let mut summary = PreflightSummary::default();
1128
1129 check_permissions(
1130 &mut summary,
1131 &config,
1132 &permission_test_zcashd_datadir(&config),
1133 )
1134 .expect("permission checks should succeed");
1135
1136 assert_eq!(summary.errors, Vec::<String>::new());
1137 }
1138
1139 #[cfg(target_os = "linux")]
1140 #[test]
1141 fn reports_unwritable_datadir_ancestor() {
1142 if running_as_root() {
1143 return;
1144 }
1145
1146 let temp_dir = TempDir::new().expect("tempdir should be created");
1147 let protected = temp_dir.path().join("protected");
1148 std_fs::create_dir(&protected).expect("protected dir should be created");
1149
1150 let mut config = permission_test_config(&temp_dir);
1151 config.zcashd_compat.zcashd_datadir =
1152 Some(protected.join("missing").join("zcashd-datadir"));
1153
1154 set_mode(&protected, 0o555);
1155 let mut summary = PreflightSummary::default();
1156 let result = check_permissions(
1157 &mut summary,
1158 &config,
1159 &permission_test_zcashd_datadir(&config),
1160 );
1161 set_mode(&protected, 0o755);
1162
1163 result.expect("permission checks should complete");
1164 assert!(
1165 summary.errors.iter().any(|error| {
1166 error.contains("nearest existing ancestor") && error.contains("zcashd datadir")
1167 }),
1168 "expected datadir ancestor error: {:?}",
1169 summary.errors
1170 );
1171 }
1172
1173 #[cfg(target_os = "linux")]
1174 #[test]
1175 fn dedups_roles_sharing_one_unwritable_directory() {
1176 if running_as_root() {
1177 return;
1178 }
1179
1180 let temp_dir = TempDir::new().expect("tempdir should be created");
1181 let shared = temp_dir.path().join("shared");
1182 std_fs::create_dir(&shared).expect("shared dir should be created");
1183
1184 let mut config = ZebradConfig::default();
1185 config.state.cache_dir = shared.clone();
1186 config.zcashd_compat.manage_zcashd = false;
1187
1188 set_mode(&shared, 0o555);
1189 let mut summary = PreflightSummary::default();
1190 let result = check_permissions(&mut summary, &config, temp_dir.path());
1191 set_mode(&shared, 0o755);
1192
1193 result.expect("permission checks should complete");
1194 assert_eq!(summary.errors.len(), 1, "errors: {:?}", summary.errors);
1195 assert!(
1196 summary.errors[0].contains("zebra state directory"),
1197 "expected state-dir error: {:?}",
1198 summary.errors
1199 );
1200 }
1201
1202 #[cfg(target_os = "linux")]
1203 #[test]
1204 fn reports_unreadable_existing_conf() {
1205 if running_as_root() {
1206 return;
1207 }
1208
1209 let temp_dir = TempDir::new().expect("tempdir should be created");
1210 let config = permission_test_config(&temp_dir);
1211 let zcashd_datadir = permission_test_zcashd_datadir(&config);
1212 std_fs::create_dir_all(&zcashd_datadir).expect("datadir should be created");
1213 let conf_path = zcashd_datadir.join("zcash.conf");
1214 std_fs::write(&conf_path, "").expect("conf should be written");
1215
1216 set_mode(&conf_path, 0o000);
1217 let mut summary = PreflightSummary::default();
1218 let result = check_permissions(&mut summary, &config, &zcashd_datadir);
1219 set_mode(&conf_path, 0o644);
1220
1221 result.expect("permission checks should complete");
1222 assert!(
1223 summary
1224 .errors
1225 .iter()
1226 .any(|error| error.contains("not readable")),
1227 "expected unreadable conf error: {:?}",
1228 summary.errors
1229 );
1230 }
1231
1232 #[cfg(target_os = "linux")]
1233 #[test]
1234 fn reports_conf_path_that_is_a_directory() {
1235 let temp_dir = TempDir::new().expect("tempdir should be created");
1236 let config = permission_test_config(&temp_dir);
1237 let zcashd_datadir = permission_test_zcashd_datadir(&config);
1238 std_fs::create_dir_all(zcashd_datadir.join("zcash.conf"))
1239 .expect("conf directory should be created");
1240 let mut summary = PreflightSummary::default();
1241
1242 check_permissions(&mut summary, &config, &zcashd_datadir)
1243 .expect("permission checks should complete");
1244
1245 assert!(
1246 summary
1247 .errors
1248 .iter()
1249 .any(|error| error.contains("is a directory")),
1250 "expected directory conf error: {:?}",
1251 summary.errors
1252 );
1253 }
1254
1255 #[cfg(target_os = "linux")]
1256 #[test]
1257 fn reports_dangling_conf_symlink() {
1258 let temp_dir = TempDir::new().expect("tempdir should be created");
1259 let config = permission_test_config(&temp_dir);
1260 let zcashd_datadir = permission_test_zcashd_datadir(&config);
1261 std_fs::create_dir_all(&zcashd_datadir).expect("datadir should be created");
1262 symlink("missing.conf", zcashd_datadir.join("zcash.conf"))
1263 .expect("dangling symlink should be created");
1264 let mut summary = PreflightSummary::default();
1265
1266 check_permissions(&mut summary, &config, &zcashd_datadir)
1267 .expect("permission checks should complete");
1268
1269 assert!(
1270 summary
1271 .errors
1272 .iter()
1273 .any(|error| error.contains("symlink to a missing target")),
1274 "expected dangling symlink error: {:?}",
1275 summary.errors
1276 );
1277 }
1278
1279 #[cfg(target_os = "linux")]
1280 #[test]
1281 fn reports_nonexecutable_zcashd_path() {
1282 let temp_dir = TempDir::new().expect("tempdir should be created");
1283 let mut config = permission_test_config(&temp_dir);
1284 let zcashd_path = temp_dir.path().join("non-executable-zcashd");
1285 std_fs::write(&zcashd_path, "#!/bin/sh\n").expect("zcashd file should be written");
1286 set_mode(&zcashd_path, 0o644);
1287 config.zcashd_compat.zcashd_path = Some(zcashd_path);
1288 let mut summary = PreflightSummary::default();
1289
1290 check_permissions(
1291 &mut summary,
1292 &config,
1293 &permission_test_zcashd_datadir(&config),
1294 )
1295 .expect("permission checks should complete");
1296
1297 assert!(
1298 summary
1299 .errors
1300 .iter()
1301 .any(|error| error.contains("not executable")),
1302 "expected non-executable zcashd error: {:?}",
1303 summary.errors
1304 );
1305 }
1306
1307 #[cfg(target_os = "linux")]
1308 #[test]
1309 fn missing_conf_requires_writable_parent() {
1310 let temp_dir = TempDir::new().expect("tempdir should be created");
1311 let config = permission_test_config(&temp_dir);
1312 let zcashd_datadir = permission_test_zcashd_datadir(&config);
1313 std_fs::create_dir_all(&zcashd_datadir).expect("datadir should be created");
1314 let mut summary = PreflightSummary::default();
1315
1316 check_permissions(&mut summary, &config, &zcashd_datadir)
1317 .expect("permission checks should complete");
1318
1319 assert_eq!(summary.errors, Vec::<String>::new());
1320 }
1321
1322 #[cfg(target_os = "linux")]
1323 #[test]
1324 fn embedded_source_checks_cache_dir_writability() {
1325 if running_as_root() {
1326 return;
1327 }
1328
1329 if zcashd_target_triple().is_none() {
1330 return;
1331 }
1332
1333 let temp_dir = TempDir::new().expect("tempdir should be created");
1334 let protected = temp_dir.path().join("protected");
1335 std_fs::create_dir(&protected).expect("protected dir should be created");
1336
1337 let mut config = permission_test_config(&temp_dir);
1338 config.state.cache_dir = protected.join("zebra-cache");
1339 config.zcashd_compat.zcashd_datadir = Some(temp_dir.path().join("zcashd-datadir"));
1340 config.zcashd_compat.zcashd_source = ConfigZcashdBinarySource::Embedded;
1341 config.zcashd_compat.zcashd_path = None;
1342
1343 set_mode(&protected, 0o555);
1344 let mut summary = PreflightSummary::default();
1345 let result = check_permissions(
1346 &mut summary,
1347 &config,
1348 &permission_test_zcashd_datadir(&config),
1349 );
1350 set_mode(&protected, 0o755);
1351
1352 result.expect("permission checks should complete");
1353 assert!(
1354 summary.errors.iter().any(|error| {
1355 error.contains("embedded zcashd cache directory")
1356 && error.contains("nearest existing ancestor")
1357 }),
1358 "expected embedded cache writability error: {:?}",
1359 summary.errors
1360 );
1361 }
1362
1363 #[cfg(target_os = "linux")]
1364 #[test]
1365 fn stale_embedded_cache_checks_cache_dir_writability() {
1366 if running_as_root() {
1367 return;
1368 }
1369
1370 if zcashd_target_triple().is_none() {
1371 return;
1372 }
1373
1374 let temp_dir = TempDir::new().expect("tempdir should be created");
1375 let mut config = permission_test_config(&temp_dir);
1376 config.zcashd_compat.zcashd_source = ConfigZcashdBinarySource::Embedded;
1377 config.zcashd_compat.zcashd_path = None;
1378
1379 let binary_path =
1380 managed_zcashd_binary_path(&config.state.cache_dir).expect("managed path should exist");
1381 let cache_dir = binary_path.parent().expect("binary should have parent");
1382 std_fs::create_dir_all(cache_dir).expect("managed cache dir should be created");
1383 std_fs::write(&binary_path, "#!/bin/sh\n").expect("cached zcashd should be written");
1384 std_fs::write(cache_dir.join("zcashd.sha256"), "stale\n")
1385 .expect("stale provenance should be written");
1386 set_mode(&binary_path, 0o755);
1387
1388 set_mode(cache_dir, 0o555);
1389 let mut summary = PreflightSummary::default();
1390 let result = check_permissions(
1391 &mut summary,
1392 &config,
1393 &permission_test_zcashd_datadir(&config),
1394 );
1395 set_mode(cache_dir, 0o755);
1396
1397 result.expect("permission checks should complete");
1398 assert!(
1399 summary.errors.iter().any(|error| {
1400 error.contains("embedded zcashd cache directory") && error.contains("not writable")
1401 }),
1402 "expected stale embedded cache writability error: {:?}",
1403 summary.errors
1404 );
1405 }
1406
1407 #[cfg(target_os = "linux")]
1408 fn permission_test_config(temp_dir: &TempDir) -> ZebradConfig {
1409 let mut config = ZebradConfig::default();
1410 config.state.cache_dir = temp_dir.path().join("zebra-cache");
1411 config.zcashd_compat.manage_zcashd = true;
1412 config.zcashd_compat.zcashd_source = ConfigZcashdBinarySource::Path;
1413 config.zcashd_compat.zcashd_datadir = Some(temp_dir.path().join("zcashd-datadir"));
1414
1415 let zcashd_path = temp_dir.path().join("zcashd");
1416 std_fs::write(&zcashd_path, "#!/bin/sh\n").expect("zcashd file should be written");
1417 set_mode(&zcashd_path, 0o755);
1418 config.zcashd_compat.zcashd_path = Some(zcashd_path);
1419
1420 config
1421 }
1422
1423 #[cfg(target_os = "linux")]
1424 fn permission_test_zcashd_datadir(config: &ZebradConfig) -> PathBuf {
1425 resolve_zcashd_datadir_path(
1426 &effective_zcashd_datadir(&config.zcashd_compat, &config.state.cache_dir),
1427 &config.zcashd_compat.zcashd_extra_args,
1428 )
1429 }
1430
1431 #[cfg(target_os = "linux")]
1432 fn set_mode(path: &Path, mode: u32) {
1433 std_fs::set_permissions(path, std_fs::Permissions::from_mode(mode))
1434 .expect("permissions should be set");
1435 }
1436
1437 #[cfg(target_os = "linux")]
1438 fn running_as_root() -> bool {
1439 uid_command_reports_root(&["-u"]) || uid_command_reports_root(&["-r", "-u"])
1440 }
1441
1442 #[cfg(target_os = "linux")]
1443 fn uid_command_reports_root(args: &[&str]) -> bool {
1444 Command::new("id")
1445 .args(args)
1446 .output()
1447 .ok()
1448 .and_then(|output| String::from_utf8(output.stdout).ok())
1449 .is_some_and(|uid| uid.trim() == "0")
1450 }
1451
1452 #[cfg(target_os = "linux")]
1453 fn parse_cgroup_value(value: &str) -> Result<Option<u64>, Report> {
1454 if value.trim().eq_ignore_ascii_case("max") {
1455 return Ok(None);
1456 }
1457
1458 let parsed_limit = value
1459 .trim()
1460 .parse::<u64>()
1461 .map_err(|error| eyre!("failed to parse cgroup memory limit: {error}"))?;
1462
1463 if parsed_limit >= 0x7fff_ffff_ffff_f000 {
1464 return Ok(None);
1465 }
1466
1467 Ok(Some(parsed_limit))
1468 }
1469}