ALPHA: wire is a tool to deploy nixos systems wire.althaea.zone/
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

add status bar

authored by

marshmallow and committed by
marshmallow
557960d9 cdc61618

+355 -27
+69 -2
Cargo.lock
··· 386 386 ] 387 387 388 388 [[package]] 389 + name = "deranged" 390 + version = "0.5.5" 391 + source = "registry+https://github.com/rust-lang/crates.io-index" 392 + checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" 393 + dependencies = [ 394 + "powerfmt", 395 + ] 396 + 397 + [[package]] 389 398 name = "derive_more" 390 399 version = "2.0.1" 391 400 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 515 524 checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" 516 525 dependencies = [ 517 526 "libc", 518 - "windows-sys 0.52.0", 527 + "windows-sys 0.60.2", 519 528 ] 520 529 521 530 [[package]] ··· 737 746 checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 738 747 739 748 [[package]] 749 + name = "humanize-duration" 750 + version = "0.0.7" 751 + source = "registry+https://github.com/rust-lang/crates.io-index" 752 + checksum = "9d17650201754d4f79437bc2fa1f157273a7779de1391c818b275d6bb40998b9" 753 + dependencies = [ 754 + "time", 755 + ] 756 + 757 + [[package]] 740 758 name = "icu_collections" 741 759 version = "2.0.0" 742 760 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 945 963 "futures", 946 964 "gethostname", 947 965 "gjson", 966 + "humanize-duration", 948 967 "im", 949 968 "itertools", 950 969 "key_agent", ··· 963 982 "strip-ansi-escapes", 964 983 "syn 2.0.110", 965 984 "tempdir", 985 + "termion", 966 986 "thiserror 2.0.17", 967 987 "tokio", 968 988 "tokio-util", ··· 1170 1190 ] 1171 1191 1172 1192 [[package]] 1193 + name = "num-conv" 1194 + version = "0.1.0" 1195 + source = "registry+https://github.com/rust-lang/crates.io-index" 1196 + checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1197 + 1198 + [[package]] 1173 1199 name = "num-traits" 1174 1200 version = "0.2.19" 1175 1201 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1199 1225 "quote", 1200 1226 "syn 2.0.110", 1201 1227 ] 1228 + 1229 + [[package]] 1230 + name = "numtoa" 1231 + version = "0.2.4" 1232 + source = "registry+https://github.com/rust-lang/crates.io-index" 1233 + checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f" 1202 1234 1203 1235 [[package]] 1204 1236 name = "object" ··· 1323 1355 ] 1324 1356 1325 1357 [[package]] 1358 + name = "powerfmt" 1359 + version = "0.2.0" 1360 + source = "registry+https://github.com/rust-lang/crates.io-index" 1361 + checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1362 + 1363 + [[package]] 1326 1364 name = "ppv-lite86" 1327 1365 version = "0.2.21" 1328 1366 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1588 1626 "errno", 1589 1627 "libc", 1590 1628 "linux-raw-sys", 1591 - "windows-sys 0.52.0", 1629 + "windows-sys 0.60.2", 1592 1630 ] 1593 1631 1594 1632 [[package]] ··· 1918 1956 ] 1919 1957 1920 1958 [[package]] 1959 + name = "termion" 1960 + version = "4.0.6" 1961 + source = "registry+https://github.com/rust-lang/crates.io-index" 1962 + checksum = "f44138a9ae08f0f502f24104d82517ef4da7330c35acd638f1f29d3cd5475ecb" 1963 + dependencies = [ 1964 + "libc", 1965 + "numtoa", 1966 + ] 1967 + 1968 + [[package]] 1921 1969 name = "textwrap" 1922 1970 version = "0.16.2" 1923 1971 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1981 2029 dependencies = [ 1982 2030 "cfg-if", 1983 2031 ] 2032 + 2033 + [[package]] 2034 + name = "time" 2035 + version = "0.3.44" 2036 + source = "registry+https://github.com/rust-lang/crates.io-index" 2037 + checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" 2038 + dependencies = [ 2039 + "deranged", 2040 + "num-conv", 2041 + "powerfmt", 2042 + "serde", 2043 + "time-core", 2044 + ] 2045 + 2046 + [[package]] 2047 + name = "time-core" 2048 + version = "0.1.6" 2049 + source = "registry+https://github.com/rust-lang/crates.io-index" 2050 + checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" 1984 2051 1985 2052 [[package]] 1986 2053 name = "tinystr"
+17 -2
wire/cli/src/apply.rs
··· 5 5 use itertools::{Either, Itertools}; 6 6 use lib::hive::node::{Context, GoalExecutor, Name, StepState, should_apply_locally}; 7 7 use lib::hive::{Hive, HiveLocation}; 8 + use lib::status::STATUS; 8 9 use lib::{SubCommandModifiers, errors::HiveLibError}; 9 10 use miette::{Diagnostic, IntoDiagnostic, Result}; 10 11 use std::collections::HashSet; 11 - use std::io::Read; 12 + use std::io::{Read, stderr}; 12 13 use std::sync::Arc; 13 14 use thiserror::Error; 14 15 use tracing::{Span, error, info}; ··· 85 86 }, 86 87 ); 87 88 88 - let mut set = hive 89 + let selected_nodes: Vec<_> = hive 89 90 .nodes 90 91 .iter_mut() 91 92 .filter(|(name, node)| { ··· 93 94 || names.contains(name) 94 95 || node.tags.iter().any(|tag| tags.contains(tag)) 95 96 }) 97 + .collect(); 98 + 99 + STATUS.lock().add_many( 100 + &selected_nodes 101 + .iter() 102 + .map(|(name, _)| *name) 103 + .collect::<Vec<_>>(), 104 + ); 105 + 106 + let mut set = selected_nodes 107 + .into_iter() 96 108 .map(|(name, node)| { 97 109 info!("Resolved {:?} to include {}", args.on, name); 98 110 ··· 143 155 std::mem::drop(header_span); 144 156 145 157 if !errors.is_empty() { 158 + // clear the status bar if we are about to print error messages 159 + STATUS.lock().clear(&mut stderr()); 160 + 146 161 return Err(NodeErrors( 147 162 errors 148 163 .into_iter()
+4 -1
wire/cli/src/main.rs
··· 1 1 // SPDX-License-Identifier: AGPL-3.0-or-later 2 2 // Copyright 2024-2025 wire Contributors 3 3 4 + #![feature(sync_nonpoison)] 5 + #![feature(nonpoison_mutex)] 6 + 4 7 use std::process::Command; 5 8 6 9 use crate::cli::Cli; ··· 35 38 let args = Cli::parse(); 36 39 37 40 let modifiers = args.to_subcommand_modifiers(); 38 - setup_logging(&args.verbose); 41 + setup_logging(&args.verbose, !&args.no_progress); 39 42 40 43 #[cfg(debug_assertions)] 41 44 if args.markdown_help {
+35 -6
wire/cli/src/tracing_setup.rs
··· 3 3 4 4 use std::{ 5 5 collections::VecDeque, 6 - io::{self, Stderr, Write, stderr}, 6 + io::{self, Stderr, Write, stderr}, time::Duration, 7 7 }; 8 8 9 9 use clap_verbosity_flag::{LogLevel, Verbosity}; 10 - use lib::STDIN_CLOBBER_LOCK; 10 + use lib::{ 11 + STDIN_CLOBBER_LOCK, 12 + status::{STATUS}, 13 + }; 11 14 use owo_colors::{OwoColorize, Stream, Style}; 12 15 use tracing::{Level, Subscriber}; 13 16 use tracing_log::AsTrace; ··· 42 45 } 43 46 } 44 47 48 + /// expects the caller to write the status line 45 49 fn dump_previous(&mut self) -> Result<(), io::Error> { 50 + STATUS.lock().clear(&mut self.stderr); 51 + 46 52 for buf in self.queue.iter().rev() { 47 53 self.stderr.write(buf).map(|_| ())?; 48 54 } 49 - 50 - self.stderr.flush()?; 51 55 52 56 Ok(()) 53 57 } ··· 58 62 if let 1.. = STDIN_CLOBBER_LOCK.available_permits() { 59 63 self.dump_previous().map(|()| 0)?; 60 64 61 - self.stderr.write(buf) 65 + STATUS.lock().write_above_status(buf, &mut self.stderr) 62 66 } else { 63 67 self.queue.push_front(buf.to_vec()); 64 68 ··· 229 233 } 230 234 } 231 235 236 + async fn status_tick_worker() { 237 + let mut interval = tokio::time::interval(Duration::from_secs(1)); 238 + let mut stderr = stderr(); 239 + 240 + loop { 241 + interval.tick().await; 242 + 243 + if STDIN_CLOBBER_LOCK.available_permits() < 1 { 244 + continue; 245 + } 246 + 247 + let mut status = STATUS.lock(); 248 + 249 + status.clear(&mut stderr); 250 + status.write_status(&mut stderr); 251 + } 252 + } 253 + 232 254 /// Set up logging for the application 233 255 /// Uses `WireFieldFormat` if -v was never passed 234 - pub fn setup_logging<L: LogLevel>(verbosity: &Verbosity<L>) { 256 + pub fn setup_logging<L: LogLevel>(verbosity: &Verbosity<L>, show_progress: bool) { 235 257 let filter = verbosity.log_level_filter().as_trace(); 236 258 let registry = tracing_subscriber::registry(); 259 + 260 + STATUS.lock().show_progress(show_progress); 261 + 262 + // spawn worker to tick the status bar 263 + if show_progress { 264 + tokio::spawn(status_tick_worker()); 265 + } 237 266 238 267 if verbosity.is_present() { 239 268 let layer = tracing_subscriber::fmt::layer()
+2
wire/lib/Cargo.toml
··· 37 37 num_enum = "0.7.5" 38 38 gjson = "0.8.1" 39 39 owo-colors = { workspace = true } 40 + termion = "4.0.6" 41 + humanize-duration = "0.0.7" 40 42 41 43 [dev-dependencies] 42 44 tempdir = "0.3"
+19 -13
wire/lib/src/commands/pty/mod.rs
··· 2 2 // Copyright 2024-2025 wire Contributors 3 3 4 4 use crate::commands::pty::output::{WatchStdoutArguments, handle_pty_stdout}; 5 + use crate::status::STATUS; 5 6 use aho_corasick::PatternID; 6 7 use itertools::Itertools; 7 8 use nix::sys::termios::{LocalFlags, SetArg, Termios, tcgetattr, tcsetattr}; ··· 10 11 use portable_pty::{CommandBuilder, NativePtySystem, PtyPair, PtySize}; 11 12 use rand::distr::Alphabetic; 12 13 use std::collections::VecDeque; 14 + use std::io::stderr; 13 15 use std::sync::{LazyLock, Mutex}; 14 16 use std::{ 15 17 io::{Read, Write}, ··· 23 25 use crate::commands::CommandArguments; 24 26 use crate::commands::pty::input::watch_stdin_from_user; 25 27 use crate::errors::CommandError; 26 - use crate::{STDIN_CLOBBER_LOCK, SubCommandModifiers}; 28 + use crate::{SubCommandModifiers, aquire_stdin_lock}; 27 29 use crate::{ 28 30 commands::{ChildOutputMode, WireCommandChip}, 29 31 errors::HiveLibError, ··· 153 155 command.env(key, value); 154 156 } 155 157 156 - let clobber_guard = STDIN_CLOBBER_LOCK.acquire().await.unwrap(); 158 + let clobber_guard = aquire_stdin_lock().await; 157 159 let _guard = StdinTermiosAttrGuard::new().map_err(HiveLibError::CommandError)?; 158 160 let child = pty_pair 159 161 .slave ··· 247 249 return Ok(()); 248 250 } 249 251 250 - eprintln!( 251 - "{} | Authenticate for \"sudo {}\":", 252 - arguments 253 - .target 254 - .map_or(Ok("localhost (!)".to_string()), |target| Ok(format!( 255 - "{}@{}:{}", 256 - target.user, 257 - target.get_preferred_host()?, 258 - target.port 259 - )))?, 260 - arguments.command_string.as_ref() 252 + let _ = STATUS.lock().write_above_status( 253 + &format!( 254 + "{} | Authenticate for \"sudo {}\":\n", 255 + arguments 256 + .target 257 + .map_or(Ok("localhost (!)".to_string()), |target| Ok(format!( 258 + "{}@{}:{}", 259 + target.user, 260 + target.get_preferred_host()?, 261 + target.port 262 + )))?, 263 + arguments.command_string.as_ref() 264 + ) 265 + .into_bytes(), 266 + &mut stderr(), 261 267 ); 262 268 263 269 Ok(())
+10
wire/lib/src/hive/node.rs
··· 21 21 use crate::hive::steps::keys::{Key, Keys, PushKeyAgent, UploadKeyAt}; 22 22 use crate::hive::steps::ping::Ping; 23 23 use crate::hive::steps::push::{PushBuildOutput, PushEvaluatedOutput}; 24 + use crate::status::STATUS; 24 25 use crate::{EvalGoal, StrictHostKeyChecking, SubCommandModifiers}; 25 26 26 27 use super::HiveLibError; ··· 293 294 pub key_agent_directory: Option<String>, 294 295 } 295 296 297 + #[allow(clippy::struct_excessive_bools)] 296 298 pub struct Context<'a> { 297 299 pub name: &'a Name, 298 300 pub node: &'a mut Node, ··· 434 436 progress = format!("{}/{length}", position + 1) 435 437 ); 436 438 439 + STATUS 440 + .lock() 441 + .set_node_step(self.context.name, step.to_string()); 442 + 437 443 if let Err(err) = step.execute(&mut self.context).await.inspect_err(|_| { 438 444 error!("Failed to execute `{step}`"); 439 445 }) { ··· 446 452 return Ok(()); 447 453 } 448 454 455 + STATUS.lock().mark_node_failed(self.context.name); 456 + 449 457 return Err(err); 450 458 } 451 459 } 460 + 461 + STATUS.lock().mark_node_succeeded(self.context.name); 452 462 453 463 Ok(()) 454 464 }
+20 -3
wire/lib/src/lib.rs
··· 3 3 4 4 #![feature(assert_matches)] 5 5 #![feature(iter_intersperse)] 6 + #![feature(sync_nonpoison)] 7 + #![feature(nonpoison_mutex)] 6 8 7 - use std::{io::IsTerminal, sync::LazyLock}; 9 + use std::{ 10 + io::{IsTerminal, stderr}, 11 + sync::LazyLock, 12 + }; 8 13 9 - use tokio::sync::Semaphore; 14 + use tokio::sync::{AcquireError, Semaphore, SemaphorePermit}; 10 15 11 - use crate::{errors::HiveLibError, hive::node::Name}; 16 + use crate::{ 17 + errors::HiveLibError, 18 + hive::node::Name, 19 + status::{STATUS}, 20 + }; 12 21 13 22 pub mod commands; 14 23 pub mod hive; 24 + pub mod status; 15 25 16 26 #[cfg(test)] 17 27 mod test_macros; ··· 54 64 } 55 65 56 66 pub static STDIN_CLOBBER_LOCK: LazyLock<Semaphore> = LazyLock::new(|| Semaphore::new(1)); 67 + 68 + pub async fn aquire_stdin_lock<'a>() -> Result<SemaphorePermit<'a>, AcquireError> { 69 + let result = STDIN_CLOBBER_LOCK.acquire().await?; 70 + STATUS.lock().wipe_out(&mut stderr()); 71 + 72 + Ok(result) 73 + }
+179
wire/lib/src/status.rs
··· 1 + // SPDX-License-Identifier: AGPL-3.0-or-later 2 + // Copyright 2024-2025 wire Contributors 3 + 4 + use owo_colors::OwoColorize; 5 + use std::{fmt::Write, time::Instant}; 6 + use termion::{clear, cursor}; 7 + 8 + use crate::{STDIN_CLOBBER_LOCK, hive::node::Name}; 9 + 10 + use std::{ 11 + collections::HashMap, 12 + sync::{LazyLock, nonpoison::Mutex}, 13 + }; 14 + 15 + #[derive(Default)] 16 + pub enum NodeStatus { 17 + #[default] 18 + Pending, 19 + Running(String), 20 + Succeeded, 21 + Failed, 22 + } 23 + 24 + pub struct Status { 25 + statuses: HashMap<String, NodeStatus>, 26 + began: Instant, 27 + show_progress: bool 28 + } 29 + 30 + /// global status used for the progress bar in the cli crate 31 + pub static STATUS: LazyLock<Mutex<Status>> = LazyLock::new(|| Mutex::new(Status::new())); 32 + 33 + impl Status { 34 + fn new() -> Self { 35 + Self { 36 + statuses: HashMap::default(), 37 + began: Instant::now(), 38 + show_progress: false 39 + } 40 + } 41 + 42 + pub const fn show_progress(&mut self, show_progress: bool) { 43 + self.show_progress = show_progress; 44 + } 45 + 46 + pub fn add_many(&mut self, names: &[&Name]) { 47 + self.statuses.extend( 48 + names 49 + .iter() 50 + .map(|name| (name.0.to_string(), NodeStatus::Pending)), 51 + ); 52 + } 53 + 54 + pub fn set_node_step(&mut self, node: &Name, step: String) { 55 + self.statuses 56 + .insert(node.0.to_string(), NodeStatus::Running(step)); 57 + } 58 + 59 + pub fn mark_node_failed(&mut self, node: &Name) { 60 + self.statuses.insert(node.0.to_string(), NodeStatus::Failed); 61 + } 62 + 63 + pub fn mark_node_succeeded(&mut self, node: &Name) { 64 + self.statuses 65 + .insert(node.0.to_string(), NodeStatus::Succeeded); 66 + } 67 + 68 + #[must_use] 69 + fn num_finished(&self) -> usize { 70 + self.statuses 71 + .iter() 72 + .filter(|(_, status)| matches!(status, NodeStatus::Succeeded | NodeStatus::Failed)) 73 + .count() 74 + } 75 + 76 + #[must_use] 77 + fn num_running(&self) -> usize { 78 + self.statuses 79 + .iter() 80 + .filter(|(_, status)| matches!(status, NodeStatus::Running(..))) 81 + .count() 82 + } 83 + 84 + #[must_use] 85 + fn num_failed(&self) -> usize { 86 + self.statuses 87 + .iter() 88 + .filter(|(_, status)| matches!(status, NodeStatus::Failed)) 89 + .count() 90 + } 91 + 92 + #[must_use] 93 + pub fn get_msg(&self) -> String { 94 + if self.statuses.is_empty() { 95 + return String::new(); 96 + } 97 + 98 + let mut msg = format!("[{} / {}", self.num_finished(), self.statuses.len(),); 99 + 100 + let num_failed = self.num_failed(); 101 + let num_running = self.num_running(); 102 + 103 + let failed = if num_failed >= 1 { 104 + Some(format!("{} Failed", num_failed.red())) 105 + } else { 106 + None 107 + }; 108 + 109 + let running = if num_running >= 1 { 110 + Some(format!("{} Deploying", num_running.blue())) 111 + } else { 112 + None 113 + }; 114 + 115 + let _ = match (failed, running) { 116 + (None, None) => write!(&mut msg, ""), 117 + (Some(message), None) | (None, Some(message)) => write!(&mut msg, " ({message})"), 118 + (Some(failed), Some(running)) => write!(&mut msg, " ({failed}, {running})"), 119 + }; 120 + 121 + let _ = write!(&mut msg, "]"); 122 + 123 + let _ = write!( 124 + &mut msg, 125 + " {}s", 126 + self.began 127 + .elapsed() 128 + .as_secs() 129 + ); 130 + 131 + msg 132 + } 133 + 134 + pub fn clear<T: std::io::Write>(&self, writer: &mut T) { 135 + if !self.show_progress { 136 + return; 137 + } 138 + 139 + let _ = write!(writer, "{}", cursor::Save); 140 + // let _ = write!(writer, "{}", cursor::Down(1)); 141 + let _ = write!(writer, "{}", cursor::Left(999)); 142 + let _ = write!(writer, "{}", clear::CurrentLine); 143 + } 144 + 145 + /// used when there is an interactive prompt 146 + pub fn wipe_out<T: std::io::Write>(&self, writer: &mut T) { 147 + if !self.show_progress { 148 + return; 149 + } 150 + 151 + let _ = write!(writer, "{}", cursor::Save); 152 + let _ = write!(writer, "{}", cursor::Left(999)); 153 + let _ = write!(writer, "{}", clear::CurrentLine); 154 + let _ = writer.flush(); 155 + } 156 + 157 + pub fn write_status<T: std::io::Write>(&mut self, writer: &mut T) { 158 + if self.show_progress { 159 + let _ = write!(writer, "{}", self.get_msg()); 160 + } 161 + } 162 + 163 + pub fn write_above_status<T: std::io::Write>( 164 + &mut self, 165 + buf: &[u8], 166 + writer: &mut T, 167 + ) -> std::io::Result<usize> { 168 + if STDIN_CLOBBER_LOCK.available_permits() != 1 { 169 + // skip 170 + return Ok(0); 171 + } 172 + 173 + self.clear(writer); 174 + let written = writer.write(buf)?; 175 + self.write_status(writer); 176 + 177 + Ok(written) 178 + } 179 + }