Add test support crate for plugin developers (#12259)

# Description

Adds a `nu-plugin-test-support` crate with an interface that supports
testing plugins.

Unlike in reality, these plugins run in the same process on separate
threads. This will allow
testing aspects of the plugin internal state and handling serialized
plugin custom values easily.
We still serialize their custom values and all of the engine to plugin
logic is still in play, so
from a logical perspective this should still expose any bugs that would
have been caused by that.
The only difference is that it doesn't run in a different process, and
doesn't try to serialize
everything to the final wire format for stdin/stdout.

TODO still:

- [x] Clean up warnings about private types exposed in trait definition
- [x] Automatically deserialize plugin custom values in the result so
they can be inspected
- [x] Automatic plugin examples test function
- [x] Write a bit more documentation
- [x] More tests
- [x] Add MIT License file to new crate

# User-Facing Changes

Plugin developers get a nice way to test their plugins.

# Tests + Formatting
Run the tests with `cargo test -p nu-plugin-test-support --
--show-output` to see some examples of what the failing test output for
examples can look like. I used the `difference` crate (MIT licensed) to
make it look nice.

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting

- [ ] Add a section to the book about testing
- [ ] Test some of the example plugins this way
- [ ] Add example tests to nu_plugin_template so plugin developers have
something to start with
This commit is contained in:
Devyn Cairns 2024-03-23 11:29:54 -07:00 committed by GitHub
parent ff41cf91ef
commit c79c43d2f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1171 additions and 163 deletions

20
Cargo.lock generated
View File

@ -1101,6 +1101,12 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "difference"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "difflib"
version = "0.4.0"
@ -3069,10 +3075,24 @@ dependencies = [
"semver",
"serde",
"serde_json",
"thiserror",
"typetag",
"windows 0.52.0",
]
[[package]]
name = "nu-plugin-test-support"
version = "0.91.1"
dependencies = [
"difference",
"nu-engine",
"nu-parser",
"nu-plugin",
"nu-protocol",
"serde",
"typetag",
]
[[package]]
name = "nu-pretty-hex"
version = "0.91.1"

View File

@ -40,6 +40,7 @@ members = [
"crates/nu-pretty-hex",
"crates/nu-protocol",
"crates/nu-plugin",
"crates/nu-plugin-test-support",
"crates/nu_plugin_inc",
"crates/nu_plugin_gstat",
"crates/nu_plugin_example",

View File

@ -3736,7 +3736,7 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
for signature in signatures {
// create plugin command declaration (need struct impl Command)
// store declaration in working set
let plugin_decl = PluginDeclaration::new(&plugin, signature);
let plugin_decl = PluginDeclaration::new(plugin.clone(), signature);
working_set.add_decl(Box::new(plugin_decl));
}

View File

@ -0,0 +1,19 @@
[package]
name = "nu-plugin-test-support"
version = "0.91.1"
edition = "2021"
license = "MIT"
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-test-support"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nu-engine = { path = "../nu-engine", version = "0.91.1", features = ["plugin"] }
nu-protocol = { path = "../nu-protocol", version = "0.91.1", features = ["plugin"] }
nu-parser = { path = "../nu-parser", version = "0.91.1", features = ["plugin"] }
nu-plugin = { path = "../nu-plugin", version = "0.91.1" }
difference = "2.0"
[dev-dependencies]
typetag = "0.2"
serde = "1.0"

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 - 2023 The Nushell Project Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,4 @@
# nu-plugin-test-support
This crate provides helpers for running tests on plugin commands, and is intended to be included in
the `dev-dependencies` of plugin crates for testing.

View File

@ -0,0 +1,71 @@
use std::{
any::Any,
sync::{Arc, OnceLock},
};
use nu_plugin::{GetPlugin, PluginInterface};
use nu_protocol::{
engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
};
pub struct FakePersistentPlugin {
identity: PluginIdentity,
plugin: OnceLock<PluginInterface>,
}
impl FakePersistentPlugin {
pub fn new(identity: PluginIdentity) -> FakePersistentPlugin {
FakePersistentPlugin {
identity,
plugin: OnceLock::new(),
}
}
pub fn initialize(&self, interface: PluginInterface) {
self.plugin.set(interface).unwrap_or_else(|_| {
panic!("Tried to initialize an already initialized FakePersistentPlugin");
})
}
}
impl RegisteredPlugin for FakePersistentPlugin {
fn identity(&self) -> &PluginIdentity {
&self.identity
}
fn is_running(&self) -> bool {
true
}
fn pid(&self) -> Option<u32> {
None
}
fn set_gc_config(&self, _gc_config: &PluginGcConfig) {
// We don't have a GC
}
fn stop(&self) -> Result<(), ShellError> {
// We can't stop
Ok(())
}
fn as_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync> {
self
}
}
impl GetPlugin for FakePersistentPlugin {
fn get_plugin(
self: Arc<Self>,
_context: Option<(&EngineState, &mut Stack)>,
) -> Result<PluginInterface, ShellError> {
self.plugin
.get()
.cloned()
.ok_or_else(|| ShellError::PluginFailedToLoad {
msg: "FakePersistentPlugin was not initialized".into(),
})
}
}

View File

@ -0,0 +1,27 @@
use std::sync::Arc;
use nu_plugin::{Plugin, PluginDeclaration};
use nu_protocol::{engine::StateWorkingSet, RegisteredPlugin, ShellError};
use crate::{fake_persistent_plugin::FakePersistentPlugin, spawn_fake_plugin::spawn_fake_plugin};
/// Register all of the commands from the plugin into the [`StateWorkingSet`]
pub fn fake_register(
working_set: &mut StateWorkingSet,
name: &str,
plugin: Arc<impl Plugin + Send + 'static>,
) -> Result<Arc<FakePersistentPlugin>, ShellError> {
let reg_plugin = spawn_fake_plugin(name, plugin.clone())?;
let reg_plugin_clone = reg_plugin.clone();
for command in plugin.commands() {
let signature = command.signature();
let decl = PluginDeclaration::new(reg_plugin.clone(), signature);
working_set.add_decl(Box::new(decl));
}
let identity = reg_plugin.identity().clone();
working_set.find_or_create_plugin(&identity, move || reg_plugin);
Ok(reg_plugin_clone)
}

View File

@ -0,0 +1,71 @@
//! Test support for [Nushell](https://nushell.sh) plugins.
//!
//! # Example
//!
//! ```rust
//! use std::sync::Arc;
//!
//! use nu_plugin::*;
//! use nu_plugin_test_support::PluginTest;
//! use nu_protocol::{PluginSignature, PipelineData, Type, Span, Value, LabeledError};
//! use nu_protocol::IntoInterruptiblePipelineData;
//!
//! struct LowercasePlugin;
//! struct Lowercase;
//!
//! impl PluginCommand for Lowercase {
//! type Plugin = LowercasePlugin;
//!
//! fn signature(&self) -> PluginSignature {
//! PluginSignature::build("lowercase")
//! .usage("Convert each string in a stream to lowercase")
//! .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into()))
//! }
//!
//! fn run(
//! &self,
//! plugin: &LowercasePlugin,
//! engine: &EngineInterface,
//! call: &EvaluatedCall,
//! input: PipelineData,
//! ) -> Result<PipelineData, LabeledError> {
//! let span = call.head;
//! Ok(input.map(move |value| {
//! value.as_str()
//! .map(|string| Value::string(string.to_lowercase(), span))
//! // Errors in a stream should be returned as values.
//! .unwrap_or_else(|err| Value::error(err, span))
//! }, None)?)
//! }
//! }
//!
//! impl Plugin for LowercasePlugin {
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
//! vec![Box::new(Lowercase)]
//! }
//! }
//!
//! fn test_lowercase() -> Result<(), LabeledError> {
//! let input = vec![Value::test_string("FooBar")].into_pipeline_data(None);
//! let output = PluginTest::new("lowercase", LowercasePlugin.into())?
//! .eval_with("lowercase", input)?
//! .into_value(Span::test_data());
//!
//! assert_eq!(
//! Value::test_list(vec![
//! Value::test_string("foobar")
//! ]),
//! output
//! );
//! Ok(())
//! }
//! #
//! # test_lowercase().unwrap();
//! ```
mod fake_persistent_plugin;
mod fake_register;
mod plugin_test;
mod spawn_fake_plugin;
pub use plugin_test::PluginTest;

View File

@ -0,0 +1,264 @@
use std::{convert::Infallible, sync::Arc};
use difference::Changeset;
use nu_engine::eval_block;
use nu_parser::parse;
use nu_plugin::{Plugin, PluginCommand, PluginCustomValue, PluginSource};
use nu_protocol::{
debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet},
report_error_new, LabeledError, PipelineData, PluginExample, ShellError, Span, Value,
};
use crate::fake_register::fake_register;
/// An object through which plugins can be tested.
pub struct PluginTest {
engine_state: EngineState,
source: Arc<PluginSource>,
entry_num: usize,
}
impl PluginTest {
/// Create a new test for the given `plugin` named `name`.
///
/// # Example
///
/// ```rust,no_run
/// # use nu_plugin_test_support::PluginTest;
/// # use nu_protocol::ShellError;
/// # use nu_plugin::*;
/// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<PluginTest, ShellError> {
/// PluginTest::new("my_plugin", MyPlugin.into())
/// # }
/// ```
pub fn new(
name: &str,
plugin: Arc<impl Plugin + Send + 'static>,
) -> Result<PluginTest, ShellError> {
let mut engine_state = EngineState::new();
let mut working_set = StateWorkingSet::new(&engine_state);
let reg_plugin = fake_register(&mut working_set, name, plugin)?;
let source = Arc::new(PluginSource::new(reg_plugin));
engine_state.merge_delta(working_set.render())?;
Ok(PluginTest {
engine_state,
source,
entry_num: 1,
})
}
/// Get the [`EngineState`].
pub fn engine_state(&self) -> &EngineState {
&self.engine_state
}
/// Get a mutable reference to the [`EngineState`].
pub fn engine_state_mut(&mut self) -> &mut EngineState {
&mut self.engine_state
}
/// Evaluate some Nushell source code with the plugin commands in scope with the given input to
/// the pipeline.
///
/// # Example
///
/// ```rust,no_run
/// # use nu_plugin_test_support::PluginTest;
/// # use nu_protocol::{ShellError, Span, Value, IntoInterruptiblePipelineData};
/// # use nu_plugin::*;
/// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<(), ShellError> {
/// let result = PluginTest::new("my_plugin", MyPlugin.into())?
/// .eval_with(
/// "my-command",
/// vec![Value::test_int(42)].into_pipeline_data(None)
/// )?
/// .into_value(Span::test_data());
/// assert_eq!(Value::test_string("42"), result);
/// # Ok(())
/// # }
/// ```
pub fn eval_with(
&mut self,
nu_source: &str,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let mut working_set = StateWorkingSet::new(&self.engine_state);
let fname = format!("entry #{}", self.entry_num);
self.entry_num += 1;
// Parse the source code
let block = parse(&mut working_set, Some(&fname), nu_source.as_bytes(), false);
// Check for parse errors
let error = if !working_set.parse_errors.is_empty() {
// ShellError doesn't have ParseError, use LabeledError to contain it.
let mut error = LabeledError::new("Example failed to parse");
error.inner.extend(
working_set
.parse_errors
.iter()
.map(LabeledError::from_diagnostic),
);
Some(ShellError::LabeledError(error.into()))
} else {
None
};
// Merge into state
self.engine_state.merge_delta(working_set.render())?;
// Return error if set. We merge the delta even if we have errors so that printing the error
// based on the engine state still works.
if let Some(error) = error {
return Err(error);
}
// Serialize custom values in the input
let source = self.source.clone();
let input = input.map(
move |mut value| match PluginCustomValue::serialize_custom_values_in(&mut value) {
Ok(()) => {
// Make sure to mark them with the source so they pass correctly, too.
PluginCustomValue::add_source(&mut value, &source);
value
}
Err(err) => Value::error(err, value.span()),
},
None,
)?;
// Eval the block with the input
let mut stack = Stack::new().capture();
eval_block::<WithoutDebug>(&self.engine_state, &mut stack, &block, input)?.map(
|mut value| {
// Make sure to deserialize custom values
match PluginCustomValue::deserialize_custom_values_in(&mut value) {
Ok(()) => value,
Err(err) => Value::error(err, value.span()),
}
},
None,
)
}
/// Evaluate some Nushell source code with the plugin commands in scope.
///
/// # Example
///
/// ```rust,no_run
/// # use nu_plugin_test_support::PluginTest;
/// # use nu_protocol::{ShellError, Span, Value, IntoInterruptiblePipelineData};
/// # use nu_plugin::*;
/// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<(), ShellError> {
/// let result = PluginTest::new("my_plugin", MyPlugin.into())?
/// .eval("42 | my-command")?
/// .into_value(Span::test_data());
/// assert_eq!(Value::test_string("42"), result);
/// # Ok(())
/// # }
/// ```
pub fn eval(&mut self, nu_source: &str) -> Result<PipelineData, ShellError> {
self.eval_with(nu_source, PipelineData::Empty)
}
/// Test a list of plugin examples. Prints an error for each failing example.
///
/// See [`.test_command_examples()`] for easier usage of this method on a command's examples.
///
/// # Example
///
/// ```rust,no_run
/// # use nu_plugin_test_support::PluginTest;
/// # use nu_protocol::{ShellError, PluginExample, Value};
/// # use nu_plugin::*;
/// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<(), ShellError> {
/// PluginTest::new("my_plugin", MyPlugin.into())?
/// .test_examples(&[
/// PluginExample {
/// example: "my-command".into(),
/// description: "Run my-command".into(),
/// result: Some(Value::test_string("my-command output")),
/// },
/// ])
/// # }
/// ```
pub fn test_examples(&mut self, examples: &[PluginExample]) -> Result<(), ShellError> {
let mut failed = false;
for example in examples {
let mut failed_header = || {
failed = true;
eprintln!("\x1b[1mExample:\x1b[0m {}", example.example);
eprintln!("\x1b[1mDescription:\x1b[0m {}", example.description);
};
if let Some(expectation) = &example.result {
match self.eval(&example.example) {
Ok(data) => {
let mut value = data.into_value(Span::test_data());
// Set all of the spans in the value to test_data() to avoid unnecessary
// differences when printing
let _: Result<(), Infallible> = value.recurse_mut(&mut |here| {
here.set_span(Span::test_data());
Ok(())
});
// Check for equality with the result
if *expectation != value {
// If they're not equal, print a diff of the debug format
let expectation_formatted = format!("{:#?}", expectation);
let value_formatted = format!("{:#?}", value);
let diff =
Changeset::new(&expectation_formatted, &value_formatted, "\n");
failed_header();
eprintln!("\x1b[1mResult:\x1b[0m {diff}");
} else {
println!("{:?}, {:?}", expectation, value);
}
}
Err(err) => {
// Report the error
failed_header();
report_error_new(&self.engine_state, &err);
}
}
}
}
if !failed {
Ok(())
} else {
Err(ShellError::GenericError {
error: "Some examples failed. See the error output for details".into(),
msg: "".into(),
span: None,
help: None,
inner: vec![],
})
}
}
/// Test examples from a command.
///
/// # Example
///
/// ```rust,no_run
/// # use nu_plugin_test_support::PluginTest;
/// # use nu_protocol::ShellError;
/// # use nu_plugin::*;
/// # fn test(MyPlugin: impl Plugin + Send + 'static, MyCommand: impl PluginCommand) -> Result<(), ShellError> {
/// PluginTest::new("my_plugin", MyPlugin.into())?
/// .test_command_examples(&MyCommand)
/// # }
/// ```
pub fn test_command_examples(
&mut self,
command: &impl PluginCommand,
) -> Result<(), ShellError> {
self.test_examples(&command.signature().examples)
}
}

View File

@ -0,0 +1,77 @@
use std::sync::{mpsc, Arc};
use nu_plugin::{
InterfaceManager, Plugin, PluginInput, PluginInterfaceManager, PluginOutput, PluginRead,
PluginSource, PluginWrite,
};
use nu_protocol::{PluginIdentity, ShellError};
use crate::fake_persistent_plugin::FakePersistentPlugin;
struct FakePluginRead<T>(mpsc::Receiver<T>);
struct FakePluginWrite<T>(mpsc::Sender<T>);
impl<T> PluginRead<T> for FakePluginRead<T> {
fn read(&mut self) -> Result<Option<T>, ShellError> {
Ok(self.0.recv().ok())
}
}
impl<T: Clone + Send> PluginWrite<T> for FakePluginWrite<T> {
fn write(&self, data: &T) -> Result<(), ShellError> {
self.0
.send(data.clone())
.map_err(|err| ShellError::IOError {
msg: err.to_string(),
})
}
fn flush(&self) -> Result<(), ShellError> {
Ok(())
}
}
fn fake_plugin_channel<T: Clone + Send>() -> (FakePluginRead<T>, FakePluginWrite<T>) {
let (tx, rx) = mpsc::channel();
(FakePluginRead(rx), FakePluginWrite(tx))
}
/// Spawn a plugin on another thread and return the registration
pub(crate) fn spawn_fake_plugin(
name: &str,
plugin: Arc<impl Plugin + Send + 'static>,
) -> Result<Arc<FakePersistentPlugin>, ShellError> {
let (input_read, input_write) = fake_plugin_channel::<PluginInput>();
let (output_read, output_write) = fake_plugin_channel::<PluginOutput>();
let identity = PluginIdentity::new_fake(name);
let reg_plugin = Arc::new(FakePersistentPlugin::new(identity.clone()));
let source = Arc::new(PluginSource::new(reg_plugin.clone()));
let mut manager = PluginInterfaceManager::new(source, input_write);
// Set up the persistent plugin with the interface before continuing
let interface = manager.get_interface();
interface.hello()?;
reg_plugin.initialize(interface);
// Start the interface reader on another thread
std::thread::Builder::new()
.name(format!("fake plugin interface reader ({name})"))
.spawn(move || manager.consume_all(output_read).expect("Plugin read error"))?;
// Start the plugin on another thread
let name_string = name.to_owned();
std::thread::Builder::new()
.name(format!("fake plugin runner ({name})"))
.spawn(move || {
nu_plugin::serve_plugin_io(
&*plugin,
&name_string,
move || input_read,
move || output_write,
)
.expect("Plugin runner error")
})?;
Ok(reg_plugin)
}

View File

@ -0,0 +1,129 @@
use std::cmp::Ordering;
use nu_plugin::{EngineInterface, EvaluatedCall, Plugin, SimplePluginCommand};
use nu_plugin_test_support::PluginTest;
use nu_protocol::{
CustomValue, LabeledError, PipelineData, PluginExample, PluginSignature, ShellError, Span,
Type, Value,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialOrd, Ord, PartialEq, Eq)]
struct CustomU32(u32);
impl CustomU32 {
pub fn into_value(self, span: Span) -> Value {
Value::custom_value(Box::new(self), span)
}
}
#[typetag::serde]
impl CustomValue for CustomU32 {
fn clone_value(&self, span: Span) -> Value {
self.clone().into_value(span)
}
fn type_name(&self) -> String {
"CustomU32".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 partial_cmp(&self, other: &Value) -> Option<Ordering> {
other
.as_custom_value()
.ok()
.and_then(|cv| cv.as_any().downcast_ref::<CustomU32>())
.and_then(|other_u32| PartialOrd::partial_cmp(self, other_u32))
}
}
struct CustomU32Plugin;
struct IntoU32;
struct IntoIntFromU32;
impl Plugin for CustomU32Plugin {
fn commands(&self) -> Vec<Box<dyn nu_plugin::PluginCommand<Plugin = Self>>> {
vec![Box::new(IntoU32), Box::new(IntoIntFromU32)]
}
}
impl SimplePluginCommand for IntoU32 {
type Plugin = CustomU32Plugin;
fn signature(&self) -> PluginSignature {
PluginSignature::build("into u32")
.input_output_type(Type::Int, Type::Custom("CustomU32".into()))
.plugin_examples(vec![PluginExample {
example: "340 | into u32".into(),
description: "Make a u32".into(),
result: Some(CustomU32(340).into_value(Span::test_data())),
}])
}
fn run(
&self,
_plugin: &Self::Plugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
) -> Result<Value, LabeledError> {
let value: i64 = input.as_int()?;
let value_u32 = u32::try_from(value).map_err(|err| {
LabeledError::new(format!("Not a valid u32: {value}"))
.with_label(err.to_string(), input.span())
})?;
Ok(CustomU32(value_u32).into_value(call.head))
}
}
impl SimplePluginCommand for IntoIntFromU32 {
type Plugin = CustomU32Plugin;
fn signature(&self) -> PluginSignature {
PluginSignature::build("into int from u32")
.input_output_type(Type::Custom("CustomU32".into()), Type::Int)
}
fn run(
&self,
_plugin: &Self::Plugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
) -> Result<Value, LabeledError> {
let value: &CustomU32 = input
.as_custom_value()?
.as_any()
.downcast_ref()
.ok_or_else(|| ShellError::TypeMismatch {
err_message: "expected CustomU32".into(),
span: input.span(),
})?;
Ok(Value::int(value.0 as i64, call.head))
}
}
#[test]
fn test_into_u32_examples() -> Result<(), ShellError> {
PluginTest::new("custom_u32", CustomU32Plugin.into())?.test_command_examples(&IntoU32)
}
#[test]
fn test_into_int_from_u32() -> Result<(), ShellError> {
let result = PluginTest::new("custom_u32", CustomU32Plugin.into())?
.eval_with(
"into int from u32",
PipelineData::Value(CustomU32(42).into_value(Span::test_data()), None),
)?
.into_value(Span::test_data());
assert_eq!(Value::test_int(42), result);
Ok(())
}

View File

@ -0,0 +1,65 @@
//! Extended from `nu-plugin` examples.
use nu_plugin::*;
use nu_plugin_test_support::PluginTest;
use nu_protocol::{LabeledError, PluginExample, PluginSignature, ShellError, Type, Value};
struct HelloPlugin;
struct Hello;
impl Plugin for HelloPlugin {
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Hello)]
}
}
impl SimplePluginCommand for Hello {
type Plugin = HelloPlugin;
fn signature(&self) -> PluginSignature {
PluginSignature::build("hello")
.input_output_type(Type::Nothing, Type::String)
.plugin_examples(vec![PluginExample {
example: "hello".into(),
description: "Print a friendly greeting".into(),
result: Some(Value::test_string("Hello, World!")),
}])
}
fn run(
&self,
_plugin: &HelloPlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
_input: &Value,
) -> Result<Value, LabeledError> {
Ok(Value::string("Hello, World!".to_owned(), call.head))
}
}
#[test]
fn test_specified_examples() -> Result<(), ShellError> {
PluginTest::new("hello", HelloPlugin.into())?.test_command_examples(&Hello)
}
#[test]
fn test_an_error_causing_example() -> Result<(), ShellError> {
let result = PluginTest::new("hello", HelloPlugin.into())?.test_examples(&[PluginExample {
example: "hello --unknown-flag".into(),
description: "Run hello with an unknown flag".into(),
result: Some(Value::test_string("Hello, World!")),
}]);
assert!(result.is_err());
Ok(())
}
#[test]
fn test_an_example_with_the_wrong_result() -> Result<(), ShellError> {
let result = PluginTest::new("hello", HelloPlugin.into())?.test_examples(&[PluginExample {
example: "hello".into(),
description: "Run hello but the example result is wrong".into(),
result: Some(Value::test_string("Goodbye, World!")),
}]);
assert!(result.is_err());
Ok(())
}

