1use crate::{
4 async_fd::{self, Fd},
5 fs_util::TempFile,
6 listener,
7 mutex::lock,
8};
9use anyhow::{Context as _, Result};
10use libc::{TIOCSWINSZ, winsize};
11use nix::{
12 cmsg_space,
13 errno::Errno,
14 fcntl::{self, FcntlArg, OFlag},
15 sys::{
16 socket::{self, ControlMessageOwned, MsgFlags},
17 termios::{self, OutputFlags, SetArg, SpecialCharacterIndices},
18 },
19};
20use std::{
21 io::{self, IoSliceMut, Read, Write},
22 os::{
23 fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd},
24 unix::io::RawFd,
25 },
26 path::{Path, PathBuf},
27 pin::Pin,
28 sync::{Arc, Mutex, Weak},
29 task::{Context, Poll, ready},
30};
31use tokio::{
32 fs,
33 io::{AsyncRead, AsyncWrite, AsyncWriteExt, Interest, ReadBuf, unix::AsyncFd},
34 net::{UnixListener, UnixStream},
35 select,
36 sync::mpsc::{self, Receiver, Sender},
37 task,
38};
39use tracing::{Instrument, debug, debug_span, error};
40
41#[derive(Debug)]
44pub(crate) struct Terminal {
45 path: TempFile,
47 connected_rx: Receiver<OwnedFd>,
48 resizer: TerminalResizer,
49}
50
51impl Terminal {
52 pub(crate) fn path(&self) -> &Path {
54 self.path.path()
55 }
56
57 pub(crate) fn resizer(&self) -> TerminalResizer {
60 self.resizer.clone()
61 }
62
63 fn tty(&self) -> Option<Arc<TerminalFd>> {
65 match &*lock(&self.resizer.0) {
66 Tty::Pending(_) => None,
67 Tty::Connected(tty) => tty.upgrade(),
68 }
69 }
70}
71
72#[derive(Debug)]
75enum Tty {
76 Pending(Option<(u16, u16)>),
79
80 Connected(Weak<TerminalFd>),
83}
84
85#[derive(Clone, Debug)]
87pub(crate) struct TerminalResizer(Arc<Mutex<Tty>>);
88
89impl Default for TerminalResizer {
90 fn default() -> Self {
91 Self(Arc::new(Mutex::new(Tty::Pending(None))))
92 }
93}
94
95impl TerminalResizer {
96 pub(crate) fn resize(&self, width: u16, height: u16) -> Result<()> {
101 debug!("Resizing terminal to width {width} and height {height}");
102 let mut tty = lock(&self.0);
103 match &*tty {
104 Tty::Pending(_) => {
105 debug!("Terminal not connected yet, resizing it once it is");
106 *tty = Tty::Pending(Some((width, height)));
107 Ok(())
108 }
109 Tty::Connected(fd) => {
110 let fd = fd.upgrade().context("terminal closed")?;
111 Self::set_size(&fd, width, height)
112 }
113 }
114 }
115
116 fn connect(&self, fd: &Arc<TerminalFd>) {
119 let mut tty = lock(&self.0);
120 if let Tty::Pending(Some((width, height))) = *tty {
121 debug!("Resizing terminal to width {width} and height {height} requested before");
122 if let Err(e) = Self::set_size(fd, width, height) {
124 debug!("Unable to resize the terminal: {e:#}");
125 }
126 }
127 *tty = Tty::Connected(Arc::downgrade(fd));
128 }
129
130 #[expect(unsafe_code, reason = "the TIOCSWINSZ ioctl")]
131 fn set_size(fd: &TerminalFd, width: u16, height: u16) -> Result<()> {
132 let ws = winsize {
133 ws_row: height,
134 ws_col: width,
135 ws_xpixel: 0,
136 ws_ypixel: 0,
137 };
138 match unsafe { libc::ioctl(fd.as_raw_fd(), TIOCSWINSZ, &ws) } {
142 0 => Ok(()),
143 _ => Err(io::Error::last_os_error().into()),
144 }
145 }
146
147 #[cfg(test)]
149 pub(crate) fn pending(&self) -> Option<(u16, u16)> {
150 match &*lock(&self.0) {
151 Tty::Pending(size) => *size,
152 Tty::Connected(_) => None,
153 }
154 }
155}
156
157#[derive(Debug)]
158struct Config {
159 path: PathBuf,
160 connected_tx: Sender<OwnedFd>,
161}
162
163impl Terminal {
164 pub(crate) fn new() -> Result<Self> {
166 debug!("Creating new terminal");
167 let temp_file = TempFile::new(None, "conmon-term-", ".sock");
173 let path = temp_file.path();
174
175 debug!("Listening terminal socket on {}", path.display());
178 let listener = listener::bind_long_path(path, |path| {
179 let fd = listener::listen_private(&path)?;
180 UnixListener::from_std(fd.into())
181 })
182 .context("bind terminal socket")?;
183
184 let (connected_tx, connected_rx) = mpsc::channel(1);
186 let config = Config {
187 path: path.to_path_buf(),
188 connected_tx,
189 };
190
191 task::spawn(
192 async move {
193 if let Err(e) = Self::listen(listener, config).await {
194 error!("Unable to listen on terminal: {e:#}");
195 }
196 }
197 .instrument(debug_span!("listen"))
198 .in_current_span(),
199 );
200
201 Ok(Self {
202 path: temp_file,
203 connected_rx,
204 resizer: TerminalResizer::default(),
205 })
206 }
207
208 pub(crate) async fn connect(&mut self) -> Result<TerminalIo> {
215 debug!("Waiting for terminal socket connection");
216 let fd = self
217 .connected_rx
218 .recv()
219 .await
220 .context("receive connected channel")?;
221 let fd = Arc::new(TerminalFd::new(fd)?);
222
223 debug!("Changing terminal settings");
224 let mut term = termios::tcgetattr(&fd)?;
225 term.output_flags |= OutputFlags::ONLCR;
226 termios::tcsetattr(&fd, SetArg::TCSANOW, &term)?;
227 self.resizer.connect(&fd);
228 Ok(TerminalIo(fd))
229 }
230
231 pub(crate) fn eof_char(&self) -> u8 {
234 const DEFAULT: u8 = 0x04;
235 let Some(tty) = self.tty() else {
236 return DEFAULT;
237 };
238 match termios::tcgetattr(&*tty) {
239 Ok(term) => term.control_chars[SpecialCharacterIndices::VEOF as usize],
240 Err(e) => {
241 debug!("Unable to get the terminal settings: {e}");
242 DEFAULT
243 }
244 }
245 }
246
247 pub(crate) fn resize(&self, width: u16, height: u16) -> Result<()> {
249 self.resizer.resize(width, height)
250 }
251
252 async fn listen(listener: UnixListener, config: Config) -> Result<()> {
257 let dropped = config.connected_tx.clone();
258 select! {
259 res = Self::accept(listener, config) => res,
260 () = dropped.closed() => {
261 debug!("Terminal dropped before receiving a descriptor, stopping listener");
262 Ok(())
263 }
264 }
265 }
266
267 async fn accept(listener: UnixListener, config: Config) -> Result<()> {
268 let stream = listener.accept().await?.0;
269 debug!("Got terminal socket stream: {stream:?}");
270
271 Self::handle_fd_receive(stream, config).await
272 }
273
274 async fn handle_fd_receive(mut stream: UnixStream, config: Config) -> Result<()> {
275 let fd = stream
276 .async_io(Interest::READABLE, || Self::recv_fd(stream.as_fd()))
277 .await
278 .context("receive terminal descriptor")?;
279
280 let sent = async {
283 let fd = fd.context("got no file descriptor")?;
284 debug!("Received terminal file descriptor");
285 config
286 .connected_tx
287 .send(fd)
288 .await
289 .context("send connected channel")
290 }
291 .await;
292
293 let path = &config.path;
296 debug!("Removing socket path {}", path.display());
297 if let Err(e) = fs::remove_file(path).await {
298 debug!("Unable to remove terminal socket: {e}");
299 }
300 debug!("Shutting down receiver stream");
301 if let Err(e) = stream.shutdown().await {
302 debug!("Unable to shut down terminal socket stream: {e}");
303 }
304
305 sent?;
306 debug!("Shutting down listener task");
307 Ok(())
308 }
309
310 #[expect(unsafe_code, reason = "taking received descriptors")]
314 fn recv_fd(socket: BorrowedFd) -> io::Result<Option<OwnedFd>> {
315 let mut cmsg_buffer = cmsg_space!([RawFd; 253]);
321 let mut iov = [IoSliceMut::new(&mut [])];
322 let msg = socket::recvmsg::<()>(
323 socket.as_raw_fd(),
324 &mut iov,
325 Some(&mut cmsg_buffer),
326 MsgFlags::MSG_CMSG_CLOEXEC,
327 )?;
328 let mut fds = vec![];
329 for cmsg in msg.cmsgs()? {
330 if let ControlMessageOwned::ScmRights(raw_fds) = cmsg {
331 fds.extend(
335 raw_fds
336 .into_iter()
337 .map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }),
338 );
339 }
340 }
341 Ok(fds.into_iter().next())
342 }
343}
344
345#[derive(Debug)]
346struct TerminalFd(AsyncFd<Fd<std::fs::File>>);
347
348impl TerminalFd {
349 fn new(fd: OwnedFd) -> io::Result<Self> {
350 let flags = fcntl::fcntl(&fd, FcntlArg::F_GETFL)?;
351 let flags = OFlag::from_bits_truncate(flags) | OFlag::O_NONBLOCK;
352 fcntl::fcntl(&fd, FcntlArg::F_SETFL(flags))?;
353 let file = std::fs::File::from(fd);
354 Ok(Self(async_fd::register(
355 file,
356 Interest::READABLE | Interest::WRITABLE,
357 )?))
358 }
359}
360
361impl AsRawFd for TerminalFd {
362 fn as_raw_fd(&self) -> RawFd {
363 self.0.as_raw_fd()
364 }
365}
366
367impl AsFd for TerminalFd {
368 fn as_fd(&self) -> BorrowedFd<'_> {
369 self.0.as_fd()
370 }
371}
372
373#[derive(Clone, Debug)]
375pub(crate) struct TerminalIo(Arc<TerminalFd>);
376
377impl TerminalIo {
378 #[expect(unsafe_code, reason = "the TIOCGPTPEER ioctl")]
381 pub(crate) fn open_peer(&self) -> nix::Result<OwnedFd> {
382 let fd = unsafe {
387 libc::ioctl(
388 self.0.as_raw_fd(),
389 libc::TIOCGPTPEER,
390 libc::O_RDWR | libc::O_NOCTTY | libc::O_CLOEXEC,
391 )
392 };
393 let fd = Errno::result(fd)?;
394 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
397 }
398}
399
400impl AsyncRead for TerminalIo {
401 fn poll_read(
402 self: Pin<&mut Self>,
403 cx: &mut Context,
404 buf: &mut ReadBuf,
405 ) -> Poll<io::Result<()>> {
406 loop {
407 let mut guard = ready!(self.0.0.poll_read_ready(cx))?;
408 match guard.try_io(|inner| (&**inner.get_ref()).read(buf.initialize_unfilled())) {
409 Ok(result) => {
410 buf.advance(result?);
411 break Poll::Ready(Ok(()));
412 }
413 Err(_would_block) => {}
414 }
415 }
416 }
417}
418
419impl AsyncWrite for TerminalIo {
420 fn poll_write(self: Pin<&mut Self>, cx: &mut Context, buf: &[u8]) -> Poll<io::Result<usize>> {
421 loop {
422 let mut guard = ready!(self.0.0.poll_write_ready(cx))?;
423 match guard.try_io(|inner| (&**inner.get_ref()).write(buf)) {
424 Ok(result) => break Poll::Ready(result),
425 Err(_would_block) => {}
426 }
427 }
428 }
429
430 fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<io::Result<()>> {
431 Poll::Ready(Ok(()))
432 }
433
434 fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context) -> Poll<io::Result<()>> {
435 Poll::Ready(Ok(()))
436 }
437}
438
439#[cfg(test)]
440pub(crate) mod tests;