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