1use crate::{
2 __ToConsoleString, Backup, Outcome, Rewriter, SourceFile, Span, WarnFlags, Warning, config,
3 framework::{self, Applicable, Postprocess, SourceFileSpanTestMap, SpanKind, ToImplementation},
4 note, source_warn, sqlite, util, warn,
5};
6use ansi_term::Style;
7use anyhow::{Context as _, Result, anyhow, bail, ensure};
8use heck::ToKebabCase;
9use indexmap::IndexSet;
10use indicatif::ProgressBar;
11use itertools::{PeekNth, peek_nth};
12use log::debug;
13use once_cell::sync::OnceCell;
14use std::{
15 cell::RefCell,
16 collections::BTreeMap,
17 env::{current_dir, var},
18 fmt::Display,
19 io::{IsTerminal, Write},
20 iter::Peekable,
21 path::{Path, PathBuf},
22 process::{Command, ExitStatus as StdExitStatus, Stdio},
23 rc::Rc,
24 sync::atomic::{AtomicBool, Ordering},
25 time::Duration,
26};
27use strum::IntoEnumIterator;
28use subprocess::{Exec, ExitStatus};
29
30const DEFAULT_TIMEOUT: Duration = Duration::from_mins(1);
31
32static CTRLC: AtomicBool = AtomicBool::new(false);
33
34#[derive(Clone)]
35pub(crate) struct Removal {
36 pub span: Span,
37 pub text: String,
38 pub outcome: Outcome,
39}
40
41#[derive(Debug)]
42enum MismatchKind {
43 Missing,
44 Unexpected,
45}
46
47impl Display for MismatchKind {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(f, "{}", format!("{self:?}").to_kebab_case())
50 }
51}
52
53struct Mismatch {
54 kind: MismatchKind,
55 removal: Removal,
56}
57
58struct Context<'a> {
59 opts: Necessist,
60 root: Rc<PathBuf>,
61 println: &'a dyn Fn(&dyn AsRef<str>),
62 backend: Box<dyn framework::Interface>,
63 progress: Option<&'a ProgressBar>,
64}
65
66impl Context<'_> {
67 fn light(&self) -> LightContext<'_> {
68 LightContext {
69 opts: &self.opts,
70 root: &self.root,
71 println: self.println,
72 }
73 }
74}
75
76pub struct LightContext<'a> {
77 pub opts: &'a Necessist,
78 pub root: &'a Rc<PathBuf>,
79 pub println: &'a dyn Fn(&dyn AsRef<str>),
80}
81
82#[allow(clippy::struct_excessive_bools)]
83#[derive(Clone, Default)]
84pub struct Necessist {
85 pub allow: Vec<Warning>,
86 pub default_config: bool,
87 pub deny: Vec<Warning>,
88 pub dump: bool,
89 pub dump_candidate_counts: bool,
90 pub dump_candidates: bool,
91 pub no_lines_or_columns: bool,
92 pub no_local_functions: bool,
93 pub no_sqlite: bool,
94 pub quiet: bool,
95 pub reset: bool,
96 pub resume: bool,
97 pub root: Option<PathBuf>,
98 pub timeout: Option<u64>,
99 pub verbose: bool,
100 pub source_files: Vec<PathBuf>,
101 pub args: Vec<String>,
102}
103
104pub fn necessist<Identifier: Applicable + Display + IntoEnumIterator + ToImplementation>(
108 opts: &Necessist,
109 framework: framework::Auto<Identifier>,
110) -> Result<()> {
111 let opts = opts.clone();
112
113 process_options(&opts)?;
114
115 let root = opts
116 .root
117 .as_ref()
118 .map_or_else(current_dir, dunce::canonicalize)
119 .map(Rc::new)?;
120
121 #[cfg(feature = "lock_root")]
122 let _file: std::fs::File = lock_root(&root)?;
123
124 let mut context = LightContext {
125 opts: &opts,
126 root: &root,
127 println: &|_| {},
128 };
129
130 let println = |msg: &dyn AsRef<str>| {
131 println!("{}", msg.as_ref());
132 };
133
134 if !opts.quiet {
135 context.println = &println;
136 }
137
138 if opts.no_local_functions {
139 warn(
140 &context,
141 Warning::OptionDeprecated,
142 "--no-local-functions is now the default; hence, this option is deprecated",
143 WarnFlags::empty(),
144 )?;
145 }
146
147 let Some((backend, n_spans, source_file_span_test_map)) = prepare(&context, framework)? else {
148 return Ok(());
149 };
150
151 let mut context = Context {
152 opts,
153 root,
154 println: &|_| {},
155 backend,
156 progress: None,
157 };
158
159 if !context.opts.quiet {
160 context.println = &println;
161 }
162
163 let progress =
164 if var("RUST_LOG").is_err() && !context.opts.quiet && std::io::stdout().is_terminal() {
165 Some(ProgressBar::new(n_spans as u64))
166 } else {
167 None
168 };
169
170 let progress_println = |msg: &dyn AsRef<str>| {
171 #[allow(clippy::unwrap_used)]
174 progress.as_ref().unwrap().println(msg);
175 };
176
177 if progress.is_some() {
179 context.println = &progress_println;
180 context.progress = progress.as_ref();
181 }
182
183 run(context, source_file_span_test_map)
184}
185
186#[allow(clippy::type_complexity)]
187#[cfg_attr(dylint_lib = "supplementary", allow(commented_out_code))]
188fn prepare<Identifier: Applicable + Display + IntoEnumIterator + ToImplementation>(
189 context: &LightContext,
190 framework: framework::Auto<Identifier>,
191) -> Result<Option<(Box<dyn framework::Interface>, usize, SourceFileSpanTestMap)>> {
192 if context.opts.default_config {
193 default_config(context, context.root)?;
194 return Ok(None);
195 }
196
197 let config = config::Toml::read(context, context.root)?;
198
199 if context.opts.dump {
200 let past_removals = past_removals_init_lazy(context)?;
201 dump(context, &past_removals);
202 return Ok(None);
203 }
204
205 let mut backend = backend_for_framework(context, framework)?;
206
207 let paths = canonicalize_source_files(context)?;
208
209 let (n_tests, source_file_span_test_map) = backend.parse(
210 context,
211 &config,
212 &paths.iter().map(AsRef::as_ref).collect::<Vec<_>>(),
213 )?;
214
215 let n_spans = source_file_span_test_map
216 .values()
217 .map(|span_test_maps| {
218 span_test_maps
219 .statement
220 .values()
221 .map(IndexSet::len)
222 .sum::<usize>()
223 + span_test_maps
224 .method_call
225 .values()
226 .map(IndexSet::len)
227 .sum::<usize>()
228 })
229 .sum();
230
231 if context.opts.dump_candidates {
232 dump_candidates(context, &source_file_span_test_map)?;
233 return Ok(None);
234 }
235
236 if context.opts.dump_candidate_counts {
237 dump_candidate_counts(context, &source_file_span_test_map);
238 return Ok(None);
239 }
240
241 let n_source_files = source_file_span_test_map.keys().len();
274 (context.println)(&format!(
275 "{} candidates in {} test{} in {} source file{}",
276 n_spans,
277 n_tests,
278 if n_tests == 1 { "" } else { "s" },
279 n_source_files,
280 if n_source_files == 1 { "" } else { "s" }
281 ));
282
283 Ok(Some((backend, n_spans, source_file_span_test_map)))
284}
285
286fn run(mut context: Context, source_file_span_test_map: SourceFileSpanTestMap) -> Result<()> {
287 ctrlc::set_handler(|| CTRLC.store(true, Ordering::SeqCst))?;
288
289 let past_removals = past_removals_init_lazy(&context.light())?;
290
291 let mut past_removal_iter = past_removals.into_iter().peekable();
292
293 for (source_file, span_test_maps) in source_file_span_test_map {
294 let mut span_test_iter = peek_nth(span_test_maps.iter());
295
296 let (mismatch, n) = skip_past_removals(&mut span_test_iter, &mut past_removal_iter);
297
298 update_progress(&context, mismatch, n)?;
299
300 if span_test_iter.peek().is_none() {
301 continue;
302 }
303
304 (context.println)(&format!(
305 "{}: dry running",
306 util::strip_current_dir(&source_file).to_string_lossy()
307 ));
308
309 let result = context.backend.dry_run(&context.light(), &source_file);
310
311 if let Err(error) = &result {
312 source_warn(
313 &context.light(),
314 Warning::DryRunFailed,
315 &source_file,
316 &format!("dry run failed: {error:?}"),
317 WarnFlags::empty(),
318 )?;
319 }
320
321 if CTRLC.load(Ordering::SeqCst) {
322 bail!("Ctrl-C detected");
323 }
324
325 if result.is_err() {
326 let n = skip_present_spans(&context, span_test_iter)?;
327 update_progress(&context, None, n)?;
328 continue;
329 }
330
331 (context.println)(&format!(
332 "{}: mutilating",
333 util::strip_current_dir(&source_file).to_string_lossy()
334 ));
335
336 let mut instrumentation_backup =
337 instrument_statements(&context, &source_file, &mut span_test_iter)?;
338
339 loop {
340 let (mismatch, n) = skip_past_removals(&mut span_test_iter, &mut past_removal_iter);
341
342 update_progress(&context, mismatch, n)?;
343
344 let Some((span, span_kind, test_names)) = span_test_iter.next() else {
345 break;
346 };
347
348 if span_kind != SpanKind::Statement {
349 drop(instrumentation_backup.take());
350 }
351
352 let text = span.source_text()?;
353
354 let explicit_removal =
355 instrumentation_backup.is_none() || span_kind != SpanKind::Statement;
356
357 let _explicit_backup = if explicit_removal {
358 let (_, explicit_backup) = span.remove()?;
359 Some(explicit_backup)
360 } else {
361 None
362 };
363
364 let outcome =
365 test_names
366 .into_iter()
367 .try_fold(Some(Outcome::Passed), |outcome, test_name| {
368 if outcome != Some(Outcome::Passed) {
369 return Ok(outcome);
370 }
371
372 let result = context.backend.exec(&context.light(), test_name, span)?;
373
374 match result {
375 Ok((exec, postprocess)) => {
376 let exec = exec.env("NECESSIST_REMOVAL", span.id());
379
380 perform_exec(&context, exec, postprocess)
381 }
382 Err(error) => {
383 assert!(
384 explicit_removal,
385 "Instrumentation failed to build after it was verified to: \
386 {error}"
387 );
388
389 Ok(Some(Outcome::Nonbuildable))
390 }
391 }
392 })?;
393
394 if CTRLC.load(Ordering::SeqCst) {
395 bail!("Ctrl-C detected");
396 }
397
398 if let Some(outcome) = outcome {
399 emit(&mut context, span, &text, outcome)?;
400 }
401
402 update_progress(&context, None, 1)?;
403 }
404 }
405
406 context.progress.map(ProgressBar::finish);
407
408 Ok(())
409}
410
411macro_rules! incompatible {
412 ($opts:ident, $x:ident, $y:ident) => {
413 ensure!(
414 !($opts.$x && $opts.$y),
415 "--{} and --{} are incompatible",
416 stringify!($x).to_kebab_case(),
417 stringify!($y).to_kebab_case()
418 );
419 };
420}
421
422fn process_options(opts: &Necessist) -> Result<()> {
423 incompatible!(opts, dump, quiet);
425 incompatible!(opts, dump, reset);
426 incompatible!(opts, dump, resume);
427 incompatible!(opts, dump, no_sqlite);
428 incompatible!(opts, quiet, verbose);
429 incompatible!(opts, reset, no_sqlite);
430 incompatible!(opts, resume, no_sqlite);
431
432 Ok(())
433}
434
435#[cfg(feature = "lock_root")]
436fn lock_root(root: &Path) -> Result<std::fs::File> {
437 if enabled("TRYCMD") {
438 crate::flock::lock_path(root)
439 } else {
440 crate::flock::try_lock_path(root)
441 }
442 .with_context(|| format!("Failed to lock `{}`", root.display()))
443}
444
445#[cfg(feature = "lock_root")]
446fn enabled(key: &str) -> bool {
447 var(key).is_ok_and(|value| value != "0")
448}
449
450fn default_config(_context: &LightContext, root: &Path) -> Result<()> {
451 let path_buf = root.join("necessist.toml");
452
453 if path_buf.try_exists()? {
454 bail!(
455 "A configuration file already exists at `{}`",
456 path_buf.display()
457 );
458 }
459
460 let toml = toml::to_string(&config::Toml::default())?;
461
462 std::fs::write(path_buf, toml).map_err(Into::into)
463}
464
465fn dump(context: &LightContext, removals: &[Removal]) {
466 let mut other_than_passed = false;
467 for removal in removals {
468 emit_to_console(context, removal);
469 other_than_passed |= removal.outcome != Outcome::Passed;
470 }
471
472 if !context.opts.verbose && other_than_passed {
473 note(context, "More output would be produced with --verbose");
474 }
475}
476
477fn backend_for_framework<Identifier: Applicable + Display + IntoEnumIterator + ToImplementation>(
478 context: &LightContext,
479 identifier: framework::Auto<Identifier>,
480) -> Result<Box<dyn framework::Interface>> {
481 let implementation = identifier.to_implementation(context)?;
482
483 drop(identifier);
484
485 implementation.ok_or_else(|| anyhow!("Found no applicable frameworks"))
486}
487
488fn canonicalize_source_files(context: &LightContext) -> Result<Vec<PathBuf>> {
489 context
490 .opts
491 .source_files
492 .iter()
493 .map(|path| {
494 let path_to_canonicalize = if path.is_absolute() {
495 path.clone()
496 } else {
497 context.root.join(path)
498 };
499 let path_buf = dunce::canonicalize(&path_to_canonicalize)
500 .with_context(|| format!("Failed to canonicalize `{}`", path.display()))?;
501 ensure!(
502 path_buf.starts_with(context.root.as_path()),
503 "`{}` is not in `{}`",
504 path_buf.display(),
505 context.root.display()
506 );
507 Ok(path_buf)
508 })
509 .collect::<Result<Vec<_>>>()
510}
511
512#[must_use]
513fn skip_past_removals<'a, I, J>(
514 span_test_iter: &mut PeekNth<I>,
515 removal_iter: &mut Peekable<J>,
516) -> (Option<Mismatch>, usize)
517where
518 I: Iterator<Item = (&'a Span, SpanKind, &'a IndexSet<String>)>,
519 J: Iterator<Item = Removal>,
520{
521 let mut mismatch = None;
522 let mut n = 0;
523 while let Some(&(span, _, _)) = span_test_iter.peek() {
524 let Some(removal) = removal_iter.peek() else {
525 break;
526 };
527 match span.cmp(&removal.span) {
528 std::cmp::Ordering::Less => {
529 mismatch = Some(Mismatch {
530 kind: MismatchKind::Unexpected,
531 removal: removal.clone(),
532 });
533 break;
534 }
535 std::cmp::Ordering::Equal => {
536 let _: Option<(&Span, _, _)> = span_test_iter.next();
537 let _removal: Option<Removal> = removal_iter.next();
538 n += 1;
539 }
540 std::cmp::Ordering::Greater => {
541 if mismatch.is_none() {
542 mismatch = Some(Mismatch {
543 kind: MismatchKind::Missing,
544 removal: removal.clone(),
545 });
546 }
547 let _removal: Option<Removal> = removal_iter.next();
548 }
549 }
550 }
551
552 (mismatch, n)
553}
554
555fn skip_present_spans<'a>(
556 context: &Context,
557 span_test_iter: impl IntoIterator<Item = (&'a Span, SpanKind, &'a IndexSet<String>)>,
558) -> Result<usize> {
559 let mut n = 0;
560
561 let sqlite = sqlite_init_lazy(&context.light())?;
562
563 for (span, _, _) in span_test_iter {
564 if let Some(sqlite) = sqlite.borrow_mut().as_mut() {
565 let text = span.source_text()?;
566 let removal = Removal {
567 span: span.clone(),
568 text,
569 outcome: Outcome::Skipped,
570 };
571 sqlite::insert(sqlite, &removal)?;
572 }
573 n += 1;
574 }
575
576 Ok(n)
577}
578
579fn update_progress(context: &Context, mismatch: Option<Mismatch>, n: usize) -> Result<()> {
580 if let Some(Mismatch {
581 kind,
582 removal: Removal { span, text, .. },
583 }) = mismatch
584 {
585 warn(
586 &context.light(),
587 Warning::FilesChanged,
588 &format!(
589 "\
590Configuration or source files have changed since necessist.db was created; the following entry is \
591 {kind}:
592 {}: `{}`",
593 span.to_console_string(),
594 text.replace('\r', ""),
595 ),
596 WarnFlags::ONCE,
597 )?;
598 }
599
600 if let Some(bar) = context.progress {
601 bar.inc(n as u64);
602 }
603
604 Ok(())
605}
606
607fn dump_candidates(
608 context: &LightContext,
609 source_file_span_test_map: &SourceFileSpanTestMap,
610) -> Result<()> {
611 for span in source_file_span_test_map
612 .values()
613 .flat_map(|span_test_maps| {
614 span_test_maps
615 .statement
616 .keys()
617 .chain(span_test_maps.method_call.keys())
618 })
619 {
620 let loc = if context.opts.no_lines_or_columns {
621 span.source_file().to_console_string()
622 } else {
623 span.to_console_string()
624 };
625 let text = span.source_text()?;
626
627 (context.println)(&format!("{loc}: `{}`", text.replace('\r', "")));
628 }
629
630 Ok(())
631}
632
633fn dump_candidate_counts(
634 context: &LightContext,
635 source_file_span_test_map: &SourceFileSpanTestMap,
636) {
637 let mut candidate_counts = source_file_span_test_map
638 .iter()
639 .map(|(source_file, span_test_maps)| {
640 (
641 span_test_maps.statement.keys().count() + span_test_maps.method_call.keys().count(),
642 source_file,
643 )
644 })
645 .collect::<Vec<_>>();
646
647 candidate_counts.sort();
648
649 let Some(width) = candidate_counts
650 .iter()
651 .map(|(count, _)| count.to_string().len())
652 .max()
653 else {
654 return;
655 };
656
657 for (count, source_file) in candidate_counts {
658 (context.println)(&format!(
659 "{count:width$} {}",
660 source_file.to_console_string(),
661 ));
662 }
663}
664
665fn instrument_statements<'a, I>(
666 context: &Context,
667 source_file: &SourceFile,
668 span_test_iter: &mut PeekNth<I>,
669) -> Result<Option<Backup>>
670where
671 I: Iterator<Item = (&'a Span, SpanKind, &'a IndexSet<String>)>,
672{
673 let backup = Backup::new(source_file)?;
674
675 let mut rewriter =
676 Rewriter::with_offset_calculator(source_file.contents(), source_file.offset_calculator());
677
678 let n_instrumentable_statements = count_instrumentable_statements(span_test_iter);
679
680 context.backend.instrument_source_file(
681 &context.light(),
682 &mut rewriter,
683 source_file,
684 n_instrumentable_statements,
685 )?;
686
687 let mut i_span = 0;
688 let mut insertion_map = BTreeMap::<_, Vec<_>>::new();
689 while let Some((span, SpanKind::Statement, _)) = span_test_iter.peek_nth(i_span) {
692 let (prefix, suffix) = context.backend.statement_prefix_and_suffix(span)?;
693 let insertions = insertion_map.entry(span.start()).or_default();
694 insertions.push(prefix);
695 let insertions = insertion_map.entry(span.end()).or_default();
696 insertions.push(suffix);
697 i_span += 1;
698 }
699
700 assert_eq!(n_instrumentable_statements, i_span);
701
702 for (line_column, insertions) in insertion_map {
703 for insertion in insertions {
704 source_file.insert(&mut rewriter, line_column, &insertion);
705 }
706 }
707
708 let mut file = std::fs::OpenOptions::new()
709 .truncate(true)
710 .write(true)
711 .open(source_file)?;
712 file.write_all(rewriter.contents().as_bytes())?;
713 drop(file);
714
715 let result = context
716 .backend
717 .build_source_file(&context.light(), source_file);
718 if let Err(error) = result {
719 warn(
720 &context.light(),
721 Warning::InstrumentationNonbuildable,
722 &format!(
723 "Instrumentation caused `{}` to be nonbuildable: {error:?}",
724 source_file.to_console_string(),
725 ),
726 WarnFlags::empty(),
727 )?;
728 return Ok(None);
729 }
730
731 Ok(Some(backup))
732}
733
734fn count_instrumentable_statements<'a, I>(span_test_iter: &mut PeekNth<I>) -> usize
735where
736 I: Iterator<Item = (&'a Span, SpanKind, &'a IndexSet<String>)>,
737{
738 let mut n_instrumentable_statements = 0;
739 while matches!(
740 span_test_iter.peek_nth(n_instrumentable_statements),
741 Some((_, SpanKind::Statement, _))
742 ) {
743 n_instrumentable_statements += 1;
744 }
745 n_instrumentable_statements
746}
747
748fn perform_exec(
749 context: &Context,
750 exec: Exec,
751 postprocess: Option<Box<Postprocess>>,
752) -> Result<Option<Outcome>> {
753 debug!("{exec:?}");
754
755 #[cfg(all(feature = "limit_threads", unix))]
756 let nprocs_prev = rlimit::set_soft_rlimit(
757 rlimit::Resource::NPROC,
758 *rlimit::NPROC_INIT + rlimit::NPROC_ALLOWANCE,
759 )?;
760
761 let job = exec.start()?;
762 let status = if let Some(dur) = timeout(&context.opts) {
763 job.wait_timeout(dur)?
764 } else {
765 job.wait().map(Option::Some)?
766 };
767
768 #[cfg(all(feature = "limit_threads", unix))]
769 rlimit::set_soft_rlimit(rlimit::Resource::NPROC, nprocs_prev)?;
770
771 if status.is_some() {
772 if let Some(postprocess) = postprocess
773 && !postprocess(&context.light(), job)?
774 {
775 return Ok(None);
776 }
777 } else {
778 let pid = job.pid();
779 transitive_kill(pid)?;
780 let _: ExitStatus = job.wait()?;
781 }
782
783 let Some(status) = status else {
784 return Ok(Some(Outcome::TimedOut));
785 };
786
787 Ok(Some(if status.success() {
788 Outcome::Passed
789 } else {
790 Outcome::Failed
791 }))
792}
793
794fn emit(context: &mut Context, span: &Span, text: &str, outcome: Outcome) -> Result<()> {
795 let removal = Removal {
796 span: span.clone(),
797 text: text.to_owned(),
798 outcome,
799 };
800
801 let sqlite = sqlite_init_lazy(&context.light())?;
802
803 if let Some(sqlite) = sqlite.borrow_mut().as_mut() {
804 sqlite::insert(sqlite, &removal)?;
805 }
806
807 emit_to_console(&context.light(), &removal);
808
809 Ok(())
810}
811
812fn emit_to_console(context: &LightContext, removal: &Removal) {
813 let Removal {
814 span,
815 text,
816 outcome,
817 } = removal;
818
819 if !context.opts.quiet && (context.opts.verbose || *outcome == Outcome::Passed) {
820 let loc = if context.opts.no_lines_or_columns {
821 span.source_file().to_console_string()
822 } else {
823 span.to_console_string()
824 };
825 let msg = format!(
826 "{loc}: `{}` {}",
827 text.replace('\r', ""),
828 if std::io::stdout().is_terminal() {
829 outcome.style().bold()
830 } else {
831 Style::default()
832 }
833 .paint(outcome.to_string())
834 );
835 (context.println)(&msg);
836 }
837}
838
839fn sqlite_init_lazy(context: &LightContext) -> Result<Rc<RefCell<Option<sqlite::Sqlite>>>> {
840 let (sqlite, _) = sqlite_and_past_removals_init_lazy(context)?;
841 Ok(sqlite)
842}
843
844fn past_removals_init_lazy(context: &LightContext) -> Result<Vec<Removal>> {
845 let (_, past_removals) = sqlite_and_past_removals_init_lazy(context)?;
846 Ok(past_removals.take())
847}
848
849thread_local! {
850 #[allow(clippy::type_complexity)]
851 static SQLITE_AND_PAST_REMOVALS: OnceCell<(
852 Rc<RefCell<Option<sqlite::Sqlite>>>,
853 Rc<RefCell<Vec<Removal>>>,
854 )> = const { OnceCell::new() };
855}
856
857#[allow(clippy::type_complexity)]
858fn sqlite_and_past_removals_init_lazy(
859 context: &LightContext,
860) -> Result<(
861 Rc<RefCell<Option<sqlite::Sqlite>>>,
862 Rc<RefCell<Vec<Removal>>>,
863)> {
864 SQLITE_AND_PAST_REMOVALS.with(|sqlite_and_past_removals| {
865 sqlite_and_past_removals
866 .get_or_try_init(|| {
867 if context.opts.no_sqlite {
868 Ok((
869 Rc::new(RefCell::new(None)),
870 Rc::new(RefCell::new(Vec::new())),
871 ))
872 } else {
873 let (sqlite, mut past_removals) = sqlite::init(
874 context,
875 context.root,
876 context.opts.dump,
877 context.opts.reset,
878 context.opts.resume,
879 )?;
880 past_removals.sort_by(|left, right| left.span.cmp(&right.span));
881 Ok((
882 Rc::new(RefCell::new(Some(sqlite))),
883 Rc::new(RefCell::new(past_removals)),
884 ))
885 }
886 })
887 .cloned()
888 })
889}
890
891#[allow(clippy::module_name_repetitions)]
892#[cfg(all(feature = "limit_threads", unix))]
893mod rlimit {
894 use anyhow::Result;
895 pub use rlimit::Resource;
896 use rlimit::{getrlimit, setrlimit};
897 use std::{process::Command, sync::LazyLock};
898
899 #[allow(clippy::unwrap_used)]
900 pub static NPROC_INIT: LazyLock<u64> = LazyLock::new(|| {
901 let output = Command::new("ps").arg("-eL").output().unwrap();
902 let stdout = std::str::from_utf8(&output.stdout).unwrap();
903 stdout.lines().count().try_into().unwrap()
904 });
905
906 pub const NPROC_ALLOWANCE: u64 = 1024;
915
916 pub fn set_soft_rlimit(resource: Resource, limit: u64) -> Result<u64> {
917 let (soft, hard) = getrlimit(resource)?;
918 setrlimit(Resource::NPROC, std::cmp::min(hard, limit), hard)?;
919 Ok(soft)
920 }
921}
922
923fn timeout(opts: &Necessist) -> Option<Duration> {
924 match opts.timeout {
925 None => Some(DEFAULT_TIMEOUT),
926 Some(0) => None,
927 Some(secs) => Some(Duration::from_secs(secs)),
928 }
929}
930
931#[cfg_attr(dylint_lib = "supplementary", allow(commented_out_code))]
932fn transitive_kill(pid: u32) -> Result<()> {
933 let mut pids = vec![(pid, false)];
934
935 while let Some((pid, visited)) = pids.pop() {
936 if visited {
937 let _status: StdExitStatus = kill()
938 .arg(pid.to_string())
939 .stdout(Stdio::null())
940 .stderr(Stdio::null())
941 .status()?;
942 } else {
945 pids.push((pid, true));
946
947 for line in child_processes(pid)? {
948 let pid = line
949 .parse::<u32>()
950 .with_context(|| format!("failed to parse `{line}`"))?;
951 pids.push((pid, false));
952 }
953 }
954 }
955
956 Ok(())
957}
958
959#[cfg(not(windows))]
960fn kill() -> Command {
961 Command::new("kill")
962}
963
964#[cfg(windows)]
965fn kill() -> Command {
966 let mut command = Command::new("taskkill");
967 command.args(["/f", "/pid"]);
968 command
969}
970
971#[cfg(not(windows))]
972fn child_processes(pid: u32) -> Result<Vec<String>> {
973 let output = Command::new("pgrep")
974 .args(["-P", &pid.to_string()])
975 .output()?;
976 let stdout = String::from_utf8(output.stdout)?;
977 Ok(stdout.lines().map(ToOwned::to_owned).collect())
978}
979
980#[cfg(windows)]
981fn child_processes(pid: u32) -> Result<Vec<String>> {
982 let mut command = Command::new("powershell");
983 command.arg(format!(
984 r#"(Get-CimInstance -ClassName Win32_Process -Filter "ParentProcessId = {pid}").ProcessId"#
985 ));
986 let output = command.output()?;
987 let stdout = String::from_utf8(output.stdout)?;
988 Ok(stdout.lines().map(|s| s.trim_end().to_owned()).collect())
989}