evobench_tools/run/sub_command/
list.rs

1use std::{
2    borrow::Cow,
3    cell::OnceCell,
4    io::{self, IsTerminal, Write, stdout},
5    path::{Path, PathBuf},
6    sync::Arc,
7    time::{Duration, SystemTime},
8};
9
10use anyhow::{Result, anyhow};
11use chrono::{DateTime, Local};
12use kstring::KString;
13
14use crate::{
15    config_file::ron_to_string_pretty,
16    io_utils::lockable_file::LockStatus,
17    key_val_fs::key_val::Entry,
18    output_table::{BarKind, FontSize, WithUrlOnDemand},
19    run::{
20        config::RunConfig,
21        output_directory::structure::{KeyDir, ToPath},
22        run_queue::RunQueue,
23        run_queues::RunQueues,
24        working_directory::Status,
25        working_directory_pool::WorkingDirectoryPoolBaseDir,
26    },
27    utillib::{arc::CloneArc, recycle::RecycleVec},
28};
29use crate::{
30    output_table::terminal::{TerminalTable, TerminalTableOpts},
31    utillib::into_arc_path::IntoArcPath,
32};
33use crate::{
34    output_table::{OutputStyle, OutputTable, OutputTableTitle},
35    run::output_directory::html_files::print_list,
36};
37
38pub const TARGET_NAME_WIDTH: usize = 14;
39
40#[derive(Debug, Clone, Copy, clap::Subcommand)]
41pub enum ParameterPathKind {
42    /// Relative from the output base directory (default)
43    Relative,
44    /// Full local file system path
45    Full,
46    /// URL for access via the web
47    Url,
48}
49
50impl Default for ParameterPathKind {
51    fn default() -> Self {
52        Self::Relative
53    }
54}
55
56#[derive(Debug, Clone, Copy, clap::Subcommand, Default)]
57pub enum ParameterView {
58    /// Show separate `Commit_id`, `Target_name`, `Custom_parameters`
59    /// columns (default)
60    #[default]
61    Separated,
62    /// Show a single column that represents a path to the job in the
63    /// outputs directory.
64    Path {
65        #[clap(subcommand)]
66        kind: Option<ParameterPathKind>,
67    },
68}
69
70impl ParameterView {
71    pub fn titles(self) -> Vec<&'static str> {
72        let mut titles = vec![
73            "Insertion_time",
74            "S", // Status
75            "Prio",
76            "WD",
77            "Reason",
78        ];
79        match self {
80            ParameterView::Separated => {
81                titles.extend_from_slice(&["Commit_id", "Target_name", "Custom_parameters"]);
82            }
83            ParameterView::Path { kind } => {
84                titles.push(match kind.unwrap_or_default() {
85                    ParameterPathKind::Relative => "Output_path",
86                    ParameterPathKind::Full => "Output_path",
87                    ParameterPathKind::Url => "Output_URL",
88                });
89            }
90        }
91        titles
92    }
93}
94
95#[derive(Debug, Clone, clap::Args)]
96pub struct OutputTableOpts {
97    /// Show details, not just one item per line
98    #[clap(short, long)]
99    pub verbose: bool,
100
101    /// Show all jobs in the extra queues (done and failures); by
102    /// default, only the last `view_jobs_max_len` jobs are shown
103    /// as stated in the QueuesConfig.
104    #[clap(short, long)]
105    pub all: bool,
106
107    /// When `--all` is not given, how many jobs to show in the extra
108    /// queues (overrides the `view_jobs_max_len` setting from the
109    /// config file)
110    #[clap(short, long)]
111    pub n: Option<usize>,
112
113    /// How to show the job parameters
114    #[clap(subcommand)]
115    pub parameter_view: Option<ParameterView>,
116}
117
118impl OutputTableOpts {
119    pub fn output_to_table<'link_skipped, Table: OutputTable>(
120        &self,
121        mut table: Table,
122        conf: &RunConfig,
123        link_skipped: Option<&'link_skipped str>,
124        working_directory_base_dir: &Arc<WorkingDirectoryPoolBaseDir>,
125        queues: &RunQueues,
126    ) -> Result<Table::Output> {
127        let Self {
128            verbose,
129            all,
130            n,
131            parameter_view,
132        } = self;
133
134        let parameter_view = parameter_view.unwrap_or_default();
135
136        // The base of the path that's used for the `path` view
137        let path_base: Option<Arc<Path>> = {
138            match parameter_view {
139                ParameterView::Separated => None,
140                ParameterView::Path { kind } => Some(match kind.unwrap_or_default() {
141                    ParameterPathKind::Relative => PathBuf::from("").into(),
142                    ParameterPathKind::Full => conf.output_dir.path.clone_arc(),
143                    ParameterPathKind::Url => {
144                        let url = conf.output_dir.url.as_ref().ok_or_else(|| {
145                            anyhow!(
146                                "the URL viewing feature requires the `output_dir.url` \
147                                 field in the configuration to be set"
148                            )
149                        })?;
150                        PathBuf::from(&**url).into()
151                    }
152                }),
153            }
154        };
155
156        // COPY-PASTE from List action in jobqueue.rs
157        let get_filename = |entry: &Entry<_, _>| -> Result<String> {
158            let file_name = entry.file_name();
159            Ok(file_name
160                .to_str()
161                .ok_or_else(|| anyhow!("filename that cannot be decoded as UTF-8: {file_name:?}"))?
162                .to_string())
163        };
164
165        let lock = working_directory_base_dir.lock("for SubCommand::List show_queue")?;
166
167        {
168            let titles: Vec<_> = parameter_view
169                .titles()
170                .into_iter()
171                .map(|s| OutputTableTitle {
172                    text: Cow::Borrowed(s),
173                    span: 1,
174                    anchor_name: None,
175                })
176                .collect();
177
178            let style = Some(OutputStyle {
179                bold: true,
180                italic: true,
181                color: Some(4),
182                font_size: Some(FontSize::Large),
183                ..Default::default()
184            });
185
186            table.write_title_row(&titles, style)?;
187        }
188
189        let full_span = table.num_columns();
190
191        let now = SystemTime::now();
192
193        // Not kept in sync with what happens during for loop; but
194        // then it is really about the status stored inside
195        // `pool`, thus that doesn't even matter!
196        let opt_current_working_directory = lock.read_current_working_directory()?;
197
198        let show_queue = |i: &str,
199                          run_queue: &RunQueue,
200                          is_extra_queue: bool,
201                          table: &mut Table,
202                          bar_kind_after: BarKind|
203         -> Result<()> {
204            let RunQueue {
205                file_name,
206                schedule_condition,
207                queue,
208            } = run_queue;
209
210            // "Insertion time"
211            // "R", "E", ""
212            // priority
213            // reason
214            // "Commit id"
215            // "Custom parameters"
216            let titles = &[OutputTableTitle {
217                text: format!(
218                    "{i}: queue {:?} ({schedule_condition}):",
219                    file_name.as_str(),
220                )
221                .into(),
222                span: full_span,
223                anchor_name: Some(KString::from_ref(file_name.as_str())),
224            }];
225
226            // It's OK to call this multiple times on the same table,
227            // <th> are allowed in any table row; not sure about the
228            // semantics, though.
229            table.write_title_row(titles, None)?;
230
231            // We want the last view_jobs_max_len items, one more
232            // if that's the complete list (the additional entry
233            // then occupying the "entries skipped" line). Don't
234            // want to collect the whole list first (leads to too
235            // many open filehandles), don't want to go through it
236            // twice (once for counting, once to skip); getting
237            // them in reverse, taking the first n, collecting,
238            // then reversing the list would be one way, but
239            // cleaner is to use a two step approach, first get
240            // the sorted collection of keys (cheap to hold in
241            // memory and needs to be retrieved underneath
242            // anyway), get the section we want, then use
243            // resolve_entries to load the items still in
244            // streaming fashion.  Note: this could show fewer
245            // than limit items even after showing "skipped",
246            // because items can vanish between getting
247            // sorted_keys and resolve_entries. But that is really
248            // no big deal.
249            let view_jobs_max_len = n.unwrap_or(conf.queues.view_jobs_max_len);
250            let limit = if is_extra_queue && !all {
251                // Get 2 more since showing "skipped 1 entry" is
252                // not economic, and we just look at number 0
253                // after subtracting, i.e. include the equal case.
254                view_jobs_max_len + 2
255            } else {
256                usize::MAX
257            };
258            let all_sorted_keys = queue.sorted_keys(false, None, false)?;
259            let shown_sorted_keys;
260            if let Some(num_skipped_2) = all_sorted_keys.len().checked_sub(limit) {
261                let num_skipped = num_skipped_2 + 2;
262                let s = format!("... ({num_skipped} entries skipped)\n");
263                let tmp;
264                let gen_url: Option<&dyn Fn() -> Option<Cow<'link_skipped, str>>> =
265                    if let Some(link) = link_skipped {
266                        let url: Cow<str> = format!("{link}#end-{}", file_name.as_str()).into();
267                        tmp = move || Some(url.clone());
268                        Some(&tmp)
269                    } else {
270                        None
271                    };
272                let value = WithUrlOnDemand {
273                    text: &s,
274                    gen_url,
275                    anchor_name: None,
276                };
277                table.print(value)?;
278                shown_sorted_keys = &all_sorted_keys[num_skipped..];
279            } else {
280                shown_sorted_keys = &all_sorted_keys;
281            }
282
283            let mut row: Vec<WithUrlOnDemand> = Vec::new();
284            for entry in queue.resolve_entries(shown_sorted_keys.into()) {
285                let mut entry = entry?;
286                let file_name = get_filename(&entry)?;
287                let key = entry.key()?;
288                let job = entry.get()?;
289                let reason = if let Some(reason) = &job.public.reason {
290                    reason.as_ref()
291                } else {
292                    ""
293                };
294                let (locking, is_locked) = if schedule_condition.is_inactive() {
295                    ("", false)
296                } else {
297                    let lock_status = entry
298                        .take_lockable_file()
299                        .expect("not taken before")
300                        .get_lock_status()?;
301                    if lock_status == LockStatus::ExclusiveLock {
302                        let s = if let Some(dir) = opt_current_working_directory {
303                            let status = lock.read_working_directory_status(dir)?;
304                            match status.status {
305                                // CheckedOut wasn't planned
306                                // to happen, but now happens
307                                // for new working dir
308                                // assignment
309                                Status::CheckedOut => "R0",
310                                Status::Processing => "R",  // running
311                                Status::Error => "F",       // failure
312                                Status::Finished => "E",    // evaluating
313                                Status::Examination => "X", // manually marked
314                            }
315                        } else {
316                            "R"
317                        };
318                        (s, true)
319                    } else {
320                        ("", false)
321                    }
322                };
323                let priority = &*job.priority()?.to_string();
324                let wd = if is_locked {
325                    opt_current_working_directory
326                        .map(|v| v.to_string())
327                        .unwrap_or_else(|| "".into())
328                } else {
329                    job.state
330                        .last_working_directory
331                        .map(|v| v.to_string())
332                        .unwrap_or_else(|| "".into())
333                };
334
335                let system_time = key.system_time();
336                let is_older = {
337                    let age = now.duration_since(system_time)?;
338                    age > Duration::from_secs(3600 * 24)
339                };
340                let time = if *verbose {
341                    format!("{file_name} ({key})")
342                } else {
343                    let datetime: DateTime<Local> = system_time.into();
344                    datetime.to_rfc3339()
345                };
346                row.extend_from_slice(&[
347                    (&*time).into(),
348                    locking.into(),
349                    priority.into(),
350                    (&*wd).into(),
351                    reason.into(),
352                ]);
353
354                let commit_id;
355                let custom_parameters;
356                let key_dir;
357                let path;
358                let gen_url_cache: OnceCell<Arc<Path>> = OnceCell::new();
359                let gen_url = || -> Option<Cow<'_, str>> {
360                    if let Some(url) = &conf.output_dir.url {
361                        Some(
362                            gen_url_cache
363                                .get_or_init(|| {
364                                    let key_dir = KeyDir::from_base_target_params(
365                                        url.into_arc_path(),
366                                        job.public.command.target_name.clone(),
367                                        &job.public.run_parameters,
368                                    );
369                                    let url_as_path = key_dir.to_path();
370                                    url_as_path.clone()
371                                })
372                                .to_str()
373                                .expect("always succeeds since generated from strings only")
374                                // Is it a Rust bug that this to_owned() is necessary?
375                                .to_owned()
376                                .into(),
377                        )
378                    } else {
379                        None
380                    }
381                };
382                match parameter_view {
383                    ParameterView::Separated => {
384                        commit_id = job.public.run_parameters.commit_id.to_string();
385                        let target_name = job.public.command.target_name.as_str();
386                        custom_parameters = job.public.run_parameters.custom_parameters.to_string();
387                        row.extend_from_slice(&[
388                            (&*commit_id).into(),
389                            WithUrlOnDemand {
390                                text: &target_name,
391                                gen_url: Some(&gen_url),
392                                anchor_name: None,
393                            },
394                            WithUrlOnDemand {
395                                text: &*custom_parameters,
396                                gen_url: Some(&gen_url),
397                                anchor_name: None,
398                            },
399                        ]);
400                    }
401                    ParameterView::Path { kind: _ } => {
402                        let base = path_base
403                            .as_ref()
404                            .expect("initialized for ParameterView::Path");
405                        key_dir = KeyDir::from_base_target_params(
406                            base.clone_arc(),
407                            job.public.command.target_name.clone(),
408                            &job.public.run_parameters,
409                        );
410                        path = key_dir.to_path().to_string_lossy();
411                        row.push(WithUrlOnDemand {
412                            text: &*path,
413                            gen_url: Some(&gen_url),
414                            anchor_name: None,
415                        });
416                    }
417                }
418                table.write_data_row(
419                    &row,
420                    if is_older {
421                        Some(OutputStyle {
422                            faded: true,
423                            ..Default::default()
424                        })
425                    } else {
426                        None
427                    },
428                )?;
429                if *verbose {
430                    let s = ron_to_string_pretty(&job)?;
431                    table.print(format!("{s}\n\n"))?;
432                }
433
434                row = row.recycle_vec();
435            }
436            table.write_bar(
437                bar_kind_after,
438                Some(&format!("end-{}", run_queue.file_name.as_str())),
439            )?;
440            Ok(())
441        };
442
443        table.write_bar(BarKind::Thick, None)?;
444
445        let pipeline_len = queues.pipeline().len();
446        for (i, run_queue) in queues.pipeline().iter().enumerate() {
447            let after_bar_kind = if i + 1 == pipeline_len {
448                BarKind::Thick
449            } else {
450                BarKind::Thin
451            };
452            show_queue(
453                &(i + 1).to_string(),
454                run_queue,
455                false,
456                &mut table,
457                after_bar_kind,
458            )?;
459        }
460
461        let perhaps_show_extra_queue = |queue_name: &str,
462                                        queue_field: &str,
463                                        run_queue: Option<&RunQueue>,
464                                        table: &mut Table|
465         -> Result<()> {
466            if let Some(run_queue) = run_queue {
467                show_queue(queue_name, run_queue, true, table, BarKind::Thin)?;
468            } else {
469                table.print(format!("No {queue_field} is configured"))?;
470            }
471            Ok(())
472        };
473        perhaps_show_extra_queue(
474            "done",
475            "done_jobs_queue",
476            queues.done_jobs_queue(),
477            &mut table,
478        )?;
479        perhaps_show_extra_queue(
480            "failures",
481            "erroneous_jobs_queue",
482            queues.erroneous_jobs_queue(),
483            &mut table,
484        )?;
485
486        table.finish()
487    }
488}
489
490#[derive(Debug, Clone, clap::Args)]
491pub struct ListOpts {
492    #[clap(flatten)]
493    terminal_table_opts: TerminalTableOpts,
494
495    /// Print table as HTML
496    #[clap(long)]
497    html: bool,
498
499    #[clap(flatten)]
500    output_table_opts: OutputTableOpts,
501}
502
503fn make_terminal_table<O: io::Write + IsTerminal>(
504    terminal_table_opts: &TerminalTableOpts,
505    out: O,
506    verbose: bool,
507    view: ParameterView,
508) -> TerminalTable<O> {
509    let insertion_time_width = if verbose { 82 } else { 37 };
510    let widths =
511    //     t                    R pr WD reason commit target
512        &[insertion_time_width, 3, 6, 5, 25, 42, TARGET_NAME_WIDTH];
513    let widths = match view {
514        ParameterView::Separated => widths,
515        ParameterView::Path { kind: _ } => &widths[0..5],
516    };
517    TerminalTable::new(widths, terminal_table_opts.clone(), out)
518}
519
520impl ListOpts {
521    pub fn run(
522        self,
523        conf: &RunConfig,
524        working_directory_base_dir: &Arc<WorkingDirectoryPoolBaseDir>,
525        queues: &RunQueues,
526    ) -> Result<()> {
527        let Self {
528            terminal_table_opts,
529            output_table_opts,
530            html,
531        } = self;
532
533        if html {
534            // From run/output_directory/html_files.rs
535            print_list(
536                conf,
537                working_directory_base_dir,
538                queues,
539                &output_table_opts,
540                None,
541                None,
542                stdout().lock(),
543            )?;
544        } else {
545            let out = stdout().lock();
546            let table = make_terminal_table(
547                &terminal_table_opts,
548                out,
549                output_table_opts.verbose,
550                output_table_opts.parameter_view.unwrap_or_default(),
551            );
552
553            let mut out = output_table_opts.output_to_table(
554                table,
555                conf,
556                None,
557                working_directory_base_dir,
558                queues,
559            )?;
560
561            out.flush()?;
562        }
563
564        Ok(())
565    }
566}