mirror of
https://github.com/nukesor/pueue
synced 2024-10-02 22:13:56 +00:00
Restructure client display code
This commit is contained in:
parent
20e99ace95
commit
07b145c491
|
@ -17,7 +17,7 @@ use crate::commands::get_state;
|
|||
use crate::commands::local_follow::local_follow;
|
||||
use crate::commands::restart::restart;
|
||||
use crate::commands::wait::wait;
|
||||
use crate::output::*;
|
||||
use crate::display::*;
|
||||
|
||||
/// This struct contains the base logic for the client.
|
||||
/// The client is responsible for connecting to the daemon, sending instructions
|
||||
|
|
|
@ -5,7 +5,7 @@ use anyhow::{bail, Result};
|
|||
use pueue::network::protocol::GenericStream;
|
||||
|
||||
use crate::commands::get_state;
|
||||
use crate::output::follow_task_logs;
|
||||
use crate::display::follow_local_task_logs;
|
||||
|
||||
pub async fn local_follow(
|
||||
stream: &mut GenericStream,
|
||||
|
@ -46,7 +46,7 @@ pub async fn local_follow(
|
|||
}
|
||||
};
|
||||
|
||||
follow_task_logs(pueue_directory, task_id, err);
|
||||
follow_local_task_logs(pueue_directory, task_id, err);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use pueue::network::protocol::GenericStream;
|
|||
use pueue::task::{Task, TaskResult, TaskStatus};
|
||||
|
||||
use crate::commands::get_state;
|
||||
use crate::output_helper::style_text;
|
||||
use crate::display::helper::style_text;
|
||||
|
||||
/// Wait until tasks are done.
|
||||
/// Tasks can be specified by:
|
||||
|
|
45
client/display/follow.rs
Normal file
45
client/display/follow.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use pueue::log::{get_log_file_handles, get_log_paths};
|
||||
|
||||
/// Follow the log ouput of running task.
|
||||
///
|
||||
/// If no task is specified, this will check for the following cases:
|
||||
///
|
||||
/// - No running task: Print an error that there are no running tasks
|
||||
/// - Single running task: Follow the output of that task
|
||||
/// - Multiple running tasks: Print out the list of possible tasks to follow.
|
||||
pub fn follow_local_task_logs(pueue_directory: &PathBuf, task_id: usize, stderr: bool) {
|
||||
let (stdout_handle, stderr_handle) = match get_log_file_handles(task_id, &pueue_directory) {
|
||||
Ok((stdout, stderr)) => (stdout, stderr),
|
||||
Err(err) => {
|
||||
println!("Failed to get log file handles: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut handle = if stderr { stderr_handle } else { stdout_handle };
|
||||
|
||||
let (out_path, err_path) = get_log_paths(task_id, &pueue_directory);
|
||||
let handle_path = if stderr { err_path } else { out_path };
|
||||
|
||||
// Stdout handler to directly write log file output to io::stdout
|
||||
// without having to load anything into memory.
|
||||
let mut stdout = io::stdout();
|
||||
loop {
|
||||
// Check whether the file still exists. Exit if it doesn't.
|
||||
if !handle_path.exists() {
|
||||
println!("File has gone away. Did somebody remove the task?");
|
||||
return;
|
||||
}
|
||||
// Read the next chunk of text from the last position.
|
||||
if let Err(err) = io::copy(&mut handle, &mut stdout) {
|
||||
println!("Error while reading file: {}", err);
|
||||
return;
|
||||
};
|
||||
let timeout = Duration::from_millis(100);
|
||||
sleep(timeout);
|
||||
}
|
||||
}
|
18
client/display/group.rs
Normal file
18
client/display/group.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use pueue::network::message::GroupResponseMessage;
|
||||
|
||||
use super::helper::*;
|
||||
|
||||
pub fn print_groups(message: GroupResponseMessage) {
|
||||
let mut text = String::new();
|
||||
let mut group_iter = message.groups.iter().peekable();
|
||||
while let Some((name, status)) = group_iter.next() {
|
||||
let parallel = *message.settings.get(name).unwrap();
|
||||
let styled = get_group_headline(name, &status, parallel);
|
||||
|
||||
text.push_str(&styled);
|
||||
if group_iter.peek().is_some() {
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
println!("{}", text);
|
||||
}
|
186
client/display/log.rs
Normal file
186
client/display/log.rs
Normal file
|
@ -0,0 +1,186 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::io;
|
||||
|
||||
use anyhow::Result;
|
||||
use comfy_table::*;
|
||||
use snap::read::FrameDecoder;
|
||||
|
||||
use pueue::log::get_log_file_handles;
|
||||
use pueue::network::message::TaskLogMessage;
|
||||
use pueue::settings::Settings;
|
||||
use pueue::task::{TaskResult, TaskStatus};
|
||||
|
||||
use super::helper::*;
|
||||
use crate::cli::SubCommand;
|
||||
|
||||
/// Print the log ouput of finished tasks.
|
||||
/// Either print the logs of every task
|
||||
/// or only print the logs of the specified tasks.
|
||||
pub fn print_logs(
|
||||
mut task_logs: BTreeMap<usize, TaskLogMessage>,
|
||||
cli_command: &SubCommand,
|
||||
settings: &Settings,
|
||||
) {
|
||||
let (json, task_ids) = match cli_command {
|
||||
SubCommand::Log { json, task_ids } => (*json, task_ids.clone()),
|
||||
_ => panic!(
|
||||
"Got wrong Subcommand {:?} in print_log. This shouldn't happen",
|
||||
cli_command
|
||||
),
|
||||
};
|
||||
if json {
|
||||
println!("{}", serde_json::to_string(&task_logs).unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
if task_ids.is_empty() && task_logs.is_empty() {
|
||||
println!("There are no finished tasks");
|
||||
return;
|
||||
}
|
||||
|
||||
if !task_ids.is_empty() && task_logs.is_empty() {
|
||||
println!("There are no finished tasks for your specified ids");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut task_iter = task_logs.iter_mut().peekable();
|
||||
while let Some((_, mut task_log)) = task_iter.next() {
|
||||
print_log(&mut task_log, settings);
|
||||
|
||||
// Add a newline if there is another task that's going to be printed.
|
||||
if let Some((_, task_log)) = task_iter.peek() {
|
||||
if !vec![TaskStatus::Done, TaskStatus::Running, TaskStatus::Paused]
|
||||
.contains(&task_log.task.status)
|
||||
{
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the log of a single task.
|
||||
pub fn print_log(task_log: &mut TaskLogMessage, settings: &Settings) {
|
||||
let task = &task_log.task;
|
||||
// We only show logs of finished or running tasks.
|
||||
if !vec![TaskStatus::Done, TaskStatus::Running, TaskStatus::Paused].contains(&task.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Print task id and exit code.
|
||||
let task_text = style_text(&format!("Task {}", task.id), None, Some(Attribute::Bold));
|
||||
let (exit_status, color) = match &task.result {
|
||||
Some(TaskResult::Success) => ("completed successfully".into(), Color::Green),
|
||||
Some(TaskResult::Failed(exit_code)) => {
|
||||
(format!("failed with exit code {}", exit_code), Color::Red)
|
||||
}
|
||||
Some(TaskResult::FailedToSpawn(err)) => (format!("failed to spawn: {}", err), Color::Red),
|
||||
Some(TaskResult::Killed) => ("killed by system or user".into(), Color::Red),
|
||||
Some(TaskResult::Errored) => ("some IO error.\n Check daemon log.".into(), Color::Red),
|
||||
Some(TaskResult::DependencyFailed) => ("dependency failed".into(), Color::Red),
|
||||
None => ("running".into(), Color::White),
|
||||
};
|
||||
let status_text = style_text(&exit_status, Some(color), None);
|
||||
println!("{} {}", task_text, status_text);
|
||||
|
||||
// Print command and path.
|
||||
println!("Command: {}", task.command);
|
||||
println!("Path: {}", task.path);
|
||||
|
||||
if let Some(start) = task.start {
|
||||
println!("Start: {}", start.to_rfc2822());
|
||||
}
|
||||
if let Some(end) = task.end {
|
||||
println!("End: {}", end.to_rfc2822());
|
||||
}
|
||||
|
||||
if settings.client.read_local_logs {
|
||||
print_local_log_output(task_log.task.id, settings);
|
||||
} else if task_log.stdout.is_some() && task_log.stderr.is_some() {
|
||||
print_task_output_from_daemon(task_log);
|
||||
} else {
|
||||
println!("Logs requested from pueue daemon, but none received. Please report this bug.");
|
||||
}
|
||||
}
|
||||
|
||||
/// The daemon didn't send any log output, thereby we didn't request any.
|
||||
/// If that's the case, read the log files from the local pueue directory
|
||||
pub fn print_local_log_output(task_id: usize, settings: &Settings) {
|
||||
let (mut stdout_log, mut stderr_log) =
|
||||
match get_log_file_handles(task_id, &settings.shared.pueue_directory) {
|
||||
Ok((stdout, stderr)) => (stdout, stderr),
|
||||
Err(err) => {
|
||||
println!("Failed to get log file handles: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Stdout handler to directly write log file output to io::stdout
|
||||
// without having to load anything into memory.
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
if let Ok(metadata) = stdout_log.metadata() {
|
||||
if metadata.len() != 0 {
|
||||
println!(
|
||||
"\n{}",
|
||||
style_text("stdout:", Some(Color::Green), Some(Attribute::Bold))
|
||||
);
|
||||
|
||||
if let Err(err) = io::copy(&mut stdout_log, &mut stdout) {
|
||||
println!("Failed reading local stdout log file: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(metadata) = stderr_log.metadata() {
|
||||
if metadata.len() != 0 {
|
||||
// Add a spacer line between stdout and stderr
|
||||
println!(
|
||||
"\n{}",
|
||||
style_text("stderr:", Some(Color::Red), Some(Attribute::Bold))
|
||||
);
|
||||
|
||||
if let Err(err) = io::copy(&mut stderr_log, &mut stdout) {
|
||||
println!("Failed reading local stderr log file: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints log output received from the daemon.
|
||||
/// We can safely call .unwrap() on stdout and stderr in here, since this
|
||||
/// branch is always called after ensuring that both are `Some`.
|
||||
pub fn print_task_output_from_daemon(task_log: &TaskLogMessage) {
|
||||
// Save whether stdout was printed, so we can add a newline between outputs.
|
||||
if !task_log.stdout.as_ref().unwrap().is_empty() {
|
||||
if let Err(err) = print_remote_task_output(&task_log, true) {
|
||||
println!("Error while parsing stdout: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if !task_log.stderr.as_ref().unwrap().is_empty() {
|
||||
if let Err(err) = print_remote_task_output(&task_log, false) {
|
||||
println!("Error while parsing stderr: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Print log output of a finished process.
|
||||
pub fn print_remote_task_output(task_log: &TaskLogMessage, stdout: bool) -> Result<()> {
|
||||
let (pre_text, color, bytes) = if stdout {
|
||||
("stdout: ", Color::Green, task_log.stdout.as_ref().unwrap())
|
||||
} else {
|
||||
("stderr: ", Color::Red, task_log.stderr.as_ref().unwrap())
|
||||
};
|
||||
|
||||
println!(
|
||||
"\n{}",
|
||||
style_text(pre_text, Some(color), Some(Attribute::Bold))
|
||||
);
|
||||
|
||||
let mut decompressor = FrameDecoder::new(bytes.as_slice());
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut write = stdout.lock();
|
||||
io::copy(&mut decompressor, &mut write)?;
|
||||
|
||||
Ok(())
|
||||
}
|
24
client/display/mod.rs
Normal file
24
client/display/mod.rs
Normal file
|
@ -0,0 +1,24 @@
|
|||
use comfy_table::Color;
|
||||
|
||||
mod follow;
|
||||
mod group;
|
||||
pub mod helper;
|
||||
mod log;
|
||||
mod state;
|
||||
|
||||
use self::helper::style_text;
|
||||
|
||||
// Re-exports
|
||||
pub use self::follow::follow_local_task_logs;
|
||||
pub use self::group::print_groups;
|
||||
pub use self::log::print_logs;
|
||||
pub use self::state::print_state;
|
||||
|
||||
pub fn print_success(message: &str) {
|
||||
println!("{}", message);
|
||||
}
|
||||
|
||||
pub fn print_error(message: &str) {
|
||||
let styled = style_text(message, Some(Color::Red), None);
|
||||
println!("{}", styled);
|
||||
}
|
206
client/display/state.rs
Normal file
206
client/display/state.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::string::ToString;
|
||||
|
||||
use comfy_table::presets::UTF8_HORIZONTAL_BORDERS_ONLY;
|
||||
use comfy_table::*;
|
||||
|
||||
use pueue::settings::Settings;
|
||||
use pueue::state::State;
|
||||
use pueue::task::{Task, TaskResult, TaskStatus};
|
||||
|
||||
use super::helper::*;
|
||||
use crate::cli::SubCommand;
|
||||
|
||||
/// Print the current state of the daemon in a nicely formatted table.
|
||||
pub fn print_state(state: State, cli_command: &SubCommand, settings: &Settings) {
|
||||
let (json, group_only) = match cli_command {
|
||||
SubCommand::Status { json, group } => (*json, group.clone()),
|
||||
_ => panic!(
|
||||
"Got wrong Subcommand {:?} in print_state. This shouldn't happen",
|
||||
cli_command
|
||||
),
|
||||
};
|
||||
|
||||
// If the json flag is specified, print the state as json and exit.
|
||||
if json {
|
||||
println!("{}", serde_json::to_string(&state).unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Early exit and hint if there are no tasks in the queue
|
||||
if state.tasks.is_empty() {
|
||||
println!("Task list is empty. Add tasks with `pueue add -- [cmd]`");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort all tasks by their respective group;
|
||||
let sorted_tasks = sort_tasks_by_group(&state.tasks);
|
||||
|
||||
// Always print the default queue at the very top.
|
||||
if group_only.is_none() && sorted_tasks.get("default").is_some() {
|
||||
let tasks = sorted_tasks.get("default").unwrap();
|
||||
let headline = get_group_headline(
|
||||
&"default",
|
||||
&state.groups.get("default").unwrap(),
|
||||
*state.settings.daemon.groups.get("default").unwrap(),
|
||||
);
|
||||
println!("{}", headline);
|
||||
print_table(&tasks, settings);
|
||||
|
||||
// Add a newline if there are further groups to be printed
|
||||
if sorted_tasks.len() > 1 {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_iter = sorted_tasks.iter().peekable();
|
||||
// Print new table for each group
|
||||
while let Some((group, tasks)) = sorted_iter.next() {
|
||||
// We always want to print the default group at the very top.
|
||||
// That's why we print it outside of this loop and skip it in here.
|
||||
if group.eq("default") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip unwanted groups, if a single group is requested
|
||||
if let Some(group_only) = &group_only {
|
||||
if group_only != group {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let headline = get_group_headline(
|
||||
&group,
|
||||
&state.groups.get(group).unwrap(),
|
||||
*state.settings.daemon.groups.get(group).unwrap(),
|
||||
);
|
||||
println!("{}", headline);
|
||||
print_table(&tasks, settings);
|
||||
|
||||
// Add a newline between groups
|
||||
if sorted_iter.peek().is_some() {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print some tasks into a nicely formatted table
|
||||
fn print_table(tasks: &BTreeMap<usize, Task>, settings: &Settings) {
|
||||
let (has_delayed_tasks, has_dependencies, has_labels) = has_special_columns(tasks);
|
||||
|
||||
// Create table header row
|
||||
let mut headers = vec![Cell::new("Index"), Cell::new("Status")];
|
||||
if has_delayed_tasks {
|
||||
headers.push(Cell::new("Enqueue At"));
|
||||
}
|
||||
if has_dependencies {
|
||||
headers.push(Cell::new("Deps"));
|
||||
}
|
||||
|
||||
headers.push(Cell::new("Exitcode"));
|
||||
|
||||
if has_labels {
|
||||
headers.push(Cell::new("Label"));
|
||||
}
|
||||
|
||||
headers.append(&mut vec![
|
||||
Cell::new("Command"),
|
||||
Cell::new("Path"),
|
||||
Cell::new("Start"),
|
||||
Cell::new("End"),
|
||||
]);
|
||||
|
||||
// Initialize comfy table.
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.load_preset(UTF8_HORIZONTAL_BORDERS_ONLY)
|
||||
.set_header(headers);
|
||||
|
||||
// Add rows one by one.
|
||||
for (id, task) in tasks {
|
||||
let mut row = Row::new();
|
||||
if let Some(height) = settings.client.max_status_lines {
|
||||
row.max_height(height);
|
||||
}
|
||||
row.add_cell(Cell::new(&id.to_string()));
|
||||
|
||||
// Determine the human readable task status representation and the respective color.
|
||||
let status_string = task.status.to_string();
|
||||
let (status_text, color) = match task.status {
|
||||
TaskStatus::Running => (status_string, Color::Green),
|
||||
TaskStatus::Paused | TaskStatus::Locked => (status_string, Color::White),
|
||||
TaskStatus::Done => match &task.result {
|
||||
Some(TaskResult::Success) => (TaskResult::Success.to_string(), Color::Green),
|
||||
Some(TaskResult::DependencyFailed) => ("Dependency failed".to_string(), Color::Red),
|
||||
Some(TaskResult::FailedToSpawn(_)) => ("Failed to spawn".to_string(), Color::Red),
|
||||
Some(result) => (result.to_string(), Color::Red),
|
||||
None => panic!("Got a 'Done' task without a task result. Please report this bug."),
|
||||
},
|
||||
_ => (status_string, Color::Yellow),
|
||||
};
|
||||
row.add_cell(Cell::new(status_text).fg(color));
|
||||
|
||||
if has_delayed_tasks {
|
||||
if let Some(enqueue_at) = task.enqueue_at {
|
||||
row.add_cell(Cell::new(enqueue_at.format("%Y-%m-%d\n%H:%M:%S")));
|
||||
} else {
|
||||
row.add_cell(Cell::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
if has_dependencies {
|
||||
let text = task
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
row.add_cell(Cell::new(text));
|
||||
}
|
||||
|
||||
// Match the color of the exit code.
|
||||
// If the exit_code is none, it has been killed by the task handler.
|
||||
let exit_code_cell = match task.result {
|
||||
Some(TaskResult::Success) => Cell::new("0").fg(Color::Green),
|
||||
Some(TaskResult::Failed(code)) => Cell::new(&code.to_string()).fg(Color::Red),
|
||||
_ => Cell::new(""),
|
||||
};
|
||||
row.add_cell(exit_code_cell);
|
||||
if has_labels {
|
||||
if let Some(label) = &task.label {
|
||||
row.add_cell(label.to_cell());
|
||||
} else {
|
||||
row.add_cell(Cell::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
// Add command and path.
|
||||
if settings.client.show_expanded_aliases {
|
||||
row.add_cell(Cell::new(&task.command));
|
||||
} else {
|
||||
row.add_cell(Cell::new(&task.original_command));
|
||||
}
|
||||
row.add_cell(Cell::new(&task.path));
|
||||
|
||||
// Add start time, if already set.
|
||||
if let Some(start) = task.start {
|
||||
let formatted = start.format("%H:%M").to_string();
|
||||
row.add_cell(Cell::new(&formatted));
|
||||
} else {
|
||||
row.add_cell(Cell::new(""));
|
||||
}
|
||||
|
||||
// Add finish time, if already set.
|
||||
if let Some(end) = task.end {
|
||||
let formatted = end.format("%H:%M").to_string();
|
||||
row.add_cell(Cell::new(&formatted));
|
||||
} else {
|
||||
row.add_cell(Cell::new(""));
|
||||
}
|
||||
|
||||
table.add_row(row);
|
||||
}
|
||||
|
||||
// Print the table.
|
||||
println!("{}", table);
|
||||
}
|
|
@ -9,8 +9,7 @@ use pueue::settings::Settings;
|
|||
pub mod cli;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod output;
|
||||
pub mod output_helper;
|
||||
pub mod display;
|
||||
|
||||
use crate::cli::{CliArguments, Shell, SubCommand};
|
||||
use crate::client::Client;
|
||||
|
|
448
client/output.rs
448
client/output.rs
|
@ -1,448 +0,0 @@
|
|||
use std::io;
|
||||
use std::string::ToString;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use std::{collections::BTreeMap, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use comfy_table::presets::UTF8_HORIZONTAL_BORDERS_ONLY;
|
||||
use comfy_table::*;
|
||||
use snap::read::FrameDecoder;
|
||||
|
||||
use pueue::log::{get_log_file_handles, get_log_paths};
|
||||
use pueue::network::message::{GroupResponseMessage, TaskLogMessage};
|
||||
use pueue::settings::Settings;
|
||||
use pueue::state::State;
|
||||
use pueue::task::{Task, TaskResult, TaskStatus};
|
||||
|
||||
use crate::cli::SubCommand;
|
||||
use crate::output_helper::*;
|
||||
|
||||
pub fn print_success(message: &str) {
|
||||
println!("{}", message);
|
||||
}
|
||||
|
||||
pub fn print_error(message: &str) {
|
||||
let styled = style_text(message, Some(Color::Red), None);
|
||||
println!("{}", styled);
|
||||
}
|
||||
|
||||
pub fn print_groups(message: GroupResponseMessage) {
|
||||
let mut text = String::new();
|
||||
let mut group_iter = message.groups.iter().peekable();
|
||||
while let Some((name, status)) = group_iter.next() {
|
||||
let parallel = *message.settings.get(name).unwrap();
|
||||
let styled = get_group_headline(name, &status, parallel);
|
||||
|
||||
text.push_str(&styled);
|
||||
if group_iter.peek().is_some() {
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
println!("{}", text);
|
||||
}
|
||||
|
||||
/// Print the current state of the daemon in a nicely formatted table.
|
||||
pub fn print_state(state: State, cli_command: &SubCommand, settings: &Settings) {
|
||||
let (json, group_only) = match cli_command {
|
||||
SubCommand::Status { json, group } => (*json, group.clone()),
|
||||
_ => panic!(
|
||||
"Got wrong Subcommand {:?} in print_state. This shouldn't happen",
|
||||
cli_command
|
||||
),
|
||||
};
|
||||
|
||||
// If the json flag is specified, print the state as json and exit.
|
||||
if json {
|
||||
println!("{}", serde_json::to_string(&state).unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
// Early exit and hint if there are no tasks in the queue
|
||||
if state.tasks.is_empty() {
|
||||
println!("Task list is empty. Add tasks with `pueue add -- [cmd]`");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort all tasks by their respective group;
|
||||
let sorted_tasks = sort_tasks_by_group(&state.tasks);
|
||||
|
||||
// Always print the default queue at the very top.
|
||||
if group_only.is_none() && sorted_tasks.get("default").is_some() {
|
||||
let tasks = sorted_tasks.get("default").unwrap();
|
||||
let headline = get_group_headline(
|
||||
&"default",
|
||||
&state.groups.get("default").unwrap(),
|
||||
*state.settings.daemon.groups.get("default").unwrap(),
|
||||
);
|
||||
println!("{}", headline);
|
||||
print_table(&tasks, settings);
|
||||
|
||||
// Add a newline if there are further groups to be printed
|
||||
if sorted_tasks.len() > 1 {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
let mut sorted_iter = sorted_tasks.iter().peekable();
|
||||
// Print new table for each group
|
||||
while let Some((group, tasks)) = sorted_iter.next() {
|
||||
// We always want to print the default group at the very top.
|
||||
// That's why we print it outside of this loop and skip it in here.
|
||||
if group.eq("default") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip unwanted groups, if a single group is requested
|
||||
if let Some(group_only) = &group_only {
|
||||
if group_only != group {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let headline = get_group_headline(
|
||||
&group,
|
||||
&state.groups.get(group).unwrap(),
|
||||
*state.settings.daemon.groups.get(group).unwrap(),
|
||||
);
|
||||
println!("{}", headline);
|
||||
print_table(&tasks, settings);
|
||||
|
||||
// Add a newline between groups
|
||||
if sorted_iter.peek().is_some() {
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print some tasks into a nicely formatted table
|
||||
fn print_table(tasks: &BTreeMap<usize, Task>, settings: &Settings) {
|
||||
let (has_delayed_tasks, has_dependencies, has_labels) = has_special_columns(tasks);
|
||||
|
||||
// Create table header row
|
||||
let mut headers = vec![Cell::new("Index"), Cell::new("Status")];
|
||||
if has_delayed_tasks {
|
||||
headers.push(Cell::new("Enqueue At"));
|
||||
}
|
||||
if has_dependencies {
|
||||
headers.push(Cell::new("Deps"));
|
||||
}
|
||||
|
||||
headers.push(Cell::new("Exitcode"));
|
||||
|
||||
if has_labels {
|
||||
headers.push(Cell::new("Label"));
|
||||
}
|
||||
|
||||
headers.append(&mut vec![
|
||||
Cell::new("Command"),
|
||||
Cell::new("Path"),
|
||||
Cell::new("Start"),
|
||||
Cell::new("End"),
|
||||
]);
|
||||
|
||||
// Initialize comfy table.
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.load_preset(UTF8_HORIZONTAL_BORDERS_ONLY)
|
||||
.set_header(headers);
|
||||
|
||||
// Add rows one by one.
|
||||
for (id, task) in tasks {
|
||||
let mut row = Row::new();
|
||||
if let Some(height) = settings.client.max_status_lines {
|
||||
row.max_height(height);
|
||||
}
|
||||
row.add_cell(Cell::new(&id.to_string()));
|
||||
|
||||
// Determine the human readable task status representation and the respective color.
|
||||
let status_string = task.status.to_string();
|
||||
let (status_text, color) = match task.status {
|
||||
TaskStatus::Running => (status_string, Color::Green),
|
||||
TaskStatus::Paused | TaskStatus::Locked => (status_string, Color::White),
|
||||
TaskStatus::Done => match &task.result {
|
||||
Some(TaskResult::Success) => (TaskResult::Success.to_string(), Color::Green),
|
||||
Some(TaskResult::DependencyFailed) => ("Dependency failed".to_string(), Color::Red),
|
||||
Some(TaskResult::FailedToSpawn(_)) => ("Failed to spawn".to_string(), Color::Red),
|
||||
Some(result) => (result.to_string(), Color::Red),
|
||||
None => panic!("Got a 'Done' task without a task result. Please report this bug."),
|
||||
},
|
||||
_ => (status_string, Color::Yellow),
|
||||
};
|
||||
row.add_cell(Cell::new(status_text).fg(color));
|
||||
|
||||
if has_delayed_tasks {
|
||||
if let Some(enqueue_at) = task.enqueue_at {
|
||||
row.add_cell(Cell::new(enqueue_at.format("%Y-%m-%d\n%H:%M:%S")));
|
||||
} else {
|
||||
row.add_cell(Cell::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
if has_dependencies {
|
||||
let text = task
|
||||
.dependencies
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
row.add_cell(Cell::new(text));
|
||||
}
|
||||
|
||||
// Match the color of the exit code.
|
||||
// If the exit_code is none, it has been killed by the task handler.
|
||||
let exit_code_cell = match task.result {
|
||||
Some(TaskResult::Success) => Cell::new("0").fg(Color::Green),
|
||||
Some(TaskResult::Failed(code)) => Cell::new(&code.to_string()).fg(Color::Red),
|
||||
_ => Cell::new(""),
|
||||
};
|
||||
row.add_cell(exit_code_cell);
|
||||
if has_labels {
|
||||
if let Some(label) = &task.label {
|
||||
row.add_cell(label.to_cell());
|
||||
} else {
|
||||
row.add_cell(Cell::new(""));
|
||||
}
|
||||
}
|
||||
|
||||
// Add command and path.
|
||||
if settings.client.show_expanded_aliases {
|
||||
row.add_cell(Cell::new(&task.command));
|
||||
} else {
|
||||
row.add_cell(Cell::new(&task.original_command));
|
||||
}
|
||||
row.add_cell(Cell::new(&task.path));
|
||||
|
||||
// Add start time, if already set.
|
||||
if let Some(start) = task.start {
|
||||
let formatted = start.format("%H:%M").to_string();
|
||||
row.add_cell(Cell::new(&formatted));
|
||||
} else {
|
||||
row.add_cell(Cell::new(""));
|
||||
}
|
||||
|
||||
// Add finish time, if already set.
|
||||
if let Some(end) = task.end {
|
||||
let formatted = end.format("%H:%M").to_string();
|
||||
row.add_cell(Cell::new(&formatted));
|
||||
} else {
|
||||
row.add_cell(Cell::new(""));
|
||||
}
|
||||
|
||||
table.add_row(row);
|
||||
}
|
||||
|
||||
// Print the table.
|
||||
println!("{}", table);
|
||||
}
|
||||
|
||||
/// Print the log ouput of finished tasks.
|
||||
/// Either print the logs of every task
|
||||
/// or only print the logs of the specified tasks.
|
||||
pub fn print_logs(
|
||||
mut task_logs: BTreeMap<usize, TaskLogMessage>,
|
||||
cli_command: &SubCommand,
|
||||
settings: &Settings,
|
||||
) {
|
||||
let (json, task_ids) = match cli_command {
|
||||
SubCommand::Log { json, task_ids } => (*json, task_ids.clone()),
|
||||
_ => panic!(
|
||||
"Got wrong Subcommand {:?} in print_log. This shouldn't happen",
|
||||
cli_command
|
||||
),
|
||||
};
|
||||
if json {
|
||||
println!("{}", serde_json::to_string(&task_logs).unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
if task_ids.is_empty() && task_logs.is_empty() {
|
||||
println!("There are no finished tasks");
|
||||
return;
|
||||
}
|
||||
|
||||
if !task_ids.is_empty() && task_logs.is_empty() {
|
||||
println!("There are no finished tasks for your specified ids");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut task_iter = task_logs.iter_mut().peekable();
|
||||
while let Some((_, mut task_log)) = task_iter.next() {
|
||||
print_log(&mut task_log, settings);
|
||||
|
||||
// Add a newline if there is another task that's going to be printed.
|
||||
if let Some((_, task_log)) = task_iter.peek() {
|
||||
if !vec![TaskStatus::Done, TaskStatus::Running, TaskStatus::Paused]
|
||||
.contains(&task_log.task.status)
|
||||
{
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print the log of a single task.
|
||||
pub fn print_log(task_log: &mut TaskLogMessage, settings: &Settings) {
|
||||
let task = &task_log.task;
|
||||
// We only show logs of finished or running tasks.
|
||||
if !vec![TaskStatus::Done, TaskStatus::Running, TaskStatus::Paused].contains(&task.status) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Print task id and exit code.
|
||||
let task_text = style_text(&format!("Task {}", task.id), None, Some(Attribute::Bold));
|
||||
let (exit_status, color) = match &task.result {
|
||||
Some(TaskResult::Success) => ("completed successfully".into(), Color::Green),
|
||||
Some(TaskResult::Failed(exit_code)) => {
|
||||
(format!("failed with exit code {}", exit_code), Color::Red)
|
||||
}
|
||||
Some(TaskResult::FailedToSpawn(err)) => (format!("failed to spawn: {}", err), Color::Red),
|
||||
Some(TaskResult::Killed) => ("killed by system or user".into(), Color::Red),
|
||||
Some(TaskResult::Errored) => ("some IO error.\n Check daemon log.".into(), Color::Red),
|
||||
Some(TaskResult::DependencyFailed) => ("dependency failed".into(), Color::Red),
|
||||
None => ("running".into(), Color::White),
|
||||
};
|
||||
let status_text = style_text(&exit_status, Some(color), None);
|
||||
println!("{} {}", task_text, status_text);
|
||||
|
||||
// Print command and path.
|
||||
println!("Command: {}", task.command);
|
||||
println!("Path: {}", task.path);
|
||||
|
||||
if let Some(start) = task.start {
|
||||
println!("Start: {}", start.to_rfc2822());
|
||||
}
|
||||
if let Some(end) = task.end {
|
||||
println!("End: {}", end.to_rfc2822());
|
||||
}
|
||||
|
||||
if settings.client.read_local_logs {
|
||||
print_local_log_output(task_log.task.id, settings);
|
||||
} else if task_log.stdout.is_some() && task_log.stderr.is_some() {
|
||||
print_task_output_from_daemon(task_log);
|
||||
} else {
|
||||
println!("Logs requested from pueue daemon, but none received. Please report this bug.");
|
||||
}
|
||||
}
|
||||
|
||||
/// The daemon didn't send any log output, thereby we didn't request any.
|
||||
/// If that's the case, read the log files from the local pueue directory
|
||||
pub fn print_local_log_output(task_id: usize, settings: &Settings) {
|
||||
let (mut stdout_log, mut stderr_log) =
|
||||
match get_log_file_handles(task_id, &settings.shared.pueue_directory) {
|
||||
Ok((stdout, stderr)) => (stdout, stderr),
|
||||
Err(err) => {
|
||||
println!("Failed to get log file handles: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Stdout handler to directly write log file output to io::stdout
|
||||
// without having to load anything into memory.
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
if let Ok(metadata) = stdout_log.metadata() {
|
||||
if metadata.len() != 0 {
|
||||
println!(
|
||||
"\n{}",
|
||||
style_text("stdout:", Some(Color::Green), Some(Attribute::Bold))
|
||||
);
|
||||
|
||||
if let Err(err) = io::copy(&mut stdout_log, &mut stdout) {
|
||||
println!("Failed reading local stdout log file: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(metadata) = stderr_log.metadata() {
|
||||
if metadata.len() != 0 {
|
||||
// Add a spacer line between stdout and stderr
|
||||
println!(
|
||||
"\n{}",
|
||||
style_text("stderr:", Some(Color::Red), Some(Attribute::Bold))
|
||||
);
|
||||
|
||||
if let Err(err) = io::copy(&mut stderr_log, &mut stdout) {
|
||||
println!("Failed reading local stderr log file: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints log output received from the daemon.
|
||||
/// We can safely call .unwrap() on stdout and stderr in here, since this
|
||||
/// branch is always called after ensuring that both are `Some`.
|
||||
pub fn print_task_output_from_daemon(task_log: &TaskLogMessage) {
|
||||
// Save whether stdout was printed, so we can add a newline between outputs.
|
||||
if !task_log.stdout.as_ref().unwrap().is_empty() {
|
||||
if let Err(err) = print_remote_task_output(&task_log, true) {
|
||||
println!("Error while parsing stdout: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if !task_log.stderr.as_ref().unwrap().is_empty() {
|
||||
if let Err(err) = print_remote_task_output(&task_log, false) {
|
||||
println!("Error while parsing stderr: {}", err);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Print log output of a finished process.
|
||||
pub fn print_remote_task_output(task_log: &TaskLogMessage, stdout: bool) -> Result<()> {
|
||||
let (pre_text, color, bytes) = if stdout {
|
||||
("stdout: ", Color::Green, task_log.stdout.as_ref().unwrap())
|
||||
} else {
|
||||
("stderr: ", Color::Red, task_log.stderr.as_ref().unwrap())
|
||||
};
|
||||
|
||||
println!(
|
||||
"\n{}",
|
||||
style_text(pre_text, Some(color), Some(Attribute::Bold))
|
||||
);
|
||||
|
||||
let mut decompressor = FrameDecoder::new(bytes.as_slice());
|
||||
|
||||
let stdout = io::stdout();
|
||||
let mut write = stdout.lock();
|
||||
io::copy(&mut decompressor, &mut write)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Follow the log ouput of running task.
|
||||
///
|
||||
/// If no task is specified, this will check for the following cases:
|
||||
///
|
||||
/// - No running task: Print an error that there are no running tasks
|
||||
/// - Single running task: Follow the output of that task
|
||||
/// - Multiple running tasks: Print out the list of possible tasks to follow.
|
||||
pub fn follow_task_logs(pueue_directory: &PathBuf, task_id: usize, stderr: bool) {
|
||||
let (stdout_handle, stderr_handle) = match get_log_file_handles(task_id, &pueue_directory) {
|
||||
Ok((stdout, stderr)) => (stdout, stderr),
|
||||
Err(err) => {
|
||||
println!("Failed to get log file handles: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut handle = if stderr { stderr_handle } else { stdout_handle };
|
||||
|
||||
let (out_path, err_path) = get_log_paths(task_id, &pueue_directory);
|
||||
let handle_path = if stderr { err_path } else { out_path };
|
||||
|
||||
// Stdout handler to directly write log file output to io::stdout
|
||||
// without having to load anything into memory.
|
||||
let mut stdout = io::stdout();
|
||||
loop {
|
||||
// Check whether the file still exists. Exit if it doesn't.
|
||||
if !handle_path.exists() {
|
||||
println!("File has gone away. Did somebody remove the task?");
|
||||
return;
|
||||
}
|
||||
// Read the next chunk of text from the last position.
|
||||
if let Err(err) = io::copy(&mut handle, &mut stdout) {
|
||||
println!("Error while reading file: {}", err);
|
||||
return;
|
||||
};
|
||||
let timeout = Duration::from_millis(100);
|
||||
sleep(timeout);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue