Skip to main content

nu_plugin_core/communication_mode/
mod.rs

1use std::ffi::OsStr;
2use std::io::{Stdin, Stdout};
3use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use nu_protocol::ShellError;
7#[cfg(feature = "local-socket")] // unused without that feature
8use nu_protocol::shell_error::io::IoError;
9
10#[cfg(feature = "local-socket")]
11mod local_socket;
12
13#[cfg(feature = "local-socket")]
14use local_socket::*;
15
16/// Controls whether plugin stderr is forwarded to Nushell's stderr.
17///
18/// Plugin stderr is inherited by default, so anything written by a plugin to
19/// stderr is shown directly to the user. When this flag is set, that forwarding
20/// is suppressed for plugin processes started while the flag is active.
21pub static SUPPRESS_STDERR: AtomicBool = AtomicBool::new(false);
22
23/// The type of communication used between the plugin and the engine.
24///
25/// `Stdio` is required to be supported by all plugins, and is attempted initially. If the
26/// `local-socket` feature is enabled and the plugin supports it, `LocalSocket` may be attempted.
27///
28/// Local socket communication has the benefit of not tying up stdio, so it's more compatible with
29/// plugins that want to take user input from the terminal in some way.
30#[derive(Debug, Clone)]
31pub enum CommunicationMode {
32    /// Communicate using `stdin` and `stdout`.
33    Stdio,
34    /// Communicate using an operating system-specific local socket.
35    #[cfg(feature = "local-socket")]
36    LocalSocket(std::ffi::OsString),
37}
38
39impl CommunicationMode {
40    /// Generate a new local socket communication mode based on the given plugin exe path.
41    #[cfg(feature = "local-socket")]
42    pub fn local_socket(plugin_exe: &std::path::Path) -> CommunicationMode {
43        use std::hash::{Hash, Hasher};
44        use std::time::SystemTime;
45
46        // Generate the unique ID based on the plugin path and the current time. The actual
47        // algorithm here is not very important, we just want this to be relatively unique very
48        // briefly. Using the default hasher in the stdlib means zero extra dependencies.
49        let mut hasher = std::collections::hash_map::DefaultHasher::new();
50
51        plugin_exe.hash(&mut hasher);
52        SystemTime::now().hash(&mut hasher);
53
54        let unique_id = format!("{:016x}", hasher.finish());
55
56        CommunicationMode::LocalSocket(make_local_socket_name(&unique_id))
57    }
58
59    pub fn args(&self) -> Vec<&OsStr> {
60        match self {
61            CommunicationMode::Stdio => vec![OsStr::new("--stdio")],
62            #[cfg(feature = "local-socket")]
63            CommunicationMode::LocalSocket(path) => {
64                vec![OsStr::new("--local-socket"), path.as_os_str()]
65            }
66        }
67    }
68
69    pub fn setup_command_io(&self, command: &mut Command) {
70        match self {
71            CommunicationMode::Stdio => {
72                // Both stdout and stdin are piped so we can receive information from the plugin
73                command.stdin(Stdio::piped());
74                command.stdout(Stdio::piped());
75            }
76            #[cfg(feature = "local-socket")]
77            CommunicationMode::LocalSocket(_) => {
78                // Stdio can be used by the plugin to talk to the terminal in local socket mode,
79                // which is the big benefit
80                command.stdin(Stdio::inherit());
81                command.stdout(Stdio::inherit());
82            }
83        }
84
85        if SUPPRESS_STDERR.load(Ordering::Relaxed) {
86            command.stderr(Stdio::null());
87        }
88    }
89
90    pub fn serve(&self) -> Result<PreparedServerCommunication, ShellError> {
91        match self {
92            // Nothing to set up for stdio - we just take it from the child.
93            CommunicationMode::Stdio => Ok(PreparedServerCommunication::Stdio),
94            // For sockets: we need to create the server so that the child won't fail to connect.
95            #[cfg(feature = "local-socket")]
96            CommunicationMode::LocalSocket(name) => {
97                use interprocess::local_socket::ListenerOptions;
98
99                let listener = interpret_local_socket_name(name)
100                    .and_then(|name| ListenerOptions::new().name(name).create_sync())
101                    .map_err(|err| {
102                        IoError::new_internal(
103                            err,
104                            format!(
105                                "Could not interpret local socket name {:?}",
106                                name.to_string_lossy()
107                            ),
108                        )
109                    })?;
110                Ok(PreparedServerCommunication::LocalSocket { listener })
111            }
112        }
113    }
114
115    pub fn connect_as_client(&self) -> Result<ClientCommunicationIo, ShellError> {
116        match self {
117            CommunicationMode::Stdio => Ok(ClientCommunicationIo::Stdio(
118                std::io::stdin(),
119                std::io::stdout(),
120            )),
121            #[cfg(feature = "local-socket")]
122            CommunicationMode::LocalSocket(name) => {
123                // Connect to the specified socket.
124                let get_socket = || {
125                    use interprocess::local_socket as ls;
126                    use ls::traits::Stream;
127
128                    interpret_local_socket_name(name)
129                        .and_then(|name| ls::Stream::connect(name))
130                        .map_err(|err| {
131                            ShellError::Io(IoError::new_internal(
132                                err,
133                                format!(
134                                    "Could not interpret local socket name {:?}",
135                                    name.to_string_lossy()
136                                ),
137                            ))
138                        })
139                };
140                // Reverse order from the server: read in, write out
141                let read_in = get_socket()?;
142                let write_out = get_socket()?;
143                Ok(ClientCommunicationIo::LocalSocket { read_in, write_out })
144            }
145        }
146    }
147}
148
149/// The result of [`CommunicationMode::serve()`], which acts as an intermediate stage for
150/// communication modes that require some kind of socket binding to occur before the client process
151/// can be started. Call [`.connect()`](Self::connect) once the client process has been started.
152///
153/// The socket may be cleaned up on `Drop` if applicable.
154pub enum PreparedServerCommunication {
155    /// Will take stdin and stdout from the process on [`.connect()`](Self::connect).
156    Stdio,
157    /// Contains the listener to accept connections on. On Unix, the socket is unlinked on `Drop`.
158    #[cfg(feature = "local-socket")]
159    LocalSocket {
160        listener: interprocess::local_socket::Listener,
161    },
162}
163
164impl PreparedServerCommunication {
165    pub fn connect(&self, child: &mut Child) -> Result<ServerCommunicationIo, ShellError> {
166        match self {
167            PreparedServerCommunication::Stdio => {
168                let stdin = child
169                    .stdin
170                    .take()
171                    .ok_or_else(|| ShellError::PluginFailedToLoad {
172                        msg: "Plugin missing stdin writer".into(),
173                    })?;
174
175                let stdout = child
176                    .stdout
177                    .take()
178                    .ok_or_else(|| ShellError::PluginFailedToLoad {
179                        msg: "Plugin missing stdout writer".into(),
180                    })?;
181
182                Ok(ServerCommunicationIo::Stdio(stdin, stdout))
183            }
184            #[cfg(feature = "local-socket")]
185            PreparedServerCommunication::LocalSocket { listener, .. } => {
186                use interprocess::local_socket::ListenerNonblockingMode;
187                use interprocess::local_socket::traits::{Listener, Stream};
188                use nu_utils::time::Instant;
189                use std::time::Duration;
190
191                const RETRY_PERIOD: Duration = Duration::from_millis(1);
192                const TIMEOUT: Duration = Duration::from_secs(10);
193
194                let start = Instant::now();
195
196                // Use a loop to try to get two clients from the listener: one for read (the plugin
197                // output) and one for write (the plugin input)
198                //
199                // Be non-blocking on Accept only, so we can timeout.
200                listener
201                    .set_nonblocking(ListenerNonblockingMode::Accept)
202                    .map_err(|err| {
203                        IoError::new_internal(
204                            err,
205                            "Could not set non-blocking mode accept for listener",
206                        )
207                    })?;
208                let mut get_socket = || {
209                    let mut result = None;
210                    while let Ok(None) = child.try_wait() {
211                        match listener.accept() {
212                            Ok(stream) => {
213                                // Success! Ensure the stream is in nonblocking mode though, for
214                                // good measure. Had an issue without this on macOS.
215                                stream.set_nonblocking(false).map_err(|err| {
216                                    IoError::new_internal(
217                                        err,
218                                        "Could not disable non-blocking mode for listener",
219                                    )
220                                })?;
221                                result = Some(stream);
222                                break;
223                            }
224                            Err(err) => {
225                                if !is_would_block_err(&err) {
226                                    // `WouldBlock` is ok, just means it's not ready yet, but some other
227                                    // kind of error should be reported
228                                    return Err(ShellError::Io(IoError::new_internal(
229                                        err,
230                                        "Accepting new data from listener failed",
231                                    )));
232                                }
233                            }
234                        }
235                        if Instant::now().saturating_duration_since(start) > TIMEOUT {
236                            return Err(ShellError::PluginFailedToLoad {
237                                msg: "Plugin timed out while waiting to connect to socket".into(),
238                            });
239                        } else {
240                            std::thread::sleep(RETRY_PERIOD);
241                        }
242                    }
243                    if let Some(stream) = result {
244                        Ok(stream)
245                    } else {
246                        // The process may have exited
247                        Err(ShellError::PluginFailedToLoad {
248                            msg: "Plugin exited without connecting".into(),
249                        })
250                    }
251                };
252                // Input stream always comes before output
253                let write_in = get_socket()?;
254                let read_out = get_socket()?;
255                Ok(ServerCommunicationIo::LocalSocket { read_out, write_in })
256            }
257        }
258    }
259}
260
261/// The required streams for communication from the engine side, i.e. the server in socket terms.
262pub enum ServerCommunicationIo {
263    Stdio(ChildStdin, ChildStdout),
264    #[cfg(feature = "local-socket")]
265    LocalSocket {
266        read_out: interprocess::local_socket::Stream,
267        write_in: interprocess::local_socket::Stream,
268    },
269}
270
271/// The required streams for communication from the plugin side, i.e. the client in socket terms.
272pub enum ClientCommunicationIo {
273    Stdio(Stdin, Stdout),
274    #[cfg(feature = "local-socket")]
275    LocalSocket {
276        read_in: interprocess::local_socket::Stream,
277        write_out: interprocess::local_socket::Stream,
278    },
279}