Split the plugin crate (#12563)

# Description

This breaks `nu-plugin` up into four crates:

- `nu-plugin-protocol`: just the type definitions for the protocol, no
I/O. If someone wanted to wire up something more bare metal, maybe for
async I/O, they could use this.
- `nu-plugin-core`: the shared stuff between engine/plugin. Less stable
interface.
- `nu-plugin-engine`: everything required for the engine to talk to
plugins. Less stable interface.
- `nu-plugin`: everything required for the plugin to talk to the engine,
what plugin developers use. Should be the most stable interface.

No changes are made to the interface exposed by `nu-plugin` - it should
all still be there. Re-exports from `nu-plugin-protocol` or
`nu-plugin-core` are used as required. Plugins shouldn't ever have to
use those crates directly.

This should be somewhat faster to compile as `nu-plugin-engine` and
`nu-plugin` can compile in parallel, and the engine doesn't need
`nu-plugin` and plugins don't need `nu-plugin-engine` (except for test
support), so that should reduce what needs to be compiled too.

The only significant change here other than splitting stuff up was to
break the `source` out of `PluginCustomValue` and create a new
`PluginCustomValueWithSource` type that contains that instead. One bonus
of that is we get rid of the option and it's now more type-safe, but it
also means that the logic for that stuff (actually running the plugin
for custom value ops) can live entirely within the `nu-plugin-engine`
crate.

# User-Facing Changes
- New crates.
- Added `local-socket` feature for `nu` to try to make it possible to
compile without that support if needed.

# Tests + Formatting
- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`
This commit is contained in:
Devyn Cairns
2024-04-27 10:08:12 -07:00
committed by GitHub
parent 884d5312bb
commit 0c4d5330ee
74 changed files with 3514 additions and 3110 deletions

View File

@ -13,30 +13,20 @@ bench = false
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.92.3" }
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
nu-system = { path = "../nu-system", version = "0.92.3" }
nu-utils = { path = "../nu-utils", version = "0.92.3" }
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.92.3" }
nu-plugin-core = { path = "../nu-plugin-core", version = "0.92.3", default-features = false }
bincode = "1.3"
rmp-serde = "1.2"
serde = { workspace = true }
serde_json = { workspace = true }
log = "0.4"
miette = { workspace = true }
semver = "1.0"
typetag = "0.2"
log = { workspace = true }
thiserror = "1.0"
interprocess = { version = "1.2.1", optional = true }
[dev-dependencies]
serde = { workspace = true }
typetag = "0.2"
[features]
default = ["local-socket"]
local-socket = ["interprocess"]
local-socket = ["nu-plugin-core/local-socket"]
[target.'cfg(target_family = "unix")'.dependencies]
# For setting the process group ID (EnterForeground / LeaveForeground)
nix = { workspace = true, default-features = false, features = ["process"] }
[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true, features = [
# For setting process creation flags
"Win32_System_Threading",
] }

View File

@ -64,35 +64,16 @@
//! [Plugin Example](https://github.com/nushell/nushell/tree/main/crates/nu_plugin_example)
//! that demonstrates the full range of plugin capabilities.
mod plugin;
mod protocol;
mod sequence;
mod serializers;
pub use plugin::{
serve_plugin, EngineInterface, Plugin, PluginCommand, PluginEncoder, PluginRead, PluginWrite,
SimplePluginCommand,
};
pub use protocol::EvaluatedCall;
pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};
#[cfg(test)]
mod test_util;
// Used by other nu crates.
#[doc(hidden)]
pub use plugin::{
add_plugin_to_working_set, create_plugin_signature, get_signature, load_plugin_file,
load_plugin_registry_item, serve_plugin_io, EngineInterfaceManager, GetPlugin, Interface,
InterfaceManager, PersistentPlugin, PluginDeclaration, PluginExecutionCommandContext,
PluginExecutionContext, PluginInterface, PluginInterfaceManager, PluginSource,
ServePluginError,
};
#[doc(hidden)]
pub use protocol::{PluginCustomValue, PluginInput, PluginOutput};
#[doc(hidden)]
pub use serializers::EncodingType;
#[doc(hidden)]
pub mod util;
pub use plugin::{serve_plugin, EngineInterface, Plugin, PluginCommand, SimplePluginCommand};
// Used by external benchmarks.
// Re-exports. Consider semver implications carefully.
pub use nu_plugin_core::{JsonSerializer, MsgPackSerializer, PluginEncoder};
pub use nu_plugin_protocol::EvaluatedCall;
// Required by other internal crates.
#[doc(hidden)]
pub use plugin::Encoder;
#[doc(hidden)]
pub use protocol::PluginCallResponse;
pub use plugin::{create_plugin_signature, serve_plugin_io};

View File

@ -1,84 +0,0 @@
use std::ffi::OsString;
#[cfg(test)]
pub(crate) mod tests;
/// Generate a name to be used for a local socket specific to this `nu` process, described by the
/// given `unique_id`, which should be unique to the purpose of the socket.
///
/// On Unix, this is a path, which should generally be 100 characters or less for compatibility. On
/// Windows, this is a name within the `\\.\pipe` namespace.
#[cfg(unix)]
pub fn make_local_socket_name(unique_id: &str) -> OsString {
// Prefer to put it in XDG_RUNTIME_DIR if set, since that's user-local
let mut base = if let Some(runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR") {
std::path::PathBuf::from(runtime_dir)
} else {
// Use std::env::temp_dir() for portability, especially since on Android this is probably
// not `/tmp`
std::env::temp_dir()
};
let socket_name = format!("nu.{}.{}.sock", std::process::id(), unique_id);
base.push(socket_name);
base.into()
}
/// Generate a name to be used for a local socket specific to this `nu` process, described by the
/// given `unique_id`, which should be unique to the purpose of the socket.
///
/// On Unix, this is a path, which should generally be 100 characters or less for compatibility. On
/// Windows, this is a name within the `\\.\pipe` namespace.
#[cfg(windows)]
pub fn make_local_socket_name(unique_id: &str) -> OsString {
format!("nu.{}.{}", std::process::id(), unique_id).into()
}
/// Determine if the error is just due to the listener not being ready yet in asynchronous mode
#[cfg(not(windows))]
pub fn is_would_block_err(err: &std::io::Error) -> bool {
err.kind() == std::io::ErrorKind::WouldBlock
}
/// Determine if the error is just due to the listener not being ready yet in asynchronous mode
#[cfg(windows)]
pub fn is_would_block_err(err: &std::io::Error) -> bool {
err.kind() == std::io::ErrorKind::WouldBlock
|| err.raw_os_error().is_some_and(|e| {
// Windows returns this error when trying to accept a pipe in non-blocking mode
e as i64 == windows::Win32::Foundation::ERROR_PIPE_LISTENING.0 as i64
})
}
/// Wraps the `interprocess` local socket stream for greater compatibility
#[derive(Debug)]
pub struct LocalSocketStream(pub interprocess::local_socket::LocalSocketStream);
impl From<interprocess::local_socket::LocalSocketStream> for LocalSocketStream {
fn from(value: interprocess::local_socket::LocalSocketStream) -> Self {
LocalSocketStream(value)
}
}
impl std::io::Read for LocalSocketStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.0.read(buf)
}
}
impl std::io::Write for LocalSocketStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
// We don't actually flush the underlying socket on Windows. The flush operation on a
// Windows named pipe actually synchronizes with read on the other side, and won't finish
// until the other side is empty. This isn't how most of our other I/O methods work, so we
// just won't do it. The BufWriter above this will have still made a write call with the
// contents of the buffer, which should be good enough.
if cfg!(not(windows)) {
self.0.flush()?;
}
Ok(())
}
}

View File

@ -1,19 +0,0 @@
use super::make_local_socket_name;
#[test]
fn local_socket_path_contains_pid() {
let name = make_local_socket_name("test-string")
.to_string_lossy()
.into_owned();
println!("{}", name);
assert!(name.to_string().contains(&std::process::id().to_string()));
}
#[test]
fn local_socket_path_contains_provided_name() {
let name = make_local_socket_name("test-string")
.to_string_lossy()
.into_owned();
println!("{}", name);
assert!(name.to_string().contains("test-string"));
}

View File

@ -1,233 +0,0 @@
use std::ffi::OsStr;
use std::io::{Stdin, Stdout};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use nu_protocol::ShellError;
#[cfg(feature = "local-socket")]
use interprocess::local_socket::LocalSocketListener;
#[cfg(feature = "local-socket")]
mod local_socket;
#[cfg(feature = "local-socket")]
use local_socket::*;
#[derive(Debug, Clone)]
pub(crate) enum CommunicationMode {
/// Communicate using `stdin` and `stdout`.
Stdio,
/// Communicate using an operating system-specific local socket.
#[cfg(feature = "local-socket")]
LocalSocket(std::ffi::OsString),
}
impl CommunicationMode {
/// Generate a new local socket communication mode based on the given plugin exe path.
#[cfg(feature = "local-socket")]
pub fn local_socket(plugin_exe: &std::path::Path) -> CommunicationMode {
use std::hash::{Hash, Hasher};
use std::time::SystemTime;
// Generate the unique ID based on the plugin path and the current time. The actual
// algorithm here is not very important, we just want this to be relatively unique very
// briefly. Using the default hasher in the stdlib means zero extra dependencies.
let mut hasher = std::collections::hash_map::DefaultHasher::new();
plugin_exe.hash(&mut hasher);
SystemTime::now().hash(&mut hasher);
let unique_id = format!("{:016x}", hasher.finish());
CommunicationMode::LocalSocket(make_local_socket_name(&unique_id))
}
pub fn args(&self) -> Vec<&OsStr> {
match self {
CommunicationMode::Stdio => vec![OsStr::new("--stdio")],
#[cfg(feature = "local-socket")]
CommunicationMode::LocalSocket(path) => {
vec![OsStr::new("--local-socket"), path.as_os_str()]
}
}
}
pub fn setup_command_io(&self, command: &mut Command) {
match self {
CommunicationMode::Stdio => {
// Both stdout and stdin are piped so we can receive information from the plugin
command.stdin(Stdio::piped());
command.stdout(Stdio::piped());
}
#[cfg(feature = "local-socket")]
CommunicationMode::LocalSocket(_) => {
// Stdio can be used by the plugin to talk to the terminal in local socket mode,
// which is the big benefit
command.stdin(Stdio::inherit());
command.stdout(Stdio::inherit());
}
}
}
pub fn serve(&self) -> Result<PreparedServerCommunication, ShellError> {
match self {
// Nothing to set up for stdio - we just take it from the child.
CommunicationMode::Stdio => Ok(PreparedServerCommunication::Stdio),
// For sockets: we need to create the server so that the child won't fail to connect.
#[cfg(feature = "local-socket")]
CommunicationMode::LocalSocket(name) => {
let listener = LocalSocketListener::bind(name.as_os_str()).map_err(|err| {
ShellError::IOError {
msg: format!("failed to open socket for plugin: {err}"),
}
})?;
Ok(PreparedServerCommunication::LocalSocket {
name: name.clone(),
listener,
})
}
}
}
pub fn connect_as_client(&self) -> Result<ClientCommunicationIo, ShellError> {
match self {
CommunicationMode::Stdio => Ok(ClientCommunicationIo::Stdio(
std::io::stdin(),
std::io::stdout(),
)),
#[cfg(feature = "local-socket")]
CommunicationMode::LocalSocket(name) => {
// Connect to the specified socket.
let get_socket = || {
use interprocess::local_socket as ls;
ls::LocalSocketStream::connect(name.as_os_str())
.map_err(|err| ShellError::IOError {
msg: format!("failed to connect to socket: {err}"),
})
.map(LocalSocketStream::from)
};
// Reverse order from the server: read in, write out
let read_in = get_socket()?;
let write_out = get_socket()?;
Ok(ClientCommunicationIo::LocalSocket { read_in, write_out })
}
}
}
}
pub(crate) enum PreparedServerCommunication {
Stdio,
#[cfg(feature = "local-socket")]
LocalSocket {
#[cfg_attr(windows, allow(dead_code))] // not used on Windows
name: std::ffi::OsString,
listener: LocalSocketListener,
},
}
impl PreparedServerCommunication {
pub fn connect(&self, child: &mut Child) -> Result<ServerCommunicationIo, ShellError> {
match self {
PreparedServerCommunication::Stdio => {
let stdin = child
.stdin
.take()
.ok_or_else(|| ShellError::PluginFailedToLoad {
msg: "Plugin missing stdin writer".into(),
})?;
let stdout = child
.stdout
.take()
.ok_or_else(|| ShellError::PluginFailedToLoad {
msg: "Plugin missing stdout writer".into(),
})?;
Ok(ServerCommunicationIo::Stdio(stdin, stdout))
}
#[cfg(feature = "local-socket")]
PreparedServerCommunication::LocalSocket { listener, .. } => {
use std::time::{Duration, Instant};
const RETRY_PERIOD: Duration = Duration::from_millis(1);
const TIMEOUT: Duration = Duration::from_secs(10);
let start = Instant::now();
// Use a loop to try to get two clients from the listener: one for read (the plugin
// output) and one for write (the plugin input)
listener.set_nonblocking(true)?;
let mut get_socket = || {
let mut result = None;
while let Ok(None) = child.try_wait() {
match listener.accept() {
Ok(stream) => {
// Success! But make sure the stream is in blocking mode.
stream.set_nonblocking(false)?;
result = Some(stream);
break;
}
Err(err) => {
if !is_would_block_err(&err) {
// `WouldBlock` is ok, just means it's not ready yet, but some other
// kind of error should be reported
return Err(err.into());
}
}
}
if Instant::now().saturating_duration_since(start) > TIMEOUT {
return Err(ShellError::PluginFailedToLoad {
msg: "Plugin timed out while waiting to connect to socket".into(),
});
} else {
std::thread::sleep(RETRY_PERIOD);
}
}
if let Some(stream) = result {
Ok(LocalSocketStream(stream))
} else {
// The process may have exited
Err(ShellError::PluginFailedToLoad {
msg: "Plugin exited without connecting".into(),
})
}
};
// Input stream always comes before output
let write_in = get_socket()?;
let read_out = get_socket()?;
Ok(ServerCommunicationIo::LocalSocket { read_out, write_in })
}
}
}
}
impl Drop for PreparedServerCommunication {
fn drop(&mut self) {
match self {
#[cfg(all(unix, feature = "local-socket"))]
PreparedServerCommunication::LocalSocket { name: path, .. } => {
// Just try to remove the socket file, it's ok if this fails
let _ = std::fs::remove_file(path);
}
_ => (),
}
}
}
pub(crate) enum ServerCommunicationIo {
Stdio(ChildStdin, ChildStdout),
#[cfg(feature = "local-socket")]
LocalSocket {
read_out: LocalSocketStream,
write_in: LocalSocketStream,
},
}
pub(crate) enum ClientCommunicationIo {
Stdio(Stdin, Stdout),
#[cfg(feature = "local-socket")]
LocalSocket {
read_in: LocalSocketStream,
write_out: LocalSocketStream,
},
}

View File

@ -1,316 +0,0 @@
use crate::util::MutableCow;
use nu_engine::{get_eval_block_with_early_return, get_full_help, ClosureEvalOnce};
use nu_protocol::{
ast::Call,
engine::{Closure, EngineState, Redirection, Stack},
Config, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Span, Spanned, Value,
};
use std::{
borrow::Cow,
collections::HashMap,
sync::{
atomic::{AtomicBool, AtomicU32},
Arc,
},
};
/// Object safe trait for abstracting operations required of the plugin context.
///
/// This is not a public API.
#[doc(hidden)]
pub trait PluginExecutionContext: Send + Sync {
/// A span pointing to the command being executed
fn span(&self) -> Span;
/// The interrupt signal, if present
fn ctrlc(&self) -> Option<&Arc<AtomicBool>>;
/// The pipeline externals state, for tracking the foreground process group, if present
fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>>;
/// Get engine configuration
fn get_config(&self) -> Result<Config, ShellError>;
/// Get plugin configuration
fn get_plugin_config(&self) -> Result<Option<Value>, ShellError>;
/// Get an environment variable from `$env`
fn get_env_var(&self, name: &str) -> Result<Option<Value>, ShellError>;
/// Get all environment variables
fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError>;
/// Get current working directory
fn get_current_dir(&self) -> Result<Spanned<String>, ShellError>;
/// Set an environment variable
fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError>;
/// Get help for the current command
fn get_help(&self) -> Result<Spanned<String>, ShellError>;
/// Get the contents of a [`Span`]
fn get_span_contents(&self, span: Span) -> Result<Spanned<Vec<u8>>, ShellError>;
/// Evaluate a closure passed to the plugin
fn eval_closure(
&self,
closure: Spanned<Closure>,
positional: Vec<Value>,
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError>;
/// Create an owned version of the context with `'static` lifetime
fn boxed(&self) -> Box<dyn PluginExecutionContext>;
}
/// The execution context of a plugin command. Can be borrowed.
///
/// This is not a public API.
#[doc(hidden)]
pub struct PluginExecutionCommandContext<'a> {
identity: Arc<PluginIdentity>,
engine_state: Cow<'a, EngineState>,
stack: MutableCow<'a, Stack>,
call: Cow<'a, Call>,
}
impl<'a> PluginExecutionCommandContext<'a> {
pub fn new(
identity: Arc<PluginIdentity>,
engine_state: &'a EngineState,
stack: &'a mut Stack,
call: &'a Call,
) -> PluginExecutionCommandContext<'a> {
PluginExecutionCommandContext {
identity,
engine_state: Cow::Borrowed(engine_state),
stack: MutableCow::Borrowed(stack),
call: Cow::Borrowed(call),
}
}
}
impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
fn span(&self) -> Span {
self.call.head
}
fn ctrlc(&self) -> Option<&Arc<AtomicBool>> {
self.engine_state.ctrlc.as_ref()
}
fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> {
Some(&self.engine_state.pipeline_externals_state)
}
fn get_config(&self) -> Result<Config, ShellError> {
Ok(nu_engine::get_config(&self.engine_state, &self.stack))
}
fn get_plugin_config(&self) -> Result<Option<Value>, ShellError> {
// Fetch the configuration for a plugin
//
// The `plugin` must match the registered name of a plugin. For
// `register nu_plugin_example` the plugin config lookup uses `"example"`
Ok(self
.get_config()?
.plugins
.get(self.identity.name())
.cloned()
.map(|value| {
let span = value.span();
match value {
Value::Closure { val, .. } => {
ClosureEvalOnce::new(&self.engine_state, &self.stack, val)
.run_with_input(PipelineData::Empty)
.map(|data| data.into_value(span))
.unwrap_or_else(|err| Value::error(err, self.call.head))
}
_ => value.clone(),
}
}))
}
fn get_env_var(&self, name: &str) -> Result<Option<Value>, ShellError> {
Ok(self.stack.get_env_var(&self.engine_state, name))
}
fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError> {
Ok(self.stack.get_env_vars(&self.engine_state))
}
fn get_current_dir(&self) -> Result<Spanned<String>, ShellError> {
let cwd = nu_engine::env::current_dir_str(&self.engine_state, &self.stack)?;
// The span is not really used, so just give it call.head
Ok(cwd.into_spanned(self.call.head))
}
fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError> {
self.stack.add_env_var(name, value);
Ok(())
}
fn get_help(&self) -> Result<Spanned<String>, ShellError> {
let decl = self.engine_state.get_decl(self.call.decl_id);
Ok(get_full_help(
&decl.signature(),
&decl.examples(),
&self.engine_state,
&mut self.stack.clone(),
false,
)
.into_spanned(self.call.head))
}
fn get_span_contents(&self, span: Span) -> Result<Spanned<Vec<u8>>, ShellError> {
Ok(self
.engine_state
.get_span_contents(span)
.to_vec()
.into_spanned(self.call.head))
}
fn eval_closure(
&self,
closure: Spanned<Closure>,
positional: Vec<Value>,
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
let block = self
.engine_state
.try_get_block(closure.item.block_id)
.ok_or_else(|| ShellError::GenericError {
error: "Plugin misbehaving".into(),
msg: format!(
"Tried to evaluate unknown block id: {}",
closure.item.block_id
),
span: Some(closure.span),
help: None,
inner: vec![],
})?;
let mut stack = self
.stack
.captures_to_stack(closure.item.captures)
.reset_pipes();
let stdout = if redirect_stdout {
Some(Redirection::Pipe(OutDest::Capture))
} else {
None
};
let stderr = if redirect_stderr {
Some(Redirection::Pipe(OutDest::Capture))
} else {
None
};
let stack = &mut stack.push_redirection(stdout, stderr);
// Set up the positional arguments
for (idx, value) in positional.into_iter().enumerate() {
if let Some(arg) = block.signature.get_positional(idx) {
if let Some(var_id) = arg.var_id {
stack.add_var(var_id, value);
} else {
return Err(ShellError::NushellFailedSpanned {
msg: "Error while evaluating closure from plugin".into(),
label: "closure argument missing var_id".into(),
span: closure.span,
});
}
}
}
let eval_block_with_early_return = get_eval_block_with_early_return(&self.engine_state);
eval_block_with_early_return(&self.engine_state, stack, block, input)
}
fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
Box::new(PluginExecutionCommandContext {
identity: self.identity.clone(),
engine_state: Cow::Owned(self.engine_state.clone().into_owned()),
stack: self.stack.owned(),
call: Cow::Owned(self.call.clone().into_owned()),
})
}
}
/// A bogus execution context for testing that doesn't really implement anything properly
#[cfg(test)]
pub(crate) struct PluginExecutionBogusContext;
#[cfg(test)]
impl PluginExecutionContext for PluginExecutionBogusContext {
fn span(&self) -> Span {
Span::test_data()
}
fn ctrlc(&self) -> Option<&Arc<AtomicBool>> {
None
}
fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> {
None
}
fn get_config(&self) -> Result<Config, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_config not implemented on bogus".into(),
})
}
fn get_plugin_config(&self) -> Result<Option<Value>, ShellError> {
Ok(None)
}
fn get_env_var(&self, _name: &str) -> Result<Option<Value>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_env_var not implemented on bogus".into(),
})
}
fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_env_vars not implemented on bogus".into(),
})
}
fn get_current_dir(&self) -> Result<Spanned<String>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_current_dir not implemented on bogus".into(),
})
}
fn add_env_var(&mut self, _name: String, _value: Value) -> Result<(), ShellError> {
Err(ShellError::NushellFailed {
msg: "add_env_var not implemented on bogus".into(),
})
}
fn get_help(&self) -> Result<Spanned<String>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_help not implemented on bogus".into(),
})
}
fn get_span_contents(&self, _span: Span) -> Result<Spanned<Vec<u8>>, ShellError> {
Err(ShellError::NushellFailed {
msg: "get_span_contents not implemented on bogus".into(),
})
}
fn eval_closure(
&self,
_closure: Spanned<Closure>,
_positional: Vec<Value>,
_input: PipelineData,
_redirect_stdout: bool,
_redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
Err(ShellError::NushellFailed {
msg: "eval_closure not implemented on bogus".into(),
})
}
fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
Box::new(PluginExecutionBogusContext)
}
}

View File

@ -1,125 +0,0 @@
use super::{GetPlugin, PluginExecutionCommandContext, PluginSource};
use crate::protocol::{CallInfo, EvaluatedCall};
use nu_engine::{command_prelude::*, get_eval_expression};
use nu_protocol::{PluginIdentity, PluginSignature};
use std::sync::Arc;
#[doc(hidden)] // Note: not for plugin authors / only used in nu-parser
#[derive(Clone)]
pub struct PluginDeclaration {
name: String,
signature: PluginSignature,
source: PluginSource,
}
impl PluginDeclaration {
pub fn new(plugin: Arc<dyn GetPlugin>, signature: PluginSignature) -> Self {
Self {
name: signature.sig.name.clone(),
signature,
source: PluginSource::new(plugin),
}
}
}
impl Command for PluginDeclaration {
fn name(&self) -> &str {
&self.name
}
fn signature(&self) -> Signature {
self.signature.sig.clone()
}
fn usage(&self) -> &str {
self.signature.sig.usage.as_str()
}
fn extra_usage(&self) -> &str {
self.signature.sig.extra_usage.as_str()
}
fn search_terms(&self) -> Vec<&str> {
self.signature
.sig
.search_terms
.iter()
.map(|term| term.as_str())
.collect()
}
fn examples(&self) -> Vec<Example> {
let mut res = vec![];
for e in self.signature.examples.iter() {
res.push(Example {
example: &e.example,
description: &e.description,
result: e.result.clone(),
})
}
res
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let eval_expression = get_eval_expression(engine_state);
// Create the EvaluatedCall to send to the plugin first - it's best for this to fail early,
// before we actually try to run the plugin command
let evaluated_call =
EvaluatedCall::try_from_call(call, engine_state, stack, eval_expression)?;
// Get the engine config
let engine_config = nu_engine::get_config(engine_state, stack);
// Get, or start, the plugin.
let plugin = self
.source
.persistent(None)
.and_then(|p| {
// Set the garbage collector config from the local config before running
p.set_gc_config(engine_config.plugin_gc.get(p.identity().name()));
p.get_plugin(Some((engine_state, stack)))
})
.map_err(|err| {
let decl = engine_state.get_decl(call.decl_id);
ShellError::GenericError {
error: format!("Unable to spawn plugin for `{}`", decl.name()),
msg: err.to_string(),
span: Some(call.head),
help: None,
inner: vec![],
}
})?;
// Create the context to execute in - this supports engine calls and custom values
let mut context = PluginExecutionCommandContext::new(
self.source.identity.clone(),
engine_state,
stack,
call,
);
plugin.run(
CallInfo {
name: self.name.clone(),
call: evaluated_call,
input,
},
&mut context,
)
}
fn is_plugin(&self) -> bool {
true
}
fn plugin_identity(&self) -> Option<&PluginIdentity> {
Some(&self.source.identity)
}
}

