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,
44 Full,
46 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 #[default]
61 Separated,
62 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", "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 #[clap(short, long)]
99 pub verbose: bool,
100
101 #[clap(short, long)]
105 pub all: bool,
106
107 #[clap(short, long)]
111 pub n: Option<usize>,
112
113 #[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 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 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 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 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 table.write_title_row(titles, None)?;
230
231 let view_jobs_max_len = n.unwrap_or(conf.queues.view_jobs_max_len);
250 let limit = if is_extra_queue && !all {
251 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 Status::CheckedOut => "R0",
310 Status::Processing => "R", Status::Error => "F", Status::Finished => "E", Status::Examination => "X", }
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 .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 #[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 &[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 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}