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