View File

@ -1,303 +0,0 @@
use crate::PersistentPlugin;
use nu_protocol::{PluginGcConfig, RegisteredPlugin};
use std::{
sync::{mpsc, Arc, Weak},
thread,
time::{Duration, Instant},
};
/// Plugin garbage collector
///
/// Many users don't want all of their plugins to stay running indefinitely after using them, so
/// this runs a thread that monitors the plugin's usage and stops it automatically if it meets
/// certain conditions of inactivity.
#[derive(Debug, Clone)]
pub struct PluginGc {
sender: mpsc::Sender<PluginGcMsg>,
}
impl PluginGc {
/// Start a new plugin garbage collector. Returns an error if the thread failed to spawn.
pub fn new(
config: PluginGcConfig,
plugin: &Arc<PersistentPlugin>,
) -> std::io::Result<PluginGc> {
let (sender, receiver) = mpsc::channel();
let mut state = PluginGcState {
config,
last_update: None,
locks: 0,
disabled: false,
plugin: Arc::downgrade(plugin),
name: plugin.identity().name().to_owned(),
};
thread::Builder::new()
.name(format!("plugin gc ({})", plugin.identity().name()))
.spawn(move || state.run(receiver))?;
Ok(PluginGc { sender })
}
/// Update the garbage collector config
pub fn set_config(&self, config: PluginGcConfig) {
let _ = self.sender.send(PluginGcMsg::SetConfig(config));
}
/// Ensure all GC messages have been processed
pub fn flush(&self) {
let (tx, rx) = mpsc::channel();
let _ = self.sender.send(PluginGcMsg::Flush(tx));
// This will block until the channel is dropped, which could be because the send failed, or
// because the GC got the message
let _ = rx.recv();
}
/// Increment the number of locks held by the plugin
pub fn increment_locks(&self, amount: i64) {
let _ = self.sender.send(PluginGcMsg::AddLocks(amount));
}
/// Decrement the number of locks held by the plugin
pub fn decrement_locks(&self, amount: i64) {
let _ = self.sender.send(PluginGcMsg::AddLocks(-amount));
}
/// Set whether the GC is disabled by explicit request from the plugin. This is separate from
/// the `enabled` option in the config, and overrides that option.
pub fn set_disabled(&self, disabled: bool) {
let _ = self.sender.send(PluginGcMsg::SetDisabled(disabled));
}
/// Tell the GC to stop tracking the plugin. The plugin will not be stopped. The GC cannot be
/// reactivated after this request - a new one must be created instead.
pub fn stop_tracking(&self) {
let _ = self.sender.send(PluginGcMsg::StopTracking);
}
/// Tell the GC that the plugin exited so that it can remove it from the persistent plugin.
///
/// The reason the plugin tells the GC rather than just stopping itself via `source` is that
/// it can't guarantee that the plugin currently pointed to by `source` is itself, but if the
/// GC is still running, it hasn't received [`.stop_tracking()`] yet, which means it should be
/// the right plugin.
pub fn exited(&self) {
let _ = self.sender.send(PluginGcMsg::Exited);
}
}
#[derive(Debug)]
enum PluginGcMsg {
SetConfig(PluginGcConfig),
Flush(mpsc::Sender<()>),
AddLocks(i64),
SetDisabled(bool),
StopTracking,
Exited,
}
#[derive(Debug)]
struct PluginGcState {
config: PluginGcConfig,
last_update: Option<Instant>,
locks: i64,
disabled: bool,
plugin: Weak<PersistentPlugin>,
name: String,
}
impl PluginGcState {
fn next_timeout(&self, now: Instant) -> Option<Duration> {
if self.locks <= 0 && !self.disabled {
self.last_update
.zip(self.config.enabled.then_some(self.config.stop_after))
.map(|(last_update, stop_after)| {
// If configured to stop, and used at some point, calculate the difference
let stop_after_duration = Duration::from_nanos(stop_after.max(0) as u64);
let duration_since_last_update = now.duration_since(last_update);
stop_after_duration.saturating_sub(duration_since_last_update)
})
} else {
// Don't timeout if there are locks set, or disabled
None
}
}
// returns `Some()` if the GC should not continue to operate, with `true` if it should stop the
// plugin, or `false` if it should not
fn handle_message(&mut self, msg: PluginGcMsg) -> Option<bool> {
match msg {
PluginGcMsg::SetConfig(config) => {
self.config = config;
}
PluginGcMsg::Flush(sender) => {
// Rather than sending a message, we just drop the channel, which causes the other
// side to disconnect equally well
drop(sender);
}
PluginGcMsg::AddLocks(amount) => {
self.locks += amount;
if self.locks < 0 {
log::warn!(
"Plugin GC ({name}) problem: locks count below zero after adding \
{amount}: locks={locks}",
name = self.name,
locks = self.locks,
);
}
// Any time locks are modified, that counts as activity
self.last_update = Some(Instant::now());
}
PluginGcMsg::SetDisabled(disabled) => {
self.disabled = disabled;
}
PluginGcMsg::StopTracking => {
// Immediately exit without stopping the plugin
return Some(false);
}
PluginGcMsg::Exited => {
// Exit and stop the plugin
return Some(true);
}
}
None
}
fn run(&mut self, receiver: mpsc::Receiver<PluginGcMsg>) {
let mut always_stop = false;
loop {
let Some(msg) = (match self.next_timeout(Instant::now()) {
Some(duration) => receiver.recv_timeout(duration).ok(),
None => receiver.recv().ok(),
}) else {
// If the timeout was reached, or the channel is disconnected, break the loop
break;
};
log::trace!("Plugin GC ({name}) message: {msg:?}", name = self.name);
if let Some(should_stop) = self.handle_message(msg) {
// Exit the GC
if should_stop {
// If should_stop = true, attempt to stop the plugin
always_stop = true;
break;
} else {
// Don't stop the plugin
return;
}
}
}
// Upon exiting the loop, if the timeout reached zero, or we are exiting due to an Exited
// message, stop the plugin
if always_stop
|| self
.next_timeout(Instant::now())
.is_some_and(|t| t.is_zero())
{
// We only hold a weak reference, and it's not an error if we fail to upgrade it -
// that just means the plugin is definitely stopped anyway.
if let Some(plugin) = self.plugin.upgrade() {
let name = &self.name;
if let Err(err) = plugin.stop() {
log::warn!("Plugin `{name}` failed to be stopped by GC: {err}");
} else {
log::debug!("Plugin `{name}` successfully stopped by GC");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_state() -> PluginGcState {
PluginGcState {
config: PluginGcConfig::default(),
last_update: None,
locks: 0,
disabled: false,
plugin: Weak::new(),
name: "test".into(),
}
}
#[test]
fn timeout_configured_as_zero() {
let now = Instant::now();
let mut state = test_state();
state.config.enabled = true;
state.config.stop_after = 0;
state.last_update = Some(now);
assert_eq!(Some(Duration::ZERO), state.next_timeout(now));
}
#[test]
fn timeout_past_deadline() {
let now = Instant::now();
let mut state = test_state();
state.config.enabled = true;
state.config.stop_after = Duration::from_secs(1).as_nanos() as i64;
state.last_update = Some(now - Duration::from_secs(2));
assert_eq!(Some(Duration::ZERO), state.next_timeout(now));
}
#[test]
fn timeout_with_deadline_in_future() {
let now = Instant::now();
let mut state = test_state();
state.config.enabled = true;
state.config.stop_after = Duration::from_secs(1).as_nanos() as i64;
state.last_update = Some(now);
assert_eq!(Some(Duration::from_secs(1)), state.next_timeout(now));
}
#[test]
fn no_timeout_if_disabled_by_config() {
let now = Instant::now();
let mut state = test_state();
state.config.enabled = false;
state.last_update = Some(now);
assert_eq!(None, state.next_timeout(now));
}
#[test]
fn no_timeout_if_disabled_by_plugin() {
let now = Instant::now();
let mut state = test_state();
state.config.enabled = true;
state.disabled = true;
state.last_update = Some(now);
assert_eq!(None, state.next_timeout(now));
}
#[test]
fn no_timeout_if_locks_count_over_zero() {
let now = Instant::now();
let mut state = test_state();
state.config.enabled = true;
state.locks = 1;
state.last_update = Some(now);
assert_eq!(None, state.next_timeout(now));
}
#[test]
fn adding_locks_changes_last_update() {
let mut state = test_state();
let original_last_update = Some(Instant::now() - Duration::from_secs(1));
state.last_update = original_last_update;
state.handle_message(PluginGcMsg::AddLocks(1));
assert_ne!(original_last_update, state.last_update, "not updated");
}
}

View File

@ -1,473 +0,0 @@
//! Implements the stream multiplexing interface for both the plugin side and the engine side.
use crate::{
plugin::Encoder,
protocol::{
ExternalStreamInfo, ListStreamInfo, PipelineDataHeader, RawStreamInfo, StreamMessage,
},
sequence::Sequence,
};
use nu_protocol::{ListStream, PipelineData, RawStream, ShellError};
use std::{
io::Write,
sync::{
atomic::{AtomicBool, Ordering::Relaxed},
Arc, Mutex,
},
thread,
};
mod stream;
mod engine;
pub use engine::{EngineInterface, EngineInterfaceManager, ReceivedPluginCall};
mod plugin;
pub use plugin::{PluginInterface, PluginInterfaceManager};
use self::stream::{StreamManager, StreamManagerHandle, StreamWriter, WriteStreamMessage};
#[cfg(test)]
mod test_util;
#[cfg(test)]
mod tests;
/// The maximum number of list stream values to send without acknowledgement. This should be tuned
/// with consideration for memory usage.
const LIST_STREAM_HIGH_PRESSURE: i32 = 100;
/// The maximum number of raw stream buffers to send without acknowledgement. This should be tuned
/// with consideration for memory usage.
const RAW_STREAM_HIGH_PRESSURE: i32 = 50;
/// Read input/output from the stream.
///
/// This is not a public API.
#[doc(hidden)]
pub trait PluginRead<T> {
/// Returns `Ok(None)` on end of stream.
fn read(&mut self) -> Result<Option<T>, ShellError>;
}
impl<R, E, T> PluginRead<T> for (R, E)
where
R: std::io::BufRead,
E: Encoder<T>,
{
fn read(&mut self) -> Result<Option<T>, ShellError> {
self.1.decode(&mut self.0)
}
}
impl<R, T> PluginRead<T> for &mut R
where
R: PluginRead<T>,
{
fn read(&mut self) -> Result<Option<T>, ShellError> {
(**self).read()
}
}
/// Write input/output to the stream.
///
/// The write should be atomic, without interference from other threads.
///
/// This is not a public API.
#[doc(hidden)]
pub trait PluginWrite<T>: Send + Sync {
fn write(&self, data: &T) -> Result<(), ShellError>;
/// Flush any internal buffers, if applicable.
fn flush(&self) -> Result<(), ShellError>;
/// True if this output is stdout, so that plugins can avoid using stdout for their own purpose
fn is_stdout(&self) -> bool {
false
}
}
impl<E, T> PluginWrite<T> for (std::io::Stdout, E)
where
E: Encoder<T>,
{
fn write(&self, data: &T) -> Result<(), ShellError> {
let mut lock = self.0.lock();
self.1.encode(data, &mut lock)
}
fn flush(&self) -> Result<(), ShellError> {
self.0.lock().flush().map_err(|err| ShellError::IOError {
msg: err.to_string(),
})
}
fn is_stdout(&self) -> bool {
true
}
}
impl<W, E, T> PluginWrite<T> for (Mutex<W>, E)
where
W: std::io::Write + Send,
E: Encoder<T>,
{
fn write(&self, data: &T) -> Result<(), ShellError> {
let mut lock = self.0.lock().map_err(|_| ShellError::NushellFailed {
msg: "writer mutex poisoned".into(),
})?;
self.1.encode(data, &mut *lock)
}
fn flush(&self) -> Result<(), ShellError> {
let mut lock = self.0.lock().map_err(|_| ShellError::NushellFailed {
msg: "writer mutex poisoned".into(),
})?;
lock.flush().map_err(|err| ShellError::IOError {
msg: err.to_string(),
})
}
}
impl<W, T> PluginWrite<T> for &W
where
W: PluginWrite<T>,
{
fn write(&self, data: &T) -> Result<(), ShellError> {
(**self).write(data)
}
fn flush(&self) -> Result<(), ShellError> {
(**self).flush()
}
fn is_stdout(&self) -> bool {
(**self).is_stdout()
}
}
/// An interface manager handles I/O and state management for communication between a plugin and the
/// engine. See [`PluginInterfaceManager`] for communication from the engine side to a plugin, or
/// [`EngineInterfaceManager`] for communication from the plugin side to the engine.
///
/// There is typically one [`InterfaceManager`] consuming input from a background thread, and
/// managing shared state.
///
/// This is not a public API.
#[doc(hidden)]
pub trait InterfaceManager {
/// The corresponding interface type.
type Interface: Interface + 'static;
/// The input message type.
type Input;
/// Make a new interface that communicates with this [`InterfaceManager`].
fn get_interface(&self) -> Self::Interface;
/// Consume an input message.
///
/// When implementing, call [`.consume_stream_message()`] for any encapsulated
/// [`StreamMessage`]s received.
fn consume(&mut self, input: Self::Input) -> Result<(), ShellError>;
/// Get the [`StreamManager`] for handling operations related to stream messages.
fn stream_manager(&self) -> &StreamManager;
/// Prepare [`PipelineData`] after reading. This is called by `read_pipeline_data()` as
/// a hook so that values that need special handling can be taken care of.
fn prepare_pipeline_data(&self, data: PipelineData) -> Result<PipelineData, ShellError>;
/// Consume an input stream message.
///
/// This method is provided for implementors to use.
fn consume_stream_message(&mut self, message: StreamMessage) -> Result<(), ShellError> {
self.stream_manager().handle_message(message)
}
/// Generate `PipelineData` for reading a stream, given a [`PipelineDataHeader`] that was
/// received from the other side.
///
/// This method is provided for implementors to use.
fn read_pipeline_data(
&self,
header: PipelineDataHeader,
ctrlc: Option<&Arc<AtomicBool>>,
) -> Result<PipelineData, ShellError> {
self.prepare_pipeline_data(match header {
PipelineDataHeader::Empty => PipelineData::Empty,
PipelineDataHeader::Value(value) => PipelineData::Value(value, None),
PipelineDataHeader::ListStream(info) => {
let handle = self.stream_manager().get_handle();
let reader = handle.read_stream(info.id, self.get_interface())?;
PipelineData::ListStream(ListStream::from_stream(reader, ctrlc.cloned()), None)
}
PipelineDataHeader::ExternalStream(info) => {
let handle = self.stream_manager().get_handle();
let span = info.span;
let new_raw_stream = |raw_info: RawStreamInfo| {
let reader = handle.read_stream(raw_info.id, self.get_interface())?;
let mut stream =
RawStream::new(Box::new(reader), ctrlc.cloned(), span, raw_info.known_size);
stream.is_binary = raw_info.is_binary;
Ok::<_, ShellError>(stream)
};
PipelineData::ExternalStream {
stdout: info.stdout.map(new_raw_stream).transpose()?,
stderr: info.stderr.map(new_raw_stream).transpose()?,
exit_code: info
.exit_code
.map(|list_info| {
handle
.read_stream(list_info.id, self.get_interface())
.map(|reader| ListStream::from_stream(reader, ctrlc.cloned()))
})
.transpose()?,
span: info.span,
metadata: None,
trim_end_newline: info.trim_end_newline,
}
}
})
}
}
/// An interface provides an API for communicating with a plugin or the engine and facilitates
/// stream I/O. See [`PluginInterface`] for the API from the engine side to a plugin, or
/// [`EngineInterface`] for the API from the plugin side to the engine.
///
/// There can be multiple copies of the interface managed by a single [`InterfaceManager`].
///
/// This is not a public API.
#[doc(hidden)]
pub trait Interface: Clone + Send {
/// The output message type, which must be capable of encapsulating a [`StreamMessage`].
type Output: From<StreamMessage>;
/// Any context required to construct [`PipelineData`]. Can be `()` if not needed.
type DataContext;
/// Write an output message.
fn write(&self, output: Self::Output) -> Result<(), ShellError>;
/// Flush the output buffer, so messages are visible to the other side.
fn flush(&self) -> Result<(), ShellError>;
/// Get the sequence for generating new [`StreamId`](crate::protocol::StreamId)s.
fn stream_id_sequence(&self) -> &Sequence;
/// Get the [`StreamManagerHandle`] for doing stream operations.
fn stream_manager_handle(&self) -> &StreamManagerHandle;
/// Prepare [`PipelineData`] to be written. This is called by `init_write_pipeline_data()` as
/// a hook so that values that need special handling can be taken care of.
fn prepare_pipeline_data(
&self,
data: PipelineData,
context: &Self::DataContext,
) -> Result<PipelineData, ShellError>;
/// Initialize a write for [`PipelineData`]. This returns two parts: the header, which can be
/// embedded in the particular message that references the stream, and a writer, which will
/// write out all of the data in the pipeline when `.write()` is called.
///
/// Note that not all [`PipelineData`] starts a stream. You should call `write()` anyway, as
/// it will automatically handle this case.
///
/// This method is provided for implementors to use.
fn init_write_pipeline_data(
&self,
data: PipelineData,
context: &Self::DataContext,
) -> Result<(PipelineDataHeader, PipelineDataWriter<Self>), ShellError> {
// Allocate a stream id and a writer
let new_stream = |high_pressure_mark: i32| {
// Get a free stream id
let id = self.stream_id_sequence().next()?;
// Create the writer
let writer =
self.stream_manager_handle()
.write_stream(id, self.clone(), high_pressure_mark)?;
Ok::<_, ShellError>((id, writer))
};
match self.prepare_pipeline_data(data, context)? {
PipelineData::Value(value, _) => {
Ok((PipelineDataHeader::Value(value), PipelineDataWriter::None))
}
PipelineData::Empty => Ok((PipelineDataHeader::Empty, PipelineDataWriter::None)),
PipelineData::ListStream(stream, _) => {
let (id, writer) = new_stream(LIST_STREAM_HIGH_PRESSURE)?;
Ok((
PipelineDataHeader::ListStream(ListStreamInfo { id }),
PipelineDataWriter::ListStream(writer, stream),
))
}
PipelineData::ExternalStream {
stdout,
stderr,
exit_code,
span,
metadata: _,
trim_end_newline,
} => {
// Create the writers and stream ids
let stdout_stream = stdout
.is_some()
.then(|| new_stream(RAW_STREAM_HIGH_PRESSURE))
.transpose()?;
let stderr_stream = stderr
.is_some()
.then(|| new_stream(RAW_STREAM_HIGH_PRESSURE))
.transpose()?;
let exit_code_stream = exit_code
.is_some()
.then(|| new_stream(LIST_STREAM_HIGH_PRESSURE))
.transpose()?;
// Generate the header, with the stream ids
let header = PipelineDataHeader::ExternalStream(ExternalStreamInfo {
span,
stdout: stdout
.as_ref()
.zip(stdout_stream.as_ref())
.map(|(stream, (id, _))| RawStreamInfo::new(*id, stream)),
stderr: stderr
.as_ref()
.zip(stderr_stream.as_ref())
.map(|(stream, (id, _))| RawStreamInfo::new(*id, stream)),
exit_code: exit_code_stream
.as_ref()
.map(|&(id, _)| ListStreamInfo { id }),
trim_end_newline,
});
// Collect the writers
let writer = PipelineDataWriter::ExternalStream {
stdout: stdout_stream.map(|(_, writer)| writer).zip(stdout),
stderr: stderr_stream.map(|(_, writer)| writer).zip(stderr),
exit_code: exit_code_stream.map(|(_, writer)| writer).zip(exit_code),
};
Ok((header, writer))
}
}
}
}
impl<T> WriteStreamMessage for T
where
T: Interface,
{
fn write_stream_message(&mut self, msg: StreamMessage) -> Result<(), ShellError> {
self.write(msg.into())
}
fn flush(&mut self) -> Result<(), ShellError> {
<Self as Interface>::flush(self)
}
}
/// Completes the write operation for a [`PipelineData`]. You must call
/// [`PipelineDataWriter::write()`] to write all of the data contained within the streams.
#[derive(Default)]
#[must_use]
pub enum PipelineDataWriter<W: WriteStreamMessage> {
#[default]
None,
ListStream(StreamWriter<W>, ListStream),
ExternalStream {
stdout: Option<(StreamWriter<W>, RawStream)>,
stderr: Option<(StreamWriter<W>, RawStream)>,
exit_code: Option<(StreamWriter<W>, ListStream)>,
},
}
impl<W> PipelineDataWriter<W>
where
W: WriteStreamMessage + Send + 'static,
{
/// Write all of the data in each of the streams. This method waits for completion.
pub(crate) fn write(self) -> Result<(), ShellError> {
match self {
// If no stream was contained in the PipelineData, do nothing.
PipelineDataWriter::None => Ok(()),
// Write a list stream.
PipelineDataWriter::ListStream(mut writer, stream) => {
writer.write_all(stream)?;
Ok(())
}
// Write all three possible streams of an ExternalStream on separate threads.
PipelineDataWriter::ExternalStream {
stdout,
stderr,
exit_code,
} => {
thread::scope(|scope| {
let stderr_thread = stderr
.map(|(mut writer, stream)| {
thread::Builder::new()
.name("plugin stderr writer".into())
.spawn_scoped(scope, move || {
writer.write_all(raw_stream_iter(stream))
})
})
.transpose()?;
let exit_code_thread = exit_code
.map(|(mut writer, stream)| {
thread::Builder::new()
.name("plugin exit_code writer".into())
.spawn_scoped(scope, move || writer.write_all(stream))
})
.transpose()?;
// Optimize for stdout: if only stdout is present, don't spawn any other
// threads.
if let Some((mut writer, stream)) = stdout {
writer.write_all(raw_stream_iter(stream))?;
}
let panicked = |thread_name: &str| {
Err(ShellError::NushellFailed {
msg: format!(
"{thread_name} thread panicked in PipelineDataWriter::write"
),
})
};
stderr_thread
.map(|t| t.join().unwrap_or_else(|_| panicked("stderr")))
.transpose()?;
exit_code_thread
.map(|t| t.join().unwrap_or_else(|_| panicked("exit_code")))
.transpose()?;
Ok(())
})
}
}
}
/// Write all of the data in each of the streams. This method returns immediately; any necessary
/// write will happen in the background. If a thread was spawned, its handle is returned.
pub(crate) fn write_background(
self,
) -> Result<Option<thread::JoinHandle<Result<(), ShellError>>>, ShellError> {
match self {
PipelineDataWriter::None => Ok(None),
_ => Ok(Some(
thread::Builder::new()
.name("plugin stream background writer".into())
.spawn(move || {
let result = self.write();
if let Err(ref err) = result {
// Assume that the background thread error probably won't be handled and log it
// here just in case.
log::warn!("Error while writing pipeline in background: {err}");
}
result
})?,
)),
}
}
}
/// Custom iterator for [`RawStream`] that respects ctrlc, but still has binary chunks
fn raw_stream_iter(stream: RawStream) -> impl Iterator<Item = Result<Vec<u8>, ShellError>> {
let ctrlc = stream.ctrlc;
stream
.stream
.take_while(move |_| ctrlc.as_ref().map(|b| !b.load(Relaxed)).unwrap_or(true))
}

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,14 @@
//! Interface used by the plugin to communicate with the engine.
use super::{
stream::{StreamManager, StreamManagerHandle},
Interface, InterfaceManager, PipelineDataWriter, PluginRead, PluginWrite, Sequence,
use nu_plugin_core::{
util::{Sequence, Waitable, WaitableMut},
Interface, InterfaceManager, PipelineDataWriter, PluginRead, PluginWrite, StreamManager,
StreamManagerHandle,
};
use crate::{
protocol::{
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, Ordering,
PluginCall, PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption,
PluginOutput, ProtocolInfo,
},
util::{Waitable, WaitableMut},
use nu_plugin_protocol::{
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, Ordering, PluginCall,
PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption, PluginOutput,
ProtocolInfo,
};
use nu_protocol::{
engine::Closure, Config, IntoInterruptiblePipelineData, LabeledError, ListStream, PipelineData,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,625 +0,0 @@
use crate::protocol::{StreamData, StreamId, StreamMessage};
use nu_protocol::{ShellError, Span, Value};
use std::{
collections::{btree_map, BTreeMap},
iter::FusedIterator,
marker::PhantomData,
sync::{mpsc, Arc, Condvar, Mutex, MutexGuard, Weak},
};
#[cfg(test)]
mod tests;
/// Receives messages from a stream read from input by a [`StreamManager`].
///
/// The receiver reads for messages of type `Result<Option<StreamData>, ShellError>` from the
/// channel, which is managed by a [`StreamManager`]. Signalling for end-of-stream is explicit
/// through `Ok(Some)`.
///
/// Failing to receive is an error. When end-of-stream is received, the `receiver` is set to `None`
/// and all further calls to `next()` return `None`.
///
/// The type `T` must implement [`FromShellError`], so that errors in the stream can be represented,
/// and `TryFrom<StreamData>` to convert it to the correct type.
///
/// For each message read, it sends [`StreamMessage::Ack`] to the writer. When dropped,
/// it sends [`StreamMessage::Drop`].
#[derive(Debug)]
pub(crate) struct StreamReader<T, W>
where
W: WriteStreamMessage,
{
id: StreamId,
receiver: Option<mpsc::Receiver<Result<Option<StreamData>, ShellError>>>,
writer: W,
/// Iterator requires the item type to be fixed, so we have to keep it as part of the type,
/// even though we're actually receiving dynamic data.
marker: PhantomData<fn() -> T>,
}
impl<T, W> StreamReader<T, W>
where
T: TryFrom<StreamData, Error = ShellError>,
W: WriteStreamMessage,
{
/// Create a new StreamReader from parts
pub(crate) fn new(
id: StreamId,
receiver: mpsc::Receiver<Result<Option<StreamData>, ShellError>>,
writer: W,
) -> StreamReader<T, W> {
StreamReader {
id,
receiver: Some(receiver),
writer,
marker: PhantomData,
}
}
/// Receive a message from the channel, or return an error if:
///
/// * the channel couldn't be received from
/// * an error was sent on the channel
/// * the message received couldn't be converted to `T`
pub(crate) fn recv(&mut self) -> Result<Option<T>, ShellError> {
let connection_lost = || ShellError::GenericError {
error: "Stream ended unexpectedly".into(),
msg: "connection lost before explicit end of stream".into(),
span: None,
help: None,
inner: vec![],
};
if let Some(ref rx) = self.receiver {
// Try to receive a message first
let msg = match rx.try_recv() {
Ok(msg) => msg?,
Err(mpsc::TryRecvError::Empty) => {
// The receiver doesn't have any messages waiting for us. It's possible that the
// other side hasn't seen our acknowledgements. Let's flush the writer and then
// wait
self.writer.flush()?;
rx.recv().map_err(|_| connection_lost())??
}
Err(mpsc::TryRecvError::Disconnected) => return Err(connection_lost()),
};
if let Some(data) = msg {
// Acknowledge the message
self.writer
.write_stream_message(StreamMessage::Ack(self.id))?;
// Try to convert it into the correct type
Ok(Some(data.try_into()?))
} else {
// Remove the receiver, so that future recv() calls always return Ok(None)
self.receiver = None;
Ok(None)
}
} else {
// Closed already
Ok(None)
}
}
}
impl<T, W> Iterator for StreamReader<T, W>
where
T: FromShellError + TryFrom<StreamData, Error = ShellError>,
W: WriteStreamMessage,
{
type Item = T;
fn next(&mut self) -> Option<T> {
// Converting the error to the value here makes the implementation a lot easier
match self.recv() {
Ok(option) => option,
Err(err) => {
// Drop the receiver so we don't keep returning errors
self.receiver = None;
Some(T::from_shell_error(err))
}
}
}
}
// Guaranteed not to return anything after the end
impl<T, W> FusedIterator for StreamReader<T, W>
where
T: FromShellError + TryFrom<StreamData, Error = ShellError>,
W: WriteStreamMessage,
{
}
impl<T, W> Drop for StreamReader<T, W>
where
W: WriteStreamMessage,
{
fn drop(&mut self) {
if let Err(err) = self
.writer
.write_stream_message(StreamMessage::Drop(self.id))
.and_then(|_| self.writer.flush())
{
log::warn!("Failed to send message to drop stream: {err}");
}
}
}
/// Values that can contain a `ShellError` to signal an error has occurred.
pub(crate) trait FromShellError {
fn from_shell_error(err: ShellError) -> Self;
}
// For List streams.
impl FromShellError for Value {
fn from_shell_error(err: ShellError) -> Self {
Value::error(err, Span::unknown())
}
}
// For Raw streams, mostly.
impl<T> FromShellError for Result<T, ShellError> {
fn from_shell_error(err: ShellError) -> Self {
Err(err)
}
}
/// Writes messages to a stream, with flow control.
///
/// The `signal` contained
#[derive(Debug)]
pub struct StreamWriter<W: WriteStreamMessage> {
id: StreamId,
signal: Arc<StreamWriterSignal>,
writer: W,
ended: bool,
}
impl<W> StreamWriter<W>
where
W: WriteStreamMessage,
{
pub(crate) fn new(id: StreamId, signal: Arc<StreamWriterSignal>, writer: W) -> StreamWriter<W> {
StreamWriter {
id,
signal,
writer,
ended: false,
}
}
/// Check if the stream was dropped from the other end. Recommended to do this before calling
/// [`.write()`], especially in a loop.
pub(crate) fn is_dropped(&self) -> Result<bool, ShellError> {
self.signal.is_dropped()
}
/// Write a single piece of data to the stream.
///
/// Error if something failed with the write, or if [`.end()`] was already called
/// previously.
pub(crate) fn write(&mut self, data: impl Into<StreamData>) -> Result<(), ShellError> {
if !self.ended {
self.writer
.write_stream_message(StreamMessage::Data(self.id, data.into()))?;
// This implements flow control, so we don't write too many messages:
if !self.signal.notify_sent()? {
// Flush the output, and then wait for acknowledgements
self.writer.flush()?;
self.signal.wait_for_drain()
} else {
Ok(())
}
} else {
Err(ShellError::GenericError {
error: "Wrote to a stream after it ended".into(),
msg: format!(
"tried to write to stream {} after it was already ended",
self.id
),
span: None,
help: Some("this may be a bug in the nu-plugin crate".into()),
inner: vec![],
})
}
}
/// Write a full iterator to the stream. Note that this doesn't end the stream, so you should
/// still call [`.end()`].
///
/// If the stream is dropped from the other end, the iterator will not be fully consumed, and
/// writing will terminate.
///
/// Returns `Ok(true)` if the iterator was fully consumed, or `Ok(false)` if a drop interrupted
/// the stream from the other side.
pub(crate) fn write_all<T>(
&mut self,
data: impl IntoIterator<Item = T>,
) -> Result<bool, ShellError>
where
T: Into<StreamData>,
{
// Check before starting
if self.is_dropped()? {
return Ok(false);
}
for item in data {
// Check again after each item is consumed from the iterator, just in case the iterator
// takes a while to produce a value
if self.is_dropped()? {
return Ok(false);
}
self.write(item)?;
}
Ok(true)
}
/// End the stream. Recommend doing this instead of relying on `Drop` so that you can catch the
/// error.
pub(crate) fn end(&mut self) -> Result<(), ShellError> {
if !self.ended {
// Set the flag first so we don't double-report in the Drop
self.ended = true;
self.writer
.write_stream_message(StreamMessage::End(self.id))?;
self.writer.flush()
} else {
Ok(())
}
}
}
impl<W> Drop for StreamWriter<W>
where
W: WriteStreamMessage,
{
fn drop(&mut self) {
// Make sure we ended the stream
if let Err(err) = self.end() {
log::warn!("Error while ending stream in Drop for StreamWriter: {err}");
}
}
}
/// Stores stream state for a writer, and can be blocked on to wait for messages to be acknowledged.
/// A key part of managing stream lifecycle and flow control.
#[derive(Debug)]
pub(crate) struct StreamWriterSignal {
mutex: Mutex<StreamWriterSignalState>,
change_cond: Condvar,
}
#[derive(Debug)]
pub(crate) struct StreamWriterSignalState {
/// Stream has been dropped and consumer is no longer interested in any messages.
dropped: bool,
/// Number of messages that have been sent without acknowledgement.
unacknowledged: i32,
/// Max number of messages to send before waiting for acknowledgement.
high_pressure_mark: i32,
}
impl StreamWriterSignal {
/// Create a new signal.
///
/// If `notify_sent()` is called more than `high_pressure_mark` times, it will wait until
/// `notify_acknowledge()` is called by another thread enough times to bring the number of
/// unacknowledged sent messages below that threshold.
pub(crate) fn new(high_pressure_mark: i32) -> StreamWriterSignal {
assert!(high_pressure_mark > 0);
StreamWriterSignal {
mutex: Mutex::new(StreamWriterSignalState {
dropped: false,
unacknowledged: 0,
high_pressure_mark,
}),
change_cond: Condvar::new(),
}
}
fn lock(&self) -> Result<MutexGuard<StreamWriterSignalState>, ShellError> {
self.mutex.lock().map_err(|_| ShellError::NushellFailed {
msg: "StreamWriterSignal mutex poisoned due to panic".into(),
})
}
/// True if the stream was dropped and the consumer is no longer interested in it. Indicates
/// that no more messages should be sent, other than `End`.
pub(crate) fn is_dropped(&self) -> Result<bool, ShellError> {
Ok(self.lock()?.dropped)
}
/// Notify the writers that the stream has been dropped, so they can stop writing.
pub(crate) fn set_dropped(&self) -> Result<(), ShellError> {
let mut state = self.lock()?;
state.dropped = true;
// Unblock the writers so they can terminate
self.change_cond.notify_all();
Ok(())
}
/// Track that a message has been sent. Returns `Ok(true)` if more messages can be sent,
/// or `Ok(false)` if the high pressure mark has been reached and [`.wait_for_drain()`] should
/// be called to block.
pub(crate) fn notify_sent(&self) -> Result<bool, ShellError> {
let mut state = self.lock()?;
state.unacknowledged =
state
.unacknowledged
.checked_add(1)
.ok_or_else(|| ShellError::NushellFailed {
msg: "Overflow in counter: too many unacknowledged messages".into(),
})?;
Ok(state.unacknowledged < state.high_pressure_mark)
}
/// Wait for acknowledgements before sending more data. Also returns if the stream is dropped.
pub(crate) fn wait_for_drain(&self) -> Result<(), ShellError> {
let mut state = self.lock()?;
while !state.dropped && state.unacknowledged >= state.high_pressure_mark {
state = self
.change_cond
.wait(state)
.map_err(|_| ShellError::NushellFailed {
msg: "StreamWriterSignal mutex poisoned due to panic".into(),
})?;
}
Ok(())
}
/// Notify the writers that a message has been acknowledged, so they can continue to write
/// if they were waiting.
pub(crate) fn notify_acknowledged(&self) -> Result<(), ShellError> {
let mut state = self.lock()?;
state.unacknowledged =
state
.unacknowledged
.checked_sub(1)
.ok_or_else(|| ShellError::NushellFailed {
msg: "Underflow in counter: too many message acknowledgements".into(),
})?;
// Unblock the writer
self.change_cond.notify_one();
Ok(())
}
}
/// A sink for a [`StreamMessage`]
pub trait WriteStreamMessage {
fn write_stream_message(&mut self, msg: StreamMessage) -> Result<(), ShellError>;
fn flush(&mut self) -> Result<(), ShellError>;
}
#[derive(Debug, Default)]
struct StreamManagerState {
reading_streams: BTreeMap<StreamId, mpsc::Sender<Result<Option<StreamData>, ShellError>>>,
writing_streams: BTreeMap<StreamId, Weak<StreamWriterSignal>>,
}
impl StreamManagerState {
/// Lock the state, or return a [`ShellError`] if the mutex is poisoned.
fn lock(
state: &Mutex<StreamManagerState>,
) -> Result<MutexGuard<StreamManagerState>, ShellError> {
state.lock().map_err(|_| ShellError::NushellFailed {
msg: "StreamManagerState mutex poisoned due to a panic".into(),
})
}
}
#[derive(Debug)]
pub struct StreamManager {
state: Arc<Mutex<StreamManagerState>>,
}
impl StreamManager {
/// Create a new StreamManager.
pub(crate) fn new() -> StreamManager {
StreamManager {
state: Default::default(),
}
}
fn lock(&self) -> Result<MutexGuard<StreamManagerState>, ShellError> {
StreamManagerState::lock(&self.state)
}
/// Create a new handle to the StreamManager for registering streams.
pub(crate) fn get_handle(&self) -> StreamManagerHandle {
StreamManagerHandle {
state: Arc::downgrade(&self.state),
}
}
/// Process a stream message, and update internal state accordingly.
pub(crate) fn handle_message(&self, message: StreamMessage) -> Result<(), ShellError> {
let mut state = self.lock()?;
match message {
StreamMessage::Data(id, data) => {
if let Some(sender) = state.reading_streams.get(&id) {
// We should ignore the error on send. This just means the reader has dropped,
// but it will have sent a Drop message to the other side, and we will receive
// an End message at which point we can remove the channel.
let _ = sender.send(Ok(Some(data)));
Ok(())
} else {
Err(ShellError::PluginFailedToDecode {
msg: format!("received Data for unknown stream {id}"),
})
}
}
StreamMessage::End(id) => {
if let Some(sender) = state.reading_streams.remove(&id) {
// We should ignore the error on the send, because the reader might have dropped
// already
let _ = sender.send(Ok(None));
Ok(())
} else {
Err(ShellError::PluginFailedToDecode {
msg: format!("received End for unknown stream {id}"),
})
}
}
StreamMessage::Drop(id) => {
if let Some(signal) = state.writing_streams.remove(&id) {
if let Some(signal) = signal.upgrade() {
// This will wake blocked writers so they can stop writing, so it's ok
signal.set_dropped()?;
}
}
// It's possible that the stream has already finished writing and we don't have it
// anymore, so we fall through to Ok
Ok(())
}
StreamMessage::Ack(id) => {
if let Some(signal) = state.writing_streams.get(&id) {
if let Some(signal) = signal.upgrade() {
// This will wake up a blocked writer
signal.notify_acknowledged()?;
} else {
// We know it doesn't exist, so might as well remove it
state.writing_streams.remove(&id);
}
}
// It's possible that the stream has already finished writing and we don't have it
// anymore, so we fall through to Ok
Ok(())
}
}
}
/// Broadcast an error to all stream readers. This is useful for error propagation.
pub(crate) fn broadcast_read_error(&self, error: ShellError) -> Result<(), ShellError> {
let state = self.lock()?;
for channel in state.reading_streams.values() {
// Ignore send errors.
let _ = channel.send(Err(error.clone()));
}
Ok(())
}
// If the `StreamManager` is dropped, we should let all of the stream writers know that they
// won't be able to write anymore. We don't need to do anything about the readers though
// because they'll know when the `Sender` is dropped automatically
fn drop_all_writers(&self) -> Result<(), ShellError> {
let mut state = self.lock()?;
let writers = std::mem::take(&mut state.writing_streams);
for (_, signal) in writers {
if let Some(signal) = signal.upgrade() {
// more important that we send to all than handling an error
let _ = signal.set_dropped();
}
}
Ok(())
}
}
impl Drop for StreamManager {
fn drop(&mut self) {
if let Err(err) = self.drop_all_writers() {
log::warn!("error during Drop for StreamManager: {}", err)
}
}
}
/// A [`StreamManagerHandle`] supports operations for interacting with the [`StreamManager`].
///
/// Streams can be registered for reading, returning a [`StreamReader`], or for writing, returning
/// a [`StreamWriter`].
#[derive(Debug, Clone)]
pub struct StreamManagerHandle {
state: Weak<Mutex<StreamManagerState>>,
}
impl StreamManagerHandle {
/// Because the handle only has a weak reference to the [`StreamManager`] state, we have to
/// first try to upgrade to a strong reference and then lock. This function wraps those two
/// operations together, handling errors appropriately.
fn with_lock<T, F>(&self, f: F) -> Result<T, ShellError>
where
F: FnOnce(MutexGuard<StreamManagerState>) -> Result<T, ShellError>,
{
let upgraded = self
.state
.upgrade()
.ok_or_else(|| ShellError::NushellFailed {
msg: "StreamManager is no longer alive".into(),
})?;
let guard = upgraded.lock().map_err(|_| ShellError::NushellFailed {
msg: "StreamManagerState mutex poisoned due to a panic".into(),
})?;
f(guard)
}
/// Register a new stream for reading, and return a [`StreamReader`] that can be used to iterate
/// on the values received. A [`StreamMessage`] writer is required for writing control messages
/// back to the producer.
pub(crate) fn read_stream<T, W>(
&self,
id: StreamId,
writer: W,
) -> Result<StreamReader<T, W>, ShellError>
where
T: TryFrom<StreamData, Error = ShellError>,
W: WriteStreamMessage,
{
let (tx, rx) = mpsc::channel();
self.with_lock(|mut state| {
// Must be exclusive
if let btree_map::Entry::Vacant(e) = state.reading_streams.entry(id) {
e.insert(tx);
Ok(())
} else {
Err(ShellError::GenericError {
error: format!("Failed to acquire reader for stream {id}"),
msg: "tried to get a reader for a stream that's already being read".into(),
span: None,
help: Some("this may be a bug in the nu-plugin crate".into()),
inner: vec![],
})
}
})?;
Ok(StreamReader::new(id, rx, writer))
}
/// Register a new stream for writing, and return a [`StreamWriter`] that can be used to send
/// data to the stream.
///
/// The `high_pressure_mark` value controls how many messages can be written without receiving
/// an acknowledgement before any further attempts to write will wait for the consumer to
/// acknowledge them. This prevents overwhelming the reader.
pub(crate) fn write_stream<W>(
&self,
id: StreamId,
writer: W,
high_pressure_mark: i32,
) -> Result<StreamWriter<W>, ShellError>
where
W: WriteStreamMessage,
{
let signal = Arc::new(StreamWriterSignal::new(high_pressure_mark));
self.with_lock(|mut state| {
// Remove dead writing streams
state
.writing_streams
.retain(|_, signal| signal.strong_count() > 0);
// Must be exclusive
if let btree_map::Entry::Vacant(e) = state.writing_streams.entry(id) {
e.insert(Arc::downgrade(&signal));
Ok(())
} else {
Err(ShellError::GenericError {
error: format!("Failed to acquire writer for stream {id}"),
msg: "tried to get a writer for a stream that's already being written".into(),
span: None,
help: Some("this may be a bug in the nu-plugin crate".into()),
inner: vec![],
})
}
})?;
Ok(StreamWriter::new(id, signal, writer))
}
}

View File

@ -1,550 +0,0 @@
use std::{
sync::{
atomic::{AtomicBool, Ordering::Relaxed},
mpsc, Arc,
},
time::{Duration, Instant},
};
use super::{StreamManager, StreamReader, StreamWriter, StreamWriterSignal, WriteStreamMessage};
use crate::protocol::{StreamData, StreamMessage};
use nu_protocol::{ShellError, Value};
// Should be long enough to definitely complete any quick operation, but not so long that tests are
// slow to complete. 10 ms is a pretty long time
const WAIT_DURATION: Duration = Duration::from_millis(10);
// Maximum time to wait for a condition to be true
const MAX_WAIT_DURATION: Duration = Duration::from_millis(500);
/// Wait for a condition to be true, or panic if the duration exceeds MAX_WAIT_DURATION
#[track_caller]
fn wait_for_condition(mut cond: impl FnMut() -> bool, message: &str) {
// Early check
if cond() {
return;
}
let start = Instant::now();
loop {
std::thread::sleep(Duration::from_millis(10));
if cond() {
return;
}
let elapsed = Instant::now().saturating_duration_since(start);
if elapsed > MAX_WAIT_DURATION {
panic!(
"{message}: Waited {:.2}sec, which is more than the maximum of {:.2}sec",
elapsed.as_secs_f64(),
MAX_WAIT_DURATION.as_secs_f64(),
);
}
}
}
#[derive(Debug, Clone, Default)]
struct TestSink(Vec<StreamMessage>);
impl WriteStreamMessage for TestSink {
fn write_stream_message(&mut self, msg: StreamMessage) -> Result<(), ShellError> {
self.0.push(msg);
Ok(())
}
fn flush(&mut self) -> Result<(), ShellError> {
Ok(())
}
}
impl WriteStreamMessage for mpsc::Sender<StreamMessage> {
fn write_stream_message(&mut self, msg: StreamMessage) -> Result<(), ShellError> {
self.send(msg).map_err(|err| ShellError::NushellFailed {
msg: err.to_string(),
})
}
fn flush(&mut self) -> Result<(), ShellError> {
Ok(())
}
}
#[test]
fn reader_recv_list_messages() -> Result<(), ShellError> {
let (tx, rx) = mpsc::channel();
let mut reader = StreamReader::new(0, rx, TestSink::default());
tx.send(Ok(Some(StreamData::List(Value::test_int(5)))))
.unwrap();
drop(tx);
assert_eq!(Some(Value::test_int(5)), reader.recv()?);
Ok(())
}
#[test]
fn list_reader_recv_wrong_type() -> Result<(), ShellError> {
let (tx, rx) = mpsc::channel();
let mut reader = StreamReader::<Value, _>::new(0, rx, TestSink::default());
tx.send(Ok(Some(StreamData::Raw(Ok(vec![10, 20])))))
.unwrap();
tx.send(Ok(Some(StreamData::List(Value::test_nothing()))))
.unwrap();
drop(tx);
reader.recv().expect_err("should be an error");
reader.recv().expect("should be able to recover");
Ok(())
}
#[test]
fn reader_recv_raw_messages() -> Result<(), ShellError> {
let (tx, rx) = mpsc::channel();
let mut reader =
StreamReader::<Result<Vec<u8>, ShellError>, _>::new(0, rx, TestSink::default());
tx.send(Ok(Some(StreamData::Raw(Ok(vec![10, 20])))))
.unwrap();
drop(tx);
assert_eq!(Some(vec![10, 20]), reader.recv()?.transpose()?);
Ok(())
}
#[test]
fn raw_reader_recv_wrong_type() -> Result<(), ShellError> {
let (tx, rx) = mpsc::channel();
let mut reader =
StreamReader::<Result<Vec<u8>, ShellError>, _>::new(0, rx, TestSink::default());
tx.send(Ok(Some(StreamData::List(Value::test_nothing()))))
.unwrap();
tx.send(Ok(Some(StreamData::Raw(Ok(vec![10, 20])))))
.unwrap();
drop(tx);
reader.recv().expect_err("should be an error");
reader.recv().expect("should be able to recover");
Ok(())
}
#[test]
fn reader_recv_acknowledge() -> Result<(), ShellError> {
let (tx, rx) = mpsc::channel();
let mut reader = StreamReader::<Value, _>::new(0, rx, TestSink::default());
tx.send(Ok(Some(StreamData::List(Value::test_int(5)))))
.unwrap();
tx.send(Ok(Some(StreamData::List(Value::test_int(6)))))
.unwrap();
drop(tx);
reader.recv()?;
reader.recv()?;
let wrote = &reader.writer.0;
assert!(wrote.len() >= 2);
assert!(
matches!(wrote[0], StreamMessage::Ack(0)),
"0 = {:?}",
wrote[0]
);
assert!(
matches!(wrote[1], StreamMessage::Ack(0)),
"1 = {:?}",
wrote[1]
);
Ok(())
}
#[test]
fn reader_recv_end_of_stream() -> Result<(), ShellError> {
let (tx, rx) = mpsc::channel();
let mut reader = StreamReader::<Value, _>::new(0, rx, TestSink::default());
tx.send(Ok(Some(StreamData::List(Value::test_int(5)))))
.unwrap();
tx.send(Ok(None)).unwrap();
drop(tx);
assert!(reader.recv()?.is_some(), "actual message");
assert!(reader.recv()?.is_none(), "on close");
assert!(reader.recv()?.is_none(), "after close");
Ok(())
}
#[test]
fn reader_iter_fuse_on_error() -> Result<(), ShellError> {
let (tx, rx) = mpsc::channel();
let mut reader = StreamReader::<Value, _>::new(0, rx, TestSink::default());
drop(tx); // should cause error, because we didn't explicitly signal the end
assert!(
reader.next().is_some_and(|e| e.is_error()),
"should be error the first time"
);
assert!(reader.next().is_none(), "should be closed the second time");
Ok(())
}
#[test]
fn reader_drop() {
let (_tx, rx) = mpsc::channel();
// Flag set if drop message is received.
struct Check(Arc<AtomicBool>);
impl WriteStreamMessage for Check {
fn write_stream_message(&mut self, msg: StreamMessage) -> Result<(), ShellError> {
assert!(matches!(msg, StreamMessage::Drop(1)), "got {:?}", msg);
self.0.store(true, Relaxed);
Ok(())
}
fn flush(&mut self) -> Result<(), ShellError> {
Ok(())
}
}
let flag = Arc::new(AtomicBool::new(false));
let reader = StreamReader::<Value, _>::new(1, rx, Check(flag.clone()));
drop(reader);
assert!(flag.load(Relaxed));
}
#[test]
fn writer_write_all_stops_if_dropped() -> Result<(), ShellError> {
let signal = Arc::new(StreamWriterSignal::new(20));
let id = 1337;
let mut writer = StreamWriter::new(id, signal.clone(), TestSink::default());
// Simulate this by having it consume a stream that will actually do the drop halfway through
let iter = (0..5).map(Value::test_int).chain({
let mut n = 5;
std::iter::from_fn(move || {
// produces numbers 5..10, but drops for the first one
if n == 5 {
signal.set_dropped().unwrap();
}
if n < 10 {
let value = Value::test_int(n);
n += 1;
Some(value)
} else {
None
}
})
});
writer.write_all(iter)?;
assert!(writer.is_dropped()?);
let wrote = &writer.writer.0;
assert_eq!(5, wrote.len(), "length wrong: {wrote:?}");
for (n, message) in (0..5).zip(wrote) {
match message {
StreamMessage::Data(msg_id, StreamData::List(value)) => {
assert_eq!(id, *msg_id, "id");
assert_eq!(Value::test_int(n), *value, "value");
}
other => panic!("unexpected message: {other:?}"),
}
}
Ok(())
}
#[test]
fn writer_end() -> Result<(), ShellError> {
let signal = Arc::new(StreamWriterSignal::new(20));
let mut writer = StreamWriter::new(9001, signal.clone(), TestSink::default());
writer.end()?;
writer
.write(Value::test_int(2))
.expect_err("shouldn't be able to write after end");
writer.end().expect("end twice should be ok");
let wrote = &writer.writer.0;
assert!(
matches!(wrote.last(), Some(StreamMessage::End(9001))),
"didn't write end message: {wrote:?}"
);
Ok(())
}
#[test]
fn signal_set_dropped() -> Result<(), ShellError> {
let signal = StreamWriterSignal::new(4);
assert!(!signal.is_dropped()?);
signal.set_dropped()?;
assert!(signal.is_dropped()?);
Ok(())
}
#[test]
fn signal_notify_sent_false_if_unacknowledged() -> Result<(), ShellError> {
let signal = StreamWriterSignal::new(2);
assert!(signal.notify_sent()?);
for _ in 0..100 {
assert!(!signal.notify_sent()?);
}
Ok(())
}
#[test]
fn signal_notify_sent_never_false_if_flowing() -> Result<(), ShellError> {
let signal = StreamWriterSignal::new(1);
for _ in 0..100 {
signal.notify_acknowledged()?;
}
for _ in 0..100 {
assert!(signal.notify_sent()?);
}
Ok(())
}
#[test]
fn signal_wait_for_drain_blocks_on_unacknowledged() -> Result<(), ShellError> {
let signal = StreamWriterSignal::new(50);
std::thread::scope(|scope| {
let spawned = scope.spawn(|| {
for _ in 0..100 {
if !signal.notify_sent()? {
signal.wait_for_drain()?;
}
}
Ok(())
});
std::thread::sleep(WAIT_DURATION);
assert!(!spawned.is_finished(), "didn't block");
for _ in 0..100 {
signal.notify_acknowledged()?;
}
wait_for_condition(|| spawned.is_finished(), "blocked at end");
spawned.join().unwrap()
})
}
#[test]
fn signal_wait_for_drain_unblocks_on_dropped() -> Result<(), ShellError> {
let signal = StreamWriterSignal::new(1);
std::thread::scope(|scope| {
let spawned = scope.spawn(|| {
while !signal.is_dropped()? {
if !signal.notify_sent()? {
signal.wait_for_drain()?;
}
}
Ok(())
});
std::thread::sleep(WAIT_DURATION);
assert!(!spawned.is_finished(), "didn't block");
signal.set_dropped()?;
wait_for_condition(|| spawned.is_finished(), "still blocked at end");
spawned.join().unwrap()
})
}
#[test]
fn stream_manager_single_stream_read_scenario() -> Result<(), ShellError> {
let manager = StreamManager::new();
let handle = manager.get_handle();
let (tx, rx) = mpsc::channel();
let readable = handle.read_stream::<Value, _>(2, tx)?;
let expected_values = vec![Value::test_int(40), Value::test_string("hello")];
for value in &expected_values {
manager.handle_message(StreamMessage::Data(2, value.clone().into()))?;
}
manager.handle_message(StreamMessage::End(2))?;
let values = readable.collect::<Vec<Value>>();
assert_eq!(expected_values, values);
// Now check the sent messages on consumption
// Should be Ack for each message, then Drop
for _ in &expected_values {
match rx.try_recv().expect("failed to receive Ack") {
StreamMessage::Ack(2) => (),
other => panic!("should have been an Ack: {other:?}"),
}
}
match rx.try_recv().expect("failed to receive Drop") {
StreamMessage::Drop(2) => (),
other => panic!("should have been a Drop: {other:?}"),
}
Ok(())
}
#[test]
fn stream_manager_multi_stream_read_scenario() -> Result<(), ShellError> {
let manager = StreamManager::new();
let handle = manager.get_handle();
let (tx, rx) = mpsc::channel();
let readable_list = handle.read_stream::<Value, _>(2, tx.clone())?;
let readable_raw = handle.read_stream::<Result<Vec<u8>, _>, _>(3, tx)?;
let expected_values = (1..100).map(Value::test_int).collect::<Vec<_>>();
let expected_raw_buffers = (1..100).map(|n| vec![n]).collect::<Vec<Vec<u8>>>();
for (value, buf) in expected_values.iter().zip(&expected_raw_buffers) {
manager.handle_message(StreamMessage::Data(2, value.clone().into()))?;
manager.handle_message(StreamMessage::Data(3, StreamData::Raw(Ok(buf.clone()))))?;
}
manager.handle_message(StreamMessage::End(2))?;
manager.handle_message(StreamMessage::End(3))?;
let values = readable_list.collect::<Vec<Value>>();
let bufs = readable_raw.collect::<Result<Vec<Vec<u8>>, _>>()?;
for (expected_value, value) in expected_values.iter().zip(&values) {
assert_eq!(expected_value, value, "in List stream");
}
for (expected_buf, buf) in expected_raw_buffers.iter().zip(&bufs) {
assert_eq!(expected_buf, buf, "in Raw stream");
}
// Now check the sent messages on consumption
// Should be Ack for each message, then Drop
for _ in &expected_values {
match rx.try_recv().expect("failed to receive Ack") {
StreamMessage::Ack(2) => (),
other => panic!("should have been an Ack(2): {other:?}"),
}
}
match rx.try_recv().expect("failed to receive Drop") {
StreamMessage::Drop(2) => (),
other => panic!("should have been a Drop(2): {other:?}"),
}
for _ in &expected_values {
match rx.try_recv().expect("failed to receive Ack") {
StreamMessage::Ack(3) => (),
other => panic!("should have been an Ack(3): {other:?}"),
}
}
match rx.try_recv().expect("failed to receive Drop") {
StreamMessage::Drop(3) => (),
other => panic!("should have been a Drop(3): {other:?}"),
}
// Should be end of stream
assert!(
rx.try_recv().is_err(),
"more messages written to stream than expected"
);
Ok(())
}
#[test]
fn stream_manager_write_scenario() -> Result<(), ShellError> {
let manager = StreamManager::new();
let handle = manager.get_handle();
let (tx, rx) = mpsc::channel();
let mut writable = handle.write_stream(4, tx, 100)?;
let expected_values = vec![b"hello".to_vec(), b"world".to_vec(), b"test".to_vec()];
for value in &expected_values {
writable.write(Ok::<_, ShellError>(value.clone()))?;
}
// Now try signalling ack
assert_eq!(
expected_values.len() as i32,
writable.signal.lock()?.unacknowledged,
"unacknowledged initial count",
);
manager.handle_message(StreamMessage::Ack(4))?;
assert_eq!(
expected_values.len() as i32 - 1,
writable.signal.lock()?.unacknowledged,
"unacknowledged post-Ack count",
);
// ...and Drop
manager.handle_message(StreamMessage::Drop(4))?;
assert!(writable.is_dropped()?);
// Drop the StreamWriter...
drop(writable);
// now check what was actually written
for value in &expected_values {
match rx.try_recv().expect("failed to receive Data") {
StreamMessage::Data(4, StreamData::Raw(Ok(received))) => {
assert_eq!(*value, received);
}
other @ StreamMessage::Data(..) => panic!("wrong Data for {value:?}: {other:?}"),
other => panic!("should have been Data: {other:?}"),
}
}
match rx.try_recv().expect("failed to receive End") {
StreamMessage::End(4) => (),
other => panic!("should have been End: {other:?}"),
}
Ok(())
}
#[test]
fn stream_manager_broadcast_read_error() -> Result<(), ShellError> {
let manager = StreamManager::new();
let handle = manager.get_handle();
let mut readable0 = handle.read_stream::<Value, _>(0, TestSink::default())?;
let mut readable1 = handle.read_stream::<Result<Vec<u8>, _>, _>(1, TestSink::default())?;
let error = ShellError::PluginFailedToDecode {
msg: "test decode error".into(),
};
manager.broadcast_read_error(error.clone())?;
drop(manager);
assert_eq!(
error.to_string(),
readable0
.recv()
.transpose()
.expect("nothing received from readable0")
.expect_err("not an error received from readable0")
.to_string()
);
assert_eq!(
error.to_string(),
readable1
.next()
.expect("nothing received from readable1")
.expect_err("not an error received from readable1")
.to_string()
);
Ok(())
}
#[test]
fn stream_manager_drop_writers_on_drop() -> Result<(), ShellError> {
let manager = StreamManager::new();
let handle = manager.get_handle();
let writable = handle.write_stream(4, TestSink::default(), 100)?;
assert!(!writable.is_dropped()?);
drop(manager);
assert!(writable.is_dropped()?);
Ok(())
}

View File

@ -1,140 +0,0 @@
use super::{EngineInterfaceManager, PluginInterfaceManager, PluginRead, PluginWrite};
use crate::{plugin::PluginSource, protocol::PluginInput, PluginOutput};
use nu_protocol::ShellError;
use std::{
collections::VecDeque,
sync::{Arc, Mutex},
};
/// Mock read/write helper for the engine and plugin interfaces.
#[derive(Debug, Clone)]
pub(crate) struct TestCase<I, O> {
r#in: Arc<Mutex<TestData<I>>>,
out: Arc<Mutex<TestData<O>>>,
}
#[derive(Debug)]
pub(crate) struct TestData<T> {
data: VecDeque<T>,
error: Option<ShellError>,
flushed: bool,
}
impl<T> Default for TestData<T> {
fn default() -> Self {
TestData {
data: VecDeque::new(),
error: None,
flushed: false,
}
}
}
impl<I, O> PluginRead<I> for TestCase<I, O> {
fn read(&mut self) -> Result<Option<I>, ShellError> {
let mut lock = self.r#in.lock().unwrap();
if let Some(err) = lock.error.take() {
Err(err)
} else {
Ok(lock.data.pop_front())
}
}
}
impl<I, O> PluginWrite<O> for TestCase<I, O>
where
I: Send + Clone,
O: Send + Clone,
{
fn write(&self, data: &O) -> Result<(), ShellError> {
let mut lock = self.out.lock().unwrap();
lock.flushed = false;
if let Some(err) = lock.error.take() {
Err(err)
} else {
lock.data.push_back(data.clone());
Ok(())
}
}
fn flush(&self) -> Result<(), ShellError> {
let mut lock = self.out.lock().unwrap();
lock.flushed = true;
Ok(())
}
}
#[allow(dead_code)]
impl<I, O> TestCase<I, O> {
pub(crate) fn new() -> TestCase<I, O> {
TestCase {
r#in: Default::default(),
out: Default::default(),
}
}
/// Clear the read buffer.
pub(crate) fn clear(&self) {
self.r#in.lock().unwrap().data.truncate(0);
}
/// Add input that will be read by the interface.
pub(crate) fn add(&self, input: impl Into<I>) {
self.r#in.lock().unwrap().data.push_back(input.into());
}
/// Add multiple inputs that will be read by the interface.
pub(crate) fn extend(&self, inputs: impl IntoIterator<Item = I>) {
self.r#in.lock().unwrap().data.extend(inputs);
}
/// Return an error from the next read operation.
pub(crate) fn set_read_error(&self, err: ShellError) {
self.r#in.lock().unwrap().error = Some(err);
}
/// Return an error from the next write operation.
pub(crate) fn set_write_error(&self, err: ShellError) {
self.out.lock().unwrap().error = Some(err);
}
/// Get the next output that was written.
pub(crate) fn next_written(&self) -> Option<O> {
self.out.lock().unwrap().data.pop_front()
}
/// Iterator over written data.
pub(crate) fn written(&self) -> impl Iterator<Item = O> + '_ {
std::iter::from_fn(|| self.next_written())
}
/// Returns true if the writer was flushed after the last write operation.
pub(crate) fn was_flushed(&self) -> bool {
self.out.lock().unwrap().flushed
}
/// Returns true if the reader has unconsumed reads.
pub(crate) fn has_unconsumed_read(&self) -> bool {
!self.r#in.lock().unwrap().data.is_empty()
}
/// Returns true if the writer has unconsumed writes.
pub(crate) fn has_unconsumed_write(&self) -> bool {
!self.out.lock().unwrap().data.is_empty()
}
}
impl TestCase<PluginOutput, PluginInput> {
/// Create a new [`PluginInterfaceManager`] that writes to this test case.
pub(crate) fn plugin(&self, name: &str) -> PluginInterfaceManager {
PluginInterfaceManager::new(PluginSource::new_fake(name).into(), None, self.clone())
}
}
impl TestCase<PluginInput, PluginOutput> {
/// Create a new [`EngineInterfaceManager`] that writes to this test case.
pub(crate) fn engine(&self) -> EngineInterfaceManager {
EngineInterfaceManager::new(self.clone())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,249 +1,36 @@
use crate::{
plugin::interface::ReceivedPluginCall,
protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput},
EncodingType,
};
use std::{
cmp::Ordering,
collections::HashMap,
env,
ffi::OsString,
io::{BufReader, BufWriter},
ops::Deref,
panic::AssertUnwindSafe,
path::Path,
process::{Child, Command as CommandSys},
sync::{
mpsc::{self, TrySendError},
Arc, Mutex,
},
sync::mpsc::{self, TrySendError},
thread,
};
use nu_engine::documentation::get_flags_section;
use nu_plugin_core::{
ClientCommunicationIo, CommunicationMode, InterfaceManager, PluginEncoder, PluginRead,
PluginWrite,
};
use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
use nu_protocol::{
ast::Operator, engine::StateWorkingSet, report_error_new, CustomValue, IntoSpanned,
LabeledError, PipelineData, PluginIdentity, PluginRegistryFile, PluginRegistryItem,
PluginRegistryItemData, PluginSignature, RegisteredPlugin, ShellError, Span, Spanned, Value,
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, ShellError, Spanned, Value,
};
use thiserror::Error;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
pub use self::interface::{PluginRead, PluginWrite};
use self::{
command::render_examples,
communication_mode::{
ClientCommunicationIo, CommunicationMode, PreparedServerCommunication,
ServerCommunicationIo,
},
gc::PluginGc,
};
use self::{command::render_examples, interface::ReceivedPluginCall};
mod command;
mod communication_mode;
mod context;
mod declaration;
mod gc;
mod interface;
mod persistent;
mod process;
mod source;
pub use command::{create_plugin_signature, PluginCommand, SimplePluginCommand};
pub use declaration::PluginDeclaration;
pub use interface::{
EngineInterface, EngineInterfaceManager, Interface, InterfaceManager, PluginInterface,
PluginInterfaceManager,
};
pub use persistent::{GetPlugin, PersistentPlugin};
pub use context::{PluginExecutionCommandContext, PluginExecutionContext};
pub use source::PluginSource;
pub use interface::{EngineInterface, EngineInterfaceManager};
#[allow(dead_code)]
pub(crate) const OUTPUT_BUFFER_SIZE: usize = 8192;
/// Encoder for a specific message type. Usually implemented on [`PluginInput`]
/// and [`PluginOutput`].
#[doc(hidden)]
pub trait Encoder<T>: Clone + Send + Sync {
/// Serialize a value in the [`PluginEncoder`]s format
///
/// Returns [`ShellError::IOError`] if there was a problem writing, or
/// [`ShellError::PluginFailedToEncode`] for a serialization error.
#[doc(hidden)]
fn encode(&self, data: &T, writer: &mut impl std::io::Write) -> Result<(), ShellError>;
/// Deserialize a value from the [`PluginEncoder`]'s format
///
/// Returns `None` if there is no more output to receive.
///
/// Returns [`ShellError::IOError`] if there was a problem reading, or
/// [`ShellError::PluginFailedToDecode`] for a deserialization error.
#[doc(hidden)]
fn decode(&self, reader: &mut impl std::io::BufRead) -> Result<Option<T>, ShellError>;
}
/// Encoding scheme that defines a plugin's communication protocol with Nu
pub trait PluginEncoder: Encoder<PluginInput> + Encoder<PluginOutput> {
/// The name of the encoder (e.g., `json`)
fn name(&self) -> &str;
}
fn create_command(path: &Path, mut shell: Option<&Path>, mode: &CommunicationMode) -> CommandSys {
log::trace!("Starting plugin: {path:?}, shell = {shell:?}, mode = {mode:?}");
let mut shell_args = vec![];
if shell.is_none() {
// We only have to do this for things that are not executable by Rust's Command API on
// Windows. They do handle bat/cmd files for us, helpfully.
//
// Also include anything that wouldn't be executable with a shebang, like JAR files.
shell = match path.extension().and_then(|e| e.to_str()) {
Some("sh") => {
if cfg!(unix) {
// We don't want to override what might be in the shebang if this is Unix, since
// some scripts will have a shebang specifying bash even if they're .sh
None
} else {
Some(Path::new("sh"))
}
}
Some("nu") => {
shell_args.push("--stdin");
Some(Path::new("nu"))
}
Some("py") => Some(Path::new("python")),
Some("rb") => Some(Path::new("ruby")),
Some("jar") => {
shell_args.push("-jar");
Some(Path::new("java"))
}
_ => None,
};
}
let mut process = if let Some(shell) = shell {
let mut process = std::process::Command::new(shell);
process.args(shell_args);
process.arg(path);
process
} else {
std::process::Command::new(path)
};
process.args(mode.args());
// Setup I/O according to the communication mode
mode.setup_command_io(&mut process);
// The plugin should be run in a new process group to prevent Ctrl-C from stopping it
#[cfg(unix)]
process.process_group(0);
#[cfg(windows)]
process.creation_flags(windows::Win32::System::Threading::CREATE_NEW_PROCESS_GROUP.0);
// In order to make bugs with improper use of filesystem without getting the engine current
// directory more obvious, the plugin always starts in the directory of its executable
if let Some(dirname) = path.parent() {
process.current_dir(dirname);
}
process
}
fn make_plugin_interface(
mut child: Child,
comm: PreparedServerCommunication,
source: Arc<PluginSource>,
pid: Option<u32>,
gc: Option<PluginGc>,
) -> Result<PluginInterface, ShellError> {
match comm.connect(&mut child)? {
ServerCommunicationIo::Stdio(stdin, stdout) => make_plugin_interface_with_streams(
stdout,
stdin,
move || {
let _ = child.wait();
},
source,
pid,
gc,
),
#[cfg(feature = "local-socket")]
ServerCommunicationIo::LocalSocket { read_out, write_in } => {
make_plugin_interface_with_streams(
read_out,
write_in,
move || {
let _ = child.wait();
},
source,
pid,
gc,
)
}
}
}
fn make_plugin_interface_with_streams(
mut reader: impl std::io::Read + Send + 'static,
writer: impl std::io::Write + Send + 'static,
after_close: impl FnOnce() + Send + 'static,
source: Arc<PluginSource>,
pid: Option<u32>,
gc: Option<PluginGc>,
) -> Result<PluginInterface, ShellError> {
let encoder = get_plugin_encoding(&mut reader)?;
let reader = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader);
let writer = BufWriter::with_capacity(OUTPUT_BUFFER_SIZE, writer);
let mut manager =
PluginInterfaceManager::new(source.clone(), pid, (Mutex::new(writer), encoder));
manager.set_garbage_collector(gc);
let interface = manager.get_interface();
interface.hello()?;
// Spawn the reader on a new thread. We need to be able to read messages at the same time that
// we write, because we are expected to be able to handle multiple messages coming in from the
// plugin at any time, including stream messages like `Drop`.
std::thread::Builder::new()
.name(format!(
"plugin interface reader ({})",
source.identity.name()
))
.spawn(move || {
if let Err(err) = manager.consume_all((reader, encoder)) {
log::warn!("Error in PluginInterfaceManager: {err}");
}
// If the loop has ended, drop the manager so everyone disconnects and then run
// after_close
drop(manager);
after_close();
})
.map_err(|err| ShellError::PluginFailedToLoad {
msg: format!("Failed to spawn thread for plugin: {err}"),
})?;
Ok(interface)
}
#[doc(hidden)] // Note: not for plugin authors / only used in nu-parser
pub fn get_signature(
plugin: Arc<PersistentPlugin>,
envs: impl FnOnce() -> Result<HashMap<String, String>, ShellError>,
) -> Result<Vec<PluginSignature>, ShellError> {
plugin.get(envs)?.get_signature()
}
/// The API for a Nushell plugin
///
/// A plugin defines multiple commands, which are added to the engine when the user calls
@ -499,6 +286,9 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
read_in,
mut write_out,
}) => {
use std::io::{BufReader, BufWriter};
use std::sync::Mutex;
tell_nushell_encoding(&mut write_out, &encoder)
.expect("failed to tell nushell encoding");
@ -895,119 +685,3 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) {
println!("{help}")
}
pub fn get_plugin_encoding(
child_stdout: &mut impl std::io::Read,
) -> Result<EncodingType, ShellError> {
let mut length_buf = [0u8; 1];
child_stdout
.read_exact(&mut length_buf)
.map_err(|e| ShellError::PluginFailedToLoad {
msg: format!("unable to get encoding from plugin: {e}"),
})?;
let mut buf = vec![0u8; length_buf[0] as usize];
child_stdout
.read_exact(&mut buf)
.map_err(|e| ShellError::PluginFailedToLoad {
msg: format!("unable to get encoding from plugin: {e}"),
})?;
EncodingType::try_from_bytes(&buf).ok_or_else(|| {
let encoding_for_debug = String::from_utf8_lossy(&buf);
ShellError::PluginFailedToLoad {
msg: format!("get unsupported plugin encoding: {encoding_for_debug}"),
}
})
}
/// Load the definitions from the plugin file into the engine state
#[doc(hidden)]
pub fn load_plugin_file(
working_set: &mut StateWorkingSet,
plugin_registry_file: &PluginRegistryFile,
span: Option<Span>,
) {
for plugin in &plugin_registry_file.plugins {
// Any errors encountered should just be logged.
if let Err(err) = load_plugin_registry_item(working_set, plugin, span) {
report_error_new(working_set.permanent_state, &err)
}
}
}
/// Load a definition from the plugin file into the engine state
#[doc(hidden)]
pub fn load_plugin_registry_item(
working_set: &mut StateWorkingSet,
plugin: &PluginRegistryItem,
span: Option<Span>,
) -> Result<Arc<PersistentPlugin>, ShellError> {
let identity =
PluginIdentity::new(plugin.filename.clone(), plugin.shell.clone()).map_err(|_| {
ShellError::GenericError {
error: "Invalid plugin filename in plugin registry file".into(),
msg: "loaded from here".into(),
span,
help: Some(format!(
"the filename for `{}` is not a valid nushell plugin: {}",
plugin.name,
plugin.filename.display()
)),
inner: vec![],
}
})?;
match &plugin.data {
PluginRegistryItemData::Valid { commands } => {
let plugin = add_plugin_to_working_set(working_set, &identity)?;
// Ensure that the plugin is reset. We're going to load new signatures, so we want to
// make sure the running plugin reflects those new signatures, and it's possible that it
// doesn't.
plugin.reset()?;
// Create the declarations from the commands
for signature in commands {
let decl = PluginDeclaration::new(plugin.clone(), signature.clone());
working_set.add_decl(Box::new(decl));
}
Ok(plugin)
}
PluginRegistryItemData::Invalid => Err(ShellError::PluginRegistryDataInvalid {
plugin_name: identity.name().to_owned(),
span,
add_command: identity.add_command(),
}),
}
}
#[doc(hidden)]
pub fn add_plugin_to_working_set(
working_set: &mut StateWorkingSet,
identity: &PluginIdentity,
) -> Result<Arc<PersistentPlugin>, ShellError> {
// Find garbage collection config for the plugin
let gc_config = working_set
.get_config()
.plugin_gc
.get(identity.name())
.clone();
// Add it to / get it from the working set
let plugin = working_set.find_or_create_plugin(identity, || {
Arc::new(PersistentPlugin::new(identity.clone(), gc_config.clone()))
});
plugin.set_gc_config(&gc_config);
// Downcast the plugin to `PersistentPlugin` - we generally expect this to succeed.
// The trait object only exists so that nu-protocol can contain plugins without knowing
// anything about their implementation, but we only use `PersistentPlugin` in practice.
plugin
.as_any()
.downcast()
.map_err(|_| ShellError::NushellFailed {
msg: "encountered unexpected RegisteredPlugin type".into(),
})
}

View File

@ -1,325 +0,0 @@
use super::{
communication_mode::CommunicationMode, create_command, gc::PluginGc, make_plugin_interface,
PluginInterface, PluginSource,
};
use nu_protocol::{
engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
/// A box that can keep a plugin that was spawned persistent for further uses. The plugin may or
/// may not be currently running. [`.get()`] gets the currently running plugin, or spawns it if it's
/// not running.
///
/// Note: used in the parser, not for plugin authors
#[doc(hidden)]
#[derive(Debug)]
pub struct PersistentPlugin {
/// Identity (filename, shell, name) of the plugin
identity: PluginIdentity,
/// Mutable state
mutable: Mutex<MutableState>,
}
/// The mutable state for the persistent plugin. This should all be behind one lock to prevent lock
/// order problems.
#[derive(Debug)]
struct MutableState {
/// Reference to the plugin if running
running: Option<RunningPlugin>,
/// Plugin's preferred communication mode (if known)
preferred_mode: Option<PreferredCommunicationMode>,
/// Garbage collector config
gc_config: PluginGcConfig,
}
#[derive(Debug, Clone, Copy)]
enum PreferredCommunicationMode {
Stdio,
#[cfg(feature = "local-socket")]
LocalSocket,
}
#[derive(Debug)]
struct RunningPlugin {
/// Interface (which can be cloned) to the running plugin
interface: PluginInterface,
/// Garbage collector for the plugin
gc: PluginGc,
}
impl PersistentPlugin {
/// Create a new persistent plugin. The plugin will not be spawned immediately.
pub fn new(identity: PluginIdentity, gc_config: PluginGcConfig) -> PersistentPlugin {
PersistentPlugin {
identity,
mutable: Mutex::new(MutableState {
running: None,
preferred_mode: None,
gc_config,
}),
}
}
/// Get the plugin interface of the running plugin, or spawn it if it's not currently running.
///
/// Will call `envs` to get environment variables to spawn the plugin if the plugin needs to be
/// spawned.
pub(crate) fn get(
self: Arc<Self>,
envs: impl FnOnce() -> Result<HashMap<String, String>, ShellError>,
) -> Result<PluginInterface, ShellError> {
let mut mutable = self.mutable.lock().map_err(|_| ShellError::NushellFailed {
msg: format!(
"plugin `{}` mutex poisoned, probably panic during spawn",
self.identity.name()
),
})?;
if let Some(ref running) = mutable.running {
// It exists, so just clone the interface
Ok(running.interface.clone())
} else {
// Try to spawn. On success, `mutable.running` should have been set to the new running
// plugin by `spawn()` so we just then need to clone the interface from there.
//
// We hold the lock the whole time to prevent others from trying to spawn and ending
// up with duplicate plugins
//
// TODO: We should probably store the envs somewhere, in case we have to launch without
// envs (e.g. from a custom value)
let envs = envs()?;
let result = self.clone().spawn(&envs, &mut mutable);
// Check if we were using an alternate communication mode and may need to fall back to
// stdio.
if result.is_err()
&& !matches!(
mutable.preferred_mode,
Some(PreferredCommunicationMode::Stdio)
)
{
log::warn!("{}: Trying again with stdio communication because mode {:?} failed with {result:?}",
self.identity.name(),
mutable.preferred_mode);
// Reset to stdio and try again, but this time don't catch any error
mutable.preferred_mode = Some(PreferredCommunicationMode::Stdio);
self.clone().spawn(&envs, &mut mutable)?;
}
Ok(mutable
.running
.as_ref()
.ok_or_else(|| ShellError::NushellFailed {
msg: "spawn() succeeded but didn't set interface".into(),
})?
.interface
.clone())
}
}
/// Run the plugin command, then set up and set `mutable.running` to the new running plugin.
fn spawn(
self: Arc<Self>,
envs: &HashMap<String, String>,
mutable: &mut MutableState,
) -> Result<(), ShellError> {
// Make sure `running` is set to None to begin
if let Some(running) = mutable.running.take() {
// Stop the GC if there was a running plugin
running.gc.stop_tracking();
}
let source_file = self.identity.filename();
// Determine the mode to use based on the preferred mode
let mode = match mutable.preferred_mode {
// If not set, we try stdio first and then might retry if another mode is supported
Some(PreferredCommunicationMode::Stdio) | None => CommunicationMode::Stdio,
// Local socket only if enabled
#[cfg(feature = "local-socket")]
Some(PreferredCommunicationMode::LocalSocket) => {
CommunicationMode::local_socket(source_file)
}
};
let mut plugin_cmd = create_command(source_file, self.identity.shell(), &mode);
// We need the current environment variables for `python` based plugins
// Or we'll likely have a problem when a plugin is implemented in a virtual Python environment.
plugin_cmd.envs(envs);
let program_name = plugin_cmd.get_program().to_os_string().into_string();
// Before running the command, prepare communication
let comm = mode.serve()?;
// Run the plugin command
let child = plugin_cmd.spawn().map_err(|err| {
let error_msg = match err.kind() {
std::io::ErrorKind::NotFound => match program_name {
Ok(prog_name) => {
format!("Can't find {prog_name}, please make sure that {prog_name} is in PATH.")
}
_ => {
format!("Error spawning child process: {err}")
}
},
_ => {
format!("Error spawning child process: {err}")
}
};
ShellError::PluginFailedToLoad { msg: error_msg }
})?;
// Start the plugin garbage collector
let gc = PluginGc::new(mutable.gc_config.clone(), &self)?;
let pid = child.id();
let interface = make_plugin_interface(
child,
comm,
Arc::new(PluginSource::new(self.clone())),
Some(pid),
Some(gc.clone()),
)?;
// If our current preferred mode is None, check to see if the plugin might support another
// mode. If so, retry spawn() with that mode
#[cfg(feature = "local-socket")]
if mutable.preferred_mode.is_none()
&& interface
.protocol_info()?
.supports_feature(&crate::protocol::Feature::LocalSocket)
{
log::trace!(
"{}: Attempting to upgrade to local socket mode",
self.identity.name()
);
// Stop the GC we just created from tracking so that we don't accidentally try to
// stop the new plugin
gc.stop_tracking();
// Set the mode and try again
mutable.preferred_mode = Some(PreferredCommunicationMode::LocalSocket);
return self.spawn(envs, mutable);
}
mutable.running = Some(RunningPlugin { interface, gc });
Ok(())
}
fn stop_internal(&self, reset: bool) -> Result<(), ShellError> {
let mut mutable = self.mutable.lock().map_err(|_| ShellError::NushellFailed {
msg: format!(
"plugin `{}` mutable mutex poisoned, probably panic during spawn",
self.identity.name()
),
})?;
// If the plugin is running, stop its GC, so that the GC doesn't accidentally try to stop
// a future plugin
if let Some(ref running) = mutable.running {
running.gc.stop_tracking();
}
// We don't try to kill the process or anything, we just drop the RunningPlugin. It should
// exit soon after
mutable.running = None;
// If this is a reset, we should also reset other learned attributes like preferred_mode
if reset {
mutable.preferred_mode = None;
}
Ok(())
}
}
impl RegisteredPlugin for PersistentPlugin {
fn identity(&self) -> &PluginIdentity {
&self.identity
}
fn is_running(&self) -> bool {
// If the lock is poisoned, we return false here. That may not be correct, but this is a
// failure state anyway that would be noticed at some point
self.mutable
.lock()
.map(|m| m.running.is_some())
.unwrap_or(false)
}
fn pid(&self) -> Option<u32> {
// Again, we return None for a poisoned lock.
self.mutable
.lock()
.ok()
.and_then(|r| r.running.as_ref().and_then(|r| r.interface.pid()))
}
fn stop(&self) -> Result<(), ShellError> {
self.stop_internal(false)
}
fn reset(&self) -> Result<(), ShellError> {
self.stop_internal(true)
}
fn set_gc_config(&self, gc_config: &PluginGcConfig) {
if let Ok(mut mutable) = self.mutable.lock() {
// Save the new config for future calls
mutable.gc_config = gc_config.clone();
// If the plugin is already running, propagate the config change to the running GC
if let Some(gc) = mutable.running.as_ref().map(|running| running.gc.clone()) {
// We don't want to get caught holding the lock
drop(mutable);
gc.set_config(gc_config.clone());
gc.flush();
}
}
}
fn as_any(self: Arc<Self>) -> Arc<dyn std::any::Any + Send + Sync> {
self
}
}
/// Anything that can produce a plugin interface.
///
/// This is not a public interface.
#[doc(hidden)]
pub trait GetPlugin: RegisteredPlugin {
/// Retrieve or spawn a [`PluginInterface`]. The `context` may be used for determining
/// environment variables to launch the plugin with.
fn get_plugin(
self: Arc<Self>,
context: Option<(&EngineState, &mut Stack)>,
) -> Result<PluginInterface, ShellError>;
}
impl GetPlugin for PersistentPlugin {
fn get_plugin(
self: Arc<Self>,
mut context: Option<(&EngineState, &mut Stack)>,
) -> Result<PluginInterface, ShellError> {
self.get(|| {
// Get envs from the context if provided.
let envs = context
.as_mut()
.map(|(engine_state, stack)| {
// We need the current environment variables for `python` based plugins. Or
// we'll likely have a problem when a plugin is implemented in a virtual Python
// environment.
let stack = &mut stack.start_capture();
nu_engine::env::env_to_strings(engine_state, stack)
})
.transpose()?;
Ok(envs.unwrap_or_default())
})
}
}

View File

@ -1,90 +0,0 @@
use std::sync::{atomic::AtomicU32, Arc, Mutex, MutexGuard};
use nu_protocol::{ShellError, Span};
use nu_system::ForegroundGuard;
/// Provides a utility interface for a plugin interface to manage the process the plugin is running
/// in.
#[derive(Debug)]
pub(crate) struct PluginProcess {
pid: u32,
mutable: Mutex<MutablePart>,
}
#[derive(Debug)]
struct MutablePart {
foreground_guard: Option<ForegroundGuard>,
}
impl PluginProcess {
/// Manage a plugin process.
pub(crate) fn new(pid: u32) -> PluginProcess {
PluginProcess {
pid,
mutable: Mutex::new(MutablePart {
foreground_guard: None,
}),
}
}
/// The process ID of the plugin.
pub(crate) fn pid(&self) -> u32 {
self.pid
}
fn lock_mutable(&self) -> Result<MutexGuard<MutablePart>, ShellError> {
self.mutable.lock().map_err(|_| ShellError::NushellFailed {
msg: "the PluginProcess mutable lock has been poisoned".into(),
})
}
/// Move the plugin process to the foreground. See [`ForegroundGuard::new`].
///
/// This produces an error if the plugin process was already in the foreground.
///
/// Returns `Some()` on Unix with the process group ID if the plugin process will need to join
/// another process group to be part of the foreground.
pub(crate) fn enter_foreground(
&self,
span: Span,
pipeline_state: &Arc<(AtomicU32, AtomicU32)>,
) -> Result<Option<u32>, ShellError> {
let pid = self.pid;
let mut mutable = self.lock_mutable()?;
if mutable.foreground_guard.is_none() {
let guard = ForegroundGuard::new(pid, pipeline_state).map_err(|err| {
ShellError::GenericError {
error: "Failed to enter foreground".into(),
msg: err.to_string(),
span: Some(span),
help: None,
inner: vec![],
}
})?;
let pgrp = guard.pgrp();
mutable.foreground_guard = Some(guard);
Ok(pgrp)
} else {
Err(ShellError::GenericError {
error: "Can't enter foreground".into(),
msg: "this plugin is already running in the foreground".into(),
span: Some(span),
help: Some(
"you may be trying to run the command in parallel, or this may be a bug in \
the plugin"
.into(),
),
inner: vec![],
})
}
}
/// Move the plugin process out of the foreground. See [`ForegroundGuard::reset`].
///
/// This is a no-op if the plugin process was already in the background.
pub(crate) fn exit_foreground(&self) -> Result<(), ShellError> {
let mut mutable = self.lock_mutable()?;
drop(mutable.foreground_guard.take());
Ok(())
}
}

View File

@ -1,70 +0,0 @@
use super::GetPlugin;
use nu_protocol::{PluginIdentity, ShellError, Span};
use std::sync::{Arc, Weak};
/// The source of a custom value or plugin command. Includes a weak reference to the persistent
/// plugin so it can be retrieved.
///
/// This is not a public interface.
#[derive(Debug, Clone)]
#[doc(hidden)]
pub struct PluginSource {
/// The identity of the plugin
pub(crate) identity: Arc<PluginIdentity>,
/// A weak reference to the persistent plugin that might hold an interface to the plugin.
///
/// This is weak to avoid cyclic references, but it does mean we might fail to upgrade if
/// the engine state lost the [`PersistentPlugin`] at some point.
pub(crate) persistent: Weak<dyn GetPlugin>,
}
impl PluginSource {
/// Create from an implementation of `GetPlugin`
pub fn new(plugin: Arc<dyn GetPlugin>) -> PluginSource {
PluginSource {
identity: plugin.identity().clone().into(),
persistent: Arc::downgrade(&plugin),
}
}
/// Create a new fake source with a fake identity, for testing
///
/// Warning: [`.persistent()`] will always return an error.
#[cfg(test)]
pub(crate) fn new_fake(name: &str) -> PluginSource {
PluginSource {
identity: PluginIdentity::new_fake(name).into(),
persistent: Weak::<crate::PersistentPlugin>::new(),
}
}
/// Try to upgrade the persistent reference, and return an error referencing `span` as the
/// object that referenced it otherwise
///
/// This is not a public API.
#[doc(hidden)]
pub fn persistent(&self, span: Option<Span>) -> Result<Arc<dyn GetPlugin>, ShellError> {
self.persistent
.upgrade()
.ok_or_else(|| ShellError::GenericError {
error: format!("The `{}` plugin is no longer present", self.identity.name()),
msg: "removed since this object was created".into(),
span,
help: Some("try recreating the object that came from the plugin".into()),
inner: vec![],
})
}
/// Sources are compatible if their identities are equal
pub(crate) fn is_compatible(&self, other: &PluginSource) -> bool {
self.identity == other.identity
}
}
impl std::ops::Deref for PluginSource {
type Target = PluginIdentity;
fn deref(&self) -> &PluginIdentity {
&self.identity
}
}

View File

@ -1,411 +0,0 @@
use nu_protocol::{
ast::{Call, Expression},
engine::{EngineState, Stack},
FromValue, ShellError, Span, Spanned, Value,
};
use serde::{Deserialize, Serialize};
/// A representation of the plugin's invocation command including command line args
///
/// The `EvaluatedCall` contains information about the way a [`Plugin`](crate::Plugin) was invoked
/// representing the [`Span`] corresponding to the invocation as well as the arguments
/// it was invoked with. It is one of three items passed to [`run()`](crate::PluginCommand::run()) along with
/// `name` which command that was invoked and a [`Value`] that represents the input.
///
/// The evaluated call is used with the Plugins because the plugin doesn't have
/// access to the Stack and the EngineState the way a built in command might. For that
/// reason, before encoding the message to the plugin all the arguments to the original
/// call (which are expressions) are evaluated and passed to Values
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvaluatedCall {
/// Span of the command invocation
pub head: Span,
/// Values of positional arguments
pub positional: Vec<Value>,
/// Names and values of named arguments
pub named: Vec<(Spanned<String>, Option<Value>)>,
}
impl EvaluatedCall {
pub(crate) fn try_from_call(
call: &Call,
engine_state: &EngineState,
stack: &mut Stack,
eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result<Value, ShellError>,
) -> Result<Self, ShellError> {
let positional =
call.rest_iter_flattened(0, |expr| eval_expression_fn(engine_state, stack, expr))?;
let mut named = Vec::with_capacity(call.named_len());
for (string, _, expr) in call.named_iter() {
let value = match expr {
None => None,
Some(expr) => Some(eval_expression_fn(engine_state, stack, expr)?),
};
named.push((string.clone(), value))
}
Ok(Self {
head: call.head,
positional,
named,
})
}
/// Check if a flag (named parameter that does not take a value) is set
/// Returns Ok(true) if flag is set or passed true value
/// Returns Ok(false) if flag is not set or passed false value
/// Returns Err if passed value is not a boolean
///
/// # Examples
/// Invoked as `my_command --foo`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # None
/// # )],
/// # };
/// assert!(call.has_flag("foo").unwrap());
/// ```
///
/// Invoked as `my_command --bar`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "bar".to_owned(), span: null_span},
/// # None
/// # )],
/// # };
/// assert!(!call.has_flag("foo").unwrap());
/// ```
///
/// Invoked as `my_command --foo=true`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::bool(true, Span::unknown()))
/// # )],
/// # };
/// assert!(call.has_flag("foo").unwrap());
/// ```
///
/// Invoked as `my_command --foo=false`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::bool(false, Span::unknown()))
/// # )],
/// # };
/// assert!(!call.has_flag("foo").unwrap());
/// ```
///
/// Invoked with wrong type as `my_command --foo=1`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::int(1, Span::unknown()))
/// # )],
/// # };
/// assert!(call.has_flag("foo").is_err());
/// ```
pub fn has_flag(&self, flag_name: &str) -> Result<bool, ShellError> {
for name in &self.named {
if flag_name == name.0.item {
return match &name.1 {
Some(Value::Bool { val, .. }) => Ok(*val),
None => Ok(true),
Some(result) => Err(ShellError::CantConvert {
to_type: "bool".into(),
from_type: result.get_type().to_string(),
span: result.span(),
help: Some("".into()),
}),
};
}
}
Ok(false)
}
/// Returns the [`Value`] of an optional named argument
///
/// # Examples
/// Invoked as `my_command --foo 123`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::int(123, null_span))
/// # )],
/// # };
/// let opt_foo = match call.get_flag_value("foo") {
/// Some(Value::Int { val, .. }) => Some(val),
/// None => None,
/// _ => panic!(),
/// };
/// assert_eq!(opt_foo, Some(123));
/// ```
///
/// Invoked as `my_command`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![],
/// # };
/// let opt_foo = match call.get_flag_value("foo") {
/// Some(Value::Int { val, .. }) => Some(val),
/// None => None,
/// _ => panic!(),
/// };
/// assert_eq!(opt_foo, None);
/// ```
pub fn get_flag_value(&self, flag_name: &str) -> Option<Value> {
for name in &self.named {
if flag_name == name.0.item {
return name.1.clone();
}
}
None
}
/// Returns the [`Value`] of a given (zero indexed) positional argument, if present
///
/// Examples:
/// Invoked as `my_command a b c`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: vec![
/// # Value::string("a".to_owned(), null_span),
/// # Value::string("b".to_owned(), null_span),
/// # Value::string("c".to_owned(), null_span),
/// # ],
/// # named: vec![],
/// # };
/// let arg = match call.nth(1) {
/// Some(Value::String { val, .. }) => val,
/// _ => panic!(),
/// };
/// assert_eq!(arg, "b".to_owned());
///
/// let arg = call.nth(7);
/// assert!(arg.is_none());
/// ```
pub fn nth(&self, pos: usize) -> Option<Value> {
self.positional.get(pos).cloned()
}
/// Returns the value of a named argument interpreted as type `T`
///
/// # Examples
/// Invoked as `my_command --foo 123`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::int(123, null_span))
/// # )],
/// # };
/// let foo = call.get_flag::<i64>("foo");
/// assert_eq!(foo.unwrap(), Some(123));
/// ```
///
/// Invoked as `my_command --bar 123`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "bar".to_owned(), span: null_span},
/// # Some(Value::int(123, null_span))
/// # )],
/// # };
/// let foo = call.get_flag::<i64>("foo");
/// assert_eq!(foo.unwrap(), None);
/// ```
///
/// Invoked as `my_command --foo abc`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: Vec::new(),
/// # named: vec![(
/// # Spanned { item: "foo".to_owned(), span: null_span},
/// # Some(Value::string("abc".to_owned(), null_span))
/// # )],
/// # };
/// let foo = call.get_flag::<i64>("foo");
/// assert!(foo.is_err());
/// ```
pub fn get_flag<T: FromValue>(&self, name: &str) -> Result<Option<T>, ShellError> {
if let Some(value) = self.get_flag_value(name) {
FromValue::from_value(value).map(Some)
} else {
Ok(None)
}
}
/// Retrieve the Nth and all following positional arguments as type `T`
///
/// # Example
/// Invoked as `my_command zero one two three`:
/// ```
/// # use nu_protocol::{Spanned, Span, Value};
/// # use nu_plugin::EvaluatedCall;
/// # let null_span = Span::new(0, 0);
/// # let call = EvaluatedCall {
/// # head: null_span,
/// # positional: vec![
/// # Value::string("zero".to_owned(), null_span),
/// # Value::string("one".to_owned(), null_span),
/// # Value::string("two".to_owned(), null_span),
/// # Value::string("three".to_owned(), null_span),
/// # ],
/// # named: Vec::new(),
/// # };
/// let args = call.rest::<String>(0);
/// assert_eq!(args.unwrap(), vec!["zero", "one", "two", "three"]);
///
/// let args = call.rest::<String>(2);
/// assert_eq!(args.unwrap(), vec!["two", "three"]);
/// ```
pub fn rest<T: FromValue>(&self, starting_pos: usize) -> Result<Vec<T>, ShellError> {
self.positional
.iter()
.skip(starting_pos)
.map(|value| FromValue::from_value(value.clone()))
.collect()
}
/// Retrieve the value of an optional positional argument interpreted as type `T`
///
/// Returns the value of a (zero indexed) positional argument of type `T`.
/// Alternatively returns [`None`] if the positional argument does not exist
/// or an error that can be passed back to the shell on error.
pub fn opt<T: FromValue>(&self, pos: usize) -> Result<Option<T>, ShellError> {
if let Some(value) = self.nth(pos) {
FromValue::from_value(value).map(Some)
} else {
Ok(None)
}
}
/// Retrieve the value of a mandatory positional argument as type `T`
///
/// Expect a positional argument of type `T` and return its value or, if the
/// argument does not exist or is of the wrong type, return an error that can
/// be passed back to the shell.
pub fn req<T: FromValue>(&self, pos: usize) -> Result<T, ShellError> {
if let Some(value) = self.nth(pos) {
FromValue::from_value(value)
} else if self.positional.is_empty() {
Err(ShellError::AccessEmptyContent { span: self.head })
} else {
Err(ShellError::AccessBeyondEnd {
max_idx: self.positional.len() - 1,
span: self.head,
})
}
}
}
#[cfg(test)]
mod test {
use super::*;
use nu_protocol::{Span, Spanned, Value};
#[test]
fn call_to_value() {
let call = EvaluatedCall {
head: Span::new(0, 10),
positional: vec![
Value::float(1.0, Span::new(0, 10)),
Value::string("something", Span::new(0, 10)),
],
named: vec![
(
Spanned {
item: "name".to_string(),
span: Span::new(0, 10),
},
Some(Value::float(1.0, Span::new(0, 10))),
),
(
Spanned {
item: "flag".to_string(),
span: Span::new(0, 10),
},
None,
),
],
};
let name: Option<f64> = call.get_flag("name").unwrap();
assert_eq!(name, Some(1.0));
assert!(call.has_flag("flag").unwrap());
let required: f64 = call.req(0).unwrap();
assert!((required - 1.0).abs() < f64::EPSILON);
let optional: Option<String> = call.opt(1).unwrap();
assert_eq!(optional, Some("something".to_string()));
let rest: Vec<String> = call.rest(1).unwrap();
assert_eq!(rest, vec!["something".to_string()]);
}
}

View File

@ -1,607 +0,0 @@
mod evaluated_call;
mod plugin_custom_value;
mod protocol_info;
#[cfg(test)]
mod tests;
#[cfg(test)]
pub(crate) mod test_util;
use nu_protocol::{
ast::Operator, engine::Closure, Config, LabeledError, PipelineData, PluginSignature, RawStream,
ShellError, Span, Spanned, Value,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub use evaluated_call::EvaluatedCall;
pub use plugin_custom_value::PluginCustomValue;
#[allow(unused_imports)] // may be unused by compile flags
pub use protocol_info::{Feature, Protocol, ProtocolInfo};
/// A sequential identifier for a stream
pub type StreamId = usize;
/// A sequential identifier for a [`PluginCall`]
pub type PluginCallId = usize;
/// A sequential identifier for an [`EngineCall`]
pub type EngineCallId = usize;
/// Information about a plugin command invocation. This includes an [`EvaluatedCall`] as a
/// serializable representation of [`nu_protocol::ast::Call`]. The type parameter determines
/// the input type.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CallInfo<D> {
/// The name of the command to be run
pub name: String,
/// Information about the invocation, including arguments
pub call: EvaluatedCall,
/// Pipeline input. This is usually [`nu_protocol::PipelineData`] or [`PipelineDataHeader`]
pub input: D,
}
impl<D> CallInfo<D> {
/// Convert the type of `input` from `D` to `T`.
pub(crate) fn map_data<T>(
self,
f: impl FnOnce(D) -> Result<T, ShellError>,
) -> Result<CallInfo<T>, ShellError> {
Ok(CallInfo {
name: self.name,
call: self.call,
input: f(self.input)?,
})
}
}
/// The initial (and perhaps only) part of any [`nu_protocol::PipelineData`] sent over the wire.
///
/// This may contain a single value, or may initiate a stream with a [`StreamId`].
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
pub enum PipelineDataHeader {
/// No input
Empty,
/// A single value
Value(Value),
/// Initiate [`nu_protocol::PipelineData::ListStream`].
///
/// Items are sent via [`StreamData`]
ListStream(ListStreamInfo),
/// Initiate [`nu_protocol::PipelineData::ExternalStream`].
///
/// Items are sent via [`StreamData`]
ExternalStream(ExternalStreamInfo),
}
impl PipelineDataHeader {
/// Return a list of stream IDs embedded in the header
pub(crate) fn stream_ids(&self) -> Vec<StreamId> {
match self {
PipelineDataHeader::Empty => vec![],
PipelineDataHeader::Value(_) => vec![],
PipelineDataHeader::ListStream(info) => vec![info.id],
PipelineDataHeader::ExternalStream(info) => {
let mut out = vec![];
if let Some(stdout) = &info.stdout {
out.push(stdout.id);
}
if let Some(stderr) = &info.stderr {
out.push(stderr.id);
}
if let Some(exit_code) = &info.exit_code {
out.push(exit_code.id);
}
out
}
}
}
}
/// Additional information about list (value) streams
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct ListStreamInfo {
pub id: StreamId,
}
/// Additional information about external streams
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct ExternalStreamInfo {
pub span: Span,
pub stdout: Option<RawStreamInfo>,
pub stderr: Option<RawStreamInfo>,
pub exit_code: Option<ListStreamInfo>,
pub trim_end_newline: bool,
}
/// Additional information about raw (byte) streams
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct RawStreamInfo {
pub id: StreamId,
pub is_binary: bool,
pub known_size: Option<u64>,
}
impl RawStreamInfo {
pub(crate) fn new(id: StreamId, stream: &RawStream) -> Self {
RawStreamInfo {
id,
is_binary: stream.is_binary,
known_size: stream.known_size,
}
}
}
/// Calls that a plugin can execute. The type parameter determines the input type.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PluginCall<D> {
Signature,
Run(CallInfo<D>),
CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp),
}
impl<D> PluginCall<D> {
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
/// not contain data.
pub(crate) fn map_data<T>(
self,
f: impl FnOnce(D) -> Result<T, ShellError>,
) -> Result<PluginCall<T>, ShellError> {
Ok(match self {
PluginCall::Signature => PluginCall::Signature,
PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?),
PluginCall::CustomValueOp(custom_value, op) => {
PluginCall::CustomValueOp(custom_value, op)
}
})
}
/// The span associated with the call.
pub fn span(&self) -> Option<Span> {
match self {
PluginCall::Signature => None,
PluginCall::Run(CallInfo { call, .. }) => Some(call.head),
PluginCall::CustomValueOp(val, _) => Some(val.span),
}
}
}
/// Operations supported for custom values.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum CustomValueOp {
/// [`to_base_value()`](nu_protocol::CustomValue::to_base_value)
ToBaseValue,
/// [`follow_path_int()`](nu_protocol::CustomValue::follow_path_int)
FollowPathInt(Spanned<usize>),
/// [`follow_path_string()`](nu_protocol::CustomValue::follow_path_string)
FollowPathString(Spanned<String>),
/// [`partial_cmp()`](nu_protocol::CustomValue::partial_cmp)
PartialCmp(Value),
/// [`operation()`](nu_protocol::CustomValue::operation)
Operation(Spanned<Operator>, Value),
/// Notify that the custom value has been dropped, if
/// [`notify_plugin_on_drop()`](nu_protocol::CustomValue::notify_plugin_on_drop) is true
Dropped,
}
impl CustomValueOp {
/// Get the name of the op, for error messages.
pub(crate) fn name(&self) -> &'static str {
match self {
CustomValueOp::ToBaseValue => "to_base_value",
CustomValueOp::FollowPathInt(_) => "follow_path_int",
CustomValueOp::FollowPathString(_) => "follow_path_string",
CustomValueOp::PartialCmp(_) => "partial_cmp",
CustomValueOp::Operation(_, _) => "operation",
CustomValueOp::Dropped => "dropped",
}
}
}
/// Any data sent to the plugin
///
/// Note: exported for internal use, not public.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[doc(hidden)]
pub enum PluginInput {
/// This must be the first message. Indicates supported protocol
Hello(ProtocolInfo),
/// Execute a [`PluginCall`], such as `Run` or `Signature`. The ID should not have been used
/// before.
Call(PluginCallId, PluginCall<PipelineDataHeader>),
/// Don't expect any more plugin calls. Exit after all currently executing plugin calls are
/// finished.
Goodbye,
/// Response to an [`EngineCall`]. The ID should be the same one sent with the engine call this
/// is responding to
EngineCallResponse(EngineCallId, EngineCallResponse<PipelineDataHeader>),
/// See [`StreamMessage::Data`].
Data(StreamId, StreamData),
/// See [`StreamMessage::End`].
End(StreamId),
/// See [`StreamMessage::Drop`].
Drop(StreamId),
/// See [`StreamMessage::Ack`].
Ack(StreamId),
}
impl TryFrom<PluginInput> for StreamMessage {
type Error = PluginInput;
fn try_from(msg: PluginInput) -> Result<StreamMessage, PluginInput> {
match msg {
PluginInput::Data(id, data) => Ok(StreamMessage::Data(id, data)),
PluginInput::End(id) => Ok(StreamMessage::End(id)),
PluginInput::Drop(id) => Ok(StreamMessage::Drop(id)),
PluginInput::Ack(id) => Ok(StreamMessage::Ack(id)),
_ => Err(msg),
}
}
}
impl From<StreamMessage> for PluginInput {
fn from(stream_msg: StreamMessage) -> PluginInput {
match stream_msg {
StreamMessage::Data(id, data) => PluginInput::Data(id, data),
StreamMessage::End(id) => PluginInput::End(id),
StreamMessage::Drop(id) => PluginInput::Drop(id),
StreamMessage::Ack(id) => PluginInput::Ack(id),
}
}
}
/// A single item of stream data for a stream.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum StreamData {
List(Value),
Raw(Result<Vec<u8>, LabeledError>),
}
impl From<Value> for StreamData {
fn from(value: Value) -> Self {
StreamData::List(value)
}
}
impl From<Result<Vec<u8>, LabeledError>> for StreamData {
fn from(value: Result<Vec<u8>, LabeledError>) -> Self {
StreamData::Raw(value)
}
}
impl From<Result<Vec<u8>, ShellError>> for StreamData {
fn from(value: Result<Vec<u8>, ShellError>) -> Self {
value.map_err(LabeledError::from).into()
}
}
impl TryFrom<StreamData> for Value {
type Error = ShellError;
fn try_from(data: StreamData) -> Result<Value, ShellError> {
match data {
StreamData::List(value) => Ok(value),
StreamData::Raw(_) => Err(ShellError::PluginFailedToDecode {
msg: "expected list stream data, found raw data".into(),
}),
}
}
}
impl TryFrom<StreamData> for Result<Vec<u8>, LabeledError> {
type Error = ShellError;
fn try_from(data: StreamData) -> Result<Result<Vec<u8>, LabeledError>, ShellError> {
match data {
StreamData::Raw(value) => Ok(value),
StreamData::List(_) => Err(ShellError::PluginFailedToDecode {
msg: "expected raw stream data, found list data".into(),
}),
}
}
}
impl TryFrom<StreamData> for Result<Vec<u8>, ShellError> {
type Error = ShellError;
fn try_from(value: StreamData) -> Result<Result<Vec<u8>, ShellError>, ShellError> {
Result::<Vec<u8>, LabeledError>::try_from(value).map(|res| res.map_err(ShellError::from))
}
}
/// A stream control or data message.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum StreamMessage {
/// Append data to the stream. Sent by the stream producer.
Data(StreamId, StreamData),
/// End of stream. Sent by the stream producer.
End(StreamId),
/// Notify that the read end of the stream has closed, and further messages should not be
/// sent. Sent by the stream consumer.
Drop(StreamId),
/// Acknowledge that a message has been consumed. This is used to implement flow control by
/// the stream producer. Sent by the stream consumer.
Ack(StreamId),
}
/// Response to a [`PluginCall`]. The type parameter determines the output type for pipeline data.
///
/// Note: exported for internal use, not public.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[doc(hidden)]
pub enum PluginCallResponse<D> {
Error(LabeledError),
Signature(Vec<PluginSignature>),
Ordering(Option<Ordering>),
PipelineData(D),
}
impl<D> PluginCallResponse<D> {
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
/// not contain data.
pub(crate) fn map_data<T>(
self,
f: impl FnOnce(D) -> Result<T, ShellError>,
) -> Result<PluginCallResponse<T>, ShellError> {
Ok(match self {
PluginCallResponse::Error(err) => PluginCallResponse::Error(err),
PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs),
PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering),
PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?),
})
}
}
impl PluginCallResponse<PipelineDataHeader> {
/// Construct a plugin call response with a single value
pub fn value(value: Value) -> PluginCallResponse<PipelineDataHeader> {
if value.is_nothing() {
PluginCallResponse::PipelineData(PipelineDataHeader::Empty)
} else {
PluginCallResponse::PipelineData(PipelineDataHeader::Value(value))
}
}
}
impl PluginCallResponse<PipelineData> {
/// Does this response have a stream?
pub(crate) fn has_stream(&self) -> bool {
match self {
PluginCallResponse::PipelineData(data) => match data {
PipelineData::Empty => false,
PipelineData::Value(..) => false,
PipelineData::ListStream(..) => true,
PipelineData::ExternalStream { .. } => true,
},
_ => false,
}
}
}
/// Options that can be changed to affect how the engine treats the plugin
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum PluginOption {
/// Send `GcDisabled(true)` to stop the plugin from being automatically garbage collected, or
/// `GcDisabled(false)` to enable it again.
///
/// See [`EngineInterface::set_gc_disabled`] for more information.
GcDisabled(bool),
}
/// This is just a serializable version of [`std::cmp::Ordering`], and can be converted 1:1
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Ordering {
Less,
Equal,
Greater,
}
impl From<std::cmp::Ordering> for Ordering {
fn from(value: std::cmp::Ordering) -> Self {
match value {
std::cmp::Ordering::Less => Ordering::Less,
std::cmp::Ordering::Equal => Ordering::Equal,
std::cmp::Ordering::Greater => Ordering::Greater,
}
}
}
impl From<Ordering> for std::cmp::Ordering {
fn from(value: Ordering) -> Self {
match value {
Ordering::Less => std::cmp::Ordering::Less,
Ordering::Equal => std::cmp::Ordering::Equal,
Ordering::Greater => std::cmp::Ordering::Greater,
}
}
}
/// Information received from the plugin
///
/// Note: exported for internal use, not public.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[doc(hidden)]
pub enum PluginOutput {
/// This must be the first message. Indicates supported protocol
Hello(ProtocolInfo),
/// Set option. No response expected
Option(PluginOption),
/// A response to a [`PluginCall`]. The ID should be the same sent with the plugin call this
/// is a response to
CallResponse(PluginCallId, PluginCallResponse<PipelineDataHeader>),
/// Execute an [`EngineCall`]. Engine calls must be executed within the `context` of a plugin
/// call, and the `id` should not have been used before
EngineCall {
/// The plugin call (by ID) to execute in the context of
context: PluginCallId,
/// A new identifier for this engine call. The response will reference this ID
id: EngineCallId,
call: EngineCall<PipelineDataHeader>,
},
/// See [`StreamMessage::Data`].
Data(StreamId, StreamData),
/// See [`StreamMessage::End`].
End(StreamId),
/// See [`StreamMessage::Drop`].
Drop(StreamId),
/// See [`StreamMessage::Ack`].
Ack(StreamId),
}
impl TryFrom<PluginOutput> for StreamMessage {
type Error = PluginOutput;
fn try_from(msg: PluginOutput) -> Result<StreamMessage, PluginOutput> {
match msg {
PluginOutput::Data(id, data) => Ok(StreamMessage::Data(id, data)),
PluginOutput::End(id) => Ok(StreamMessage::End(id)),
PluginOutput::Drop(id) => Ok(StreamMessage::Drop(id)),
PluginOutput::Ack(id) => Ok(StreamMessage::Ack(id)),
_ => Err(msg),
}
}
}
impl From<StreamMessage> for PluginOutput {
fn from(stream_msg: StreamMessage) -> PluginOutput {
match stream_msg {
StreamMessage::Data(id, data) => PluginOutput::Data(id, data),
StreamMessage::End(id) => PluginOutput::End(id),
StreamMessage::Drop(id) => PluginOutput::Drop(id),
StreamMessage::Ack(id) => PluginOutput::Ack(id),
}
}
}
/// A remote call back to the engine during the plugin's execution.
///
/// The type parameter determines the input type, for calls that take pipeline data.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum EngineCall<D> {
/// Get the full engine configuration
GetConfig,
/// Get the plugin-specific configuration (`$env.config.plugins.NAME`)
GetPluginConfig,
/// Get an environment variable
GetEnvVar(String),
/// Get all environment variables
GetEnvVars,
/// Get current working directory
GetCurrentDir,
/// Set an environment variable in the caller's scope
AddEnvVar(String, Value),
/// Get help for the current command
GetHelp,
/// Move the plugin into the foreground for terminal interaction
EnterForeground,
/// Move the plugin out of the foreground once terminal interaction has finished
LeaveForeground,
/// Get the contents of a span. Response is a binary which may not parse to UTF-8
GetSpanContents(Span),
/// Evaluate a closure with stream input/output
EvalClosure {
/// The closure to call.
///
/// This may come from a [`Value::Closure`] passed in as an argument to the plugin.
closure: Spanned<Closure>,
/// Positional arguments to add to the closure call
positional: Vec<Value>,
/// Input to the closure
input: D,
/// Whether to redirect stdout from external commands
redirect_stdout: bool,
/// Whether to redirect stderr from external commands
redirect_stderr: bool,
},
}
impl<D> EngineCall<D> {
/// Get the name of the engine call so it can be embedded in things like error messages
pub fn name(&self) -> &'static str {
match self {
EngineCall::GetConfig => "GetConfig",
EngineCall::GetPluginConfig => "GetPluginConfig",
EngineCall::GetEnvVar(_) => "GetEnv",
EngineCall::GetEnvVars => "GetEnvs",
EngineCall::GetCurrentDir => "GetCurrentDir",
EngineCall::AddEnvVar(..) => "AddEnvVar",
EngineCall::GetHelp => "GetHelp",
EngineCall::EnterForeground => "EnterForeground",
EngineCall::LeaveForeground => "LeaveForeground",
EngineCall::GetSpanContents(_) => "GetSpanContents",
EngineCall::EvalClosure { .. } => "EvalClosure",
}
}
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
/// not contain data.
pub(crate) fn map_data<T>(
self,
f: impl FnOnce(D) -> Result<T, ShellError>,
) -> Result<EngineCall<T>, ShellError> {
Ok(match self {
EngineCall::GetConfig => EngineCall::GetConfig,
EngineCall::GetPluginConfig => EngineCall::GetPluginConfig,
EngineCall::GetEnvVar(name) => EngineCall::GetEnvVar(name),
EngineCall::GetEnvVars => EngineCall::GetEnvVars,
EngineCall::GetCurrentDir => EngineCall::GetCurrentDir,
EngineCall::AddEnvVar(name, value) => EngineCall::AddEnvVar(name, value),
EngineCall::GetHelp => EngineCall::GetHelp,
EngineCall::EnterForeground => EngineCall::EnterForeground,
EngineCall::LeaveForeground => EngineCall::LeaveForeground,
EngineCall::GetSpanContents(span) => EngineCall::GetSpanContents(span),
EngineCall::EvalClosure {
closure,
positional,
input,
redirect_stdout,
redirect_stderr,
} => EngineCall::EvalClosure {
closure,
positional,
input: f(input)?,
redirect_stdout,
redirect_stderr,
},
})
}
}
/// The response to an [`EngineCall`]. The type parameter determines the output type for pipeline
/// data.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum EngineCallResponse<D> {
Error(ShellError),
PipelineData(D),
Config(Box<Config>),
ValueMap(HashMap<String, Value>),
}
impl<D> EngineCallResponse<D> {
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
/// not contain data.
pub(crate) fn map_data<T>(
self,
f: impl FnOnce(D) -> Result<T, ShellError>,
) -> Result<EngineCallResponse<T>, ShellError> {
Ok(match self {
EngineCallResponse::Error(err) => EngineCallResponse::Error(err),
EngineCallResponse::PipelineData(data) => EngineCallResponse::PipelineData(f(data)?),
EngineCallResponse::Config(config) => EngineCallResponse::Config(config),
EngineCallResponse::ValueMap(map) => EngineCallResponse::ValueMap(map),
})
}
}
impl EngineCallResponse<PipelineData> {
/// Build an [`EngineCallResponse::PipelineData`] from a [`Value`]
pub(crate) fn value(value: Value) -> EngineCallResponse<PipelineData> {
EngineCallResponse::PipelineData(PipelineData::Value(value, None))
}
/// An [`EngineCallResponse::PipelineData`] with [`PipelineData::Empty`]
pub(crate) const fn empty() -> EngineCallResponse<PipelineData> {
EngineCallResponse::PipelineData(PipelineData::Empty)
}
}

