-
Notifications
You must be signed in to change notification settings - Fork 5.4k
/
draw_thread.rs
240 lines (210 loc) · 7.31 KB
/
draw_thread.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
use console_static_text::ConsoleStaticText;
use deno_core::parking_lot::Mutex;
use deno_core::unsync::spawn_blocking;
use deno_runtime::ops::tty::ConsoleSize;
use once_cell::sync::Lazy;
use std::io::IsTerminal;
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 {
id: u16,
renderer: Arc<dyn DrawThreadRenderer>,
}
#[derive(Debug)]
struct InternalState {
// this ensures only one actual draw thread is running
drawer_id: usize,
hide_count: usize,
has_draw_thread: bool,
next_entry_id: u16,
entries: Vec<InternalEntry>,
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<Arc<Mutex<InternalState>>> = Lazy::new(|| {
Arc::new(Mutex::new(InternalState {
drawer_id: 0,
hide_count: 0,
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),
}
}),
}))
});
static IS_TTY_WITH_CONSOLE_SIZE: Lazy<bool> = Lazy::new(|| {
std::io::stderr().is_terminal()
&& console_size()
.map(|s| s.cols > 0 && s.rows > 0)
.unwrap_or(false)
});
/// 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 {
// don't put the log level in the lazy because the
// log level may change as the application runs
log::log_enabled!(log::Level::Info) && *IS_TTY_WITH_CONSOLE_SIZE
}
/// Adds a renderer to the draw thread.
pub fn add_entry(renderer: Arc<dyn DrawThreadRenderer>) -> 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, renderer });
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.
pub fn hide() {
let internal_state = &*INTERNAL_STATE;
let mut internal_state = internal_state.lock();
internal_state.hide_count += 1;
Self::clear_and_stop_draw_thread(&mut internal_state);
}
/// Shows the draw thread if it was previously hidden.
pub fn show() {
let internal_state = &*INTERNAL_STATE;
let mut internal_state = internal_state.lock();
if internal_state.hide_count > 0 {
internal_state.hide_count -= 1;
if internal_state.hide_count == 0 {
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_count > 0
|| internal_state.entries.is_empty()
|| !DrawThread::is_supported()
{
return;
}
internal_state.drawer_id += 1;
internal_state.has_draw_thread = true;
let drawer_id = internal_state.drawer_id;
spawn_blocking(move || {
let mut previous_size = console_size();
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()
};
// this should always be set, but have the code handle
// it not being for some reason
let size = console_size();
// 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();
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 let Some(size) = size {
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));
}
});
}
}