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