View File

@ -1,402 +0,0 @@
use std::cmp::Ordering;
use std::sync::Arc;
use crate::{
plugin::{PluginInterface, PluginSource},
util::with_custom_values_in,
};
use nu_protocol::{ast::Operator, CustomValue, IntoSpanned, ShellError, Span, Spanned, Value};
use nu_utils::SharedCow;
use serde::{Deserialize, Serialize};
#[cfg(test)]
mod tests;
/// An opaque container for a custom value that is handled fully by a plugin
///
/// This is the only type of custom value that is allowed to cross the plugin serialization
/// boundary.
///
/// [`EngineInterface`](crate::interface::EngineInterface) is responsible for ensuring
/// that local plugin custom values are converted to and from [`PluginCustomData`] on the boundary.
///
/// [`PluginInterface`](crate::interface::PluginInterface) is responsible for adding the
/// appropriate [`PluginSource`](crate::plugin::PluginSource), ensuring that only
/// [`PluginCustomData`] is contained within any values sent, and that the `source` of any
/// values sent matches the plugin it is being sent to.
///
/// This is not a public API.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[doc(hidden)]
pub struct PluginCustomValue {
#[serde(flatten)]
shared: SharedCow<SharedContent>,
/// Which plugin the custom value came from. This is not defined on the plugin side. The engine
/// side is responsible for maintaining it, and it is not sent over the serialization boundary.
#[serde(skip, default)]
source: Option<Arc<PluginSource>>,
}
/// Content shared across copies of a plugin custom value.
#[derive(Clone, Debug, Serialize, Deserialize)]
struct SharedContent {
/// The name of the type of the custom value as defined by the plugin (`type_name()`)
name: String,
/// The bincoded representation of the custom value on the plugin side
data: Vec<u8>,
/// True if the custom value should notify the source if all copies of it are dropped.
///
/// This is not serialized if `false`, since most custom values don't need it.
#[serde(default, skip_serializing_if = "is_false")]
notify_on_drop: bool,
}
fn is_false(b: &bool) -> bool {
!b
}
impl PluginCustomValue {
pub fn into_value(self, span: Span) -> Value {
Value::custom(Box::new(self), span)
}
}
#[typetag::serde]
impl CustomValue for PluginCustomValue {
fn clone_value(&self, span: Span) -> Value {
self.clone().into_value(span)
}
fn type_name(&self) -> String {
self.name().to_owned()
}
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
self.get_plugin(Some(span), "get base value")?
.custom_value_to_base_value(self.clone().into_spanned(span))
}
fn follow_path_int(
&self,
self_span: Span,
index: usize,
path_span: Span,
) -> Result<Value, ShellError> {
self.get_plugin(Some(self_span), "follow cell path")?
.custom_value_follow_path_int(
self.clone().into_spanned(self_span),
index.into_spanned(path_span),
)
}
fn follow_path_string(
&self,
self_span: Span,
column_name: String,
path_span: Span,
) -> Result<Value, ShellError> {
self.get_plugin(Some(self_span), "follow cell path")?
.custom_value_follow_path_string(
self.clone().into_spanned(self_span),
column_name.into_spanned(path_span),
)
}
fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
self.get_plugin(Some(other.span()), "perform comparison")
.and_then(|plugin| {
// We're passing Span::unknown() here because we don't have one, and it probably
// shouldn't matter here and is just a consequence of the API
plugin.custom_value_partial_cmp(self.clone(), other.clone())
})
.unwrap_or_else(|err| {
// We can't do anything with the error other than log it.
log::warn!(
"Error in partial_cmp on plugin custom value (source={source:?}): {err}",
source = self.source
);
None
})
.map(|ordering| ordering.into())
}
fn operation(
&self,
lhs_span: Span,
operator: Operator,
op_span: Span,
right: &Value,
) -> Result<Value, ShellError> {
self.get_plugin(Some(lhs_span), "invoke operator")?
.custom_value_operation(
self.clone().into_spanned(lhs_span),
operator.into_spanned(op_span),
right.clone(),
)
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
self
}
}
impl PluginCustomValue {
/// Create a new [`PluginCustomValue`].
pub(crate) fn new(
name: String,
data: Vec<u8>,
notify_on_drop: bool,
source: Option<Arc<PluginSource>>,
) -> PluginCustomValue {
PluginCustomValue {
shared: SharedCow::new(SharedContent {
name,
data,
notify_on_drop,
}),
source,
}
}
/// The name of the type of the custom value as defined by the plugin (`type_name()`)
pub fn name(&self) -> &str {
&self.shared.name
}
/// The bincoded representation of the custom value on the plugin side
pub fn data(&self) -> &[u8] {
&self.shared.data
}
/// True if the custom value should notify the source if all copies of it are dropped.
pub fn notify_on_drop(&self) -> bool {
self.shared.notify_on_drop
}
/// Which plugin the custom value came from. This is not defined on the plugin side. The engine
/// side is responsible for maintaining it, and it is not sent over the serialization boundary.
pub fn source(&self) -> &Option<Arc<PluginSource>> {
&self.source
}
/// Set the [`PluginSource`] for this [`PluginCustomValue`].
pub fn set_source(&mut self, source: Option<Arc<PluginSource>>) {
self.source = source;
}
/// Create the [`PluginCustomValue`] with the given source.
#[cfg(test)]
pub(crate) fn with_source(mut self, source: Option<Arc<PluginSource>>) -> PluginCustomValue {
self.source = source;
self
}
/// Helper to get the plugin to implement an op
fn get_plugin(&self, span: Option<Span>, for_op: &str) -> Result<PluginInterface, ShellError> {
let wrap_err = |err: ShellError| ShellError::GenericError {
error: format!(
"Unable to spawn plugin `{}` to {for_op}",
self.source
.as_ref()
.map(|s| s.name())
.unwrap_or("<unknown>")
),
msg: err.to_string(),
span,
help: None,
inner: vec![err],
};
let source = self.source.clone().ok_or_else(|| {
wrap_err(ShellError::NushellFailed {
msg: "The plugin source for the custom value was not set".into(),
})
})?;
source
.persistent(span)
.and_then(|p| p.get_plugin(None))
.map_err(wrap_err)
}
/// Serialize a custom value into a [`PluginCustomValue`]. This should only be done on the
/// plugin side.
pub fn serialize_from_custom_value(
custom_value: &dyn CustomValue,
span: Span,
) -> Result<PluginCustomValue, ShellError> {
let name = custom_value.type_name();
let notify_on_drop = custom_value.notify_plugin_on_drop();
bincode::serialize(custom_value)
.map(|data| PluginCustomValue::new(name, data, notify_on_drop, None))
.map_err(|err| ShellError::CustomValueFailedToEncode {
msg: err.to_string(),
span,
})
}
/// Deserialize a [`PluginCustomValue`] into a `Box<dyn CustomValue>`. This should only be done
/// on the plugin side.
pub fn deserialize_to_custom_value(
&self,
span: Span,
) -> Result<Box<dyn CustomValue>, ShellError> {
bincode::deserialize::<Box<dyn CustomValue>>(self.data()).map_err(|err| {
ShellError::CustomValueFailedToDecode {
msg: err.to_string(),
span,
}
})
}
/// Add a [`PluginSource`] to the given [`CustomValue`] if it is a [`PluginCustomValue`].
pub fn add_source(value: &mut dyn CustomValue, source: &Arc<PluginSource>) {
if let Some(custom_value) = value.as_mut_any().downcast_mut::<PluginCustomValue>() {
custom_value.set_source(Some(source.clone()));
}
}
/// Add a [`PluginSource`] to all [`PluginCustomValue`]s within the value, recursively.
pub fn add_source_in(value: &mut Value, source: &Arc<PluginSource>) -> Result<(), ShellError> {
with_custom_values_in(value, |custom_value| {
Self::add_source(custom_value.item, source);
Ok::<_, ShellError>(())
})
}
/// Check that a [`CustomValue`] is a [`PluginCustomValue`] that come from the given `source`,
/// and return an error if not.
///
/// This method will collapse `LazyRecord` in-place as necessary to make the guarantee,
/// since `LazyRecord` could return something different the next time it is called.
pub fn verify_source(
value: Spanned<&dyn CustomValue>,
source: &PluginSource,
) -> Result<(), ShellError> {
if let Some(custom_value) = value.item.as_any().downcast_ref::<PluginCustomValue>() {
if custom_value
.source
.as_ref()
.map(|s| s.is_compatible(source))
.unwrap_or(false)
{
Ok(())
} else {
Err(ShellError::CustomValueIncorrectForPlugin {
name: custom_value.name().to_owned(),
span: value.span,
dest_plugin: source.name().to_owned(),
src_plugin: custom_value.source.as_ref().map(|s| s.name().to_owned()),
})
}
} else {
// Only PluginCustomValues can be sent
Err(ShellError::CustomValueIncorrectForPlugin {
name: value.item.type_name(),
span: value.span,
dest_plugin: source.name().to_owned(),
src_plugin: None,
})
}
}
/// Convert all plugin-native custom values to [`PluginCustomValue`] within the given `value`,
/// recursively. This should only be done on the plugin side.
pub fn serialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
value.recurse_mut(&mut |value| {
let span = value.span();
match value {
Value::Custom { ref val, .. } => {
if val.as_any().downcast_ref::<PluginCustomValue>().is_some() {
// Already a PluginCustomValue
Ok(())
} else {
let serialized = Self::serialize_from_custom_value(&**val, span)?;
*value = Value::custom(Box::new(serialized), span);
Ok(())
}
}
// Collect LazyRecord before proceeding
Value::LazyRecord { ref val, .. } => {
*value = val.collect()?;
Ok(())
}
_ => Ok(()),
}
})
}
/// Convert all [`PluginCustomValue`]s to plugin-native custom values within the given `value`,
/// recursively. This should only be done on the plugin side.
pub fn deserialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
value.recurse_mut(&mut |value| {
let span = value.span();
match value {
Value::Custom { ref val, .. } => {
if let Some(val) = val.as_any().downcast_ref::<PluginCustomValue>() {
let deserialized = val.deserialize_to_custom_value(span)?;
*value = Value::custom(deserialized, span);
Ok(())
} else {
// Already not a PluginCustomValue
Ok(())
}
}
// Collect LazyRecord before proceeding
Value::LazyRecord { ref val, .. } => {
*value = val.collect()?;
Ok(())
}
_ => Ok(()),
}
})
}
/// Render any custom values in the `Value` using `to_base_value()`
pub fn render_to_base_value_in(value: &mut Value) -> Result<(), ShellError> {
value.recurse_mut(&mut |value| {
let span = value.span();
match value {
Value::Custom { ref val, .. } => {
*value = val.to_base_value(span)?;
Ok(())
}
// Collect LazyRecord before proceeding
Value::LazyRecord { ref val, .. } => {
*value = val.collect()?;
Ok(())
}
_ => Ok(()),
}
})
}
}
impl Drop for PluginCustomValue {
fn drop(&mut self) {
// If the custom value specifies notify_on_drop and this is the last copy, we need to let
// the plugin know about it if we can.
if self.source.is_some() && self.notify_on_drop() && SharedCow::ref_count(&self.shared) == 1
{
self.get_plugin(None, "drop")
// While notifying drop, we don't need a copy of the source
.and_then(|plugin| {
plugin.custom_value_dropped(PluginCustomValue {
shared: self.shared.clone(),
source: None,
})
})
.unwrap_or_else(|err| {
// We shouldn't do anything with the error except log it
let name = self.name();
log::warn!("Failed to notify drop of custom value ({name}): {err}")
});
}
}
}

