pipette/
execute.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Handler for the execute request.
5
6// UNSAFETY: Required for libc::chroot() and libc::chdir() in pre_exec on Linux.
7#![cfg_attr(target_os = "linux", expect(unsafe_code))]
8
9use futures::executor::block_on;
10use futures::io::AllowStdIo;
11#[cfg(target_os = "linux")]
12use std::os::unix::process::CommandExt;
13use std::process::Stdio;
14
15pub fn handle_execute(
16    mut request: pipette_protocol::ExecuteRequest,
17) -> anyhow::Result<pipette_protocol::ExecuteResponse> {
18    tracing::debug!(?request, "execute request");
19
20    let mut command = std::process::Command::new(&request.program);
21    command.args(&request.args);
22    if let Some(dir) = &request.current_dir {
23        command.current_dir(dir);
24    }
25
26    // If a chroot is requested, set up a pre_exec hook to chroot the child process.
27    if let Some(ref root) = request.chroot {
28        #[cfg(target_os = "linux")]
29        {
30            let root = std::ffi::CString::new(root.as_str())?;
31            // SAFETY: calling libc::chroot and libc::chdir in the child process
32            // before exec. These are async-signal-safe on Linux.
33            unsafe {
34                command.pre_exec(move || {
35                    if libc::chroot(root.as_ptr()) != 0 {
36                        return Err(std::io::Error::last_os_error());
37                    }
38                    if libc::chdir(c"/".as_ptr()) != 0 {
39                        return Err(std::io::Error::last_os_error());
40                    }
41                    Ok(())
42                });
43            }
44        }
45        #[cfg(not(target_os = "linux"))]
46        {
47            let _ = root;
48            anyhow::bail!("chroot is only supported on Linux");
49        }
50    }
51
52    if request.clear_env {
53        command.env_clear();
54    }
55    for pipette_protocol::EnvPair { name, value } in request.env {
56        if let Some(value) = value {
57            command.env(name, value);
58        } else {
59            command.env_remove(name);
60        }
61    }
62    if request.stdin.is_some() {
63        command.stdin(Stdio::piped());
64    } else {
65        command.stdin(Stdio::null());
66    }
67    if request.stdout.is_some() {
68        command.stdout(Stdio::piped());
69    } else {
70        command.stdout(Stdio::null());
71    }
72    if request.stderr.is_some() {
73        command.stderr(Stdio::piped());
74    } else {
75        command.stderr(Stdio::null());
76    }
77    let mut child = command.spawn()?;
78    let pid = child.id();
79    let (send, recv) = mesh::oneshot();
80
81    if let (Some(stdin_write), Some(stdin_read)) = (child.stdin.take(), request.stdin.take()) {
82        std::thread::spawn(move || {
83            let _ = block_on(futures::io::copy(
84                stdin_read,
85                &mut AllowStdIo::new(stdin_write),
86            ));
87        });
88    }
89    if let (Some(stdout_read), Some(mut stdout_write)) =
90        (child.stdout.take(), request.stdout.take())
91    {
92        std::thread::spawn(move || {
93            let _ = block_on(futures::io::copy(
94                AllowStdIo::new(stdout_read),
95                &mut stdout_write,
96            ));
97        });
98    }
99    if let (Some(stderr_read), Some(mut stderr_write)) =
100        (child.stderr.take(), request.stderr.take())
101    {
102        std::thread::spawn(move || {
103            let _ = block_on(futures::io::copy(
104                AllowStdIo::new(stderr_read),
105                &mut stderr_write,
106            ));
107        });
108    }
109
110    std::thread::spawn(move || {
111        let exit_status = child.wait().unwrap();
112        let status = convert_exit_status(exit_status);
113        tracing::debug!(pid, ?status, "process exited");
114        send.send(status);
115    });
116    Ok(pipette_protocol::ExecuteResponse { pid, result: recv })
117}
118
119fn convert_exit_status(exit_status: std::process::ExitStatus) -> pipette_protocol::ExitStatus {
120    if let Some(code) = exit_status.code() {
121        return pipette_protocol::ExitStatus::Normal(code);
122    }
123
124    #[cfg(unix)]
125    if let Some(signal) = std::os::unix::process::ExitStatusExt::signal(&exit_status) {
126        return pipette_protocol::ExitStatus::Signal(signal);
127    }
128
129    pipette_protocol::ExitStatus::Unknown
130}