Clean up logging / progress display

This commit is contained in:
Zhaofeng Li 2021-02-09 19:28:45 -08:00
parent 8934726664
commit a2fa8f1da7
15 changed files with 486 additions and 346 deletions

1
Cargo.lock generated
View file

@ -96,6 +96,7 @@ name = "colmena"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"atty",
"clap", "clap",
"console", "console",
"env_logger", "env_logger",

View file

@ -8,6 +8,7 @@ edition = "2018"
[dependencies] [dependencies]
async-trait = "0.1.42" async-trait = "0.1.42"
atty = "0.2"
clap = "2.33.3" clap = "2.33.3"
console = "0.13.0" console = "0.13.0"
env_logger = "0.8.2" env_logger = "0.8.2"

View file

@ -17,5 +17,5 @@ in rustPlatform.buildRustPackage {
src = ./.; src = ./.;
}; };
}; };
cargoSha256 = "0xxp6hklnpdidrydvahv26vvpdysa7x7sf19vll8fb8zgbhjfvjm"; cargoSha256 = "0imalrw8im6zl5lq8k5j05msykax85lya39vq0fxagifdckcdfsb";
} }

View file

@ -5,8 +5,8 @@ use clap::{Arg, App, SubCommand, ArgMatches};
use crate::nix::deployment::{ use crate::nix::deployment::{
Deployment, Deployment,
DeploymentGoal, Goal,
DeploymentTarget, Target,
DeploymentOptions, DeploymentOptions,
EvaluationNodeLimit, EvaluationNodeLimit,
ParallelismLimit, ParallelismLimit,
@ -134,12 +134,12 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
// FIXME: This is ugly :/ Make an enum wrapper for this fake "keys" goal // FIXME: This is ugly :/ Make an enum wrapper for this fake "keys" goal
let goal_arg = local_args.value_of("goal").unwrap(); let goal_arg = local_args.value_of("goal").unwrap();
let goal = if goal_arg == "keys" { let goal = if goal_arg == "keys" {
DeploymentGoal::Build Goal::Build
} else { } else {
DeploymentGoal::from_str(goal_arg).unwrap() Goal::from_str(goal_arg).unwrap()
}; };
let build_only = goal == DeploymentGoal::Build && goal_arg != "keys"; let build_only = goal == Goal::Build && goal_arg != "keys";
let mut targets = HashMap::new(); let mut targets = HashMap::new();
for node in &selected_nodes { for node in &selected_nodes {
@ -149,14 +149,14 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
Some(host) => { Some(host) => {
targets.insert( targets.insert(
node.clone(), node.clone(),
DeploymentTarget::new(host, config.clone()), Target::new(host, config.clone()),
); );
} }
None => { None => {
if build_only { if build_only {
targets.insert( targets.insert(
node.clone(), node.clone(),
DeploymentTarget::new(localhost(), config.clone()), Target::new(localhost(), config.clone()),
); );
} }
} }

View file

@ -8,8 +8,8 @@ use tokio::process::Command;
use crate::nix::deployment::{ use crate::nix::deployment::{
Deployment, Deployment,
DeploymentGoal, Goal,
DeploymentTarget, Target,
DeploymentOptions, DeploymentOptions,
}; };
use crate::nix::host; use crate::nix::host;
@ -88,19 +88,19 @@ pub async fn run(_global_args: &ArgMatches<'_>, local_args: &ArgMatches<'_>) {
hostname::get().expect("Could not get hostname") hostname::get().expect("Could not get hostname")
.to_string_lossy().into_owned() .to_string_lossy().into_owned()
}; };
let goal = DeploymentGoal::from_str(local_args.value_of("goal").unwrap()).unwrap(); let goal = Goal::from_str(local_args.value_of("goal").unwrap()).unwrap();
log::info!("Enumerating nodes..."); log::info!("Enumerating nodes...");
let all_nodes = hive.deployment_info().await.unwrap(); let all_nodes = hive.deployment_info().await.unwrap();
let target: DeploymentTarget = { let target: Target = {
if let Some(info) = all_nodes.get(&hostname) { if let Some(info) = all_nodes.get(&hostname) {
if !info.allows_local_deployment() { if !info.allows_local_deployment() {
log::error!("Local deployment is not enabled for host {}.", hostname); log::error!("Local deployment is not enabled for host {}.", hostname);
log::error!("Hint: Set deployment.allowLocalDeployment to true."); log::error!("Hint: Set deployment.allowLocalDeployment to true.");
quit::with_code(2); quit::with_code(2);
} }
DeploymentTarget::new( Target::new(
host::local(), host::local(),
info.clone(), info.clone(),
) )

View file

@ -1,3 +1,5 @@
#![feature(async_closure)]
use std::env; use std::env;
use clap::{App, AppSettings, Arg}; use clap::{App, AppSettings, Arg};

View file

@ -3,12 +3,10 @@ use std::sync::Arc;
use std::collections::HashMap; use std::collections::HashMap;
use futures::future::join_all; use futures::future::join_all;
use futures::join;
use indicatif::{MultiProgress, ProgressBar, ProgressStyle, ProgressDrawTarget};
use tokio::sync::{Mutex, Semaphore}; use tokio::sync::{Mutex, Semaphore};
use super::{Hive, Host, CopyOptions, NodeConfig, Profile, StoreDerivation, ProfileMap, host}; use super::{Hive, Host, CopyOptions, NodeConfig, Profile, StoreDerivation, ProfileMap, host};
use crate::progress::get_spinner_styles; use crate::progress::{Progress, ProcessProgress, OutputStyle};
/// Amount of RAM reserved for the system, in MB. /// Amount of RAM reserved for the system, in MB.
const EVAL_RESERVE_MB: u64 = 1024; const EVAL_RESERVE_MB: u64 = 1024;
@ -19,26 +17,21 @@ const EVAL_PER_HOST_MB: u64 = 512;
const BATCH_OPERATION_LABEL: &'static str = "(...)"; const BATCH_OPERATION_LABEL: &'static str = "(...)";
macro_rules! set_up_batch_progress_bar { macro_rules! set_up_batch_progress_bar {
($multi:ident, $style:ident, $chunk:ident, $single_text:expr, $batch_text:expr) => {{ ($progress:ident, $style:ident, $chunk:ident, $single_text:expr, $batch_text:expr) => {{
let bar = $multi.add(ProgressBar::new(100));
bar.set_style($style.clone());
bar.enable_steady_tick(100);
if $chunk.len() == 1 { if $chunk.len() == 1 {
bar.set_prefix(&$chunk[0]); let mut bar = $progress.create_process_progress($chunk[0].to_string());
bar.set_message($single_text); bar.log($single_text);
bar
} else { } else {
bar.set_prefix(BATCH_OPERATION_LABEL); let mut bar = $progress.create_process_progress(BATCH_OPERATION_LABEL.to_string());
bar.set_message(&format!($batch_text, $chunk.len())); bar.log(&format!($batch_text, $chunk.len()));
bar
} }
bar.inc(0);
bar
}}; }};
} }
#[derive(Debug, Copy, Clone, PartialEq)] #[derive(Debug, Copy, Clone, PartialEq)]
pub enum DeploymentGoal { pub enum Goal {
/// Build the configurations only. /// Build the configurations only.
Build, Build,
@ -58,7 +51,7 @@ pub enum DeploymentGoal {
DryActivate, DryActivate,
} }
impl DeploymentGoal { impl Goal {
pub fn from_str(s: &str) -> Option<Self> { pub fn from_str(s: &str) -> Option<Self> {
match s { match s {
"build" => Some(Self::Build), "build" => Some(Self::Build),
@ -72,7 +65,7 @@ impl DeploymentGoal {
} }
pub fn as_str(&self) -> Option<&'static str> { pub fn as_str(&self) -> Option<&'static str> {
use DeploymentGoal::*; use Goal::*;
match self { match self {
Build => None, Build => None,
Push => None, Push => None,
@ -84,7 +77,7 @@ impl DeploymentGoal {
} }
pub fn success_str(&self) -> Option<&'static str> { pub fn success_str(&self) -> Option<&'static str> {
use DeploymentGoal::*; use Goal::*;
match self { match self {
Build => Some("Configuration built"), Build => Some("Configuration built"),
Push => Some("Pushed"), Push => Some("Pushed"),
@ -96,7 +89,7 @@ impl DeploymentGoal {
} }
pub fn should_switch_profile(&self) -> bool { pub fn should_switch_profile(&self) -> bool {
use DeploymentGoal::*; use Goal::*;
match self { match self {
Boot | Switch => true, Boot | Switch => true,
_ => false, _ => false,
@ -104,7 +97,7 @@ impl DeploymentGoal {
} }
pub fn requires_activation(&self) -> bool { pub fn requires_activation(&self) -> bool {
use DeploymentGoal::*; use Goal::*;
match self { match self {
Build | Push => false, Build | Push => false,
_ => true, _ => true,
@ -114,7 +107,7 @@ impl DeploymentGoal {
/// Internal deployment stages. /// Internal deployment stages.
#[derive(Debug)] #[derive(Debug)]
enum DeploymentStage { enum Stage {
Evaluate(Vec<String>), Evaluate(Vec<String>),
Build(Vec<String>), Build(Vec<String>),
Apply(String), Apply(String),
@ -124,7 +117,7 @@ enum DeploymentStage {
#[derive(Debug)] #[derive(Debug)]
struct DeploymentResult { struct DeploymentResult {
/// Stage in which the deployment ended. /// Stage in which the deployment ended.
stage: DeploymentStage, stage: Stage,
/// Whether the deployment succeeded or not. /// Whether the deployment succeeded or not.
success: bool, success: bool,
@ -134,7 +127,7 @@ struct DeploymentResult {
} }
impl DeploymentResult { impl DeploymentResult {
fn success(stage: DeploymentStage, logs: Option<String>) -> Self { fn success(stage: Stage, logs: Option<String>) -> Self {
Self { Self {
stage, stage,
success: true, success: true,
@ -142,7 +135,7 @@ impl DeploymentResult {
} }
} }
fn failure(stage: DeploymentStage, logs: Option<String>) -> Self { fn failure(stage: Stage, logs: Option<String>) -> Self {
Self { Self {
stage, stage,
success: false, success: false,
@ -155,7 +148,7 @@ impl DeploymentResult {
} }
fn print(&self) { fn print(&self) {
use DeploymentStage::*; use Stage::*;
if self.is_successful() { if self.is_successful() {
unimplemented!(); unimplemented!();
@ -197,12 +190,12 @@ impl DeploymentResult {
/// A deployment target. /// A deployment target.
#[derive(Debug)] #[derive(Debug)]
pub struct DeploymentTarget { pub struct Target {
host: Box<dyn Host>, host: Box<dyn Host>,
config: NodeConfig, config: NodeConfig,
} }
impl DeploymentTarget { impl Target {
pub fn new(host: Box<dyn Host>, config: NodeConfig) -> Self { pub fn new(host: Box<dyn Host>, config: NodeConfig) -> Self {
Self { host, config } Self { host, config }
} }
@ -211,10 +204,10 @@ impl DeploymentTarget {
#[derive(Debug)] #[derive(Debug)]
pub struct Deployment { pub struct Deployment {
hive: Hive, hive: Hive,
goal: DeploymentGoal, goal: Goal,
target_names: Vec<String>, target_names: Vec<String>,
targets: Mutex<HashMap<String, DeploymentTarget>>, targets: Mutex<HashMap<String, Target>>,
progress_alignment: usize, label_width: usize,
parallelism_limit: ParallelismLimit, parallelism_limit: ParallelismLimit,
evaluation_node_limit: EvaluationNodeLimit, evaluation_node_limit: EvaluationNodeLimit,
options: DeploymentOptions, options: DeploymentOptions,
@ -222,11 +215,10 @@ pub struct Deployment {
} }
impl Deployment { impl Deployment {
pub fn new(hive: Hive, targets: HashMap<String, DeploymentTarget>, goal: DeploymentGoal) -> Self { pub fn new(hive: Hive, targets: HashMap<String, Target>, goal: Goal) -> Self {
let target_names: Vec<String> = targets.keys().cloned().collect(); let target_names: Vec<String> = targets.keys().cloned().collect();
let label_width = if let Some(len) = target_names.iter().map(|n| n.len()).max() {
let progress_alignment = if let Some(len) = target_names.iter().map(|n| n.len()).max() {
max(BATCH_OPERATION_LABEL.len(), len) max(BATCH_OPERATION_LABEL.len(), len)
} else { } else {
BATCH_OPERATION_LABEL.len() BATCH_OPERATION_LABEL.len()
@ -237,7 +229,7 @@ impl Deployment {
goal, goal,
target_names, target_names,
targets: Mutex::new(targets), targets: Mutex::new(targets),
progress_alignment, label_width,
parallelism_limit: ParallelismLimit::default(), parallelism_limit: ParallelismLimit::default(),
evaluation_node_limit: EvaluationNodeLimit::default(), evaluation_node_limit: EvaluationNodeLimit::default(),
options: DeploymentOptions::default(), options: DeploymentOptions::default(),
@ -257,213 +249,171 @@ impl Deployment {
self.evaluation_node_limit = limit; self.evaluation_node_limit = limit;
} }
// FIXME: Duplication
/// Uploads keys only (user-facing) /// Uploads keys only (user-facing)
pub async fn upload_keys(self: Arc<Self>) { pub async fn upload_keys(self: Arc<Self>) {
let multi = Arc::new(MultiProgress::new()); let progress = {
let root_bar = Arc::new(multi.add(ProgressBar::new(100))); let mut progress = Progress::default();
multi.set_draw_target(ProgressDrawTarget::stderr_nohz()); progress.set_label_width(self.label_width);
Arc::new(progress)
{
let (style, _) = self.spinner_styles();
root_bar.set_message("Uploading keys...");
root_bar.set_style(style);
root_bar.tick();
root_bar.enable_steady_tick(100);
}
let arc_self = self.clone();
let mut futures = Vec::new();
for node in self.target_names.iter() {
let node = node.to_owned();
let mut target = {
let mut targets = arc_self.targets.lock().await;
targets.remove(&node).unwrap()
};
let multi = multi.clone();
let arc_self = self.clone();
futures.push(tokio::spawn(async move {
let permit = arc_self.parallelism_limit.apply.acquire().await.unwrap();
let bar = multi.add(ProgressBar::new(100));
let (style, fail_style) = arc_self.spinner_styles();
bar.set_style(style);
bar.set_prefix(&node);
bar.tick();
bar.enable_steady_tick(100);
if let Err(e) = target.host.upload_keys(&target.config.keys).await {
bar.set_style(fail_style);
bar.abandon_with_message(&format!("Failed to upload keys: {}", e));
let mut results = arc_self.results.lock().await;
let stage = DeploymentStage::Apply(node.to_string());
let logs = target.host.dump_logs().await.map(|s| s.to_string());
results.push(DeploymentResult::failure(stage, logs));
return;
} else {
bar.finish_with_message("Keys uploaded");
}
drop(permit);
}));
}
let wait_for_tasks = tokio::spawn(async move {
join_all(futures).await;
root_bar.finish_with_message("Finished");
});
let tasks_result = if self.options.progress_bar {
let wait_for_bars = tokio::task::spawn_blocking(move || {
multi.join().unwrap();
});
let (tasks_result, _) = join!(wait_for_tasks, wait_for_bars);
tasks_result
} else {
wait_for_tasks.await
}; };
if let Err(e) = tasks_result { let arc_self = self.clone();
log::error!("Deployment process failed: {}", e);
{
let arc_self = self.clone();
progress.run(async move |progress| {
let mut futures = Vec::new();
for node in self.target_names.iter() {
let node = node.to_owned();
let mut target = {
let mut targets = arc_self.targets.lock().await;
targets.remove(&node).unwrap()
};
let arc_self = self.clone();
let progress = progress.clone();
// how come the bars show up only when initialized here???
futures.push(tokio::spawn(async move {
let permit = arc_self.parallelism_limit.apply.acquire().await.unwrap();
let mut process = progress.create_process_progress(node.clone());
process.log("Uploading keys...");
if let Err(e) = target.host.upload_keys(&target.config.keys).await {
process.failure(&format!("Failed to upload keys: {}", e));
let mut results = arc_self.results.lock().await;
let stage = Stage::Apply(node.to_string());
let logs = target.host.dump_logs().await.map(|s| s.to_string());
results.push(DeploymentResult::failure(stage, logs));
return;
} else {
process.success("Keys uploaded");
}
drop(permit);
}));
}
join_all(futures).await
}).await;
} }
self.print_logs().await; arc_self.print_logs().await;
} }
/// Executes the deployment (user-facing) /// Executes the deployment (user-facing)
/// ///
/// Self must be wrapped inside an Arc. /// Self must be wrapped inside an Arc.
pub async fn execute(self: Arc<Self>) { pub async fn execute(self: Arc<Self>) {
let multi = Arc::new(MultiProgress::new()); let progress = {
let root_bar = Arc::new(multi.add(ProgressBar::new(100))); let mut progress = if !self.options.progress_bar {
multi.set_draw_target(ProgressDrawTarget::stderr_nohz()); Progress::with_style(OutputStyle::Plain)
} else {
{ Progress::default()
let (style, _) = self.spinner_styles(); };
root_bar.set_message("Running..."); progress.set_label_width(self.label_width);
root_bar.set_style(style); Arc::new(progress)
root_bar.tick(); };
root_bar.enable_steady_tick(100);
}
let arc_self = self.clone(); let arc_self = self.clone();
let eval_limit = arc_self.clone().eval_limit();
// FIXME: Saner logging {
let mut futures = Vec::new();
for chunk in self.target_names.chunks(eval_limit) {
let arc_self = self.clone(); let arc_self = self.clone();
let multi = multi.clone(); let eval_limit = arc_self.clone().eval_limit();
let (style, _) = self.spinner_styles();
// FIXME: Eww progress.run(async move |progress| {
let chunk: Vec<String> = chunk.iter().map(|s| s.to_string()).collect();
futures.push(tokio::spawn(async move {
let drv = {
// Evaluation phase
let permit = arc_self.parallelism_limit.evaluation.acquire().await.unwrap();
let bar = set_up_batch_progress_bar!(multi, style, chunk,
"Evaluating configuration...",
"Evaluating configurations for {} nodes"
);
let arc_self = arc_self.clone();
let drv = match arc_self.eval_profiles(&chunk, bar).await {
Some(drv) => drv,
None => {
return;
}
};
drop(permit);
drv
};
let profiles = {
// Build phase
let permit = arc_self.parallelism_limit.build.acquire().await.unwrap();
let bar = set_up_batch_progress_bar!(multi, style, chunk,
"Building configuration...",
"Building configurations for {} nodes"
);
let goal = arc_self.goal;
let arc_self = arc_self.clone();
let profiles = arc_self.build_profiles(&chunk, drv, bar.clone()).await;
let profiles = match profiles {
Some(profiles) => profiles,
None => {
return;
}
};
if goal != DeploymentGoal::Build {
bar.finish_and_clear();
}
drop(permit);
profiles
};
// Should we continue?
if arc_self.goal == DeploymentGoal::Build {
return;
}
// Apply phase
let mut futures = Vec::new(); let mut futures = Vec::new();
for node in chunk {
let arc_self = arc_self.clone();
let multi = multi.clone();
let target = { for chunk in self.target_names.chunks(eval_limit) {
let mut targets = arc_self.targets.lock().await; let arc_self = arc_self.clone();
targets.remove(&node).unwrap() let progress = progress.clone();
};
let profile = profiles.get(&node).cloned() // FIXME: Eww
.expect(&format!("Somehow profile for {} was not built", node)); let chunk: Vec<String> = chunk.iter().map(|s| s.to_string()).collect();
futures.push(tokio::spawn(async move { futures.push(tokio::spawn(async move {
arc_self.apply_profile(&node, target, profile, multi).await let drv = {
// Evaluation phase
let permit = arc_self.parallelism_limit.evaluation.acquire().await.unwrap();
let bar = set_up_batch_progress_bar!(progress, style, chunk,
"Evaluating configuration...",
"Evaluating configurations for {} nodes"
);
let arc_self = arc_self.clone();
let drv = match arc_self.eval_profiles(&chunk, bar).await {
Some(drv) => drv,
None => {
return;
}
};
drop(permit);
drv
};
let profiles = {
// Build phase
let permit = arc_self.parallelism_limit.build.acquire().await.unwrap();
let bar = set_up_batch_progress_bar!(progress, style, chunk,
"Building configuration...",
"Building configurations for {} nodes"
);
let goal = arc_self.goal;
let arc_self = arc_self.clone();
let profiles = arc_self.build_profiles(&chunk, drv, bar.clone()).await;
let profiles = match profiles {
Some(profiles) => profiles,
None => {
return;
}
};
if goal != Goal::Build {
bar.success_quiet();
}
drop(permit);
profiles
};
// Should we continue?
if arc_self.goal == Goal::Build {
return;
}
// Apply phase
let mut futures = Vec::new();
for node in chunk {
let arc_self = arc_self.clone();
let progress = progress.clone();
let target = {
let mut targets = arc_self.targets.lock().await;
targets.remove(&node).unwrap()
};
let profile = profiles.get(&node).cloned()
.expect(&format!("Somehow profile for {} was not built", node));
futures.push(tokio::spawn(async move {
arc_self.apply_profile(&node, target, profile, progress).await
}));
}
})); }));
} }
join_all(futures).await; join_all(futures).await;
})); }).await;
} }
let wait_for_tasks = tokio::spawn(async move { arc_self.print_logs().await;
join_all(futures).await;
root_bar.finish_with_message("Finished");
});
let tasks_result = if self.options.progress_bar {
let wait_for_bars = tokio::task::spawn_blocking(move || {
multi.join().unwrap();
});
let (tasks_result, _) = join!(wait_for_tasks, wait_for_bars);
tasks_result
} else {
wait_for_tasks.await
};
if let Err(e) = tasks_result {
log::error!("Deployment process failed: {}", e);
}
self.print_logs().await;
} }
async fn print_logs(&self) { async fn print_logs(&self) {
@ -475,53 +425,47 @@ impl Deployment {
} }
} }
async fn eval_profiles(self: Arc<Self>, chunk: &Vec<String>, progress: ProgressBar) -> Option<StoreDerivation<ProfileMap>> { async fn eval_profiles(self: Arc<Self>, chunk: &Vec<String>, progress: ProcessProgress) -> Option<StoreDerivation<ProfileMap>> {
let (eval, logs) = self.hive.eval_selected(&chunk, Some(progress.clone())).await; let (eval, logs) = self.hive.eval_selected(&chunk, progress.clone()).await;
match eval { match eval {
Ok(drv) => { Ok(drv) => {
progress.finish_and_clear(); progress.success_quiet();
Some(drv) Some(drv)
} }
Err(e) => { Err(e) => {
let (_, fail_style) = self.spinner_styles(); progress.failure(&format!("Evalation failed: {}", e));
progress.set_style(fail_style.clone());
progress.abandon_with_message(&format!("Evalation failed: {}", e));
let mut results = self.results.lock().await; let mut results = self.results.lock().await;
let stage = DeploymentStage::Evaluate(chunk.clone()); let stage = Stage::Evaluate(chunk.clone());
results.push(DeploymentResult::failure(stage, logs)); results.push(DeploymentResult::failure(stage, logs));
None None
} }
} }
} }
async fn build_profiles(self: Arc<Self>, chunk: &Vec<String>, derivation: StoreDerivation<ProfileMap>, progress: ProgressBar) -> Option<ProfileMap> { async fn build_profiles(self: Arc<Self>, chunk: &Vec<String>, derivation: StoreDerivation<ProfileMap>, progress: ProcessProgress) -> Option<ProfileMap> {
// FIXME: Remote build? // FIXME: Remote build?
let mut builder = host::local(); let mut builder = host::local();
if self.options.progress_bar { builder.set_progress_bar(progress.clone());
builder.set_progress_bar(progress.clone());
}
match derivation.realize(&mut *builder).await { match derivation.realize(&mut *builder).await {
Ok(profiles) => { Ok(profiles) => {
progress.finish_with_message("Successfully built"); progress.success("Successfully built");
let mut results = self.results.lock().await; let mut results = self.results.lock().await;
let stage = DeploymentStage::Build(chunk.clone()); let stage = Stage::Build(chunk.clone());
let logs = builder.dump_logs().await.map(|s| s.to_string()); let logs = builder.dump_logs().await.map(|s| s.to_string());
results.push(DeploymentResult::success(stage, logs)); results.push(DeploymentResult::success(stage, logs));
Some(profiles) Some(profiles)
} }
Err(e) => { Err(e) => {
let (_, fail_style) = self.spinner_styles(); progress.failure(&format!("Build failed: {}", e));
progress.set_style(fail_style);
progress.abandon_with_message(&format!("Build failed: {}", e));
let mut results = self.results.lock().await; let mut results = self.results.lock().await;
let stage = DeploymentStage::Build(chunk.clone()); let stage = Stage::Build(chunk.clone());
let logs = builder.dump_logs().await.map(|s| s.to_string()); let logs = builder.dump_logs().await.map(|s| s.to_string());
results.push(DeploymentResult::failure(stage, logs)); results.push(DeploymentResult::failure(stage, logs));
None None
@ -529,55 +473,46 @@ impl Deployment {
} }
} }
async fn apply_profile(self: Arc<Self>, name: &str, mut target: DeploymentTarget, profile: Profile, multi: Arc<MultiProgress>) { async fn apply_profile(self: Arc<Self>, name: &str, mut target: Target, profile: Profile, multi: Arc<Progress>) {
let (style, fail_style) = self.spinner_styles();
let permit = self.parallelism_limit.apply.acquire().await.unwrap(); let permit = self.parallelism_limit.apply.acquire().await.unwrap();
let bar = multi.add(ProgressBar::new(100)); let mut bar = multi.create_process_progress(name.to_string());
bar.set_style(style);
bar.set_prefix(name);
bar.tick();
bar.enable_steady_tick(100);
if self.options.upload_keys && !target.config.keys.is_empty() { if self.options.upload_keys && !target.config.keys.is_empty() {
bar.set_message("Uploading keys..."); bar.log("Uploading keys...");
if let Err(e) = target.host.upload_keys(&target.config.keys).await { if let Err(e) = target.host.upload_keys(&target.config.keys).await {
bar.set_style(fail_style); bar.failure(&format!("Failed to upload keys: {}", e));
bar.abandon_with_message(&format!("Failed to upload keys: {}", e));
let mut results = self.results.lock().await; let mut results = self.results.lock().await;
let stage = DeploymentStage::Apply(name.to_string()); let stage = Stage::Apply(name.to_string());
let logs = target.host.dump_logs().await.map(|s| s.to_string()); let logs = target.host.dump_logs().await.map(|s| s.to_string());
results.push(DeploymentResult::failure(stage, logs)); results.push(DeploymentResult::failure(stage, logs));
return; return;
} }
} }
bar.set_message("Starting..."); bar.log("Starting...");
if self.options.progress_bar { target.host.set_progress_bar(bar.clone());
target.host.set_progress_bar(bar.clone());
}
let copy_options = self.options.to_copy_options() let copy_options = self.options.to_copy_options()
.include_outputs(true); .include_outputs(true);
match target.host.deploy(&profile, self.goal, copy_options).await { match target.host.deploy(&profile, self.goal, copy_options).await {
Ok(_) => { Ok(_) => {
bar.finish_with_message(self.goal.success_str().unwrap()); bar.success(self.goal.success_str().unwrap());
let mut results = self.results.lock().await; let mut results = self.results.lock().await;
let stage = DeploymentStage::Apply(name.to_string()); let stage = Stage::Apply(name.to_string());
let logs = target.host.dump_logs().await.map(|s| s.to_string()); let logs = target.host.dump_logs().await.map(|s| s.to_string());
results.push(DeploymentResult::success(stage, logs)); results.push(DeploymentResult::success(stage, logs));
} }
Err(e) => { Err(e) => {
bar.set_style(fail_style); bar.failure(&format!("Failed: {}", e));
bar.abandon_with_message(&format!("Failed: {}", e));
let mut results = self.results.lock().await; let mut results = self.results.lock().await;
let stage = DeploymentStage::Apply(name.to_string()); let stage = Stage::Apply(name.to_string());
let logs = target.host.dump_logs().await.map(|s| s.to_string()); let logs = target.host.dump_logs().await.map(|s| s.to_string());
results.push(DeploymentResult::failure(stage, logs)); results.push(DeploymentResult::failure(stage, logs));
} }
@ -586,10 +521,6 @@ impl Deployment {
drop(permit); drop(permit);
} }
fn spinner_styles(&self) -> (ProgressStyle, ProgressStyle) {
get_spinner_styles(self.progress_alignment)
}
fn eval_limit(&self) -> usize { fn eval_limit(&self) -> usize {
if let Some(limit) = self.evaluation_node_limit.get_limit() { if let Some(limit) = self.evaluation_node_limit.get_limit() {
limit limit

View file

@ -2,7 +2,6 @@ use std::collections::HashMap;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use indicatif::ProgressBar;
use tempfile::{NamedTempFile, TempPath}; use tempfile::{NamedTempFile, TempPath};
use tokio::process::Command; use tokio::process::Command;
use serde::Serialize; use serde::Serialize;
@ -16,6 +15,7 @@ use super::{
}; };
use super::NixCommand; use super::NixCommand;
use crate::util::CommandExecution; use crate::util::CommandExecution;
use crate::progress::ProcessProgress;
const HIVE_EVAL: &'static [u8] = include_bytes!("eval.nix"); const HIVE_EVAL: &'static [u8] = include_bytes!("eval.nix");
@ -67,7 +67,7 @@ impl Hive {
/// Evaluation may take up a lot of memory, so we make it possible /// Evaluation may take up a lot of memory, so we make it possible
/// to split up the evaluation process into chunks and run them /// to split up the evaluation process into chunks and run them
/// concurrently with other processes (e.g., build and apply). /// concurrently with other processes (e.g., build and apply).
pub async fn eval_selected(&self, nodes: &Vec<String>, progress_bar: Option<ProgressBar>) -> (NixResult<StoreDerivation<ProfileMap>>, Option<String>) { pub async fn eval_selected(&self, nodes: &Vec<String>, progress_bar: ProcessProgress) -> (NixResult<StoreDerivation<ProfileMap>>, Option<String>) {
// FIXME: The return type is ugly... // FIXME: The return type is ugly...
let nodes_expr = SerializedNixExpresssion::new(nodes); let nodes_expr = SerializedNixExpresssion::new(nodes);
@ -79,11 +79,8 @@ impl Hive {
let expr = format!("hive.buildSelected {{ names = {}; }}", nodes_expr.expression()); let expr = format!("hive.buildSelected {{ names = {}; }}", nodes_expr.expression());
let command = self.nix_instantiate(&expr).instantiate(); let command = self.nix_instantiate(&expr).instantiate();
let mut execution = CommandExecution::new("(eval)", command); let mut execution = CommandExecution::new(command);
execution.set_progress_bar(progress_bar);
if let Some(bar) = progress_bar {
execution.set_progress_bar(bar);
}
let eval = execution let eval = execution
.capture_store_path().await; .capture_store_path().await;

View file

@ -1,4 +1,3 @@
//!
use std::convert::TryInto; use std::convert::TryInto;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
@ -6,12 +5,12 @@ use std::io::Write;
use async_trait::async_trait; use async_trait::async_trait;
use tokio::process::Command; use tokio::process::Command;
use indicatif::ProgressBar;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use super::{CopyDirection, CopyOptions, Host}; use super::{CopyDirection, CopyOptions, Host};
use crate::nix::{StorePath, Profile, DeploymentGoal, NixResult, NixCommand, Key, SYSTEM_PROFILE}; use crate::nix::{StorePath, Profile, Goal, NixResult, NixCommand, Key, SYSTEM_PROFILE};
use crate::util::CommandExecution; use crate::util::CommandExecution;
use crate::progress::ProcessProgress;
/// The local machine running Colmena. /// The local machine running Colmena.
/// ///
@ -19,14 +18,14 @@ use crate::util::CommandExecution;
/// (e.g., building Linux derivations on macOS). /// (e.g., building Linux derivations on macOS).
#[derive(Debug)] #[derive(Debug)]
pub struct Local { pub struct Local {
progress_bar: Option<ProgressBar>, progress_bar: ProcessProgress,
logs: String, logs: String,
} }
impl Local { impl Local {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
progress_bar: None, progress_bar: ProcessProgress::default(),
logs: String::new(), logs: String::new(),
} }
} }
@ -44,11 +43,9 @@ impl Host for Local {
.arg("--realise") .arg("--realise")
.arg(derivation.as_path()); .arg(derivation.as_path());
let mut execution = CommandExecution::new("local", command); let mut execution = CommandExecution::new(command);
if let Some(bar) = self.progress_bar.as_ref() { execution.set_progress_bar(self.progress_bar.clone());
execution.set_progress_bar(bar.clone());
}
let result = execution.run().await; let result = execution.run().await;
@ -69,7 +66,7 @@ impl Host for Local {
Ok(()) Ok(())
} }
async fn activate(&mut self, profile: &Profile, goal: DeploymentGoal) -> NixResult<()> { async fn activate(&mut self, profile: &Profile, goal: Goal) -> NixResult<()> {
if goal.should_switch_profile() { if goal.should_switch_profile() {
let path = profile.as_path().to_str().unwrap(); let path = profile.as_path().to_str().unwrap();
Command::new("nix-env") Command::new("nix-env")
@ -84,11 +81,9 @@ impl Host for Local {
command command
.args(&activation_command[1..]); .args(&activation_command[1..]);
let mut execution = CommandExecution::new("local", command); let mut execution = CommandExecution::new(command);
if let Some(bar) = self.progress_bar.as_ref() { execution.set_progress_bar(self.progress_bar.clone());
execution.set_progress_bar(bar.clone());
}
let result = execution.run().await; let result = execution.run().await;
@ -99,8 +94,8 @@ impl Host for Local {
result result
} }
fn set_progress_bar(&mut self, bar: ProgressBar) { fn set_progress_bar(&mut self, bar: ProcessProgress) {
self.progress_bar = Some(bar); self.progress_bar = bar;
} }
async fn dump_logs(&self) -> Option<&str> { async fn dump_logs(&self) -> Option<&str> {
Some(&self.logs) Some(&self.logs)
@ -110,9 +105,7 @@ impl Host for Local {
impl Local { impl Local {
/// "Uploads" a single key. /// "Uploads" a single key.
async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> { async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> {
if let Some(progress_bar) = self.progress_bar.as_ref() { self.progress_bar.log(&format!("Deploying key {}", name));
progress_bar.set_message(&format!("Deploying key {}", name));
}
let dest_path = key.dest_dir.join(name); let dest_path = key.dest_dir.join(name);
@ -129,7 +122,7 @@ impl Local {
.arg(&key.permissions) .arg(&key.permissions)
.arg(&temp_path); .arg(&temp_path);
let mut execution = CommandExecution::new("local", command); let mut execution = CommandExecution::new(command);
let exit = execution.run().await; let exit = execution.run().await;
let (stdout, stderr) = execution.get_logs(); let (stdout, stderr) = execution.get_logs();
@ -144,7 +137,7 @@ impl Local {
.arg(&format!("{}:{}", key.user, key.group)) .arg(&format!("{}:{}", key.user, key.group))
.arg(&temp_path); .arg(&temp_path);
let mut execution = CommandExecution::new("local", command); let mut execution = CommandExecution::new(command);
let exit = execution.run().await; let exit = execution.run().await;
let (stdout, stderr) = execution.get_logs(); let (stdout, stderr) = execution.get_logs();

View file

@ -1,9 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use indicatif::ProgressBar;
use super::{StorePath, Profile, DeploymentGoal, NixResult, NixError, Key}; use super::{StorePath, Profile, Goal, NixResult, NixError, Key};
use crate::progress::ProcessProgress;
mod ssh; mod ssh;
pub use ssh::Ssh; pub use ssh::Ssh;
@ -84,7 +84,7 @@ pub trait Host: Send + Sync + std::fmt::Debug {
} }
/// Pushes and optionally activates a profile to the host. /// Pushes and optionally activates a profile to the host.
async fn deploy(&mut self, profile: &Profile, goal: DeploymentGoal, copy_options: CopyOptions) -> NixResult<()> { async fn deploy(&mut self, profile: &Profile, goal: Goal, copy_options: CopyOptions) -> NixResult<()> {
self.copy_closure(profile.as_store_path(), CopyDirection::ToRemote, copy_options).await?; self.copy_closure(profile.as_store_path(), CopyDirection::ToRemote, copy_options).await?;
if goal.requires_activation() { if goal.requires_activation() {
@ -104,13 +104,13 @@ pub trait Host: Send + Sync + std::fmt::Debug {
/// Activates a system profile on the host, if it runs NixOS. /// Activates a system profile on the host, if it runs NixOS.
/// ///
/// The profile must already exist on the host. You should probably use deploy instead. /// The profile must already exist on the host. You should probably use deploy instead.
async fn activate(&mut self, profile: &Profile, goal: DeploymentGoal) -> NixResult<()> { async fn activate(&mut self, profile: &Profile, goal: Goal) -> NixResult<()> {
Err(NixError::Unsupported) Err(NixError::Unsupported)
} }
#[allow(unused_variables)] #[allow(unused_variables)]
/// Provides a ProgressBar to use during operations. /// Provides a ProcessProgress to use during operations.
fn set_progress_bar(&mut self, bar: ProgressBar) { fn set_progress_bar(&mut self, bar: ProcessProgress) {
} }
/// Dumps human-readable unstructured log messages related to the host. /// Dumps human-readable unstructured log messages related to the host.

View file

@ -4,13 +4,13 @@ use std::process::Stdio;
use async_trait::async_trait; use async_trait::async_trait;
use futures::future::join3; use futures::future::join3;
use indicatif::ProgressBar;
use tokio::process::Command; use tokio::process::Command;
use tokio::io::{AsyncWriteExt, BufReader}; use tokio::io::{AsyncWriteExt, BufReader};
use super::{CopyDirection, CopyOptions, Host}; use super::{CopyDirection, CopyOptions, Host};
use crate::nix::{StorePath, Profile, DeploymentGoal, NixResult, NixCommand, NixError, Key, SYSTEM_PROFILE}; use crate::nix::{StorePath, Profile, Goal, NixResult, NixCommand, NixError, Key, SYSTEM_PROFILE};
use crate::util::{CommandExecution, capture_stream}; use crate::util::{CommandExecution, capture_stream};
use crate::progress::ProcessProgress;
const DEPLOY_KEY_TEMPLATE: &'static str = include_str!("./deploy-key.template"); const DEPLOY_KEY_TEMPLATE: &'static str = include_str!("./deploy-key.template");
@ -25,7 +25,7 @@ pub struct Ssh {
friendly_name: String, friendly_name: String,
path_cache: HashSet<StorePath>, path_cache: HashSet<StorePath>,
progress_bar: Option<ProgressBar>, progress_bar: ProcessProgress,
logs: String, logs: String,
} }
@ -55,7 +55,7 @@ impl Host for Ssh {
Ok(()) Ok(())
} }
async fn activate(&mut self, profile: &Profile, goal: DeploymentGoal) -> NixResult<()> { async fn activate(&mut self, profile: &Profile, goal: Goal) -> NixResult<()> {
if goal.should_switch_profile() { if goal.should_switch_profile() {
let path = profile.as_path().to_str().unwrap(); let path = profile.as_path().to_str().unwrap();
let set_profile = self.ssh(&["nix-env", "--profile", SYSTEM_PROFILE, "--set", path]); let set_profile = self.ssh(&["nix-env", "--profile", SYSTEM_PROFILE, "--set", path]);
@ -67,8 +67,8 @@ impl Host for Ssh {
let command = self.ssh(&v); let command = self.ssh(&v);
self.run_command(command).await self.run_command(command).await
} }
fn set_progress_bar(&mut self, bar: ProgressBar) { fn set_progress_bar(&mut self, bar: ProcessProgress) {
self.progress_bar = Some(bar); self.progress_bar = bar;
} }
async fn dump_logs(&self) -> Option<&str> { async fn dump_logs(&self) -> Option<&str> {
Some(&self.logs) Some(&self.logs)
@ -83,17 +83,15 @@ impl Ssh {
host, host,
friendly_name, friendly_name,
path_cache: HashSet::new(), path_cache: HashSet::new(),
progress_bar: None, progress_bar: ProcessProgress::default(),
logs: String::new(), logs: String::new(),
} }
} }
async fn run_command(&mut self, command: Command) -> NixResult<()> { async fn run_command(&mut self, command: Command) -> NixResult<()> {
let mut execution = CommandExecution::new(&self.friendly_name, command); let mut execution = CommandExecution::new(command);
if let Some(bar) = self.progress_bar.as_ref() { execution.set_progress_bar(self.progress_bar.clone());
execution.set_progress_bar(bar.clone());
}
let result = execution.run().await; let result = execution.run().await;
@ -154,9 +152,7 @@ impl Ssh {
impl Ssh { impl Ssh {
/// Uploads a single key. /// Uploads a single key.
async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> { async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> {
if let Some(progress_bar) = self.progress_bar.as_ref() { self.progress_bar.log(&format!("Deploying key {}", name));
progress_bar.set_message(&format!("Deploying key {}", name));
}
let dest_path = key.dest_dir.join(name); let dest_path = key.dest_dir.join(name);
@ -183,8 +179,8 @@ impl Ssh {
let stderr = BufReader::new(child.stderr.take().unwrap()); let stderr = BufReader::new(child.stderr.take().unwrap());
let futures = join3( let futures = join3(
capture_stream(stdout, &self.friendly_name, self.progress_bar.clone()), capture_stream(stdout, self.progress_bar.clone()),
capture_stream(stderr, &self.friendly_name, self.progress_bar.clone()), capture_stream(stderr, self.progress_bar.clone()),
child.wait(), child.wait(),
); );
let (stdout_str, stderr_str, exit) = futures.await; let (stdout_str, stderr_str, exit) = futures.await;

View file

@ -29,7 +29,7 @@ pub mod profile;
pub use profile::{Profile, ProfileMap}; pub use profile::{Profile, ProfileMap};
pub mod deployment; pub mod deployment;
pub use deployment::{DeploymentGoal, DeploymentTarget, Deployment}; pub use deployment::{Goal, Target, Deployment};
pub const SYSTEM_PROFILE: &'static str = "/nix/var/nix/profiles/system"; pub const SYSTEM_PROFILE: &'static str = "/nix/var/nix/profiles/system";

View file

@ -5,7 +5,7 @@ use std::fs;
use std::path::Path; use std::path::Path;
use super::{ use super::{
DeploymentGoal, Goal,
NixResult, NixResult,
NixError, NixError,
StorePath, StorePath,
@ -32,7 +32,7 @@ impl Profile {
} }
/// Returns the command to activate this profile. /// Returns the command to activate this profile.
pub fn activation_command(&self, goal: DeploymentGoal) -> Option<Vec<String>> { pub fn activation_command(&self, goal: Goal) -> Option<Vec<String>> {
if let Some(goal) = goal.as_str() { if let Some(goal) = goal.as_str() {
let path = self.as_path().join("bin/switch-to-configuration"); let path = self.as_path().join("bin/switch-to-configuration");
let switch_to_configuration = path.to_str() let switch_to_configuration = path.to_str()

View file

@ -1,15 +1,242 @@
use indicatif::ProgressStyle; //! Progress display utilities.
pub fn get_spinner_styles(node_name_alignment: usize) -> (ProgressStyle, ProgressStyle) { use std::future::Future;
let template = format!("{{prefix:>{}.bold.dim}} {{spinner}} {{elapsed}} {{wide_msg}}", node_name_alignment); use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread;
use std::time::Duration;
use atty::Stream;
use console::Style;
use futures::join;
use indicatif::{
MultiProgress,
ProgressStyle as IndicatifStyle,
ProgressBar as IndicatifBar,
};
pub fn get_spinner_styles(label_width: usize) -> (IndicatifStyle, IndicatifStyle) {
let template = format!("{{prefix:>{}.bold.dim}} {{spinner}} {{elapsed}} {{wide_msg}}", label_width);
( (
ProgressStyle::default_spinner() IndicatifStyle::default_spinner()
.tick_chars("🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚✅") .tick_chars("🕛🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚✅")
.template(&template), .template(&template),
ProgressStyle::default_spinner() IndicatifStyle::default_spinner()
.tick_chars("❌❌") .tick_chars("❌❌")
.template(&template), .template(&template),
) )
} }
pub enum OutputStyle {
/// Show condensed progress bars with fancy spinners.
///
/// Not usable in a non-interactive environment.
Condensed,
/// Output log lines directly to console.
Plain,
}
/// Parallel progress display.
///
/// Currently a simple wrapper over MultiProgress.
/// Sometimes we need to log directly to the console, in case
/// stdout is not connected to a TTY or the user requests
/// verbose logging via `--verbose`.
///
/// This is normally only usable as Arc<Progress>.
pub struct Progress {
multi: Option<Arc<MultiProgress>>, // eww
/// Width of the labels for alignment
label_width: usize,
}
impl Progress {
pub fn with_style(output_style: OutputStyle) -> Self {
let multi = match output_style {
OutputStyle::Condensed => Some(Arc::new(Self::init_multi())),
OutputStyle::Plain => None,
};
Self {
multi,
label_width: 10,
}
}
pub fn set_label_width(&mut self, width: usize) {
self.label_width = width;
}
/// Returns a handle for a process to display progress information.
pub fn create_process_progress(&self, label: String) -> ProcessProgress {
let mut progress = ProcessProgress::new(label.clone(), self.label_width);
if let Some(multi) = self.multi.as_ref() {
let bar = multi.add(IndicatifBar::new(100));
let (style, _) = get_spinner_styles(self.label_width);
bar.set_prefix(&label);
bar.set_style(style);
bar.enable_steady_tick(100);
progress.set_bar(bar);
}
progress
}
/// Runs code that may initate multiple processes.
pub async fn run<F: Future, U>(self: Arc<Self>, func: U) -> F::Output
where U: FnOnce(Arc<Progress>) -> F
{
if let Some(multi) = self.multi.as_ref() {
let multi = multi.clone();
let finished = Arc::new(AtomicBool::new(false));
let redraw_future = {
let finished = finished.clone();
tokio::task::spawn_blocking(move || {
while !finished.load(Ordering::SeqCst) {
multi.join().unwrap();
thread::sleep(Duration::from_millis(100));
}
multi.join().unwrap();
})
};
let func_future = {
let finished = finished.clone();
async move {
let result = func(self).await;
finished.store(true, Ordering::SeqCst);
// root_bar.finish_and_clear();
result
}
};
let (func_result, _) = join!(func_future, redraw_future);
func_result
} else {
// Plain output. Simple.
func(self.clone()).await
}
}
fn init_multi() -> MultiProgress {
let multi = MultiProgress::new();
multi
}
fn detect_output() -> OutputStyle {
if atty::is(Stream::Stdout) {
OutputStyle::Condensed
} else {
OutputStyle::Plain
}
}
}
impl Default for Progress {
fn default() -> Self {
let style = Self::detect_output();
Self::with_style(style)
}
}
/// Progress display for a single process.
#[derive(Debug, Clone)]
pub struct ProcessProgress {
label: String,
label_width: usize,
bar: Option<IndicatifBar>,
quiet: bool,
}
impl ProcessProgress {
fn new(label: String, label_width: usize) -> Self {
Self {
label,
label_width,
bar: None,
quiet: false,
}
}
fn set_bar(&mut self, bar: IndicatifBar) {
self.bar = Some(bar);
}
/// Displays a new line of log.
pub fn log(&mut self, message: &str) {
if self.quiet {
return;
}
if let Some(bar) = self.bar.as_ref() {
bar.set_message(message);
} else {
let style = Style::new().bold();
self.plain_print(style, message);
}
}
/// Marks the process as successful, consuming the spinner.
pub fn success(self, message: &str) {
if self.quiet {
return;
}
if let Some(bar) = self.bar.as_ref() {
bar.finish_with_message(message);
} else {
let style = Style::new().bold().green();
self.plain_print(style, message);
}
}
/// Marks the process as successful, consuming the spinner.
pub fn success_quiet(self) {
if self.quiet {
return;
}
if let Some(bar) = self.bar.as_ref() {
bar.finish_and_clear();
}
}
/// Marks the process as unsuccessful, consuming the spinner.
pub fn failure(self, message: &str) {
if self.quiet {
return;
}
if let Some(bar) = self.bar.as_ref() {
let (_, fail_style) = get_spinner_styles(self.label_width);
bar.set_style(fail_style);
bar.abandon_with_message(message);
} else {
let style = Style::new().bold().red();
self.plain_print(style, message);
}
}
fn plain_print(&self, style: Style, line: &str) {
eprintln!("{:>width$} | {}", style.apply_to(&self.label), line, width = self.label_width);
}
}
impl Default for ProcessProgress {
/// Creates a ProcessProgress that does nothing.
fn default() -> Self {
Self {
label: String::new(),
label_width: 0,
bar: None,
quiet: true,
}
}
}

View file

@ -1,18 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::AsRef;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Stdio; use std::process::Stdio;
use clap::{App, Arg, ArgMatches}; use clap::{App, Arg, ArgMatches};
use console::style;
use futures::future::join3; use futures::future::join3;
use glob::Pattern as GlobPattern; use glob::Pattern as GlobPattern;
use indicatif::ProgressBar;
use tokio::io::{AsyncRead, AsyncBufReadExt, BufReader}; use tokio::io::{AsyncRead, AsyncBufReadExt, BufReader};
use tokio::process::Command; use tokio::process::Command;
use super::nix::{NodeConfig, Hive, NixResult, NixError}; use super::nix::{NodeConfig, Hive, NixResult, NixError};
use super::progress::ProcessProgress;
enum NodeFilter { enum NodeFilter {
NameFilter(GlobPattern), NameFilter(GlobPattern),
@ -21,27 +19,25 @@ enum NodeFilter {
/// Non-interactive execution of an arbitrary Nix command. /// Non-interactive execution of an arbitrary Nix command.
pub struct CommandExecution { pub struct CommandExecution {
label: String,
command: Command, command: Command,
progress_bar: Option<ProgressBar>, progress_bar: ProcessProgress,
stdout: Option<String>, stdout: Option<String>,
stderr: Option<String>, stderr: Option<String>,
} }
impl CommandExecution { impl CommandExecution {
pub fn new<S: AsRef<str>>(label: S, command: Command) -> Self { pub fn new(command: Command) -> Self {
Self { Self {
label: label.as_ref().to_string(),
command, command,
progress_bar: None, progress_bar: ProcessProgress::default(),
stdout: None, stdout: None,
stderr: None, stderr: None,
} }
} }
/// Provides a ProgressBar to use to display output. /// Provides a ProcessProgress to use to display output.
pub fn set_progress_bar(&mut self, bar: ProgressBar) { pub fn set_progress_bar(&mut self, bar: ProcessProgress) {
self.progress_bar = Some(bar); self.progress_bar = bar;
} }
/// Retrieve logs from the last invocation. /// Retrieve logs from the last invocation.
@ -64,8 +60,8 @@ impl CommandExecution {
let stderr = BufReader::new(child.stderr.take().unwrap()); let stderr = BufReader::new(child.stderr.take().unwrap());
let futures = join3( let futures = join3(
capture_stream(stdout, &self.label, self.progress_bar.clone()), capture_stream(stdout, self.progress_bar.clone()),
capture_stream(stderr, &self.label, self.progress_bar.clone()), capture_stream(stderr, self.progress_bar.clone()),
child.wait(), child.wait(),
); );
@ -219,7 +215,7 @@ fn canonicalize_cli_path(path: String) -> PathBuf {
} }
} }
pub async fn capture_stream<R: AsyncRead + Unpin>(mut stream: BufReader<R>, label: &str, mut progress_bar: Option<ProgressBar>) -> String { pub async fn capture_stream<R: AsyncRead + Unpin>(mut stream: BufReader<R>, mut progress_bar: ProcessProgress) -> String {
let mut log = String::new(); let mut log = String::new();
loop { loop {
@ -231,11 +227,7 @@ pub async fn capture_stream<R: AsyncRead + Unpin>(mut stream: BufReader<R>, labe
} }
let trimmed = line.trim_end(); let trimmed = line.trim_end();
if let Some(progress_bar) = progress_bar.as_mut() { progress_bar.log(trimmed);
progress_bar.set_message(trimmed);
} else {
eprintln!("{} | {}", style(label).cyan(), trimmed);
}
log += trimmed; log += trimmed;
log += "\n"; log += "\n";