View File

@ -0,0 +1,76 @@
use nu_plugin::*;
use nu_plugin_test_support::PluginTest;
use nu_protocol::{
IntoInterruptiblePipelineData, LabeledError, PipelineData, PluginExample, PluginSignature,
ShellError, Span, Type, Value,
};
struct LowercasePlugin;
struct Lowercase;
impl PluginCommand for Lowercase {
type Plugin = LowercasePlugin;
fn signature(&self) -> PluginSignature {
PluginSignature::build("lowercase")
.usage("Convert each string in a stream to lowercase")
.input_output_type(
Type::List(Type::String.into()),
Type::List(Type::String.into()),
)
.plugin_examples(vec![PluginExample {
example: r#"[Hello wORLD] | lowercase"#.into(),
description: "Lowercase a list of strings".into(),
result: Some(Value::test_list(vec![
Value::test_string("hello"),
Value::test_string("world"),
])),
}])
}
fn run(
&self,
_plugin: &LowercasePlugin,
_engine: &EngineInterface,
call: &EvaluatedCall,
input: PipelineData,
) -> Result<PipelineData, LabeledError> {
let span = call.head;
Ok(input.map(
move |value| {
value
.as_str()
.map(|string| Value::string(string.to_lowercase(), span))
// Errors in a stream should be returned as values.
.unwrap_or_else(|err| Value::error(err, span))
},
None,
)?)
}
}
impl Plugin for LowercasePlugin {
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
vec![Box::new(Lowercase)]
}
}
#[test]
fn test_lowercase_using_eval_with() -> Result<(), ShellError> {
let result = PluginTest::new("lowercase", LowercasePlugin.into())?.eval_with(
"lowercase",
vec![Value::test_string("HeLlO wOrLd")].into_pipeline_data(None),
)?;
assert_eq!(
Value::test_list(vec![Value::test_string("hello world")]),
result.into_value(Span::test_data())
);
Ok(())
}
#[test]
fn test_lowercase_examples() -> Result<(), ShellError> {
PluginTest::new("lowercase", LowercasePlugin.into())?.test_command_examples(&Lowercase)
}

