Restructure client display code

This commit is contained in:
Arne Beer 2021-01-15 17:13:26 +01:00
parent 20e99ace95
commit 07b145c491
11 changed files with 484 additions and 454 deletions

View file

@ -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

View file

@ -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(())
}

View file

@ -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
View 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
View 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
View 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
View 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
View 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);
}

View file

@ -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;

View file

@ -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);
}
}