View File

@ -1,395 +0,0 @@
use super::PluginCustomValue;
use crate::{
plugin::PluginSource,
protocol::test_util::{
expected_test_custom_value, test_plugin_custom_value, test_plugin_custom_value_with_source,
TestCustomValue,
},
};
use nu_protocol::{engine::Closure, record, CustomValue, IntoSpanned, ShellError, Span, Value};
use std::sync::Arc;
#[test]
fn serialize_deserialize() -> Result<(), ShellError> {
let original_value = TestCustomValue(32);
let span = Span::test_data();
let serialized = PluginCustomValue::serialize_from_custom_value(&original_value, span)?;
assert_eq!(original_value.type_name(), serialized.name());
assert!(serialized.source.is_none());
let deserialized = serialized.deserialize_to_custom_value(span)?;
let downcasted = deserialized
.as_any()
.downcast_ref::<TestCustomValue>()
.expect("failed to downcast: not TestCustomValue");
assert_eq!(original_value, *downcasted);
Ok(())
}
#[test]
fn expected_serialize_output() -> Result<(), ShellError> {
let original_value = expected_test_custom_value();
let span = Span::test_data();
let serialized = PluginCustomValue::serialize_from_custom_value(&original_value, span)?;
assert_eq!(
test_plugin_custom_value().data(),
serialized.data(),
"The bincode configuration is probably different from what we expected. \
Fix test_plugin_custom_value() to match it"
);
Ok(())
}
#[test]
fn add_source_in_at_root() -> Result<(), ShellError> {
let mut val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
let source = Arc::new(PluginSource::new_fake("foo"));
PluginCustomValue::add_source_in(&mut val, &source)?;
let custom_value = val.as_custom_value()?;
let plugin_custom_value: &PluginCustomValue = custom_value
.as_any()
.downcast_ref()
.expect("not PluginCustomValue");
assert_eq!(
Some(Arc::as_ptr(&source)),
plugin_custom_value.source.as_ref().map(Arc::as_ptr)
);
Ok(())
}
fn check_record_custom_values(
val: &Value,
keys: &[&str],
mut f: impl FnMut(&str, &dyn CustomValue) -> Result<(), ShellError>,
) -> Result<(), ShellError> {
let record = val.as_record()?;
for key in keys {
let val = record
.get(key)
.unwrap_or_else(|| panic!("record does not contain '{key}'"));
let custom_value = val
.as_custom_value()
.unwrap_or_else(|_| panic!("'{key}' not custom value"));
f(key, custom_value)?;
}
Ok(())
}
#[test]
fn add_source_in_nested_record() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
let mut val = Value::test_record(record! {
"foo" => orig_custom_val.clone(),
"bar" => orig_custom_val.clone(),
});
let source = Arc::new(PluginSource::new_fake("foo"));
PluginCustomValue::add_source_in(&mut val, &source)?;
check_record_custom_values(&val, &["foo", "bar"], |key, custom_value| {
let plugin_custom_value: &PluginCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("'{key}' not PluginCustomValue"));
assert_eq!(
Some(Arc::as_ptr(&source)),
plugin_custom_value.source.as_ref().map(Arc::as_ptr),
"'{key}' source not set correctly"
);
Ok(())
})
}
fn check_list_custom_values(
val: &Value,
indices: impl IntoIterator<Item = usize>,
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
) -> Result<(), ShellError> {
let list = val.as_list()?;
for index in indices {
let val = list
.get(index)
.unwrap_or_else(|| panic!("[{index}] not present in list"));
let custom_value = val
.as_custom_value()
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
f(index, custom_value)?;
}
Ok(())
}
#[test]
fn add_source_in_nested_list() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
let mut val = Value::test_list(vec![orig_custom_val.clone(), orig_custom_val.clone()]);
let source = Arc::new(PluginSource::new_fake("foo"));
PluginCustomValue::add_source_in(&mut val, &source)?;
check_list_custom_values(&val, 0..=1, |index, custom_value| {
let plugin_custom_value: &PluginCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValue"));
assert_eq!(
Some(Arc::as_ptr(&source)),
plugin_custom_value.source.as_ref().map(Arc::as_ptr),
"[{index}] source not set correctly"
);
Ok(())
})
}
fn check_closure_custom_values(
val: &Value,
indices: impl IntoIterator<Item = usize>,
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
) -> Result<(), ShellError> {
let closure = val.as_closure()?;
for index in indices {
let val = closure
.captures
.get(index)
.unwrap_or_else(|| panic!("[{index}] not present in closure"));
let custom_value = val
.1
.as_custom_value()
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
f(index, custom_value)?;
}
Ok(())
}
#[test]
fn add_source_in_nested_closure() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
let mut val = Value::test_closure(Closure {
block_id: 0,
captures: vec![(0, orig_custom_val.clone()), (1, orig_custom_val.clone())],
});
let source = Arc::new(PluginSource::new_fake("foo"));
PluginCustomValue::add_source_in(&mut val, &source)?;
check_closure_custom_values(&val, 0..=1, |index, custom_value| {
let plugin_custom_value: &PluginCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValue"));
assert_eq!(
Some(Arc::as_ptr(&source)),
plugin_custom_value.source.as_ref().map(Arc::as_ptr),
"[{index}] source not set correctly"
);
Ok(())
})
}
#[test]
fn verify_source_error_message() -> Result<(), ShellError> {
let span = Span::new(5, 7);
let ok_val = test_plugin_custom_value_with_source();
let native_val = TestCustomValue(32);
let foreign_val = {
let mut val = test_plugin_custom_value();
val.source = Some(Arc::new(PluginSource::new_fake("other")));
val
};
let source = PluginSource::new_fake("test");
PluginCustomValue::verify_source((&ok_val as &dyn CustomValue).into_spanned(span), &source)
.expect("ok_val should be verified ok");
for (val, src_plugin) in [
(&native_val as &dyn CustomValue, None),
(&foreign_val as &dyn CustomValue, Some("other")),
] {
let error = PluginCustomValue::verify_source(val.into_spanned(span), &source).expect_err(
&format!("a custom value from {src_plugin:?} should result in an error"),
);
if let ShellError::CustomValueIncorrectForPlugin {
name,
span: err_span,
dest_plugin,
src_plugin: err_src_plugin,
} = error
{
assert_eq!("TestCustomValue", name, "error.name from {src_plugin:?}");
assert_eq!(span, err_span, "error.span from {src_plugin:?}");
assert_eq!("test", dest_plugin, "error.dest_plugin from {src_plugin:?}");
assert_eq!(src_plugin, err_src_plugin.as_deref(), "error.src_plugin");
} else {
panic!("the error returned should be CustomValueIncorrectForPlugin");
}
}
Ok(())
}
#[test]
fn serialize_in_root() -> Result<(), ShellError> {
let span = Span::new(4, 10);
let mut val = Value::custom(Box::new(expected_test_custom_value()), span);
PluginCustomValue::serialize_custom_values_in(&mut val)?;
assert_eq!(span, val.span());
let custom_value = val.as_custom_value()?;
if let Some(plugin_custom_value) = custom_value.as_any().downcast_ref::<PluginCustomValue>() {
assert_eq!("TestCustomValue", plugin_custom_value.name());
assert_eq!(
test_plugin_custom_value().data(),
plugin_custom_value.data()
);
assert!(plugin_custom_value.source.is_none());
} else {
panic!("Failed to downcast to PluginCustomValue");
}
Ok(())
}
#[test]
fn serialize_in_record() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(TestCustomValue(32)));
let mut val = Value::test_record(record! {
"foo" => orig_custom_val.clone(),
"bar" => orig_custom_val.clone(),
});
PluginCustomValue::serialize_custom_values_in(&mut val)?;
check_record_custom_values(&val, &["foo", "bar"], |key, custom_value| {
let plugin_custom_value: &PluginCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("'{key}' not PluginCustomValue"));
assert_eq!(
"TestCustomValue",
plugin_custom_value.name(),
"'{key}' name not set correctly"
);
Ok(())
})
}
#[test]
fn serialize_in_list() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(TestCustomValue(24)));
let mut val = Value::test_list(vec![orig_custom_val.clone(), orig_custom_val.clone()]);
PluginCustomValue::serialize_custom_values_in(&mut val)?;
check_list_custom_values(&val, 0..=1, |index, custom_value| {
let plugin_custom_value: &PluginCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValue"));
assert_eq!(
"TestCustomValue",
plugin_custom_value.name(),
"[{index}] name not set correctly"
);
Ok(())
})
}
#[test]
fn serialize_in_closure() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(TestCustomValue(24)));
let mut val = Value::test_closure(Closure {
block_id: 0,
captures: vec![(0, orig_custom_val.clone()), (1, orig_custom_val.clone())],
});
PluginCustomValue::serialize_custom_values_in(&mut val)?;
check_closure_custom_values(&val, 0..=1, |index, custom_value| {
let plugin_custom_value: &PluginCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValue"));
assert_eq!(
"TestCustomValue",
plugin_custom_value.name(),
"[{index}] name not set correctly"
);
Ok(())
})
}
#[test]
fn deserialize_in_root() -> Result<(), ShellError> {
let span = Span::new(4, 10);
let mut val = Value::custom(Box::new(test_plugin_custom_value()), span);
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
assert_eq!(span, val.span());
let custom_value = val.as_custom_value()?;
if let Some(test_custom_value) = custom_value.as_any().downcast_ref::<TestCustomValue>() {
assert_eq!(expected_test_custom_value(), *test_custom_value);
} else {
panic!("Failed to downcast to TestCustomValue");
}
Ok(())
}
#[test]
fn deserialize_in_record() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
let mut val = Value::test_record(record! {
"foo" => orig_custom_val.clone(),
"bar" => orig_custom_val.clone(),
});
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
check_record_custom_values(&val, &["foo", "bar"], |key, custom_value| {
let test_custom_value: &TestCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("'{key}' not TestCustomValue"));
assert_eq!(
expected_test_custom_value(),
*test_custom_value,
"{key} not deserialized correctly"
);
Ok(())
})
}
#[test]
fn deserialize_in_list() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
let mut val = Value::test_list(vec![orig_custom_val.clone(), orig_custom_val.clone()]);
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
check_list_custom_values(&val, 0..=1, |index, custom_value| {
let test_custom_value: &TestCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("[{index}] not TestCustomValue"));
assert_eq!(
expected_test_custom_value(),
*test_custom_value,
"[{index}] name not deserialized correctly"
);
Ok(())
})
}
#[test]
fn deserialize_in_closure() -> Result<(), ShellError> {
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
let mut val = Value::test_closure(Closure {
block_id: 0,
captures: vec![(0, orig_custom_val.clone()), (1, orig_custom_val.clone())],
});
PluginCustomValue::deserialize_custom_values_in(&mut val)?;
check_closure_custom_values(&val, 0..=1, |index, custom_value| {
let test_custom_value: &TestCustomValue = custom_value
.as_any()
.downcast_ref()
.unwrap_or_else(|| panic!("[{index}] not TestCustomValue"));
assert_eq!(
expected_test_custom_value(),
*test_custom_value,
"[{index}] name not deserialized correctly"
);
Ok(())
})
}

