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;
28
29#[derive(Default)]
30pub struct ConsoleLaunchOptions {
32 pub window_title: Option<String>,
34}
35
36async fn relay_stdio(
41 read: impl AsyncRead + Unpin,
42 mut write: impl AsyncWrite + Unpin + Send + 'static,
43 console_title: &str,
44) -> anyhow::Result<()> {
45 crossterm::terminal::enable_raw_mode().expect("failed to set raw console mode");
46 if let Err(err) = crossterm::execute!(
47 std::io::stdout(),
48 crossterm::terminal::SetTitle(console_title)
49 ) {
50 tracing::warn!("failed to set console title: {}", err);
51 }
52
53 std::thread::Builder::new()
54 .name("input_thread".into())
55 .spawn(move || {
56 block_on(futures::io::copy(
57 AllowStdIo::new(std::io::stdin()),
58 &mut write,
59 ))
60 })
61 .unwrap();
62
63 futures::io::copy(read, &mut AllowStdIo::new(raw_stdout())).await?;
64 Ok(())
66}
67
68pub fn relay_console(path: &Path, console_title: &str) -> anyhow::Result<()> {
71 block_with_io(async |driver| {
72 #[cfg(unix)]
73 let (read, write) = {
74 let pipe = pal_async::socket::PolledSocket::connect_unix(&driver, path)
75 .await
76 .context("failed to connect to console socket")?;
77 pipe.split()
78 };
79 #[cfg(windows)]
80 let (read, write) = {
81 let pipe = std::fs::OpenOptions::new()
82 .read(true)
83 .write(true)
84 .open(path)
85 .context("failed to connect to console pipe")?;
86 let pipe = pal_async::pipe::PolledPipe::new(&driver, pipe)
87 .context("failed to create polled pipe")?;
88 AsyncReadExt::split(pipe)
89 };
90
91 relay_stdio(read, write, console_title).await
92 })
93}
94
95#[cfg(windows)]
99pub fn relay_console_pipe(pipe: std::fs::File, console_title: &str) -> anyhow::Result<()> {
100 block_with_io(async |driver| {
101 let pipe = pal_async::pipe::PolledPipe::new(&driver, pipe)
102 .context("failed to create polled pipe")?;
103 let (read, write) = AsyncReadExt::split(pipe);
104 relay_stdio(read, write, console_title).await
105 })
106}
107
108struct App<'a> {
109 path: Cow<'a, Path>,
110 args: Vec<Cow<'a, OsStr>>,
111}
112
113impl<'a, T: AsRef<OsStr> + ?Sized> From<&'a T> for App<'a> {
114 fn from(value: &'a T) -> Self {
115 Self {
116 path: Path::new(value).into(),
117 args: Vec::new(),
118 }
119 }
120}
121
122fn choose_terminal_apps(app: Option<&Path>) -> Vec<App<'_>> {
123 if let Some(app) = app {
125 return vec![app.into()];
126 }
127
128 let mut apps = Vec::new();
129
130 let env_set = |key| std::env::var_os(key).is_some_and(|x| !x.is_empty());
131
132 if env_set("TMUX") {
134 apps.push(App {
135 args: vec![OsStr::new("new-window").into()],
136 .."tmux".into()
137 });
138 }
139
140 if cfg!(unix) && env_set("DISPLAY") {
142 apps.push("x-terminal-emulator".into());
143 apps.push("xterm".into());
144 }
145
146 if cfg!(windows) {
148 apps.push("wt.exe".into());
149 apps.push("conhost.exe".into());
150 }
151
152 apps
153}
154
155pub fn launch_console(
159 app: Option<&Path>,
160 path: &Path,
161 launch_options: ConsoleLaunchOptions,
162) -> anyhow::Result<()> {
163 let apps = choose_terminal_apps(app);
164
165 for app in &apps {
166 let mut command = Command::new(app.path.as_ref());
167 command.args(&app.args);
168 add_argument_separator(&mut command, app.path.as_ref());
169 let mut child_builder = command
170 .arg(std::env::current_exe().context("could not determine current exe path")?)
171 .arg("--relay-console-path")
172 .arg(path);
173
174 if let Some(title) = &launch_options.window_title {
176 child_builder = child_builder.arg("--relay-console-title").arg(title);
177 }
178
179 let child = child_builder
180 .stdin(std::process::Stdio::null())
181 .stdout(std::process::Stdio::null())
182 .spawn();
183
184 match child {
185 Ok(mut child) => {
186 std::thread::Builder::new()
187 .name("console_waiter".into())
188 .spawn(move || {
189 let _ = child.wait();
190 })
191 .unwrap();
192
193 return Ok(());
194 }
195 Err(err) if err.kind() == std::io::ErrorKind::NotFound && apps.len() != 1 => continue,
196 Err(err) => Err(err)
197 .with_context(|| format!("failed to launch terminal {}", app.path.display()))?,
198 };
199 }
200
201 anyhow::bail!("could not find a terminal emulator");
202}
203
204fn add_argument_separator(command: &mut Command, app: &Path) {
207 if let Some(file_name) = app.file_name().and_then(|s| s.to_str()) {
208 let arg = match file_name {
209 "xterm" | "rxvt" | "urxvt" | "x-terminal-emulator" => "-e",
210 _ => "--",
211 };
212 command.arg(arg);
213 };
214}
215
216pub fn random_console_path() -> PathBuf {
218 #[cfg(windows)]
219 let mut path = PathBuf::from("\\\\.\\pipe");
220 #[cfg(unix)]
221 let mut path = std::env::temp_dir();
222
223 let mut random = [0; 16];
224 getrandom::fill(&mut random).expect("rng failure");
225 path.push(u128::from_ne_bytes(random).to_string());
226
227 path
228}
229
230pub struct Console {
235 #[cfg(windows)]
236 sys: windows::WindowsNamedPipeConsole,
237 #[cfg(unix)]
238 sys: unix::UnixSocketConsole,
239}
240
241impl Console {
242 pub fn new(
252 driver: impl Driver,
253 app: Option<&Path>,
254 launch_options: Option<ConsoleLaunchOptions>,
255 ) -> anyhow::Result<Self> {
256 let path = random_console_path();
257 let this = Self::new_from_path(driver, &path)?;
258 launch_console(app, &path, launch_options.unwrap_or_default())
259 .context("failed to launch console")?;
260 Ok(this)
261 }
262
263 fn new_from_path(driver: impl Driver, path: &Path) -> anyhow::Result<Self> {
264 #[cfg(windows)]
265 let sys = windows::WindowsNamedPipeConsole::new(Box::new(driver), path)
266 .context("failed to create console pipe")?;
267 #[cfg(unix)]
268 let sys = unix::UnixSocketConsole::new(Box::new(driver), path)
269 .context("failed to create console socket")?;
270 Ok(Console { sys })
271 }
272
273 pub async fn relay(&mut self, io: impl AsyncRead + AsyncWrite) -> anyhow::Result<()> {
275 let (pipe_recv, mut pipe_send) = { AsyncReadExt::split(self) };
276
277 let (socket_recv, mut socket_send) = io.split();
278
279 let task_a = async move {
280 let r = futures::io::copy(pipe_recv, &mut socket_send).await;
281 let _ = socket_send.close().await;
282 r
283 };
284 let task_b = async move {
285 let r = futures::io::copy(socket_recv, &mut pipe_send).await;
286 let _ = pipe_send.close().await;
287 r
288 };
289 futures::future::try_join(task_a, task_b).await?;
290 anyhow::Result::<_>::Ok(())
291 }
292}
293
294impl AsyncRead for Console {
295 fn poll_read(
296 self: Pin<&mut Self>,
297 cx: &mut Context<'_>,
298 buf: &mut [u8],
299 ) -> std::task::Poll<std::io::Result<usize>> {
300 Pin::new(&mut self.get_mut().sys).poll_read(cx, buf)
301 }
302}
303
304impl AsyncWrite for Console {
305 fn poll_write(
306 self: Pin<&mut Self>,
307 cx: &mut Context<'_>,
308 buf: &[u8],
309 ) -> std::task::Poll<std::io::Result<usize>> {
310 Pin::new(&mut self.get_mut().sys).poll_write(cx, buf)
311 }
312
313 fn poll_flush(
314 self: Pin<&mut Self>,
315 cx: &mut Context<'_>,
316 ) -> std::task::Poll<std::io::Result<()>> {
317 Pin::new(&mut self.get_mut().sys).poll_flush(cx)
318 }
319
320 fn poll_close(
321 self: Pin<&mut Self>,
322 cx: &mut Context<'_>,
323 ) -> std::task::Poll<std::io::Result<()>> {
324 Pin::new(&mut self.get_mut().sys).poll_close(cx)
325 }
326}