diff --git a/Cargo.lock b/Cargo.lock index 48972fa8f3..c194fc6e2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,9 +501,9 @@ dependencies = [ [[package]] name = "console_static_text" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d749e1f5316d8a15ec592516a631ab9b8099cc6d085b69b905462fc071caedb" +checksum = "f166cdfb9db0607e2079b382ba64bc4164344006c733b95c1ecfa782a180a34a" dependencies = [ "unicode-width", "vte", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index e7fc497ccc..665a715ae4 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -59,7 +59,7 @@ chrono = { version = "=0.4.22", default-features = false, features = ["clock"] } clap = "=3.1.12" clap_complete = "=3.1.2" clap_complete_fig = "=3.1.5" -console_static_text = "=0.3.3" +console_static_text = "=0.3.4" data-url.workspace = true dissimilar = "=1.0.4" dprint-plugin-json = "=0.17.0" diff --git a/cli/util/draw_thread.rs b/cli/util/draw_thread.rs new file mode 100644 index 0000000000..b069a14b7c --- /dev/null +++ b/cli/util/draw_thread.rs @@ -0,0 +1,237 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use console_static_text::ConsoleStaticText; +use deno_core::parking_lot::Mutex; +use deno_runtime::ops::tty::ConsoleSize; +use once_cell::sync::Lazy; +use std::sync::Arc; +use std::time::Duration; + +use crate::util::console::console_size; + +/// Renders text that will be displayed stacked in a +/// static place on the console. +pub trait DrawThreadRenderer: Send + Sync + std::fmt::Debug { + fn render(&self, data: &ConsoleSize) -> String; +} + +/// Draw thread guard. Keep this alive for the duration +/// that you wish the entry to be drawn for. Once it is +/// dropped, then the entry will be removed from the draw +/// thread. +#[derive(Debug)] +pub struct DrawThreadGuard(u16); + +impl Drop for DrawThreadGuard { + fn drop(&mut self) { + DrawThread::finish_entry(self.0) + } +} + +#[derive(Debug, Clone)] +struct InternalEntry { + priority: u8, + id: u16, + renderer: Arc, +} + +#[derive(Debug)] +struct InternalState { + // this ensures only one actual draw thread is running + drawer_id: usize, + hide: bool, + has_draw_thread: bool, + next_entry_id: u16, + entries: Vec, + static_text: ConsoleStaticText, +} + +impl InternalState { + pub fn should_exit_draw_thread(&self, drawer_id: usize) -> bool { + self.drawer_id != drawer_id || self.entries.is_empty() + } +} + +static INTERNAL_STATE: Lazy>> = Lazy::new(|| { + Arc::new(Mutex::new(InternalState { + drawer_id: 0, + hide: false, + has_draw_thread: false, + entries: Vec::new(), + next_entry_id: 0, + 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), + } + }), + })) +}); + +/// The draw thread is responsible for rendering multiple active +/// `DrawThreadRenderer`s to stderr. It is global because the +/// concept of stderr in the process is also a global concept. +#[derive(Clone, Debug)] +pub struct DrawThread; + +impl DrawThread { + /// Is using a draw thread supported. + pub fn is_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) + } + + /// Adds a renderer to the draw thread with a given priority. + /// Renderers are sorted by priority with higher priority + /// entries appearing at the bottom of the screen. + pub fn add_entry( + priority: u8, + renderer: Arc, + ) -> DrawThreadGuard { + let internal_state = &*INTERNAL_STATE; + let mut internal_state = internal_state.lock(); + let id = internal_state.next_entry_id; + internal_state.entries.push(InternalEntry { + id, + priority, + renderer, + }); + internal_state + .entries + .sort_by(|a, b| a.priority.cmp(&b.priority)); + + if internal_state.next_entry_id == u16::MAX { + internal_state.next_entry_id = 0; + } else { + internal_state.next_entry_id += 1; + } + + Self::maybe_start_draw_thread(&mut internal_state); + + DrawThreadGuard(id) + } + + /// Hides the draw thread. + #[allow(dead_code)] + pub fn hide() { + let internal_state = &*INTERNAL_STATE; + let mut internal_state = internal_state.lock(); + internal_state.hide = true; + Self::clear_and_stop_draw_thread(&mut internal_state); + } + + /// Shows the draw thread if it was previously hidden. + #[allow(dead_code)] + pub fn show() { + let internal_state = &*INTERNAL_STATE; + let mut internal_state = internal_state.lock(); + internal_state.hide = false; + + Self::maybe_start_draw_thread(&mut internal_state); + } + + fn finish_entry(entry_id: u16) { + let internal_state = &*INTERNAL_STATE; + let mut internal_state = internal_state.lock(); + + if let Some(index) = + internal_state.entries.iter().position(|e| e.id == entry_id) + { + internal_state.entries.remove(index); + + if internal_state.entries.is_empty() { + Self::clear_and_stop_draw_thread(&mut internal_state); + } + } + } + + fn clear_and_stop_draw_thread(internal_state: &mut InternalState) { + if internal_state.has_draw_thread { + 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 maybe_start_draw_thread(internal_state: &mut InternalState) { + if internal_state.has_draw_thread || internal_state.hide { + return; + } + + internal_state.drawer_id += 1; + internal_state.has_draw_thread = true; + + let drawer_id = internal_state.drawer_id; + tokio::task::spawn_blocking(move || { + let mut previous_size = console_size().unwrap(); + loop { + let mut delay_ms = 120; + { + // Get the entries to render. + let entries = { + let internal_state = &*INTERNAL_STATE; + let internal_state = internal_state.lock(); + if internal_state.should_exit_draw_thread(drawer_id) { + break; + } + internal_state.entries.clone() + }; + + // Call into the renderers outside the lock to prevent a potential + // deadlock between our internal state lock and the renderers + // internal state lock. + // + // Example deadlock if this code didn't do this: + // 1. Other thread - Renderer - acquired internal lock to update state + // 2. This thread - Acquired internal state + // 3. Other thread - Renderer - drops DrawThreadGuard + // 4. This thread - Calls renderer.render within internal lock, + // which attempts to acquire the other thread's Render's internal + // lock causing a deadlock + let mut text = String::new(); + 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 { + let mut should_new_line_next = false; + for entry in entries { + let new_text = entry.renderer.render(&size); + if should_new_line_next && !new_text.is_empty() { + text.push('\n'); + } + should_new_line_next = !new_text.is_empty(); + text.push_str(&new_text); + } + } + + // now reacquire the lock, ensure we should still be drawing, then + // output the text + { + let internal_state = &*INTERNAL_STATE; + let mut internal_state = internal_state.lock(); + if internal_state.should_exit_draw_thread(drawer_id) { + break; + } + 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)); + } + }); + } +} diff --git a/cli/util/mod.rs b/cli/util/mod.rs index ab311ee86f..a7a72f3b4a 100644 --- a/cli/util/mod.rs +++ b/cli/util/mod.rs @@ -5,6 +5,7 @@ pub mod checksum; pub mod console; pub mod diff; pub mod display; +pub mod draw_thread; pub mod file_watcher; pub mod fs; pub mod logger; diff --git a/cli/util/progress_bar/draw_thread.rs b/cli/util/progress_bar/draw_thread.rs deleted file mode 100644 index 89e8ab53f0..0000000000 --- a/cli/util/progress_bar/draw_thread.rs +++ /dev/null @@ -1,218 +0,0 @@ -// 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, - total_size: Arc, - 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, - static_text: ConsoleStaticText, - renderer: Box, -} - -#[derive(Clone, Debug)] -pub struct DrawThread { - state: Arc>, -} - -impl DrawThread { - pub fn new(renderer: Box) -> 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)); - } - }); - } -} diff --git a/cli/util/progress_bar/mod.rs b/cli/util/progress_bar/mod.rs index 83292e2d1c..8651e2d209 100644 --- a/cli/util/progress_bar/mod.rs +++ b/cli/util/progress_bar/mod.rs @@ -1,13 +1,23 @@ // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::SystemTime; + +use deno_core::parking_lot::Mutex; +use deno_runtime::ops::tty::ConsoleSize; + use crate::colors; -use self::draw_thread::DrawThread; -use self::draw_thread::ProgressBarEntry; +use self::renderer::ProgressBarRenderer; +use self::renderer::ProgressData; +use self::renderer::ProgressDataDisplayEntry; -use super::console::console_size; +use super::draw_thread::DrawThread; +use super::draw_thread::DrawThreadGuard; +use super::draw_thread::DrawThreadRenderer; -mod draw_thread; mod renderer; // Inspired by Indicatif, but this custom implementation allows @@ -45,30 +55,197 @@ pub enum ProgressBarStyle { TextOnly, } +#[derive(Clone, Debug)] +struct ProgressBarEntry { + id: usize, + pub message: String, + pos: Arc, + total_size: Arc, + progress_bar: ProgressBarInner, +} + +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.progress_bar.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 { + /// If this guard exists, then it means the progress + /// bar is displaying in the draw thread. + draw_thread_guard: Option, + start_time: SystemTime, + keep_alive_count: usize, + total_entries: usize, + entries: Vec, +} + +#[derive(Clone, Debug)] +struct ProgressBarInner { + state: Arc>, + renderer: Arc, +} + +impl ProgressBarInner { + fn new(renderer: Arc) -> Self { + Self { + state: Arc::new(Mutex::new(InternalState { + draw_thread_guard: None, + start_time: SystemTime::now(), + keep_alive_count: 0, + total_entries: 0, + entries: Vec::new(), + })), + 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, + message, + pos: Default::default(), + total_size: Default::default(), + progress_bar: self.clone(), + }; + internal_state.entries.push(entry.clone()); + internal_state.total_entries += 1; + internal_state.keep_alive_count += 1; + + self.maybe_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, state: &mut InternalState) { + state.keep_alive_count -= 1; + + if state.keep_alive_count == 0 { + // drop the guard to remove this from the draw thread + state.draw_thread_guard.take(); + } + } + + fn maybe_start_draw_thread(&self, internal_state: &mut InternalState) { + if internal_state.draw_thread_guard.is_none() + && internal_state.keep_alive_count > 0 + { + internal_state.start_time = SystemTime::now(); + internal_state.draw_thread_guard = + Some(DrawThread::add_entry(0, Arc::new(self.clone()))); + } + } +} + +impl DrawThreadRenderer for ProgressBarInner { + fn render(&self, size: &ConsoleSize) -> String { + let data = { + let state = self.state.lock(); + if state.entries.is_empty() { + return String::new(); + } + let preferred_entry = state + .entries + .iter() + .find(|e| e.percent() > 0f64) + .or_else(|| state.entries.iter().last()) + .unwrap(); + ProgressData { + duration: state.start_time.elapsed().unwrap(), + terminal_width: size.cols, + pending_entries: state.entries.len(), + total_entries: 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 &state.entries { + total_percent_sum += entry.percent(); + } + total_percent_sum += + (state.total_entries - state.entries.len()) as f64; + total_percent_sum / (state.total_entries as f64) + }, + } + }; + self.renderer.render(data) + } +} + #[derive(Clone, Debug)] pub struct ProgressBar { - draw_thread: Option, + inner: Option, } 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) + DrawThread::is_supported() } pub fn new(style: ProgressBarStyle) -> Self { Self { - draw_thread: match Self::are_supported() { - true => Some(DrawThread::new(match style { + inner: match Self::are_supported() { + true => Some(ProgressBarInner::new(match style { ProgressBarStyle::DownloadBars => { - Box::new(renderer::BarProgressBarRenderer) + Arc::new(renderer::BarProgressBarRenderer) } ProgressBarStyle::TextOnly => { - Box::new(renderer::TextOnlyProgressBarRenderer) + Arc::new(renderer::TextOnlyProgressBarRenderer) } })), false => None, @@ -77,9 +254,9 @@ impl ProgressBar { } pub fn update(&self, msg: &str) -> UpdateGuard { - match &self.draw_thread { - Some(draw_thread) => { - let entry = draw_thread.add_entry(msg.to_string()); + match &self.inner { + Some(inner) => { + let entry = inner.add_entry(msg.to_string()); UpdateGuard { maybe_entry: Some(entry), } @@ -95,15 +272,15 @@ impl ProgressBar { } pub fn clear_guard(&self) -> ClearGuard { - if let Some(draw_thread) = &self.draw_thread { - draw_thread.increment_clear(); + if let Some(inner) = &self.inner { + inner.increment_clear(); } ClearGuard { pb: self.clone() } } fn decrement_clear(&self) { - if let Some(draw_thread) = &self.draw_thread { - draw_thread.decrement_clear(); + if let Some(inner) = &self.inner { + inner.decrement_clear(); } } } diff --git a/cli/util/progress_bar/renderer.rs b/cli/util/progress_bar/renderer.rs index 75a4cafed8..d8fa1769d5 100644 --- a/cli/util/progress_bar/renderer.rs +++ b/cli/util/progress_bar/renderer.rs @@ -23,7 +23,7 @@ pub struct ProgressData { pub duration: Duration, } -pub trait ProgressBarRenderer: Send + std::fmt::Debug { +pub trait ProgressBarRenderer: Send + Sync + std::fmt::Debug { fn render(&self, data: ProgressData) -> String; }