tuwunel_service/admin/
console.rs1#![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
231pub fn print_err(markdown: &str) {
233 let output = configure_output_err(MadSkin::default_dark());
234 output.print_text(markdown);
235}
236pub 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}