1#![forbid(unsafe_code)]
7
8mod unix;
9mod windows;
10
11use anyhow::Context as _;
12use futures::AsyncRead;
13use futures::AsyncWrite;
14use futures::AsyncWriteExt;
15use futures::executor::block_on;
16use futures::io::AllowStdIo;
17use futures::io::AsyncReadExt;
18use pal_async::driver::Driver;
19use pal_async::local::block_with_io;
20use std::borrow::Cow;
21use std::ffi::OsStr;
22use std::path::Path;
23use std::path::PathBuf;
24use std::pin::Pin;
25use std::process::Command;
26use std::task::Context;
27use term::raw_stdout;
28use term::set_console_title;
29use term::set_raw_console;
30
31#[derive(Default)]
32pub struct ConsoleLaunchOptions {
34 pub window_title: Option<String>,
36}
37
38pub fn relay_console(path: &Path, console_title: &str) -> anyhow::Result<()> {
41 block_with_io(async |driver| {
49 #[cfg(unix)]
50 let (read, mut write) = {
51 let pipe = pal_async::socket::PolledSocket::connect_unix(&driver, path)
52 .await
53 .context("failed to connect to console socket")?;
54 pipe.split()
55 };
56 #[cfg(windows)]
57 let (read, mut write) = {
58 let pipe = std::fs::OpenOptions::new()
59 .read(true)
60 .write(true)
61 .open(path)
62 .context("failed to connect to console pipe")?;
63 let pipe = pal_async::pipe::PolledPipe::new(&driver, pipe)
64 .context("failed to create polled pipe")?;
65 AsyncReadExt::split(pipe)
66 };
67
68 set_raw_console(true).expect("failed to set raw console mode");
69 if let Err(err) = set_console_title(console_title) {
70 tracing::warn!("failed to set console title: {}", err);
71 }
72
73 std::thread::Builder::new()
74 .name("input_thread".into())
75 .spawn({
76 move || {
77 block_on(futures::io::copy(
78 AllowStdIo::new(std::io::stdin()),
79 &mut write,
80 ))
81 }
82 })
83 .unwrap();
84
85 futures::io::copy(read, &mut AllowStdIo::new(raw_stdout())).await?;
86 Ok(())
88 })
89}
90
91struct App<'a> {
92 path: Cow<'a, Path>,
93 args: Vec<Cow<'a, OsStr>>,
94}
95
96impl<'a, T: AsRef<OsStr> + ?Sized> From<&'a T> for App<'a> {
97 fn from(value: &'a T) -> Self {
98 Self {
99 path: Path::new(value).into(),
100 args: Vec::new(),
101 }
102 }
103}
104
105fn choose_terminal_apps(app: Option<&Path>) -> Vec<App<'_>> {
106 if let Some(app) = app {
108 return vec![app.into()];
109 }
110
111 let mut apps = Vec::new();
112
113 let env_set = |key| std::env::var_os(key).is_some_and(|x| !x.is_empty());
114
115 if env_set("TMUX") {
117 apps.push(App {
118 args: vec![OsStr::new("new-window").into()],
119 .."tmux".into()
120 });
121 }
122
123 if cfg!(unix) && env_set("DISPLAY") {
125 apps.push("x-terminal-emulator".into());
126 apps.push("xterm".into());
127 }
128
129 if cfg!(windows) {
131 apps.push("wt.exe".into());
132 apps.push("conhost.exe".into());
133 }
134
135 apps
136}
137
138pub fn launch_console(
142 app: Option<&Path>,
143 path: &Path,
144 launch_options: ConsoleLaunchOptions,
145) -> anyhow::Result<()> {
146 let apps = choose_terminal_apps(app);
147
148 for app in &apps {
149 let mut command = Command::new(app.path.as_ref());
150 command.args(&app.args);
151 add_argument_separator(&mut command, app.path.as_ref());
152 let mut child_builder = command
153 .arg(std::env::current_exe().context("could not determine current exe path")?)
154 .arg("--relay-console-path")
155 .arg(path);
156
157 if let Some(title) = &launch_options.window_title {
159 child_builder = child_builder.arg("--relay-console-title").arg(title);
160 }
161
162 let child = child_builder
163 .stdin(std::process::Stdio::null())
164 .stdout(std::process::Stdio::null())
165 .spawn();
166
167 match child {
168 Ok(mut child) => {
169 std::thread::Builder::new()
170 .name("console_waiter".into())
171 .spawn(move || {
172 let _ = child.wait();
173 })
174 .unwrap();
175
176 return Ok(());
177 }
178 Err(err) if err.kind() == std::io::ErrorKind::NotFound && apps.len() != 1 => continue,
179 Err(err) => Err(err)
180 .with_context(|| format!("failed to launch terminal {}", app.path.display()))?,
181 };
182 }
183
184 anyhow::bail!("could not find a terminal emulator");
185}
186
187fn add_argument_separator(command: &mut Command, app: &Path) {
190 if let Some(file_name) = app.file_name().and_then(|s| s.to_str()) {
191 let arg = match file_name {
192 "xterm" | "rxvt" | "urxvt" | "x-terminal-emulator" => "-e",
193 _ => "--",
194 };
195 command.arg(arg);
196 };
197}
198
199pub fn random_console_path() -> PathBuf {
201 #[cfg(windows)]
202 let mut path = PathBuf::from("\\\\.\\pipe");
203 #[cfg(unix)]
204 let mut path = std::env::temp_dir();
205
206 let mut random = [0; 16];
207 getrandom::fill(&mut random).expect("rng failure");
208 path.push(u128::from_ne_bytes(random).to_string());
209
210 path
211}
212
213pub struct Console {
218 #[cfg(windows)]
219 sys: windows::WindowsNamedPipeConsole,
220 #[cfg(unix)]
221 sys: unix::UnixSocketConsole,
222}
223
224impl Console {
225 pub fn new(
235 driver: impl Driver,
236 app: Option<&Path>,
237 launch_options: Option<ConsoleLaunchOptions>,
238 ) -> anyhow::Result<Self> {
239 let path = random_console_path();
240 let this = Self::new_from_path(driver, &path)?;
241 launch_console(app, &path, launch_options.unwrap_or_default())
242 .context("failed to launch console")?;
243 Ok(this)
244 }
245
246 fn new_from_path(driver: impl Driver, path: &Path) -> anyhow::Result<Self> {
247 #[cfg(windows)]
248 let sys = windows::WindowsNamedPipeConsole::new(Box::new(driver), path)
249 .context("failed to create console pipe")?;
250 #[cfg(unix)]
251 let sys = unix::UnixSocketConsole::new(Box::new(driver), path)
252 .context("failed to create console socket")?;
253 Ok(Console { sys })
254 }
255
256 pub async fn relay(&mut self, io: impl AsyncRead + AsyncWrite) -> anyhow::Result<()> {
258 let (pipe_recv, mut pipe_send) = { AsyncReadExt::split(self) };
259
260 let (socket_recv, mut socket_send) = io.split();
261
262 let task_a = async move {
263 let r = futures::io::copy(pipe_recv, &mut socket_send).await;
264 let _ = socket_send.close().await;
265 r
266 };
267 let task_b = async move {
268 let r = futures::io::copy(socket_recv, &mut pipe_send).await;
269 let _ = pipe_send.close().await;
270 r
271 };
272 futures::future::try_join(task_a, task_b).await?;
273 anyhow::Result::<_>::Ok(())
274 }
275}
276
277impl AsyncRead for Console {
278 fn poll_read(
279 self: Pin<&mut Self>,
280 cx: &mut Context<'_>,
281 buf: &mut [u8],
282 ) -> std::task::Poll<std::io::Result<usize>> {
283 Pin::new(&mut self.get_mut().sys).poll_read(cx, buf)
284 }
285}
286
287impl AsyncWrite for Console {
288 fn poll_write(
289 self: Pin<&mut Self>,
290 cx: &mut Context<'_>,
291 buf: &[u8],
292 ) -> std::task::Poll<std::io::Result<usize>> {
293 Pin::new(&mut self.get_mut().sys).poll_write(cx, buf)
294 }
295
296 fn poll_flush(
297 self: Pin<&mut Self>,
298 cx: &mut Context<'_>,
299 ) -> std::task::Poll<std::io::Result<()>> {
300 Pin::new(&mut self.get_mut().sys).poll_flush(cx)
301 }
302
303 fn poll_close(
304 self: Pin<&mut Self>,
305 cx: &mut Context<'_>,
306 ) -> std::task::Poll<std::io::Result<()>> {
307 Pin::new(&mut self.get_mut().sys).poll_close(cx)
308 }
309}