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