Skip to main content

libcontainer/
notify_socket.rs

1use std::env;
2use std::io::prelude::*;
3use std::os::fd::FromRawFd;
4use std::os::unix::io::AsRawFd;
5use std::os::unix::net::{UnixListener, UnixStream};
6use std::path::{Path, PathBuf};
7
8use nix::unistd::{self, close};
9
10pub const NOTIFY_FILE: &str = "notify.sock";
11
12#[derive(Debug, thiserror::Error)]
13pub enum NotifyListenerError {
14    #[error("failed to chdir {path} while creating notify socket: {source}")]
15    Chdir { source: nix::Error, path: PathBuf },
16    #[error("invalid path: {0}")]
17    InvalidPath(PathBuf),
18    #[error("failed to bind notify socket: {name}")]
19    Bind {
20        source: std::io::Error,
21        name: String,
22    },
23    #[error("failed to connect to notify socket: {name}")]
24    Connect {
25        source: std::io::Error,
26        name: String,
27    },
28    #[error("failed to get cwd")]
29    GetCwd(#[source] std::io::Error),
30    #[error("failed to accept notify listener")]
31    Accept(#[source] std::io::Error),
32    #[error("failed to close notify listener")]
33    Close(#[source] nix::errno::Errno),
34    #[error("failed to read notify listener")]
35    Read(#[source] std::io::Error),
36    #[error("failed to send start container")]
37    SendStartContainer(#[source] std::io::Error),
38}
39
40type Result<T> = std::result::Result<T, NotifyListenerError>;
41
42pub struct NotifyListener {
43    socket: UnixListener,
44}
45
46impl NotifyListener {
47    pub fn new(socket_path: &Path) -> Result<Self> {
48        tracing::debug!(?socket_path, "create notify listener");
49        // Unix domain socket has a maximum length of 108, different from
50        // normal path length of 255. Due to how docker create the path name
51        // to the container working directory, there is a high chance that
52        // the full absolute path is over the limit. To work around this
53        // limitation, we chdir first into the workdir where the socket is,
54        // and chdir back after the socket is created.
55        let workdir = socket_path
56            .parent()
57            .ok_or_else(|| NotifyListenerError::InvalidPath(socket_path.to_owned()))?;
58        let socket_name = socket_path
59            .file_name()
60            .ok_or_else(|| NotifyListenerError::InvalidPath(socket_path.to_owned()))?;
61        let cwd = env::current_dir().map_err(NotifyListenerError::GetCwd)?;
62        tracing::debug!(?cwd, "the cwd to create the notify socket");
63        unistd::chdir(workdir).map_err(|e| NotifyListenerError::Chdir {
64            source: e,
65            path: workdir.to_owned(),
66        })?;
67        let stream = UnixListener::bind(socket_name).map_err(|e| NotifyListenerError::Bind {
68            source: e,
69            // ok to unwrap here as OsStr should always be utf-8 compatible
70            name: socket_name.to_str().unwrap().to_owned(),
71        })?;
72        unistd::chdir(&cwd).map_err(|e| NotifyListenerError::Chdir {
73            source: e,
74            path: cwd,
75        })?;
76
77        Ok(Self { socket: stream })
78    }
79
80    pub fn wait_for_container_start(&self) -> Result<()> {
81        match self.socket.accept() {
82            Ok((mut socket, _)) => {
83                let mut response = String::new();
84                socket
85                    .read_to_string(&mut response)
86                    .map_err(NotifyListenerError::Read)?;
87                tracing::debug!("received: {}", response);
88            }
89            Err(e) => Err(NotifyListenerError::Accept(e))?,
90        }
91
92        Ok(())
93    }
94
95    pub fn close(&self) -> Result<()> {
96        close(self.socket.as_raw_fd()).map_err(NotifyListenerError::Close)?;
97        Ok(())
98    }
99}
100
101impl Clone for NotifyListener {
102    fn clone(&self) -> Self {
103        let fd = self.socket.as_raw_fd();
104        // This is safe because we just duplicate a valid fd. Theoretically, to
105        // truly clone a unix listener, we have to use dup(2) to duplicate the
106        // fd, and then use from_raw_fd to create a new UnixListener. However,
107        // for our purposes, fd is just an integer to pass around for the same
108        // socket. Our main usage is to pass the notify_listener across process
109        // boundary. Since fd tables are cloned during clone/fork calls, this
110        // should be safe to use, as long as we be careful with not closing the
111        // same fd in different places. If we observe an issue, we will switch
112        // to `dup`.
113        let socket = unsafe { UnixListener::from_raw_fd(fd) };
114        Self { socket }
115    }
116}
117
118pub struct NotifySocket {
119    path: PathBuf,
120}
121
122impl NotifySocket {
123    pub fn new<P: Into<PathBuf>>(socket_path: P) -> Self {
124        Self {
125            path: socket_path.into(),
126        }
127    }
128
129    pub fn notify_container_start(&mut self) -> Result<()> {
130        tracing::debug!("notify container start");
131        let cwd = env::current_dir().map_err(NotifyListenerError::GetCwd)?;
132        let workdir = self
133            .path
134            .parent()
135            .ok_or_else(|| NotifyListenerError::InvalidPath(self.path.to_owned()))?;
136        unistd::chdir(workdir).map_err(|e| NotifyListenerError::Chdir {
137            source: e,
138            path: workdir.to_owned(),
139        })?;
140        let socket_name = self
141            .path
142            .file_name()
143            .ok_or_else(|| NotifyListenerError::InvalidPath(self.path.to_owned()))?;
144        let mut stream =
145            UnixStream::connect(socket_name).map_err(|e| NotifyListenerError::Connect {
146                source: e,
147                // ok to unwrap as OsStr should always be utf-8 compatible
148                name: socket_name.to_str().unwrap().to_owned(),
149            })?;
150        stream
151            .write_all(b"start container")
152            .map_err(NotifyListenerError::SendStartContainer)?;
153        tracing::debug!("notify finished");
154        unistd::chdir(&cwd).map_err(|e| NotifyListenerError::Chdir {
155            source: e,
156            path: cwd,
157        })?;
158        Ok(())
159    }
160}
161
162#[cfg(test)]
163mod test {
164    use tempfile::tempdir;
165
166    use super::*;
167
168    #[test]
169    /// Test that the listener can be cloned and function correctly. This test
170    /// also serves as a test for the normal case.
171    fn test_notify_listener_clone() {
172        let tempdir = tempdir().unwrap();
173        let socket_path = tempdir.path().join("notify.sock");
174        // listener needs to be created first because it will create the socket.
175        let listener = NotifyListener::new(&socket_path).unwrap();
176        let mut socket = NotifySocket::new(socket_path.clone());
177        // This is safe without race because the unix domain socket is already
178        // created. It is OK for the socket to send the start notification
179        // before the listener wait is called.
180        let thread_handle = std::thread::spawn({
181            move || {
182                // We clone the listener and listen on the cloned listener to
183                // make sure the cloned fd functions correctly.
184                listener.wait_for_container_start().unwrap();
185            }
186        });
187
188        socket.notify_container_start().unwrap();
189        thread_handle.join().unwrap();
190    }
191}