Plugin json (#474)

* json encoder

* thread to pass messages

* description for example
This commit is contained in:
Fernando Herrera
2021-12-12 11:50:35 +00:00
committed by GitHub
parent f8e6620e48
commit 4d7dd23779
30 changed files with 1010 additions and 523 deletions

View File

@ -0,0 +1,137 @@
use crate::{EncodingType, EvaluatedCall};
use super::{create_command, OUTPUT_BUFFER_SIZE};
use crate::protocol::{CallInfo, PluginCall, PluginResponse};
use std::io::BufReader;
use std::path::{Path, PathBuf};
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{ast::Call, Signature, Value};
use nu_protocol::{PipelineData, ShellError};
#[derive(Clone)]
pub struct PluginDeclaration {
name: String,
signature: Signature,
filename: PathBuf,
encoding: EncodingType,
}
impl PluginDeclaration {
pub fn new(filename: PathBuf, signature: Signature, encoding: EncodingType) -> Self {
Self {
name: signature.name.clone(),
signature,
filename,
encoding,
}
}
}
impl Command for PluginDeclaration {
fn name(&self) -> &str {
&self.name
}
fn signature(&self) -> Signature {
self.signature.clone()
}
fn usage(&self) -> &str {
self.signature.usage.as_str()
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
// Call the command with self path
// Decode information from plugin
// Create PipelineData
let source_file = Path::new(&self.filename);
let mut plugin_cmd = create_command(source_file);
let mut child = plugin_cmd.spawn().map_err(|err| {
let decl = engine_state.get_decl(call.decl_id);
ShellError::SpannedLabeledError(
format!("Unable to spawn plugin for {}", decl.name()),
format!("{}", err),
call.head,
)
})?;
let input = match input {
PipelineData::Value(value, ..) => value,
PipelineData::Stream(stream, ..) => {
let values = stream.collect::<Vec<Value>>();
Value::List {
vals: values,
span: call.head,
}
}
};
// 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)
});
}
// 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 decl = engine_state.get_decl(call.decl_id);
ShellError::SpannedLabeledError(
format!("Unable to decode call for {}", decl.name()),
err.to_string(),
call.head,
)
})?;
match response {
PluginResponse::Value(value) => {
Ok(PipelineData::Value(value.as_ref().clone(), None))
}
PluginResponse::Error(err) => Err(err.into()),
PluginResponse::Signature(..) => Err(ShellError::SpannedLabeledError(
"Plugin missing value".into(),
"Received a signature from plugin instead of value".into(),
call.head,
)),
}
} else {
Err(ShellError::SpannedLabeledError(
"Error with stdout reader".into(),
"no stdout reader".into(),
call.head,
))
}?;
// There is no need to wait for the child process to finish
// The response has been collected from the plugin call
Ok(pipeline_data)
}
fn is_plugin(&self) -> Option<(&PathBuf, &str)> {
Some((&self.filename, self.encoding.to_str()))
}
}

View File

@ -0,0 +1,161 @@
mod declaration;
pub use declaration::PluginDeclaration;
use crate::protocol::{LabeledError, PluginCall, PluginResponse};
use crate::EncodingType;
use std::io::BufReader;
use std::path::Path;
use std::process::{Command as CommandSys, Stdio};
use nu_protocol::ShellError;
use nu_protocol::{Signature, Value};
use super::EvaluatedCall;
const OUTPUT_BUFFER_SIZE: usize = 8192;
pub trait PluginEncoder: Clone {
fn encode_call(
&self,
plugin_call: &PluginCall,
writer: &mut impl std::io::Write,
) -> Result<(), ShellError>;
fn decode_call(&self, reader: &mut impl std::io::BufRead) -> Result<PluginCall, ShellError>;
fn encode_response(
&self,
plugin_response: &PluginResponse,
writer: &mut impl std::io::Write,
) -> Result<(), ShellError>;
fn decode_response(
&self,
reader: &mut impl std::io::BufRead,
) -> Result<PluginResponse, ShellError>;
}
fn create_command(path: &Path) -> CommandSys {
//TODO. The selection of shell could be modifiable from the config file.
let mut process = if cfg!(windows) {
let mut process = CommandSys::new("cmd");
process.arg("/c").arg(path);
process
} else {
let mut process = CommandSys::new("sh");
process.arg("-c").arg(path);
process
};
// Both stdout and stdin are piped so we can receive information from the plugin
process.stdout(Stdio::piped()).stdin(Stdio::piped());
process
}
pub fn get_signature(path: &Path, encoding: &EncodingType) -> Result<Vec<Signature>, ShellError> {
let mut plugin_cmd = create_command(path);
let mut child = plugin_cmd.spawn().map_err(|err| {
ShellError::PluginFailedToLoad(format!("Error spawning child process: {}", err))
})?;
// 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 = encoding.clone();
std::thread::spawn(move || {
encoding_clone.encode_call(&PluginCall::Signature, &mut stdin_writer)
});
}
// deserialize response from plugin to extract the signature
let signatures = 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 = encoding.decode_response(&mut buf_read)?;
match response {
PluginResponse::Signature(sign) => Ok(sign),
PluginResponse::Error(err) => Err(err.into()),
_ => Err(ShellError::PluginFailedToLoad(
"Plugin missing signature".into(),
)),
}
} else {
Err(ShellError::PluginFailedToLoad(
"Plugin missing stdout reader".into(),
))
}?;
// There is no need to wait for the child process to finish since the
// signature has being collected
Ok(signatures)
}
// The next trait and functions are part of the plugin that is being created
// The `Plugin` trait defines the API which plugins use to "hook" into nushell.
pub trait Plugin {
fn signature(&self) -> Vec<Signature>;
fn run(
&mut self,
name: &str,
call: &EvaluatedCall,
input: &Value,
) -> Result<Value, LabeledError>;
}
// Function used in the plugin definition for the communication protocol between
// nushell and the external plugin.
// When creating a new plugin you have to use this function as the main
// entry point for the plugin, e.g.
//
// fn main() {
// serve_plugin(plugin)
// }
//
// where plugin is your struct that implements the Plugin trait
//
// Note. When defining a plugin in other language but Rust, you will have to compile
// the plugin.capnp schema to create the object definitions that will be returned from
// the plugin.
// The object that is expected to be received by nushell is the PluginResponse struct.
// That should be encoded correctly and sent to StdOut for nushell to decode and
// and present its result
pub fn serve_plugin(plugin: &mut impl Plugin, encoder: impl PluginEncoder) {
let mut stdin_buf = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, std::io::stdin());
let plugin_call = encoder.decode_call(&mut stdin_buf);
match plugin_call {
Err(err) => {
let response = PluginResponse::Error(err.into());
encoder
.encode_response(&response, &mut std::io::stdout())
.expect("Error encoding response");
}
Ok(plugin_call) => {
match plugin_call {
// Sending the signature back to nushell to create the declaration definition
PluginCall::Signature => {
let response = PluginResponse::Signature(plugin.signature());
encoder
.encode_response(&response, &mut std::io::stdout())
.expect("Error encoding response");
}
PluginCall::CallInfo(call_info) => {
let value = plugin.run(&call_info.name, &call_info.call, &call_info.input);
let response = match value {
Ok(value) => PluginResponse::Value(Box::new(value)),
Err(err) => PluginResponse::Error(err),
};
encoder
.encode_response(&response, &mut std::io::stdout())
.expect("Error encoding response");
}
}
}
}
}