mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
feat: improve download progress bar (#16984)
Co-authored-by: David Sherret <dsherret@gmail.com>
This commit is contained in:
parent
4a17c93088
commit
8c026dab92
17 changed files with 852 additions and 306 deletions
68
Cargo.lock
generated
68
Cargo.lock
generated
|
@ -500,17 +500,13 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "console"
|
name = "console_static_text"
|
||||||
version = "0.15.1"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
|
checksum = "3d749e1f5316d8a15ec592516a631ab9b8099cc6d085b69b905462fc071caedb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"encode_unicode",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"terminal_size",
|
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"winapi 0.3.9",
|
"vte",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -778,6 +774,7 @@ dependencies = [
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"clap_complete_fig",
|
"clap_complete_fig",
|
||||||
|
"console_static_text",
|
||||||
"data-url",
|
"data-url",
|
||||||
"deno_ast",
|
"deno_ast",
|
||||||
"deno_bench_util",
|
"deno_bench_util",
|
||||||
|
@ -804,7 +801,6 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"import_map",
|
"import_map",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"indicatif",
|
|
||||||
"jsonc-parser",
|
"jsonc-parser",
|
||||||
"junction",
|
"junction",
|
||||||
"libc",
|
"libc",
|
||||||
|
@ -1558,12 +1554,6 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "encode_unicode"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.31"
|
version = "0.8.31"
|
||||||
|
@ -2323,17 +2313,6 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indicatif"
|
|
||||||
version = "0.17.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bfddc9561e8baf264e0e45e197fd7696320026eb10a8180340debc27b18f535b"
|
|
||||||
dependencies = [
|
|
||||||
"console",
|
|
||||||
"number_prefix",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inotify"
|
name = "inotify"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
@ -2997,12 +2976,6 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "number_prefix"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc"
|
name = "objc"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
|
@ -4710,16 +4683,6 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "terminal_size"
|
|
||||||
version = "0.1.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "test_ffi"
|
name = "test_ffi"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -5416,6 +5379,27 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aae21c12ad2ec2d168c236f369c38ff332bc1134f7246350dca641437365045"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"utf8parse",
|
||||||
|
"vte_generate_state_changes",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vte_generate_state_changes"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2 1.0.47",
|
||||||
|
"quote 1.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
|
|
|
@ -59,6 +59,7 @@ chrono = { version = "=0.4.22", default-features = false, features = ["clock"] }
|
||||||
clap = "=3.1.12"
|
clap = "=3.1.12"
|
||||||
clap_complete = "=3.1.2"
|
clap_complete = "=3.1.2"
|
||||||
clap_complete_fig = "=3.1.5"
|
clap_complete_fig = "=3.1.5"
|
||||||
|
console_static_text = "=0.3.3"
|
||||||
data-url.workspace = true
|
data-url.workspace = true
|
||||||
dissimilar = "=1.0.4"
|
dissimilar = "=1.0.4"
|
||||||
dprint-plugin-json = "=0.17.0"
|
dprint-plugin-json = "=0.17.0"
|
||||||
|
@ -72,7 +73,6 @@ flate2.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
import_map = "=0.13.0"
|
import_map = "=0.13.0"
|
||||||
indexmap = "=1.9.2"
|
indexmap = "=1.9.2"
|
||||||
indicatif = "=0.17.1"
|
|
||||||
jsonc-parser = { version = "=0.21.0", features = ["serde"] }
|
jsonc-parser = { version = "=0.21.0", features = ["serde"] }
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
log = { workspace = true, features = ["serde"] }
|
log = { workspace = true, features = ["serde"] }
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||||
use crate::auth_tokens::AuthToken;
|
use crate::auth_tokens::AuthToken;
|
||||||
|
use crate::util::progress_bar::UpdateGuard;
|
||||||
use crate::version::get_user_agent;
|
use crate::version::get_user_agent;
|
||||||
|
|
||||||
use cache_control::Cachability;
|
use cache_control::Cachability;
|
||||||
use cache_control::CacheControl;
|
use cache_control::CacheControl;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
|
use deno_core::anyhow::bail;
|
||||||
use deno_core::error::custom_error;
|
use deno_core::error::custom_error;
|
||||||
use deno_core::error::generic_error;
|
use deno_core::error::generic_error;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
|
use deno_core::futures::StreamExt;
|
||||||
use deno_core::url::Url;
|
use deno_core::url::Url;
|
||||||
use deno_runtime::deno_fetch::create_http_client;
|
use deno_runtime::deno_fetch::create_http_client;
|
||||||
use deno_runtime::deno_fetch::reqwest;
|
use deno_runtime::deno_fetch::reqwest;
|
||||||
|
@ -243,6 +246,44 @@ impl HttpClient {
|
||||||
self.0.get(url)
|
self.0.get(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn download_with_progress<U: reqwest::IntoUrl>(
|
||||||
|
&self,
|
||||||
|
url: U,
|
||||||
|
progress_guard: &UpdateGuard,
|
||||||
|
) -> Result<Option<Vec<u8>>, AnyError> {
|
||||||
|
let response = self.get(url).send().await?;
|
||||||
|
|
||||||
|
if response.status() == 404 {
|
||||||
|
Ok(None)
|
||||||
|
} else if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let maybe_response_text = response.text().await.ok();
|
||||||
|
bail!(
|
||||||
|
"Bad response: {:?}{}",
|
||||||
|
status,
|
||||||
|
match maybe_response_text {
|
||||||
|
Some(text) => format!("\n\n{}", text),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else if let Some(total_size) = response.content_length() {
|
||||||
|
progress_guard.set_total_size(total_size);
|
||||||
|
let mut current_size = 0;
|
||||||
|
let mut data = Vec::with_capacity(total_size as usize);
|
||||||
|
let mut stream = response.bytes_stream();
|
||||||
|
while let Some(item) = stream.next().await {
|
||||||
|
let bytes = item?;
|
||||||
|
current_size += bytes.len() as u64;
|
||||||
|
progress_guard.set_position(current_size);
|
||||||
|
data.extend(bytes.into_iter());
|
||||||
|
}
|
||||||
|
Ok(Some(data))
|
||||||
|
} else {
|
||||||
|
let bytes = response.bytes().await?;
|
||||||
|
Ok(Some(bytes.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Asynchronously fetches the given HTTP URL one pass only.
|
/// Asynchronously fetches the given HTTP URL one pass only.
|
||||||
/// If no redirect is present and no error occurs,
|
/// If no redirect is present and no error occurs,
|
||||||
/// yields Code(ResultPayload).
|
/// yields Code(ResultPayload).
|
||||||
|
|
|
@ -80,6 +80,7 @@ use crate::util::fs::remove_dir_all_if_exists;
|
||||||
use crate::util::path::ensure_directory_specifier;
|
use crate::util::path::ensure_directory_specifier;
|
||||||
use crate::util::path::specifier_to_file_path;
|
use crate::util::path::specifier_to_file_path;
|
||||||
use crate::util::progress_bar::ProgressBar;
|
use crate::util::progress_bar::ProgressBar;
|
||||||
|
use crate::util::progress_bar::ProgressBarStyle;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LanguageServer(Arc<tokio::sync::Mutex<Inner>>);
|
pub struct LanguageServer(Arc<tokio::sync::Mutex<Inner>>);
|
||||||
|
@ -240,7 +241,7 @@ fn create_lsp_npm_resolver(
|
||||||
http_client: HttpClient,
|
http_client: HttpClient,
|
||||||
) -> NpmPackageResolver {
|
) -> NpmPackageResolver {
|
||||||
let registry_url = RealNpmRegistryApi::default_url();
|
let registry_url = RealNpmRegistryApi::default_url();
|
||||||
let progress_bar = ProgressBar::default();
|
let progress_bar = ProgressBar::new(ProgressBarStyle::TextOnly);
|
||||||
let npm_cache = NpmCache::from_deno_dir(
|
let npm_cache = NpmCache::from_deno_dir(
|
||||||
dir,
|
dir,
|
||||||
// Use an "only" cache setting in order to make the
|
// Use an "only" cache setting in order to make the
|
||||||
|
|
|
@ -409,27 +409,19 @@ impl NpmCache {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _guard = self.progress_bar.update(&dist.tarball);
|
let guard = self.progress_bar.update(&dist.tarball);
|
||||||
let response = self.http_client.get(&dist.tarball).send().await?;
|
let maybe_bytes = self
|
||||||
|
.http_client
|
||||||
if response.status() == 404 {
|
.download_with_progress(&dist.tarball, &guard)
|
||||||
bail!("Could not find npm package tarball at: {}", dist.tarball);
|
.await?;
|
||||||
} else if !response.status().is_success() {
|
match maybe_bytes {
|
||||||
let status = response.status();
|
Some(bytes) => {
|
||||||
let maybe_response_text = response.text().await.ok();
|
|
||||||
bail!(
|
|
||||||
"Bad response: {:?}{}",
|
|
||||||
status,
|
|
||||||
match maybe_response_text {
|
|
||||||
Some(text) => format!("\n\n{}", text),
|
|
||||||
None => String::new(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let bytes = response.bytes().await?;
|
|
||||||
|
|
||||||
verify_and_extract_tarball(package, &bytes, dist, &package_folder)
|
verify_and_extract_tarball(package, &bytes, dist, &package_folder)
|
||||||
}
|
}
|
||||||
|
None => {
|
||||||
|
bail!("Could not find npm package tarball at: {}", dist.tarball);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensures a copy of the package exists in the global cache.
|
/// Ensures a copy of the package exists in the global cache.
|
||||||
|
|
|
@ -439,39 +439,20 @@ impl RealNpmRegistryApiInner {
|
||||||
}
|
}
|
||||||
|
|
||||||
let package_url = self.get_package_url(name);
|
let package_url = self.get_package_url(name);
|
||||||
let _guard = self.progress_bar.update(package_url.as_str());
|
let guard = self.progress_bar.update(package_url.as_str());
|
||||||
|
|
||||||
let response = match self.http_client.get(package_url).send().await {
|
let maybe_bytes = self
|
||||||
Ok(response) => response,
|
.http_client
|
||||||
Err(err) => {
|
.download_with_progress(package_url, &guard)
|
||||||
// attempt to use the local cache
|
.await?;
|
||||||
if let Some(info) = self.load_file_cached_package_info(name) {
|
match maybe_bytes {
|
||||||
return Ok(Some(info));
|
Some(bytes) => {
|
||||||
} else {
|
|
||||||
return Err(err.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if response.status() == 404 {
|
|
||||||
Ok(None)
|
|
||||||
} else if !response.status().is_success() {
|
|
||||||
let status = response.status();
|
|
||||||
let maybe_response_text = response.text().await.ok();
|
|
||||||
bail!(
|
|
||||||
"Bad response: {:?}{}",
|
|
||||||
status,
|
|
||||||
match maybe_response_text {
|
|
||||||
Some(text) => format!("\n\n{}", text),
|
|
||||||
None => String::new(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let bytes = response.bytes().await?;
|
|
||||||
let package_info = serde_json::from_slice(&bytes)?;
|
let package_info = serde_json::from_slice(&bytes)?;
|
||||||
self.save_package_info_to_file_cache(name, &package_info);
|
self.save_package_info_to_file_cache(name, &package_info);
|
||||||
Ok(Some(package_info))
|
Ok(Some(package_info))
|
||||||
}
|
}
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_package_url(&self, name: &str) -> Url {
|
fn get_package_url(&self, name: &str) -> Url {
|
||||||
|
|
|
@ -31,6 +31,7 @@ use crate::npm::RealNpmRegistryApi;
|
||||||
use crate::resolver::CliResolver;
|
use crate::resolver::CliResolver;
|
||||||
use crate::tools::check;
|
use crate::tools::check;
|
||||||
use crate::util::progress_bar::ProgressBar;
|
use crate::util::progress_bar::ProgressBar;
|
||||||
|
use crate::util::progress_bar::ProgressBarStyle;
|
||||||
|
|
||||||
use deno_ast::MediaType;
|
use deno_ast::MediaType;
|
||||||
use deno_core::anyhow::anyhow;
|
use deno_core::anyhow::anyhow;
|
||||||
|
@ -158,7 +159,7 @@ impl ProcState {
|
||||||
let http_cache = HttpCache::new(&deps_cache_location);
|
let http_cache = HttpCache::new(&deps_cache_location);
|
||||||
let root_cert_store = cli_options.resolve_root_cert_store()?;
|
let root_cert_store = cli_options.resolve_root_cert_store()?;
|
||||||
let cache_usage = cli_options.cache_setting();
|
let cache_usage = cli_options.cache_setting();
|
||||||
let progress_bar = ProgressBar::default();
|
let progress_bar = ProgressBar::new(ProgressBarStyle::TextOnly);
|
||||||
let http_client = HttpClient::new(
|
let http_client = HttpClient::new(
|
||||||
Some(root_cert_store.clone()),
|
Some(root_cert_store.clone()),
|
||||||
cli_options.unsafely_ignore_certificate_errors().clone(),
|
cli_options.unsafely_ignore_certificate_errors().clone(),
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
use crate::args::UpgradeFlags;
|
use crate::args::UpgradeFlags;
|
||||||
use crate::colors;
|
use crate::colors;
|
||||||
|
use crate::util::display::human_download_size;
|
||||||
use crate::util::progress_bar::ProgressBar;
|
use crate::util::progress_bar::ProgressBar;
|
||||||
|
use crate::util::progress_bar::ProgressBarStyle;
|
||||||
use crate::version;
|
use crate::version;
|
||||||
|
|
||||||
use deno_core::anyhow::bail;
|
use deno_core::anyhow::bail;
|
||||||
|
@ -348,7 +350,10 @@ pub async fn upgrade(upgrade_flags: UpgradeFlags) -> Result<(), AnyError> {
|
||||||
fs::set_permissions(&new_exe_path, permissions)?;
|
fs::set_permissions(&new_exe_path, permissions)?;
|
||||||
check_exe(&new_exe_path)?;
|
check_exe(&new_exe_path)?;
|
||||||
|
|
||||||
if !upgrade_flags.dry_run {
|
if upgrade_flags.dry_run {
|
||||||
|
fs::remove_file(&new_exe_path)?;
|
||||||
|
log::info!("Upgraded successfully (dry run)");
|
||||||
|
} else {
|
||||||
let output_exe_path =
|
let output_exe_path =
|
||||||
upgrade_flags.output.as_ref().unwrap_or(¤t_exe_path);
|
upgrade_flags.output.as_ref().unwrap_or(¤t_exe_path);
|
||||||
let output_result = if *output_exe_path == current_exe_path {
|
let output_result = if *output_exe_path == current_exe_path {
|
||||||
|
@ -377,11 +382,8 @@ pub async fn upgrade(upgrade_flags: UpgradeFlags) -> Result<(), AnyError> {
|
||||||
return Err(err.into());
|
return Err(err.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
fs::remove_file(&new_exe_path)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Upgraded successfully");
|
log::info!("Upgraded successfully");
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -436,35 +438,37 @@ async fn download_package(
|
||||||
let res = client.get(download_url).send().await?;
|
let res = client.get(download_url).send().await?;
|
||||||
|
|
||||||
if res.status().is_success() {
|
if res.status().is_success() {
|
||||||
let total_size = res.content_length().unwrap() as f64;
|
let total_size = res.content_length().unwrap();
|
||||||
let mut current_size = 0.0;
|
let mut current_size = 0;
|
||||||
let mut data = Vec::with_capacity(total_size as usize);
|
let mut data = Vec::with_capacity(total_size as usize);
|
||||||
let mut stream = res.bytes_stream();
|
let mut stream = res.bytes_stream();
|
||||||
let mut skip_print = 0;
|
let mut skip_print = 0;
|
||||||
const MEBIBYTE: f64 = 1024.0 * 1024.0;
|
let progress_bar = ProgressBar::new(ProgressBarStyle::DownloadBars);
|
||||||
let progress_bar = ProgressBar::default();
|
let progress = progress_bar.update("");
|
||||||
let clear_guard = progress_bar.clear_guard();
|
progress.set_total_size(total_size);
|
||||||
while let Some(item) = stream.next().await {
|
while let Some(item) = stream.next().await {
|
||||||
let bytes = item?;
|
let bytes = item?;
|
||||||
current_size += bytes.len() as f64;
|
current_size += bytes.len() as u64;
|
||||||
data.extend_from_slice(&bytes);
|
data.extend_from_slice(&bytes);
|
||||||
if skip_print == 0 {
|
if progress_bar.is_enabled() {
|
||||||
progress_bar.update(&format!(
|
progress.set_position(current_size);
|
||||||
"{:>4.1} MiB / {:.1} MiB ({:^5.1}%)",
|
} else if skip_print == 0 {
|
||||||
current_size / MEBIBYTE,
|
log::info!(
|
||||||
total_size / MEBIBYTE,
|
"{} / {} ({:^5.1}%)",
|
||||||
(current_size / total_size) * 100.0,
|
human_download_size(current_size, total_size),
|
||||||
));
|
human_download_size(total_size, total_size),
|
||||||
|
(current_size as f64 / total_size as f64) * 100.0,
|
||||||
|
);
|
||||||
skip_print = 10;
|
skip_print = 10;
|
||||||
} else {
|
} else {
|
||||||
skip_print -= 1;
|
skip_print -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drop(clear_guard);
|
drop(progress);
|
||||||
log::info!(
|
log::info!(
|
||||||
"{:.1} MiB / {:.1} MiB (100.0%)",
|
"{} / {} (100.0%)",
|
||||||
current_size / MEBIBYTE,
|
human_download_size(current_size, total_size),
|
||||||
total_size / MEBIBYTE
|
human_download_size(total_size, total_size)
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
|
|
7
cli/util/console.rs
Normal file
7
cli/util/console.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
use deno_runtime::ops::tty::ConsoleSize;
|
||||||
|
|
||||||
|
/// Gets the console size.
|
||||||
|
pub fn console_size() -> Option<ConsoleSize> {
|
||||||
|
let stderr = &deno_runtime::ops::io::STDERR_HANDLE;
|
||||||
|
deno_runtime::ops::tty::console_size(stderr).ok()
|
||||||
|
}
|
|
@ -26,6 +26,25 @@ pub fn human_size(size: f64) -> String {
|
||||||
format!("{}{}{}", negative, pretty_bytes, unit)
|
format!("{}{}{}", negative, pretty_bytes, unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BYTES_TO_KIB: u64 = 2u64.pow(10);
|
||||||
|
const BYTES_TO_MIB: u64 = 2u64.pow(20);
|
||||||
|
|
||||||
|
/// Gets the size used for downloading data. The total bytes is used to
|
||||||
|
/// determine the units to use.
|
||||||
|
pub fn human_download_size(byte_count: u64, total_bytes: u64) -> String {
|
||||||
|
return if total_bytes < BYTES_TO_MIB {
|
||||||
|
get_in_format(byte_count, BYTES_TO_KIB, "KiB")
|
||||||
|
} else {
|
||||||
|
get_in_format(byte_count, BYTES_TO_MIB, "MiB")
|
||||||
|
};
|
||||||
|
|
||||||
|
fn get_in_format(byte_count: u64, conversion: u64, suffix: &str) -> String {
|
||||||
|
let converted_value = byte_count / conversion;
|
||||||
|
let decimal = (byte_count % conversion) * 100 / conversion;
|
||||||
|
format!("{}.{:0>2}{}", converted_value, decimal, suffix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A function that converts a milisecond elapsed time to a string that
|
/// A function that converts a milisecond elapsed time to a string that
|
||||||
/// represents a human readable version of that time.
|
/// represents a human readable version of that time.
|
||||||
pub fn human_elapsed(elapsed: u128) -> String {
|
pub fn human_elapsed(elapsed: u128) -> String {
|
||||||
|
@ -84,6 +103,31 @@ mod tests {
|
||||||
assert_eq!(human_size(-10_f64), "-10B");
|
assert_eq!(human_size(-10_f64), "-10B");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_human_download_size() {
|
||||||
|
assert_eq!(
|
||||||
|
human_download_size(BYTES_TO_KIB / 100 - 1, BYTES_TO_KIB),
|
||||||
|
"0.00KiB"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
human_download_size(BYTES_TO_KIB / 100 + 1, BYTES_TO_KIB),
|
||||||
|
"0.01KiB"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
human_download_size(BYTES_TO_KIB / 5, BYTES_TO_KIB),
|
||||||
|
"0.19KiB"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
human_download_size(BYTES_TO_MIB - 1, BYTES_TO_MIB - 1),
|
||||||
|
"1023.99KiB"
|
||||||
|
);
|
||||||
|
assert_eq!(human_download_size(BYTES_TO_MIB, BYTES_TO_MIB), "1.00MiB");
|
||||||
|
assert_eq!(
|
||||||
|
human_download_size(BYTES_TO_MIB * 9 - 1523, BYTES_TO_MIB),
|
||||||
|
"8.99MiB"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_human_elapsed() {
|
fn test_human_elapsed() {
|
||||||
assert_eq!(human_elapsed(1), "1ms");
|
assert_eq!(human_elapsed(1), "1ms");
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
// Note: Only add code in this folder that has no application specific logic
|
// Note: Only add code in this folder that has no application specific logic
|
||||||
pub mod checksum;
|
pub mod checksum;
|
||||||
|
pub mod console;
|
||||||
pub mod diff;
|
pub mod diff;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod file_watcher;
|
pub mod file_watcher;
|
||||||
|
|
|
@ -1,143 +0,0 @@
|
||||||
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
|
||||||
|
|
||||||
use crate::colors;
|
|
||||||
use deno_core::parking_lot::Mutex;
|
|
||||||
use indexmap::IndexSet;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct ProgressBar(Arc<Mutex<ProgressBarInner>>);
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ProgressBarInner {
|
|
||||||
pb: Option<indicatif::ProgressBar>,
|
|
||||||
is_tty: bool,
|
|
||||||
in_flight: IndexSet<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ProgressBarInner {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
pb: None,
|
|
||||||
is_tty: colors::is_tty(),
|
|
||||||
in_flight: IndexSet::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProgressBarInner {
|
|
||||||
fn get_or_create_pb(&mut self) -> indicatif::ProgressBar {
|
|
||||||
if let Some(pb) = self.pb.as_ref() {
|
|
||||||
return pb.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
let pb = indicatif::ProgressBar::new_spinner();
|
|
||||||
pb.enable_steady_tick(Duration::from_millis(120));
|
|
||||||
pb.set_prefix("Download");
|
|
||||||
pb.set_style(
|
|
||||||
indicatif::ProgressStyle::with_template(
|
|
||||||
"{prefix:.green} {spinner:.green} {msg}",
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]),
|
|
||||||
);
|
|
||||||
self.pb = Some(pb);
|
|
||||||
self.pb.as_ref().unwrap().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_in_flight(&mut self, msg: &str) {
|
|
||||||
if self.in_flight.contains(msg) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.in_flight.insert(msg.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns if removed "in-flight" was last entry and progress
|
|
||||||
/// bar needs to be updated.
|
|
||||||
fn remove_in_flight(&mut self, msg: &str) -> bool {
|
|
||||||
if !self.in_flight.contains(msg) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut is_last = false;
|
|
||||||
if let Some(last) = self.in_flight.last() {
|
|
||||||
is_last = last == msg;
|
|
||||||
}
|
|
||||||
self.in_flight.remove(msg);
|
|
||||||
is_last
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_progress_bar(&mut self) {
|
|
||||||
let pb = self.get_or_create_pb();
|
|
||||||
if let Some(msg) = self.in_flight.last() {
|
|
||||||
pb.set_message(msg.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct UpdateGuard {
|
|
||||||
pb: ProgressBar,
|
|
||||||
msg: String,
|
|
||||||
noop: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for UpdateGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.noop {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut inner = self.pb.0.lock();
|
|
||||||
if inner.remove_in_flight(&self.msg) {
|
|
||||||
inner.update_progress_bar();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProgressBar {
|
|
||||||
pub fn update(&self, msg: &str) -> UpdateGuard {
|
|
||||||
let mut guard = UpdateGuard {
|
|
||||||
pb: self.clone(),
|
|
||||||
msg: msg.to_string(),
|
|
||||||
noop: false,
|
|
||||||
};
|
|
||||||
let mut inner = self.0.lock();
|
|
||||||
|
|
||||||
// If we're not running in TTY we're just gonna fallback
|
|
||||||
// to using logger crate.
|
|
||||||
if !inner.is_tty {
|
|
||||||
log::log!(log::Level::Info, "{} {}", colors::green("Download"), msg);
|
|
||||||
guard.noop = true;
|
|
||||||
return guard;
|
|
||||||
}
|
|
||||||
|
|
||||||
inner.add_in_flight(msg);
|
|
||||||
inner.update_progress_bar();
|
|
||||||
guard
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear(&self) {
|
|
||||||
let mut inner = self.0.lock();
|
|
||||||
|
|
||||||
if let Some(pb) = inner.pb.as_ref() {
|
|
||||||
pb.finish_and_clear();
|
|
||||||
inner.pb = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_guard(&self) -> ClearGuard {
|
|
||||||
ClearGuard { pb: self.clone() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ClearGuard {
|
|
||||||
pb: ProgressBar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for ClearGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.pb.clear();
|
|
||||||
}
|
|
||||||
}
|
|
218
cli/util/progress_bar/draw_thread.rs
Normal file
218
cli/util/progress_bar/draw_thread.rs
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use console_static_text::ConsoleStaticText;
|
||||||
|
use deno_core::parking_lot::Mutex;
|
||||||
|
use std::sync::atomic::AtomicU64;
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use crate::util::console::console_size;
|
||||||
|
|
||||||
|
use super::renderer::ProgressBarRenderer;
|
||||||
|
use super::renderer::ProgressData;
|
||||||
|
use super::renderer::ProgressDataDisplayEntry;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ProgressBarEntry {
|
||||||
|
id: usize,
|
||||||
|
pub message: String,
|
||||||
|
pos: Arc<AtomicU64>,
|
||||||
|
total_size: Arc<AtomicU64>,
|
||||||
|
draw_thread: DrawThread,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressBarEntry {
|
||||||
|
pub fn position(&self) -> u64 {
|
||||||
|
self.pos.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_position(&self, new_pos: u64) {
|
||||||
|
self.pos.store(new_pos, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_size(&self) -> u64 {
|
||||||
|
self.total_size.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_total_size(&self, new_size: u64) {
|
||||||
|
self.total_size.store(new_size, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(&self) {
|
||||||
|
self.draw_thread.finish_entry(self.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn percent(&self) -> f64 {
|
||||||
|
let pos = self.pos.load(Ordering::Relaxed) as f64;
|
||||||
|
let total_size = self.total_size.load(Ordering::Relaxed) as f64;
|
||||||
|
if total_size == 0f64 {
|
||||||
|
0f64
|
||||||
|
} else {
|
||||||
|
pos / total_size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct InternalState {
|
||||||
|
start_time: SystemTime,
|
||||||
|
// this ensures only one draw thread is running
|
||||||
|
drawer_id: usize,
|
||||||
|
keep_alive_count: usize,
|
||||||
|
has_draw_thread: bool,
|
||||||
|
total_entries: usize,
|
||||||
|
entries: Vec<ProgressBarEntry>,
|
||||||
|
static_text: ConsoleStaticText,
|
||||||
|
renderer: Box<dyn ProgressBarRenderer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct DrawThread {
|
||||||
|
state: Arc<Mutex<InternalState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawThread {
|
||||||
|
pub fn new(renderer: Box<dyn ProgressBarRenderer>) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Arc::new(Mutex::new(InternalState {
|
||||||
|
start_time: SystemTime::now(),
|
||||||
|
drawer_id: 0,
|
||||||
|
keep_alive_count: 0,
|
||||||
|
has_draw_thread: false,
|
||||||
|
total_entries: 0,
|
||||||
|
entries: Vec::new(),
|
||||||
|
static_text: ConsoleStaticText::new(|| {
|
||||||
|
let size = console_size().unwrap();
|
||||||
|
console_static_text::ConsoleSize {
|
||||||
|
cols: Some(size.cols as u16),
|
||||||
|
rows: Some(size.rows as u16),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
renderer,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_entry(&self, message: String) -> ProgressBarEntry {
|
||||||
|
let mut internal_state = self.state.lock();
|
||||||
|
let id = internal_state.total_entries;
|
||||||
|
let entry = ProgressBarEntry {
|
||||||
|
id,
|
||||||
|
draw_thread: self.clone(),
|
||||||
|
message,
|
||||||
|
pos: Default::default(),
|
||||||
|
total_size: Default::default(),
|
||||||
|
};
|
||||||
|
internal_state.entries.push(entry.clone());
|
||||||
|
internal_state.total_entries += 1;
|
||||||
|
internal_state.keep_alive_count += 1;
|
||||||
|
|
||||||
|
if !internal_state.has_draw_thread {
|
||||||
|
self.start_draw_thread(&mut internal_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_entry(&self, entry_id: usize) {
|
||||||
|
let mut internal_state = self.state.lock();
|
||||||
|
|
||||||
|
if let Ok(index) = internal_state
|
||||||
|
.entries
|
||||||
|
.binary_search_by(|e| e.id.cmp(&entry_id))
|
||||||
|
{
|
||||||
|
internal_state.entries.remove(index);
|
||||||
|
self.decrement_keep_alive(&mut internal_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_clear(&self) {
|
||||||
|
let mut internal_state = self.state.lock();
|
||||||
|
internal_state.keep_alive_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrement_clear(&self) {
|
||||||
|
let mut internal_state = self.state.lock();
|
||||||
|
self.decrement_keep_alive(&mut internal_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_keep_alive(&self, internal_state: &mut InternalState) {
|
||||||
|
internal_state.keep_alive_count -= 1;
|
||||||
|
|
||||||
|
if internal_state.keep_alive_count == 0 {
|
||||||
|
internal_state.static_text.eprint_clear();
|
||||||
|
// bump the drawer id to exit the draw thread
|
||||||
|
internal_state.drawer_id += 1;
|
||||||
|
internal_state.has_draw_thread = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_draw_thread(&self, internal_state: &mut InternalState) {
|
||||||
|
internal_state.drawer_id += 1;
|
||||||
|
internal_state.start_time = SystemTime::now();
|
||||||
|
internal_state.has_draw_thread = true;
|
||||||
|
let drawer_id = internal_state.drawer_id;
|
||||||
|
let internal_state = self.state.clone();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let mut previous_size = console_size().unwrap();
|
||||||
|
loop {
|
||||||
|
let mut delay_ms = 120;
|
||||||
|
{
|
||||||
|
let mut internal_state = internal_state.lock();
|
||||||
|
// exit if not the current draw thread
|
||||||
|
if internal_state.drawer_id != drawer_id {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = console_size().unwrap();
|
||||||
|
if size != previous_size {
|
||||||
|
// means the user is actively resizing the console...
|
||||||
|
// wait a little bit until they stop resizing
|
||||||
|
previous_size = size;
|
||||||
|
delay_ms = 200;
|
||||||
|
} else if !internal_state.entries.is_empty() {
|
||||||
|
let preferred_entry = internal_state
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.percent() > 0f64)
|
||||||
|
.or_else(|| internal_state.entries.iter().last())
|
||||||
|
.unwrap();
|
||||||
|
let text = internal_state.renderer.render(ProgressData {
|
||||||
|
duration: internal_state.start_time.elapsed().unwrap(),
|
||||||
|
terminal_width: size.cols,
|
||||||
|
pending_entries: internal_state.entries.len(),
|
||||||
|
total_entries: internal_state.total_entries,
|
||||||
|
display_entry: ProgressDataDisplayEntry {
|
||||||
|
message: preferred_entry.message.clone(),
|
||||||
|
position: preferred_entry.position(),
|
||||||
|
total_size: preferred_entry.total_size(),
|
||||||
|
},
|
||||||
|
percent_done: {
|
||||||
|
let mut total_percent_sum = 0f64;
|
||||||
|
for entry in &internal_state.entries {
|
||||||
|
total_percent_sum += entry.percent();
|
||||||
|
}
|
||||||
|
total_percent_sum += (internal_state.total_entries
|
||||||
|
- internal_state.entries.len())
|
||||||
|
as f64;
|
||||||
|
total_percent_sum / (internal_state.total_entries as f64)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
internal_state.static_text.eprint_with_size(
|
||||||
|
&text,
|
||||||
|
console_static_text::ConsoleSize {
|
||||||
|
cols: Some(size.cols as u16),
|
||||||
|
rows: Some(size.rows as u16),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::sleep(Duration::from_millis(delay_ms));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
123
cli/util/progress_bar/mod.rs
Normal file
123
cli/util/progress_bar/mod.rs
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use crate::colors;
|
||||||
|
|
||||||
|
use self::draw_thread::DrawThread;
|
||||||
|
use self::draw_thread::ProgressBarEntry;
|
||||||
|
|
||||||
|
use super::console::console_size;
|
||||||
|
|
||||||
|
mod draw_thread;
|
||||||
|
mod renderer;
|
||||||
|
|
||||||
|
// Inspired by Indicatif, but this custom implementation allows
|
||||||
|
// for more control over what's going on under the hood.
|
||||||
|
|
||||||
|
pub struct UpdateGuard {
|
||||||
|
maybe_entry: Option<ProgressBarEntry>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for UpdateGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(entry) = &self.maybe_entry {
|
||||||
|
entry.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateGuard {
|
||||||
|
pub fn set_position(&self, value: u64) {
|
||||||
|
if let Some(entry) = &self.maybe_entry {
|
||||||
|
entry.set_position(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_total_size(&self, value: u64) {
|
||||||
|
if let Some(entry) = &self.maybe_entry {
|
||||||
|
entry.set_total_size(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ProgressBarStyle {
|
||||||
|
DownloadBars,
|
||||||
|
TextOnly,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ProgressBar {
|
||||||
|
draw_thread: Option<DrawThread>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProgressBar {
|
||||||
|
/// Checks if progress bars are supported
|
||||||
|
pub fn are_supported() -> bool {
|
||||||
|
atty::is(atty::Stream::Stderr)
|
||||||
|
&& log::log_enabled!(log::Level::Info)
|
||||||
|
&& console_size()
|
||||||
|
.map(|s| s.cols > 0 && s.rows > 0)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(style: ProgressBarStyle) -> Self {
|
||||||
|
Self {
|
||||||
|
draw_thread: match Self::are_supported() {
|
||||||
|
true => Some(DrawThread::new(match style {
|
||||||
|
ProgressBarStyle::DownloadBars => {
|
||||||
|
Box::new(renderer::BarProgressBarRenderer)
|
||||||
|
}
|
||||||
|
ProgressBarStyle::TextOnly => {
|
||||||
|
Box::new(renderer::TextOnlyProgressBarRenderer)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
false => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_enabled(&self) -> bool {
|
||||||
|
self.draw_thread.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&self, msg: &str) -> UpdateGuard {
|
||||||
|
match &self.draw_thread {
|
||||||
|
Some(draw_thread) => {
|
||||||
|
let entry = draw_thread.add_entry(msg.to_string());
|
||||||
|
UpdateGuard {
|
||||||
|
maybe_entry: Some(entry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// if we're not running in TTY, fallback to using logger crate
|
||||||
|
if !msg.is_empty() {
|
||||||
|
log::log!(log::Level::Info, "{} {}", colors::green("Download"), msg);
|
||||||
|
}
|
||||||
|
UpdateGuard { maybe_entry: None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_guard(&self) -> ClearGuard {
|
||||||
|
if let Some(draw_thread) = &self.draw_thread {
|
||||||
|
draw_thread.increment_clear();
|
||||||
|
}
|
||||||
|
ClearGuard { pb: self.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrement_clear(&self) {
|
||||||
|
if let Some(draw_thread) = &self.draw_thread {
|
||||||
|
draw_thread.decrement_clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ClearGuard {
|
||||||
|
pb: ProgressBar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ClearGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.pb.decrement_clear();
|
||||||
|
}
|
||||||
|
}
|
278
cli/util/progress_bar/renderer.rs
Normal file
278
cli/util/progress_bar/renderer.rs
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use deno_runtime::colors;
|
||||||
|
|
||||||
|
use crate::util::display::human_download_size;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProgressDataDisplayEntry {
|
||||||
|
pub message: String,
|
||||||
|
pub position: u64,
|
||||||
|
pub total_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ProgressData {
|
||||||
|
pub terminal_width: u32,
|
||||||
|
pub display_entry: ProgressDataDisplayEntry,
|
||||||
|
pub pending_entries: usize,
|
||||||
|
pub percent_done: f64,
|
||||||
|
pub total_entries: usize,
|
||||||
|
pub duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ProgressBarRenderer: Send + std::fmt::Debug {
|
||||||
|
fn render(&self, data: ProgressData) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indicatif style progress bar.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BarProgressBarRenderer;
|
||||||
|
|
||||||
|
impl ProgressBarRenderer for BarProgressBarRenderer {
|
||||||
|
fn render(&self, data: ProgressData) -> String {
|
||||||
|
let (bytes_text, bytes_text_max_width) = {
|
||||||
|
let total_size = data.display_entry.total_size;
|
||||||
|
let pos = data.display_entry.position;
|
||||||
|
if total_size == 0 {
|
||||||
|
(String::new(), 0)
|
||||||
|
} else {
|
||||||
|
let total_size_str = human_download_size(total_size, total_size);
|
||||||
|
(
|
||||||
|
format!(
|
||||||
|
" {}/{}",
|
||||||
|
human_download_size(pos, total_size),
|
||||||
|
total_size_str,
|
||||||
|
),
|
||||||
|
2 + total_size_str.len() * 2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (total_text, total_text_max_width) = if data.total_entries <= 1 {
|
||||||
|
(String::new(), 0)
|
||||||
|
} else {
|
||||||
|
let total_entries_str = data.total_entries.to_string();
|
||||||
|
(
|
||||||
|
format!(
|
||||||
|
" ({}/{})",
|
||||||
|
data.total_entries - data.pending_entries,
|
||||||
|
data.total_entries
|
||||||
|
),
|
||||||
|
4 + total_entries_str.len() * 2,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let elapsed_text = get_elapsed_text(data.duration);
|
||||||
|
let mut text = String::new();
|
||||||
|
if !data.display_entry.message.is_empty() {
|
||||||
|
text.push_str(&format!(
|
||||||
|
"{} {}{}\n",
|
||||||
|
colors::green("Download"),
|
||||||
|
data.display_entry.message,
|
||||||
|
bytes_text,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
text.push_str(&elapsed_text);
|
||||||
|
let max_width =
|
||||||
|
std::cmp::max(10, std::cmp::min(75, data.terminal_width as i32 - 5))
|
||||||
|
as usize;
|
||||||
|
let same_line_text_width =
|
||||||
|
elapsed_text.len() + total_text_max_width + bytes_text_max_width + 3; // space, open and close brace
|
||||||
|
let total_bars = if same_line_text_width > max_width {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
max_width - same_line_text_width
|
||||||
|
};
|
||||||
|
let completed_bars =
|
||||||
|
(total_bars as f64 * data.percent_done).floor() as usize;
|
||||||
|
text.push_str(" [");
|
||||||
|
if completed_bars != total_bars {
|
||||||
|
if completed_bars > 0 {
|
||||||
|
text.push_str(&format!(
|
||||||
|
"{}",
|
||||||
|
colors::cyan(format!("{}{}", "#".repeat(completed_bars - 1), ">"))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
text.push_str(&format!(
|
||||||
|
"{}",
|
||||||
|
colors::intense_blue("-".repeat(total_bars - completed_bars))
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
text.push_str(&format!("{}", colors::cyan("#".repeat(completed_bars))))
|
||||||
|
}
|
||||||
|
text.push(']');
|
||||||
|
|
||||||
|
// suffix
|
||||||
|
if data.display_entry.message.is_empty() {
|
||||||
|
text.push_str(&colors::gray(bytes_text).to_string());
|
||||||
|
}
|
||||||
|
text.push_str(&colors::gray(total_text).to_string());
|
||||||
|
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TextOnlyProgressBarRenderer;
|
||||||
|
|
||||||
|
impl ProgressBarRenderer for TextOnlyProgressBarRenderer {
|
||||||
|
fn render(&self, data: ProgressData) -> String {
|
||||||
|
let bytes_text = {
|
||||||
|
let total_size = data.display_entry.total_size;
|
||||||
|
let pos = data.display_entry.position;
|
||||||
|
if total_size == 0 {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
" {}/{}",
|
||||||
|
human_download_size(pos, total_size),
|
||||||
|
human_download_size(total_size, total_size)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let total_text = if data.total_entries <= 1 {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
" ({}/{})",
|
||||||
|
data.total_entries - data.pending_entries,
|
||||||
|
data.total_entries
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{} {}{}{}",
|
||||||
|
colors::green("Download"),
|
||||||
|
data.display_entry.message,
|
||||||
|
colors::gray(bytes_text),
|
||||||
|
colors::gray(total_text),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_elapsed_text(elapsed: Duration) -> String {
|
||||||
|
let elapsed_secs = elapsed.as_secs();
|
||||||
|
let seconds = elapsed_secs % 60;
|
||||||
|
let minutes = elapsed_secs / 60;
|
||||||
|
format!("[{:0>2}:{:0>2}]", minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_get_elapsed_text() {
|
||||||
|
assert_eq!(get_elapsed_text(Duration::from_secs(1)), "[00:01]");
|
||||||
|
assert_eq!(get_elapsed_text(Duration::from_secs(20)), "[00:20]");
|
||||||
|
assert_eq!(get_elapsed_text(Duration::from_secs(59)), "[00:59]");
|
||||||
|
assert_eq!(get_elapsed_text(Duration::from_secs(60)), "[01:00]");
|
||||||
|
assert_eq!(
|
||||||
|
get_elapsed_text(Duration::from_secs(60 * 5 + 23)),
|
||||||
|
"[05:23]"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_elapsed_text(Duration::from_secs(60 * 59 + 59)),
|
||||||
|
"[59:59]"
|
||||||
|
);
|
||||||
|
assert_eq!(get_elapsed_text(Duration::from_secs(60 * 60)), "[60:00]");
|
||||||
|
assert_eq!(
|
||||||
|
get_elapsed_text(Duration::from_secs(60 * 60 * 3 + 20 * 60 + 2)),
|
||||||
|
"[200:02]"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_elapsed_text(Duration::from_secs(60 * 60 * 99)),
|
||||||
|
"[5940:00]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BYTES_TO_KIB: u64 = 2u64.pow(10);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_render_bar_progress() {
|
||||||
|
let renderer = BarProgressBarRenderer;
|
||||||
|
let mut data = ProgressData {
|
||||||
|
display_entry: ProgressDataDisplayEntry {
|
||||||
|
message: "data".to_string(),
|
||||||
|
position: 0,
|
||||||
|
total_size: 10 * BYTES_TO_KIB,
|
||||||
|
},
|
||||||
|
duration: Duration::from_secs(1),
|
||||||
|
pending_entries: 1,
|
||||||
|
total_entries: 1,
|
||||||
|
percent_done: 0f64,
|
||||||
|
terminal_width: 50,
|
||||||
|
};
|
||||||
|
let text = renderer.render(data.clone());
|
||||||
|
let text = test_util::strip_ansi_codes(&text);
|
||||||
|
assert_eq!(
|
||||||
|
text,
|
||||||
|
concat!(
|
||||||
|
"Download data 0.00KiB/10.00KiB\n",
|
||||||
|
"[00:01] [-----------------]",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
data.percent_done = 0.5f64;
|
||||||
|
data.display_entry.position = 5 * BYTES_TO_KIB;
|
||||||
|
data.display_entry.message = String::new();
|
||||||
|
data.total_entries = 3;
|
||||||
|
let text = renderer.render(data.clone());
|
||||||
|
let text = test_util::strip_ansi_codes(&text);
|
||||||
|
assert_eq!(text, "[00:01] [####>------] 5.00KiB/10.00KiB (2/3)",);
|
||||||
|
|
||||||
|
// just ensure this doesn't panic
|
||||||
|
data.terminal_width = 0;
|
||||||
|
let text = renderer.render(data.clone());
|
||||||
|
let text = test_util::strip_ansi_codes(&text);
|
||||||
|
assert_eq!(text, "[00:01] [-] 5.00KiB/10.00KiB (2/3)",);
|
||||||
|
|
||||||
|
data.terminal_width = 50;
|
||||||
|
data.pending_entries = 0;
|
||||||
|
data.display_entry.position = 10 * BYTES_TO_KIB;
|
||||||
|
data.percent_done = 1.0f64;
|
||||||
|
let text = renderer.render(data.clone());
|
||||||
|
let text = test_util::strip_ansi_codes(&text);
|
||||||
|
assert_eq!(text, "[00:01] [###########] 10.00KiB/10.00KiB (3/3)",);
|
||||||
|
|
||||||
|
data.display_entry.position = 0;
|
||||||
|
data.display_entry.total_size = 0;
|
||||||
|
data.pending_entries = 0;
|
||||||
|
data.total_entries = 1;
|
||||||
|
let text = renderer.render(data);
|
||||||
|
let text = test_util::strip_ansi_codes(&text);
|
||||||
|
assert_eq!(text, "[00:01] [###################################]",);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_render_text_only_progress() {
|
||||||
|
let renderer = TextOnlyProgressBarRenderer;
|
||||||
|
let mut data = ProgressData {
|
||||||
|
display_entry: ProgressDataDisplayEntry {
|
||||||
|
message: "data".to_string(),
|
||||||
|
position: 0,
|
||||||
|
total_size: 10 * BYTES_TO_KIB,
|
||||||
|
},
|
||||||
|
duration: Duration::from_secs(1),
|
||||||
|
pending_entries: 1,
|
||||||
|
total_entries: 3,
|
||||||
|
percent_done: 0f64,
|
||||||
|
terminal_width: 50,
|
||||||
|
};
|
||||||
|
let text = renderer.render(data.clone());
|
||||||
|
let text = test_util::strip_ansi_codes(&text);
|
||||||
|
assert_eq!(text, "Download data 0.00KiB/10.00KiB (2/3)");
|
||||||
|
|
||||||
|
data.pending_entries = 0;
|
||||||
|
data.total_entries = 1;
|
||||||
|
data.display_entry.position = 0;
|
||||||
|
data.display_entry.total_size = 0;
|
||||||
|
let text = renderer.render(data);
|
||||||
|
let text = test_util::strip_ansi_codes(&text);
|
||||||
|
assert_eq!(text, "Download data");
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,33 +44,33 @@ use {
|
||||||
// alive for the duration of the application since the last handle/fd
|
// alive for the duration of the application since the last handle/fd
|
||||||
// being dropped will close the corresponding pipe.
|
// being dropped will close the corresponding pipe.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
static STDIN_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
pub static STDIN_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
||||||
// SAFETY: corresponds to OS stdin
|
// SAFETY: corresponds to OS stdin
|
||||||
unsafe { StdFile::from_raw_fd(0) }
|
unsafe { StdFile::from_raw_fd(0) }
|
||||||
});
|
});
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
static STDOUT_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
pub static STDOUT_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
||||||
// SAFETY: corresponds to OS stdout
|
// SAFETY: corresponds to OS stdout
|
||||||
unsafe { StdFile::from_raw_fd(1) }
|
unsafe { StdFile::from_raw_fd(1) }
|
||||||
});
|
});
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
static STDERR_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
pub static STDERR_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
||||||
// SAFETY: corresponds to OS stderr
|
// SAFETY: corresponds to OS stderr
|
||||||
unsafe { StdFile::from_raw_fd(2) }
|
unsafe { StdFile::from_raw_fd(2) }
|
||||||
});
|
});
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
static STDIN_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
pub static STDIN_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
||||||
// SAFETY: corresponds to OS stdin
|
// SAFETY: corresponds to OS stdin
|
||||||
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_INPUT_HANDLE)) }
|
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_INPUT_HANDLE)) }
|
||||||
});
|
});
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
static STDOUT_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
pub static STDOUT_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
||||||
// SAFETY: corresponds to OS stdout
|
// SAFETY: corresponds to OS stdout
|
||||||
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_OUTPUT_HANDLE)) }
|
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_OUTPUT_HANDLE)) }
|
||||||
});
|
});
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
static STDERR_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
pub static STDERR_HANDLE: Lazy<StdFile> = Lazy::new(|| {
|
||||||
// SAFETY: corresponds to OS stderr
|
// SAFETY: corresponds to OS stderr
|
||||||
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_ERROR_HANDLE)) }
|
unsafe { StdFile::from_raw_handle(GetStdHandle(winbase::STD_ERROR_HANDLE)) }
|
||||||
});
|
});
|
||||||
|
|
|
@ -201,46 +201,10 @@ fn op_console_size(
|
||||||
rid: u32,
|
rid: u32,
|
||||||
) -> Result<(), AnyError> {
|
) -> Result<(), AnyError> {
|
||||||
StdFileResource::with_file(state, rid, move |std_file| {
|
StdFileResource::with_file(state, rid, move |std_file| {
|
||||||
#[cfg(windows)]
|
let size = console_size(std_file)?;
|
||||||
{
|
result[0] = size.cols;
|
||||||
use std::os::windows::io::AsRawHandle;
|
result[1] = size.rows;
|
||||||
let handle = std_file.as_raw_handle();
|
|
||||||
|
|
||||||
// SAFETY: winapi calls
|
|
||||||
unsafe {
|
|
||||||
let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO =
|
|
||||||
std::mem::zeroed();
|
|
||||||
|
|
||||||
if winapi::um::wincon::GetConsoleScreenBufferInfo(
|
|
||||||
handle,
|
|
||||||
&mut bufinfo,
|
|
||||||
) == 0
|
|
||||||
{
|
|
||||||
return Err(Error::last_os_error().into());
|
|
||||||
}
|
|
||||||
result[0] = bufinfo.dwSize.X as u32;
|
|
||||||
result[1] = bufinfo.dwSize.Y as u32;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
|
|
||||||
let fd = std_file.as_raw_fd();
|
|
||||||
// TODO(bartlomieju):
|
|
||||||
#[allow(clippy::undocumented_unsafe_blocks)]
|
|
||||||
unsafe {
|
|
||||||
let mut size: libc::winsize = std::mem::zeroed();
|
|
||||||
if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 {
|
|
||||||
return Err(Error::last_os_error().into());
|
|
||||||
}
|
|
||||||
result[0] = size.ws_col as u32;
|
|
||||||
result[1] = size.ws_row as u32;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,3 +220,53 @@ fn op_console_size(
|
||||||
|
|
||||||
last_result
|
last_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
pub struct ConsoleSize {
|
||||||
|
pub cols: u32,
|
||||||
|
pub rows: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn console_size(
|
||||||
|
std_file: &std::fs::File,
|
||||||
|
) -> Result<ConsoleSize, std::io::Error> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
let handle = std_file.as_raw_handle();
|
||||||
|
|
||||||
|
// SAFETY: winapi calls
|
||||||
|
unsafe {
|
||||||
|
let mut bufinfo: winapi::um::wincon::CONSOLE_SCREEN_BUFFER_INFO =
|
||||||
|
std::mem::zeroed();
|
||||||
|
|
||||||
|
if winapi::um::wincon::GetConsoleScreenBufferInfo(handle, &mut bufinfo)
|
||||||
|
== 0
|
||||||
|
{
|
||||||
|
return Err(Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(ConsoleSize {
|
||||||
|
cols: bufinfo.dwSize.X as u32,
|
||||||
|
rows: bufinfo.dwSize.Y as u32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
let fd = std_file.as_raw_fd();
|
||||||
|
// SAFETY: libc calls
|
||||||
|
unsafe {
|
||||||
|
let mut size: libc::winsize = std::mem::zeroed();
|
||||||
|
if libc::ioctl(fd, libc::TIOCGWINSZ, &mut size as *mut _) != 0 {
|
||||||
|
return Err(Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(ConsoleSize {
|
||||||
|
cols: size.ws_col as u32,
|
||||||
|
rows: size.ws_row as u32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue