mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 11:05:40 +02:00
Plugin json (#474)
* json encoder * thread to pass messages * description for example
This commit is contained in:
137
crates/nu-plugin/src/plugin/declaration.rs
Normal file
137
crates/nu-plugin/src/plugin/declaration.rs
Normal 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()))
|
||||
}
|
||||
}
|
161
crates/nu-plugin/src/plugin/mod.rs
Normal file
161
crates/nu-plugin/src/plugin/mod.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user