1#![cfg(feature = "console")]
2
3use std::{
4 collections::VecDeque,
5 sync::{Arc, Mutex},
6};
7
8use futures::future::{AbortHandle, Abortable};
9use rustyline_async::{Readline, ReadlineError, ReadlineEvent};
10use termimad::{
11 FmtText, MadSkin,
12 minimad::{Compound, Line, Options, Text, parse_text},
13 terminal_size,
14};
15use tokio::task::JoinHandle;
16use tuwunel_core::{Server, debug, defer, error, log, log::is_systemd_mode};
17
18use super::CommandOutput;
19
20pub struct Console {
21 server: Arc<Server>,
22 services: Arc<crate::services::OnceServices>,
23 worker_join: Mutex<Option<JoinHandle<()>>>,
24 input_abort: Mutex<Option<AbortHandle>>,
25 command_abort: Mutex<Option<AbortHandle>>,
26 history: Mutex<VecDeque<String>>,
27 output: MadSkin,
28}
29
30const PROMPT: &str = "uwu> ";
31const HISTORY_LIMIT: usize = 48;
32
33impl Console {
34 pub(super) fn new(args: &crate::Args<'_>) -> Arc<Self> {
35 Arc::new(Self {
36 server: args.server.clone(),
37 services: args.services.clone(),
38 worker_join: None.into(),
39 input_abort: None.into(),
40 command_abort: None.into(),
41 history: VecDeque::with_capacity(HISTORY_LIMIT).into(),
42 output: configure_output(MadSkin::default_dark()),
43 })
44 }
45
46 pub(super) fn handle_signal(self: &Arc<Self>, sig: &'static str) {
47 if !self.server.is_running() {
48 self.interrupt();
49 } else if sig == "SIGINT" {
50 self.interrupt_command();
51 self.start();
52 }
53 }
54
55 pub fn start(self: &Arc<Self>) {
56 let mut worker_join = self.worker_join.lock().expect("locked");
57 if worker_join.is_none() {
58 let self_ = Arc::clone(self);
59 _ = worker_join.insert(self.server.runtime().spawn(self_.worker()));
60 }
61 }
62
63 pub async fn close(self: &Arc<Self>) {
64 self.interrupt();
65
66 let Some(worker_join) = self.worker_join.lock().expect("locked").take() else {
67 return;
68 };
69
70 _ = worker_join.await;
71 }
72
73 pub fn interrupt(self: &Arc<Self>) {
74 self.interrupt_command();
75 self.interrupt_readline();
76 self.worker_join
77 .lock()
78 .expect("locked")
79 .as_ref()
80 .map(JoinHandle::abort);
81 }
82
83 pub fn interrupt_readline(self: &Arc<Self>) {
84 let Some(input_abort) = self.input_abort.lock().expect("locked").take() else {
85 return;
86 };
87
88 debug!("Interrupting console readline...");
89 input_abort.abort();
90 }
91
92 pub fn interrupt_command(self: &Arc<Self>) {
93 let Some(command_abort) = self.command_abort.lock().expect("locked").take() else {
94 return;
95 };
96
97 debug!("Interrupting console command...");
98 command_abort.abort();
99 }
100
101 #[tracing::instrument(skip_all, name = "console", level = "trace")]
102 async fn worker(self: Arc<Self>) {
103 debug!("session starting");
104
105 self.output
106 .print_inline(&format!("**tuwunel {}** admin console\n", tuwunel_core::version()));
107 self.output
108 .print_text("\"help\" for help, ^D to exit the console, ^\\ to stop the server\n");
109
110 while self.server.is_running() {
111 match self.readline().await {
112 | Ok(event) => match event {
113 | ReadlineEvent::Line(string) => self.clone().handle(string).await,
114 | ReadlineEvent::Interrupted => {},
115 | ReadlineEvent::Eof => break,
116 | ReadlineEvent::Quit => self
117 .server
118 .shutdown()
119 .unwrap_or_else(error::default_log),
120 },
121 | Err(error) => match error {
122 | ReadlineError::Closed => break,
123 | ReadlineError::IO(error) => {
124 error!("console I/O: {error:?}");
125 break;
126 },
127 },
128 }
129 }
130
131 debug!("session ending");
132 self.worker_join.lock().expect("locked").take();
133 }
134
135 async fn readline(self: &Arc<Self>) -> Result<ReadlineEvent, ReadlineError> {
136 let _suppression = (!is_systemd_mode()).then(|| log::Suppress::new(&self.server));
137
138 let (mut readline, _writer) = Readline::new(PROMPT.to_owned())?;
139 let self_ = Arc::clone(self);
140 readline.set_tab_completer(move |line| self_.tab_complete(line));
141 self.set_history(&mut readline);
142
143 let future = readline.readline();
144
145 let (abort, abort_reg) = AbortHandle::new_pair();
146 let future = Abortable::new(future, abort_reg);
147 _ = self
148 .input_abort
149 .lock()
150 .expect("locked")
151 .insert(abort);
152 defer! {{
153 _ = self.input_abort.lock().expect("locked").take();
154 }}
155
156 let Ok(result) = future.await else {
157 return Ok(ReadlineEvent::Eof);
158 };
159
160 readline.flush()?;
161 result
162 }
163
164 async fn handle(self: Arc<Self>, line: String) {
165 if line.trim().is_empty() {
166 return;
167 }
168
169 self.add_history(line.clone());
170 let future = self.clone().process(line);
171
172 let (abort, abort_reg) = AbortHandle::new_pair();
173 let future = Abortable::new(future, abort_reg);
174 _ = self
175 .command_abort
176 .lock()
177 .expect("locked")
178 .insert(abort);
179 defer! {{
180 _ = self.command_abort.lock().expect("locked").take();
181 }}
182
183 _ = future.await;
184 }
185
186 async fn process(self: Arc<Self>, line: String) {
187 match self
188 .services
189 .admin
190 .command_in_place(line.into())
191 .await
192 {
193 | Ok(Some(ref content)) => self.output(content),
194 | Err(ref content) => self.output_err(content),
195 | _ => unreachable!(),
196 }
197 }
198
199 fn output_err(self: Arc<Self>, output_content: &CommandOutput) {
200 let output = configure_output_err(self.output.clone());
201
202 print_output(&output, output_content.as_str());
203 }
204
205 fn output(self: Arc<Self>, output_content: &CommandOutput) {
206 print_output(&self.output, output_content.as_str());
207 }
208
209 fn set_history(&self, readline: &mut Readline) {
210 self.history
211 .lock()
212 .expect("locked")
213 .iter()
214 .rev()
215 .for_each(|entry| {
216 readline
217 .add_history_entry(entry.clone())
218 .expect("added history entry");
219 });
220 }
221
222 fn add_history(&self, line: String) {
223 let mut history = self.history.lock().expect("locked");
224 history.push_front(line);
225 history.truncate(HISTORY_LIMIT);
226 }
227
228 fn tab_complete(&self, line: &str) -> String {
229 self.services
230 .admin
231 .complete_command(line)
232 .unwrap_or_else(|| line.to_owned())
233 }
234}
235
236pub fn print_err(markdown: &str) {
238 let output = configure_output_err(MadSkin::default_dark());
239
240 print_output(&output, markdown);
241}
242pub fn print(markdown: &str) {
244 let output = configure_output(MadSkin::default_dark());
245
246 print_output(&output, markdown);
247}
248
249fn print_output(output: &MadSkin, markdown: &str) {
250 let (width, _) = terminal_size();
251 let output = format_output(output, markdown, usize::from(width));
252
253 print!("{output}");
254}
255
256fn format_output<'k, 's>(
257 output: &'k MadSkin,
258 markdown: &'s str,
259 width: usize,
260) -> FmtText<'k, 's> {
261 let lines = parse_text(markdown, Options::default())
262 .lines
263 .into_iter()
264 .map(|mut line| {
265 if let Line::TableRow(row) = &mut line {
266 for cell in &mut row.cells {
267 cell.compounds.insert(0, Compound::raw_str(" "));
268 cell.compounds.push(Compound::raw_str(" "));
269 }
270 }
271
272 line
273 })
274 .collect();
275
276 FmtText::from_text(output, Text { lines }, Some(width))
277}
278
279fn configure_output_err(mut output: MadSkin) -> MadSkin {
280 use termimad::{Alignment, CompoundStyle, LineStyle, crossterm::style::Color};
281
282 let code_style = CompoundStyle::with_fgbg(Color::AnsiValue(196), Color::AnsiValue(234));
283 output.inline_code = code_style;
284 output.code_block = LineStyle {
285 left_margin: 0,
286 right_margin: 0,
287 align: Alignment::Left,
288 compound_style: code_style,
289 };
290
291 output
292}
293
294fn configure_output(mut output: MadSkin) -> MadSkin {
295 use termimad::{Alignment, CompoundStyle, LineStyle, crossterm::style::Color};
296
297 let code_style = CompoundStyle::with_fgbg(Color::AnsiValue(40), Color::AnsiValue(234));
298 output.inline_code = code_style;
299 output.code_block = LineStyle {
300 left_margin: 0,
301 right_margin: 0,
302 align: Alignment::Left,
303 compound_style: code_style,
304 };
305
306 let table_style = CompoundStyle::default();
307 output.table = LineStyle {
308 left_margin: 0,
309 right_margin: 0,
310 align: Alignment::Left,
311 compound_style: table_style,
312 };
313
314 output
315}