View File

@ -0,0 +1,3 @@
mod custom_value;
mod hello;
mod lowercase;

View File

@ -22,6 +22,7 @@ log = "0.4"
miette = { workspace = true }
semver = "1.0"
typetag = "0.2"
thiserror = "1.0"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.52", features = [

View File

@ -0,0 +1,6 @@
# nu-plugin
This crate provides the API for [Nushell](https://nushell.sh/) plugins. See
[the book](https://www.nushell.sh/contributor-book/plugins.html) for more information on how to get
started.

View File

@ -62,14 +62,21 @@ mod serializers;
mod util;
pub use plugin::{
serve_plugin, EngineInterface, Plugin, PluginCommand, PluginEncoder, SimplePluginCommand,
serve_plugin, EngineInterface, Plugin, PluginCommand, PluginEncoder, PluginRead, PluginWrite,
SimplePluginCommand,
};
pub use protocol::EvaluatedCall;
pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};
// Used by other nu crates.
#[doc(hidden)]
pub use plugin::{get_signature, PersistentPlugin, PluginDeclaration};
pub use plugin::{
get_signature, 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;
@ -77,4 +84,4 @@ pub use serializers::EncodingType;
#[doc(hidden)]
pub use plugin::Encoder;
#[doc(hidden)]
pub use protocol::{PluginCallResponse, PluginOutput};
pub use protocol::PluginCallResponse;

View File

@ -14,7 +14,10 @@ use nu_protocol::{
use crate::util::MutableCow;
/// Object safe trait for abstracting operations required of the plugin context.
pub(crate) trait PluginExecutionContext: Send + Sync {
///
/// This is not a public API.
#[doc(hidden)]
pub trait PluginExecutionContext: Send + Sync {
/// The interrupt signal, if present
fn ctrlc(&self) -> Option<&Arc<AtomicBool>>;
/// Get engine configuration
@ -43,7 +46,10 @@ pub(crate) trait PluginExecutionContext: Send + Sync {
}
/// The execution context of a plugin command. Can be borrowed.
pub(crate) struct PluginExecutionCommandContext<'a> {
///
/// This is not a public API.
#[doc(hidden)]
pub struct PluginExecutionCommandContext<'a> {
identity: Arc<PluginIdentity>,
engine_state: Cow<'a, EngineState>,
stack: MutableCow<'a, Stack>,

View File

@ -1,4 +1,4 @@
use super::{PersistentPlugin, PluginExecutionCommandContext, PluginSource};
use super::{GetPlugin, PluginExecutionCommandContext, PluginSource};
use crate::protocol::{CallInfo, EvaluatedCall};
use std::sync::Arc;
@ -6,7 +6,7 @@ use nu_engine::get_eval_expression;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ast::Call, PluginSignature, Signature};
use nu_protocol::{Example, PipelineData, PluginIdentity, RegisteredPlugin, ShellError};
use nu_protocol::{Example, PipelineData, PluginIdentity, ShellError};
#[doc(hidden)] // Note: not for plugin authors / only used in nu-parser
#[derive(Clone)]
@ -17,7 +17,7 @@ pub struct PluginDeclaration {
}
impl PluginDeclaration {
pub fn new(plugin: &Arc<PersistentPlugin>, signature: PluginSignature) -> Self {
pub fn new(plugin: Arc<dyn GetPlugin>, signature: PluginSignature) -> Self {
Self {
name: signature.sig.name.clone(),
signature,
@ -88,13 +88,7 @@ impl Command for PluginDeclaration {
.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(|| {
// 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)
})
p.get_plugin(Some((engine_state, stack)))
})
.map_err(|err| {
let decl = engine_state.get_decl(call.decl_id);

View File

@ -22,11 +22,10 @@ use crate::{
mod stream;
mod engine;
pub use engine::EngineInterface;
pub(crate) use engine::{EngineInterfaceManager, ReceivedPluginCall};
pub use engine::{EngineInterface, EngineInterfaceManager, ReceivedPluginCall};
mod plugin;
pub(crate) use plugin::{PluginInterface, PluginInterfaceManager};
pub use plugin::{PluginInterface, PluginInterfaceManager};
use self::stream::{StreamManager, StreamManagerHandle, StreamWriter, WriteStreamMessage};
@ -45,7 +44,10 @@ const LIST_STREAM_HIGH_PRESSURE: i32 = 100;
const RAW_STREAM_HIGH_PRESSURE: i32 = 50;
/// Read input/output from the stream.
pub(crate) trait PluginRead<T> {
///
/// 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>;
}
@ -72,7 +74,10 @@ where
/// Write input/output to the stream.
///
/// The write should be atomic, without interference from other threads.
pub(crate) trait PluginWrite<T>: Send + Sync {
///
/// 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.
@ -136,7 +141,10 @@ where
///
/// There is typically one [`InterfaceManager`] consuming input from a background thread, and
/// managing shared state.
pub(crate) trait InterfaceManager {
///
/// This is not a public API.
#[doc(hidden)]
pub trait InterfaceManager {
/// The corresponding interface type.
type Interface: Interface + 'static;
@ -218,7 +226,10 @@ pub(crate) trait InterfaceManager {
/// [`EngineInterface`] for the API from the plugin side to the engine.
///
/// There can be multiple copies of the interface managed by a single [`InterfaceManager`].
pub(crate) trait Interface: Clone + Send {
///
/// 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>;
@ -338,7 +349,7 @@ where
/// [`PipelineDataWriter::write()`] to write all of the data contained within the streams.
#[derive(Default)]
#[must_use]
pub(crate) enum PipelineDataWriter<W: WriteStreamMessage> {
pub enum PipelineDataWriter<W: WriteStreamMessage> {
#[default]
None,
ListStream(StreamWriter<W>, ListStream),

View File

@ -30,8 +30,11 @@ use crate::sequence::Sequence;
/// With each call, an [`EngineInterface`] is included that can be provided to the plugin code
/// and should be used to send the response. The interface sent includes the [`PluginCallId`] for
/// sending associated messages with the correct context.
///
/// This is not a public API.
#[derive(Debug)]
pub(crate) enum ReceivedPluginCall {
#[doc(hidden)]
pub enum ReceivedPluginCall {
Signature {
engine: EngineInterface,
},
@ -76,8 +79,11 @@ impl std::fmt::Debug for EngineInterfaceState {
}
/// Manages reading and dispatching messages for [`EngineInterface`]s.
///
/// This is not a public API.
#[derive(Debug)]
pub(crate) struct EngineInterfaceManager {
#[doc(hidden)]
pub struct EngineInterfaceManager {
/// Shared state
state: Arc<EngineInterfaceState>,
/// Channel to send received PluginCalls to. This is removed after `Goodbye` is received.

View File

@ -104,8 +104,11 @@ struct PluginCallState {
}
/// Manages reading and dispatching messages for [`PluginInterface`]s.
///
/// This is not a public API.
#[derive(Debug)]
pub(crate) struct PluginInterfaceManager {
#[doc(hidden)]
pub struct PluginInterfaceManager {
/// Shared state
state: Arc<PluginInterfaceState>,
/// Manages stream messages and state
@ -125,7 +128,7 @@ pub(crate) struct PluginInterfaceManager {
}
impl PluginInterfaceManager {
pub(crate) fn new(
pub fn new(
source: Arc<PluginSource>,
writer: impl PluginWrite<PluginInput> + 'static,
) -> PluginInterfaceManager {
@ -152,7 +155,7 @@ impl PluginInterfaceManager {
/// Add a garbage collector to this plugin. The manager will notify the garbage collector about
/// the state of the plugin so that it can be automatically cleaned up if the plugin is
/// inactive.
pub(crate) fn set_garbage_collector(&mut self, gc: Option<PluginGc>) {
pub fn set_garbage_collector(&mut self, gc: Option<PluginGc>) {
self.gc = gc;
}
@ -359,14 +362,14 @@ impl PluginInterfaceManager {
/// True if there are no other copies of the state (which would mean there are no interfaces
/// and no stream readers/writers)
pub(crate) fn is_finished(&self) -> bool {
pub fn is_finished(&self) -> bool {
Arc::strong_count(&self.state) < 2
}
/// Loop on input from the given reader as long as `is_finished()` is false
///
/// Any errors will be propagated to all read streams automatically.
pub(crate) fn consume_all(
pub fn consume_all(
&mut self,
mut reader: impl PluginRead<PluginOutput>,
) -> Result<(), ShellError> {
@ -545,8 +548,11 @@ impl InterfaceManager for PluginInterfaceManager {
}
/// A reference through which a plugin can be interacted with during execution.
///
/// This is not a public API.
#[derive(Debug, Clone)]
pub(crate) struct PluginInterface {
#[doc(hidden)]
pub struct PluginInterface {
/// Shared state
state: Arc<PluginInterfaceState>,
/// Handle to stream manager
@ -557,7 +563,7 @@ pub(crate) struct PluginInterface {
impl PluginInterface {
/// Write the protocol info. This should be done after initialization
pub(crate) fn hello(&self) -> Result<(), ShellError> {
pub fn hello(&self) -> Result<(), ShellError> {
self.write(PluginInput::Hello(ProtocolInfo::default()))?;
self.flush()
}
@ -567,14 +573,14 @@ impl PluginInterface {
///
/// Note that this is automatically called when the last existing `PluginInterface` is dropped.
/// You probably do not need to call this manually.
pub(crate) fn goodbye(&self) -> Result<(), ShellError> {
pub fn goodbye(&self) -> Result<(), ShellError> {
self.write(PluginInput::Goodbye)?;
self.flush()
}
/// Write an [`EngineCallResponse`]. Writes the full stream contained in any [`PipelineData`]
/// before returning.
pub(crate) fn write_engine_call_response(
pub fn write_engine_call_response(
&self,
id: EngineCallId,
response: EngineCallResponse<PipelineData>,
@ -782,7 +788,7 @@ impl PluginInterface {
}
/// Get the command signatures from the plugin.
pub(crate) fn get_signature(&self) -> Result<Vec<PluginSignature>, ShellError> {
pub fn get_signature(&self) -> Result<Vec<PluginSignature>, ShellError> {
match self.plugin_call(PluginCall::Signature, None)? {
PluginCallResponse::Signature(sigs) => Ok(sigs),
PluginCallResponse::Error(err) => Err(err.into()),
@ -793,7 +799,7 @@ impl PluginInterface {
}
/// Run the plugin with the given call and execution context.
pub(crate) fn run(
pub fn run(
&self,
call: CallInfo<PipelineData>,
context: &mut dyn PluginExecutionContext,
@ -826,7 +832,7 @@ impl PluginInterface {
}
/// Collapse a custom value to its base value.
pub(crate) fn custom_value_to_base_value(
pub fn custom_value_to_base_value(
&self,
value: Spanned<PluginCustomValue>,
) -> Result<Value, ShellError> {
@ -834,7 +840,7 @@ impl PluginInterface {
}
/// Follow a numbered cell path on a custom value - e.g. `value.0`.
pub(crate) fn custom_value_follow_path_int(
pub fn custom_value_follow_path_int(
&self,
value: Spanned<PluginCustomValue>,
index: Spanned<usize>,
@ -843,7 +849,7 @@ impl PluginInterface {
}
/// Follow a named cell path on a custom value - e.g. `value.column`.
pub(crate) fn custom_value_follow_path_string(
pub fn custom_value_follow_path_string(
&self,
value: Spanned<PluginCustomValue>,
column_name: Spanned<String>,
@ -852,7 +858,7 @@ impl PluginInterface {
}
/// Invoke comparison logic for custom values.
pub(crate) fn custom_value_partial_cmp(
pub fn custom_value_partial_cmp(
&self,
value: PluginCustomValue,
mut other_value: Value,
@ -874,7 +880,7 @@ impl PluginInterface {
}
/// Invoke functionality for an operator on a custom value.
pub(crate) fn custom_value_operation(
pub fn custom_value_operation(
&self,
left: Spanned<PluginCustomValue>,
operator: Spanned<Operator>,
@ -885,7 +891,7 @@ impl PluginInterface {
}
/// Notify the plugin about a dropped custom value.
pub(crate) fn custom_value_dropped(&self, value: PluginCustomValue) -> Result<(), ShellError> {
pub fn custom_value_dropped(&self, value: PluginCustomValue) -> Result<(), ShellError> {
// Note: the protocol is always designed to have a span with the custom value, but this
// operation doesn't support one.
self.custom_value_op_expecting_value(

View File

@ -170,7 +170,7 @@ impl<T> FromShellError for Result<T, ShellError> {
///
/// The `signal` contained
#[derive(Debug)]
pub(crate) struct StreamWriter<W: WriteStreamMessage> {
pub struct StreamWriter<W: WriteStreamMessage> {
id: StreamId,
signal: Arc<StreamWriterSignal>,
writer: W,
@ -308,7 +308,7 @@ impl StreamWriterSignal {
/// 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 fn new(high_pressure_mark: i32) -> StreamWriterSignal {
pub(crate) fn new(high_pressure_mark: i32) -> StreamWriterSignal {
assert!(high_pressure_mark > 0);
StreamWriterSignal {
@ -329,12 +329,12 @@ impl StreamWriterSignal {
/// 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 fn is_dropped(&self) -> Result<bool, ShellError> {
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 fn set_dropped(&self) -> Result<(), ShellError> {
pub(crate) fn set_dropped(&self) -> Result<(), ShellError> {
let mut state = self.lock()?;
state.dropped = true;
// Unblock the writers so they can terminate
@ -345,7 +345,7 @@ impl StreamWriterSignal {
/// 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 fn notify_sent(&self) -> Result<bool, ShellError> {
pub(crate) fn notify_sent(&self) -> Result<bool, ShellError> {
let mut state = self.lock()?;
state.unacknowledged =
state
@ -359,7 +359,7 @@ impl StreamWriterSignal {
}
/// Wait for acknowledgements before sending more data. Also returns if the stream is dropped.
pub fn wait_for_drain(&self) -> Result<(), ShellError> {
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
@ -374,7 +374,7 @@ impl StreamWriterSignal {
/// Notify the writers that a message has been acknowledged, so they can continue to write
/// if they were waiting.
pub fn notify_acknowledged(&self) -> Result<(), ShellError> {
pub(crate) fn notify_acknowledged(&self) -> Result<(), ShellError> {
let mut state = self.lock()?;
state.unacknowledged =
state
@ -390,7 +390,7 @@ impl StreamWriterSignal {
}
/// A sink for a [`StreamMessage`]
pub(crate) trait WriteStreamMessage {
pub trait WriteStreamMessage {
fn write_stream_message(&mut self, msg: StreamMessage) -> Result<(), ShellError>;
fn flush(&mut self) -> Result<(), ShellError>;
}
@ -413,7 +413,7 @@ impl StreamManagerState {
}
#[derive(Debug)]
pub(crate) struct StreamManager {
pub struct StreamManager {
state: Arc<Mutex<StreamManagerState>>,
}
@ -532,7 +532,7 @@ impl Drop for StreamManager {
/// Streams can be registered for reading, returning a [`StreamReader`], or for writing, returning
/// a [`StreamWriter`].
#[derive(Debug, Clone)]
pub(crate) struct StreamManagerHandle {
pub struct StreamManagerHandle {
state: Weak<Mutex<StreamManagerState>>,
}

View File

@ -1,5 +1,6 @@
use nu_engine::documentation::get_flags_section;
use nu_protocol::LabeledError;
use thiserror::Error;
use std::cmp::Ordering;
use std::collections::HashMap;
@ -13,7 +14,7 @@ use std::{env, thread};
use std::sync::mpsc::TrySendError;
use std::sync::{mpsc, Arc, Mutex};
use crate::plugin::interface::{EngineInterfaceManager, ReceivedPluginCall};
use crate::plugin::interface::ReceivedPluginCall;
use crate::protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
use crate::EncodingType;
@ -29,6 +30,7 @@ use nu_protocol::{
};
use self::gc::PluginGc;
pub use self::interface::{PluginRead, PluginWrite};
mod command;
mod context;
@ -40,14 +42,14 @@ mod source;
pub use command::{PluginCommand, SimplePluginCommand};
pub use declaration::PluginDeclaration;
pub use interface::EngineInterface;
pub use persistent::PersistentPlugin;
pub use interface::{
EngineInterface, EngineInterfaceManager, Interface, InterfaceManager, PluginInterface,
PluginInterfaceManager,
};
pub use persistent::{GetPlugin, PersistentPlugin};
pub(crate) use context::PluginExecutionCommandContext;
pub(crate) use interface::PluginInterface;
pub(crate) use source::PluginSource;
use interface::{InterfaceManager, PluginInterfaceManager};
pub use context::{PluginExecutionCommandContext, PluginExecutionContext};
pub use source::PluginSource;
pub(crate) const OUTPUT_BUFFER_SIZE: usize = 8192;
@ -440,19 +442,6 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
std::process::exit(1)
}
// Build commands map, to make running a command easier
let mut commands: HashMap<String, _> = HashMap::new();
for command in plugin.commands() {
if let Some(previous) = commands.insert(command.signature().sig.name.clone(), command) {
eprintln!(
"Plugin `{plugin_name}` warning: command `{}` shadowed by another command with the \
same name. Check your command signatures",
previous.signature().sig.name
);
}
}
// tell nushell encoding.
//
// 1 byte
@ -471,7 +460,114 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
.expect("Failed to tell nushell my encoding when flushing stdout");
}
let mut manager = EngineInterfaceManager::new((stdout, encoder.clone()));
let encoder_clone = encoder.clone();
let result = serve_plugin_io(
plugin,
&plugin_name,
move || (std::io::stdin().lock(), encoder_clone),
move || (std::io::stdout(), encoder),
);
match result {
Ok(()) => (),
// Write unreported errors to the console
Err(ServePluginError::UnreportedError(err)) => {
eprintln!("Plugin `{plugin_name}` error: {err}");
std::process::exit(1);
}
Err(_) => std::process::exit(1),
}
}
/// An error from [`serve_plugin_io()`]
#[derive(Debug, Error)]
pub enum ServePluginError {
/// An error occurred that could not be reported to the engine.
#[error("{0}")]
UnreportedError(#[source] ShellError),
/// An error occurred that could be reported to the engine.
#[error("{0}")]
ReportedError(#[source] ShellError),
/// A version mismatch occurred.
#[error("{0}")]
Incompatible(#[source] ShellError),
/// An I/O error occurred.
#[error("{0}")]
IOError(#[source] ShellError),
/// A thread spawning error occurred.
#[error("{0}")]
ThreadSpawnError(#[source] std::io::Error),
}
impl From<ShellError> for ServePluginError {
fn from(error: ShellError) -> Self {
match error {
ShellError::IOError { .. } => ServePluginError::IOError(error),
ShellError::PluginFailedToLoad { .. } => ServePluginError::Incompatible(error),
_ => ServePluginError::UnreportedError(error),
}
}
}
/// Convert result error to ReportedError if it can be reported to the engine.
trait TryToReport {
type T;
fn try_to_report(self, engine: &EngineInterface) -> Result<Self::T, ServePluginError>;
}
impl<T, E> TryToReport for Result<T, E>
where
E: Into<ServePluginError>,
{
type T = T;
fn try_to_report(self, engine: &EngineInterface) -> Result<T, ServePluginError> {
self.map_err(|e| match e.into() {
ServePluginError::UnreportedError(err) => {
if engine.write_response(Err(err.clone())).is_ok() {
ServePluginError::ReportedError(err)
} else {
ServePluginError::UnreportedError(err)
}
}
other => other,
})
}
}
/// Serve a plugin on the given input & output.
///
/// Unlike [`serve_plugin`], this doesn't assume total control over the process lifecycle / stdin /
/// stdout, and can be used for more advanced use cases.
///
/// This is not a public API.
#[doc(hidden)]
pub fn serve_plugin_io<I, O>(
plugin: &impl Plugin,
plugin_name: &str,
input: impl FnOnce() -> I + Send + 'static,
output: impl FnOnce() -> O + Send + 'static,
) -> Result<(), ServePluginError>
where
I: PluginRead<PluginInput> + 'static,
O: PluginWrite<PluginOutput> + 'static,
{
let (error_tx, error_rx) = mpsc::channel();
// Build commands map, to make running a command easier
let mut commands: HashMap<String, _> = HashMap::new();
for command in plugin.commands() {
if let Some(previous) = commands.insert(command.signature().sig.name.clone(), command) {
eprintln!(
"Plugin `{plugin_name}` warning: command `{}` shadowed by another command with the \
same name. Check your command signatures",
previous.signature().sig.name
);
}
}
let mut manager = EngineInterfaceManager::new(output());
let call_receiver = manager
.take_plugin_call_receiver()
// This expect should be totally safe, as we just created the manager
@ -480,54 +576,22 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
// We need to hold on to the interface to keep the manager alive. We can drop it at the end
let interface = manager.get_interface();
// Try an operation that could result in ShellError. Exit if an I/O error is encountered.
// Try to report the error to nushell otherwise, and failing that, panic.
macro_rules! try_or_report {
($interface:expr, $expr:expr) => (match $expr {
Ok(val) => val,
// Just exit if there is an I/O error. Most likely this just means that nushell
// interrupted us. If not, the error probably happened on the other side too, so we
// don't need to also report it.
Err(ShellError::IOError { .. }) => std::process::exit(1),
// If there is another error, try to send it to nushell and then exit.
Err(err) => {
let _ = $interface.write_response(Err(err.clone())).unwrap_or_else(|_| {
// If we can't send it to nushell, panic with it so at least we get the output
panic!("Plugin `{plugin_name}`: {}", err)
});
std::process::exit(1)
}
})
}
// Send Hello message
try_or_report!(interface, interface.hello());
let plugin_name_clone = plugin_name.clone();
interface.hello()?;
{
// Spawn the reader thread
let error_tx = error_tx.clone();
std::thread::Builder::new()
.name("engine interface reader".into())
.spawn(move || {
if let Err(err) = manager.consume_all((std::io::stdin().lock(), encoder)) {
// Do our best to report the read error. Most likely there is some kind of
// incompatibility between the plugin and nushell, so it makes more sense to try to
// report it on stderr than to send something.
//
// Don't report a `PluginFailedToLoad` error, as it's probably just from Hello
// version mismatch which the engine side would also report.
if !matches!(err, ShellError::PluginFailedToLoad { .. }) {
eprintln!("Plugin `{plugin_name_clone}` read error: {err}");
}
std::process::exit(1);
// Report the error on the channel if we get an error
if let Err(err) = manager.consume_all(input()) {
let _ = error_tx.send(ServePluginError::from(err));
}
})
.unwrap_or_else(|err| {
// If we fail to spawn the reader thread, we should exit
eprintln!("Plugin `{plugin_name}` failed to launch: {err}");
std::process::exit(1);
});
.map_err(ServePluginError::ThreadSpawnError)?;
}
// Handle each Run plugin call on a thread
thread::scope(|scope| {
@ -545,8 +609,11 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
};
let write_result = engine
.write_response(result)
.and_then(|writer| writer.write());
try_or_report!(engine, write_result);
.and_then(|writer| writer.write())
.try_to_report(&engine);
if let Err(err) = write_result {
let _ = error_tx.send(err);
}
};
// As an optimization: create one thread that can be reused for Run calls in sequence
@ -558,13 +625,14 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
run(engine, call);
}
})
.unwrap_or_else(|err| {
// If we fail to spawn the runner thread, we should exit
eprintln!("Plugin `{plugin_name}` failed to launch: {err}");
std::process::exit(1);
});
.map_err(ServePluginError::ThreadSpawnError)?;
for plugin_call in call_receiver {
// Check for pending errors
if let Ok(error) = error_rx.try_recv() {
return Err(error);
}
match plugin_call {
// Sending the signature back to nushell to create the declaration definition
ReceivedPluginCall::Signature { engine } => {
@ -572,7 +640,7 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
.values()
.map(|command| command.signature())
.collect();
try_or_report!(engine, engine.write_signature(sigs));
engine.write_signature(sigs).try_to_report(&engine)?;
}
// Run the plugin on a background thread, handling any input or output streams
ReceivedPluginCall::Run { engine, call } => {
@ -582,14 +650,10 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
// If the primary thread isn't ready, spawn a secondary thread to do it
Err(TrySendError::Full((engine, call)))
| Err(TrySendError::Disconnected((engine, call))) => {
let engine_clone = engine.clone();
try_or_report!(
engine_clone,
thread::Builder::new()
.name("plugin runner (secondary)".into())
.spawn_scoped(scope, move || run(engine, call))
.map_err(ShellError::from)
);
.map_err(ServePluginError::ThreadSpawnError)?;
}
}
}
@ -599,14 +663,23 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
custom_value,
op,
} => {
try_or_report!(engine, custom_value_op(plugin, &engine, custom_value, op));
custom_value_op(plugin, &engine, custom_value, op).try_to_report(&engine)?;
}
}
}
});
Ok::<_, ServePluginError>(())
})?;
// This will stop the manager
drop(interface);
// Receive any error left on the channel
if let Ok(err) = error_rx.try_recv() {
Err(err)
} else {
Ok(())
}
}
fn custom_value_op(

View File

@ -3,7 +3,10 @@ use std::{
sync::{Arc, Mutex},
};
use nu_protocol::{PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError};
use nu_protocol::{
engine::{EngineState, Stack},
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
};
use super::{create_command, gc::PluginGc, make_plugin_interface, PluginInterface, PluginSource};
@ -81,6 +84,9 @@ impl PersistentPlugin {
//
// 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 new_running = self.clone().spawn(envs()?, &mutable.gc_config)?;
let interface = new_running.interface.clone();
mutable.running = Some(new_running);
@ -126,7 +132,7 @@ impl PersistentPlugin {
let pid = child.id();
let interface =
make_plugin_interface(child, Arc::new(PluginSource::new(&self)), Some(gc.clone()))?;
make_plugin_interface(child, Arc::new(PluginSource::new(self)), Some(gc.clone()))?;
Ok(RunningPlugin { pid, interface, gc })
}
@ -193,3 +199,38 @@ impl RegisteredPlugin for PersistentPlugin {
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>,
context: Option<(&EngineState, &mut Stack)>,
) -> Result<PluginInterface, ShellError> {
self.get(|| {
// Get envs from the context if provided.
let envs = context
.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.into_iter().flatten())
})
}
}

View File

@ -1,26 +1,31 @@
use std::sync::{Arc, Weak};
use nu_protocol::{PluginIdentity, RegisteredPlugin, ShellError, Span};
use nu_protocol::{PluginIdentity, ShellError, Span};
use super::PersistentPlugin;
use super::GetPlugin;
/// 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)]
pub(crate) struct PluginSource {
#[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<PersistentPlugin>,
pub(crate) persistent: Weak<dyn GetPlugin>,
}
impl PluginSource {
/// Create from an `Arc<PersistentPlugin>`
pub(crate) fn new(plugin: &Arc<PersistentPlugin>) -> 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),
persistent: Arc::downgrade(&plugin),
}
}
@ -31,16 +36,13 @@ impl PluginSource {
pub(crate) fn new_fake(name: &str) -> PluginSource {
PluginSource {
identity: PluginIdentity::new_fake(name).into(),
persistent: Weak::new(),
persistent: Weak::<crate::PersistentPlugin>::new(),
}
}
/// Try to upgrade the persistent reference, and return an error referencing `span` as the
/// object that referenced it otherwise
pub(crate) fn persistent(
&self,
span: Option<Span>,
) -> Result<Arc<PersistentPlugin>, ShellError> {
pub(crate) fn persistent(&self, span: Option<Span>) -> Result<Arc<dyn GetPlugin>, ShellError> {
self.persistent
.upgrade()
.ok_or_else(|| ShellError::GenericError {

View File

@ -192,7 +192,10 @@ impl CustomValueOp {
}
/// 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),

View File

@ -20,7 +20,10 @@ mod tests;
/// 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: SerdeArc<SharedContent>,
@ -197,12 +200,9 @@ impl PluginCustomValue {
})
})?;
// Envs probably should be passed here, but it's likely that the plugin is already running
let empty_envs = std::iter::empty::<(&str, &str)>();
source
.persistent(span)
.and_then(|p| p.get(|| Ok(empty_envs)))
.and_then(|p| p.get_plugin(None))
.map_err(wrap_err)
}
@ -237,7 +237,7 @@ impl PluginCustomValue {
}
/// Add a [`PluginSource`] to all [`PluginCustomValue`]s within a value, recursively.
pub(crate) fn add_source(value: &mut Value, source: &Arc<PluginSource>) {
pub fn add_source(value: &mut Value, source: &Arc<PluginSource>) {
// This can't cause an error.
let _: Result<(), Infallible> = value.recurse_mut(&mut |value| {
let span = value.span();
@ -318,7 +318,7 @@ impl PluginCustomValue {
/// Convert all plugin-native custom values to [`PluginCustomValue`] within the given `value`,
/// recursively. This should only be done on the plugin side.
pub(crate) fn serialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
pub fn serialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
value.recurse_mut(&mut |value| {
let span = value.span();
match value {
@ -344,7 +344,7 @@ impl PluginCustomValue {
/// Convert all [`PluginCustomValue`]s to plugin-native custom values within the given `value`,
/// recursively. This should only be done on the plugin side.
pub(crate) fn deserialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
pub fn deserialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
value.recurse_mut(&mut |value| {
let span = value.span();
match value {

View File

@ -4,7 +4,7 @@ use nu_protocol::ShellError;
/// Implements an atomically incrementing sequential series of numbers
#[derive(Debug, Default)]
pub(crate) struct Sequence(AtomicUsize);
pub struct Sequence(AtomicUsize);
impl Sequence {
/// Return the next available id from a sequence, returning an error on overflow

View File

@ -754,9 +754,9 @@ impl Value {
}
}
/// Update the value with a new span
pub fn with_span(mut self, new_span: Span) -> Value {
match &mut self {
/// Set the value's span to a new span
pub fn set_span(&mut self, new_span: Span) {
match self {
Value::Bool { internal_span, .. }
| Value::Int { internal_span, .. }
| Value::Float { internal_span, .. }
@ -777,7 +777,11 @@ impl Value {
| Value::CustomValue { internal_span, .. } => *internal_span = new_span,
Value::Error { .. } => (),
}
}
/// Update the value with a new span
pub fn with_span(mut self, new_span: Span) -> Value {
self.set_span(new_span);
self
}