Skip to main content

socket_to_stdio/
lib.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::Context as _;
6#[cfg(target_os = "fuchsia")]
7use fidl_fuchsia_io as fio;
8#[cfg(not(target_os = "fuchsia"))]
9use fidl_fuchsia_io_common as fio;
10use futures::future::Either;
11use futures::stream::StreamExt as _;
12use futures::{AsyncReadExt as _, AsyncWriteExt as _};
13use std::io::StdoutLock;
14use termion::raw::IntoRawMode as _;
15
16/// Abstracts stdout for `connect_socket_to_stdio`. Allows callers to determine if stdout should be
17/// exclusively owned for the duration of the call.
18pub enum Stdout<'a> {
19    /// Exclusive ownership of stdout (nothing else can write to stdout while this exists),
20    /// put into raw mode.
21    Raw(termion::raw::RawTerminal<StdoutLock<'a>>),
22    /// Shared ownership of stdout (output may be interleaved with output from other sources).
23    Buffered,
24}
25
26impl std::io::Write for Stdout<'_> {
27    fn flush(&mut self) -> Result<(), std::io::Error> {
28        match self {
29            Self::Raw(r) => r.flush(),
30            Self::Buffered => std::io::stdout().flush(),
31        }
32    }
33    fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
34        match self {
35            Self::Raw(r) => r.write(buf),
36            Self::Buffered => std::io::stdout().write(buf),
37        }
38    }
39}
40
41impl Stdout<'_> {
42    pub fn raw() -> anyhow::Result<Self> {
43        let stdout = std::io::stdout();
44
45        if !termion::is_tty(&stdout) {
46            anyhow::bail!("interactive mode does not support piping");
47        }
48
49        // Put the host terminal into raw mode, so input characters are not echoed, streams are not
50        // buffered and newlines are not changed.
51        let term_out =
52            stdout.lock().into_raw_mode().context("could not set raw mode on terminal")?;
53
54        Ok(Self::Raw(term_out))
55    }
56
57    pub fn buffered() -> Self {
58        Self::Buffered
59    }
60}
61
62/// Concurrently:
63///   1. locks stdin and copies the input to `socket`
64///   2. reads data from `socket` and writes it to `stdout`
65/// Finishes when the remote end of the socket closes (when (2) completes).
66#[cfg(target_os = "fuchsia")]
67pub async fn connect_socket_to_stdio(
68    socket: fidl::Socket,
69    stdout: Stdout<'_>,
70) -> anyhow::Result<()> {
71    connect_socket_to_stdio_impl(
72        fidl::AsyncSocket::from_socket(socket),
73        || std::io::stdin().lock(),
74        stdout,
75    )?
76    .await
77}
78
79/// Same as `connect_socket_to_stdio` but operates on an FDomain socket.
80pub async fn connect_fdomain_socket_to_stdio(
81    socket: fdomain_client::Socket,
82    stdout: Stdout<'_>,
83) -> anyhow::Result<()> {
84    connect_socket_to_stdio_impl(socket, || std::io::stdin().lock(), stdout)?.await
85}
86
87fn connect_socket_to_stdio_impl<R>(
88    socket: impl futures::AsyncRead + futures::AsyncWrite,
89    stdin: impl FnOnce() -> R + Send + 'static,
90    mut stdout: impl std::io::Write,
91) -> anyhow::Result<impl futures::Future<Output = anyhow::Result<()>>>
92where
93    R: std::io::Read,
94{
95    // Use a separate thread to read from stdin without blocking the executor.
96    let (stdin_send, mut stdin_recv) = futures::channel::mpsc::unbounded();
97    let _: std::thread::JoinHandle<_> = std::thread::Builder::new()
98        .name("connect_socket_to_stdio stdin thread".into())
99        .spawn(move || {
100            let mut stdin = stdin();
101            let mut buf = [0u8; fio::MAX_BUF as usize];
102            loop {
103                let bytes_read = stdin.read(&mut buf)?;
104                if bytes_read == 0 {
105                    return Ok::<(), anyhow::Error>(());
106                }
107                let () = stdin_send.unbounded_send(buf[..bytes_read].to_vec())?;
108            }
109        })
110        .context("spawning stdin thread")?;
111
112    let (mut socket_in, mut socket_out) = socket.split();
113
114    let stdin_to_socket = async move {
115        while let Some(stdin) = stdin_recv.next().await {
116            socket_out.write_all(&stdin).await.context("writing to socket")?;
117            socket_out.flush().await.context("flushing socket")?;
118        }
119        Ok::<(), anyhow::Error>(())
120    };
121
122    let socket_to_stdout = async move {
123        let mut buf = vec![0u8; fio::MAX_BUF as usize];
124        loop {
125            let bytes_read = socket_in.read(&mut buf).await.context("reading from socket")?;
126            if bytes_read == 0 {
127                break;
128            }
129            stdout.write_all(&buf[..bytes_read]).context("writing to stdout")?;
130            stdout.flush().context("flushing stdout")?;
131        }
132        Ok::<(), anyhow::Error>(())
133    };
134
135    Ok(async move {
136        futures::pin_mut!(stdin_to_socket);
137        futures::pin_mut!(socket_to_stdout);
138        Ok(match futures::future::select(stdin_to_socket, socket_to_stdout).await {
139            Either::Left((stdin_to_socket, socket_to_stdout)) => {
140                let () = stdin_to_socket?;
141                // Wait for output even after stdin closes. The remote may be responding to the
142                // final input, or the remote may not be reading from stdin at all (consider
143                // "bash -c $CMD").
144                let () = socket_to_stdout.await?;
145            }
146            Either::Right((socket_to_stdout, _)) => {
147                let () = socket_to_stdout?;
148                // No reason to wait for stdin because the socket is closed so writing stdin to it
149                // would fail.
150            }
151        })
152    })
153}
154
155#[cfg(all(test, target_os = "fuchsia"))]
156mod tests {
157    use super::*;
158
159    #[fuchsia::test]
160    async fn stdin_to_socket() {
161        let (socket, socket_remote) = fidl::Socket::create_stream();
162        let socket_remote = fidl::AsyncSocket::from_socket(socket_remote);
163
164        let connect_fut =
165            connect_socket_to_stdio_impl(socket_remote, || &b"test input"[..], vec![]).unwrap();
166
167        let (connect_res, bytes_from_socket) = futures::join!(connect_fut, async move {
168            let mut socket = fidl::AsyncSocket::from_socket(socket);
169            let mut out = vec![0u8; 100];
170            let bytes_read = socket.read(&mut out).await.unwrap();
171            drop(socket);
172            out.resize(bytes_read, 0);
173            out
174        });
175        let () = connect_res.unwrap();
176
177        assert_eq!(bytes_from_socket, &b"test input"[..]);
178    }
179
180    #[fuchsia::test]
181    async fn socket_to_stdout() {
182        let (socket, socket_remote) = fidl::Socket::create_stream();
183        assert_eq!(socket.write(&b"test input"[..]).unwrap(), 10);
184        drop(socket);
185        let mut stdout = vec![];
186        let (unblocker, block_until) = std::sync::mpsc::channel();
187
188        let socket_remote = fidl::AsyncSocket::from_socket(socket_remote);
189        let () = connect_socket_to_stdio_impl(
190            socket_remote,
191            move || {
192                let () = block_until.recv().unwrap();
193                &[][..]
194            },
195            &mut stdout,
196        )
197        .unwrap()
198        .await
199        .unwrap();
200
201        // let the stdin_to_socket thread finish before test cleanup
202        unblocker.send(()).unwrap();
203
204        assert_eq!(&stdout[..], &b"test input"[..]);
205    }
206}