1use 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#[derive(Clone)]
17pub struct ServerPty {
18 proxy: DeviceProxy,
20}
21
22pub struct ShellProcess {
23 pub pty: ServerPty,
24
25 process: zx::Process,
28}
29
30impl ServerPty {
31 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 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 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 #[cfg(fuchsia_api_level_at_least = "26")]
89 let () = proxy.clone(server_end)?;
90 #[cfg(not(fuchsia_api_level_at_least = "26"))]
91 let () = proxy.clone2(server_end)?;
92 let file: File = fdio::create_fd(client_end.into())
93 .context("failed to create FD from server PTY")?
94 .into();
95 let fd = file.as_raw_fd();
96 let previous = {
97 let res = unsafe { libc::fcntl(fd, libc::F_GETFL) };
98 if res == -1 {
99 Err(std::io::Error::last_os_error()).context("failed to get file status flags")
100 } else {
101 Ok(res)
102 }
103 }?;
104 let new = previous | libc::O_NONBLOCK;
105 if new != previous {
106 let res = unsafe { libc::fcntl(fd, libc::F_SETFL, new) };
107 let () = if res == -1 {
108 Err(std::io::Error::last_os_error()).context("failed to set file status flags")
109 } else {
110 Ok(())
111 }?;
112 }
113 Ok(file)
114 }
115
116 pub async fn resize(&self, window_size: WindowSize) -> Result<(), Error> {
118 ftrace::duration!(c"pty", c"Pty:resize");
119 let Self { proxy } = self;
120 let () = proxy
121 .set_window_size(&window_size)
122 .await
123 .map(zx::Status::ok)
124 .context("unable to call resize window")?
125 .context("failed to resize window")?;
126 Ok(())
127 }
128
129 async fn open_client_pty(&self) -> Result<OwnedFd, Error> {
131 ftrace::duration!(c"pty", c"Pty:open_client_pty");
132 let (client_end, server_end) = fidl::endpoints::create_endpoints();
133 let () = self.open_client(server_end).await.context("failed to open client")?;
134 let fd =
135 fdio::create_fd(client_end.into()).context("failed to create FD from client PTY")?;
136 Ok(fd)
137 }
138
139 pub async fn open_client(&self, server_end: ServerEnd<DeviceMarker>) -> Result<(), Error> {
143 let Self { proxy } = self;
144 ftrace::duration!(c"pty", c"Pty:open_client");
145
146 let () = proxy
147 .open_client(0, server_end)
148 .await
149 .map(zx::Status::ok)
150 .context("failed to interact with PTY device")?
151 .context("failed to attach PTY to channel")?;
152
153 Ok(())
154 }
155}
156
157impl ShellProcess {
158 pub fn process_info(&self) -> Result<ProcessInfo, Error> {
160 let Self { pty: _, process } = self;
161 process.info().context("failed to get process info")
162 }
163
164 pub fn is_running(&self) -> bool {
166 self.process_info()
167 .map(|info| {
168 let flags = ProcessInfoFlags::from_bits(info.flags).unwrap();
169 flags.contains(zx::ProcessInfoFlags::STARTED)
170 && !flags.contains(ProcessInfoFlags::EXITED)
171 })
172 .unwrap_or_default()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use fuchsia_async as fasync;
180 use futures::io::AsyncWriteExt as _;
181 use std::os::unix::io::AsRawFd as _;
182 use zx::AsHandleRef as _;
183
184 #[fasync::run_singlethreaded(test)]
185 async fn can_create_pty() -> Result<(), Error> {
186 let _ = ServerPty::new()?;
187 Ok(())
188 }
189
190 #[fasync::run_singlethreaded(test)]
191 async fn can_open_client_pty() -> Result<(), Error> {
192 let server_pty = ServerPty::new()?;
193 let client_pty = server_pty.open_client_pty().await?;
194 assert!(client_pty.as_raw_fd() > 0);
195
196 Ok(())
197 }
198
199 #[fasync::run_singlethreaded(test)]
200 async fn can_spawn_shell_process() -> Result<(), Error> {
201 let server_pty = ServerPty::new()?;
202 let cmd = c"/pkg/bin/sh";
203 let process = server_pty.spawn_with_argv(&cmd, &[cmd], None).await?;
204
205 let mut started = false;
206 if let Ok(info) = process.process_info() {
207 started = ProcessInfoFlags::from_bits(info.flags)
208 .unwrap()
209 .contains(zx::ProcessInfoFlags::STARTED);
210 }
211
212 assert_eq!(started, true);
213
214 Ok(())
215 }
216
217 #[fasync::run_singlethreaded(test)]
218 async fn shell_process_is_spawned() -> Result<(), Error> {
219 let process = spawn_pty().await?;
220
221 let info = process.process_info().unwrap();
222 assert!(ProcessInfoFlags::from_bits(info.flags)
223 .unwrap()
224 .contains(zx::ProcessInfoFlags::STARTED));
225
226 Ok(())
227 }
228
229 #[fasync::run_singlethreaded(test)]
230 async fn spawned_shell_process_is_running() -> Result<(), Error> {
231 let process = spawn_pty().await?;
232
233 assert!(process.is_running());
234 Ok(())
235 }
236
237 #[fasync::run_singlethreaded(test)]
238 async fn exited_shell_process_is_not_running() -> Result<(), Error> {
239 let window_size = WindowSize { width: 300 as u32, height: 300 as u32 };
240 let pty = ServerPty::new().unwrap();
241
242 let process = pty.spawn_with_argv(&c"/pkg/bin/exit_with_code_util", &[c"42"], None).await?;
245 let () = process.pty.resize(window_size).await?;
246
247 process
250 .process
251 .wait_handle(
252 zx::Signals::PROCESS_TERMINATED,
253 zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(60)),
254 )
255 .expect("shell process did not exit in time");
256
257 assert!(!process.is_running());
258 Ok(())
259 }
260
261 #[fasync::run_singlethreaded(test)]
262 async fn can_write_to_shell() -> Result<(), Error> {
263 let process = spawn_pty().await?;
264 let mut evented_fd = unsafe { fasync::net::EventedFd::new(process.pty.try_clone_fd()?)? };
269
270 evented_fd.write_all("a".as_bytes()).await?;
271
272 Ok(())
273 }
274
275 #[ignore] #[fasync::run_singlethreaded(test)]
277 async fn shell_process_is_not_running_after_writing_exit() -> Result<(), Error> {
278 let process = spawn_pty().await?;
279 let mut evented_fd = unsafe { fasync::net::EventedFd::new(process.pty.try_clone_fd()?)? };
284
285 evented_fd.write_all("exit\n".as_bytes()).await?;
286
287 process
290 .process
291 .wait_handle(
292 zx::Signals::PROCESS_TERMINATED,
293 zx::MonotonicInstant::after(zx::MonotonicDuration::from_seconds(60)),
294 )
295 .expect("shell process did not exit in time");
296
297 assert!(!process.is_running());
298
299 Ok(())
300 }
301
302 #[fasync::run_singlethreaded(test)]
303 async fn can_resize_window() -> Result<(), Error> {
304 let process = spawn_pty().await?;
305 let () = process.pty.resize(WindowSize { width: 400, height: 400 }).await?;
306 Ok(())
307 }
308
309 async fn spawn_pty() -> Result<ShellProcess, Error> {
310 let window_size = WindowSize { width: 300 as u32, height: 300 as u32 };
311 let pty = ServerPty::new()?;
312 let process = pty.spawn(Some(&c"/pkg/bin/sh"), None).await.context("failed to spawn")?;
313 let () = process.pty.resize(window_size).await?;
314 Ok(process)
315 }
316}