View File

@ -1,106 +0,0 @@
use nu_protocol::ShellError;
use serde::{Deserialize, Serialize};
/// Protocol information, sent as a `Hello` message on initialization. This determines the
/// compatibility of the plugin and engine. They are considered to be compatible if the lower
/// version is semver compatible with the higher one.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProtocolInfo {
/// The name of the protocol being implemented. Only one protocol is supported. This field
/// can be safely ignored, because not matching is a deserialization error
pub protocol: Protocol,
/// The semantic version of the protocol. This should be the version of the `nu-plugin`
/// crate
pub version: String,
/// Supported optional features. This helps to maintain semver compatibility when adding new
/// features
pub features: Vec<Feature>,
}
impl Default for ProtocolInfo {
fn default() -> ProtocolInfo {
ProtocolInfo {
protocol: Protocol::NuPlugin,
version: env!("CARGO_PKG_VERSION").into(),
features: default_features(),
}
}
}
impl ProtocolInfo {
/// True if the version specified in `self` is compatible with the version specified in `other`.
pub fn is_compatible_with(&self, other: &ProtocolInfo) -> Result<bool, ShellError> {
fn parse_failed(error: semver::Error) -> ShellError {
ShellError::PluginFailedToLoad {
msg: format!("Failed to parse protocol version: {error}"),
}
}
let mut versions = [
semver::Version::parse(&self.version).map_err(parse_failed)?,
semver::Version::parse(&other.version).map_err(parse_failed)?,
];
versions.sort();
// For example, if the lower version is 1.1.0, and the higher version is 1.2.3, the
// requirement is that 1.2.3 matches ^1.1.0 (which it does)
Ok(semver::Comparator {
op: semver::Op::Caret,
major: versions[0].major,
minor: Some(versions[0].minor),
patch: Some(versions[0].patch),
pre: versions[0].pre.clone(),
}
.matches(&versions[1]))
}
/// True if the protocol info contains a feature compatible with the given feature.
pub fn supports_feature(&self, feature: &Feature) -> bool {
self.features.iter().any(|f| feature.is_compatible_with(f))
}
}
/// Indicates the protocol in use. Only one protocol is supported.
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub enum Protocol {
/// Serializes to the value `"nu-plugin"`
#[serde(rename = "nu-plugin")]
#[default]
NuPlugin,
}
/// Indicates optional protocol features. This can help to make non-breaking-change additions to
/// the protocol. Features are not restricted to plain strings and can contain additional
/// configuration data.
///
/// Optional features should not be used by the protocol if they are not present in the
/// [`ProtocolInfo`] sent by the other side.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "name")]
pub enum Feature {
/// The plugin supports running with a local socket passed via `--local-socket` instead of
/// stdio.
LocalSocket,
/// A feature that was not recognized on deserialization. Attempting to serialize this feature
/// is an error. Matching against it may only be used if necessary to determine whether
/// unsupported features are present.
#[serde(other, skip_serializing)]
Unknown,
}
impl Feature {
/// True if the feature is considered to be compatible with another feature.
pub fn is_compatible_with(&self, other: &Feature) -> bool {
matches!((self, other), (Feature::LocalSocket, Feature::LocalSocket))
}
}
/// Protocol features compiled into this version of `nu-plugin`.
pub fn default_features() -> Vec<Feature> {
vec![
// Only available if compiled with the `local-socket` feature flag (enabled by default).
#[cfg(feature = "local-socket")]
Feature::LocalSocket,
]
}

