mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 00:13:21 +01:00
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:
parent
ff41cf91ef
commit
c79c43d2f8
20
Cargo.lock
generated
20
Cargo.lock
generated
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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));
|
||||
}
|
||||
|
19
crates/nu-plugin-test-support/Cargo.toml
Normal file
19
crates/nu-plugin-test-support/Cargo.toml
Normal 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"
|
21
crates/nu-plugin-test-support/LICENSE
Normal file
21
crates/nu-plugin-test-support/LICENSE
Normal 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.
|
4
crates/nu-plugin-test-support/README.md
Normal file
4
crates/nu-plugin-test-support/README.md
Normal 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.
|
71
crates/nu-plugin-test-support/src/fake_persistent_plugin.rs
Normal file
71
crates/nu-plugin-test-support/src/fake_persistent_plugin.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
}
|
27
crates/nu-plugin-test-support/src/fake_register.rs
Normal file
27
crates/nu-plugin-test-support/src/fake_register.rs
Normal 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)
|
||||
}
|
71
crates/nu-plugin-test-support/src/lib.rs
Normal file
71
crates/nu-plugin-test-support/src/lib.rs
Normal 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;
|
264
crates/nu-plugin-test-support/src/plugin_test.rs
Normal file
264
crates/nu-plugin-test-support/src/plugin_test.rs
Normal 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)
|
||||
}
|
||||
}
|
77
crates/nu-plugin-test-support/src/spawn_fake_plugin.rs
Normal file
77
crates/nu-plugin-test-support/src/spawn_fake_plugin.rs
Normal 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)
|
||||
}
|
129
crates/nu-plugin-test-support/tests/custom_value/mod.rs
Normal file
129
crates/nu-plugin-test-support/tests/custom_value/mod.rs
Normal 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(())
|
||||
}
|
65
crates/nu-plugin-test-support/tests/hello/mod.rs
Normal file
65
crates/nu-plugin-test-support/tests/hello/mod.rs
Normal 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(())
|
||||
}
|
76
crates/nu-plugin-test-support/tests/lowercase/mod.rs
Normal file
76
crates/nu-plugin-test-support/tests/lowercase/mod.rs
Normal 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)
|
||||
}
|
3
crates/nu-plugin-test-support/tests/main.rs
Normal file
3
crates/nu-plugin-test-support/tests/main.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod custom_value;
|
||||
mod hello;
|
||||
mod lowercase;
|
@ -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 = [
|
||||
|
6
crates/nu-plugin/README.md
Normal file
6
crates/nu-plugin/README.md
Normal 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.
|
||||
|
@ -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;
|
||||
|
@ -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>,
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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>>,
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
interface.hello()?;
|
||||
|
||||
let plugin_name_clone = plugin_name.clone();
|
||||
|
||||
// Spawn the reader thread
|
||||
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}");
|
||||
{
|
||||
// Spawn the reader thread
|
||||
let error_tx = error_tx.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("engine interface reader".into())
|
||||
.spawn(move || {
|
||||
// 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));
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
})
|
||||
.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)
|
||||
);
|
||||
thread::Builder::new()
|
||||
.name("plugin runner (secondary)".into())
|
||||
.spawn_scoped(scope, move || run(engine, call))
|
||||
.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(
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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),
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user