pty/
pty.rs

1// Copyright 2019 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 _, Error};
6use fidl::endpoints::ServerEnd;
7use fidl_fuchsia_hardware_pty::{DeviceMarker, DeviceProxy, WindowSize};
8use fuchsia_component::client::connect_to_protocol;
9use fuchsia_trace as ftrace;
10use std::ffi::CStr;
11use std::fs::File;
12use std::os::fd::OwnedFd;
13use zx::{self as zx, HandleBased as _, ProcessInfo, ProcessInfoFlags};
14
15/// An object used for interacting with the shell.
16#[derive(Clone)]
17pub struct ServerPty {
18    // The server side pty connection.
19    proxy: DeviceProxy,
20}
21
22pub struct ShellProcess {
23    pub pty: ServerPty,
24
25    // The running shell process. This process will be closed when the
26    // Pty goes out of scope so there is no need to explicitly close it.
27    process: zx::Process,
28}
29
30impl ServerPty {
31    /// Creates a new instance of the Pty which must later be spawned.
32    pub fn new() -> Result<Self, Error> {
33        ftrace::duration!(c"pty", c"Pty:new");
34        let proxy =
35            connect_to_protocol::<DeviceMarker>().context("could not connect to pty service")?;
36        Ok(Self { proxy })
37    }
38
39    /// Spawns the Pty.
40    ///
41    /// If no command is provided the default /boot/bin/sh will be used.
42    ///
43    /// After calling this method the user must call resize to give the process a
44    /// valid window size before it will respond.
45    ///
46    /// The launched process will close when the Pty is dropped so you do not need to
47    /// explicitly close it.
48    pub async fn spawn(
49        self,
50        command: Option<&CStr>,
51        environ: Option<&[&CStr]>,
52    ) -> Result<ShellProcess, Error> {
53        let command = command.unwrap_or(&c"/boot/bin/sh");
54        self.spawn_with_argv(command, &[command], environ).await
55    }
56
57    pub async fn spawn_with_argv(
58        self,
59        command: &CStr,
60        argv: &[&CStr],
61        environ: Option<&[&CStr]>,
62    ) -> Result<ShellProcess, Error> {
63        ftrace::duration!(c"pty", c"Pty:spawn");
64        let client_pty = self.open_client_pty().await.context("unable to create client_pty")?;
65        let process = match fdio::spawn_etc(
66            &zx::Job::from_handle(zx::Handle::invalid()),
67            fdio::SpawnOptions::CLONE_ALL - fdio::SpawnOptions::CLONE_STDIO,
68            command,
69            argv,
70            environ,
71            &mut [fdio::SpawnAction::transfer_fd(client_pty, fdio::SpawnAction::USE_FOR_STDIO)],
72        ) {
73            Ok(process) => process,
74            Err((status, reason)) => {
75                return Err(status).context(format!("failed to spawn shell: {}", reason));
76            }
77        };
78
79        Ok(ShellProcess { pty: self, process })
80    }
81
82    /// Attempts to clone the server side of the file descriptor.
83    pub fn try_clone_fd(&self) -> Result<File, Error> {
84        use std::os::fd::AsRawFd as _;
85
86        let Self { proxy } = self;
87        let (client_end, server_end) = fidl::endpoints::create_endpoints();
88        let () = proxy.clone(server_end)?;
89        let file: File = fdio::create_fd(client_end.into())
90            .context("failed to create FD from server PTY")?
91            .into();
92        let fd = file.as_raw_fd();
93        let previous = {
94            let res = unsafe { libc::fcntl(fd, libc::F_GETFL) };
95            if res == -1 {
96                Err(std::io::Error::last_os_error()).context("failed to get file status flags")
97            } else {
98                Ok(res)
99            }
100        }?;
101        let new = previous | libc::O_NONBLOCK;
102        if new != previous {
103            let res = unsafe { libc::fcntl(fd, libc::F_SETFL, new) };
104            let () = if res == -1 {
105                Err(std::io::Error::last_os_error()).context("failed to set file status flags")
106            } else {
107                Ok(())
108            }?;
109        }
110        Ok(file)
111    }
112
113    /// Sends a message to the shell that the window has been resized.
114    pub async fn resize(&self, window_size: WindowSize) -> Result<(), Error> {
115        ftrace::duration!(c"pty", c"Pty:resize");
116        let Self { proxy } = self;
117        let () = proxy
118            .set_window_size(&window_size)
119            .await
120            .map(zx::Status::ok)
121            .context("unable to call resize window")?
122            .context("failed to resize window")?;
123        Ok(())
124    }
125
126    /// Creates a File which is suitable to use as the client side of the Pty.
127    async fn open_client_pty(&self) -> Result<OwnedFd, Error> {
128        ftrace::duration!(c"pty", c"Pty:open_client_pty");
129        let (client_end, server_end) = fidl::endpoints::create_endpoints();
130        let () = self.open_client(server_end).await.context("failed to open client")?;
131        let fd =
132            fdio::create_fd(client_end.into()).context("failed to create FD from client PTY")?;
133        Ok(fd)
134    }
135
136    /// Open a client Pty device. `server_end` should be a handle
137    /// to one endpoint of a channel that (on success) will become an open
138    /// connection to the newly created device.
139    pub async fn open_client(&self, server_end: ServerEnd<DeviceMarker>) -> Result<(), Error> {
140        let Self { proxy } = self;
141        ftrace::duration!(c"pty", c"Pty:open_client");
142
143        let () = proxy
144            .open_client(0, server_end)
145            .await
146            .map(zx::Status::ok)
147            .context("failed to interact with PTY device")?
148            .context("failed to attach PTY to channel")?;
149
150        Ok(())
151    }
152}
153
154impl ShellProcess {
155    /// Returns the shell process info, if available.
156    pub fn process_info(&self) -> Result<ProcessInfo, Error> {
157        let Self { pty: _, process } = self;
158        process.info().context("failed to get process info")
159    }
160
161    /// Checks that the shell process has been started and has not exited.
162    pub fn is_running(&self) -> bool {
163        self.process_info()
164            .map(|info| {
165                let flags = ProcessInfoFlags::from_bits(info.flags).unwrap();
166                flags.contains(zx::ProcessInfoFlags::STARTED)
167                    && !flags.contains(ProcessInfoFlags::EXITED)
168            })
169            .unwrap_or_default()
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use fuchsia_async as fasync;
177    use futures::io::AsyncWriteExt as _;
178    use std::os::unix::io::AsRawFd as _;
179    use zx::AsHandleRef as _;
180
181    #[fasync::run_singlethreaded(test)]
182    async fn can_create_pty() -> Result<(), Error> {
183        let _ = ServerPty::new()?;
184        Ok(())
185    }
186
187    #[fasync::run_singlethreaded(test)]
188    async fn can_open_client_pty() -> Result<(), Error> {
189        let server_pty = ServerPty::new()?;
190        let client_pty = server_pty.open_client_pty().await?;
191        assert!(client_pty.as_raw_fd() > 0);
192
193        Ok(())
194    }
195
196    #[fasync::run_singlethreaded(test)]
197    async fn can_spawn_shell_process() -> Result<(), Error> {
198        let server_pty = ServerPty::new()?;
199        let cmd = c"/pkg/bin/sh";
200        let process = server_pty.spawn_with_argv(&cmd, &[cmd], None).await?;
201
202        let mut started = false;
203        if let Ok(info) = process.process_info() {
204            started = ProcessInfoFlags::from_bits(info.flags)
205                .unwrap()
206                .contains(zx::ProcessInfoFlags::STARTED);
207        }
208
209        assert_eq!(started, true);
210
211        Ok(())
212    }
213
214    #[fasync::run_singlethreaded(test)]
215    async fn shell_process_is_spawned() -> Result<(), Error> {
216        let process = spawn_pty().await?;
217
218        let info = process.process_info().unwrap();
219        assert!(
220            ProcessInfoFlags::from_bits(info.flags)
221                .unwrap()
222                .contains(zx::ProcessInfoFlags::STARTED)
223        );
224
225        Ok(())
226    }
227
228    #[fasync::run_singlethreaded(test)]
229    async fn spawned_shell_process_is_running() -> Result<(), Error> {
230        let process = spawn_pty().await?;
231
232        assert!(process.is_running());
233        Ok(())
234    }
235
236    #[fasync::run_singlethreaded(test)]
237    async fn exited_shell_process_is_not_running() -> Result<(), Error> {
238        let window_size = WindowSize { width: 300 as u32, height: 300 as u32 };
239        let pty = ServerPty::new().unwrap();
240
241        // While argv[0] is usually the executable path, this particular program expects it to be
242        // an integer which is then parsed and returned as the status code.
243        let process = pty.spawn_with_argv(&c"/pkg/bin/exit_with_code_util", &[c"42"], None).await?;
244        let () = process.pty.resize(window_size).await?;
245
246        // Since these tests don't seem to timeout automatically, we must
247        // specify a deadline and cannot simply rely on fasync::OnSignals.
248        process
249            .process
250            .wait_handle(
251                zx::Signals::PROCESS_TERMINATED,
252                zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(60)),
253            )
254            .expect("shell process did not exit in time");
255
256        assert!(!process.is_running());
257        Ok(())
258    }
259
260    #[fasync::run_singlethreaded(test)]
261    async fn can_write_to_shell() -> Result<(), Error> {
262        let process = spawn_pty().await?;
263        // EventedFd::new() is unsafe because it can't guarantee the lifetime of
264        // the file descriptor passed to it exceeds the lifetime of the EventedFd.
265        // Since we're cloning the file when passing it in, the EventedFd
266        // effectively owns that file descriptor and thus controls it's lifetime.
267        let mut evented_fd = unsafe { fasync::net::EventedFd::new(process.pty.try_clone_fd()?)? };
268
269        evented_fd.write_all("a".as_bytes()).await?;
270
271        Ok(())
272    }
273
274    #[ignore] // TODO(63868): until we figure out why this test is flaking.
275    #[fasync::run_singlethreaded(test)]
276    async fn shell_process_is_not_running_after_writing_exit() -> Result<(), Error> {
277        let process = spawn_pty().await?;
278        // EventedFd::new() is unsafe because it can't guarantee the lifetime of
279        // the file descriptor passed to it exceeds the lifetime of the EventedFd.
280        // Since we're cloning the file when passing it in, the EventedFd
281        // effectively owns that file descriptor and thus controls it's lifetime.
282        let mut evented_fd = unsafe { fasync::net::EventedFd::new(process.pty.try_clone_fd()?)? };
283
284        evented_fd.write_all("exit\n".as_bytes()).await?;
285
286        // Since these tests don't seem to timeout automatically, we must
287        // specify a deadline and cannot simply rely on fasync::OnSignals.
288        process
289            .process
290            .wait_handle(
291                zx::Signals::PROCESS_TERMINATED,
292                zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(60)),
293            )
294            .expect("shell process did not exit in time");
295
296        assert!(!process.is_running());
297
298        Ok(())
299    }
300
301    #[fasync::run_singlethreaded(test)]
302    async fn can_resize_window() -> Result<(), Error> {
303        let process = spawn_pty().await?;
304        let () = process.pty.resize(WindowSize { width: 400, height: 400 }).await?;
305        Ok(())
306    }
307
308    async fn spawn_pty() -> Result<ShellProcess, Error> {
309        let window_size = WindowSize { width: 300 as u32, height: 300 as u32 };
310        let pty = ServerPty::new()?;
311        let process = pty.spawn(Some(&c"/pkg/bin/sh"), None).await.context("failed to spawn")?;
312        let () = process.pty.resize(window_size).await?;
313        Ok(process)
314    }
315}