evobench/
evobench.rs

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
77/// True since the configuration uses times, too, and those are
78/// probably better local time, and in general, just use whatever the
79/// TZ is set to. You can set TZ to UTC, too.
80const 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)]
90/// Schedule and query benchmarking jobs.
91struct Opts {
92    #[clap(flatten)]
93    log_level_opts: LogLevelOpts,
94
95    /// Alternative to --quiet / --verbose / --debug for setting the
96    /// log-level (an error is reported if both are given and they
97    /// don't agree). (Default: "warn")
98    #[clap(long)]
99    log_level: Option<LogLevel>,
100
101    /// Override the path to the config file (default: the paths
102    /// `~/.evobench.*` where a single one exists where the `*` is the
103    /// suffix for one of the supported config file formats (run
104    /// `config-formats` to get the list).
105    #[clap(long)]
106    config: Option<PathBuf>,
107
108    /// The subcommand to run. Use `--help` after the sub-command to
109    /// get a list of the allowed options there.
110    #[clap(subcommand)]
111    subcommand: SubCommand,
112}
113
114#[derive(clap::Subcommand, Debug)]
115enum SubCommand {
116    /// Show the table of all inserted jobs, including already
117    /// processed ones. This is the table that `evobench insert`
118    /// checks to avoid duplicate inserts by default. .
119    ListAll {
120        #[clap(flatten)]
121        opts: ListAllOpts,
122    },
123
124    /// List the jobs that are being processed (per queue)
125    List {
126        #[clap(flatten)]
127        opts: ListOpts,
128    },
129
130    /// Insert zero or more jobs, either from a complete benchmarking
131    /// job description file, or using the job templates from one list
132    /// from the configuration combined with zero or more commits. For
133    /// automatic periodic insertion, see the `poll` sub-command
134    /// instead. .
135    Insert {
136        #[clap(subcommand)]
137        method: Insert,
138    },
139
140    /// Insert jobs for new commits on branch names configured in the
141    /// config option `remote_branch_names_for_poll`. For one-off
142    /// manual insertion see `insert` instead. .
143    Poll {
144        // No QuietOpt since that must be the default. Also, another
145        // force option since the help text is different here.
146        /// Normally, the same job parameters are only inserted once,
147        /// subsequent polls yielding the same commits remain
148        /// no-ops. This overrides the check and inserts the found
149        /// commits anyway. .
150        #[clap(long)]
151        force: bool,
152
153        /// Suppress printing the "inserted n jobs" message when n >
154        /// 0, i.e. always be quiet.
155        #[clap(long)]
156        quiet: bool,
157
158        /// Report an error if any of the given (branch or other)
159        /// names do not resolve.
160        #[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 the existing jobs; this takes a lock or stops with an
171    /// error if the lock is already taken
172    Run {
173        #[clap(subcommand)]
174        mode: RunMode,
175    },
176
177    /// Handle working directories
178    Wd {
179        /// The subcommand to run. Use `--help` after the sub-command to
180        /// get a list of the allowed options there.
181        #[clap(subcommand)]
182        subcommand: Wd,
183    },
184
185    /// Parse URLs to output directories
186    Url {
187        /// Open a new $SHELL (or bash) in the output
188        /// directory. Default: print the path instead. .
189        #[clap(long)]
190        cd: bool,
191
192        /// The URL or (partial) path to parse
193        url: String,
194    },
195
196    /// General program status information (but also see `list`, `wd
197    /// list`, `list-all`, `run daemon status`, `poll daemon status`)
198    Status {},
199
200    /// Generate a shell completions file. (Redirect stdout to a file
201    /// that is `source`d from the shell.)
202    Completions {
203        /// The shell to generate the completions for.
204        #[arg(value_enum)]
205        shell: clap_complete_command::Shell,
206    },
207
208    /// Show the supported config format types.
209    ConfigFormats,
210
211    /// Re-encode the config file (with the serialization type
212    /// determined by the file extension) and save it at the given
213    /// path.
214    ConfigSave { output_path: PathBuf },
215}
216
217#[derive(Debug, clap::Subcommand)]
218pub enum RunMode {
219    /// Carry out a single run
220    One {
221        /// Exit with code 1 if there is no runnable job / there were
222        /// no jobs to insert.
223        #[clap(long)]
224        false_if_none: bool,
225    },
226    /// Run forever, until terminated (note: evobench uses
227    /// restart-on-failures and local-time by default; the local-time
228    /// setting has no effect on times in the config file, those are
229    /// always parsed as local-time)
230    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        /// The logging level while running as daemon (as alternative
242        /// to the --verbose, --debug, --quiet options on this level,
243        /// and overriding the top-level logging options --log-level,
244        /// --verbose, --debug, --quiet) (default: "info" for run
245        /// daemon, "warn" for poll daemon)
246        #[clap(short, long)]
247        log_level: Option<LogLevel>,
248
249        /// Whether to run in the foreground, or start or stop a
250        /// daemon running in the background (or report the status
251        /// about it). Give `help` to see the options. evobench
252        /// defaults to the 'hard' actions.
253        action: DaemonMode,
254    },
255}
256
257enum RunResult {
258    /// In one-job mode, indicates whether it ran any job
259    OnceResult(bool),
260    /// In daemon mode
261    StopOrRestart,
262}
263
264/// Run through the queues forever unless `once` is true (in which
265/// case it returns whether a job was run), but pick up config
266/// changes; it also returns in non-once mode if the binary changes
267/// and true was given for `restart_on_upgrades`. Requires holding the
268/// `_run_lock`, the lock for executing "run" actions.
269fn 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    // Test-run
286    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                    // Fetch the tags so that comparing dataset versions
304                    // can work. (Avoid the risk of an old working
305                    // directory having an older HEAD than all dataset
306                    // versions.)
307                    working_directory
308                        .git_working_dir
309                        .git(&["fetch", "--tags"], true)?;
310
311                    // XX capture all errors and return as Ok? Or is it OK
312                    // to re-clone the repo on all such errors?
313                    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        // XX handle errors without exiting? Or do that above
357
358        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        // XX have something better than polling?
397        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        // Do we need to re-initialize the working directory pool?
406        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            // Use the requested time setting for
451            // local time stamp generation, too (now
452            // the default is UTC, which is expected
453            // for a daemon).
454            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    // Interactive use should get local time. (Daemon mode possibly
511    // overwrites this.) true or LOCAL_TIME_DEFAULT?
512    LOCAL_TIME.store(LOCAL_TIME_DEFAULT, Ordering::SeqCst);
513
514    let config: Option<Arc<Path>> = config.map(Into::into);
515
516    // Have to handle ConfigFormats before attempting to read the
517    // config
518    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    // Do not attempt to pass those to `open_run_queues` above; maybe
551    // they are needed without needing the queues?
552    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            // Returns whether at least 1 job was inserted
593            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                        // Must use quiet so that it can try to insert *all*
631                        // given jobs (XX: should it continue even with
632                        // errors, for the other code places?)
633                        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                    // See comment further down on the other instance
724                    // of `CleanupHandler::start` for the importance
725                    // of the execution order here!
726                    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                    // The code that runs in the daemon and executes the jobs
766                    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                        // FileCleanupHandler must be started in
771                        // daemon child so that logging output goes to
772                        // the daemon log, but before any threads are
773                        // started; but after the RunLock has been
774                        // taken (above), so that the child shares it, so that
775                        // when the daemon re-exec's, it will not
776                        // start new things until the cleanup daemon
777                        // from the previous instance is done.
778                        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            // Allow both copy-paste from web browser (at least
829            // Firefox encodes '=' in path part via url_encoding), and
830            // local paths. First see if it's a URL (XX false
831            // positives?).
832            let path = match Url::from_str(&url) {
833                Ok(mut url) => {
834                    url.set_fragment(None);
835                    url.set_query(None);
836                    // At that point '=' are still URL-encoded, thus:
837                    url_decode(url.as_str())?
838                }
839                Err(_) => {
840                    // Unparseable as Url; no problem, just can't
841                    // remove any fragment and query parts. Don't just
842                    // url_decode, since custom variables might
843                    // contain such parts, too; only do it if there
844                    // are no '=' in the path, OK?
845                    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, "\nPaths:")?;
904            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}