mirror of
https://github.com/nushell/nushell.git
synced 2024-11-21 16:03:19 +01:00
Split the plugin crate (#12563)
# Description This breaks `nu-plugin` up into four crates: - `nu-plugin-protocol`: just the type definitions for the protocol, no I/O. If someone wanted to wire up something more bare metal, maybe for async I/O, they could use this. - `nu-plugin-core`: the shared stuff between engine/plugin. Less stable interface. - `nu-plugin-engine`: everything required for the engine to talk to plugins. Less stable interface. - `nu-plugin`: everything required for the plugin to talk to the engine, what plugin developers use. Should be the most stable interface. No changes are made to the interface exposed by `nu-plugin` - it should all still be there. Re-exports from `nu-plugin-protocol` or `nu-plugin-core` are used as required. Plugins shouldn't ever have to use those crates directly. This should be somewhat faster to compile as `nu-plugin-engine` and `nu-plugin` can compile in parallel, and the engine doesn't need `nu-plugin` and plugins don't need `nu-plugin-engine` (except for test support), so that should reduce what needs to be compiled too. The only significant change here other than splitting stuff up was to break the `source` out of `PluginCustomValue` and create a new `PluginCustomValueWithSource` type that contains that instead. One bonus of that is we get rid of the option and it's now more type-safe, but it also means that the logic for that stuff (actually running the plugin for custom value ops) can live entirely within the `nu-plugin-engine` crate. # User-Facing Changes - New crates. - Added `local-socket` feature for `nu` to try to make it possible to compile without that support if needed. # Tests + Formatting - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib`
This commit is contained in:
parent
884d5312bb
commit
0c4d5330ee
67
Cargo.lock
generated
67
Cargo.lock
generated
@ -2876,7 +2876,9 @@ dependencies = [
|
||||
"nu-lsp",
|
||||
"nu-parser",
|
||||
"nu-path",
|
||||
"nu-plugin",
|
||||
"nu-plugin-core",
|
||||
"nu-plugin-engine",
|
||||
"nu-plugin-protocol",
|
||||
"nu-protocol",
|
||||
"nu-std",
|
||||
"nu-system",
|
||||
@ -2923,7 +2925,7 @@ dependencies = [
|
||||
"nu-engine",
|
||||
"nu-parser",
|
||||
"nu-path",
|
||||
"nu-plugin",
|
||||
"nu-plugin-engine",
|
||||
"nu-protocol",
|
||||
"nu-test-support",
|
||||
"nu-utils",
|
||||
@ -3017,7 +3019,7 @@ dependencies = [
|
||||
"itertools 0.12.1",
|
||||
"nu-engine",
|
||||
"nu-path",
|
||||
"nu-plugin",
|
||||
"nu-plugin-engine",
|
||||
"nu-protocol",
|
||||
]
|
||||
|
||||
@ -3221,7 +3223,7 @@ dependencies = [
|
||||
"log",
|
||||
"nu-engine",
|
||||
"nu-path",
|
||||
"nu-plugin",
|
||||
"nu-plugin-engine",
|
||||
"nu-protocol",
|
||||
"rstest",
|
||||
"serde_json",
|
||||
@ -3240,24 +3242,58 @@ dependencies = [
|
||||
name = "nu-plugin"
|
||||
version = "0.92.3"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"interprocess",
|
||||
"log",
|
||||
"miette",
|
||||
"nix",
|
||||
"nu-engine",
|
||||
"nu-plugin-core",
|
||||
"nu-plugin-protocol",
|
||||
"nu-protocol",
|
||||
"nu-system",
|
||||
"nu-utils",
|
||||
"rmp-serde",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"typetag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-plugin-core"
|
||||
version = "0.92.3"
|
||||
dependencies = [
|
||||
"interprocess",
|
||||
"log",
|
||||
"nu-plugin-protocol",
|
||||
"nu-protocol",
|
||||
"rmp-serde",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-plugin-engine"
|
||||
version = "0.92.3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"nu-engine",
|
||||
"nu-plugin-core",
|
||||
"nu-plugin-protocol",
|
||||
"nu-protocol",
|
||||
"nu-system",
|
||||
"serde",
|
||||
"typetag",
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-plugin-protocol"
|
||||
version = "0.92.3"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"nu-protocol",
|
||||
"nu-utils",
|
||||
"semver",
|
||||
"serde",
|
||||
"typetag",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-plugin-test-support"
|
||||
version = "0.92.3"
|
||||
@ -3267,6 +3303,9 @@ dependencies = [
|
||||
"nu-engine",
|
||||
"nu-parser",
|
||||
"nu-plugin",
|
||||
"nu-plugin-core",
|
||||
"nu-plugin-engine",
|
||||
"nu-plugin-protocol",
|
||||
"nu-protocol",
|
||||
"serde",
|
||||
"similar",
|
||||
@ -4980,9 +5019,9 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422"
|
||||
|
||||
[[package]]
|
||||
name = "rmp"
|
||||
version = "0.8.13"
|
||||
version = "0.8.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bddb316f4b9cae1a3e89c02f1926d557d1142d0d2e684b038c11c1b77705229a"
|
||||
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"num-traits",
|
||||
|
10
Cargo.toml
10
Cargo.toml
@ -41,6 +41,9 @@ members = [
|
||||
"crates/nu-pretty-hex",
|
||||
"crates/nu-protocol",
|
||||
"crates/nu-plugin",
|
||||
"crates/nu-plugin-core",
|
||||
"crates/nu-plugin-engine",
|
||||
"crates/nu-plugin-protocol",
|
||||
"crates/nu-plugin-test-support",
|
||||
"crates/nu_plugin_inc",
|
||||
"crates/nu_plugin_gstat",
|
||||
@ -90,6 +93,7 @@ heck = "0.5.0"
|
||||
human-date-parser = "0.1.1"
|
||||
indexmap = "2.2"
|
||||
indicatif = "0.17"
|
||||
interprocess = "1.2.1"
|
||||
is_executable = "1.0"
|
||||
itertools = "0.12"
|
||||
libc = "0.2"
|
||||
@ -184,7 +188,7 @@ nu-explore = { path = "./crates/nu-explore", version = "0.92.3" }
|
||||
nu-lsp = { path = "./crates/nu-lsp/", version = "0.92.3" }
|
||||
nu-parser = { path = "./crates/nu-parser", version = "0.92.3" }
|
||||
nu-path = { path = "./crates/nu-path", version = "0.92.3" }
|
||||
nu-plugin = { path = "./crates/nu-plugin", optional = true, version = "0.92.3" }
|
||||
nu-plugin-engine = { path = "./crates/nu-plugin-engine", optional = true, version = "0.92.3" }
|
||||
nu-protocol = { path = "./crates/nu-protocol", version = "0.92.3" }
|
||||
nu-std = { path = "./crates/nu-std", version = "0.92.3" }
|
||||
nu-system = { path = "./crates/nu-system", version = "0.92.3" }
|
||||
@ -218,6 +222,8 @@ nix = { workspace = true, default-features = false, features = [
|
||||
|
||||
[dev-dependencies]
|
||||
nu-test-support = { path = "./crates/nu-test-support", version = "0.92.3" }
|
||||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.92.3" }
|
||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.92.3" }
|
||||
assert_cmd = "2.0"
|
||||
dirs-next = { workspace = true }
|
||||
divan = "0.1.14"
|
||||
@ -228,7 +234,7 @@ tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
plugin = [
|
||||
"nu-plugin",
|
||||
"nu-plugin-engine",
|
||||
"nu-cmd-plugin",
|
||||
"nu-cli/plugin",
|
||||
"nu-parser/plugin",
|
||||
|
@ -1,6 +1,7 @@
|
||||
use nu_cli::{eval_source, evaluate_commands};
|
||||
use nu_parser::parse;
|
||||
use nu_plugin::{Encoder, EncodingType, PluginCallResponse, PluginOutput};
|
||||
use nu_plugin_core::{Encoder, EncodingType};
|
||||
use nu_plugin_protocol::{PluginCallResponse, PluginOutput};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
eval_const::create_nu_constant,
|
||||
|
@ -21,7 +21,7 @@ nu-cmd-base = { path = "../nu-cmd-base", version = "0.92.3" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.92.3" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.92.3", optional = true }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.92.3", optional = true }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.92.3" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.92.3" }
|
||||
@ -45,5 +45,5 @@ uuid = { workspace = true, features = ["v4"] }
|
||||
which = { workspace = true }
|
||||
|
||||
[features]
|
||||
plugin = ["nu-plugin"]
|
||||
plugin = ["nu-plugin-engine"]
|
||||
system-clipboard = ["reedline/system_clipboard"]
|
||||
|
@ -150,7 +150,7 @@ pub fn read_plugin_file(
|
||||
|
||||
let mut working_set = StateWorkingSet::new(engine_state);
|
||||
|
||||
nu_plugin::load_plugin_file(&mut working_set, &contents, span);
|
||||
nu_plugin_engine::load_plugin_file(&mut working_set, &contents, span);
|
||||
|
||||
if let Err(err) = engine_state.merge_delta(working_set.render()) {
|
||||
report_error_new(engine_state, &err);
|
||||
|
@ -13,7 +13,7 @@ version = "0.92.3"
|
||||
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3", features = ["plugin"] }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.92.3" }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.92.3" }
|
||||
|
||||
itertools = { workspace = true }
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nu_engine::{command_prelude::*, current_dir};
|
||||
use nu_plugin::{GetPlugin, PersistentPlugin};
|
||||
use nu_plugin_engine::{GetPlugin, PersistentPlugin};
|
||||
use nu_protocol::{PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin};
|
||||
|
||||
use crate::util::{get_plugin_dirs, modify_plugin_file};
|
||||
|
@ -14,7 +14,7 @@ bench = false
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||
nu-plugin = { path = "../nu-plugin", optional = true, version = "0.92.3" }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", optional = true, version = "0.92.3" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
|
||||
|
||||
bytesize = { workspace = true }
|
||||
@ -27,4 +27,4 @@ serde_json = { workspace = true }
|
||||
rstest = { workspace = true, default-features = false }
|
||||
|
||||
[features]
|
||||
plugin = ["nu-plugin"]
|
||||
plugin = ["nu-plugin-engine"]
|
||||
|
@ -3555,7 +3555,7 @@ pub fn parse_where(working_set: &mut StateWorkingSet, lite_command: &LiteCommand
|
||||
/// `register` is deprecated and will be removed in 0.94. Use `plugin add` and `plugin use` instead.
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline {
|
||||
use nu_plugin::{get_signature, PluginDeclaration};
|
||||
use nu_plugin_engine::PluginDeclaration;
|
||||
use nu_protocol::{
|
||||
engine::Stack, ErrSpan, ParseWarning, PluginIdentity, PluginRegistryItem, PluginSignature,
|
||||
RegisteredPlugin,
|
||||
@ -3714,7 +3714,7 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
|
||||
// Create the plugin identity. This validates that the plugin name starts with `nu_plugin_`
|
||||
let identity = PluginIdentity::new(path, shell).err_span(path_span)?;
|
||||
|
||||
let plugin = nu_plugin::add_plugin_to_working_set(working_set, &identity)
|
||||
let plugin = nu_plugin_engine::add_plugin_to_working_set(working_set, &identity)
|
||||
.map_err(|err| err.wrap(working_set, call.head))?;
|
||||
|
||||
let signatures = signature.map_or_else(
|
||||
@ -3731,7 +3731,11 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
|
||||
)
|
||||
})?;
|
||||
|
||||
let signatures = get_signature(plugin.clone(), get_envs).map_err(|err| {
|
||||
let signatures = plugin
|
||||
.clone()
|
||||
.get(get_envs)
|
||||
.and_then(|p| p.get_signature())
|
||||
.map_err(|err| {
|
||||
log::warn!("Error getting signatures: {err:?}");
|
||||
ParseError::LabeledError(
|
||||
"Error getting signatures".into(),
|
||||
@ -3863,7 +3867,7 @@ pub fn parse_plugin_use(working_set: &mut StateWorkingSet, call: Box<Call>) -> P
|
||||
})?;
|
||||
|
||||
// Now add the signatures to the working set
|
||||
nu_plugin::load_plugin_registry_item(working_set, plugin_item, Some(call.head))
|
||||
nu_plugin_engine::load_plugin_registry_item(working_set, plugin_item, Some(call.head))
|
||||
.map_err(|err| err.wrap(working_set, call.head))?;
|
||||
|
||||
Ok(())
|
||||
|
28
crates/nu-plugin-core/Cargo.toml
Normal file
28
crates/nu-plugin-core/Cargo.toml
Normal file
@ -0,0 +1,28 @@
|
||||
[package]
|
||||
authors = ["The Nushell Project Developers"]
|
||||
description = "Shared internal functionality to support Nushell plugins"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-core"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-plugin-core"
|
||||
version = "0.92.3"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
|
||||
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.92.3" }
|
||||
|
||||
rmp-serde = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
interprocess = { workspace = true, optional = true }
|
||||
|
||||
[features]
|
||||
default = ["local-socket"]
|
||||
local-socket = ["interprocess"]
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { workspace = true }
|
21
crates/nu-plugin-core/LICENSE
Normal file
21
crates/nu-plugin-core/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.
|
3
crates/nu-plugin-core/README.md
Normal file
3
crates/nu-plugin-core/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# nu-plugin-core
|
||||
|
||||
This crate provides functionality that is shared by the [Nushell](https://nushell.sh/) engine and plugins.
|
@ -13,8 +13,15 @@ mod local_socket;
|
||||
#[cfg(feature = "local-socket")]
|
||||
use local_socket::*;
|
||||
|
||||
/// The type of communication used between the plugin and the engine.
|
||||
///
|
||||
/// `Stdio` is required to be supported by all plugins, and is attempted initially. If the
|
||||
/// `local-socket` feature is enabled and the plugin supports it, `LocalSocket` may be attempted.
|
||||
///
|
||||
/// Local socket communication has the benefit of not tying up stdio, so it's more compatible with
|
||||
/// plugins that want to take user input from the terminal in some way.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum CommunicationMode {
|
||||
pub enum CommunicationMode {
|
||||
/// Communicate using `stdin` and `stdout`.
|
||||
Stdio,
|
||||
/// Communicate using an operating system-specific local socket.
|
||||
@ -115,8 +122,15 @@ impl CommunicationMode {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum PreparedServerCommunication {
|
||||
/// The result of [`CommunicationMode::serve()`], which acts as an intermediate stage for
|
||||
/// communication modes that require some kind of socket binding to occur before the client process
|
||||
/// can be started. Call [`.connect()`] once the client process has been started.
|
||||
///
|
||||
/// The socket may be cleaned up on `Drop` if applicable.
|
||||
pub enum PreparedServerCommunication {
|
||||
/// Will take stdin and stdout from the process on [`.connect()`].
|
||||
Stdio,
|
||||
/// Contains the listener to accept connections on. On Unix, the socket is unlinked on `Drop`.
|
||||
#[cfg(feature = "local-socket")]
|
||||
LocalSocket {
|
||||
#[cfg_attr(windows, allow(dead_code))] // not used on Windows
|
||||
@ -214,7 +228,8 @@ impl Drop for PreparedServerCommunication {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ServerCommunicationIo {
|
||||
/// The required streams for communication from the engine side, i.e. the server in socket terms.
|
||||
pub enum ServerCommunicationIo {
|
||||
Stdio(ChildStdin, ChildStdout),
|
||||
#[cfg(feature = "local-socket")]
|
||||
LocalSocket {
|
||||
@ -223,7 +238,8 @@ pub(crate) enum ServerCommunicationIo {
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) enum ClientCommunicationIo {
|
||||
/// The required streams for communication from the plugin side, i.e. the client in socket terms.
|
||||
pub enum ClientCommunicationIo {
|
||||
Stdio(Stdin, Stdout),
|
||||
#[cfg(feature = "local-socket")]
|
||||
LocalSocket {
|
@ -1,11 +1,7 @@
|
||||
//! Implements the stream multiplexing interface for both the plugin side and the engine side.
|
||||
|
||||
use crate::{
|
||||
plugin::Encoder,
|
||||
protocol::{
|
||||
use nu_plugin_protocol::{
|
||||
ExternalStreamInfo, ListStreamInfo, PipelineDataHeader, RawStreamInfo, StreamMessage,
|
||||
},
|
||||
sequence::Sequence,
|
||||
};
|
||||
use nu_protocol::{ListStream, PipelineData, RawStream, ShellError};
|
||||
use std::{
|
||||
@ -17,18 +13,13 @@ use std::{
|
||||
thread,
|
||||
};
|
||||
|
||||
mod stream;
|
||||
pub mod stream;
|
||||
|
||||
mod engine;
|
||||
pub use engine::{EngineInterface, EngineInterfaceManager, ReceivedPluginCall};
|
||||
|
||||
mod plugin;
|
||||
pub use plugin::{PluginInterface, PluginInterfaceManager};
|
||||
use crate::{util::Sequence, Encoder};
|
||||
|
||||
use self::stream::{StreamManager, StreamManagerHandle, StreamWriter, WriteStreamMessage};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_util;
|
||||
pub mod test_util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@ -42,9 +33,6 @@ const LIST_STREAM_HIGH_PRESSURE: i32 = 100;
|
||||
const RAW_STREAM_HIGH_PRESSURE: i32 = 50;
|
||||
|
||||
/// Read input/output from the stream.
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[doc(hidden)]
|
||||
pub trait PluginRead<T> {
|
||||
/// Returns `Ok(None)` on end of stream.
|
||||
fn read(&mut self) -> Result<Option<T>, ShellError>;
|
||||
@ -72,9 +60,6 @@ where
|
||||
/// Write input/output to the stream.
|
||||
///
|
||||
/// The write should be atomic, without interference from other threads.
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[doc(hidden)]
|
||||
pub trait PluginWrite<T>: Send + Sync {
|
||||
fn write(&self, data: &T) -> Result<(), ShellError>;
|
||||
|
||||
@ -146,15 +131,13 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// An interface manager handles I/O and state management for communication between a plugin and the
|
||||
/// engine. See [`PluginInterfaceManager`] for communication from the engine side to a plugin, or
|
||||
/// [`EngineInterfaceManager`] for communication from the plugin side to the engine.
|
||||
/// An interface manager handles I/O and state management for communication between a plugin and
|
||||
/// the engine. See `PluginInterfaceManager` in `nu-plugin-engine` for communication from the engine
|
||||
/// side to a plugin, or `EngineInterfaceManager` in `nu-plugin` for communication from the plugin
|
||||
/// side to the engine.
|
||||
///
|
||||
/// There is typically one [`InterfaceManager`] consuming input from a background thread, and
|
||||
/// managing shared state.
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[doc(hidden)]
|
||||
pub trait InterfaceManager {
|
||||
/// The corresponding interface type.
|
||||
type Interface: Interface + 'static;
|
||||
@ -233,13 +216,10 @@ pub trait InterfaceManager {
|
||||
}
|
||||
|
||||
/// An interface provides an API for communicating with a plugin or the engine and facilitates
|
||||
/// stream I/O. See [`PluginInterface`] for the API from the engine side to a plugin, or
|
||||
/// [`EngineInterface`] for the API from the plugin side to the engine.
|
||||
/// stream I/O. See `PluginInterface` in `nu-plugin-engine` for the API from the engine side to a
|
||||
/// plugin, or `EngineInterface` in `nu-plugin` for the API from the plugin side to the engine.
|
||||
///
|
||||
/// There can be multiple copies of the interface managed by a single [`InterfaceManager`].
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[doc(hidden)]
|
||||
pub trait Interface: Clone + Send {
|
||||
/// The output message type, which must be capable of encapsulating a [`StreamMessage`].
|
||||
type Output: From<StreamMessage>;
|
||||
@ -253,7 +233,7 @@ pub trait Interface: Clone + Send {
|
||||
/// Flush the output buffer, so messages are visible to the other side.
|
||||
fn flush(&self) -> Result<(), ShellError>;
|
||||
|
||||
/// Get the sequence for generating new [`StreamId`](crate::protocol::StreamId)s.
|
||||
/// Get the sequence for generating new [`StreamId`](nu_plugin_protocol::StreamId)s.
|
||||
fn stream_id_sequence(&self) -> &Sequence;
|
||||
|
||||
/// Get the [`StreamManagerHandle`] for doing stream operations.
|
||||
@ -384,7 +364,7 @@ where
|
||||
W: WriteStreamMessage + Send + 'static,
|
||||
{
|
||||
/// Write all of the data in each of the streams. This method waits for completion.
|
||||
pub(crate) fn write(self) -> Result<(), ShellError> {
|
||||
pub fn write(self) -> Result<(), ShellError> {
|
||||
match self {
|
||||
// If no stream was contained in the PipelineData, do nothing.
|
||||
PipelineDataWriter::None => Ok(()),
|
||||
@ -442,7 +422,7 @@ where
|
||||
|
||||
/// Write all of the data in each of the streams. This method returns immediately; any necessary
|
||||
/// write will happen in the background. If a thread was spawned, its handle is returned.
|
||||
pub(crate) fn write_background(
|
||||
pub fn write_background(
|
||||
self,
|
||||
) -> Result<Option<thread::JoinHandle<Result<(), ShellError>>>, ShellError> {
|
||||
match self {
|
@ -1,4 +1,4 @@
|
||||
use crate::protocol::{StreamData, StreamId, StreamMessage};
|
||||
use nu_plugin_protocol::{StreamData, StreamId, StreamMessage};
|
||||
use nu_protocol::{ShellError, Span, Value};
|
||||
use std::{
|
||||
collections::{btree_map, BTreeMap},
|
||||
@ -25,7 +25,7 @@ mod tests;
|
||||
/// For each message read, it sends [`StreamMessage::Ack`] to the writer. When dropped,
|
||||
/// it sends [`StreamMessage::Drop`].
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct StreamReader<T, W>
|
||||
pub struct StreamReader<T, W>
|
||||
where
|
||||
W: WriteStreamMessage,
|
||||
{
|
||||
@ -43,7 +43,7 @@ where
|
||||
W: WriteStreamMessage,
|
||||
{
|
||||
/// Create a new StreamReader from parts
|
||||
pub(crate) fn new(
|
||||
fn new(
|
||||
id: StreamId,
|
||||
receiver: mpsc::Receiver<Result<Option<StreamData>, ShellError>>,
|
||||
writer: W,
|
||||
@ -61,7 +61,7 @@ where
|
||||
/// * the channel couldn't be received from
|
||||
/// * an error was sent on the channel
|
||||
/// * the message received couldn't be converted to `T`
|
||||
pub(crate) fn recv(&mut self) -> Result<Option<T>, ShellError> {
|
||||
pub fn recv(&mut self) -> Result<Option<T>, ShellError> {
|
||||
let connection_lost = || ShellError::GenericError {
|
||||
error: "Stream ended unexpectedly".into(),
|
||||
msg: "connection lost before explicit end of stream".into(),
|
||||
@ -146,7 +146,7 @@ where
|
||||
}
|
||||
|
||||
/// Values that can contain a `ShellError` to signal an error has occurred.
|
||||
pub(crate) trait FromShellError {
|
||||
pub trait FromShellError {
|
||||
fn from_shell_error(err: ShellError) -> Self;
|
||||
}
|
||||
|
||||
@ -179,7 +179,7 @@ impl<W> StreamWriter<W>
|
||||
where
|
||||
W: WriteStreamMessage,
|
||||
{
|
||||
pub(crate) fn new(id: StreamId, signal: Arc<StreamWriterSignal>, writer: W) -> StreamWriter<W> {
|
||||
fn new(id: StreamId, signal: Arc<StreamWriterSignal>, writer: W) -> StreamWriter<W> {
|
||||
StreamWriter {
|
||||
id,
|
||||
signal,
|
||||
@ -190,7 +190,7 @@ where
|
||||
|
||||
/// Check if the stream was dropped from the other end. Recommended to do this before calling
|
||||
/// [`.write()`], especially in a loop.
|
||||
pub(crate) fn is_dropped(&self) -> Result<bool, ShellError> {
|
||||
pub fn is_dropped(&self) -> Result<bool, ShellError> {
|
||||
self.signal.is_dropped()
|
||||
}
|
||||
|
||||
@ -198,7 +198,7 @@ where
|
||||
///
|
||||
/// Error if something failed with the write, or if [`.end()`] was already called
|
||||
/// previously.
|
||||
pub(crate) fn write(&mut self, data: impl Into<StreamData>) -> Result<(), ShellError> {
|
||||
pub fn write(&mut self, data: impl Into<StreamData>) -> Result<(), ShellError> {
|
||||
if !self.ended {
|
||||
self.writer
|
||||
.write_stream_message(StreamMessage::Data(self.id, data.into()))?;
|
||||
@ -232,10 +232,7 @@ where
|
||||
///
|
||||
/// Returns `Ok(true)` if the iterator was fully consumed, or `Ok(false)` if a drop interrupted
|
||||
/// the stream from the other side.
|
||||
pub(crate) fn write_all<T>(
|
||||
&mut self,
|
||||
data: impl IntoIterator<Item = T>,
|
||||
) -> Result<bool, ShellError>
|
||||
pub fn write_all<T>(&mut self, data: impl IntoIterator<Item = T>) -> Result<bool, ShellError>
|
||||
where
|
||||
T: Into<StreamData>,
|
||||
{
|
||||
@ -257,7 +254,7 @@ where
|
||||
|
||||
/// End the stream. Recommend doing this instead of relying on `Drop` so that you can catch the
|
||||
/// error.
|
||||
pub(crate) fn end(&mut self) -> Result<(), ShellError> {
|
||||
pub fn end(&mut self) -> Result<(), ShellError> {
|
||||
if !self.ended {
|
||||
// Set the flag first so we don't double-report in the Drop
|
||||
self.ended = true;
|
||||
@ -285,13 +282,13 @@ where
|
||||
/// Stores stream state for a writer, and can be blocked on to wait for messages to be acknowledged.
|
||||
/// A key part of managing stream lifecycle and flow control.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct StreamWriterSignal {
|
||||
pub struct StreamWriterSignal {
|
||||
mutex: Mutex<StreamWriterSignalState>,
|
||||
change_cond: Condvar,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct StreamWriterSignalState {
|
||||
pub struct StreamWriterSignalState {
|
||||
/// Stream has been dropped and consumer is no longer interested in any messages.
|
||||
dropped: bool,
|
||||
/// Number of messages that have been sent without acknowledgement.
|
||||
@ -306,7 +303,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(crate) fn new(high_pressure_mark: i32) -> StreamWriterSignal {
|
||||
fn new(high_pressure_mark: i32) -> StreamWriterSignal {
|
||||
assert!(high_pressure_mark > 0);
|
||||
|
||||
StreamWriterSignal {
|
||||
@ -327,12 +324,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(crate) fn is_dropped(&self) -> Result<bool, ShellError> {
|
||||
pub fn is_dropped(&self) -> Result<bool, ShellError> {
|
||||
Ok(self.lock()?.dropped)
|
||||
}
|
||||
|
||||
/// Notify the writers that the stream has been dropped, so they can stop writing.
|
||||
pub(crate) fn set_dropped(&self) -> Result<(), ShellError> {
|
||||
pub fn set_dropped(&self) -> Result<(), ShellError> {
|
||||
let mut state = self.lock()?;
|
||||
state.dropped = true;
|
||||
// Unblock the writers so they can terminate
|
||||
@ -343,7 +340,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(crate) fn notify_sent(&self) -> Result<bool, ShellError> {
|
||||
pub fn notify_sent(&self) -> Result<bool, ShellError> {
|
||||
let mut state = self.lock()?;
|
||||
state.unacknowledged =
|
||||
state
|
||||
@ -357,7 +354,7 @@ impl StreamWriterSignal {
|
||||
}
|
||||
|
||||
/// Wait for acknowledgements before sending more data. Also returns if the stream is dropped.
|
||||
pub(crate) fn wait_for_drain(&self) -> Result<(), ShellError> {
|
||||
pub fn wait_for_drain(&self) -> Result<(), ShellError> {
|
||||
let mut state = self.lock()?;
|
||||
while !state.dropped && state.unacknowledged >= state.high_pressure_mark {
|
||||
state = self
|
||||
@ -372,7 +369,7 @@ impl StreamWriterSignal {
|
||||
|
||||
/// Notify the writers that a message has been acknowledged, so they can continue to write
|
||||
/// if they were waiting.
|
||||
pub(crate) fn notify_acknowledged(&self) -> Result<(), ShellError> {
|
||||
pub fn notify_acknowledged(&self) -> Result<(), ShellError> {
|
||||
let mut state = self.lock()?;
|
||||
state.unacknowledged =
|
||||
state
|
||||
@ -417,7 +414,7 @@ pub struct StreamManager {
|
||||
|
||||
impl StreamManager {
|
||||
/// Create a new StreamManager.
|
||||
pub(crate) fn new() -> StreamManager {
|
||||
pub fn new() -> StreamManager {
|
||||
StreamManager {
|
||||
state: Default::default(),
|
||||
}
|
||||
@ -428,14 +425,14 @@ impl StreamManager {
|
||||
}
|
||||
|
||||
/// Create a new handle to the StreamManager for registering streams.
|
||||
pub(crate) fn get_handle(&self) -> StreamManagerHandle {
|
||||
pub fn get_handle(&self) -> StreamManagerHandle {
|
||||
StreamManagerHandle {
|
||||
state: Arc::downgrade(&self.state),
|
||||
}
|
||||
}
|
||||
|
||||
/// Process a stream message, and update internal state accordingly.
|
||||
pub(crate) fn handle_message(&self, message: StreamMessage) -> Result<(), ShellError> {
|
||||
pub fn handle_message(&self, message: StreamMessage) -> Result<(), ShellError> {
|
||||
let mut state = self.lock()?;
|
||||
match message {
|
||||
StreamMessage::Data(id, data) => {
|
||||
@ -492,7 +489,7 @@ impl StreamManager {
|
||||
}
|
||||
|
||||
/// Broadcast an error to all stream readers. This is useful for error propagation.
|
||||
pub(crate) fn broadcast_read_error(&self, error: ShellError) -> Result<(), ShellError> {
|
||||
pub fn broadcast_read_error(&self, error: ShellError) -> Result<(), ShellError> {
|
||||
let state = self.lock()?;
|
||||
for channel in state.reading_streams.values() {
|
||||
// Ignore send errors.
|
||||
@ -517,6 +514,12 @@ impl StreamManager {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StreamManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for StreamManager {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = self.drop_all_writers() {
|
||||
@ -557,7 +560,7 @@ impl StreamManagerHandle {
|
||||
/// Register a new stream for reading, and return a [`StreamReader`] that can be used to iterate
|
||||
/// on the values received. A [`StreamMessage`] writer is required for writing control messages
|
||||
/// back to the producer.
|
||||
pub(crate) fn read_stream<T, W>(
|
||||
pub fn read_stream<T, W>(
|
||||
&self,
|
||||
id: StreamId,
|
||||
writer: W,
|
||||
@ -591,7 +594,7 @@ impl StreamManagerHandle {
|
||||
/// The `high_pressure_mark` value controls how many messages can be written without receiving
|
||||
/// an acknowledgement before any further attempts to write will wait for the consumer to
|
||||
/// acknowledge them. This prevents overwhelming the reader.
|
||||
pub(crate) fn write_stream<W>(
|
||||
pub fn write_stream<W>(
|
||||
&self,
|
||||
id: StreamId,
|
||||
writer: W,
|
@ -7,7 +7,7 @@ use std::{
|
||||
};
|
||||
|
||||
use super::{StreamManager, StreamReader, StreamWriter, StreamWriterSignal, WriteStreamMessage};
|
||||
use crate::protocol::{StreamData, StreamMessage};
|
||||
use nu_plugin_protocol::{StreamData, StreamMessage};
|
||||
use nu_protocol::{ShellError, Value};
|
||||
|
||||
// Should be long enough to definitely complete any quick operation, but not so long that tests are
|
@ -1,20 +1,22 @@
|
||||
use super::{EngineInterfaceManager, PluginInterfaceManager, PluginRead, PluginWrite};
|
||||
use crate::{plugin::PluginSource, protocol::PluginInput, PluginOutput};
|
||||
use nu_protocol::ShellError;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::{PluginRead, PluginWrite};
|
||||
|
||||
const FAILED: &str = "failed to lock TestCase";
|
||||
|
||||
/// Mock read/write helper for the engine and plugin interfaces.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct TestCase<I, O> {
|
||||
pub struct TestCase<I, O> {
|
||||
r#in: Arc<Mutex<TestData<I>>>,
|
||||
out: Arc<Mutex<TestData<O>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct TestData<T> {
|
||||
pub struct TestData<T> {
|
||||
data: VecDeque<T>,
|
||||
error: Option<ShellError>,
|
||||
flushed: bool,
|
||||
@ -32,7 +34,7 @@ impl<T> Default for TestData<T> {
|
||||
|
||||
impl<I, O> PluginRead<I> for TestCase<I, O> {
|
||||
fn read(&mut self) -> Result<Option<I>, ShellError> {
|
||||
let mut lock = self.r#in.lock().unwrap();
|
||||
let mut lock = self.r#in.lock().expect(FAILED);
|
||||
if let Some(err) = lock.error.take() {
|
||||
Err(err)
|
||||
} else {
|
||||
@ -47,7 +49,7 @@ where
|
||||
O: Send + Clone,
|
||||
{
|
||||
fn write(&self, data: &O) -> Result<(), ShellError> {
|
||||
let mut lock = self.out.lock().unwrap();
|
||||
let mut lock = self.out.lock().expect(FAILED);
|
||||
lock.flushed = false;
|
||||
|
||||
if let Some(err) = lock.error.take() {
|
||||
@ -59,7 +61,7 @@ where
|
||||
}
|
||||
|
||||
fn flush(&self) -> Result<(), ShellError> {
|
||||
let mut lock = self.out.lock().unwrap();
|
||||
let mut lock = self.out.lock().expect(FAILED);
|
||||
lock.flushed = true;
|
||||
Ok(())
|
||||
}
|
||||
@ -67,7 +69,7 @@ where
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl<I, O> TestCase<I, O> {
|
||||
pub(crate) fn new() -> TestCase<I, O> {
|
||||
pub fn new() -> TestCase<I, O> {
|
||||
TestCase {
|
||||
r#in: Default::default(),
|
||||
out: Default::default(),
|
||||
@ -75,66 +77,58 @@ impl<I, O> TestCase<I, O> {
|
||||
}
|
||||
|
||||
/// Clear the read buffer.
|
||||
pub(crate) fn clear(&self) {
|
||||
self.r#in.lock().unwrap().data.truncate(0);
|
||||
pub fn clear(&self) {
|
||||
self.r#in.lock().expect(FAILED).data.truncate(0);
|
||||
}
|
||||
|
||||
/// Add input that will be read by the interface.
|
||||
pub(crate) fn add(&self, input: impl Into<I>) {
|
||||
self.r#in.lock().unwrap().data.push_back(input.into());
|
||||
pub fn add(&self, input: impl Into<I>) {
|
||||
self.r#in.lock().expect(FAILED).data.push_back(input.into());
|
||||
}
|
||||
|
||||
/// Add multiple inputs that will be read by the interface.
|
||||
pub(crate) fn extend(&self, inputs: impl IntoIterator<Item = I>) {
|
||||
self.r#in.lock().unwrap().data.extend(inputs);
|
||||
pub fn extend(&self, inputs: impl IntoIterator<Item = I>) {
|
||||
self.r#in.lock().expect(FAILED).data.extend(inputs);
|
||||
}
|
||||
|
||||
/// Return an error from the next read operation.
|
||||
pub(crate) fn set_read_error(&self, err: ShellError) {
|
||||
self.r#in.lock().unwrap().error = Some(err);
|
||||
pub fn set_read_error(&self, err: ShellError) {
|
||||
self.r#in.lock().expect(FAILED).error = Some(err);
|
||||
}
|
||||
|
||||
/// Return an error from the next write operation.
|
||||
pub(crate) fn set_write_error(&self, err: ShellError) {
|
||||
self.out.lock().unwrap().error = Some(err);
|
||||
pub fn set_write_error(&self, err: ShellError) {
|
||||
self.out.lock().expect(FAILED).error = Some(err);
|
||||
}
|
||||
|
||||
/// Get the next output that was written.
|
||||
pub(crate) fn next_written(&self) -> Option<O> {
|
||||
self.out.lock().unwrap().data.pop_front()
|
||||
pub fn next_written(&self) -> Option<O> {
|
||||
self.out.lock().expect(FAILED).data.pop_front()
|
||||
}
|
||||
|
||||
/// Iterator over written data.
|
||||
pub(crate) fn written(&self) -> impl Iterator<Item = O> + '_ {
|
||||
pub fn written(&self) -> impl Iterator<Item = O> + '_ {
|
||||
std::iter::from_fn(|| self.next_written())
|
||||
}
|
||||
|
||||
/// Returns true if the writer was flushed after the last write operation.
|
||||
pub(crate) fn was_flushed(&self) -> bool {
|
||||
self.out.lock().unwrap().flushed
|
||||
pub fn was_flushed(&self) -> bool {
|
||||
self.out.lock().expect(FAILED).flushed
|
||||
}
|
||||
|
||||
/// Returns true if the reader has unconsumed reads.
|
||||
pub(crate) fn has_unconsumed_read(&self) -> bool {
|
||||
!self.r#in.lock().unwrap().data.is_empty()
|
||||
pub fn has_unconsumed_read(&self) -> bool {
|
||||
!self.r#in.lock().expect(FAILED).data.is_empty()
|
||||
}
|
||||
|
||||
/// Returns true if the writer has unconsumed writes.
|
||||
pub(crate) fn has_unconsumed_write(&self) -> bool {
|
||||
!self.out.lock().unwrap().data.is_empty()
|
||||
pub fn has_unconsumed_write(&self) -> bool {
|
||||
!self.out.lock().expect(FAILED).data.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase<PluginOutput, PluginInput> {
|
||||
/// Create a new [`PluginInterfaceManager`] that writes to this test case.
|
||||
pub(crate) fn plugin(&self, name: &str) -> PluginInterfaceManager {
|
||||
PluginInterfaceManager::new(PluginSource::new_fake(name).into(), None, self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCase<PluginInput, PluginOutput> {
|
||||
/// Create a new [`EngineInterfaceManager`] that writes to this test case.
|
||||
pub(crate) fn engine(&self) -> EngineInterfaceManager {
|
||||
EngineInterfaceManager::new(self.clone())
|
||||
impl<I, O> Default for TestCase<I, O> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
573
crates/nu-plugin-core/src/interface/tests.rs
Normal file
573
crates/nu-plugin-core/src/interface/tests.rs
Normal file
@ -0,0 +1,573 @@
|
||||
use crate::util::Sequence;
|
||||
|
||||
use super::{
|
||||
stream::{StreamManager, StreamManagerHandle},
|
||||
test_util::TestCase,
|
||||
Interface, InterfaceManager, PluginRead, PluginWrite,
|
||||
};
|
||||
use nu_plugin_protocol::{
|
||||
ExternalStreamInfo, ListStreamInfo, PipelineDataHeader, PluginInput, PluginOutput,
|
||||
RawStreamInfo, StreamData, StreamMessage,
|
||||
};
|
||||
use nu_protocol::{
|
||||
DataSource, ListStream, PipelineData, PipelineMetadata, RawStream, ShellError, Span, Value,
|
||||
};
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
fn test_metadata() -> PipelineMetadata {
|
||||
PipelineMetadata {
|
||||
data_source: DataSource::FilePath("/test/path".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestInterfaceManager {
|
||||
stream_manager: StreamManager,
|
||||
test: TestCase<PluginInput, PluginOutput>,
|
||||
seq: Arc<Sequence>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TestInterface {
|
||||
stream_manager_handle: StreamManagerHandle,
|
||||
test: TestCase<PluginInput, PluginOutput>,
|
||||
seq: Arc<Sequence>,
|
||||
}
|
||||
|
||||
impl TestInterfaceManager {
|
||||
fn new(test: &TestCase<PluginInput, PluginOutput>) -> TestInterfaceManager {
|
||||
TestInterfaceManager {
|
||||
stream_manager: StreamManager::new(),
|
||||
test: test.clone(),
|
||||
seq: Arc::new(Sequence::default()),
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_all(&mut self) -> Result<(), ShellError> {
|
||||
while let Some(msg) = self.test.read()? {
|
||||
self.consume(msg)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl InterfaceManager for TestInterfaceManager {
|
||||
type Interface = TestInterface;
|
||||
type Input = PluginInput;
|
||||
|
||||
fn get_interface(&self) -> Self::Interface {
|
||||
TestInterface {
|
||||
stream_manager_handle: self.stream_manager.get_handle(),
|
||||
test: self.test.clone(),
|
||||
seq: self.seq.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn consume(&mut self, input: Self::Input) -> Result<(), ShellError> {
|
||||
match input {
|
||||
PluginInput::Data(..)
|
||||
| PluginInput::End(..)
|
||||
| PluginInput::Drop(..)
|
||||
| PluginInput::Ack(..) => self.consume_stream_message(
|
||||
input
|
||||
.try_into()
|
||||
.expect("failed to convert message to StreamMessage"),
|
||||
),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stream_manager(&self) -> &StreamManager {
|
||||
&self.stream_manager
|
||||
}
|
||||
|
||||
fn prepare_pipeline_data(&self, data: PipelineData) -> Result<PipelineData, ShellError> {
|
||||
Ok(data.set_metadata(Some(test_metadata())))
|
||||
}
|
||||
}
|
||||
|
||||
impl Interface for TestInterface {
|
||||
type Output = PluginOutput;
|
||||
type DataContext = ();
|
||||
|
||||
fn write(&self, output: Self::Output) -> Result<(), ShellError> {
|
||||
self.test.write(&output)
|
||||
}
|
||||
|
||||
fn flush(&self) -> Result<(), ShellError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stream_id_sequence(&self) -> &Sequence {
|
||||
&self.seq
|
||||
}
|
||||
|
||||
fn stream_manager_handle(&self) -> &StreamManagerHandle {
|
||||
&self.stream_manager_handle
|
||||
}
|
||||
|
||||
fn prepare_pipeline_data(
|
||||
&self,
|
||||
data: PipelineData,
|
||||
_context: &(),
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
// Add an arbitrary check to the data to verify this is being called
|
||||
match data {
|
||||
PipelineData::Value(Value::Binary { .. }, None) => Err(ShellError::NushellFailed {
|
||||
msg: "TEST can't send binary".into(),
|
||||
}),
|
||||
_ => Ok(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_pipeline_data_empty() -> Result<(), ShellError> {
|
||||
let manager = TestInterfaceManager::new(&TestCase::new());
|
||||
let header = PipelineDataHeader::Empty;
|
||||
|
||||
assert!(matches!(
|
||||
manager.read_pipeline_data(header, None)?,
|
||||
PipelineData::Empty
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_pipeline_data_value() -> Result<(), ShellError> {
|
||||
let manager = TestInterfaceManager::new(&TestCase::new());
|
||||
let value = Value::test_int(4);
|
||||
let header = PipelineDataHeader::Value(value.clone());
|
||||
|
||||
match manager.read_pipeline_data(header, None)? {
|
||||
PipelineData::Value(read_value, _) => assert_eq!(value, read_value),
|
||||
PipelineData::ListStream(_, _) => panic!("unexpected ListStream"),
|
||||
PipelineData::ExternalStream { .. } => panic!("unexpected ExternalStream"),
|
||||
PipelineData::Empty => panic!("unexpected Empty"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_pipeline_data_list_stream() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
let mut manager = TestInterfaceManager::new(&test);
|
||||
|
||||
let data = (0..100).map(Value::test_int).collect::<Vec<_>>();
|
||||
|
||||
for value in &data {
|
||||
test.add(StreamMessage::Data(7, value.clone().into()));
|
||||
}
|
||||
test.add(StreamMessage::End(7));
|
||||
|
||||
let header = PipelineDataHeader::ListStream(ListStreamInfo { id: 7 });
|
||||
|
||||
let pipe = manager.read_pipeline_data(header, None)?;
|
||||
assert!(
|
||||
matches!(pipe, PipelineData::ListStream(..)),
|
||||
"unexpected PipelineData: {pipe:?}"
|
||||
);
|
||||
|
||||
// need to consume input
|
||||
manager.consume_all()?;
|
||||
|
||||
let mut count = 0;
|
||||
for (expected, read) in data.into_iter().zip(pipe) {
|
||||
assert_eq!(expected, read);
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(100, count);
|
||||
|
||||
assert!(test.has_unconsumed_write());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_pipeline_data_external_stream() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
let mut manager = TestInterfaceManager::new(&test);
|
||||
|
||||
let iterations = 100;
|
||||
let out_pattern = b"hello".to_vec();
|
||||
let err_pattern = vec![5, 4, 3, 2];
|
||||
|
||||
test.add(StreamMessage::Data(14, Value::test_int(1).into()));
|
||||
for _ in 0..iterations {
|
||||
test.add(StreamMessage::Data(
|
||||
12,
|
||||
StreamData::Raw(Ok(out_pattern.clone())),
|
||||
));
|
||||
test.add(StreamMessage::Data(
|
||||
13,
|
||||
StreamData::Raw(Ok(err_pattern.clone())),
|
||||
));
|
||||
}
|
||||
test.add(StreamMessage::End(12));
|
||||
test.add(StreamMessage::End(13));
|
||||
test.add(StreamMessage::End(14));
|
||||
|
||||
let test_span = Span::new(10, 13);
|
||||
let header = PipelineDataHeader::ExternalStream(ExternalStreamInfo {
|
||||
span: test_span,
|
||||
stdout: Some(RawStreamInfo {
|
||||
id: 12,
|
||||
is_binary: false,
|
||||
known_size: Some((out_pattern.len() * iterations) as u64),
|
||||
}),
|
||||
stderr: Some(RawStreamInfo {
|
||||
id: 13,
|
||||
is_binary: true,
|
||||
known_size: None,
|
||||
}),
|
||||
exit_code: Some(ListStreamInfo { id: 14 }),
|
||||
trim_end_newline: true,
|
||||
});
|
||||
|
||||
let pipe = manager.read_pipeline_data(header, None)?;
|
||||
|
||||
// need to consume input
|
||||
manager.consume_all()?;
|
||||
|
||||
match pipe {
|
||||
PipelineData::ExternalStream {
|
||||
stdout,
|
||||
stderr,
|
||||
exit_code,
|
||||
span,
|
||||
metadata,
|
||||
trim_end_newline,
|
||||
} => {
|
||||
let stdout = stdout.expect("stdout is None");
|
||||
let stderr = stderr.expect("stderr is None");
|
||||
let exit_code = exit_code.expect("exit_code is None");
|
||||
assert_eq!(test_span, span);
|
||||
assert!(
|
||||
metadata.is_some(),
|
||||
"expected metadata to be Some due to prepare_pipeline_data()"
|
||||
);
|
||||
assert!(trim_end_newline);
|
||||
|
||||
assert!(!stdout.is_binary);
|
||||
assert!(stderr.is_binary);
|
||||
|
||||
assert_eq!(
|
||||
Some((out_pattern.len() * iterations) as u64),
|
||||
stdout.known_size
|
||||
);
|
||||
assert_eq!(None, stderr.known_size);
|
||||
|
||||
// check the streams
|
||||
let mut count = 0;
|
||||
for chunk in stdout.stream {
|
||||
assert_eq!(out_pattern, chunk?);
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(iterations, count, "stdout length");
|
||||
let mut count = 0;
|
||||
|
||||
for chunk in stderr.stream {
|
||||
assert_eq!(err_pattern, chunk?);
|
||||
count += 1;
|
||||
}
|
||||
assert_eq!(iterations, count, "stderr length");
|
||||
|
||||
assert_eq!(vec![Value::test_int(1)], exit_code.collect::<Vec<_>>());
|
||||
}
|
||||
_ => panic!("unexpected PipelineData: {pipe:?}"),
|
||||
}
|
||||
|
||||
// Don't need to check exactly what was written, just be sure that there is some output
|
||||
assert!(test.has_unconsumed_write());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_pipeline_data_ctrlc() -> Result<(), ShellError> {
|
||||
let manager = TestInterfaceManager::new(&TestCase::new());
|
||||
let header = PipelineDataHeader::ListStream(ListStreamInfo { id: 0 });
|
||||
let ctrlc = Default::default();
|
||||
match manager.read_pipeline_data(header, Some(&ctrlc))? {
|
||||
PipelineData::ListStream(
|
||||
ListStream {
|
||||
ctrlc: stream_ctrlc,
|
||||
..
|
||||
},
|
||||
_,
|
||||
) => {
|
||||
assert!(Arc::ptr_eq(&ctrlc, &stream_ctrlc.expect("ctrlc not set")));
|
||||
Ok(())
|
||||
}
|
||||
_ => panic!("Unexpected PipelineData, should have been ListStream"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_pipeline_data_prepared_properly() -> Result<(), ShellError> {
|
||||
let manager = TestInterfaceManager::new(&TestCase::new());
|
||||
let header = PipelineDataHeader::ListStream(ListStreamInfo { id: 0 });
|
||||
match manager.read_pipeline_data(header, None)? {
|
||||
PipelineData::ListStream(_, meta) => match meta {
|
||||
Some(PipelineMetadata { data_source }) => match data_source {
|
||||
DataSource::FilePath(path) => {
|
||||
assert_eq!(Path::new("/test/path"), path);
|
||||
Ok(())
|
||||
}
|
||||
_ => panic!("wrong metadata: {data_source:?}"),
|
||||
},
|
||||
None => panic!("metadata not set"),
|
||||
},
|
||||
_ => panic!("Unexpected PipelineData, should have been ListStream"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_pipeline_data_empty() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
let manager = TestInterfaceManager::new(&test);
|
||||
let interface = manager.get_interface();
|
||||
|
||||
let (header, writer) = interface.init_write_pipeline_data(PipelineData::Empty, &())?;
|
||||
|
||||
assert!(matches!(header, PipelineDataHeader::Empty));
|
||||
|
||||
writer.write()?;
|
||||
|
||||
assert!(
|
||||
!test.has_unconsumed_write(),
|
||||
"Empty shouldn't write any stream messages, test: {test:#?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_pipeline_data_value() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
let manager = TestInterfaceManager::new(&test);
|
||||
let interface = manager.get_interface();
|
||||
let value = Value::test_int(7);
|
||||
|
||||
let (header, writer) =
|
||||
interface.init_write_pipeline_data(PipelineData::Value(value.clone(), None), &())?;
|
||||
|
||||
match header {
|
||||
PipelineDataHeader::Value(read_value) => assert_eq!(value, read_value),
|
||||
_ => panic!("unexpected header: {header:?}"),
|
||||
}
|
||||
|
||||
writer.write()?;
|
||||
|
||||
assert!(
|
||||
!test.has_unconsumed_write(),
|
||||
"Value shouldn't write any stream messages, test: {test:#?}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_pipeline_data_prepared_properly() {
|
||||
let manager = TestInterfaceManager::new(&TestCase::new());
|
||||
let interface = manager.get_interface();
|
||||
|
||||
// Sending a binary should be an error in our test scenario
|
||||
let value = Value::test_binary(vec![7, 8]);
|
||||
|
||||
match interface.init_write_pipeline_data(PipelineData::Value(value, None), &()) {
|
||||
Ok(_) => panic!("prepare_pipeline_data was not called"),
|
||||
Err(err) => {
|
||||
assert_eq!(
|
||||
ShellError::NushellFailed {
|
||||
msg: "TEST can't send binary".into()
|
||||
}
|
||||
.to_string(),
|
||||
err.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_pipeline_data_list_stream() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
let manager = TestInterfaceManager::new(&test);
|
||||
let interface = manager.get_interface();
|
||||
|
||||
let values = vec![
|
||||
Value::test_int(40),
|
||||
Value::test_bool(false),
|
||||
Value::test_string("this is a test"),
|
||||
];
|
||||
|
||||
// Set up pipeline data for a list stream
|
||||
let pipe = PipelineData::ListStream(
|
||||
ListStream::from_stream(values.clone().into_iter(), None),
|
||||
None,
|
||||
);
|
||||
|
||||
let (header, writer) = interface.init_write_pipeline_data(pipe, &())?;
|
||||
|
||||
let info = match header {
|
||||
PipelineDataHeader::ListStream(info) => info,
|
||||
_ => panic!("unexpected header: {header:?}"),
|
||||
};
|
||||
|
||||
writer.write()?;
|
||||
|
||||
// Now make sure the stream messages have been written
|
||||
for value in values {
|
||||
match test.next_written().expect("unexpected end of stream") {
|
||||
PluginOutput::Data(id, data) => {
|
||||
assert_eq!(info.id, id, "Data id");
|
||||
match data {
|
||||
StreamData::List(read_value) => assert_eq!(value, read_value, "Data value"),
|
||||
_ => panic!("unexpected Data: {data:?}"),
|
||||
}
|
||||
}
|
||||
other => panic!("unexpected output: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
match test.next_written().expect("unexpected end of stream") {
|
||||
PluginOutput::End(id) => {
|
||||
assert_eq!(info.id, id, "End id");
|
||||
}
|
||||
other => panic!("unexpected output: {other:?}"),
|
||||
}
|
||||
|
||||
assert!(!test.has_unconsumed_write());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_pipeline_data_external_stream() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
let manager = TestInterfaceManager::new(&test);
|
||||
let interface = manager.get_interface();
|
||||
|
||||
let stdout_bufs = vec![
|
||||
b"hello".to_vec(),
|
||||
b"world".to_vec(),
|
||||
b"these are tests".to_vec(),
|
||||
];
|
||||
let stdout_len = stdout_bufs.iter().map(|b| b.len() as u64).sum::<u64>();
|
||||
let stderr_bufs = vec![b"error messages".to_vec(), b"go here".to_vec()];
|
||||
let exit_code = Value::test_int(7);
|
||||
|
||||
let span = Span::new(400, 500);
|
||||
|
||||
// Set up pipeline data for an external stream
|
||||
let pipe = PipelineData::ExternalStream {
|
||||
stdout: Some(RawStream::new(
|
||||
Box::new(stdout_bufs.clone().into_iter().map(Ok)),
|
||||
None,
|
||||
span,
|
||||
Some(stdout_len),
|
||||
)),
|
||||
stderr: Some(RawStream::new(
|
||||
Box::new(stderr_bufs.clone().into_iter().map(Ok)),
|
||||
None,
|
||||
span,
|
||||
None,
|
||||
)),
|
||||
exit_code: Some(ListStream::from_stream(
|
||||
std::iter::once(exit_code.clone()),
|
||||
None,
|
||||
)),
|
||||
span,
|
||||
metadata: None,
|
||||
trim_end_newline: true,
|
||||
};
|
||||
|
||||
let (header, writer) = interface.init_write_pipeline_data(pipe, &())?;
|
||||
|
||||
let info = match header {
|
||||
PipelineDataHeader::ExternalStream(info) => info,
|
||||
_ => panic!("unexpected header: {header:?}"),
|
||||
};
|
||||
|
||||
writer.write()?;
|
||||
|
||||
let stdout_info = info.stdout.as_ref().expect("stdout info is None");
|
||||
let stderr_info = info.stderr.as_ref().expect("stderr info is None");
|
||||
let exit_code_info = info.exit_code.as_ref().expect("exit code info is None");
|
||||
|
||||
assert_eq!(span, info.span);
|
||||
assert!(info.trim_end_newline);
|
||||
|
||||
assert_eq!(Some(stdout_len), stdout_info.known_size);
|
||||
assert_eq!(None, stderr_info.known_size);
|
||||
|
||||
// Now make sure the stream messages have been written
|
||||
let mut stdout_iter = stdout_bufs.into_iter();
|
||||
let mut stderr_iter = stderr_bufs.into_iter();
|
||||
let mut exit_code_iter = std::iter::once(exit_code);
|
||||
|
||||
let mut stdout_ended = false;
|
||||
let mut stderr_ended = false;
|
||||
let mut exit_code_ended = false;
|
||||
|
||||
// There's no specific order these messages must come in with respect to how the streams are
|
||||
// interleaved, but all of the data for each stream must be in its original order, and the
|
||||
// End must come after all Data
|
||||
for msg in test.written() {
|
||||
match msg {
|
||||
PluginOutput::Data(id, data) => {
|
||||
if id == stdout_info.id {
|
||||
let result: Result<Vec<u8>, ShellError> =
|
||||
data.try_into().expect("wrong data in stdout stream");
|
||||
assert_eq!(
|
||||
stdout_iter.next().expect("too much data in stdout"),
|
||||
result.expect("unexpected error in stdout stream")
|
||||
);
|
||||
} else if id == stderr_info.id {
|
||||
let result: Result<Vec<u8>, ShellError> =
|
||||
data.try_into().expect("wrong data in stderr stream");
|
||||
assert_eq!(
|
||||
stderr_iter.next().expect("too much data in stderr"),
|
||||
result.expect("unexpected error in stderr stream")
|
||||
);
|
||||
} else if id == exit_code_info.id {
|
||||
let code: Value = data.try_into().expect("wrong data in stderr stream");
|
||||
assert_eq!(
|
||||
exit_code_iter.next().expect("too much data in stderr"),
|
||||
code
|
||||
);
|
||||
} else {
|
||||
panic!("unrecognized stream id: {id}");
|
||||
}
|
||||
}
|
||||
PluginOutput::End(id) => {
|
||||
if id == stdout_info.id {
|
||||
assert!(!stdout_ended, "double End of stdout");
|
||||
assert!(stdout_iter.next().is_none(), "unexpected end of stdout");
|
||||
stdout_ended = true;
|
||||
} else if id == stderr_info.id {
|
||||
assert!(!stderr_ended, "double End of stderr");
|
||||
assert!(stderr_iter.next().is_none(), "unexpected end of stderr");
|
||||
stderr_ended = true;
|
||||
} else if id == exit_code_info.id {
|
||||
assert!(!exit_code_ended, "double End of exit_code");
|
||||
assert!(
|
||||
exit_code_iter.next().is_none(),
|
||||
"unexpected end of exit_code"
|
||||
);
|
||||
exit_code_ended = true;
|
||||
} else {
|
||||
panic!("unrecognized stream id: {id}");
|
||||
}
|
||||
}
|
||||
other => panic!("unexpected output: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
assert!(stdout_ended, "stdout did not End");
|
||||
assert!(stderr_ended, "stderr did not End");
|
||||
assert!(exit_code_ended, "exit_code did not End");
|
||||
|
||||
Ok(())
|
||||
}
|
24
crates/nu-plugin-core/src/lib.rs
Normal file
24
crates/nu-plugin-core/src/lib.rs
Normal file
@ -0,0 +1,24 @@
|
||||
//! Functionality and types shared between the plugin and the engine, other than protocol types.
|
||||
//!
|
||||
//! If you are writing a plugin, you probably don't need this crate. We will make fewer guarantees
|
||||
//! for the stability of the interface of this crate than for `nu_plugin`.
|
||||
|
||||
pub mod util;
|
||||
|
||||
mod communication_mode;
|
||||
mod interface;
|
||||
mod serializers;
|
||||
|
||||
pub use communication_mode::{
|
||||
ClientCommunicationIo, CommunicationMode, PreparedServerCommunication, ServerCommunicationIo,
|
||||
};
|
||||
pub use interface::{
|
||||
stream::{FromShellError, StreamManager, StreamManagerHandle, StreamReader, StreamWriter},
|
||||
Interface, InterfaceManager, PipelineDataWriter, PluginRead, PluginWrite,
|
||||
};
|
||||
pub use serializers::{
|
||||
json::JsonSerializer, msgpack::MsgPackSerializer, Encoder, EncodingType, PluginEncoder,
|
||||
};
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use interface::test_util as interface_test_util;
|
@ -1,10 +1,9 @@
|
||||
use crate::{
|
||||
plugin::{Encoder, PluginEncoder},
|
||||
protocol::{PluginInput, PluginOutput},
|
||||
};
|
||||
use nu_plugin_protocol::{PluginInput, PluginOutput};
|
||||
use nu_protocol::ShellError;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{Encoder, PluginEncoder};
|
||||
|
||||
/// A `PluginEncoder` that enables the plugin to communicate with Nushell with JSON
|
||||
/// serialized data.
|
||||
///
|
71
crates/nu-plugin-core/src/serializers/mod.rs
Normal file
71
crates/nu-plugin-core/src/serializers/mod.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use nu_plugin_protocol::{PluginInput, PluginOutput};
|
||||
use nu_protocol::ShellError;
|
||||
|
||||
pub mod json;
|
||||
pub mod msgpack;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Encoder for a specific message type. Usually implemented on [`PluginInput`]
|
||||
/// and [`PluginOutput`].
|
||||
pub trait Encoder<T>: Clone + Send + Sync {
|
||||
/// Serialize a value in the [`PluginEncoder`]s format
|
||||
///
|
||||
/// Returns [`ShellError::IOError`] if there was a problem writing, or
|
||||
/// [`ShellError::PluginFailedToEncode`] for a serialization error.
|
||||
fn encode(&self, data: &T, writer: &mut impl std::io::Write) -> Result<(), ShellError>;
|
||||
|
||||
/// Deserialize a value from the [`PluginEncoder`]'s format
|
||||
///
|
||||
/// Returns `None` if there is no more output to receive.
|
||||
///
|
||||
/// Returns [`ShellError::IOError`] if there was a problem reading, or
|
||||
/// [`ShellError::PluginFailedToDecode`] for a deserialization error.
|
||||
fn decode(&self, reader: &mut impl std::io::BufRead) -> Result<Option<T>, ShellError>;
|
||||
}
|
||||
|
||||
/// Encoding scheme that defines a plugin's communication protocol with Nu
|
||||
pub trait PluginEncoder: Encoder<PluginInput> + Encoder<PluginOutput> {
|
||||
/// The name of the encoder (e.g., `json`)
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
/// Enum that supports all of the plugin serialization formats.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum EncodingType {
|
||||
Json(json::JsonSerializer),
|
||||
MsgPack(msgpack::MsgPackSerializer),
|
||||
}
|
||||
|
||||
impl EncodingType {
|
||||
/// Determine the plugin encoding type from the provided byte string (either `b"json"` or
|
||||
/// `b"msgpack"`).
|
||||
pub fn try_from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
match bytes {
|
||||
b"json" => Some(Self::Json(json::JsonSerializer {})),
|
||||
b"msgpack" => Some(Self::MsgPack(msgpack::MsgPackSerializer {})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Encoder<T> for EncodingType
|
||||
where
|
||||
json::JsonSerializer: Encoder<T>,
|
||||
msgpack::MsgPackSerializer: Encoder<T>,
|
||||
{
|
||||
fn encode(&self, data: &T, writer: &mut impl std::io::Write) -> Result<(), ShellError> {
|
||||
match self {
|
||||
EncodingType::Json(encoder) => encoder.encode(data, writer),
|
||||
EncodingType::MsgPack(encoder) => encoder.encode(data, writer),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(&self, reader: &mut impl std::io::BufRead) -> Result<Option<T>, ShellError> {
|
||||
match self {
|
||||
EncodingType::Json(encoder) => encoder.decode(reader),
|
||||
EncodingType::MsgPack(encoder) => encoder.decode(reader),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
use std::io::ErrorKind;
|
||||
|
||||
use crate::{
|
||||
plugin::{Encoder, PluginEncoder},
|
||||
protocol::{PluginInput, PluginOutput},
|
||||
};
|
||||
use nu_plugin_protocol::{PluginInput, PluginOutput};
|
||||
use nu_protocol::ShellError;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{Encoder, PluginEncoder};
|
||||
|
||||
/// A `PluginEncoder` that enables the plugin to communicate with Nushell with MsgPack
|
||||
/// serialized data.
|
||||
///
|
@ -1,6 +1,6 @@
|
||||
macro_rules! generate_tests {
|
||||
($encoder:expr) => {
|
||||
use crate::protocol::{
|
||||
use nu_plugin_protocol::{
|
||||
CallInfo, CustomValueOp, EvaluatedCall, PipelineDataHeader, PluginCall,
|
||||
PluginCallResponse, PluginCustomValue, PluginInput, PluginOption, PluginOutput,
|
||||
StreamData,
|
||||
@ -178,7 +178,7 @@ macro_rules! generate_tests {
|
||||
|
||||
let custom_value_op = PluginCall::CustomValueOp(
|
||||
Spanned {
|
||||
item: PluginCustomValue::new("Foo".into(), data.clone(), false, None),
|
||||
item: PluginCustomValue::new("Foo".into(), data.clone(), false),
|
||||
span,
|
||||
},
|
||||
CustomValueOp::ToBaseValue,
|
||||
@ -321,12 +321,7 @@ macro_rules! generate_tests {
|
||||
let span = Span::new(2, 30);
|
||||
|
||||
let value = Value::custom(
|
||||
Box::new(PluginCustomValue::new(
|
||||
name.into(),
|
||||
data.clone(),
|
||||
true,
|
||||
None,
|
||||
)),
|
||||
Box::new(PluginCustomValue::new(name.into(), data.clone(), true)),
|
||||
span,
|
||||
);
|
||||
|
7
crates/nu-plugin-core/src/util/mod.rs
Normal file
7
crates/nu-plugin-core/src/util/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod sequence;
|
||||
mod waitable;
|
||||
mod with_custom_values_in;
|
||||
|
||||
pub use sequence::Sequence;
|
||||
pub use waitable::*;
|
||||
pub use with_custom_values_in::with_custom_values_in;
|
@ -8,7 +8,7 @@ pub struct Sequence(AtomicUsize);
|
||||
impl Sequence {
|
||||
/// Return the next available id from a sequence, returning an error on overflow
|
||||
#[track_caller]
|
||||
pub(crate) fn next(&self) -> Result<usize, ShellError> {
|
||||
pub fn next(&self) -> Result<usize, ShellError> {
|
||||
// It's totally safe to use Relaxed ordering here, as there aren't other memory operations
|
||||
// that depend on this value having been set for safety
|
||||
//
|
@ -6,7 +6,7 @@ use nu_protocol::{CustomValue, IntoSpanned, ShellError, Spanned, Value};
|
||||
/// `LazyRecord`s will be collected to plain values for completeness.
|
||||
pub fn with_custom_values_in<E>(
|
||||
value: &mut Value,
|
||||
mut f: impl FnMut(Spanned<&mut (dyn CustomValue + '_)>) -> Result<(), E>,
|
||||
mut f: impl FnMut(Spanned<&mut Box<dyn CustomValue>>) -> Result<(), E>,
|
||||
) -> Result<(), E>
|
||||
where
|
||||
E: From<ShellError>,
|
||||
@ -16,7 +16,7 @@ where
|
||||
match value {
|
||||
Value::Custom { val, .. } => {
|
||||
// Operate on a CustomValue.
|
||||
f(val.as_mut().into_spanned(span))
|
||||
f(val.into_spanned(span))
|
||||
}
|
||||
// LazyRecord would be a problem for us, since it could return something else the
|
||||
// next time, and we have to collect it anyway to serialize it. Collect it in place,
|
||||
@ -32,7 +32,7 @@ where
|
||||
|
||||
#[test]
|
||||
fn find_custom_values() {
|
||||
use crate::protocol::test_util::test_plugin_custom_value;
|
||||
use nu_plugin_protocol::test_util::test_plugin_custom_value;
|
||||
use nu_protocol::{engine::Closure, record, LazyRecord, Span};
|
||||
|
||||
#[derive(Debug, Clone)]
|
34
crates/nu-plugin-engine/Cargo.toml
Normal file
34
crates/nu-plugin-engine/Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[package]
|
||||
authors = ["The Nushell Project Developers"]
|
||||
description = "Functionality for running Nushell plugins from a Nushell engine"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-engine"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-plugin-engine"
|
||||
version = "0.92.3"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
|
||||
nu-system = { path = "../nu-system", version = "0.92.3" }
|
||||
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.92.3" }
|
||||
nu-plugin-core = { path = "../nu-plugin-core", version = "0.92.3", default-features = false }
|
||||
|
||||
serde = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
typetag = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["local-socket"]
|
||||
local-socket = ["nu-plugin-core/local-socket"]
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { workspace = true, features = [
|
||||
# For setting process creation flags
|
||||
"Win32_System_Threading",
|
||||
] }
|
21
crates/nu-plugin-engine/LICENSE
Normal file
21
crates/nu-plugin-engine/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.
|
3
crates/nu-plugin-engine/README.md
Normal file
3
crates/nu-plugin-engine/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# nu-plugin-engine
|
||||
|
||||
This crate provides functionality for the [Nushell](https://nushell.sh/) engine to spawn and interact with plugins.
|
@ -15,9 +15,6 @@ use std::{
|
||||
};
|
||||
|
||||
/// Object safe trait for abstracting operations required of the plugin context.
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[doc(hidden)]
|
||||
pub trait PluginExecutionContext: Send + Sync {
|
||||
/// A span pointing to the command being executed
|
||||
fn span(&self) -> Span;
|
||||
@ -55,9 +52,6 @@ pub trait PluginExecutionContext: Send + Sync {
|
||||
}
|
||||
|
||||
/// The execution context of a plugin command. Can be borrowed.
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[doc(hidden)]
|
||||
pub struct PluginExecutionCommandContext<'a> {
|
||||
identity: Arc<PluginIdentity>,
|
||||
engine_state: Cow<'a, EngineState>,
|
@ -1,10 +1,11 @@
|
||||
use super::{GetPlugin, PluginExecutionCommandContext, PluginSource};
|
||||
use crate::protocol::{CallInfo, EvaluatedCall};
|
||||
use nu_engine::{command_prelude::*, get_eval_expression};
|
||||
use nu_plugin_protocol::{CallInfo, EvaluatedCall};
|
||||
use nu_protocol::{PluginIdentity, PluginSignature};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[doc(hidden)] // Note: not for plugin authors / only used in nu-parser
|
||||
use crate::{GetPlugin, PluginExecutionCommandContext, PluginSource};
|
||||
|
||||
/// The command declaration proxy used within the engine for all plugin commands.
|
||||
#[derive(Clone)]
|
||||
pub struct PluginDeclaration {
|
||||
name: String,
|
306
crates/nu-plugin-engine/src/init.rs
Normal file
306
crates/nu-plugin-engine/src/init.rs
Normal file
@ -0,0 +1,306 @@
|
||||
use std::{
|
||||
io::{BufReader, BufWriter},
|
||||
path::Path,
|
||||
process::Child,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use nu_plugin_core::{
|
||||
CommunicationMode, EncodingType, InterfaceManager, PreparedServerCommunication,
|
||||
ServerCommunicationIo,
|
||||
};
|
||||
use nu_protocol::{
|
||||
engine::StateWorkingSet, report_error_new, PluginIdentity, PluginRegistryFile,
|
||||
PluginRegistryItem, PluginRegistryItemData, RegisteredPlugin, ShellError, Span,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
PersistentPlugin, PluginDeclaration, PluginGc, PluginInterface, PluginInterfaceManager,
|
||||
PluginSource,
|
||||
};
|
||||
|
||||
pub(crate) const OUTPUT_BUFFER_SIZE: usize = 8192;
|
||||
|
||||
/// Spawn the command for a plugin, in the given `mode`. After spawning, it can be passed to
|
||||
/// [`make_plugin_interface()`] to get a [`PluginInterface`].
|
||||
pub fn create_command(
|
||||
path: &Path,
|
||||
mut shell: Option<&Path>,
|
||||
mode: &CommunicationMode,
|
||||
) -> std::process::Command {
|
||||
log::trace!("Starting plugin: {path:?}, shell = {shell:?}, mode = {mode:?}");
|
||||
|
||||
let mut shell_args = vec![];
|
||||
|
||||
if shell.is_none() {
|
||||
// We only have to do this for things that are not executable by Rust's Command API on
|
||||
// Windows. They do handle bat/cmd files for us, helpfully.
|
||||
//
|
||||
// Also include anything that wouldn't be executable with a shebang, like JAR files.
|
||||
shell = match path.extension().and_then(|e| e.to_str()) {
|
||||
Some("sh") => {
|
||||
if cfg!(unix) {
|
||||
// We don't want to override what might be in the shebang if this is Unix, since
|
||||
// some scripts will have a shebang specifying bash even if they're .sh
|
||||
None
|
||||
} else {
|
||||
Some(Path::new("sh"))
|
||||
}
|
||||
}
|
||||
Some("nu") => {
|
||||
shell_args.push("--stdin");
|
||||
Some(Path::new("nu"))
|
||||
}
|
||||
Some("py") => Some(Path::new("python")),
|
||||
Some("rb") => Some(Path::new("ruby")),
|
||||
Some("jar") => {
|
||||
shell_args.push("-jar");
|
||||
Some(Path::new("java"))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
let mut process = if let Some(shell) = shell {
|
||||
let mut process = std::process::Command::new(shell);
|
||||
process.args(shell_args);
|
||||
process.arg(path);
|
||||
|
||||
process
|
||||
} else {
|
||||
std::process::Command::new(path)
|
||||
};
|
||||
|
||||
process.args(mode.args());
|
||||
|
||||
// Setup I/O according to the communication mode
|
||||
mode.setup_command_io(&mut process);
|
||||
|
||||
// The plugin should be run in a new process group to prevent Ctrl-C from stopping it
|
||||
#[cfg(unix)]
|
||||
process.process_group(0);
|
||||
#[cfg(windows)]
|
||||
process.creation_flags(windows::Win32::System::Threading::CREATE_NEW_PROCESS_GROUP.0);
|
||||
|
||||
// In order to make bugs with improper use of filesystem without getting the engine current
|
||||
// directory more obvious, the plugin always starts in the directory of its executable
|
||||
if let Some(dirname) = path.parent() {
|
||||
process.current_dir(dirname);
|
||||
}
|
||||
|
||||
process
|
||||
}
|
||||
|
||||
/// Create a plugin interface from a spawned child process.
|
||||
///
|
||||
/// `comm` determines the communication type the process was spawned with, and whether stdio will
|
||||
/// be taken from the child.
|
||||
pub fn make_plugin_interface(
|
||||
mut child: Child,
|
||||
comm: PreparedServerCommunication,
|
||||
source: Arc<PluginSource>,
|
||||
pid: Option<u32>,
|
||||
gc: Option<PluginGc>,
|
||||
) -> Result<PluginInterface, ShellError> {
|
||||
match comm.connect(&mut child)? {
|
||||
ServerCommunicationIo::Stdio(stdin, stdout) => make_plugin_interface_with_streams(
|
||||
stdout,
|
||||
stdin,
|
||||
move || {
|
||||
let _ = child.wait();
|
||||
},
|
||||
source,
|
||||
pid,
|
||||
gc,
|
||||
),
|
||||
#[cfg(feature = "local-socket")]
|
||||
ServerCommunicationIo::LocalSocket { read_out, write_in } => {
|
||||
make_plugin_interface_with_streams(
|
||||
read_out,
|
||||
write_in,
|
||||
move || {
|
||||
let _ = child.wait();
|
||||
},
|
||||
source,
|
||||
pid,
|
||||
gc,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a plugin interface from low-level components.
|
||||
///
|
||||
/// - `after_close` is called to clean up after the `reader` ends.
|
||||
/// - `source` is required so that custom values produced by the plugin can spawn it.
|
||||
/// - `pid` may be provided for process management (e.g. `EnterForeground`).
|
||||
/// - `gc` may be provided for communication with the plugin's GC (e.g. `SetGcDisabled`).
|
||||
pub fn make_plugin_interface_with_streams(
|
||||
mut reader: impl std::io::Read + Send + 'static,
|
||||
writer: impl std::io::Write + Send + 'static,
|
||||
after_close: impl FnOnce() + Send + 'static,
|
||||
source: Arc<PluginSource>,
|
||||
pid: Option<u32>,
|
||||
gc: Option<PluginGc>,
|
||||
) -> Result<PluginInterface, ShellError> {
|
||||
let encoder = get_plugin_encoding(&mut reader)?;
|
||||
|
||||
let reader = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader);
|
||||
let writer = BufWriter::with_capacity(OUTPUT_BUFFER_SIZE, writer);
|
||||
|
||||
let mut manager =
|
||||
PluginInterfaceManager::new(source.clone(), pid, (Mutex::new(writer), encoder));
|
||||
manager.set_garbage_collector(gc);
|
||||
|
||||
let interface = manager.get_interface();
|
||||
interface.hello()?;
|
||||
|
||||
// Spawn the reader on a new thread. We need to be able to read messages at the same time that
|
||||
// we write, because we are expected to be able to handle multiple messages coming in from the
|
||||
// plugin at any time, including stream messages like `Drop`.
|
||||
std::thread::Builder::new()
|
||||
.name(format!(
|
||||
"plugin interface reader ({})",
|
||||
source.identity.name()
|
||||
))
|
||||
.spawn(move || {
|
||||
if let Err(err) = manager.consume_all((reader, encoder)) {
|
||||
log::warn!("Error in PluginInterfaceManager: {err}");
|
||||
}
|
||||
// If the loop has ended, drop the manager so everyone disconnects and then run
|
||||
// after_close
|
||||
drop(manager);
|
||||
after_close();
|
||||
})
|
||||
.map_err(|err| ShellError::PluginFailedToLoad {
|
||||
msg: format!("Failed to spawn thread for plugin: {err}"),
|
||||
})?;
|
||||
|
||||
Ok(interface)
|
||||
}
|
||||
|
||||
/// Determine the plugin's encoding from a freshly opened stream.
|
||||
///
|
||||
/// The plugin is expected to send a 1-byte length and either `json` or `msgpack`, so this reads
|
||||
/// that and determines the right length.
|
||||
pub fn get_plugin_encoding(
|
||||
child_stdout: &mut impl std::io::Read,
|
||||
) -> Result<EncodingType, ShellError> {
|
||||
let mut length_buf = [0u8; 1];
|
||||
child_stdout
|
||||
.read_exact(&mut length_buf)
|
||||
.map_err(|e| ShellError::PluginFailedToLoad {
|
||||
msg: format!("unable to get encoding from plugin: {e}"),
|
||||
})?;
|
||||
|
||||
let mut buf = vec![0u8; length_buf[0] as usize];
|
||||
child_stdout
|
||||
.read_exact(&mut buf)
|
||||
.map_err(|e| ShellError::PluginFailedToLoad {
|
||||
msg: format!("unable to get encoding from plugin: {e}"),
|
||||
})?;
|
||||
|
||||
EncodingType::try_from_bytes(&buf).ok_or_else(|| {
|
||||
let encoding_for_debug = String::from_utf8_lossy(&buf);
|
||||
ShellError::PluginFailedToLoad {
|
||||
msg: format!("get unsupported plugin encoding: {encoding_for_debug}"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the definitions from the plugin file into the engine state
|
||||
pub fn load_plugin_file(
|
||||
working_set: &mut StateWorkingSet,
|
||||
plugin_registry_file: &PluginRegistryFile,
|
||||
span: Option<Span>,
|
||||
) {
|
||||
for plugin in &plugin_registry_file.plugins {
|
||||
// Any errors encountered should just be logged.
|
||||
if let Err(err) = load_plugin_registry_item(working_set, plugin, span) {
|
||||
report_error_new(working_set.permanent_state, &err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a definition from the plugin file into the engine state
|
||||
pub fn load_plugin_registry_item(
|
||||
working_set: &mut StateWorkingSet,
|
||||
plugin: &PluginRegistryItem,
|
||||
span: Option<Span>,
|
||||
) -> Result<Arc<PersistentPlugin>, ShellError> {
|
||||
let identity =
|
||||
PluginIdentity::new(plugin.filename.clone(), plugin.shell.clone()).map_err(|_| {
|
||||
ShellError::GenericError {
|
||||
error: "Invalid plugin filename in plugin registry file".into(),
|
||||
msg: "loaded from here".into(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
"the filename for `{}` is not a valid nushell plugin: {}",
|
||||
plugin.name,
|
||||
plugin.filename.display()
|
||||
)),
|
||||
inner: vec![],
|
||||
}
|
||||
})?;
|
||||
|
||||
match &plugin.data {
|
||||
PluginRegistryItemData::Valid { commands } => {
|
||||
let plugin = add_plugin_to_working_set(working_set, &identity)?;
|
||||
|
||||
// Ensure that the plugin is reset. We're going to load new signatures, so we want to
|
||||
// make sure the running plugin reflects those new signatures, and it's possible that it
|
||||
// doesn't.
|
||||
plugin.reset()?;
|
||||
|
||||
// Create the declarations from the commands
|
||||
for signature in commands {
|
||||
let decl = PluginDeclaration::new(plugin.clone(), signature.clone());
|
||||
working_set.add_decl(Box::new(decl));
|
||||
}
|
||||
Ok(plugin)
|
||||
}
|
||||
PluginRegistryItemData::Invalid => Err(ShellError::PluginRegistryDataInvalid {
|
||||
plugin_name: identity.name().to_owned(),
|
||||
span,
|
||||
add_command: identity.add_command(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find [`PersistentPlugin`] with the given `identity` in the `working_set`, or construct it
|
||||
/// if it doesn't exist.
|
||||
///
|
||||
/// The garbage collection config is always found and set in either case.
|
||||
pub fn add_plugin_to_working_set(
|
||||
working_set: &mut StateWorkingSet,
|
||||
identity: &PluginIdentity,
|
||||
) -> Result<Arc<PersistentPlugin>, ShellError> {
|
||||
// Find garbage collection config for the plugin
|
||||
let gc_config = working_set
|
||||
.get_config()
|
||||
.plugin_gc
|
||||
.get(identity.name())
|
||||
.clone();
|
||||
|
||||
// Add it to / get it from the working set
|
||||
let plugin = working_set.find_or_create_plugin(identity, || {
|
||||
Arc::new(PersistentPlugin::new(identity.clone(), gc_config.clone()))
|
||||
});
|
||||
|
||||
plugin.set_gc_config(&gc_config);
|
||||
|
||||
// Downcast the plugin to `PersistentPlugin` - we generally expect this to succeed.
|
||||
// The trait object only exists so that nu-protocol can contain plugins without knowing
|
||||
// anything about their implementation, but we only use `PersistentPlugin` in practice.
|
||||
plugin
|
||||
.as_any()
|
||||
.downcast()
|
||||
.map_err(|_| ShellError::NushellFailed {
|
||||
msg: "encountered unexpected RegisteredPlugin type".into(),
|
||||
})
|
||||
}
|
@ -1,18 +1,14 @@
|
||||
//! Interface used by the engine to communicate with the plugin.
|
||||
|
||||
use super::{
|
||||
stream::{StreamManager, StreamManagerHandle},
|
||||
Interface, InterfaceManager, PipelineDataWriter, PluginRead, PluginWrite,
|
||||
use nu_plugin_core::{
|
||||
util::{with_custom_values_in, Sequence, Waitable, WaitableMut},
|
||||
Interface, InterfaceManager, PipelineDataWriter, PluginRead, PluginWrite, StreamManager,
|
||||
StreamManagerHandle,
|
||||
};
|
||||
use crate::{
|
||||
plugin::{context::PluginExecutionContext, gc::PluginGc, process::PluginProcess, PluginSource},
|
||||
protocol::{
|
||||
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, Ordering,
|
||||
use nu_plugin_protocol::{
|
||||
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, EvaluatedCall, Ordering,
|
||||
PluginCall, PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption,
|
||||
PluginOutput, ProtocolInfo, StreamId, StreamMessage,
|
||||
},
|
||||
sequence::Sequence,
|
||||
util::{with_custom_values_in, Waitable, WaitableMut},
|
||||
};
|
||||
use nu_protocol::{
|
||||
ast::Operator, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, ListStream,
|
||||
@ -23,6 +19,11 @@ use std::{
|
||||
sync::{atomic::AtomicBool, mpsc, Arc, OnceLock},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
process::PluginProcess, PluginCustomValueWithSource, PluginExecutionContext, PluginGc,
|
||||
PluginSource,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@ -113,8 +114,8 @@ struct PluginCallState {
|
||||
/// them in memory so they can be dropped at the end of the call. We hold the sender as well so
|
||||
/// we can generate the CurrentCallState.
|
||||
keep_plugin_custom_values: (
|
||||
mpsc::Sender<PluginCustomValue>,
|
||||
mpsc::Receiver<PluginCustomValue>,
|
||||
mpsc::Sender<PluginCustomValueWithSource>,
|
||||
mpsc::Receiver<PluginCustomValueWithSource>,
|
||||
),
|
||||
/// Number of streams that still need to be read from the plugin call response
|
||||
remaining_streams_to_read: i32,
|
||||
@ -131,10 +132,7 @@ impl Drop for PluginCallState {
|
||||
}
|
||||
|
||||
/// Manages reading and dispatching messages for [`PluginInterface`]s.
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[derive(Debug)]
|
||||
#[doc(hidden)]
|
||||
pub struct PluginInterfaceManager {
|
||||
/// Shared state
|
||||
state: Arc<PluginInterfaceState>,
|
||||
@ -557,7 +555,10 @@ impl InterfaceManager for PluginInterfaceManager {
|
||||
} => {
|
||||
for arg in positional.iter_mut() {
|
||||
// Add source to any plugin custom values in the arguments
|
||||
PluginCustomValue::add_source_in(arg, &self.state.source)?;
|
||||
PluginCustomValueWithSource::add_source_in(
|
||||
arg,
|
||||
&self.state.source,
|
||||
)?;
|
||||
}
|
||||
Ok(engine_call)
|
||||
}
|
||||
@ -586,7 +587,7 @@ impl InterfaceManager for PluginInterfaceManager {
|
||||
match data {
|
||||
PipelineData::Value(ref mut value, _) => {
|
||||
with_custom_values_in(value, |custom_value| {
|
||||
PluginCustomValue::add_source(custom_value.item, &self.state.source);
|
||||
PluginCustomValueWithSource::add_source(custom_value.item, &self.state.source);
|
||||
Ok::<_, ShellError>(())
|
||||
})?;
|
||||
Ok(data)
|
||||
@ -595,7 +596,7 @@ impl InterfaceManager for PluginInterfaceManager {
|
||||
let source = self.state.source.clone();
|
||||
Ok(stream
|
||||
.map(move |mut value| {
|
||||
let _ = PluginCustomValue::add_source_in(&mut value, &source);
|
||||
let _ = PluginCustomValueWithSource::add_source_in(&mut value, &source);
|
||||
value
|
||||
})
|
||||
.into_pipeline_data_with_metadata(meta, ctrlc))
|
||||
@ -718,12 +719,7 @@ impl PluginInterface {
|
||||
PluginCall::CustomValueOp(value, op) => {
|
||||
(PluginCall::CustomValueOp(value, op), Default::default())
|
||||
}
|
||||
PluginCall::Run(CallInfo {
|
||||
name,
|
||||
mut call,
|
||||
input,
|
||||
}) => {
|
||||
state.prepare_call_args(&mut call, &self.state.source)?;
|
||||
PluginCall::Run(CallInfo { name, call, input }) => {
|
||||
let (header, writer) = self.init_write_pipeline_data(input, &state)?;
|
||||
(
|
||||
PluginCall::Run(CallInfo {
|
||||
@ -945,12 +941,16 @@ impl PluginInterface {
|
||||
/// Do a custom value op that expects a value response (i.e. most of them)
|
||||
fn custom_value_op_expecting_value(
|
||||
&self,
|
||||
value: Spanned<PluginCustomValue>,
|
||||
value: Spanned<PluginCustomValueWithSource>,
|
||||
op: CustomValueOp,
|
||||
) -> Result<Value, ShellError> {
|
||||
let op_name = op.name();
|
||||
let span = value.span;
|
||||
let call = PluginCall::CustomValueOp(value, op);
|
||||
|
||||
// Check that the value came from the right source
|
||||
value.item.verify_source(span, &self.state.source)?;
|
||||
|
||||
let call = PluginCall::CustomValueOp(value.map(|cv| cv.without_source()), op);
|
||||
match self.plugin_call(call, None)? {
|
||||
PluginCallResponse::PipelineData(out_data) => Ok(out_data.into_value(span)),
|
||||
PluginCallResponse::Error(err) => Err(err.into()),
|
||||
@ -963,7 +963,7 @@ impl PluginInterface {
|
||||
/// Collapse a custom value to its base value.
|
||||
pub fn custom_value_to_base_value(
|
||||
&self,
|
||||
value: Spanned<PluginCustomValue>,
|
||||
value: Spanned<PluginCustomValueWithSource>,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.custom_value_op_expecting_value(value, CustomValueOp::ToBaseValue)
|
||||
}
|
||||
@ -971,7 +971,7 @@ impl PluginInterface {
|
||||
/// Follow a numbered cell path on a custom value - e.g. `value.0`.
|
||||
pub fn custom_value_follow_path_int(
|
||||
&self,
|
||||
value: Spanned<PluginCustomValue>,
|
||||
value: Spanned<PluginCustomValueWithSource>,
|
||||
index: Spanned<usize>,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.custom_value_op_expecting_value(value, CustomValueOp::FollowPathInt(index))
|
||||
@ -980,7 +980,7 @@ impl PluginInterface {
|
||||
/// Follow a named cell path on a custom value - e.g. `value.column`.
|
||||
pub fn custom_value_follow_path_string(
|
||||
&self,
|
||||
value: Spanned<PluginCustomValue>,
|
||||
value: Spanned<PluginCustomValueWithSource>,
|
||||
column_name: Spanned<String>,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.custom_value_op_expecting_value(value, CustomValueOp::FollowPathString(column_name))
|
||||
@ -989,13 +989,16 @@ impl PluginInterface {
|
||||
/// Invoke comparison logic for custom values.
|
||||
pub fn custom_value_partial_cmp(
|
||||
&self,
|
||||
value: PluginCustomValue,
|
||||
value: PluginCustomValueWithSource,
|
||||
other_value: Value,
|
||||
) -> Result<Option<Ordering>, ShellError> {
|
||||
// Check that the value came from the right source
|
||||
value.verify_source(Span::unknown(), &self.state.source)?;
|
||||
|
||||
// Note: the protocol is always designed to have a span with the custom value, but this
|
||||
// operation doesn't support one.
|
||||
let call = PluginCall::CustomValueOp(
|
||||
value.into_spanned(Span::unknown()),
|
||||
value.without_source().into_spanned(Span::unknown()),
|
||||
CustomValueOp::PartialCmp(other_value),
|
||||
);
|
||||
match self.plugin_call(call, None)? {
|
||||
@ -1010,7 +1013,7 @@ impl PluginInterface {
|
||||
/// Invoke functionality for an operator on a custom value.
|
||||
pub fn custom_value_operation(
|
||||
&self,
|
||||
left: Spanned<PluginCustomValue>,
|
||||
left: Spanned<PluginCustomValueWithSource>,
|
||||
operator: Spanned<Operator>,
|
||||
right: Value,
|
||||
) -> Result<Value, ShellError> {
|
||||
@ -1118,9 +1121,6 @@ struct WritePluginCallResult {
|
||||
}
|
||||
|
||||
/// State related to the current plugin call being executed.
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[doc(hidden)]
|
||||
#[derive(Default, Clone)]
|
||||
pub struct CurrentCallState {
|
||||
/// Sender for context, which should be sent if the plugin call returned a stream so that
|
||||
@ -1128,7 +1128,7 @@ pub struct CurrentCallState {
|
||||
context_tx: Option<mpsc::Sender<Context>>,
|
||||
/// Sender for a channel that retains plugin custom values that need to stay alive for the
|
||||
/// duration of a plugin call.
|
||||
keep_plugin_custom_values_tx: Option<mpsc::Sender<PluginCustomValue>>,
|
||||
keep_plugin_custom_values_tx: Option<mpsc::Sender<PluginCustomValueWithSource>>,
|
||||
/// The plugin call entered the foreground: this should be cleaned up automatically when the
|
||||
/// plugin call returns.
|
||||
entered_foreground: bool,
|
||||
@ -1141,18 +1141,21 @@ impl CurrentCallState {
|
||||
/// shouldn't be dropped immediately.
|
||||
fn prepare_custom_value(
|
||||
&self,
|
||||
custom_value: Spanned<&mut (dyn CustomValue + '_)>,
|
||||
custom_value: Spanned<&mut Box<dyn CustomValue>>,
|
||||
source: &PluginSource,
|
||||
) -> Result<(), ShellError> {
|
||||
// Ensure we can use it
|
||||
PluginCustomValue::verify_source(custom_value.as_deref(), source)?;
|
||||
PluginCustomValueWithSource::verify_source_of_custom_value(
|
||||
custom_value.as_deref().map(|cv| &**cv),
|
||||
source,
|
||||
)?;
|
||||
|
||||
// Check whether we need to keep it
|
||||
if let Some(keep_tx) = &self.keep_plugin_custom_values_tx {
|
||||
if let Some(custom_value) = custom_value
|
||||
.item
|
||||
.as_any()
|
||||
.downcast_ref::<PluginCustomValue>()
|
||||
.downcast_ref::<PluginCustomValueWithSource>()
|
||||
{
|
||||
if custom_value.notify_on_drop() {
|
||||
log::trace!("Keeping custom value for drop later: {:?}", custom_value);
|
||||
@ -1164,6 +1167,10 @@ impl CurrentCallState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip the source from it so it can be serialized
|
||||
PluginCustomValueWithSource::remove_source(&mut *custom_value.item);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1177,7 +1184,7 @@ impl CurrentCallState {
|
||||
/// Prepare call arguments for write.
|
||||
fn prepare_call_args(
|
||||
&self,
|
||||
call: &mut crate::EvaluatedCall,
|
||||
call: &mut EvaluatedCall,
|
||||
source: &PluginSource,
|
||||
) -> Result<(), ShellError> {
|
||||
for arg in call.positional.iter_mut() {
|
||||
@ -1199,11 +1206,7 @@ impl CurrentCallState {
|
||||
match call {
|
||||
PluginCall::Signature => Ok(()),
|
||||
PluginCall::Run(CallInfo { call, .. }) => self.prepare_call_args(call, source),
|
||||
PluginCall::CustomValueOp(custom_value, op) => {
|
||||
// `source` isn't present on Dropped.
|
||||
if !matches!(op, CustomValueOp::Dropped) {
|
||||
self.prepare_custom_value(custom_value.as_mut().map(|r| r as &mut _), source)?;
|
||||
}
|
||||
PluginCall::CustomValueOp(_, op) => {
|
||||
// Handle anything within the op.
|
||||
match op {
|
||||
CustomValueOp::ToBaseValue => Ok(()),
|
@ -2,21 +2,17 @@ use super::{
|
||||
Context, PluginCallState, PluginInterface, PluginInterfaceManager, ReceivedPluginCallMessage,
|
||||
};
|
||||
use crate::{
|
||||
plugin::{
|
||||
context::PluginExecutionBogusContext,
|
||||
interface::{plugin::CurrentCallState, test_util::TestCase, Interface, InterfaceManager},
|
||||
context::PluginExecutionBogusContext, interface::CurrentCallState,
|
||||
plugin_custom_value_with_source::WithSource, test_util::*, PluginCustomValueWithSource,
|
||||
PluginSource,
|
||||
},
|
||||
protocol::{
|
||||
test_util::{
|
||||
expected_test_custom_value, test_plugin_custom_value,
|
||||
test_plugin_custom_value_with_source,
|
||||
},
|
||||
CallInfo, CustomValueOp, EngineCall, EngineCallResponse, ExternalStreamInfo,
|
||||
ListStreamInfo, PipelineDataHeader, PluginCall, PluginCallId, PluginCustomValue,
|
||||
PluginInput, Protocol, ProtocolInfo, RawStreamInfo, StreamData, StreamMessage,
|
||||
},
|
||||
EvaluatedCall, PluginCallResponse, PluginOutput,
|
||||
};
|
||||
use nu_plugin_core::{interface_test_util::TestCase, Interface, InterfaceManager};
|
||||
use nu_plugin_protocol::{
|
||||
test_util::{expected_test_custom_value, test_plugin_custom_value},
|
||||
CallInfo, CustomValueOp, EngineCall, EngineCallResponse, EvaluatedCall, ExternalStreamInfo,
|
||||
ListStreamInfo, PipelineDataHeader, PluginCall, PluginCallId, PluginCallResponse,
|
||||
PluginCustomValue, PluginInput, PluginOutput, Protocol, ProtocolInfo, RawStreamInfo,
|
||||
StreamData, StreamMessage,
|
||||
};
|
||||
use nu_protocol::{
|
||||
ast::{Math, Operator},
|
||||
@ -666,17 +662,13 @@ fn manager_prepare_pipeline_data_adds_source_to_values() -> Result<(), ShellErro
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("prepared pipeline data is empty");
|
||||
let custom_value: &PluginCustomValue = value
|
||||
let custom_value: &PluginCustomValueWithSource = value
|
||||
.as_custom_value()?
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.expect("custom value is not a PluginCustomValue");
|
||||
.expect("{value:?} is not a PluginCustomValueWithSource");
|
||||
|
||||
if let Some(source) = custom_value.source() {
|
||||
assert_eq!("test", source.name());
|
||||
} else {
|
||||
panic!("source was not set");
|
||||
}
|
||||
assert_eq!("test", custom_value.source().name());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -696,17 +688,13 @@ fn manager_prepare_pipeline_data_adds_source_to_list_streams() -> Result<(), She
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("prepared pipeline data is empty");
|
||||
let custom_value: &PluginCustomValue = value
|
||||
let custom_value: &PluginCustomValueWithSource = value
|
||||
.as_custom_value()?
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.expect("custom value is not a PluginCustomValue");
|
||||
.expect("{value:?} is not a PluginCustomValueWithSource");
|
||||
|
||||
if let Some(source) = custom_value.source() {
|
||||
assert_eq!("test", source.name());
|
||||
} else {
|
||||
panic!("source was not set");
|
||||
}
|
||||
assert_eq!("test", custom_value.source().name());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -792,7 +780,7 @@ fn interface_write_plugin_call_writes_custom_value_op() -> Result<(), ShellError
|
||||
let result = interface.write_plugin_call(
|
||||
PluginCall::CustomValueOp(
|
||||
Spanned {
|
||||
item: test_plugin_custom_value_with_source(),
|
||||
item: test_plugin_custom_value(),
|
||||
span: Span::test_data(),
|
||||
},
|
||||
CustomValueOp::ToBaseValue,
|
||||
@ -1113,13 +1101,12 @@ fn interface_custom_value_to_base_value() -> Result<(), ShellError> {
|
||||
fn normal_values(interface: &PluginInterface) -> Vec<Value> {
|
||||
vec![
|
||||
Value::test_int(5),
|
||||
Value::test_custom_value(Box::new(PluginCustomValue::new(
|
||||
"SomeTest".into(),
|
||||
vec![1, 2, 3],
|
||||
false,
|
||||
Value::test_custom_value(Box::new(
|
||||
PluginCustomValue::new("SomeTest".into(), vec![1, 2, 3], false).with_source(
|
||||
// Has the same source, so it should be accepted
|
||||
Some(interface.state.source.clone()),
|
||||
))),
|
||||
interface.state.source.clone(),
|
||||
),
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
@ -1173,15 +1160,12 @@ fn bad_custom_values() -> Vec<Value> {
|
||||
"SomeTest".into(),
|
||||
vec![1, 2, 3],
|
||||
false,
|
||||
None,
|
||||
))),
|
||||
// Has a different source, so it should be rejected
|
||||
Value::test_custom_value(Box::new(PluginCustomValue::new(
|
||||
"SomeTest".into(),
|
||||
vec![1, 2, 3],
|
||||
false,
|
||||
Some(PluginSource::new_fake("pluto").into()),
|
||||
))),
|
||||
Value::test_custom_value(Box::new(
|
||||
PluginCustomValue::new("SomeTest".into(), vec![1, 2, 3], false)
|
||||
.with_source(PluginSource::new_fake("pluto").into()),
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
@ -1227,7 +1211,7 @@ fn prepare_custom_value_verifies_source() {
|
||||
let span = Span::test_data();
|
||||
let source = Arc::new(PluginSource::new_fake("test"));
|
||||
|
||||
let mut val = test_plugin_custom_value();
|
||||
let mut val: Box<dyn CustomValue> = Box::new(test_plugin_custom_value());
|
||||
assert!(CurrentCallState::default()
|
||||
.prepare_custom_value(
|
||||
Spanned {
|
||||
@ -1238,7 +1222,8 @@ fn prepare_custom_value_verifies_source() {
|
||||
)
|
||||
.is_err());
|
||||
|
||||
let mut val = test_plugin_custom_value().with_source(Some(source.clone()));
|
||||
let mut val: Box<dyn CustomValue> =
|
||||
Box::new(test_plugin_custom_value().with_source(source.clone()));
|
||||
assert!(CurrentCallState::default()
|
||||
.prepare_custom_value(
|
||||
Spanned {
|
||||
@ -1289,8 +1274,10 @@ fn prepare_custom_value_sends_to_keep_channel_if_drop_notify() -> Result<(), She
|
||||
..Default::default()
|
||||
};
|
||||
// Try with a custom val that has drop check set
|
||||
let mut drop_val = PluginCustomValue::serialize_from_custom_value(&DropCustomVal, span)?
|
||||
.with_source(Some(source.clone()));
|
||||
let mut drop_val: Box<dyn CustomValue> = Box::new(
|
||||
PluginCustomValue::serialize_from_custom_value(&DropCustomVal, span)?
|
||||
.with_source(source.clone()),
|
||||
);
|
||||
state.prepare_custom_value(
|
||||
Spanned {
|
||||
item: &mut drop_val,
|
||||
@ -1301,7 +1288,8 @@ fn prepare_custom_value_sends_to_keep_channel_if_drop_notify() -> Result<(), She
|
||||
// Check that the custom value was actually sent
|
||||
assert!(rx.try_recv().is_ok());
|
||||
// Now try with one that doesn't have it
|
||||
let mut not_drop_val = test_plugin_custom_value().with_source(Some(source.clone()));
|
||||
let mut not_drop_val: Box<dyn CustomValue> =
|
||||
Box::new(test_plugin_custom_value().with_source(source.clone()));
|
||||
state.prepare_custom_value(
|
||||
Spanned {
|
||||
item: &mut not_drop_val,
|
||||
@ -1321,10 +1309,10 @@ fn prepare_plugin_call_run() {
|
||||
let source = Arc::new(PluginSource::new_fake("test"));
|
||||
let other_source = Arc::new(PluginSource::new_fake("other"));
|
||||
let cv_ok = test_plugin_custom_value()
|
||||
.with_source(Some(source.clone()))
|
||||
.with_source(source.clone())
|
||||
.into_value(span);
|
||||
let cv_bad = test_plugin_custom_value()
|
||||
.with_source(Some(other_source))
|
||||
.with_source(other_source)
|
||||
.into_value(span);
|
||||
|
||||
let fixtures = [
|
||||
@ -1414,9 +1402,9 @@ fn prepare_plugin_call_custom_value_op() {
|
||||
let span = Span::test_data();
|
||||
let source = Arc::new(PluginSource::new_fake("test"));
|
||||
let other_source = Arc::new(PluginSource::new_fake("other"));
|
||||
let cv_ok = test_plugin_custom_value().with_source(Some(source.clone()));
|
||||
let cv_ok = test_plugin_custom_value().with_source(source.clone());
|
||||
let cv_ok_val = cv_ok.clone_value(span);
|
||||
let cv_bad = test_plugin_custom_value().with_source(Some(other_source));
|
||||
let cv_bad = test_plugin_custom_value().with_source(other_source);
|
||||
let cv_bad_val = cv_bad.clone_value(span);
|
||||
|
||||
let fixtures = [
|
||||
@ -1424,17 +1412,7 @@ fn prepare_plugin_call_custom_value_op() {
|
||||
true, // should succeed
|
||||
PluginCall::CustomValueOp::<PipelineData>(
|
||||
Spanned {
|
||||
item: cv_ok.clone(),
|
||||
span,
|
||||
},
|
||||
CustomValueOp::ToBaseValue,
|
||||
),
|
||||
),
|
||||
(
|
||||
false, // should fail
|
||||
PluginCall::CustomValueOp(
|
||||
Spanned {
|
||||
item: cv_bad.clone(),
|
||||
item: cv_ok.clone().without_source(),
|
||||
span,
|
||||
},
|
||||
CustomValueOp::ToBaseValue,
|
||||
@ -1455,7 +1433,7 @@ fn prepare_plugin_call_custom_value_op() {
|
||||
true, // should succeed
|
||||
PluginCall::CustomValueOp::<PipelineData>(
|
||||
Spanned {
|
||||
item: cv_ok.clone(),
|
||||
item: cv_ok.clone().without_source(),
|
||||
span,
|
||||
},
|
||||
CustomValueOp::PartialCmp(cv_ok_val.clone()),
|
||||
@ -1465,7 +1443,7 @@ fn prepare_plugin_call_custom_value_op() {
|
||||
false, // should fail
|
||||
PluginCall::CustomValueOp(
|
||||
Spanned {
|
||||
item: cv_ok.clone(),
|
||||
item: cv_ok.clone().without_source(),
|
||||
span,
|
||||
},
|
||||
CustomValueOp::PartialCmp(cv_bad_val.clone()),
|
||||
@ -1475,7 +1453,7 @@ fn prepare_plugin_call_custom_value_op() {
|
||||
true, // should succeed
|
||||
PluginCall::CustomValueOp::<PipelineData>(
|
||||
Spanned {
|
||||
item: cv_ok.clone(),
|
||||
item: cv_ok.clone().without_source(),
|
||||
span,
|
||||
},
|
||||
CustomValueOp::Operation(
|
||||
@ -1488,7 +1466,7 @@ fn prepare_plugin_call_custom_value_op() {
|
||||
false, // should fail
|
||||
PluginCall::CustomValueOp(
|
||||
Spanned {
|
||||
item: cv_ok.clone(),
|
||||
item: cv_ok.clone().without_source(),
|
||||
span,
|
||||
},
|
||||
CustomValueOp::Operation(
|
24
crates/nu-plugin-engine/src/lib.rs
Normal file
24
crates/nu-plugin-engine/src/lib.rs
Normal file
@ -0,0 +1,24 @@
|
||||
//! Provides functionality for running Nushell plugins from a Nushell engine.
|
||||
|
||||
mod context;
|
||||
mod declaration;
|
||||
mod gc;
|
||||
mod init;
|
||||
mod interface;
|
||||
mod persistent;
|
||||
mod plugin_custom_value_with_source;
|
||||
mod process;
|
||||
mod source;
|
||||
mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_util;
|
||||
|
||||
pub use context::{PluginExecutionCommandContext, PluginExecutionContext};
|
||||
pub use declaration::PluginDeclaration;
|
||||
pub use gc::PluginGc;
|
||||
pub use init::*;
|
||||
pub use interface::{PluginInterface, PluginInterfaceManager};
|
||||
pub use persistent::{GetPlugin, PersistentPlugin};
|
||||
pub use plugin_custom_value_with_source::{PluginCustomValueWithSource, WithSource};
|
||||
pub use source::PluginSource;
|
@ -1,7 +1,10 @@
|
||||
use super::{
|
||||
communication_mode::CommunicationMode, create_command, gc::PluginGc, make_plugin_interface,
|
||||
PluginInterface, PluginSource,
|
||||
use crate::{
|
||||
init::{create_command, make_plugin_interface},
|
||||
PluginGc,
|
||||
};
|
||||
|
||||
use super::{PluginInterface, PluginSource};
|
||||
use nu_plugin_core::CommunicationMode;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
|
||||
@ -14,9 +17,6 @@ use std::{
|
||||
/// A box that can keep a plugin that was spawned persistent for further uses. The plugin may or
|
||||
/// may not be currently running. [`.get()`] gets the currently running plugin, or spawns it if it's
|
||||
/// not running.
|
||||
///
|
||||
/// Note: used in the parser, not for plugin authors
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug)]
|
||||
pub struct PersistentPlugin {
|
||||
/// Identity (filename, shell, name) of the plugin
|
||||
@ -69,7 +69,7 @@ impl PersistentPlugin {
|
||||
///
|
||||
/// Will call `envs` to get environment variables to spawn the plugin if the plugin needs to be
|
||||
/// spawned.
|
||||
pub(crate) fn get(
|
||||
pub fn get(
|
||||
self: Arc<Self>,
|
||||
envs: impl FnOnce() -> Result<HashMap<String, String>, ShellError>,
|
||||
) -> Result<PluginInterface, ShellError> {
|
||||
@ -194,7 +194,7 @@ impl PersistentPlugin {
|
||||
if mutable.preferred_mode.is_none()
|
||||
&& interface
|
||||
.protocol_info()?
|
||||
.supports_feature(&crate::protocol::Feature::LocalSocket)
|
||||
.supports_feature(&nu_plugin_protocol::Feature::LocalSocket)
|
||||
{
|
||||
log::trace!(
|
||||
"{}: Attempting to upgrade to local socket mode",
|
||||
@ -289,9 +289,6 @@ impl RegisteredPlugin for PersistentPlugin {
|
||||
}
|
||||
|
||||
/// 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.
|
@ -0,0 +1,274 @@
|
||||
use std::{cmp::Ordering, sync::Arc};
|
||||
|
||||
use nu_plugin_core::util::with_custom_values_in;
|
||||
use nu_plugin_protocol::PluginCustomValue;
|
||||
use nu_protocol::{ast::Operator, CustomValue, IntoSpanned, ShellError, Span, Spanned, Value};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{PluginInterface, PluginSource};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Wraps a [`PluginCustomValue`] together with its [`PluginSource`], so that the [`CustomValue`]
|
||||
/// methods can be implemented by calling the plugin, and to ensure that any custom values sent to a
|
||||
/// plugin came from it originally.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginCustomValueWithSource {
|
||||
inner: PluginCustomValue,
|
||||
|
||||
/// Which plugin the custom value came from. This is not sent over the serialization boundary.
|
||||
source: Arc<PluginSource>,
|
||||
}
|
||||
|
||||
impl PluginCustomValueWithSource {
|
||||
/// Wrap a [`PluginCustomValue`] together with its source.
|
||||
pub fn new(inner: PluginCustomValue, source: Arc<PluginSource>) -> PluginCustomValueWithSource {
|
||||
PluginCustomValueWithSource { inner, source }
|
||||
}
|
||||
|
||||
/// Create a [`Value`] containing this custom value.
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
Value::custom(Box::new(self), span)
|
||||
}
|
||||
|
||||
/// Which plugin the custom value came from. This provides a direct reference to be able to get
|
||||
/// a plugin interface in order to make a call, when needed.
|
||||
pub fn source(&self) -> &Arc<PluginSource> {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// Unwrap the [`PluginCustomValueWithSource`], discarding the source.
|
||||
pub fn without_source(self) -> PluginCustomValue {
|
||||
// Because of the `Drop` implementation, we can't destructure this.
|
||||
self.inner.clone()
|
||||
}
|
||||
|
||||
/// Helper to get the plugin to implement an op
|
||||
fn get_plugin(&self, span: Option<Span>, for_op: &str) -> Result<PluginInterface, ShellError> {
|
||||
let wrap_err = |err: ShellError| ShellError::GenericError {
|
||||
error: format!(
|
||||
"Unable to spawn plugin `{}` to {for_op}",
|
||||
self.source.name()
|
||||
),
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
help: None,
|
||||
inner: vec![err],
|
||||
};
|
||||
|
||||
self.source
|
||||
.clone()
|
||||
.persistent(span)
|
||||
.and_then(|p| p.get_plugin(None))
|
||||
.map_err(wrap_err)
|
||||
}
|
||||
|
||||
/// Add a [`PluginSource`] to the given [`CustomValue`] if it is a [`PluginCustomValue`].
|
||||
pub fn add_source(value: &mut Box<dyn CustomValue>, source: &Arc<PluginSource>) {
|
||||
if let Some(custom_value) = value.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
*value = Box::new(custom_value.clone().with_source(source.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a [`PluginSource`] to all [`PluginCustomValue`]s within the value, recursively.
|
||||
pub fn add_source_in(value: &mut Value, source: &Arc<PluginSource>) -> Result<(), ShellError> {
|
||||
with_custom_values_in(value, |custom_value| {
|
||||
Self::add_source(custom_value.item, source);
|
||||
Ok::<_, ShellError>(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove a [`PluginSource`] from the given [`CustomValue`] if it is a
|
||||
/// [`PluginCustomValueWithSource`]. This will turn it back into a [`PluginCustomValue`].
|
||||
pub fn remove_source(value: &mut Box<dyn CustomValue>) {
|
||||
if let Some(custom_value) = value.as_any().downcast_ref::<PluginCustomValueWithSource>() {
|
||||
*value = Box::new(custom_value.clone().without_source());
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the [`PluginSource`] from all [`PluginCustomValue`]s within the value, recursively.
|
||||
pub fn remove_source_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
with_custom_values_in(value, |custom_value| {
|
||||
Self::remove_source(custom_value.item);
|
||||
Ok::<_, ShellError>(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Check that `self` came from the given `source`, and return an `error` if not.
|
||||
pub fn verify_source(&self, span: Span, source: &PluginSource) -> Result<(), ShellError> {
|
||||
if self.source.is_compatible(source) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ShellError::CustomValueIncorrectForPlugin {
|
||||
name: self.name().to_owned(),
|
||||
span,
|
||||
dest_plugin: source.name().to_owned(),
|
||||
src_plugin: Some(self.source.name().to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Check that a [`CustomValue`] is a [`PluginCustomValueWithSource`] that came from the given
|
||||
/// `source`, and return an error if not.
|
||||
pub fn verify_source_of_custom_value(
|
||||
value: Spanned<&dyn CustomValue>,
|
||||
source: &PluginSource,
|
||||
) -> Result<(), ShellError> {
|
||||
if let Some(custom_value) = value
|
||||
.item
|
||||
.as_any()
|
||||
.downcast_ref::<PluginCustomValueWithSource>()
|
||||
{
|
||||
custom_value.verify_source(value.span, source)
|
||||
} else {
|
||||
// Only PluginCustomValueWithSource can be sent
|
||||
Err(ShellError::CustomValueIncorrectForPlugin {
|
||||
name: value.item.type_name(),
|
||||
span: value.span,
|
||||
dest_plugin: source.name().to_owned(),
|
||||
src_plugin: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for PluginCustomValueWithSource {
|
||||
type Target = PluginCustomValue;
|
||||
|
||||
fn deref(&self) -> &PluginCustomValue {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
/// This `Serialize` implementation always produces an error. Strip the source before sending.
|
||||
impl Serialize for PluginCustomValueWithSource {
|
||||
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
use serde::ser::Error;
|
||||
Err(Error::custom(
|
||||
"can't serialize PluginCustomValueWithSource, remove the source first",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl CustomValue for PluginCustomValueWithSource {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
self.clone().into_value(span)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> String {
|
||||
self.name().to_owned()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
||||
self.get_plugin(Some(span), "get base value")?
|
||||
.custom_value_to_base_value(self.clone().into_spanned(span))
|
||||
}
|
||||
|
||||
fn follow_path_int(
|
||||
&self,
|
||||
self_span: Span,
|
||||
index: usize,
|
||||
path_span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.get_plugin(Some(self_span), "follow cell path")?
|
||||
.custom_value_follow_path_int(
|
||||
self.clone().into_spanned(self_span),
|
||||
index.into_spanned(path_span),
|
||||
)
|
||||
}
|
||||
|
||||
fn follow_path_string(
|
||||
&self,
|
||||
self_span: Span,
|
||||
column_name: String,
|
||||
path_span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.get_plugin(Some(self_span), "follow cell path")?
|
||||
.custom_value_follow_path_string(
|
||||
self.clone().into_spanned(self_span),
|
||||
column_name.into_spanned(path_span),
|
||||
)
|
||||
}
|
||||
|
||||
fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
|
||||
self.get_plugin(Some(other.span()), "perform comparison")
|
||||
.and_then(|plugin| {
|
||||
// We're passing Span::unknown() here because we don't have one, and it probably
|
||||
// shouldn't matter here and is just a consequence of the API
|
||||
plugin.custom_value_partial_cmp(self.clone(), other.clone())
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
// We can't do anything with the error other than log it.
|
||||
log::warn!(
|
||||
"Error in partial_cmp on plugin custom value (source={source:?}): {err}",
|
||||
source = self.source
|
||||
);
|
||||
None
|
||||
})
|
||||
.map(|ordering| ordering.into())
|
||||
}
|
||||
|
||||
fn operation(
|
||||
&self,
|
||||
lhs_span: Span,
|
||||
operator: Operator,
|
||||
op_span: Span,
|
||||
right: &Value,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.get_plugin(Some(lhs_span), "invoke operator")?
|
||||
.custom_value_operation(
|
||||
self.clone().into_spanned(lhs_span),
|
||||
operator.into_spanned(op_span),
|
||||
right.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn typetag_name(&self) -> &'static str {
|
||||
"PluginCustomValueWithSource"
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
fn typetag_deserialize(&self) {}
|
||||
}
|
||||
|
||||
impl Drop for PluginCustomValueWithSource {
|
||||
fn drop(&mut self) {
|
||||
// If the custom value specifies notify_on_drop and this is the last copy, we need to let
|
||||
// the plugin know about it if we can.
|
||||
if self.notify_on_drop() && self.inner.ref_count() == 1 {
|
||||
self.get_plugin(None, "drop")
|
||||
// While notifying drop, we don't need a copy of the source
|
||||
.and_then(|plugin| plugin.custom_value_dropped(self.inner.clone()))
|
||||
.unwrap_or_else(|err| {
|
||||
// We shouldn't do anything with the error except log it
|
||||
let name = self.name();
|
||||
log::warn!("Failed to notify drop of custom value ({name}): {err}")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper trait for adding a source to a [`PluginCustomValue`]
|
||||
pub trait WithSource {
|
||||
/// Add a source to a plugin custom value
|
||||
fn with_source(self, source: Arc<PluginSource>) -> PluginCustomValueWithSource;
|
||||
}
|
||||
|
||||
impl WithSource for PluginCustomValue {
|
||||
fn with_source(self, source: Arc<PluginSource>) -> PluginCustomValueWithSource {
|
||||
PluginCustomValueWithSource::new(self, source)
|
||||
}
|
||||
}
|
@ -0,0 +1,198 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nu_plugin_protocol::test_util::{test_plugin_custom_value, TestCustomValue};
|
||||
use nu_protocol::{engine::Closure, record, CustomValue, IntoSpanned, ShellError, Span, Value};
|
||||
|
||||
use crate::{
|
||||
test_util::test_plugin_custom_value_with_source, PluginCustomValueWithSource, PluginSource,
|
||||
};
|
||||
|
||||
use super::WithSource;
|
||||
|
||||
#[test]
|
||||
fn add_source_in_at_root() -> Result<(), ShellError> {
|
||||
let mut val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let source = Arc::new(PluginSource::new_fake("foo"));
|
||||
PluginCustomValueWithSource::add_source_in(&mut val, &source)?;
|
||||
|
||||
let custom_value = val.as_custom_value()?;
|
||||
let plugin_custom_value: &PluginCustomValueWithSource = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.expect("not PluginCustomValueWithSource");
|
||||
assert_eq!(
|
||||
Arc::as_ptr(&source),
|
||||
Arc::as_ptr(&plugin_custom_value.source)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_record_custom_values(
|
||||
val: &Value,
|
||||
keys: &[&str],
|
||||
mut f: impl FnMut(&str, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let record = val.as_record()?;
|
||||
for key in keys {
|
||||
let val = record
|
||||
.get(key)
|
||||
.unwrap_or_else(|| panic!("record does not contain '{key}'"));
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("'{key}' not custom value"));
|
||||
f(key, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_in_nested_record() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_record(record! {
|
||||
"foo" => orig_custom_val.clone(),
|
||||
"bar" => orig_custom_val.clone(),
|
||||
});
|
||||
let source = Arc::new(PluginSource::new_fake("foo"));
|
||||
PluginCustomValueWithSource::add_source_in(&mut val, &source)?;
|
||||
|
||||
check_record_custom_values(&val, &["foo", "bar"], |key, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValueWithSource = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("'{key}' not PluginCustomValueWithSource"));
|
||||
assert_eq!(
|
||||
Arc::as_ptr(&source),
|
||||
Arc::as_ptr(&plugin_custom_value.source),
|
||||
"'{key}' source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn check_list_custom_values(
|
||||
val: &Value,
|
||||
indices: impl IntoIterator<Item = usize>,
|
||||
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let list = val.as_list()?;
|
||||
for index in indices {
|
||||
let val = list
|
||||
.get(index)
|
||||
.unwrap_or_else(|| panic!("[{index}] not present in list"));
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
|
||||
f(index, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_in_nested_list() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_list(vec![orig_custom_val.clone(), orig_custom_val.clone()]);
|
||||
let source = Arc::new(PluginSource::new_fake("foo"));
|
||||
PluginCustomValueWithSource::add_source_in(&mut val, &source)?;
|
||||
|
||||
check_list_custom_values(&val, 0..=1, |index, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValueWithSource = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValueWithSource"));
|
||||
assert_eq!(
|
||||
Arc::as_ptr(&source),
|
||||
Arc::as_ptr(&plugin_custom_value.source),
|
||||
"[{index}] source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn check_closure_custom_values(
|
||||
val: &Value,
|
||||
indices: impl IntoIterator<Item = usize>,
|
||||
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let closure = val.as_closure()?;
|
||||
for index in indices {
|
||||
let val = closure
|
||||
.captures
|
||||
.get(index)
|
||||
.unwrap_or_else(|| panic!("[{index}] not present in closure"));
|
||||
let custom_value = val
|
||||
.1
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
|
||||
f(index, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_in_nested_closure() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_closure(Closure {
|
||||
block_id: 0,
|
||||
captures: vec![(0, orig_custom_val.clone()), (1, orig_custom_val.clone())],
|
||||
});
|
||||
let source = Arc::new(PluginSource::new_fake("foo"));
|
||||
PluginCustomValueWithSource::add_source_in(&mut val, &source)?;
|
||||
|
||||
check_closure_custom_values(&val, 0..=1, |index, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValueWithSource = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValueWithSource"));
|
||||
assert_eq!(
|
||||
Arc::as_ptr(&source),
|
||||
Arc::as_ptr(&plugin_custom_value.source),
|
||||
"[{index}] source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_source_error_message() -> Result<(), ShellError> {
|
||||
let span = Span::new(5, 7);
|
||||
let ok_val = test_plugin_custom_value_with_source();
|
||||
let native_val = TestCustomValue(32);
|
||||
let foreign_val =
|
||||
test_plugin_custom_value().with_source(Arc::new(PluginSource::new_fake("other")));
|
||||
let source = PluginSource::new_fake("test");
|
||||
|
||||
PluginCustomValueWithSource::verify_source_of_custom_value(
|
||||
(&ok_val as &dyn CustomValue).into_spanned(span),
|
||||
&source,
|
||||
)
|
||||
.expect("ok_val should be verified ok");
|
||||
|
||||
for (val, src_plugin) in [
|
||||
(&native_val as &dyn CustomValue, None),
|
||||
(&foreign_val as &dyn CustomValue, Some("other")),
|
||||
] {
|
||||
let error = PluginCustomValueWithSource::verify_source_of_custom_value(
|
||||
val.into_spanned(span),
|
||||
&source,
|
||||
)
|
||||
.expect_err(&format!(
|
||||
"a custom value from {src_plugin:?} should result in an error"
|
||||
));
|
||||
if let ShellError::CustomValueIncorrectForPlugin {
|
||||
name,
|
||||
span: err_span,
|
||||
dest_plugin,
|
||||
src_plugin: err_src_plugin,
|
||||
} = error
|
||||
{
|
||||
assert_eq!("TestCustomValue", name, "error.name from {src_plugin:?}");
|
||||
assert_eq!(span, err_span, "error.span from {src_plugin:?}");
|
||||
assert_eq!("test", dest_plugin, "error.dest_plugin from {src_plugin:?}");
|
||||
assert_eq!(src_plugin, err_src_plugin.as_deref(), "error.src_plugin");
|
||||
} else {
|
||||
panic!("the error returned should be CustomValueIncorrectForPlugin");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@ -4,10 +4,7 @@ use std::sync::{Arc, Weak};
|
||||
|
||||
/// The source of a custom value or plugin command. Includes a weak reference to the persistent
|
||||
/// plugin so it can be retrieved.
|
||||
///
|
||||
/// This is not a public interface.
|
||||
#[derive(Debug, Clone)]
|
||||
#[doc(hidden)]
|
||||
pub struct PluginSource {
|
||||
/// The identity of the plugin
|
||||
pub(crate) identity: Arc<PluginIdentity>,
|
||||
@ -30,8 +27,7 @@ impl PluginSource {
|
||||
/// Create a new fake source with a fake identity, for testing
|
||||
///
|
||||
/// Warning: [`.persistent()`] will always return an error.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_fake(name: &str) -> PluginSource {
|
||||
pub fn new_fake(name: &str) -> PluginSource {
|
||||
PluginSource {
|
||||
identity: PluginIdentity::new_fake(name).into(),
|
||||
persistent: Weak::<crate::PersistentPlugin>::new(),
|
||||
@ -40,9 +36,6 @@ impl PluginSource {
|
||||
|
||||
/// Try to upgrade the persistent reference, and return an error referencing `span` as the
|
||||
/// object that referenced it otherwise
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[doc(hidden)]
|
||||
pub fn persistent(&self, span: Option<Span>) -> Result<Arc<dyn GetPlugin>, ShellError> {
|
||||
self.persistent
|
||||
.upgrade()
|
24
crates/nu-plugin-engine/src/test_util.rs
Normal file
24
crates/nu-plugin-engine/src/test_util.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nu_plugin_core::interface_test_util::TestCase;
|
||||
use nu_plugin_protocol::{test_util::test_plugin_custom_value, PluginInput, PluginOutput};
|
||||
|
||||
use crate::{PluginCustomValueWithSource, PluginInterfaceManager, PluginSource};
|
||||
|
||||
pub trait TestCaseExt {
|
||||
/// Create a new [`PluginInterfaceManager`] that writes to this test case.
|
||||
fn plugin(&self, name: &str) -> PluginInterfaceManager;
|
||||
}
|
||||
|
||||
impl TestCaseExt for TestCase<PluginOutput, PluginInput> {
|
||||
fn plugin(&self, name: &str) -> PluginInterfaceManager {
|
||||
PluginInterfaceManager::new(PluginSource::new_fake(name).into(), None, self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn test_plugin_custom_value_with_source() -> PluginCustomValueWithSource {
|
||||
PluginCustomValueWithSource::new(
|
||||
test_plugin_custom_value(),
|
||||
Arc::new(PluginSource::new_fake("test")),
|
||||
)
|
||||
}
|
3
crates/nu-plugin-engine/src/util/mod.rs
Normal file
3
crates/nu-plugin-engine/src/util/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod mutable_cow;
|
||||
|
||||
pub use mutable_cow::MutableCow;
|
20
crates/nu-plugin-protocol/Cargo.toml
Normal file
20
crates/nu-plugin-protocol/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
authors = ["The Nushell Project Developers"]
|
||||
description = "Protocol type definitions for Nushell plugins"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-protocol"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-plugin-protocol"
|
||||
version = "0.92.3"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3", features = ["plugin"] }
|
||||
nu-utils = { path = "../nu-utils", version = "0.92.3" }
|
||||
|
||||
bincode = "1.3"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
semver = "1.0"
|
||||
typetag = "0.2"
|
21
crates/nu-plugin-protocol/LICENSE
Normal file
21
crates/nu-plugin-protocol/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.
|
5
crates/nu-plugin-protocol/README.md
Normal file
5
crates/nu-plugin-protocol/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# nu-plugin-protocol
|
||||
|
||||
This crate provides serde-compatible types for implementing the [Nushell plugin protocol](https://www.nushell.sh/contributor-book/plugin_protocol_reference.html). It is primarily used by the `nu-plugin` family of crates, but can also be used separately as well.
|
||||
|
||||
The specifics of I/O and serialization are not included in this crate. Use `serde_json` and/or `rmp-serde` (with the `named` serialization) to turn the types in this crate into data in the wire format.
|
@ -7,10 +7,10 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A representation of the plugin's invocation command including command line args
|
||||
///
|
||||
/// The `EvaluatedCall` contains information about the way a [`Plugin`](crate::Plugin) was invoked
|
||||
/// representing the [`Span`] corresponding to the invocation as well as the arguments
|
||||
/// it was invoked with. It is one of three items passed to [`run()`](crate::PluginCommand::run()) along with
|
||||
/// `name` which command that was invoked and a [`Value`] that represents the input.
|
||||
/// The `EvaluatedCall` contains information about the way a `Plugin` was invoked representing the
|
||||
/// [`Span`] corresponding to the invocation as well as the arguments it was invoked with. It is
|
||||
/// one of the items passed to `PluginCommand::run()`, along with the plugin reference, the engine
|
||||
/// interface, and a [`Value`] that represents the input.
|
||||
///
|
||||
/// The evaluated call is used with the Plugins because the plugin doesn't have
|
||||
/// access to the Stack and the EngineState the way a built in command might. For that
|
||||
@ -27,7 +27,8 @@ pub struct EvaluatedCall {
|
||||
}
|
||||
|
||||
impl EvaluatedCall {
|
||||
pub(crate) fn try_from_call(
|
||||
/// Try to create an [`EvaluatedCall`] from a command `Call`.
|
||||
pub fn try_from_call(
|
||||
call: &Call,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
@ -62,7 +63,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command --foo`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -78,7 +79,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command --bar`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -94,7 +95,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command --foo=true`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -110,7 +111,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command --foo=false`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -126,7 +127,7 @@ impl EvaluatedCall {
|
||||
/// Invoked with wrong type as `my_command --foo=1`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -163,7 +164,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command --foo 123`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -184,7 +185,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -214,7 +215,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command a b c`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -244,7 +245,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command --foo 123`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -261,7 +262,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command --bar 123`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -278,7 +279,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command --foo abc`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
||||
@ -305,7 +306,7 @@ impl EvaluatedCall {
|
||||
/// Invoked as `my_command zero one two three`:
|
||||
/// ```
|
||||
/// # use nu_protocol::{Spanned, Span, Value};
|
||||
/// # use nu_plugin::EvaluatedCall;
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let null_span = Span::new(0, 0);
|
||||
/// # let call = EvaluatedCall {
|
||||
/// # head: null_span,
|
@ -1,3 +1,14 @@
|
||||
//! Type definitions, including full `Serialize` and `Deserialize` implementations, for the protocol
|
||||
//! used for communication between the engine and a plugin.
|
||||
//!
|
||||
//! See the [plugin protocol reference](https://www.nushell.sh/contributor-book/plugin_protocol_reference.html)
|
||||
//! for more details on what exactly is being specified here.
|
||||
//!
|
||||
//! Plugins accept messages of [`PluginInput`] and send messages back of [`PluginOutput`]. This
|
||||
//! crate explicitly avoids implementing any functionality that depends on I/O, so the exact
|
||||
//! byte-level encoding scheme is not implemented here. See the protocol ref or `nu_plugin_core` for
|
||||
//! more details on how that works.
|
||||
|
||||
mod evaluated_call;
|
||||
mod plugin_custom_value;
|
||||
mod protocol_info;
|
||||
@ -5,8 +16,10 @@ mod protocol_info;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test_util;
|
||||
/// Things that can help with protocol-related tests. Not part of the public API, just used by other
|
||||
/// nushell crates.
|
||||
#[doc(hidden)]
|
||||
pub mod test_util;
|
||||
|
||||
use nu_protocol::{
|
||||
ast::Operator, engine::Closure, Config, LabeledError, PipelineData, PluginSignature, RawStream,
|
||||
@ -44,7 +57,7 @@ pub struct CallInfo<D> {
|
||||
|
||||
impl<D> CallInfo<D> {
|
||||
/// Convert the type of `input` from `D` to `T`.
|
||||
pub(crate) fn map_data<T>(
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<CallInfo<T>, ShellError> {
|
||||
@ -77,7 +90,7 @@ pub enum PipelineDataHeader {
|
||||
|
||||
impl PipelineDataHeader {
|
||||
/// Return a list of stream IDs embedded in the header
|
||||
pub(crate) fn stream_ids(&self) -> Vec<StreamId> {
|
||||
pub fn stream_ids(&self) -> Vec<StreamId> {
|
||||
match self {
|
||||
PipelineDataHeader::Empty => vec![],
|
||||
PipelineDataHeader::Value(_) => vec![],
|
||||
@ -124,7 +137,7 @@ pub struct RawStreamInfo {
|
||||
}
|
||||
|
||||
impl RawStreamInfo {
|
||||
pub(crate) fn new(id: StreamId, stream: &RawStream) -> Self {
|
||||
pub fn new(id: StreamId, stream: &RawStream) -> Self {
|
||||
RawStreamInfo {
|
||||
id,
|
||||
is_binary: stream.is_binary,
|
||||
@ -144,7 +157,7 @@ pub enum PluginCall<D> {
|
||||
impl<D> PluginCall<D> {
|
||||
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
|
||||
/// not contain data.
|
||||
pub(crate) fn map_data<T>(
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<PluginCall<T>, ShellError> {
|
||||
@ -187,7 +200,7 @@ pub enum CustomValueOp {
|
||||
|
||||
impl CustomValueOp {
|
||||
/// Get the name of the op, for error messages.
|
||||
pub(crate) fn name(&self) -> &'static str {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
CustomValueOp::ToBaseValue => "to_base_value",
|
||||
CustomValueOp::FollowPathInt(_) => "follow_path_int",
|
||||
@ -200,10 +213,7 @@ 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),
|
||||
@ -326,10 +336,7 @@ pub enum StreamMessage {
|
||||
}
|
||||
|
||||
/// Response to a [`PluginCall`]. The type parameter determines the output type for pipeline data.
|
||||
///
|
||||
/// Note: exported for internal use, not public.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[doc(hidden)]
|
||||
pub enum PluginCallResponse<D> {
|
||||
Error(LabeledError),
|
||||
Signature(Vec<PluginSignature>),
|
||||
@ -340,7 +347,7 @@ pub enum PluginCallResponse<D> {
|
||||
impl<D> PluginCallResponse<D> {
|
||||
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
|
||||
/// not contain data.
|
||||
pub(crate) fn map_data<T>(
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<PluginCallResponse<T>, ShellError> {
|
||||
@ -366,7 +373,7 @@ impl PluginCallResponse<PipelineDataHeader> {
|
||||
|
||||
impl PluginCallResponse<PipelineData> {
|
||||
/// Does this response have a stream?
|
||||
pub(crate) fn has_stream(&self) -> bool {
|
||||
pub fn has_stream(&self) -> bool {
|
||||
match self {
|
||||
PluginCallResponse::PipelineData(data) => match data {
|
||||
PipelineData::Empty => false,
|
||||
@ -385,7 +392,7 @@ pub enum PluginOption {
|
||||
/// Send `GcDisabled(true)` to stop the plugin from being automatically garbage collected, or
|
||||
/// `GcDisabled(false)` to enable it again.
|
||||
///
|
||||
/// See [`EngineInterface::set_gc_disabled`] for more information.
|
||||
/// See `EngineInterface::set_gc_disabled()` in `nu-plugin` for more information.
|
||||
GcDisabled(bool),
|
||||
}
|
||||
|
||||
@ -418,10 +425,7 @@ impl From<Ordering> for std::cmp::Ordering {
|
||||
}
|
||||
|
||||
/// Information received from the plugin
|
||||
///
|
||||
/// Note: exported for internal use, not public.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[doc(hidden)]
|
||||
pub enum PluginOutput {
|
||||
/// This must be the first message. Indicates supported protocol
|
||||
Hello(ProtocolInfo),
|
||||
@ -536,7 +540,7 @@ impl<D> EngineCall<D> {
|
||||
|
||||
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
|
||||
/// not contain data.
|
||||
pub(crate) fn map_data<T>(
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<EngineCall<T>, ShellError> {
|
||||
@ -581,7 +585,7 @@ pub enum EngineCallResponse<D> {
|
||||
impl<D> EngineCallResponse<D> {
|
||||
/// Convert the data type from `D` to `T`. The function will not be called if the variant does
|
||||
/// not contain data.
|
||||
pub(crate) fn map_data<T>(
|
||||
pub fn map_data<T>(
|
||||
self,
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<EngineCallResponse<T>, ShellError> {
|
||||
@ -596,12 +600,12 @@ impl<D> EngineCallResponse<D> {
|
||||
|
||||
impl EngineCallResponse<PipelineData> {
|
||||
/// Build an [`EngineCallResponse::PipelineData`] from a [`Value`]
|
||||
pub(crate) fn value(value: Value) -> EngineCallResponse<PipelineData> {
|
||||
pub fn value(value: Value) -> EngineCallResponse<PipelineData> {
|
||||
EngineCallResponse::PipelineData(PipelineData::Value(value, None))
|
||||
}
|
||||
|
||||
/// An [`EngineCallResponse::PipelineData`] with [`PipelineData::Empty`]
|
||||
pub(crate) const fn empty() -> EngineCallResponse<PipelineData> {
|
||||
pub const fn empty() -> EngineCallResponse<PipelineData> {
|
||||
EngineCallResponse::PipelineData(PipelineData::Empty)
|
||||
}
|
||||
}
|
236
crates/nu-plugin-protocol/src/plugin_custom_value/mod.rs
Normal file
236
crates/nu-plugin-protocol/src/plugin_custom_value/mod.rs
Normal file
@ -0,0 +1,236 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use nu_protocol::{ast::Operator, CustomValue, ShellError, Span, Value};
|
||||
use nu_utils::SharedCow;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// An opaque container for a custom value that is handled fully by a plugin.
|
||||
///
|
||||
/// This is the only type of custom value that is allowed to cross the plugin serialization
|
||||
/// boundary.
|
||||
///
|
||||
/// The plugin is responsible for ensuring that local plugin custom values are converted to and from
|
||||
/// [`PluginCustomValue`] on the boundary.
|
||||
///
|
||||
/// The engine is responsible for adding tracking the source of the custom value, ensuring that only
|
||||
/// [`PluginCustomValue`] is contained within any values sent, and that the source of any values
|
||||
/// sent matches the plugin it is being sent to.
|
||||
///
|
||||
/// Most of the [`CustomValue`] methods on this type will result in a panic. The source must be
|
||||
/// added (see `nu_plugin_engine::PluginCustomValueWithSource`) in order to implement the
|
||||
/// functionality via plugin calls.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PluginCustomValue(SharedCow<SharedContent>);
|
||||
|
||||
/// Content shared across copies of a plugin custom value.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct SharedContent {
|
||||
/// The name of the type of the custom value as defined by the plugin (`type_name()`)
|
||||
name: String,
|
||||
/// The bincoded representation of the custom value on the plugin side
|
||||
data: Vec<u8>,
|
||||
/// True if the custom value should notify the source if all copies of it are dropped.
|
||||
///
|
||||
/// This is not serialized if `false`, since most custom values don't need it.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
notify_on_drop: bool,
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!b
|
||||
}
|
||||
|
||||
#[typetag::serde]
|
||||
impl CustomValue for PluginCustomValue {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
self.clone().into_value(span)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> String {
|
||||
self.name().to_owned()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, _span: Span) -> Result<Value, ShellError> {
|
||||
panic!("to_base_value() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn follow_path_int(
|
||||
&self,
|
||||
_self_span: Span,
|
||||
_index: usize,
|
||||
_path_span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
panic!("follow_path_int() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn follow_path_string(
|
||||
&self,
|
||||
_self_span: Span,
|
||||
_column_name: String,
|
||||
_path_span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
panic!("follow_path_string() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn partial_cmp(&self, _other: &Value) -> Option<Ordering> {
|
||||
panic!("partial_cmp() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn operation(
|
||||
&self,
|
||||
_lhs_span: Span,
|
||||
_operator: Operator,
|
||||
_op_span: Span,
|
||||
_right: &Value,
|
||||
) -> Result<Value, ShellError> {
|
||||
panic!("operation() not available on plugin custom value without source");
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginCustomValue {
|
||||
/// Create a new [`PluginCustomValue`].
|
||||
pub fn new(name: String, data: Vec<u8>, notify_on_drop: bool) -> PluginCustomValue {
|
||||
PluginCustomValue(SharedCow::new(SharedContent {
|
||||
name,
|
||||
data,
|
||||
notify_on_drop,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Create a [`Value`] containing this custom value.
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
Value::custom(Box::new(self), span)
|
||||
}
|
||||
|
||||
/// The name of the type of the custom value as defined by the plugin (`type_name()`)
|
||||
pub fn name(&self) -> &str {
|
||||
&self.0.name
|
||||
}
|
||||
|
||||
/// The bincoded representation of the custom value on the plugin side
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.0.data
|
||||
}
|
||||
|
||||
/// True if the custom value should notify the source if all copies of it are dropped.
|
||||
pub fn notify_on_drop(&self) -> bool {
|
||||
self.0.notify_on_drop
|
||||
}
|
||||
|
||||
/// Count the number of shared copies of this [`PluginCustomValue`].
|
||||
pub fn ref_count(&self) -> usize {
|
||||
SharedCow::ref_count(&self.0)
|
||||
}
|
||||
|
||||
/// Serialize a custom value into a [`PluginCustomValue`]. This should only be done on the
|
||||
/// plugin side.
|
||||
pub fn serialize_from_custom_value(
|
||||
custom_value: &dyn CustomValue,
|
||||
span: Span,
|
||||
) -> Result<PluginCustomValue, ShellError> {
|
||||
let name = custom_value.type_name();
|
||||
let notify_on_drop = custom_value.notify_plugin_on_drop();
|
||||
bincode::serialize(custom_value)
|
||||
.map(|data| PluginCustomValue::new(name, data, notify_on_drop))
|
||||
.map_err(|err| ShellError::CustomValueFailedToEncode {
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserialize a [`PluginCustomValue`] into a `Box<dyn CustomValue>`. This should only be done
|
||||
/// on the plugin side.
|
||||
pub fn deserialize_to_custom_value(
|
||||
&self,
|
||||
span: Span,
|
||||
) -> Result<Box<dyn CustomValue>, ShellError> {
|
||||
bincode::deserialize::<Box<dyn CustomValue>>(self.data()).map_err(|err| {
|
||||
ShellError::CustomValueFailedToDecode {
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
}
|
||||
})
|
||||
}
|
||||
/// Convert all plugin-native custom values to [`PluginCustomValue`] within the given `value`,
|
||||
/// recursively. This should only be done on the plugin side.
|
||||
pub fn serialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { ref val, .. } => {
|
||||
if val.as_any().downcast_ref::<PluginCustomValue>().is_some() {
|
||||
// Already a PluginCustomValue
|
||||
Ok(())
|
||||
} else {
|
||||
let serialized = Self::serialize_from_custom_value(&**val, span)?;
|
||||
*value = Value::custom(Box::new(serialized), span);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert all [`PluginCustomValue`]s to plugin-native custom values within the given `value`,
|
||||
/// recursively. This should only be done on the plugin side.
|
||||
pub fn deserialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { ref val, .. } => {
|
||||
if let Some(val) = val.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
let deserialized = val.deserialize_to_custom_value(span)?;
|
||||
*value = Value::custom(deserialized, span);
|
||||
Ok(())
|
||||
} else {
|
||||
// Already not a PluginCustomValue
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Render any custom values in the `Value` using `to_base_value()`
|
||||
pub fn render_to_base_value_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { ref val, .. } => {
|
||||
*value = val.to_base_value(span)?;
|
||||
Ok(())
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,13 +1,63 @@
|
||||
use crate::test_util::{expected_test_custom_value, test_plugin_custom_value, TestCustomValue};
|
||||
|
||||
use super::PluginCustomValue;
|
||||
use crate::{
|
||||
plugin::PluginSource,
|
||||
protocol::test_util::{
|
||||
expected_test_custom_value, test_plugin_custom_value, test_plugin_custom_value_with_source,
|
||||
TestCustomValue,
|
||||
},
|
||||
};
|
||||
use nu_protocol::{engine::Closure, record, CustomValue, IntoSpanned, ShellError, Span, Value};
|
||||
use std::sync::Arc;
|
||||
use nu_protocol::{engine::Closure, record, CustomValue, ShellError, Span, Value};
|
||||
|
||||
fn check_record_custom_values(
|
||||
val: &Value,
|
||||
keys: &[&str],
|
||||
mut f: impl FnMut(&str, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let record = val.as_record()?;
|
||||
for key in keys {
|
||||
let val = record
|
||||
.get(key)
|
||||
.unwrap_or_else(|| panic!("record does not contain '{key}'"));
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("'{key}' not custom value"));
|
||||
f(key, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_list_custom_values(
|
||||
val: &Value,
|
||||
indices: impl IntoIterator<Item = usize>,
|
||||
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let list = val.as_list()?;
|
||||
for index in indices {
|
||||
let val = list
|
||||
.get(index)
|
||||
.unwrap_or_else(|| panic!("[{index}] not present in list"));
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
|
||||
f(index, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_closure_custom_values(
|
||||
val: &Value,
|
||||
indices: impl IntoIterator<Item = usize>,
|
||||
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let closure = val.as_closure()?;
|
||||
for index in indices {
|
||||
let val = closure
|
||||
.captures
|
||||
.get(index)
|
||||
.unwrap_or_else(|| panic!("[{index}] not present in closure"));
|
||||
let custom_value = val
|
||||
.1
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
|
||||
f(index, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_deserialize() -> Result<(), ShellError> {
|
||||
@ -15,7 +65,6 @@ fn serialize_deserialize() -> Result<(), ShellError> {
|
||||
let span = Span::test_data();
|
||||
let serialized = PluginCustomValue::serialize_from_custom_value(&original_value, span)?;
|
||||
assert_eq!(original_value.type_name(), serialized.name());
|
||||
assert!(serialized.source.is_none());
|
||||
let deserialized = serialized.deserialize_to_custom_value(span)?;
|
||||
let downcasted = deserialized
|
||||
.as_any()
|
||||
@ -39,190 +88,6 @@ fn expected_serialize_output() -> Result<(), ShellError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_in_at_root() -> Result<(), ShellError> {
|
||||
let mut val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let source = Arc::new(PluginSource::new_fake("foo"));
|
||||
PluginCustomValue::add_source_in(&mut val, &source)?;
|
||||
|
||||
let custom_value = val.as_custom_value()?;
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.expect("not PluginCustomValue");
|
||||
assert_eq!(
|
||||
Some(Arc::as_ptr(&source)),
|
||||
plugin_custom_value.source.as_ref().map(Arc::as_ptr)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_record_custom_values(
|
||||
val: &Value,
|
||||
keys: &[&str],
|
||||
mut f: impl FnMut(&str, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let record = val.as_record()?;
|
||||
for key in keys {
|
||||
let val = record
|
||||
.get(key)
|
||||
.unwrap_or_else(|| panic!("record does not contain '{key}'"));
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("'{key}' not custom value"));
|
||||
f(key, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_in_nested_record() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_record(record! {
|
||||
"foo" => orig_custom_val.clone(),
|
||||
"bar" => orig_custom_val.clone(),
|
||||
});
|
||||
let source = Arc::new(PluginSource::new_fake("foo"));
|
||||
PluginCustomValue::add_source_in(&mut val, &source)?;
|
||||
|
||||
check_record_custom_values(&val, &["foo", "bar"], |key, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("'{key}' not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
Some(Arc::as_ptr(&source)),
|
||||
plugin_custom_value.source.as_ref().map(Arc::as_ptr),
|
||||
"'{key}' source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn check_list_custom_values(
|
||||
val: &Value,
|
||||
indices: impl IntoIterator<Item = usize>,
|
||||
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let list = val.as_list()?;
|
||||
for index in indices {
|
||||
let val = list
|
||||
.get(index)
|
||||
.unwrap_or_else(|| panic!("[{index}] not present in list"));
|
||||
let custom_value = val
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
|
||||
f(index, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_in_nested_list() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_list(vec![orig_custom_val.clone(), orig_custom_val.clone()]);
|
||||
let source = Arc::new(PluginSource::new_fake("foo"));
|
||||
PluginCustomValue::add_source_in(&mut val, &source)?;
|
||||
|
||||
check_list_custom_values(&val, 0..=1, |index, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
Some(Arc::as_ptr(&source)),
|
||||
plugin_custom_value.source.as_ref().map(Arc::as_ptr),
|
||||
"[{index}] source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn check_closure_custom_values(
|
||||
val: &Value,
|
||||
indices: impl IntoIterator<Item = usize>,
|
||||
mut f: impl FnMut(usize, &dyn CustomValue) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let closure = val.as_closure()?;
|
||||
for index in indices {
|
||||
let val = closure
|
||||
.captures
|
||||
.get(index)
|
||||
.unwrap_or_else(|| panic!("[{index}] not present in closure"));
|
||||
let custom_value = val
|
||||
.1
|
||||
.as_custom_value()
|
||||
.unwrap_or_else(|_| panic!("[{index}] not custom value"));
|
||||
f(index, custom_value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_source_in_nested_closure() -> Result<(), ShellError> {
|
||||
let orig_custom_val = Value::test_custom_value(Box::new(test_plugin_custom_value()));
|
||||
let mut val = Value::test_closure(Closure {
|
||||
block_id: 0,
|
||||
captures: vec![(0, orig_custom_val.clone()), (1, orig_custom_val.clone())],
|
||||
});
|
||||
let source = Arc::new(PluginSource::new_fake("foo"));
|
||||
PluginCustomValue::add_source_in(&mut val, &source)?;
|
||||
|
||||
check_closure_custom_values(&val, 0..=1, |index, custom_value| {
|
||||
let plugin_custom_value: &PluginCustomValue = custom_value
|
||||
.as_any()
|
||||
.downcast_ref()
|
||||
.unwrap_or_else(|| panic!("[{index}] not PluginCustomValue"));
|
||||
assert_eq!(
|
||||
Some(Arc::as_ptr(&source)),
|
||||
plugin_custom_value.source.as_ref().map(Arc::as_ptr),
|
||||
"[{index}] source not set correctly"
|
||||
);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_source_error_message() -> Result<(), ShellError> {
|
||||
let span = Span::new(5, 7);
|
||||
let ok_val = test_plugin_custom_value_with_source();
|
||||
let native_val = TestCustomValue(32);
|
||||
let foreign_val = {
|
||||
let mut val = test_plugin_custom_value();
|
||||
val.source = Some(Arc::new(PluginSource::new_fake("other")));
|
||||
val
|
||||
};
|
||||
let source = PluginSource::new_fake("test");
|
||||
|
||||
PluginCustomValue::verify_source((&ok_val as &dyn CustomValue).into_spanned(span), &source)
|
||||
.expect("ok_val should be verified ok");
|
||||
|
||||
for (val, src_plugin) in [
|
||||
(&native_val as &dyn CustomValue, None),
|
||||
(&foreign_val as &dyn CustomValue, Some("other")),
|
||||
] {
|
||||
let error = PluginCustomValue::verify_source(val.into_spanned(span), &source).expect_err(
|
||||
&format!("a custom value from {src_plugin:?} should result in an error"),
|
||||
);
|
||||
if let ShellError::CustomValueIncorrectForPlugin {
|
||||
name,
|
||||
span: err_span,
|
||||
dest_plugin,
|
||||
src_plugin: err_src_plugin,
|
||||
} = error
|
||||
{
|
||||
assert_eq!("TestCustomValue", name, "error.name from {src_plugin:?}");
|
||||
assert_eq!(span, err_span, "error.span from {src_plugin:?}");
|
||||
assert_eq!("test", dest_plugin, "error.dest_plugin from {src_plugin:?}");
|
||||
assert_eq!(src_plugin, err_src_plugin.as_deref(), "error.src_plugin");
|
||||
} else {
|
||||
panic!("the error returned should be CustomValueIncorrectForPlugin");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_in_root() -> Result<(), ShellError> {
|
||||
let span = Span::new(4, 10);
|
||||
@ -238,7 +103,6 @@ fn serialize_in_root() -> Result<(), ShellError> {
|
||||
test_plugin_custom_value().data(),
|
||||
plugin_custom_value.data()
|
||||
);
|
||||
assert!(plugin_custom_value.source.is_none());
|
||||
} else {
|
||||
panic!("Failed to downcast to PluginCustomValue");
|
||||
}
|
@ -9,7 +9,7 @@ pub struct ProtocolInfo {
|
||||
/// The name of the protocol being implemented. Only one protocol is supported. This field
|
||||
/// can be safely ignored, because not matching is a deserialization error
|
||||
pub protocol: Protocol,
|
||||
/// The semantic version of the protocol. This should be the version of the `nu-plugin`
|
||||
/// The semantic version of the protocol. This should be the version of the `nu-plugin-protocol`
|
||||
/// crate
|
||||
pub version: String,
|
||||
/// Supported optional features. This helps to maintain semver compatibility when adding new
|
@ -1,12 +1,12 @@
|
||||
use super::PluginCustomValue;
|
||||
use crate::plugin::PluginSource;
|
||||
use crate::PluginCustomValue;
|
||||
use nu_protocol::{CustomValue, ShellError, Span, Value};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A custom value that can be used for testing.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct TestCustomValue(pub i32);
|
||||
pub struct TestCustomValue(pub i32);
|
||||
|
||||
#[typetag::serde]
|
||||
#[typetag::serde(name = "nu_plugin_protocol::test_util::TestCustomValue")]
|
||||
impl CustomValue for TestCustomValue {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
Value::custom(Box::new(self.clone()), span)
|
||||
@ -29,17 +29,15 @@ impl CustomValue for TestCustomValue {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn test_plugin_custom_value() -> PluginCustomValue {
|
||||
/// A [`TestCustomValue`] serialized as a [`PluginCustomValue`].
|
||||
pub fn test_plugin_custom_value() -> PluginCustomValue {
|
||||
let data = bincode::serialize(&expected_test_custom_value() as &dyn CustomValue)
|
||||
.expect("bincode serialization of the expected_test_custom_value() failed");
|
||||
|
||||
PluginCustomValue::new("TestCustomValue".into(), data, false, None)
|
||||
PluginCustomValue::new("TestCustomValue".into(), data, false)
|
||||
}
|
||||
|
||||
pub(crate) fn expected_test_custom_value() -> TestCustomValue {
|
||||
/// The expected [`TestCustomValue`] that [`test_plugin_custom_value()`] should deserialize into.
|
||||
pub fn expected_test_custom_value() -> TestCustomValue {
|
||||
TestCustomValue(-1)
|
||||
}
|
||||
|
||||
pub(crate) fn test_plugin_custom_value_with_source() -> PluginCustomValue {
|
||||
test_plugin_custom_value().with_source(Some(PluginSource::new_fake("test").into()))
|
||||
}
|
@ -6,6 +6,9 @@ license = "MIT"
|
||||
description = "Testing support for Nushell plugins"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-plugin-test-support"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
@ -13,6 +16,9 @@ nu-engine = { path = "../nu-engine", version = "0.92.3", features = ["plugin"] }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3", features = ["plugin"] }
|
||||
nu-parser = { path = "../nu-parser", version = "0.92.3", features = ["plugin"] }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.92.3" }
|
||||
nu-plugin-core = { path = "../nu-plugin-core", version = "0.92.3" }
|
||||
nu-plugin-engine = { path = "../nu-plugin-engine", version = "0.92.3" }
|
||||
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.92.3" }
|
||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.92.3" }
|
||||
nu-ansi-term = { workspace = true }
|
||||
similar = "2.5"
|
||||
|
@ -3,7 +3,7 @@ use std::{
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use nu_plugin::{GetPlugin, PluginInterface};
|
||||
use nu_plugin_engine::{GetPlugin, PluginInterface};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use nu_plugin::{create_plugin_signature, Plugin, PluginDeclaration};
|
||||
use nu_plugin::{create_plugin_signature, Plugin};
|
||||
use nu_plugin_engine::PluginDeclaration;
|
||||
use nu_protocol::{engine::StateWorkingSet, RegisteredPlugin, ShellError};
|
||||
|
||||
use crate::{fake_persistent_plugin::FakePersistentPlugin, spawn_fake_plugin::spawn_fake_plugin};
|
||||
|
@ -4,7 +4,9 @@ use nu_ansi_term::Style;
|
||||
use nu_cmd_lang::create_default_context;
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
use nu_plugin::{Plugin, PluginCommand, PluginCustomValue, PluginSource};
|
||||
use nu_plugin::{Plugin, PluginCommand};
|
||||
use nu_plugin_engine::{PluginCustomValueWithSource, PluginSource, WithSource};
|
||||
use nu_plugin_protocol::PluginCustomValue;
|
||||
use nu_protocol::{
|
||||
debugger::WithoutDebug,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
@ -135,13 +137,14 @@ impl PluginTest {
|
||||
// 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(()) => {
|
||||
move |mut value| {
|
||||
let result = PluginCustomValue::serialize_custom_values_in(&mut value)
|
||||
// Make sure to mark them with the source so they pass correctly, too.
|
||||
let _ = PluginCustomValue::add_source_in(&mut value, &source);
|
||||
value
|
||||
}
|
||||
.and_then(|_| PluginCustomValueWithSource::add_source_in(&mut value, &source));
|
||||
match result {
|
||||
Ok(()) => value,
|
||||
Err(err) => Value::error(err, value.span()),
|
||||
}
|
||||
},
|
||||
None,
|
||||
)?;
|
||||
@ -151,7 +154,9 @@ impl PluginTest {
|
||||
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) {
|
||||
let result = PluginCustomValueWithSource::remove_source_in(&mut value)
|
||||
.and_then(|_| PluginCustomValue::deserialize_custom_values_in(&mut value));
|
||||
match result {
|
||||
Ok(()) => value,
|
||||
Err(err) => Value::error(err, value.span()),
|
||||
}
|
||||
@ -284,12 +289,12 @@ impl PluginTest {
|
||||
match (a, b) {
|
||||
(Value::Custom { val, .. }, _) => {
|
||||
// We have to serialize both custom values before handing them to the plugin
|
||||
let mut serialized =
|
||||
PluginCustomValue::serialize_from_custom_value(val.as_ref(), a.span())?;
|
||||
serialized.set_source(Some(self.source.clone()));
|
||||
let serialized =
|
||||
PluginCustomValue::serialize_from_custom_value(val.as_ref(), a.span())?
|
||||
.with_source(self.source.clone());
|
||||
let mut b_serialized = b.clone();
|
||||
PluginCustomValue::serialize_custom_values_in(&mut b_serialized)?;
|
||||
PluginCustomValue::add_source_in(&mut b_serialized, &self.source)?;
|
||||
PluginCustomValueWithSource::add_source_in(&mut b_serialized, &self.source)?;
|
||||
// Now get the plugin reference and execute the comparison
|
||||
let persistent = self.source.persistent(None)?.get_plugin(None)?;
|
||||
let ordering = persistent.custom_value_partial_cmp(serialized, b_serialized)?;
|
||||
@ -354,8 +359,8 @@ impl PluginTest {
|
||||
val: &dyn CustomValue,
|
||||
span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
let mut serialized = PluginCustomValue::serialize_from_custom_value(val, span)?;
|
||||
serialized.set_source(Some(self.source.clone()));
|
||||
let serialized = PluginCustomValue::serialize_from_custom_value(val, span)?
|
||||
.with_source(self.source.clone());
|
||||
let persistent = self.source.persistent(None)?.get_plugin(None)?;
|
||||
persistent.custom_value_to_base_value(serialized.into_spanned(span))
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::sync::{mpsc, Arc};
|
||||
|
||||
use nu_plugin::{
|
||||
InterfaceManager, Plugin, PluginInput, PluginInterfaceManager, PluginOutput, PluginRead,
|
||||
PluginSource, PluginWrite,
|
||||
};
|
||||
use nu_plugin::Plugin;
|
||||
use nu_plugin_core::{InterfaceManager, PluginRead, PluginWrite};
|
||||
use nu_plugin_engine::{PluginInterfaceManager, PluginSource};
|
||||
use nu_plugin_protocol::{PluginInput, PluginOutput};
|
||||
use nu_protocol::{PluginIdentity, ShellError};
|
||||
|
||||
use crate::fake_persistent_plugin::FakePersistentPlugin;
|
||||
|
@ -13,30 +13,20 @@ bench = false
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
|
||||
nu-system = { path = "../nu-system", version = "0.92.3" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.92.3" }
|
||||
nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.92.3" }
|
||||
nu-plugin-core = { path = "../nu-plugin-core", version = "0.92.3", default-features = false }
|
||||
|
||||
bincode = "1.3"
|
||||
rmp-serde = "1.2"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = "0.4"
|
||||
miette = { workspace = true }
|
||||
semver = "1.0"
|
||||
typetag = "0.2"
|
||||
log = { workspace = true }
|
||||
thiserror = "1.0"
|
||||
interprocess = { version = "1.2.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde = { workspace = true }
|
||||
typetag = "0.2"
|
||||
|
||||
[features]
|
||||
default = ["local-socket"]
|
||||
local-socket = ["interprocess"]
|
||||
local-socket = ["nu-plugin-core/local-socket"]
|
||||
|
||||
[target.'cfg(target_family = "unix")'.dependencies]
|
||||
# For setting the process group ID (EnterForeground / LeaveForeground)
|
||||
nix = { workspace = true, default-features = false, features = ["process"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { workspace = true, features = [
|
||||
# For setting process creation flags
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
|
@ -64,35 +64,16 @@
|
||||
//! [Plugin Example](https://github.com/nushell/nushell/tree/main/crates/nu_plugin_example)
|
||||
//! that demonstrates the full range of plugin capabilities.
|
||||
mod plugin;
|
||||
mod protocol;
|
||||
mod sequence;
|
||||
mod serializers;
|
||||
|
||||
pub use plugin::{
|
||||
serve_plugin, EngineInterface, Plugin, PluginCommand, PluginEncoder, PluginRead, PluginWrite,
|
||||
SimplePluginCommand,
|
||||
};
|
||||
pub use protocol::EvaluatedCall;
|
||||
pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};
|
||||
#[cfg(test)]
|
||||
mod test_util;
|
||||
|
||||
// Used by other nu crates.
|
||||
#[doc(hidden)]
|
||||
pub use plugin::{
|
||||
add_plugin_to_working_set, create_plugin_signature, get_signature, load_plugin_file,
|
||||
load_plugin_registry_item, serve_plugin_io, EngineInterfaceManager, GetPlugin, Interface,
|
||||
InterfaceManager, PersistentPlugin, PluginDeclaration, PluginExecutionCommandContext,
|
||||
PluginExecutionContext, PluginInterface, PluginInterfaceManager, PluginSource,
|
||||
ServePluginError,
|
||||
};
|
||||
#[doc(hidden)]
|
||||
pub use protocol::{PluginCustomValue, PluginInput, PluginOutput};
|
||||
#[doc(hidden)]
|
||||
pub use serializers::EncodingType;
|
||||
#[doc(hidden)]
|
||||
pub mod util;
|
||||
pub use plugin::{serve_plugin, EngineInterface, Plugin, PluginCommand, SimplePluginCommand};
|
||||
|
||||
// Used by external benchmarks.
|
||||
// Re-exports. Consider semver implications carefully.
|
||||
pub use nu_plugin_core::{JsonSerializer, MsgPackSerializer, PluginEncoder};
|
||||
pub use nu_plugin_protocol::EvaluatedCall;
|
||||
|
||||
// Required by other internal crates.
|
||||
#[doc(hidden)]
|
||||
pub use plugin::Encoder;
|
||||
#[doc(hidden)]
|
||||
pub use protocol::PluginCallResponse;
|
||||
pub use plugin::{create_plugin_signature, serve_plugin_io};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,16 +1,14 @@
|
||||
//! Interface used by the plugin to communicate with the engine.
|
||||
|
||||
use super::{
|
||||
stream::{StreamManager, StreamManagerHandle},
|
||||
Interface, InterfaceManager, PipelineDataWriter, PluginRead, PluginWrite, Sequence,
|
||||
use nu_plugin_core::{
|
||||
util::{Sequence, Waitable, WaitableMut},
|
||||
Interface, InterfaceManager, PipelineDataWriter, PluginRead, PluginWrite, StreamManager,
|
||||
StreamManagerHandle,
|
||||
};
|
||||
use crate::{
|
||||
protocol::{
|
||||
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, Ordering,
|
||||
PluginCall, PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption,
|
||||
PluginOutput, ProtocolInfo,
|
||||
},
|
||||
util::{Waitable, WaitableMut},
|
||||
use nu_plugin_protocol::{
|
||||
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, Ordering, PluginCall,
|
||||
PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption, PluginOutput,
|
||||
ProtocolInfo,
|
||||
};
|
||||
use nu_protocol::{
|
||||
engine::Closure, Config, IntoInterruptiblePipelineData, LabeledError, ListStream, PipelineData,
|
File diff suppressed because it is too large
Load Diff
@ -1,249 +1,36 @@
|
||||
use crate::{
|
||||
plugin::interface::ReceivedPluginCall,
|
||||
protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput},
|
||||
EncodingType,
|
||||
};
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
collections::HashMap,
|
||||
env,
|
||||
ffi::OsString,
|
||||
io::{BufReader, BufWriter},
|
||||
ops::Deref,
|
||||
panic::AssertUnwindSafe,
|
||||
path::Path,
|
||||
process::{Child, Command as CommandSys},
|
||||
sync::{
|
||||
mpsc::{self, TrySendError},
|
||||
Arc, Mutex,
|
||||
},
|
||||
sync::mpsc::{self, TrySendError},
|
||||
thread,
|
||||
};
|
||||
|
||||
use nu_engine::documentation::get_flags_section;
|
||||
use nu_plugin_core::{
|
||||
ClientCommunicationIo, CommunicationMode, InterfaceManager, PluginEncoder, PluginRead,
|
||||
PluginWrite,
|
||||
};
|
||||
use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
|
||||
use nu_protocol::{
|
||||
ast::Operator, engine::StateWorkingSet, report_error_new, CustomValue, IntoSpanned,
|
||||
LabeledError, PipelineData, PluginIdentity, PluginRegistryFile, PluginRegistryItem,
|
||||
PluginRegistryItemData, PluginSignature, RegisteredPlugin, ShellError, Span, Spanned, Value,
|
||||
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, ShellError, Spanned, Value,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
pub use self::interface::{PluginRead, PluginWrite};
|
||||
use self::{
|
||||
command::render_examples,
|
||||
communication_mode::{
|
||||
ClientCommunicationIo, CommunicationMode, PreparedServerCommunication,
|
||||
ServerCommunicationIo,
|
||||
},
|
||||
gc::PluginGc,
|
||||
};
|
||||
use self::{command::render_examples, interface::ReceivedPluginCall};
|
||||
|
||||
mod command;
|
||||
mod communication_mode;
|
||||
mod context;
|
||||
mod declaration;
|
||||
mod gc;
|
||||
mod interface;
|
||||
mod persistent;
|
||||
mod process;
|
||||
mod source;
|
||||
|
||||
pub use command::{create_plugin_signature, PluginCommand, SimplePluginCommand};
|
||||
pub use declaration::PluginDeclaration;
|
||||
pub use interface::{
|
||||
EngineInterface, EngineInterfaceManager, Interface, InterfaceManager, PluginInterface,
|
||||
PluginInterfaceManager,
|
||||
};
|
||||
pub use persistent::{GetPlugin, PersistentPlugin};
|
||||
|
||||
pub use context::{PluginExecutionCommandContext, PluginExecutionContext};
|
||||
pub use source::PluginSource;
|
||||
pub use interface::{EngineInterface, EngineInterfaceManager};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const OUTPUT_BUFFER_SIZE: usize = 8192;
|
||||
|
||||
/// Encoder for a specific message type. Usually implemented on [`PluginInput`]
|
||||
/// and [`PluginOutput`].
|
||||
#[doc(hidden)]
|
||||
pub trait Encoder<T>: Clone + Send + Sync {
|
||||
/// Serialize a value in the [`PluginEncoder`]s format
|
||||
///
|
||||
/// Returns [`ShellError::IOError`] if there was a problem writing, or
|
||||
/// [`ShellError::PluginFailedToEncode`] for a serialization error.
|
||||
#[doc(hidden)]
|
||||
fn encode(&self, data: &T, writer: &mut impl std::io::Write) -> Result<(), ShellError>;
|
||||
|
||||
/// Deserialize a value from the [`PluginEncoder`]'s format
|
||||
///
|
||||
/// Returns `None` if there is no more output to receive.
|
||||
///
|
||||
/// Returns [`ShellError::IOError`] if there was a problem reading, or
|
||||
/// [`ShellError::PluginFailedToDecode`] for a deserialization error.
|
||||
#[doc(hidden)]
|
||||
fn decode(&self, reader: &mut impl std::io::BufRead) -> Result<Option<T>, ShellError>;
|
||||
}
|
||||
|
||||
/// Encoding scheme that defines a plugin's communication protocol with Nu
|
||||
pub trait PluginEncoder: Encoder<PluginInput> + Encoder<PluginOutput> {
|
||||
/// The name of the encoder (e.g., `json`)
|
||||
fn name(&self) -> &str;
|
||||
}
|
||||
|
||||
fn create_command(path: &Path, mut shell: Option<&Path>, mode: &CommunicationMode) -> CommandSys {
|
||||
log::trace!("Starting plugin: {path:?}, shell = {shell:?}, mode = {mode:?}");
|
||||
|
||||
let mut shell_args = vec![];
|
||||
|
||||
if shell.is_none() {
|
||||
// We only have to do this for things that are not executable by Rust's Command API on
|
||||
// Windows. They do handle bat/cmd files for us, helpfully.
|
||||
//
|
||||
// Also include anything that wouldn't be executable with a shebang, like JAR files.
|
||||
shell = match path.extension().and_then(|e| e.to_str()) {
|
||||
Some("sh") => {
|
||||
if cfg!(unix) {
|
||||
// We don't want to override what might be in the shebang if this is Unix, since
|
||||
// some scripts will have a shebang specifying bash even if they're .sh
|
||||
None
|
||||
} else {
|
||||
Some(Path::new("sh"))
|
||||
}
|
||||
}
|
||||
Some("nu") => {
|
||||
shell_args.push("--stdin");
|
||||
Some(Path::new("nu"))
|
||||
}
|
||||
Some("py") => Some(Path::new("python")),
|
||||
Some("rb") => Some(Path::new("ruby")),
|
||||
Some("jar") => {
|
||||
shell_args.push("-jar");
|
||||
Some(Path::new("java"))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
let mut process = if let Some(shell) = shell {
|
||||
let mut process = std::process::Command::new(shell);
|
||||
process.args(shell_args);
|
||||
process.arg(path);
|
||||
|
||||
process
|
||||
} else {
|
||||
std::process::Command::new(path)
|
||||
};
|
||||
|
||||
process.args(mode.args());
|
||||
|
||||
// Setup I/O according to the communication mode
|
||||
mode.setup_command_io(&mut process);
|
||||
|
||||
// The plugin should be run in a new process group to prevent Ctrl-C from stopping it
|
||||
#[cfg(unix)]
|
||||
process.process_group(0);
|
||||
#[cfg(windows)]
|
||||
process.creation_flags(windows::Win32::System::Threading::CREATE_NEW_PROCESS_GROUP.0);
|
||||
|
||||
// In order to make bugs with improper use of filesystem without getting the engine current
|
||||
// directory more obvious, the plugin always starts in the directory of its executable
|
||||
if let Some(dirname) = path.parent() {
|
||||
process.current_dir(dirname);
|
||||
}
|
||||
|
||||
process
|
||||
}
|
||||
|
||||
fn make_plugin_interface(
|
||||
mut child: Child,
|
||||
comm: PreparedServerCommunication,
|
||||
source: Arc<PluginSource>,
|
||||
pid: Option<u32>,
|
||||
gc: Option<PluginGc>,
|
||||
) -> Result<PluginInterface, ShellError> {
|
||||
match comm.connect(&mut child)? {
|
||||
ServerCommunicationIo::Stdio(stdin, stdout) => make_plugin_interface_with_streams(
|
||||
stdout,
|
||||
stdin,
|
||||
move || {
|
||||
let _ = child.wait();
|
||||
},
|
||||
source,
|
||||
pid,
|
||||
gc,
|
||||
),
|
||||
#[cfg(feature = "local-socket")]
|
||||
ServerCommunicationIo::LocalSocket { read_out, write_in } => {
|
||||
make_plugin_interface_with_streams(
|
||||
read_out,
|
||||
write_in,
|
||||
move || {
|
||||
let _ = child.wait();
|
||||
},
|
||||
source,
|
||||
pid,
|
||||
gc,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_plugin_interface_with_streams(
|
||||
mut reader: impl std::io::Read + Send + 'static,
|
||||
writer: impl std::io::Write + Send + 'static,
|
||||
after_close: impl FnOnce() + Send + 'static,
|
||||
source: Arc<PluginSource>,
|
||||
pid: Option<u32>,
|
||||
gc: Option<PluginGc>,
|
||||
) -> Result<PluginInterface, ShellError> {
|
||||
let encoder = get_plugin_encoding(&mut reader)?;
|
||||
|
||||
let reader = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader);
|
||||
let writer = BufWriter::with_capacity(OUTPUT_BUFFER_SIZE, writer);
|
||||
|
||||
let mut manager =
|
||||
PluginInterfaceManager::new(source.clone(), pid, (Mutex::new(writer), encoder));
|
||||
manager.set_garbage_collector(gc);
|
||||
|
||||
let interface = manager.get_interface();
|
||||
interface.hello()?;
|
||||
|
||||
// Spawn the reader on a new thread. We need to be able to read messages at the same time that
|
||||
// we write, because we are expected to be able to handle multiple messages coming in from the
|
||||
// plugin at any time, including stream messages like `Drop`.
|
||||
std::thread::Builder::new()
|
||||
.name(format!(
|
||||
"plugin interface reader ({})",
|
||||
source.identity.name()
|
||||
))
|
||||
.spawn(move || {
|
||||
if let Err(err) = manager.consume_all((reader, encoder)) {
|
||||
log::warn!("Error in PluginInterfaceManager: {err}");
|
||||
}
|
||||
// If the loop has ended, drop the manager so everyone disconnects and then run
|
||||
// after_close
|
||||
drop(manager);
|
||||
after_close();
|
||||
})
|
||||
.map_err(|err| ShellError::PluginFailedToLoad {
|
||||
msg: format!("Failed to spawn thread for plugin: {err}"),
|
||||
})?;
|
||||
|
||||
Ok(interface)
|
||||
}
|
||||
|
||||
#[doc(hidden)] // Note: not for plugin authors / only used in nu-parser
|
||||
pub fn get_signature(
|
||||
plugin: Arc<PersistentPlugin>,
|
||||
envs: impl FnOnce() -> Result<HashMap<String, String>, ShellError>,
|
||||
) -> Result<Vec<PluginSignature>, ShellError> {
|
||||
plugin.get(envs)?.get_signature()
|
||||
}
|
||||
|
||||
/// The API for a Nushell plugin
|
||||
///
|
||||
/// A plugin defines multiple commands, which are added to the engine when the user calls
|
||||
@ -499,6 +286,9 @@ pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static)
|
||||
read_in,
|
||||
mut write_out,
|
||||
}) => {
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::sync::Mutex;
|
||||
|
||||
tell_nushell_encoding(&mut write_out, &encoder)
|
||||
.expect("failed to tell nushell encoding");
|
||||
|
||||
@ -895,119 +685,3 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) {
|
||||
|
||||
println!("{help}")
|
||||
}
|
||||
|
||||
pub fn get_plugin_encoding(
|
||||
child_stdout: &mut impl std::io::Read,
|
||||
) -> Result<EncodingType, ShellError> {
|
||||
let mut length_buf = [0u8; 1];
|
||||
child_stdout
|
||||
.read_exact(&mut length_buf)
|
||||
.map_err(|e| ShellError::PluginFailedToLoad {
|
||||
msg: format!("unable to get encoding from plugin: {e}"),
|
||||
})?;
|
||||
|
||||
let mut buf = vec![0u8; length_buf[0] as usize];
|
||||
child_stdout
|
||||
.read_exact(&mut buf)
|
||||
.map_err(|e| ShellError::PluginFailedToLoad {
|
||||
msg: format!("unable to get encoding from plugin: {e}"),
|
||||
})?;
|
||||
|
||||
EncodingType::try_from_bytes(&buf).ok_or_else(|| {
|
||||
let encoding_for_debug = String::from_utf8_lossy(&buf);
|
||||
ShellError::PluginFailedToLoad {
|
||||
msg: format!("get unsupported plugin encoding: {encoding_for_debug}"),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the definitions from the plugin file into the engine state
|
||||
#[doc(hidden)]
|
||||
pub fn load_plugin_file(
|
||||
working_set: &mut StateWorkingSet,
|
||||
plugin_registry_file: &PluginRegistryFile,
|
||||
span: Option<Span>,
|
||||
) {
|
||||
for plugin in &plugin_registry_file.plugins {
|
||||
// Any errors encountered should just be logged.
|
||||
if let Err(err) = load_plugin_registry_item(working_set, plugin, span) {
|
||||
report_error_new(working_set.permanent_state, &err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a definition from the plugin file into the engine state
|
||||
#[doc(hidden)]
|
||||
pub fn load_plugin_registry_item(
|
||||
working_set: &mut StateWorkingSet,
|
||||
plugin: &PluginRegistryItem,
|
||||
span: Option<Span>,
|
||||
) -> Result<Arc<PersistentPlugin>, ShellError> {
|
||||
let identity =
|
||||
PluginIdentity::new(plugin.filename.clone(), plugin.shell.clone()).map_err(|_| {
|
||||
ShellError::GenericError {
|
||||
error: "Invalid plugin filename in plugin registry file".into(),
|
||||
msg: "loaded from here".into(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
"the filename for `{}` is not a valid nushell plugin: {}",
|
||||
plugin.name,
|
||||
plugin.filename.display()
|
||||
)),
|
||||
inner: vec![],
|
||||
}
|
||||
})?;
|
||||
|
||||
match &plugin.data {
|
||||
PluginRegistryItemData::Valid { commands } => {
|
||||
let plugin = add_plugin_to_working_set(working_set, &identity)?;
|
||||
|
||||
// Ensure that the plugin is reset. We're going to load new signatures, so we want to
|
||||
// make sure the running plugin reflects those new signatures, and it's possible that it
|
||||
// doesn't.
|
||||
plugin.reset()?;
|
||||
|
||||
// Create the declarations from the commands
|
||||
for signature in commands {
|
||||
let decl = PluginDeclaration::new(plugin.clone(), signature.clone());
|
||||
working_set.add_decl(Box::new(decl));
|
||||
}
|
||||
Ok(plugin)
|
||||
}
|
||||
PluginRegistryItemData::Invalid => Err(ShellError::PluginRegistryDataInvalid {
|
||||
plugin_name: identity.name().to_owned(),
|
||||
span,
|
||||
add_command: identity.add_command(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn add_plugin_to_working_set(
|
||||
working_set: &mut StateWorkingSet,
|
||||
identity: &PluginIdentity,
|
||||
) -> Result<Arc<PersistentPlugin>, ShellError> {
|
||||
// Find garbage collection config for the plugin
|
||||
let gc_config = working_set
|
||||
.get_config()
|
||||
.plugin_gc
|
||||
.get(identity.name())
|
||||
.clone();
|
||||
|
||||
// Add it to / get it from the working set
|
||||
let plugin = working_set.find_or_create_plugin(identity, || {
|
||||
Arc::new(PersistentPlugin::new(identity.clone(), gc_config.clone()))
|
||||
});
|
||||
|
||||
plugin.set_gc_config(&gc_config);
|
||||
|
||||
// Downcast the plugin to `PersistentPlugin` - we generally expect this to succeed.
|
||||
// The trait object only exists so that nu-protocol can contain plugins without knowing
|
||||
// anything about their implementation, but we only use `PersistentPlugin` in practice.
|
||||
plugin
|
||||
.as_any()
|
||||
.downcast()
|
||||
.map_err(|_| ShellError::NushellFailed {
|
||||
msg: "encountered unexpected RegisteredPlugin type".into(),
|
||||
})
|
||||
}
|
||||
|
@ -1,402 +0,0 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
plugin::{PluginInterface, PluginSource},
|
||||
util::with_custom_values_in,
|
||||
};
|
||||
use nu_protocol::{ast::Operator, CustomValue, IntoSpanned, ShellError, Span, Spanned, Value};
|
||||
use nu_utils::SharedCow;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// An opaque container for a custom value that is handled fully by a plugin
|
||||
///
|
||||
/// This is the only type of custom value that is allowed to cross the plugin serialization
|
||||
/// boundary.
|
||||
///
|
||||
/// [`EngineInterface`](crate::interface::EngineInterface) is responsible for ensuring
|
||||
/// that local plugin custom values are converted to and from [`PluginCustomData`] on the boundary.
|
||||
///
|
||||
/// [`PluginInterface`](crate::interface::PluginInterface) is responsible for adding the
|
||||
/// appropriate [`PluginSource`](crate::plugin::PluginSource), ensuring that only
|
||||
/// [`PluginCustomData`] is contained within any values sent, and that the `source` of any
|
||||
/// values sent matches the plugin it is being sent to.
|
||||
///
|
||||
/// This is not a public API.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[doc(hidden)]
|
||||
pub struct PluginCustomValue {
|
||||
#[serde(flatten)]
|
||||
shared: SharedCow<SharedContent>,
|
||||
|
||||
/// Which plugin the custom value came from. This is not defined on the plugin side. The engine
|
||||
/// side is responsible for maintaining it, and it is not sent over the serialization boundary.
|
||||
#[serde(skip, default)]
|
||||
source: Option<Arc<PluginSource>>,
|
||||
}
|
||||
|
||||
/// Content shared across copies of a plugin custom value.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct SharedContent {
|
||||
/// The name of the type of the custom value as defined by the plugin (`type_name()`)
|
||||
name: String,
|
||||
/// The bincoded representation of the custom value on the plugin side
|
||||
data: Vec<u8>,
|
||||
/// True if the custom value should notify the source if all copies of it are dropped.
|
||||
///
|
||||
/// This is not serialized if `false`, since most custom values don't need it.
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
notify_on_drop: bool,
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!b
|
||||
}
|
||||
|
||||
impl PluginCustomValue {
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
Value::custom(Box::new(self), span)
|
||||
}
|
||||
}
|
||||
|
||||
#[typetag::serde]
|
||||
impl CustomValue for PluginCustomValue {
|
||||
fn clone_value(&self, span: Span) -> Value {
|
||||
self.clone().into_value(span)
|
||||
}
|
||||
|
||||
fn type_name(&self) -> String {
|
||||
self.name().to_owned()
|
||||
}
|
||||
|
||||
fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
|
||||
self.get_plugin(Some(span), "get base value")?
|
||||
.custom_value_to_base_value(self.clone().into_spanned(span))
|
||||
}
|
||||
|
||||
fn follow_path_int(
|
||||
&self,
|
||||
self_span: Span,
|
||||
index: usize,
|
||||
path_span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.get_plugin(Some(self_span), "follow cell path")?
|
||||
.custom_value_follow_path_int(
|
||||
self.clone().into_spanned(self_span),
|
||||
index.into_spanned(path_span),
|
||||
)
|
||||
}
|
||||
|
||||
fn follow_path_string(
|
||||
&self,
|
||||
self_span: Span,
|
||||
column_name: String,
|
||||
path_span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.get_plugin(Some(self_span), "follow cell path")?
|
||||
.custom_value_follow_path_string(
|
||||
self.clone().into_spanned(self_span),
|
||||
column_name.into_spanned(path_span),
|
||||
)
|
||||
}
|
||||
|
||||
fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
|
||||
self.get_plugin(Some(other.span()), "perform comparison")
|
||||
.and_then(|plugin| {
|
||||
// We're passing Span::unknown() here because we don't have one, and it probably
|
||||
// shouldn't matter here and is just a consequence of the API
|
||||
plugin.custom_value_partial_cmp(self.clone(), other.clone())
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
// We can't do anything with the error other than log it.
|
||||
log::warn!(
|
||||
"Error in partial_cmp on plugin custom value (source={source:?}): {err}",
|
||||
source = self.source
|
||||
);
|
||||
None
|
||||
})
|
||||
.map(|ordering| ordering.into())
|
||||
}
|
||||
|
||||
fn operation(
|
||||
&self,
|
||||
lhs_span: Span,
|
||||
operator: Operator,
|
||||
op_span: Span,
|
||||
right: &Value,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.get_plugin(Some(lhs_span), "invoke operator")?
|
||||
.custom_value_operation(
|
||||
self.clone().into_spanned(lhs_span),
|
||||
operator.into_spanned(op_span),
|
||||
right.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_mut_any(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginCustomValue {
|
||||
/// Create a new [`PluginCustomValue`].
|
||||
pub(crate) fn new(
|
||||
name: String,
|
||||
data: Vec<u8>,
|
||||
notify_on_drop: bool,
|
||||
source: Option<Arc<PluginSource>>,
|
||||
) -> PluginCustomValue {
|
||||
PluginCustomValue {
|
||||
shared: SharedCow::new(SharedContent {
|
||||
name,
|
||||
data,
|
||||
notify_on_drop,
|
||||
}),
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of the type of the custom value as defined by the plugin (`type_name()`)
|
||||
pub fn name(&self) -> &str {
|
||||
&self.shared.name
|
||||
}
|
||||
|
||||
/// The bincoded representation of the custom value on the plugin side
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.shared.data
|
||||
}
|
||||
|
||||
/// True if the custom value should notify the source if all copies of it are dropped.
|
||||
pub fn notify_on_drop(&self) -> bool {
|
||||
self.shared.notify_on_drop
|
||||
}
|
||||
|
||||
/// Which plugin the custom value came from. This is not defined on the plugin side. The engine
|
||||
/// side is responsible for maintaining it, and it is not sent over the serialization boundary.
|
||||
pub fn source(&self) -> &Option<Arc<PluginSource>> {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// Set the [`PluginSource`] for this [`PluginCustomValue`].
|
||||
pub fn set_source(&mut self, source: Option<Arc<PluginSource>>) {
|
||||
self.source = source;
|
||||
}
|
||||
|
||||
/// Create the [`PluginCustomValue`] with the given source.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_source(mut self, source: Option<Arc<PluginSource>>) -> PluginCustomValue {
|
||||
self.source = source;
|
||||
self
|
||||
}
|
||||
|
||||
/// Helper to get the plugin to implement an op
|
||||
fn get_plugin(&self, span: Option<Span>, for_op: &str) -> Result<PluginInterface, ShellError> {
|
||||
let wrap_err = |err: ShellError| ShellError::GenericError {
|
||||
error: format!(
|
||||
"Unable to spawn plugin `{}` to {for_op}",
|
||||
self.source
|
||||
.as_ref()
|
||||
.map(|s| s.name())
|
||||
.unwrap_or("<unknown>")
|
||||
),
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
help: None,
|
||||
inner: vec![err],
|
||||
};
|
||||
|
||||
let source = self.source.clone().ok_or_else(|| {
|
||||
wrap_err(ShellError::NushellFailed {
|
||||
msg: "The plugin source for the custom value was not set".into(),
|
||||
})
|
||||
})?;
|
||||
|
||||
source
|
||||
.persistent(span)
|
||||
.and_then(|p| p.get_plugin(None))
|
||||
.map_err(wrap_err)
|
||||
}
|
||||
|
||||
/// Serialize a custom value into a [`PluginCustomValue`]. This should only be done on the
|
||||
/// plugin side.
|
||||
pub fn serialize_from_custom_value(
|
||||
custom_value: &dyn CustomValue,
|
||||
span: Span,
|
||||
) -> Result<PluginCustomValue, ShellError> {
|
||||
let name = custom_value.type_name();
|
||||
let notify_on_drop = custom_value.notify_plugin_on_drop();
|
||||
bincode::serialize(custom_value)
|
||||
.map(|data| PluginCustomValue::new(name, data, notify_on_drop, None))
|
||||
.map_err(|err| ShellError::CustomValueFailedToEncode {
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserialize a [`PluginCustomValue`] into a `Box<dyn CustomValue>`. This should only be done
|
||||
/// on the plugin side.
|
||||
pub fn deserialize_to_custom_value(
|
||||
&self,
|
||||
span: Span,
|
||||
) -> Result<Box<dyn CustomValue>, ShellError> {
|
||||
bincode::deserialize::<Box<dyn CustomValue>>(self.data()).map_err(|err| {
|
||||
ShellError::CustomValueFailedToDecode {
|
||||
msg: err.to_string(),
|
||||
span,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Add a [`PluginSource`] to the given [`CustomValue`] if it is a [`PluginCustomValue`].
|
||||
pub fn add_source(value: &mut dyn CustomValue, source: &Arc<PluginSource>) {
|
||||
if let Some(custom_value) = value.as_mut_any().downcast_mut::<PluginCustomValue>() {
|
||||
custom_value.set_source(Some(source.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a [`PluginSource`] to all [`PluginCustomValue`]s within the value, recursively.
|
||||
pub fn add_source_in(value: &mut Value, source: &Arc<PluginSource>) -> Result<(), ShellError> {
|
||||
with_custom_values_in(value, |custom_value| {
|
||||
Self::add_source(custom_value.item, source);
|
||||
Ok::<_, ShellError>(())
|
||||
})
|
||||
}
|
||||
|
||||
/// Check that a [`CustomValue`] is a [`PluginCustomValue`] that come from the given `source`,
|
||||
/// and return an error if not.
|
||||
///
|
||||
/// This method will collapse `LazyRecord` in-place as necessary to make the guarantee,
|
||||
/// since `LazyRecord` could return something different the next time it is called.
|
||||
pub fn verify_source(
|
||||
value: Spanned<&dyn CustomValue>,
|
||||
source: &PluginSource,
|
||||
) -> Result<(), ShellError> {
|
||||
if let Some(custom_value) = value.item.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
if custom_value
|
||||
.source
|
||||
.as_ref()
|
||||
.map(|s| s.is_compatible(source))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ShellError::CustomValueIncorrectForPlugin {
|
||||
name: custom_value.name().to_owned(),
|
||||
span: value.span,
|
||||
dest_plugin: source.name().to_owned(),
|
||||
src_plugin: custom_value.source.as_ref().map(|s| s.name().to_owned()),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Only PluginCustomValues can be sent
|
||||
Err(ShellError::CustomValueIncorrectForPlugin {
|
||||
name: value.item.type_name(),
|
||||
span: value.span,
|
||||
dest_plugin: source.name().to_owned(),
|
||||
src_plugin: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert all plugin-native custom values to [`PluginCustomValue`] within the given `value`,
|
||||
/// recursively. This should only be done on the plugin side.
|
||||
pub fn serialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { ref val, .. } => {
|
||||
if val.as_any().downcast_ref::<PluginCustomValue>().is_some() {
|
||||
// Already a PluginCustomValue
|
||||
Ok(())
|
||||
} else {
|
||||
let serialized = Self::serialize_from_custom_value(&**val, span)?;
|
||||
*value = Value::custom(Box::new(serialized), span);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert all [`PluginCustomValue`]s to plugin-native custom values within the given `value`,
|
||||
/// recursively. This should only be done on the plugin side.
|
||||
pub fn deserialize_custom_values_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { ref val, .. } => {
|
||||
if let Some(val) = val.as_any().downcast_ref::<PluginCustomValue>() {
|
||||
let deserialized = val.deserialize_to_custom_value(span)?;
|
||||
*value = Value::custom(deserialized, span);
|
||||
Ok(())
|
||||
} else {
|
||||
// Already not a PluginCustomValue
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Render any custom values in the `Value` using `to_base_value()`
|
||||
pub fn render_to_base_value_in(value: &mut Value) -> Result<(), ShellError> {
|
||||
value.recurse_mut(&mut |value| {
|
||||
let span = value.span();
|
||||
match value {
|
||||
Value::Custom { ref val, .. } => {
|
||||
*value = val.to_base_value(span)?;
|
||||
Ok(())
|
||||
}
|
||||
// Collect LazyRecord before proceeding
|
||||
Value::LazyRecord { ref val, .. } => {
|
||||
*value = val.collect()?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PluginCustomValue {
|
||||
fn drop(&mut self) {
|
||||
// If the custom value specifies notify_on_drop and this is the last copy, we need to let
|
||||
// the plugin know about it if we can.
|
||||
if self.source.is_some() && self.notify_on_drop() && SharedCow::ref_count(&self.shared) == 1
|
||||
{
|
||||
self.get_plugin(None, "drop")
|
||||
// While notifying drop, we don't need a copy of the source
|
||||
.and_then(|plugin| {
|
||||
plugin.custom_value_dropped(PluginCustomValue {
|
||||
shared: self.shared.clone(),
|
||||
source: None,
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|err| {
|
||||
// We shouldn't do anything with the error except log it
|
||||
let name = self.name();
|
||||
log::warn!("Failed to notify drop of custom value ({name}): {err}")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
use crate::plugin::Encoder;
|
||||
use nu_protocol::ShellError;
|
||||
|
||||
pub mod json;
|
||||
pub mod msgpack;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum EncodingType {
|
||||
Json(json::JsonSerializer),
|
||||
MsgPack(msgpack::MsgPackSerializer),
|
||||
}
|
||||
|
||||
impl EncodingType {
|
||||
pub fn try_from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
match bytes {
|
||||
b"json" => Some(Self::Json(json::JsonSerializer {})),
|
||||
b"msgpack" => Some(Self::MsgPack(msgpack::MsgPackSerializer {})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Encoder<T> for EncodingType
|
||||
where
|
||||
json::JsonSerializer: Encoder<T>,
|
||||
msgpack::MsgPackSerializer: Encoder<T>,
|
||||
{
|
||||
fn encode(&self, data: &T, writer: &mut impl std::io::Write) -> Result<(), ShellError> {
|
||||
match self {
|
||||
EncodingType::Json(encoder) => encoder.encode(data, writer),
|
||||
EncodingType::MsgPack(encoder) => encoder.encode(data, writer),
|
||||
}
|
||||
}
|
||||
|
||||
fn decode(&self, reader: &mut impl std::io::BufRead) -> Result<Option<T>, ShellError> {
|
||||
match self {
|
||||
EncodingType::Json(encoder) => encoder.decode(reader),
|
||||
EncodingType::MsgPack(encoder) => encoder.decode(reader),
|
||||
}
|
||||
}
|
||||
}
|
15
crates/nu-plugin/src/test_util.rs
Normal file
15
crates/nu-plugin/src/test_util.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use nu_plugin_core::interface_test_util::TestCase;
|
||||
use nu_plugin_protocol::{PluginInput, PluginOutput};
|
||||
|
||||
use crate::plugin::EngineInterfaceManager;
|
||||
|
||||
pub trait TestCaseExt {
|
||||
/// Create a new [`EngineInterfaceManager`] that writes to this test case.
|
||||
fn engine(&self) -> EngineInterfaceManager;
|
||||
}
|
||||
|
||||
impl TestCaseExt for TestCase<PluginInput, PluginOutput> {
|
||||
fn engine(&self) -> EngineInterfaceManager {
|
||||
EngineInterfaceManager::new(self.clone())
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
mod mutable_cow;
|
||||
mod waitable;
|
||||
mod with_custom_values_in;
|
||||
|
||||
pub(crate) use mutable_cow::*;
|
||||
pub use waitable::*;
|
||||
pub use with_custom_values_in::*;
|
@ -16,4 +16,4 @@ bench = false
|
||||
# assumptions about the serialized format
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
interprocess = "1.2.1"
|
||||
interprocess = { workspace = true }
|
||||
|
@ -391,7 +391,7 @@ fn main() -> Result<()> {
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
if let Some(plugins) = &parsed_nu_cli_args.plugins {
|
||||
use nu_plugin::{GetPlugin, PluginDeclaration};
|
||||
use nu_plugin_engine::{GetPlugin, PluginDeclaration};
|
||||
use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity};
|
||||
|
||||
// Load any plugins specified with --plugins
|
||||
@ -409,7 +409,7 @@ fn main() -> Result<()> {
|
||||
.map_err(ShellError::from)?;
|
||||
|
||||
// Create the plugin and add it to the working set
|
||||
let plugin = nu_plugin::add_plugin_to_working_set(&mut working_set, &identity)?;
|
||||
let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
|
||||
|
||||
// Spawn the plugin to get its signatures, and then add the commands to the working set
|
||||
for signature in plugin.clone().get_plugin(None)?.get_signature()? {
|
||||
|
Loading…
Reference in New Issue
Block a user