Add CustomValue support to plugins (#6070)

* Skeleton implementation

Lots and lots of TODOs

* Bootstrap simple CustomValue plugin support test

* Create nu_plugin_custom_value

* Skeleton for nu_plugin_custom_values

* Return a custom value from plugin

* Encode CustomValues from plugin calls as PluginResponse::PluginData

* Add new PluginCall variant CollapseCustomValue

* Handle CollapseCustomValue plugin calls

* Add CallInput::Data variant to CallInfo inputs

* Handle CallInfo with CallInput::Data plugin calls

* Send CallInput::Data if Value is PluginCustomValue from plugin calls

* Remove unnecessary boxing of plugins CallInfo

* Add fields needed to collapse PluginCustomValue to it

* Document PluginCustomValue and its purpose

* Impl collapsing using plugin calls in PluginCustomValue::to_base_value

* Implement proper typetag based deserialization for CoolCustomValue

* Test demonstrating that passing back a custom value to plugin works

* Added a failing test for describing plugin CustomValues

* Support describe for PluginCustomValues

- Add name to PluginResponse::PluginData
  - Also turn it into a struct for clarity
- Add name to PluginCustomValue
- Return name field from PluginCustomValue

* Demonstrate that plugins can create and handle multiple CustomValues

* Add bincode to nu-plugin dependencies

This is for demonstration purposes, any schemaless binary seralization
format will work. I picked bincode since it's the most popular for Rust
but there are defintely better options out there for this usecase

* serde_json::Value -> Vec<u8>

* Update capnp schema for new CallInfo.input field

* Move call_input capnp serialization and deserialization into new file

* Deserialize Value's span from Value itself instead of passing call.head

I am not sure if this was correct and I am breaking it or if it was a
bug, I don't fully understand how nu creates and uses Spans. What should
reuse spans and what should recreate new ones?
But yeah it felt weird that the Value's Span was being ignored since in
the json serializer just uses the Value's Span

* Add call_info value round trip test

* Add capnp CallInput::Data serialization and deserialization support

* Add CallInfo::CollapseCustomValue to capnp schema

* Add capnp PluginCall::CollapseCustomValue serialization and deserialization support

* Add PluginResponse::PluginData to capnp schema

* Add capnp PluginResponse::PluginData serialization and deserialization support

* Switch plugins::custom_values tests to capnp

Both json and capnp would work now! Sadly I can't choose both at the
same time :(

* Add missing JsonSerializer round trip tests

* Handle plugin returning PluginData as a response to CollapseCustomValue

* Refactor plugin calling into a reusable function

Many less levels of indentation now!

* Export PluginData from nu_plugin

So plugins can create their very own serve_plugin with whatever
CustomValue behavior they may desire

* Error if CustomValue cannot be handled by Plugin
This commit is contained in:
Mathspy
2022-07-25 12:32:56 -04:00
committed by GitHub
parent 9097e865ca
commit daa2148136
23 changed files with 1944 additions and 84 deletions

View File

@ -1,13 +1,14 @@
use crate::{EncodingType, EvaluatedCall};
use super::{create_command, OUTPUT_BUFFER_SIZE};
use crate::protocol::{CallInfo, PluginCall, PluginResponse};
use std::io::BufReader;
use super::{call_plugin, create_command};
use crate::protocol::{
CallInfo, CallInput, PluginCall, PluginCustomValue, PluginData, PluginResponse,
};
use std::path::{Path, PathBuf};
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ast::Call, Signature};
use nu_protocol::{PipelineData, ShellError};
use nu_protocol::{PipelineData, ShellError, Value};
#[derive(Clone)]
pub struct PluginDeclaration {
@ -73,28 +74,41 @@ impl Command for PluginDeclaration {
})?;
let input = input.into_value(call.head);
let input = match input {
Value::CustomValue { val, span } => {
match val.as_any().downcast_ref::<PluginCustomValue>() {
Some(plugin_data) if plugin_data.filename == self.filename => {
CallInput::Data(PluginData {
data: plugin_data.data.clone(),
span,
})
}
_ => {
let custom_value_name = val.value_string();
return Err(ShellError::GenericError(
format!(
"Plugin {} can not handle the custom value {}",
self.name, custom_value_name
),
format!("custom value {}", custom_value_name),
Some(span),
None,
Vec::new(),
));
}
}
}
value => CallInput::Value(value),
};
// Create message to plugin to indicate that signature is required and
// send call to plugin asking for signature
if let Some(mut stdin_writer) = child.stdin.take() {
let encoding_clone = self.encoding.clone();
let plugin_call = PluginCall::CallInfo(Box::new(CallInfo {
name: self.name.clone(),
call: EvaluatedCall::try_from_call(call, engine_state, stack)?,
input,
}));
std::thread::spawn(move || {
// PluginCall information
encoding_clone.encode_call(&plugin_call, &mut stdin_writer)
});
}
let plugin_call = PluginCall::CallInfo(CallInfo {
name: self.name.clone(),
call: EvaluatedCall::try_from_call(call, engine_state, stack)?,
input,
});
// Deserialize response from plugin to extract the resulting value
let pipeline_data = if let Some(stdout_reader) = &mut child.stdout {
let reader = stdout_reader;
let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader);
let response = self.encoding.decode_response(&mut buf_read).map_err(|err| {
let response =
call_plugin(&mut child, plugin_call, &self.encoding, call.head).map_err(|err| {
let decl = engine_state.get_decl(call.decl_id);
ShellError::GenericError(
format!("Unable to decode call for {}", decl.name()),
@ -105,28 +119,33 @@ impl Command for PluginDeclaration {
)
});
match response {
Ok(PluginResponse::Value(value)) => {
Ok(PipelineData::Value(value.as_ref().clone(), None))
}
Ok(PluginResponse::Error(err)) => Err(err.into()),
Ok(PluginResponse::Signature(..)) => Err(ShellError::GenericError(
"Plugin missing value".into(),
"Received a signature from plugin instead of value".into(),
Some(call.head),
None,
Vec::new(),
)),
Err(err) => Err(err),
let pipeline_data = match response {
Ok(PluginResponse::Value(value)) => {
Ok(PipelineData::Value(value.as_ref().clone(), None))
}
} else {
Err(ShellError::GenericError(
"Error with stdout reader".into(),
"no stdout reader".into(),
Ok(PluginResponse::PluginData(name, plugin_data)) => Ok(PipelineData::Value(
Value::CustomValue {
val: Box::new(PluginCustomValue {
name,
data: plugin_data.data,
filename: self.filename.clone(),
shell: self.shell.clone(),
encoding: self.encoding.clone(),
source: engine_state.get_decl(call.decl_id).name().to_owned(),
}),
span: plugin_data.span,
},
None,
)),
Ok(PluginResponse::Error(err)) => Err(err.into()),
Ok(PluginResponse::Signature(..)) => Err(ShellError::GenericError(
"Plugin missing value".into(),
"Received a signature from plugin instead of value".into(),
Some(call.head),
None,
Vec::new(),
))
)),
Err(err) => Err(err),
};
// We need to call .wait() on the child, or we'll risk summoning the zombie horde

View File

@ -1,18 +1,18 @@
mod declaration;
pub use declaration::PluginDeclaration;
use crate::protocol::{LabeledError, PluginCall, PluginResponse};
use crate::protocol::{CallInput, LabeledError, PluginCall, PluginData, PluginResponse};
use crate::EncodingType;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::process::{Command as CommandSys, Stdio};
use std::process::{Child, Command as CommandSys, Stdio};
use nu_protocol::ShellError;
use nu_protocol::{CustomValue, ShellError, Span};
use nu_protocol::{Signature, Value};
use super::EvaluatedCall;
const OUTPUT_BUFFER_SIZE: usize = 8192;
pub(crate) const OUTPUT_BUFFER_SIZE: usize = 8192;
pub trait PluginEncoder: Clone {
fn encode_call(
@ -35,7 +35,7 @@ pub trait PluginEncoder: Clone {
) -> Result<PluginResponse, ShellError>;
}
fn create_command(path: &Path, shell: &Option<PathBuf>) -> CommandSys {
pub(crate) fn create_command(path: &Path, shell: &Option<PathBuf>) -> CommandSys {
let mut process = match (path.extension(), shell) {
(_, Some(shell)) => {
let mut process = std::process::Command::new(shell);
@ -77,6 +77,37 @@ fn create_command(path: &Path, shell: &Option<PathBuf>) -> CommandSys {
process
}
pub(crate) fn call_plugin(
child: &mut Child,
plugin_call: PluginCall,
encoding: &EncodingType,
span: Span,
) -> Result<PluginResponse, ShellError> {
if let Some(mut stdin_writer) = child.stdin.take() {
let encoding_clone = encoding.clone();
std::thread::spawn(move || {
// PluginCall information
encoding_clone.encode_call(&plugin_call, &mut stdin_writer)
});
}
// Deserialize response from plugin to extract the resulting value
if let Some(stdout_reader) = &mut child.stdout {
let reader = stdout_reader;
let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader);
encoding.decode_response(&mut buf_read)
} else {
Err(ShellError::GenericError(
"Error with stdout reader".into(),
"no stdout reader".into(),
Some(span),
None,
Vec::new(),
))
}
}
pub fn get_signature(
path: &Path,
encoding: &EncodingType,
@ -172,9 +203,33 @@ pub fn serve_plugin(plugin: &mut impl Plugin, encoder: impl PluginEncoder) {
.expect("Error encoding response");
}
PluginCall::CallInfo(call_info) => {
let value = plugin.run(&call_info.name, &call_info.call, &call_info.input);
let input = match call_info.input {
CallInput::Value(value) => Ok(value),
CallInput::Data(plugin_data) => {
bincode::deserialize::<Box<dyn CustomValue>>(&plugin_data.data)
.map(|custom_value| Value::CustomValue {
val: custom_value,
span: plugin_data.span,
})
.map_err(|err| ShellError::PluginFailedToDecode(err.to_string()))
}
};
let value = match input {
Ok(input) => plugin.run(&call_info.name, &call_info.call, &input),
Err(err) => Err(err.into()),
};
let response = match value {
Ok(Value::CustomValue { val, span }) => match bincode::serialize(&val) {
Ok(data) => {
let name = val.value_string();
PluginResponse::PluginData(name, PluginData { data, span })
}
Err(err) => PluginResponse::Error(
ShellError::PluginFailedToEncode(err.to_string()).into(),
),
},
Ok(value) => PluginResponse::Value(Box::new(value)),
Err(err) => PluginResponse::Error(err),
};
@ -182,6 +237,18 @@ pub fn serve_plugin(plugin: &mut impl Plugin, encoder: impl PluginEncoder) {
.encode_response(&response, &mut std::io::stdout())
.expect("Error encoding response");
}
PluginCall::CollapseCustomValue(plugin_data) => {
let response = bincode::deserialize::<Box<dyn CustomValue>>(&plugin_data.data)
.map_err(|err| ShellError::PluginFailedToDecode(err.to_string()))
.and_then(|val| val.to_base_value(plugin_data.span))
.map(Box::new)
.map_err(LabeledError::from)
.map_or_else(PluginResponse::Error, PluginResponse::Value);
encoder
.encode_response(&response, &mut std::io::stdout())
.expect("Error encoding response");
}
}
}
}