View File

@ -1,45 +0,0 @@
use super::PluginCustomValue;
use crate::plugin::PluginSource;
use nu_protocol::{CustomValue, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct TestCustomValue(pub i32);
#[typetag::serde]
impl CustomValue for TestCustomValue {
fn clone_value(&self, span: Span) -> Value {
Value::custom(Box::new(self.clone()), span)
}
fn type_name(&self) -> String {
"TestCustomValue".into()
}
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
Ok(Value::int(self.0 as i64, span))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
self
}
}
pub(crate) fn test_plugin_custom_value() -> PluginCustomValue {
let data = bincode::serialize(&expected_test_custom_value() as &dyn CustomValue)
.expect("bincode serialization of the expected_test_custom_value() failed");
PluginCustomValue::new("TestCustomValue".into(), data, false, None)
}
pub(crate) fn expected_test_custom_value() -> TestCustomValue {
TestCustomValue(-1)
}
pub(crate) fn test_plugin_custom_value_with_source() -> PluginCustomValue {
test_plugin_custom_value().with_source(Some(PluginSource::new_fake("test").into()))
}

View File

@ -1,35 +0,0 @@
use super::*;
#[test]
fn protocol_info_compatible() -> Result<(), ShellError> {
let ver_1_2_3 = ProtocolInfo {
protocol: Protocol::NuPlugin,
version: "1.2.3".into(),
features: vec![],
};
let ver_1_1_0 = ProtocolInfo {
protocol: Protocol::NuPlugin,
version: "1.1.0".into(),
features: vec![],
};
assert!(ver_1_1_0.is_compatible_with(&ver_1_2_3)?);
assert!(ver_1_2_3.is_compatible_with(&ver_1_1_0)?);
Ok(())
}
#[test]
fn protocol_info_incompatible() -> Result<(), ShellError> {
let ver_2_0_0 = ProtocolInfo {
protocol: Protocol::NuPlugin,
version: "2.0.0".into(),
features: vec![],
};
let ver_1_1_0 = ProtocolInfo {
protocol: Protocol::NuPlugin,
version: "1.1.0".into(),
features: vec![],
};
assert!(!ver_2_0_0.is_compatible_with(&ver_1_1_0)?);
assert!(!ver_1_1_0.is_compatible_with(&ver_2_0_0)?);
Ok(())
}

