1use anyhow::{Context, Result, anyhow, bail};
2use auri::url_encoding::url_decode;
3use chj_unix_util::{
4 daemon::{
5 Daemon, DaemonCheckExit, DaemonMode, DaemonOpts, DaemonPaths, ExecutionResult,
6 warrants_restart::{
7 RestartForConfigChangeOpts, RestartForExecutableChangeOpts,
8 RestartForExecutableOrConfigChange,
9 },
10 },
11 logging::{TimestampMode, TimestampOpts},
12 polling_signals::PollingSignalsSender,
13 timestamp_formatter::TimestampFormatter,
14};
15use clap::{CommandFactory, Parser};
16use itertools::Itertools;
17use url::Url;
18
19use std::{
20 io::{StdoutLock, Write, stdout},
21 os::unix::{ffi::OsStrExt, process::CommandExt},
22 path::{Path, PathBuf},
23 process::{Command, exit},
24 str::FromStr,
25 sync::{Arc, atomic::Ordering},
26 thread,
27 time::Duration,
28};
29
30use evobench_tools::{
31 config_file::{self, ConfigFile, save_config_file},
32 ctx, debug,
33 git::GitHash,
34 info,
35 io_utils::{lockable_file::StandaloneExclusiveFileLock, shell::preferred_shell},
36 lazyresult,
37 run::{
38 bench_tmp_dir::bench_tmp_dir,
39 benchmarking_job::{BenchmarkingJobOpts, BenchmarkingJobReasonOpt},
40 config::{RunConfig, RunConfigBundle, RunConfigOpts},
41 global_app_state_dir::GlobalAppStateDir,
42 insert_jobs::{DryRunOpt, ForceOpt, QuietOpt, insert_jobs},
43 open_run_queues::open_run_queues,
44 output_directory::structure::{OutputSubdir, SubDirs},
45 run_context::RunContext,
46 run_job::JobRunner,
47 run_queues::RunQueues,
48 sub_command::{
49 insert::{Insert, InsertBenchmarkingJobOpts},
50 list::ListOpts,
51 list_all::ListAllOpts,
52 open_polling_pool, open_working_directory_pool,
53 wd::{
54 Wd, get_run_lock, open_queue_change_signals, open_working_directory_change_signals,
55 },
56 },
57 versioned_dataset_dir::VersionedDatasetDir,
58 working_directory_pool::{WorkingDirectoryPool, WorkingDirectoryPoolBaseDir},
59 },
60 serde_types::date_and_time::{DateTimeWithOffset, LOCAL_TIME},
61 util::clap_styles::clap_styles,
62 utillib::{
63 arc::CloneArc,
64 cleanup_daemon::CleanupHandler,
65 get_terminal_width::get_terminal_width,
66 into_arc_path::IntoArcPath,
67 logging::{LogLevel, LogLevelOpts, set_log_level},
68 },
69};
70
71type CheckExit<'t> =
72 DaemonCheckExit<'t, RestartForExecutableOrConfigChange<Arc<ConfigFile<RunConfigOpts>>>>;
73
74const DEFAULT_RESTART_ON_UPGRADES: bool = true;
75const DEFAULT_RESTART_ON_CONFIG_CHANGE: bool = true;
76
77const LOCAL_TIME_DEFAULT: bool = true;
81
82#[derive(clap::Parser, Debug)]
83#[command(
84 next_line_help = true,
85 styles = clap_styles(),
86 term_width = get_terminal_width(4),
87 allow_hyphen_values = true,
88 bin_name = "evobench",
89)]
90struct Opts {
92 #[clap(flatten)]
93 log_level_opts: LogLevelOpts,
94
95 #[clap(long)]
99 log_level: Option<LogLevel>,
100
101 #[clap(long)]
106 config: Option<PathBuf>,
107
108 #[clap(subcommand)]
111 subcommand: SubCommand,
112}
113
114#[derive(clap::Subcommand, Debug)]
115enum SubCommand {
116 ListAll {
120 #[clap(flatten)]
121 opts: ListAllOpts,
122 },
123
124 List {
126 #[clap(flatten)]
127 opts: ListOpts,
128 },
129
130 Insert {
136 #[clap(subcommand)]
137 method: Insert,
138 },
139
140 Poll {
144 #[clap(long)]
151 force: bool,
152
153 #[clap(long)]
156 quiet: bool,
157
158 #[clap(long)]
161 fail: bool,
162
163 #[clap(flatten)]
164 dry_run_opt: DryRunOpt,
165
166 #[clap(subcommand)]
167 mode: RunMode,
168 },
169
170 Run {
173 #[clap(subcommand)]
174 mode: RunMode,
175 },
176
177 Wd {
179 #[clap(subcommand)]
182 subcommand: Wd,
183 },
184
185 Url {
187 #[clap(long)]
190 cd: bool,
191
192 url: String,
194 },
195
196 Status {},
199
200 Completions {
203 #[arg(value_enum)]
205 shell: clap_complete_command::Shell,
206 },
207
208 ConfigFormats,
210
211 ConfigSave { output_path: PathBuf },
215}
216
217#[derive(Debug, clap::Subcommand)]
218pub enum RunMode {
219 One {
221 #[clap(long)]
224 false_if_none: bool,
225 },
226 Daemon {
231 #[clap(flatten)]
232 opts: DaemonOpts,
233 #[clap(flatten)]
234 restart_for_executable_change_opts: RestartForExecutableChangeOpts,
235 #[clap(flatten)]
236 restart_for_config_change_opts: RestartForConfigChangeOpts,
237
238 #[clap(flatten)]
239 log_level_opts: LogLevelOpts,
240
241 #[clap(short, long)]
247 log_level: Option<LogLevel>,
248
249 action: DaemonMode,
254 },
255}
256
257enum RunResult {
258 OnceResult(bool),
260 StopOrRestart,
262}
263
264fn run_queues<'ce>(
270 run_config_bundle: RunConfigBundle,
271 queues: RunQueues,
272 working_directory_base_dir: Arc<WorkingDirectoryPoolBaseDir>,
273 mut working_directory_pool: WorkingDirectoryPool,
274 once: bool,
275 daemon_check_exit: Option<CheckExit<'ce>>,
276 queue_change_signals: PollingSignalsSender,
277 file_cleanup_handler: &CleanupHandler,
278 _run_lock: StandaloneExclusiveFileLock,
279) -> Result<RunResult> {
280 let conf = &run_config_bundle.shareable.run_config;
281
282 let mut run_context = RunContext::default();
283 let versioned_dataset_dir = VersionedDatasetDir::new();
284
285 if let Some(versioned_dataset_base_dir) = &conf.versioned_datasets_base_dir {
287 debug!("Test-running versioned dataset search");
288
289 let working_directory_id;
290 {
291 let mut pool = working_directory_pool.lock_mut("evobench::run_queues")?;
292 working_directory_id = pool.get_first()?;
293 pool.clear_current_working_directory()?;
294 }
295 debug!("Got working directory {working_directory_id:?}");
296 let ((), token) = working_directory_pool
297 .process_in_working_directory(
298 working_directory_id,
299 &DateTimeWithOffset::now(None),
300 |working_directory| -> Result<()> {
301 let working_directory = working_directory.into_inner().expect("still there");
302
303 working_directory
308 .git_working_dir
309 .git(&["fetch", "--tags"], true)?;
310
311 let head_commit_str = working_directory
314 .git_working_dir
315 .git_rev_parse("HEAD", true)?
316 .ok_or_else(|| anyhow!("can't resolve HEAD"))?;
317 let head_commit: GitHash = head_commit_str.parse().map_err(|e| {
318 anyhow!(
319 "parsing commit id from HEAD from polling working dir: \
320 {head_commit_str:?}: {e:#}"
321 )
322 })?;
323 let lock = versioned_dataset_dir
324 .updated_git_graph(&working_directory.git_working_dir, &head_commit)?;
325
326 for dataset_name_entry in std::fs::read_dir(&versioned_dataset_base_dir)
327 .map_err(ctx!("can't open directory {versioned_dataset_base_dir:?}"))?
328 {
329 let dataset_name_entry = dataset_name_entry?;
330 let dataset_name = dataset_name_entry.file_name();
331 let dataset_name_str = dataset_name.to_str().ok_or_else(|| {
332 anyhow!("can't decode entry {:?}", dataset_name_entry.path())
333 })?;
334 let x = lock.dataset_dir_for_commit(
335 &versioned_dataset_base_dir,
336 dataset_name_str,
337 )?;
338 debug!(
339 "Test-run of versioned dataset search for HEAD commit {head_commit_str} \
340 gave path: {x:?}"
341 );
342 }
343 Ok(())
344 },
345 None,
346 "test-running versioned dataset search",
347 None,
348 )
349 .context("while early-checking versioned datasets at startup")?;
350 working_directory_pool.working_directory_cleanup(token)?;
351 }
352
353 let mut working_directory_change_signals = open_working_directory_change_signals(conf)?;
354
355 loop {
356 let queues_data = queues.data()?;
359
360 let ran = queues_data.run_next_job(
361 JobRunner {
362 working_directory_pool: &mut working_directory_pool,
363 output_base_dir: &conf.output_dir.path,
364 timestamp: DateTimeWithOffset::now(None),
365 shareable_config: &run_config_bundle.shareable,
366 versioned_dataset_dir: &versioned_dataset_dir,
367 file_cleanup_handler: &file_cleanup_handler,
368 },
369 &mut run_context,
370 )?;
371
372 if let Some((job, job_status)) = ran {
373 if !job_status.can_run_again() {
374 let parameters = job.benchmarking_job_parameters();
375 let key_dir = parameters.to_key_dir(conf.output_dir.path.clone_arc());
376 for run_dir in key_dir.sub_dirs()? {
377 let run_dir = run_dir?;
378 let uncompressed_path = run_dir.evobench_log_uncompressed_path();
379 match std::fs::remove_file(&uncompressed_path) {
380 Ok(_) => info!("deleted {uncompressed_path:?}"),
381 Err(e) => match e.kind() {
382 std::io::ErrorKind::NotFound => {
383 info!("no {uncompressed_path:?} to delete")
384 }
385 _ => info!("ignoring error deleting {uncompressed_path:?}: {e:#}"),
386 },
387 }
388 }
389 }
390 }
391
392 if once {
393 return Ok(RunResult::OnceResult(ran.is_some()));
394 }
395
396 thread::sleep(Duration::from_secs(1));
398
399 if let Some(daemon_check_exit) = daemon_check_exit.as_ref() {
400 if daemon_check_exit.want_exit() {
401 return Ok(RunResult::StopOrRestart);
402 }
403 }
404
405 if working_directory_change_signals.got_signals() {
407 info!("the working directory pool was updated outside the app, reload it");
408 working_directory_pool = open_working_directory_pool(
409 conf,
410 working_directory_base_dir.clone_arc(),
411 false,
412 Some(queue_change_signals.clone()),
413 )?
414 .into_inner();
415 }
416 }
417}
418
419struct EvobenchDaemon<F: FnOnce(CheckExit) -> Result<()>> {
420 paths: DaemonPaths,
421 opts: DaemonOpts,
422 log_level: LogLevel,
423 restart_for_executable_change_opts: RestartForExecutableChangeOpts,
424 restart_for_config_change_opts: RestartForConfigChangeOpts,
425 config_file: Arc<ConfigFile<RunConfigOpts>>,
426 inner_run: F,
427}
428
429impl<F: FnOnce(CheckExit) -> Result<()>> EvobenchDaemon<F> {
430 fn into_daemon(
431 self,
432 ) -> Result<
433 Daemon<
434 RestartForExecutableOrConfigChange<Arc<ConfigFile<RunConfigOpts>>>,
435 impl FnOnce(CheckExit) -> Result<()>,
436 >,
437 > {
438 let Self {
439 log_level,
440 restart_for_executable_change_opts,
441 restart_for_config_change_opts,
442 opts,
443 paths,
444 config_file,
445 inner_run,
446 } = self;
447 let local_time = opts.logging_opts.local_time(LOCAL_TIME_DEFAULT);
448
449 let run = move |daemon_check_exit: CheckExit| -> Result<()> {
450 LOCAL_TIME.store(local_time, Ordering::SeqCst);
455
456 set_log_level(log_level);
457
458 inner_run(daemon_check_exit)
459 };
460
461 let other_restart_checks = restart_for_executable_change_opts
462 .to_restarter(
463 DEFAULT_RESTART_ON_UPGRADES,
464 TimestampFormatter {
465 use_rfc3339: true,
466 local_time,
467 },
468 )?
469 .and_config_change_opts(
470 restart_for_config_change_opts,
471 DEFAULT_RESTART_ON_CONFIG_CHANGE,
472 config_file,
473 );
474
475 Ok(Daemon {
476 opts,
477 restart_on_failures_default: true,
478 restart_opts: None,
479 timestamp_opts: TimestampOpts {
480 use_rfc3339: true,
481 mode: TimestampMode::Automatic {
482 mark_added_timestamps: true,
483 },
484 },
485 paths,
486 other_restart_checks,
487 run,
488 local_time_default: LOCAL_TIME_DEFAULT,
489 })
490 }
491}
492
493const DEFAULT_IS_HARD: bool = true;
494
495fn run() -> Result<Option<ExecutionResult>> {
496 let Opts {
497 log_level_opts,
498 log_level,
499 config,
500 subcommand,
501 } = Opts::parse();
502
503 let top_level_log_level = log_level_opts
504 .xor_log_level(log_level)
505 .map_err(ctx!("parsing top-level log level options"))?;
506 set_log_level(top_level_log_level.clone().unwrap_or_default());
507 #[allow(unused)]
508 let (log_level_opts, log_level) = ((), ());
509
510 LOCAL_TIME.store(LOCAL_TIME_DEFAULT, Ordering::SeqCst);
513
514 let config: Option<Arc<Path>> = config.map(Into::into);
515
516 match &subcommand {
519 SubCommand::ConfigFormats => {
520 println!(
521 "These configuration file extensions / formats are supported:\n\n {}\n",
522 config_file::supported_formats().join("\n ")
523 );
524 return Ok(None);
525 }
526 _ => (),
527 }
528
529 let run_config_bundle = RunConfigBundle::load(
530 config,
531 |msg| bail!("need a config file, {msg}"),
532 GlobalAppStateDir::new()?,
533 )?;
534
535 let conf = &run_config_bundle.shareable.run_config;
536
537 let working_directory_base_dir = Arc::new(WorkingDirectoryPoolBaseDir::new(
538 conf.working_directory_pool.base_dir.clone(),
539 &|| {
540 run_config_bundle
541 .shareable
542 .global_app_state_dir
543 .working_directory_pool_base()
544 },
545 )?);
546
547 let queues = lazyresult! {
548 open_run_queues(&run_config_bundle.shareable)
549 };
550 let queue_change_signals = {
553 let gasd = run_config_bundle.shareable.global_app_state_dir.clone();
554 lazyresult!(move open_queue_change_signals(&gasd).map(|s| s.sender()))
555 };
556
557 match subcommand {
558 SubCommand::ConfigFormats => unreachable!("already dispatched above"),
559
560 SubCommand::ConfigSave { output_path } => {
561 save_config_file(&output_path, &**run_config_bundle.config_file)?;
562 Ok(None)
563 }
564
565 SubCommand::ListAll { opts } => {
566 opts.run(&run_config_bundle.shareable)?;
567 Ok(None)
568 }
569
570 SubCommand::List { opts } => {
571 let (queues, regenerate_index_files) = queues.force()?;
572 opts.run(conf, &working_directory_base_dir, queues)?;
573 regenerate_index_files.run_one();
574 Ok(None)
575 }
576
577 SubCommand::Insert { method } => {
578 let (queues, regenerate_index_files) = queues.force()?;
579 let n = method.run(&run_config_bundle, &queues)?;
580 println!("Inserted {n} job{}.", if n == 1 { "" } else { "s" });
581 regenerate_index_files.run_one();
582 Ok(None)
583 }
584
585 SubCommand::Poll {
586 force,
587 quiet,
588 fail,
589 dry_run_opt,
590 mode,
591 } => {
592 let try_run_poll = |daemon_check_exit: Option<CheckExit>| -> Result<bool> {
594 loop {
595 let (commits, non_resolving) = {
596 let mut polling_pool = open_polling_pool(&run_config_bundle.shareable)?;
597
598 let working_directory_id = polling_pool.updated_working_dir()?;
599 polling_pool.resolve_branch_names(
600 working_directory_id,
601 &conf.remote_repository.remote_branch_names_for_poll,
602 )?
603 };
604 let num_commits = commits.len();
605
606 let mut benchmarking_jobs = Vec::new();
607 for (branch_name, commit_id, job_templates) in commits {
608 let opts = BenchmarkingJobOpts {
609 insert_benchmarking_job_opts: InsertBenchmarkingJobOpts {
610 reason: BenchmarkingJobReasonOpt {
611 reason: branch_name.as_str().to_owned().into(),
612 },
613 benchmarking_job_settings: (*conf.benchmarking_job_settings)
614 .clone(),
615 priority: None,
616 initial_boost: None,
617 },
618 commit_id,
619 };
620 benchmarking_jobs.append(&mut opts.complete_jobs(&job_templates));
621 }
622
623 let n_original = benchmarking_jobs.len();
624 let (queues, regenerate_index_files) = queues.force()?;
625 let n = insert_jobs(
626 benchmarking_jobs,
627 &run_config_bundle.shareable,
628 dry_run_opt.clone(),
629 ForceOpt { force },
630 QuietOpt { quiet: true },
634 &queues,
635 )?;
636 regenerate_index_files.run_one();
637
638 if non_resolving.is_empty() || !fail {
639 if !quiet {
640 if n > 0 {
641 println!(
642 "inserted {n}/{n_original} jobs (for {num_commits} commits)"
643 );
644 }
645 }
646 } else {
647 bail!(
648 "inserted {n}/{n_original} jobs (for {num_commits} commits), \
649 but the following names did not resolve: {non_resolving:?}"
650 )
651 }
652
653 if let Some(daemon_check_exit) = &daemon_check_exit {
654 if daemon_check_exit.want_exit() {
655 return Ok(n >= 1);
656 }
657 } else {
658 return Ok(n >= 1);
659 }
660
661 std::thread::sleep(Duration::from_secs(15));
662 }
663 };
664
665 match mode {
666 RunMode::One { false_if_none } => {
667 let did_insert = try_run_poll(None)?;
668 if false_if_none && !did_insert {
669 exit(1);
670 }
671 Ok(None)
672 }
673 RunMode::Daemon {
674 opts,
675 restart_for_executable_change_opts,
676 restart_for_config_change_opts,
677 log_level_opts,
678 log_level,
679 action,
680 } => {
681 let paths = conf.polling_daemon.clone();
682 let config_file = run_config_bundle.config_file.clone_arc();
683 let inner_run = |daemon_check_exit: CheckExit| -> Result<()> {
684 try_run_poll(Some(daemon_check_exit))?;
685 Ok(())
686 };
687
688 let log_level = log_level_opts
689 .xor_log_level(log_level)
690 .map_err(ctx!("parsing `poll daemon` log level options"))?
691 .or(top_level_log_level)
692 .unwrap_or(LogLevel::Warn);
693
694 let daemon = EvobenchDaemon {
695 paths,
696 opts,
697 log_level,
698 restart_for_executable_change_opts,
699 restart_for_config_change_opts,
700 config_file,
701 inner_run,
702 }
703 .into_daemon()?;
704 let r = daemon.execute(action, DEFAULT_IS_HARD)?;
705 Ok(Some(r))
706 }
707 }
708 }
709
710 SubCommand::Run { mode } => {
711 let open_working_directory_pool = |conf: &RunConfig| -> Result<_> {
712 Ok(open_working_directory_pool(
713 conf,
714 working_directory_base_dir.clone(),
715 false,
716 Some(queue_change_signals.force()?.clone()),
717 )?
718 .into_inner())
719 };
720
721 match mode {
722 RunMode::One { false_if_none } => {
723 let run_lock = get_run_lock(conf)?;
727 let file_cleanup_handler = CleanupHandler::start()?;
728 let (queues, regenerate_index_files) = queues.into_value()?;
729 let working_directory_pool = open_working_directory_pool(conf)?;
730 let r = run_queues(
731 run_config_bundle,
732 queues,
733 working_directory_base_dir,
734 working_directory_pool,
735 true,
736 None,
737 queue_change_signals.force()?.clone(),
738 &file_cleanup_handler,
739 run_lock,
740 );
741 regenerate_index_files.run_one();
742 match r? {
743 RunResult::OnceResult(ran) => {
744 if false_if_none {
745 exit(if ran { 0 } else { 1 })
746 } else {
747 Ok(None)
748 }
749 }
750 RunResult::StopOrRestart => unreachable!("only daemon mode issues this"),
751 }
752 }
753 RunMode::Daemon {
754 opts,
755 restart_for_executable_change_opts,
756 restart_for_config_change_opts,
757 log_level_opts,
758 log_level,
759 action,
760 } => {
761 let paths = conf.run_jobs_daemon.clone();
762 let config_file = run_config_bundle.config_file.clone_arc();
763 let (queues, regenerate_index_files) = queues.into_value()?;
764
765 let inner_run = |daemon_check_exit: CheckExit| -> Result<()> {
767 let conf = &run_config_bundle.shareable.run_config;
768
769 let run_lock = get_run_lock(conf)?;
770 let file_cleanup_handler = CleanupHandler::start()?;
779 regenerate_index_files.spawn_runner_thread()?;
780
781 let working_directory_pool = open_working_directory_pool(conf)?;
782 run_queues(
783 run_config_bundle,
784 queues,
785 working_directory_base_dir.clone(),
786 working_directory_pool,
787 false,
788 Some(daemon_check_exit.clone()),
789 queue_change_signals.force()?.clone(),
790 &file_cleanup_handler,
791 run_lock,
792 )?;
793 Ok(())
794 };
795
796 let log_level = log_level_opts
797 .xor_log_level(log_level)
798 .map_err(ctx!("parsing `run daemon` log level options"))?
799 .or(top_level_log_level)
800 .unwrap_or(LogLevel::Info);
801
802 let daemon = EvobenchDaemon {
803 paths,
804 opts,
805 log_level,
806 restart_for_executable_change_opts,
807 restart_for_config_change_opts,
808 config_file,
809 inner_run,
810 }
811 .into_daemon()?;
812 let r = daemon.execute(action, DEFAULT_IS_HARD)?;
813 Ok(Some(r))
814 }
815 }
816 }
817
818 SubCommand::Wd { subcommand } => {
819 subcommand.run(
820 &run_config_bundle.shareable,
821 &working_directory_base_dir,
822 queue_change_signals.force()?.clone(),
823 )?;
824 Ok(None)
825 }
826
827 SubCommand::Url { cd, url } => {
828 let path = match Url::from_str(&url) {
833 Ok(mut url) => {
834 url.set_fragment(None);
835 url.set_query(None);
836 url_decode(url.as_str())?
838 }
839 Err(_) => {
840 if url.contains('=') {
846 url
847 } else {
848 url_decode(&url)?
849 }
850 }
851 }
852 .into_arc_path();
853
854 let subdir = OutputSubdir::try_from(path)?;
855 let subdir = subdir.replace_base_path(conf.output_dir.path.clone_arc());
856 let local_path = subdir.to_path();
857
858 if cd {
859 let shell = preferred_shell()?;
860 Err(Command::new(&shell).current_dir(&local_path).exec())
861 .map_err(ctx!("executing {shell:?} in {local_path:?}"))?;
862 } else {
863 (|| -> Result<(), std::io::Error> {
864 let mut out = stdout().lock();
865 out.write_all(local_path.as_os_str().as_bytes())?;
866 out.write_all(b"\n")?;
867 out.flush()
868 })()
869 .map_err(ctx!("stdout"))?;
870 }
871
872 Ok(None)
873 }
874
875 SubCommand::Status {} => {
876 let show_status =
877 |daemon_name: &str, paths: &DaemonPaths, out: &mut StdoutLock| -> Result<_> {
878 let daemon = EvobenchDaemon {
879 paths: paths.clone(),
880 opts: DaemonOpts::default(),
881 log_level: LogLevel::Quiet,
882 restart_for_executable_change_opts: RestartForExecutableChangeOpts::default(
883 ),
884 restart_for_config_change_opts: RestartForConfigChangeOpts::default(),
885 config_file: run_config_bundle.config_file.clone_arc(),
886 inner_run: |_| Ok(()),
887 }
888 .into_daemon()?;
889 let s = daemon.status_string(true)?;
890 let logs = &paths.log_dir;
891 writeln!(out, " {daemon_name} daemon: {s}, logs: {logs:?}")?;
892 Ok(())
893 };
894
895 let mut out = stdout().lock();
896 writeln!(
897 &mut out,
898 "Evobench system status and configuration information:\n"
899 )?;
900 show_status(" run", &conf.run_jobs_daemon, &mut out)?;
901 show_status("poll", &conf.polling_daemon, &mut out)?;
902
903 writeln!(&mut out, "")?;
905 writeln!(
906 &mut out,
907 " Queues: {:?}",
908 conf.queues
909 .run_queues_basedir(false, &run_config_bundle.shareable.global_app_state_dir)?
910 )?;
911 writeln!(
912 &mut out,
913 " Working directories: {:?} -- but modify via `evobench wd` only",
914 working_directory_base_dir.path()
915 )?;
916 writeln!(
917 &mut out,
918 " Temporary dir: {:?}",
919 bench_tmp_dir()?.as_ref(),
920 )?;
921 writeln!(
922 &mut out,
923 " Outputs: {:?}",
924 conf.output_dir.path,
925 )?;
926 writeln!(&mut out, " Outputs URL: {:?}", conf.output_dir.url,)?;
927 writeln!(
928 &mut out,
929 " Config file: {:?}",
930 run_config_bundle.config_file.path()
931 )?;
932
933 out.flush()?;
934 Ok(None)
935 }
936
937 SubCommand::Completions { shell } => {
938 shell.generate(&mut Opts::command(), &mut std::io::stdout());
939 Ok(None)
940 }
941 }
942}
943
944fn main() -> Result<()> {
945 if let Some(execution_result) = run()? {
946 execution_result.daemon_cleanup();
947 }
948 Ok(())
949}