command_stream/zx/
goods.rs1use std::future::Future;
4use std::io::{IsTerminal, Write};
5use std::path::{Path, PathBuf};
6use std::time::Duration;
7
8use super::error::ZxError;
9use super::log::{log, LogEntry};
10use super::shell::{configure, current_options, within};
11use super::util::random_id;
12
13pub async fn sleep(duration: Duration) {
15 tokio::time::sleep(duration).await;
16}
17
18pub async fn sleep_for(duration: &str) -> Result<(), ZxError> {
20 let duration = super::util::parse_duration(duration)?;
21 tokio::time::sleep(duration).await;
22 Ok(())
23}
24
25pub async fn retry<T, E, F, Fut>(count: usize, f: F) -> Result<T, E>
30where
31 F: FnMut() -> Fut,
32 Fut: Future<Output = Result<T, E>>,
33{
34 retry_with_backoff(count, std::iter::repeat(Duration::ZERO), f).await
35}
36
37pub async fn retry_with_delay<T, E, F, Fut>(count: usize, delay: Duration, f: F) -> Result<T, E>
39where
40 F: FnMut() -> Fut,
41 Fut: Future<Output = Result<T, E>>,
42{
43 retry_with_backoff(count, std::iter::repeat(delay), f).await
44}
45
46pub async fn retry_with_backoff<T, E, F, Fut, I>(count: usize, delays: I, mut f: F) -> Result<T, E>
49where
50 F: FnMut() -> Fut,
51 Fut: Future<Output = Result<T, E>>,
52 I: IntoIterator<Item = Duration>,
53{
54 let total = count.max(1);
55 let mut delays = delays.into_iter();
56 let mut attempt = 0;
57 loop {
58 attempt += 1;
59 let err = match f().await {
60 Ok(value) => return Ok(value),
61 Err(err) => err,
62 };
63 if attempt >= total {
64 return Err(err);
65 }
66 let delay = delays.next().unwrap_or(Duration::ZERO);
67 let opts = current_options();
68 let entry = LogEntry::Retry {
69 attempt,
70 total: Some(total),
71 delay,
72 };
73 log(&entry, opts.verbose && !opts.quiet);
74 if !delay.is_zero() {
75 tokio::time::sleep(delay).await;
76 }
77 }
78}
79
80pub fn exp_backoff(max: Duration, delay: Duration) -> impl Iterator<Item = Duration> {
82 (0u32..).map(move |n| {
83 let factor = 2u32.checked_pow(n.min(31)).unwrap_or(u32::MAX);
84 delay.checked_mul(factor).map_or(max, |d| d.min(max))
85 })
86}
87
88pub fn exp_backoff_default() -> impl Iterator<Item = Duration> {
90 exp_backoff(Duration::from_secs(60), Duration::from_millis(100))
91}
92
93pub const SPINNER_FRAMES: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
95
96pub fn spinner_enabled() -> bool {
99 !current_options().quiet && std::env::var_os("CI").is_none() && std::io::stderr().is_terminal()
100}
101
102pub async fn spinner<F: Future>(title: &str, fut: F) -> F::Output {
105 if !spinner_enabled() {
106 return fut.await;
107 }
108 let title = title.to_string();
109 let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel::<()>();
110 let ticker = tokio::spawn(async move {
111 let mut frame = 0usize;
112 let mut interval = tokio::time::interval(Duration::from_millis(100));
113 interval.tick().await;
114 loop {
115 tokio::select! {
116 _ = &mut stop_rx => break,
117 _ = interval.tick() => {
118 let mut err = std::io::stderr();
119 let _ = write!(err, " {} {title}\r", SPINNER_FRAMES[frame % 10]);
120 let _ = err.flush();
121 frame += 1;
122 }
123 }
124 }
125 let width = title.chars().count() + 4;
126 let mut err = std::io::stderr();
127 let _ = write!(err, "{}\r", " ".repeat(width));
128 let _ = err.flush();
129 });
130 let output = within(async {
131 configure(|o| o.verbose = false);
132 fut.await
133 })
134 .await;
135 let _ = stop_tx.send(());
136 let _ = ticker.await;
137 output
138}
139
140pub fn echo<I, S>(parts: I)
142where
143 I: IntoIterator<Item = S>,
144 S: std::fmt::Display,
145{
146 println!("{}", echo_line(parts));
147}
148
149pub fn echo_line<I, S>(parts: I) -> String
151where
152 I: IntoIterator<Item = S>,
153 S: std::fmt::Display,
154{
155 parts
156 .into_iter()
157 .map(|p| p.to_string())
158 .collect::<Vec<_>>()
159 .join(" ")
160}
161
162pub fn tempdir(prefix: Option<&str>) -> Result<PathBuf, ZxError> {
165 let name = prefix.map_or_else(|| format!("zx-{}", random_id()), str::to_string);
166 let dir = std::env::temp_dir().join(name);
167 std::fs::create_dir_all(&dir)?;
168 Ok(dir)
169}
170
171pub fn tempfile(name: Option<&str>, data: Option<&[u8]>) -> Result<PathBuf, ZxError> {
177 let path = match name {
178 Some(name) => tempdir(None)?.join(name),
179 None => std::env::temp_dir().join(format!("zx-{}", random_id())),
180 };
181 std::fs::write(&path, data.unwrap_or_default())?;
182 Ok(path)
183}
184
185pub fn which(name: &str) -> Option<PathBuf> {
187 ::which::which(name).ok()
188}
189
190pub fn glob(pattern: &str) -> Result<Vec<PathBuf>, ZxError> {
193 let entries = ::glob::glob(pattern).map_err(|e| ZxError::new(e.to_string()))?;
194 let mut paths: Vec<PathBuf> = entries.filter_map(Result::ok).collect();
195 paths.sort();
196 Ok(paths)
197}
198
199pub fn glob_in(cwd: impl AsRef<Path>, pattern: &str) -> Result<Vec<PathBuf>, ZxError> {
201 let cwd = cwd.as_ref();
202 let full = cwd.join(pattern);
203 let found = glob(&full.to_string_lossy())?;
204 Ok(found
205 .into_iter()
206 .map(|p| p.strip_prefix(cwd).map(Path::to_path_buf).unwrap_or(p))
207 .collect())
208}