View File

@ -1,64 +0,0 @@
use nu_protocol::ShellError;
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
/// Implements an atomically incrementing sequential series of numbers
#[derive(Debug, Default)]
pub struct Sequence(AtomicUsize);
impl Sequence {
/// Return the next available id from a sequence, returning an error on overflow
#[track_caller]
pub(crate) fn next(&self) -> Result<usize, ShellError> {
// It's totally safe to use Relaxed ordering here, as there aren't other memory operations
// that depend on this value having been set for safety
//
// We're only not using `fetch_add` so that we can check for overflow, as wrapping with the
// identifier would lead to a serious bug - however unlikely that is.
self.0
.fetch_update(Relaxed, Relaxed, |current| current.checked_add(1))
.map_err(|_| ShellError::NushellFailedHelp {
msg: "an accumulator for identifiers overflowed".into(),
help: format!("see {}", std::panic::Location::caller()),
})
}
}
#[test]
fn output_is_sequential() {
let sequence = Sequence::default();
for (expected, generated) in (0..1000).zip(std::iter::repeat_with(|| sequence.next())) {
assert_eq!(expected, generated.expect("error in sequence"));
}
}
#[test]
fn output_is_unique_even_under_contention() {
let sequence = Sequence::default();
std::thread::scope(|scope| {
// Spawn four threads, all advancing the sequence simultaneously
let threads = (0..4)
.map(|_| {
scope.spawn(|| {
(0..100000)
.map(|_| sequence.next())
.collect::<Result<Vec<_>, _>>()
})
})
.collect::<Vec<_>>();
// Collect all of the results into a single flat vec
let mut results = threads
.into_iter()
.flat_map(|thread| thread.join().expect("panicked").expect("error"))
.collect::<Vec<usize>>();
// Check uniqueness
results.sort();
let initial_length = results.len();
results.dedup();
let deduplicated_length = results.len();
assert_eq!(initial_length, deduplicated_length);
})
}

View File

