Skip to main content

tuwunel_service/admin/
console.rs

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
236/// Standalone/static markdown printer for errors.
237pub fn print_err(markdown: &str) {
238	let output = configure_output_err(MadSkin::default_dark());
239
240	print_output(&output, markdown);
241}
242/// Standalone/static markdown printer.
243pub 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}