nu_plugin_core/communication_mode/
mod.rs1use std::ffi::OsStr;
2use std::io::{Stdin, Stdout};
3#[cfg(all(feature = "local-socket", unix))]
4use std::path::{Path, PathBuf};
5use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
6use std::sync::atomic::{AtomicBool, Ordering};
7
8use nu_protocol::ShellError;
9#[cfg(feature = "local-socket")] use nu_protocol::shell_error::io::IoError;
11
12#[cfg(feature = "local-socket")]
13mod local_socket;
14
15#[cfg(feature = "local-socket")]
16use local_socket::*;
17
18pub static SUPPRESS_STDERR: AtomicBool = AtomicBool::new(false);
24
25#[derive(Debug, Clone)]
33pub enum CommunicationMode {
34 Stdio,
36 #[cfg(feature = "local-socket")]
38 LocalSocket(std::ffi::OsString),
39}
40
41impl CommunicationMode {
42 #[cfg(feature = "local-socket")]
44 pub fn local_socket(plugin_exe: &std::path::Path) -> CommunicationMode {
45 use std::hash::{Hash, Hasher};
46 use std::time::SystemTime;
47
48 let mut hasher = std::collections::hash_map::DefaultHasher::new();
52
53 plugin_exe.hash(&mut hasher);
54 SystemTime::now().hash(&mut hasher);
55
56 let unique_id = format!("{:016x}", hasher.finish());
57
58 CommunicationMode::LocalSocket(make_local_socket_name(&unique_id))
59 }
60
61 pub fn args(&self) -> Vec<&OsStr> {
62 match self {
63 CommunicationMode::Stdio => vec![OsStr::new("--stdio")],
64 #[cfg(feature = "local-socket")]
65 CommunicationMode::LocalSocket(path) => {
66 vec![OsStr::new("--local-socket"), path.as_os_str()]
67 }
68 }
69 }
70
71 pub fn setup_command_io(&self, command: &mut Command) {
72 match self {
73 CommunicationMode::Stdio => {
74 command.stdin(Stdio::piped());
76 command.stdout(Stdio::piped());
77 }
78 #[cfg(feature = "local-socket")]
79 CommunicationMode::LocalSocket(_) => {
80 command.stdin(Stdio::inherit());
83 command.stdout(Stdio::inherit());
84 }
85 }
86
87 if SUPPRESS_STDERR.load(Ordering::Relaxed) {
88 command.stderr(Stdio::null());
89 }
90 }
91
92 pub fn serve(&self) -> Result<PreparedServerCommunication, ShellError> {
93 match self {
94 CommunicationMode::Stdio => Ok(PreparedServerCommunication::Stdio),
96 #[cfg(feature = "local-socket")]
98 CommunicationMode::LocalSocket(name) => {
99 use interprocess::local_socket::ListenerOptions;
100
101 #[cfg(unix)]
102 let socket_dir = prepare_local_socket_path(name).map_err(|err| {
103 IoError::new_internal(
104 err,
105 format!(
106 "Could not prepare local socket path {:?}",
107 name.to_string_lossy()
108 ),
109 )
110 })?;
111
112 let listener = match interpret_local_socket_name(name)
113 .and_then(|name| ListenerOptions::new().name(name).create_sync())
114 {
115 Ok(listener) => listener,
116 Err(err) => {
117 #[cfg(unix)]
118 cleanup_local_socket_dir(socket_dir.as_ref());
119 return Err(ShellError::Io(IoError::new_internal(
120 err,
121 format!(
122 "Could not interpret local socket name {:?}",
123 name.to_string_lossy()
124 ),
125 )));
126 }
127 };
128 Ok(PreparedServerCommunication::LocalSocket {
129 listener,
130 #[cfg(unix)]
131 socket_dir,
132 })
133 }
134 }
135 }
136
137 pub fn connect_as_client(&self) -> Result<ClientCommunicationIo, ShellError> {
138 match self {
139 CommunicationMode::Stdio => Ok(ClientCommunicationIo::Stdio(
140 std::io::stdin(),
141 std::io::stdout(),
142 )),
143 #[cfg(feature = "local-socket")]
144 CommunicationMode::LocalSocket(name) => {
145 let get_socket = || {
147 use interprocess::local_socket as ls;
148 use ls::traits::Stream;
149
150 interpret_local_socket_name(name)
151 .and_then(|name| ls::Stream::connect(name))
152 .map_err(|err| {
153 ShellError::Io(IoError::new_internal(
154 err,
155 format!(
156 "Could not interpret local socket name {:?}",
157 name.to_string_lossy()
158 ),
159 ))
160 })
161 };
162 let read_in = get_socket()?;
164 let write_out = get_socket()?;
165 Ok(ClientCommunicationIo::LocalSocket { read_in, write_out })
166 }
167 }
168 }
169}
170
171pub enum PreparedServerCommunication {
177 Stdio,
179 #[cfg(feature = "local-socket")]
181 LocalSocket {
182 listener: interprocess::local_socket::Listener,
183 #[cfg(unix)]
184 socket_dir: Option<PathBuf>,
185 },
186}
187
188#[cfg(all(feature = "local-socket", unix))]
189fn prepare_local_socket_path(name: &OsStr) -> Result<Option<PathBuf>, std::io::Error> {
190 use std::os::unix::fs::DirBuilderExt;
191
192 let path = Path::new(name);
193 let Some(parent) = path.parent() else {
194 return Ok(None);
195 };
196
197 let mut builder = std::fs::DirBuilder::new();
198 builder.mode(0o700).create(parent)?;
199 Ok(Some(parent.to_path_buf()))
200}
201
202#[cfg(all(feature = "local-socket", unix))]
203fn cleanup_local_socket_dir(socket_dir: Option<&PathBuf>) {
204 if let Some(socket_dir) = socket_dir {
205 let _ = std::fs::remove_dir_all(socket_dir);
206 }
207}
208
209#[cfg(all(test, feature = "local-socket", unix))]
210mod tests {
211 use super::{CommunicationMode, make_local_socket_name};
212 use std::os::unix::fs::PermissionsExt;
213 use std::path::Path;
214 use std::process::Command;
215
216 const OWNER_ONLY_UMASK_CHILD: &str = "NU_PLUGIN_CORE_OWNER_ONLY_UMASK_CHILD";
217
218 #[test]
219 fn local_socket_dir_is_owner_only() {
220 if std::env::var_os(OWNER_ONLY_UMASK_CHILD).is_none() {
221 let test_binary =
222 std::env::current_exe().expect("test binary path should be available");
223 let status = Command::new("sh")
224 .args(["-c", "umask 000; exec \"$@\"", "sh"])
225 .arg(test_binary)
226 .args([
227 "--exact",
228 "communication_mode::tests::local_socket_dir_is_owner_only",
229 ])
230 .env(OWNER_ONLY_UMASK_CHILD, "1")
231 .status()
232 .expect("owner-only test should run in a child process");
233
234 assert!(status.success(), "owner-only child test failed");
235 return;
236 }
237
238 let name = make_local_socket_name("owner-only-test");
239 let socket_dir = Path::new(&name)
240 .parent()
241 .expect("socket should have a parent dir")
242 .to_owned();
243
244 let mode = CommunicationMode::LocalSocket(name)
245 .serve()
246 .expect("local socket should bind");
247
248 let permissions = std::fs::metadata(&socket_dir)
249 .expect("local socket dir should exist")
250 .permissions()
251 .mode();
252
253 drop(mode);
254
255 assert_eq!(0o700, permissions & 0o777);
256 assert!(!socket_dir.exists());
257 }
258
259 #[test]
260 fn pre_existing_local_socket_dir_is_rejected() {
261 let name = make_local_socket_name("pre-existing-dir-test");
262 let socket_path = Path::new(&name);
263 let socket_dir = socket_path
264 .parent()
265 .expect("socket should have a parent dir");
266
267 std::fs::create_dir(socket_dir).expect("pre-existing socket dir should be created");
268 std::fs::set_permissions(socket_dir, std::fs::Permissions::from_mode(0o777))
269 .expect("pre-existing socket dir should be world-accessible");
270 let permissions = std::fs::metadata(socket_dir)
271 .expect("pre-existing socket dir should exist")
272 .permissions()
273 .mode();
274 assert_eq!(0o777, permissions & 0o777);
275
276 let result = CommunicationMode::LocalSocket(name.clone()).serve();
277
278 assert!(
279 result.is_err(),
280 "a pre-existing socket dir must be rejected"
281 );
282 assert!(
283 socket_dir.exists(),
284 "the pre-existing dir must not be removed"
285 );
286 assert!(!socket_path.exists(), "the socket must not be bound");
287
288 std::fs::remove_dir(socket_dir).expect("pre-existing socket dir should be removed");
289 }
290}
291
292impl Drop for PreparedServerCommunication {
293 fn drop(&mut self) {
294 match self {
295 PreparedServerCommunication::Stdio => {}
296 #[cfg(feature = "local-socket")]
297 PreparedServerCommunication::LocalSocket {
298 #[cfg(unix)]
299 socket_dir,
300 ..
301 } => {
302 #[cfg(unix)]
303 cleanup_local_socket_dir(socket_dir.as_ref());
304 }
305 }
306 }
307}
308
309impl PreparedServerCommunication {
310 pub fn connect(&self, child: &mut Child) -> Result<ServerCommunicationIo, ShellError> {
311 match self {
312 PreparedServerCommunication::Stdio => {
313 let stdin = child
314 .stdin
315 .take()
316 .ok_or_else(|| ShellError::PluginFailedToLoad {
317 msg: "Plugin missing stdin writer".into(),
318 })?;
319
320 let stdout = child
321 .stdout
322 .take()
323 .ok_or_else(|| ShellError::PluginFailedToLoad {
324 msg: "Plugin missing stdout writer".into(),
325 })?;
326
327 Ok(ServerCommunicationIo::Stdio(stdin, stdout))
328 }
329 #[cfg(feature = "local-socket")]
330 PreparedServerCommunication::LocalSocket { listener, .. } => {
331 use interprocess::local_socket::ListenerNonblockingMode;
332 use interprocess::local_socket::traits::{Listener, Stream};
333 use nu_utils::time::Instant;
334 use std::time::Duration;
335
336 const RETRY_PERIOD: Duration = Duration::from_millis(1);
337 const TIMEOUT: Duration = Duration::from_secs(10);
338
339 let start = Instant::now();
340
341 listener
346 .set_nonblocking(ListenerNonblockingMode::Accept)
347 .map_err(|err| {
348 IoError::new_internal(
349 err,
350 "Could not set non-blocking mode accept for listener",
351 )
352 })?;
353 let mut get_socket = || {
354 let mut result = None;
355 while let Ok(None) = child.try_wait() {
356 match listener.accept() {
357 Ok(stream) => {
358 stream.set_nonblocking(false).map_err(|err| {
361 IoError::new_internal(
362 err,
363 "Could not disable non-blocking mode for listener",
364 )
365 })?;
366 result = Some(stream);
367 break;
368 }
369 Err(err) => {
370 if !is_would_block_err(&err) {
371 return Err(ShellError::Io(IoError::new_internal(
374 err,
375 "Accepting new data from listener failed",
376 )));
377 }
378 }
379 }
380 if Instant::now().saturating_duration_since(start) > TIMEOUT {
381 return Err(ShellError::PluginFailedToLoad {
382 msg: "Plugin timed out while waiting to connect to socket".into(),
383 });
384 } else {
385 std::thread::sleep(RETRY_PERIOD);
386 }
387 }
388 if let Some(stream) = result {
389 Ok(stream)
390 } else {
391 Err(ShellError::PluginFailedToLoad {
393 msg: "Plugin exited without connecting".into(),
394 })
395 }
396 };
397 let write_in = get_socket()?;
399 let read_out = get_socket()?;
400 Ok(ServerCommunicationIo::LocalSocket { read_out, write_in })
401 }
402 }
403 }
404}
405
406pub enum ServerCommunicationIo {
408 Stdio(ChildStdin, ChildStdout),
409 #[cfg(feature = "local-socket")]
410 LocalSocket {
411 read_out: interprocess::local_socket::Stream,
412 write_in: interprocess::local_socket::Stream,
413 },
414}
415
416pub enum ClientCommunicationIo {
418 Stdio(Stdin, Stdout),
419 #[cfg(feature = "local-socket")]
420 LocalSocket {
421 read_in: interprocess::local_socket::Stream,
422 write_out: interprocess::local_socket::Stream,
423 },
424}