@ -1,134 +0,0 @@
use crate::{
plugin::{Encoder, PluginEncoder},
protocol::{PluginInput, PluginOutput},
};
use nu_protocol::ShellError;
use serde::Deserialize;
/// A `PluginEncoder` that enables the plugin to communicate with Nushell with JSON
/// serialized data.
///
/// Each message in the stream is followed by a newline when serializing, but is not required for
/// deserialization. The output is not pretty printed and each object does not contain newlines.
/// If it is more convenient, a plugin may choose to separate messages by newline.
#[derive(Clone, Copy, Debug)]
pub struct JsonSerializer;
impl PluginEncoder for JsonSerializer {
fn name(&self) -> &str {
"json"
}
}
impl Encoder<PluginInput> for JsonSerializer {
fn encode(
&self,
plugin_input: &PluginInput,
writer: &mut impl std::io::Write,
) -> Result<(), nu_protocol::ShellError> {
serde_json::to_writer(&mut *writer, plugin_input).map_err(json_encode_err)?;
writer.write_all(b"\n").map_err(|err| ShellError::IOError {
msg: err.to_string(),
})
}
fn decode(
&self,
reader: &mut impl std::io::BufRead,
) -> Result<Option<PluginInput>, nu_protocol::ShellError> {
let mut de = serde_json::Deserializer::from_reader(reader);
PluginInput::deserialize(&mut de)
.map(Some)
.or_else(json_decode_err)
}
}
impl Encoder<PluginOutput> for JsonSerializer {
fn encode(
&self,
plugin_output: &PluginOutput,
writer: &mut impl std::io::Write,
) -> Result<(), ShellError> {
serde_json::to_writer(&mut *writer, plugin_output).map_err(json_encode_err)?;
writer.write_all(b"\n").map_err(|err| ShellError::IOError {
msg: err.to_string(),
})
}
fn decode(
&self,
reader: &mut impl std::io::BufRead,
) -> Result<Option<PluginOutput>, ShellError> {
let mut de = serde_json::Deserializer::from_reader(reader);
PluginOutput::deserialize(&mut de)
.map(Some)
.or_else(json_decode_err)
}
}
/// Handle a `serde_json` encode error.
fn json_encode_err(err: serde_json::Error) -> ShellError {
if err.is_io() {
ShellError::IOError {
msg: err.to_string(),
}
} else {
ShellError::PluginFailedToEncode {
msg: err.to_string(),
}
}
}
/// Handle a `serde_json` decode error. Returns `Ok(None)` on eof.
fn json_decode_err<T>(err: serde_json::Error) -> Result<Option<T>, ShellError> {
if err.is_eof() {
Ok(None)
} else if err.is_io() {
Err(ShellError::IOError {
msg: err.to_string(),
})
} else {
Err(ShellError::PluginFailedToDecode {
msg: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
crate::serializers::tests::generate_tests!(JsonSerializer {});
#[test]
fn json_ends_in_newline() {
let mut out = vec![];
JsonSerializer {}
.encode(&PluginInput::Call(0, PluginCall::Signature), &mut out)
.expect("serialization error");
let string = std::str::from_utf8(&out).expect("utf-8 error");
assert!(
string.ends_with('\n'),
"doesn't end with newline: {:?}",
string
);
}
#[test]
fn json_has_no_other_newlines() {
let mut out = vec![];
// use something deeply nested, to try to trigger any pretty printing
let output = PluginOutput::Data(
0,
StreamData::List(Value::test_list(vec![
Value::test_int(4),
// in case escaping failed
Value::test_string("newline\ncontaining\nstring"),
])),
);
JsonSerializer {}
.encode(&output, &mut out)
.expect("serialization error");
let string = std::str::from_utf8(&out).expect("utf-8 error");
assert_eq!(1, string.chars().filter(|ch| *ch == '\n').count());
}
}

View File

@ -1,45 +0,0 @@
use crate::plugin::Encoder;
use nu_protocol::ShellError;
pub mod json;
pub mod msgpack;
#[cfg(test)]
mod tests;
#[doc(hidden)]
#[derive(Clone, Copy, Debug)]
pub enum EncodingType {
Json(json::JsonSerializer),
MsgPack(msgpack::MsgPackSerializer),
}
impl EncodingType {
pub fn try_from_bytes(bytes: &[u8]) -> Option<Self> {
match bytes {
b"json" => Some(Self::Json(json::JsonSerializer {})),
b"msgpack" => Some(Self::MsgPack(msgpack::MsgPackSerializer {})),
_ => None,
}
}
}
impl<T> Encoder<T> for EncodingType
where
json::JsonSerializer: Encoder<T>,
msgpack::MsgPackSerializer: Encoder<T>,
{
fn encode(&self, data: &T, writer: &mut impl std::io::Write) -> Result<(), ShellError> {
match self {
EncodingType::Json(encoder) => encoder.encode(data, writer),
EncodingType::MsgPack(encoder) => encoder.encode(data, writer),
}
}
fn decode(&self, reader: &mut impl std::io::BufRead) -> Result<Option<T>, ShellError> {
match self {
EncodingType::Json(encoder) => encoder.decode(reader),
EncodingType::MsgPack(encoder) => encoder.decode(reader),
}
}
}

View File

@ -1,109 +0,0 @@
use std::io::ErrorKind;
use crate::{
plugin::{Encoder, PluginEncoder},
protocol::{PluginInput, PluginOutput},
};
use nu_protocol::ShellError;
use serde::Deserialize;
/// A `PluginEncoder` that enables the plugin to communicate with Nushell with MsgPack
/// serialized data.
///
/// Each message is written as a MessagePack object. There is no message envelope or separator.
#[derive(Clone, Copy, Debug)]
pub struct MsgPackSerializer;
impl PluginEncoder for MsgPackSerializer {
fn name(&self) -> &str {
"msgpack"
}
}
impl Encoder<PluginInput> for MsgPackSerializer {
fn encode(
&self,
plugin_input: &PluginInput,
writer: &mut impl std::io::Write,
) -> Result<(), nu_protocol::ShellError> {
rmp_serde::encode::write_named(writer, plugin_input).map_err(rmp_encode_err)
}
fn decode(
&self,
reader: &mut impl std::io::BufRead,
) -> Result<Option<PluginInput>, ShellError> {
let mut de = rmp_serde::Deserializer::new(reader);
PluginInput::deserialize(&mut de)
.map(Some)
.or_else(rmp_decode_err)
}
}
impl Encoder<PluginOutput> for MsgPackSerializer {
fn encode(
&self,
plugin_output: &PluginOutput,
writer: &mut impl std::io::Write,
) -> Result<(), ShellError> {
rmp_serde::encode::write_named(writer, plugin_output).map_err(rmp_encode_err)
}
fn decode(
&self,
reader: &mut impl std::io::BufRead,
) -> Result<Option<PluginOutput>, ShellError> {
let mut de = rmp_serde::Deserializer::new(reader);
PluginOutput::deserialize(&mut de)
.map(Some)
.or_else(rmp_decode_err)
}
}
/// Handle a msgpack encode error
fn rmp_encode_err(err: rmp_serde::encode::Error) -> ShellError {
match err {
rmp_serde::encode::Error::InvalidValueWrite(_) => {
// I/O error
ShellError::IOError {
msg: err.to_string(),
}
}
_ => {
// Something else
ShellError::PluginFailedToEncode {
msg: err.to_string(),
}
}
}
}
/// Handle a msgpack decode error. Returns `Ok(None)` on eof
fn rmp_decode_err<T>(err: rmp_serde::decode::Error) -> Result<Option<T>, ShellError> {
match err {
rmp_serde::decode::Error::InvalidMarkerRead(err)
| rmp_serde::decode::Error::InvalidDataRead(err) => {
if matches!(err.kind(), ErrorKind::UnexpectedEof) {
// EOF
Ok(None)
} else {
// I/O error
Err(ShellError::IOError {
msg: err.to_string(),
})
}
}
_ => {
// Something else
Err(ShellError::PluginFailedToDecode {
msg: err.to_string(),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
crate::serializers::tests::generate_tests!(MsgPackSerializer {});
}

View File

@ -1,562 +0,0 @@
macro_rules! generate_tests {
($encoder:expr) => {
use crate::protocol::{
CallInfo, CustomValueOp, EvaluatedCall, PipelineDataHeader, PluginCall,
PluginCallResponse, PluginCustomValue, PluginInput, PluginOption, PluginOutput,
StreamData,
};
use nu_protocol::{
LabeledError, PluginSignature, Signature, Span, Spanned, SyntaxShape, Value,
};
#[test]
fn decode_eof() {
let mut buffer: &[u8] = &[];
let encoder = $encoder;
let result: Option<PluginInput> = encoder
.decode(&mut buffer)
.expect("eof should not result in an error");
assert!(result.is_none(), "decode result: {result:?}");
let result: Option<PluginOutput> = encoder
.decode(&mut buffer)
.expect("eof should not result in an error");
assert!(result.is_none(), "decode result: {result:?}");
}
#[test]
fn decode_io_error() {
struct ErrorProducer;
impl std::io::Read for ErrorProducer {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::from(std::io::ErrorKind::ConnectionReset))
}
}
let encoder = $encoder;
let mut buffered = std::io::BufReader::new(ErrorProducer);
match Encoder::<PluginInput>::decode(&encoder, &mut buffered) {
Ok(_) => panic!("decode: i/o error was not passed through"),
Err(ShellError::IOError { .. }) => (), // okay
Err(other) => panic!(
"decode: got other error, should have been a \
ShellError::IOError: {other:?}"
),
}
match Encoder::<PluginOutput>::decode(&encoder, &mut buffered) {
Ok(_) => panic!("decode: i/o error was not passed through"),
Err(ShellError::IOError { .. }) => (), // okay
Err(other) => panic!(
"decode: got other error, should have been a \
ShellError::IOError: {other:?}"
),
}
}
#[test]
fn decode_gibberish() {
// just a sequence of bytes that shouldn't be valid in anything we use
let gibberish: &[u8] = &[
0, 80, 74, 85, 117, 122, 86, 100, 74, 115, 20, 104, 55, 98, 67, 203, 83, 85, 77,
112, 74, 79, 254, 71, 80,
];
let encoder = $encoder;
let mut buffered = std::io::BufReader::new(&gibberish[..]);
match Encoder::<PluginInput>::decode(&encoder, &mut buffered) {
Ok(value) => panic!("decode: parsed successfully => {value:?}"),
Err(ShellError::PluginFailedToDecode { .. }) => (), // okay
Err(other) => panic!(
"decode: got other error, should have been a \
ShellError::PluginFailedToDecode: {other:?}"
),
}
let mut buffered = std::io::BufReader::new(&gibberish[..]);
match Encoder::<PluginOutput>::decode(&encoder, &mut buffered) {
Ok(value) => panic!("decode: parsed successfully => {value:?}"),
Err(ShellError::PluginFailedToDecode { .. }) => (), // okay
Err(other) => panic!(
"decode: got other error, should have been a \
ShellError::PluginFailedToDecode: {other:?}"
),
}
}
#[test]
fn call_round_trip_signature() {
let plugin_call = PluginCall::Signature;
let plugin_input = PluginInput::Call(0, plugin_call);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&plugin_input, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginInput::Call(0, PluginCall::Signature) => {}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn call_round_trip_run() {
let name = "test".to_string();
let input = Value::bool(false, Span::new(1, 20));
let call = EvaluatedCall {
head: Span::new(0, 10),
positional: vec![
Value::float(1.0, Span::new(0, 10)),
Value::string("something", Span::new(0, 10)),
],
named: vec![(
Spanned {
item: "name".to_string(),
span: Span::new(0, 10),
},
Some(Value::float(1.0, Span::new(0, 10))),
)],
};
let plugin_call = PluginCall::Run(CallInfo {
name: name.clone(),
call: call.clone(),
input: PipelineDataHeader::Value(input.clone()),
});
let plugin_input = PluginInput::Call(1, plugin_call);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&plugin_input, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginInput::Call(1, PluginCall::Run(call_info)) => {
assert_eq!(name, call_info.name);
assert_eq!(PipelineDataHeader::Value(input), call_info.input);
assert_eq!(call.head, call_info.call.head);
assert_eq!(call.positional.len(), call_info.call.positional.len());
call.positional
.iter()
.zip(call_info.call.positional.iter())
.for_each(|(lhs, rhs)| assert_eq!(lhs, rhs));
call.named
.iter()
.zip(call_info.call.named.iter())
.for_each(|(lhs, rhs)| {
// Comparing the keys
assert_eq!(lhs.0.item, rhs.0.item);
match (&lhs.1, &rhs.1) {
(None, None) => {}
(Some(a), Some(b)) => assert_eq!(a, b),
_ => panic!("not matching values"),
}
});
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn call_round_trip_customvalueop() {
let data = vec![1, 2, 3, 4, 5, 6, 7];
let span = Span::new(0, 20);
let custom_value_op = PluginCall::CustomValueOp(
Spanned {
item: PluginCustomValue::new("Foo".into(), data.clone(), false, None),
span,
},
CustomValueOp::ToBaseValue,
);
let plugin_input = PluginInput::Call(2, custom_value_op);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&plugin_input, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginInput::Call(2, PluginCall::CustomValueOp(val, op)) => {
assert_eq!("Foo", val.item.name());
assert_eq!(data, val.item.data());
assert_eq!(span, val.span);
#[allow(unreachable_patterns)]
match op {
CustomValueOp::ToBaseValue => (),
_ => panic!("wrong op: {op:?}"),
}
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn response_round_trip_signature() {
let signature = PluginSignature::new(
Signature::build("nu-plugin")
.required("first", SyntaxShape::String, "first required")
.required("second", SyntaxShape::Int, "second required")
.required_named("first-named", SyntaxShape::String, "first named", Some('f'))
.required_named(
"second-named",
SyntaxShape::String,
"second named",
Some('s'),
)
.rest("remaining", SyntaxShape::Int, "remaining"),
vec![],
);
let response = PluginCallResponse::Signature(vec![signature.clone()]);
let output = PluginOutput::CallResponse(3, response);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&output, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginOutput::CallResponse(
3,
PluginCallResponse::Signature(returned_signature),
) => {
assert_eq!(returned_signature.len(), 1);
assert_eq!(signature.sig.name, returned_signature[0].sig.name);
assert_eq!(signature.sig.usage, returned_signature[0].sig.usage);
assert_eq!(
signature.sig.extra_usage,
returned_signature[0].sig.extra_usage
);
assert_eq!(signature.sig.is_filter, returned_signature[0].sig.is_filter);
signature
.sig
.required_positional
.iter()
.zip(returned_signature[0].sig.required_positional.iter())
.for_each(|(lhs, rhs)| assert_eq!(lhs, rhs));
signature
.sig
.optional_positional
.iter()
.zip(returned_signature[0].sig.optional_positional.iter())
.for_each(|(lhs, rhs)| assert_eq!(lhs, rhs));
signature
.sig
.named
.iter()
.zip(returned_signature[0].sig.named.iter())
.for_each(|(lhs, rhs)| assert_eq!(lhs, rhs));
assert_eq!(
signature.sig.rest_positional,
returned_signature[0].sig.rest_positional,
);
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn response_round_trip_value() {
let value = Value::int(10, Span::new(2, 30));
let response = PluginCallResponse::value(value.clone());
let output = PluginOutput::CallResponse(4, response);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&output, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginOutput::CallResponse(
4,
PluginCallResponse::PipelineData(PipelineDataHeader::Value(returned_value)),
) => {
assert_eq!(value, returned_value)
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn response_round_trip_plugin_custom_value() {
let name = "test";
let data = vec![1, 2, 3, 4, 5];
let span = Span::new(2, 30);
let value = Value::custom(
Box::new(PluginCustomValue::new(
name.into(),
data.clone(),
true,
None,
)),
span,
);
let response = PluginCallResponse::PipelineData(PipelineDataHeader::Value(value));
let output = PluginOutput::CallResponse(5, response);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&output, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginOutput::CallResponse(
5,
PluginCallResponse::PipelineData(PipelineDataHeader::Value(returned_value)),
) => {
assert_eq!(span, returned_value.span());
if let Some(plugin_val) = returned_value
.as_custom_value()
.unwrap()
.as_any()
.downcast_ref::<PluginCustomValue>()
{
assert_eq!(name, plugin_val.name());
assert_eq!(data, plugin_val.data());
assert!(plugin_val.notify_on_drop());
} else {
panic!("returned CustomValue is not a PluginCustomValue");
}
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn response_round_trip_error() {
let error = LabeledError::new("label")
.with_code("test::error")
.with_url("https://example.org/test/error")
.with_help("some help")
.with_label("msg", Span::new(2, 30))
.with_inner(ShellError::IOError {
msg: "io error".into(),
});
let response = PluginCallResponse::Error(error.clone());
let output = PluginOutput::CallResponse(6, response);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&output, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginOutput::CallResponse(6, PluginCallResponse::Error(msg)) => {
assert_eq!(error, msg)
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn response_round_trip_error_none() {
let error = LabeledError::new("error");
let response = PluginCallResponse::Error(error.clone());
let output = PluginOutput::CallResponse(7, response);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&output, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginOutput::CallResponse(7, PluginCallResponse::Error(msg)) => {
assert_eq!(error, msg)
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn input_round_trip_stream_data_list() {
let span = Span::new(12, 30);
let item = Value::int(1, span);
let stream_data = StreamData::List(item.clone());
let plugin_input = PluginInput::Data(0, stream_data);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&plugin_input, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginInput::Data(id, StreamData::List(list_data)) => {
assert_eq!(0, id);
assert_eq!(item, list_data);
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn input_round_trip_stream_data_raw() {
let data = b"Hello world";
let stream_data = StreamData::Raw(Ok(data.to_vec()));
let plugin_input = PluginInput::Data(1, stream_data);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&plugin_input, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginInput::Data(id, StreamData::Raw(bytes)) => {
assert_eq!(1, id);
match bytes {
Ok(bytes) => assert_eq!(data, &bytes[..]),
Err(err) => panic!("decoded into error variant: {err:?}"),
}
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn output_round_trip_stream_data_list() {
let span = Span::new(12, 30);
let item = Value::int(1, span);
let stream_data = StreamData::List(item.clone());
let plugin_output = PluginOutput::Data(4, stream_data);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&plugin_output, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginOutput::Data(id, StreamData::List(list_data)) => {
assert_eq!(4, id);
assert_eq!(item, list_data);
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn output_round_trip_stream_data_raw() {
let data = b"Hello world";
let stream_data = StreamData::Raw(Ok(data.to_vec()));
let plugin_output = PluginOutput::Data(5, stream_data);
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&plugin_output, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginOutput::Data(id, StreamData::Raw(bytes)) => {
assert_eq!(5, id);
match bytes {
Ok(bytes) => assert_eq!(data, &bytes[..]),
Err(err) => panic!("decoded into error variant: {err:?}"),
}
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
#[test]
fn output_round_trip_option() {
let plugin_output = PluginOutput::Option(PluginOption::GcDisabled(true));
let encoder = $encoder;
let mut buffer: Vec<u8> = Vec::new();
encoder
.encode(&plugin_output, &mut buffer)
.expect("unable to serialize message");
let returned = encoder
.decode(&mut buffer.as_slice())
.expect("unable to deserialize message")
.expect("eof");
match returned {
PluginOutput::Option(PluginOption::GcDisabled(disabled)) => {
assert!(disabled);
}
_ => panic!("decoded into wrong value: {returned:?}"),
}
}
};
}
pub(crate) use generate_tests;

View File

@ -0,0 +1,15 @@
use nu_plugin_core::interface_test_util::TestCase;
use nu_plugin_protocol::{PluginInput, PluginOutput};
use crate::plugin::EngineInterfaceManager;
pub trait TestCaseExt {
/// Create a new [`EngineInterfaceManager`] that writes to this test case.
fn engine(&self) -> EngineInterfaceManager;
}
impl TestCaseExt for TestCase<PluginInput, PluginOutput> {
fn engine(&self) -> EngineInterfaceManager {
EngineInterfaceManager::new(self.clone())
}
}

View File

@ -1,7 +0,0 @@
mod mutable_cow;
mod waitable;
mod with_custom_values_in;
pub(crate) use mutable_cow::*;
pub use waitable::*;
pub use with_custom_values_in::*;

View File

@ -1,35 +0,0 @@
/// Like [`Cow`] but with a mutable reference instead. So not exactly clone-on-write, but can be
/// made owned.
pub enum MutableCow<'a, T> {
Borrowed(&'a mut T),
Owned(T),
}
impl<'a, T: Clone> MutableCow<'a, T> {
pub fn owned(&self) -> MutableCow<'static, T> {
match self {
MutableCow::Borrowed(r) => MutableCow::Owned((*r).clone()),
MutableCow::Owned(o) => MutableCow::Owned(o.clone()),
}
}
}
impl<'a, T> std::ops::Deref for MutableCow<'a, T> {
type Target = T;
fn deref(&self) -> &T {
match self {
MutableCow::Borrowed(r) => r,
MutableCow::Owned(o) => o,
}
}
}
impl<'a, T> std::ops::DerefMut for MutableCow<'a, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
MutableCow::Borrowed(r) => r,
MutableCow::Owned(o) => o,
}
}
}

View File

@ -1,181 +0,0 @@
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc, Condvar, Mutex, MutexGuard, PoisonError,
};
use nu_protocol::ShellError;
/// A shared container that may be empty, and allows threads to block until it has a value.
///
/// This side is read-only - use [`WaitableMut`] on threads that might write a value.
#[derive(Debug, Clone)]
pub struct Waitable<T: Clone + Send> {
shared: Arc<WaitableShared<T>>,
}
#[derive(Debug)]
pub struct WaitableMut<T: Clone + Send> {
shared: Arc<WaitableShared<T>>,
}
#[derive(Debug)]
struct WaitableShared<T: Clone + Send> {
is_set: AtomicBool,
mutex: Mutex<SyncState<T>>,
condvar: Condvar,
}
#[derive(Debug)]
struct SyncState<T: Clone + Send> {
writers: usize,
value: Option<T>,
}
#[track_caller]
fn fail_if_poisoned<'a, T>(
result: Result<MutexGuard<'a, T>, PoisonError<MutexGuard<'a, T>>>,
) -> Result<MutexGuard<'a, T>, ShellError> {
match result {
Ok(guard) => Ok(guard),
Err(_) => Err(ShellError::NushellFailedHelp {
msg: "Waitable mutex poisoned".into(),
help: std::panic::Location::caller().to_string(),
}),
}
}
impl<T: Clone + Send> WaitableMut<T> {
/// Create a new empty `WaitableMut`. Call [`.reader()`] to get [`Waitable`].
pub fn new() -> WaitableMut<T> {
WaitableMut {
shared: Arc::new(WaitableShared {
is_set: AtomicBool::new(false),
mutex: Mutex::new(SyncState {
writers: 1,
value: None,
}),
condvar: Condvar::new(),
}),
}
}
pub fn reader(&self) -> Waitable<T> {
Waitable {
shared: self.shared.clone(),
}
}
/// Set the value and let waiting threads know.
#[track_caller]
pub fn set(&self, value: T) -> Result<(), ShellError> {
let mut sync_state = fail_if_poisoned(self.shared.mutex.lock())?;
self.shared.is_set.store(true, Ordering::SeqCst);
sync_state.value = Some(value);
self.shared.condvar.notify_all();
Ok(())
}
}
impl<T: Clone + Send> Default for WaitableMut<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Clone + Send> Clone for WaitableMut<T> {
fn clone(&self) -> Self {
let shared = self.shared.clone();
shared
.mutex
.lock()
.expect("failed to lock mutex to increment writers")
.writers += 1;
WaitableMut { shared }
}
}
impl<T: Clone + Send> Drop for WaitableMut<T> {
fn drop(&mut self) {
// Decrement writers...
if let Ok(mut sync_state) = self.shared.mutex.lock() {
sync_state.writers = sync_state
.writers
.checked_sub(1)
.expect("would decrement writers below zero");
}
// and notify waiting threads so they have a chance to see it.
self.shared.condvar.notify_all();
}
}
impl<T: Clone + Send> Waitable<T> {
/// Wait for a value to be available and then clone it.
///
/// Returns `Ok(None)` if there are no writers left that could possibly place a value.
#[track_caller]
pub fn get(&self) -> Result<Option<T>, ShellError> {
let sync_state = fail_if_poisoned(self.shared.mutex.lock())?;
if let Some(value) = sync_state.value.clone() {
Ok(Some(value))
} else if sync_state.writers == 0 {
// There can't possibly be a value written, so no point in waiting.
Ok(None)
} else {
let sync_state = fail_if_poisoned(
self.shared
.condvar
.wait_while(sync_state, |g| g.writers > 0 && g.value.is_none()),
)?;
Ok(sync_state.value.clone())
}
}
/// Clone the value if one is available, but don't wait if not.
#[track_caller]
pub fn try_get(&self) -> Result<Option<T>, ShellError> {
let sync_state = fail_if_poisoned(self.shared.mutex.lock())?;
Ok(sync_state.value.clone())
}
/// Returns true if value is available.
#[track_caller]
pub fn is_set(&self) -> bool {
self.shared.is_set.load(Ordering::SeqCst)
}
}
#[test]
fn set_from_other_thread() -> Result<(), ShellError> {
let waitable_mut = WaitableMut::new();
let waitable = waitable_mut.reader();
assert!(!waitable.is_set());
std::thread::spawn(move || {
waitable_mut.set(42).expect("error on set");
});
assert_eq!(Some(42), waitable.get()?);
assert_eq!(Some(42), waitable.try_get()?);
assert!(waitable.is_set());
Ok(())
}
#[test]
fn dont_deadlock_if_waiting_without_writer() {
use std::time::Duration;
let (tx, rx) = std::sync::mpsc::channel();
let writer = WaitableMut::<()>::new();
let waitable = writer.reader();
// Ensure there are no writers
drop(writer);
std::thread::spawn(move || {
let _ = tx.send(waitable.get());
});
let result = rx
.recv_timeout(Duration::from_secs(10))
.expect("timed out")
.expect("error");
assert!(result.is_none());
}

View File

@ -1,96 +0,0 @@
use nu_protocol::{CustomValue, IntoSpanned, ShellError, Spanned, Value};
/// Do something with all [`CustomValue`]s recursively within a `Value`. This is not limited to
/// plugin custom values.
///
/// `LazyRecord`s will be collected to plain values for completeness.
pub fn with_custom_values_in<E>(
value: &mut Value,
mut f: impl FnMut(Spanned<&mut (dyn CustomValue + '_)>) -> Result<(), E>,
) -> Result<(), E>
where
E: From<ShellError>,
{
value.recurse_mut(&mut |value| {
let span = value.span();
match value {
Value::Custom { val, .. } => {
// Operate on a CustomValue.
f(val.as_mut().into_spanned(span))
}
// LazyRecord would be a problem for us, since it could return something else the
// next time, and we have to collect it anyway to serialize it. Collect it in place,
// and then use the result
Value::LazyRecord { val, .. } => {
*value = val.collect()?;
Ok(())
}
_ => Ok(()),
}
})
}
#[test]
fn find_custom_values() {
use crate::protocol::test_util::test_plugin_custom_value;
use nu_protocol::{engine::Closure, record, LazyRecord, Span};
#[derive(Debug, Clone)]
struct Lazy;
impl<'a> LazyRecord<'a> for Lazy {
fn column_names(&'a self) -> Vec<&'a str> {
vec!["custom", "plain"]
}
fn get_column_value(&self, column: &str) -> Result<Value, ShellError> {
Ok(match column {
"custom" => Value::test_custom_value(Box::new(test_plugin_custom_value())),
"plain" => Value::test_int(42),
_ => unimplemented!(),
})
}
fn span(&self) -> Span {
Span::test_data()
}
fn clone_value(&self, span: Span) -> Value {
Value::lazy_record(Box::new(self.clone()), span)
}
}
let mut cv = Value::test_custom_value(Box::new(test_plugin_custom_value()));
let mut value = Value::test_record(record! {
"bare" => cv.clone(),
"list" => Value::test_list(vec![
cv.clone(),
Value::test_int(4),
]),
"closure" => Value::test_closure(
Closure {
block_id: 0,
captures: vec![(0, cv.clone()), (1, Value::test_string("foo"))]
}
),
"lazy" => Value::test_lazy_record(Box::new(Lazy)),
});
// Do with_custom_values_in, and count the number of custom values found
let mut found = 0;
with_custom_values_in::<ShellError>(&mut value, |_| {
found += 1;
Ok(())
})
.expect("error");
assert_eq!(4, found, "found in value");
// Try it on bare custom value too
found = 0;
with_custom_values_in::<ShellError>(&mut cv, |_| {
found += 1;
Ok(())
})
.expect("error");
assert_eq!(1, found, "bare custom value didn't work");
}