mirror of
https://github.com/nushell/nushell
synced 2024-12-28 14:03:09 +00:00
plugin call function
This commit is contained in:
parent
a390f66dbf
commit
43c3cfecf7
8 changed files with 3059 additions and 39 deletions
|
@ -1,4 +1,4 @@
|
||||||
use nu_plugin::plugin::get_signature;
|
use nu_plugin::plugin::{get_signature, PluginDeclaration};
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Block, Call, Expr, Expression, ImportPatternMember, Pipeline, Statement},
|
ast::{Block, Call, Expr, Expression, ImportPatternMember, Pipeline, Statement},
|
||||||
engine::StateWorkingSet,
|
engine::StateWorkingSet,
|
||||||
|
@ -887,12 +887,21 @@ pub fn parse_plugin(
|
||||||
if let Ok(filename) = String::from_utf8(name_expr.to_vec()) {
|
if let Ok(filename) = String::from_utf8(name_expr.to_vec()) {
|
||||||
let source_file = Path::new(&filename);
|
let source_file = Path::new(&filename);
|
||||||
|
|
||||||
// get signature from plugin
|
if source_file.exists() & source_file.is_file() {
|
||||||
// create plugin command declaration (need struct impl Command)
|
// get signature from plugin
|
||||||
// store declaration in working set
|
match get_signature(source_file) {
|
||||||
match get_signature(source_file) {
|
Err(err) => Some(ParseError::PluginError(format!("{}", err))),
|
||||||
Err(err) => Some(ParseError::PluginError(format!("{}", err))),
|
Ok(signature) => {
|
||||||
Ok(_signature) => None,
|
// create plugin command declaration (need struct impl Command)
|
||||||
|
// store declaration in working set
|
||||||
|
let plugin_decl = PluginDeclaration::new(filename, signature);
|
||||||
|
working_set.add_decl(Box::new(plugin_decl));
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Some(ParseError::FileNotFound(filename))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Some(ParseError::NonUtf8(spans[1]))
|
Some(ParseError::NonUtf8(spans[1]))
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
fn main() {
|
|
||||||
capnpc::CompilerCommand::new()
|
|
||||||
.src_prefix("schema")
|
|
||||||
.file("schema/plugin.capnp")
|
|
||||||
.run()
|
|
||||||
.expect("compiling schema");
|
|
||||||
}
|
|
|
@ -4,6 +4,8 @@
|
||||||
# This schema, together with the command capnp proto is used to generate
|
# This schema, together with the command capnp proto is used to generate
|
||||||
# the rust file that defines the serialization/deserialization objects
|
# the rust file that defines the serialization/deserialization objects
|
||||||
# required to comunicate with the plugins created for nushell
|
# required to comunicate with the plugins created for nushell
|
||||||
|
# If you modify the schema remember to compile it to generate the corresponding
|
||||||
|
# rust file and place that file into the main nu-plugin folder
|
||||||
|
|
||||||
# Generic structs used as helpers for the encoding
|
# Generic structs used as helpers for the encoding
|
||||||
struct Option(T) {
|
struct Option(T) {
|
||||||
|
@ -121,7 +123,8 @@ struct PluginCall {
|
||||||
|
|
||||||
struct PluginResponse {
|
struct PluginResponse {
|
||||||
union {
|
union {
|
||||||
signature @0 :Signature;
|
error @0 :Text;
|
||||||
value @1 :Value;
|
signature @1 :Signature;
|
||||||
|
value @2 :Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
pub mod plugin_call;
|
pub mod plugin_call;
|
||||||
|
pub mod plugin_capnp;
|
||||||
pub mod serializers;
|
pub mod serializers;
|
||||||
|
|
||||||
pub mod plugin_capnp {
|
pub use plugin::{serve_plugin, Plugin};
|
||||||
include!(concat!(env!("OUT_DIR"), "/plugin_capnp.rs"));
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
use std::process::{Command, Stdio};
|
use crate::plugin_call::{self, decode_call, encode_response};
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::process::{Command as CommandSys, Stdio};
|
||||||
use std::{fmt::Display, path::Path};
|
use std::{fmt::Display, path::Path};
|
||||||
|
|
||||||
|
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||||
use nu_protocol::{ast::Call, Signature, Value};
|
use nu_protocol::{ast::Call, Signature, Value};
|
||||||
|
use nu_protocol::{PipelineData, ShellError};
|
||||||
|
|
||||||
//use nu_protocol::{ShellError, Value};
|
const OUTPUT_BUFFER_SIZE: usize = 8192;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CallInfo {
|
pub struct CallInfo {
|
||||||
|
@ -21,16 +25,15 @@ pub enum PluginCall {
|
||||||
// Information received from the plugin
|
// Information received from the plugin
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PluginResponse {
|
pub enum PluginResponse {
|
||||||
|
Error(String),
|
||||||
Signature(Box<Signature>),
|
Signature(Box<Signature>),
|
||||||
Value(Box<Value>),
|
Value(Box<Value>),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `Plugin` trait defines the API which plugins may use to "hook" into nushell.
|
|
||||||
pub trait Plugin {}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PluginError {
|
pub enum PluginError {
|
||||||
MissingSignature,
|
MissingSignature,
|
||||||
|
UnableToGetStdout,
|
||||||
UnableToSpawn(String),
|
UnableToSpawn(String),
|
||||||
EncodingError(String),
|
EncodingError(String),
|
||||||
DecodingError(String),
|
DecodingError(String),
|
||||||
|
@ -40,6 +43,7 @@ impl Display for PluginError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
PluginError::MissingSignature => write!(f, "missing signature in plugin"),
|
PluginError::MissingSignature => write!(f, "missing signature in plugin"),
|
||||||
|
PluginError::UnableToGetStdout => write!(f, "couldn't get stdout from child process"),
|
||||||
PluginError::UnableToSpawn(err) => {
|
PluginError::UnableToSpawn(err) => {
|
||||||
write!(f, "error in spawned child process: {}", err)
|
write!(f, "error in spawned child process: {}", err)
|
||||||
}
|
}
|
||||||
|
@ -53,7 +57,7 @@ impl Display for PluginError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_signature(path: &Path) -> Result<Signature, PluginError> {
|
pub fn get_signature(path: &Path) -> Result<Box<Signature>, PluginError> {
|
||||||
let mut plugin_cmd = create_command(path);
|
let mut plugin_cmd = create_command(path);
|
||||||
|
|
||||||
// Both stdout and stdin are piped so we can get the information from the plugin
|
// Both stdout and stdin are piped so we can get the information from the plugin
|
||||||
|
@ -63,29 +67,196 @@ pub fn get_signature(path: &Path) -> Result<Signature, PluginError> {
|
||||||
match plugin_cmd.spawn() {
|
match plugin_cmd.spawn() {
|
||||||
Err(err) => Err(PluginError::UnableToSpawn(format!("{}", err))),
|
Err(err) => Err(PluginError::UnableToSpawn(format!("{}", err))),
|
||||||
Ok(mut child) => {
|
Ok(mut child) => {
|
||||||
// create message to plugin to indicate signature
|
// Create message to plugin to indicate that signature is required and
|
||||||
// send message to plugin
|
// send call to plugin asking for signature
|
||||||
// deserialize message with signature
|
if let Some(mut stdin_writer) = child.stdin.take() {
|
||||||
|
plugin_call::encode_call(&PluginCall::Signature, &mut stdin_writer)?
|
||||||
|
}
|
||||||
|
|
||||||
|
// deserialize response from plugin to extract the signature
|
||||||
|
let signature = if let Some(stdout_reader) = child.stdout.take() {
|
||||||
|
let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, stdout_reader);
|
||||||
|
let response = plugin_call::decode_response(&mut buf_read)?;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
PluginResponse::Signature(sign) => Ok(sign),
|
||||||
|
PluginResponse::Error(msg) => Err(PluginError::DecodingError(msg)),
|
||||||
|
_ => Err(PluginError::DecodingError("signature not found".into())),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(PluginError::UnableToGetStdout)
|
||||||
|
}?;
|
||||||
|
|
||||||
match child.wait() {
|
match child.wait() {
|
||||||
Err(err) => Err(PluginError::UnableToSpawn(format!("{}", err))),
|
Err(err) => Err(PluginError::UnableToSpawn(format!("{}", err))),
|
||||||
Ok(_) => Ok(Signature::build("testing")),
|
Ok(_) => Ok(signature),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_command(path: &Path) -> Command {
|
fn create_command(path: &Path) -> CommandSys {
|
||||||
//TODO. The selection of shell could be modifiable from the config file.
|
//TODO. The selection of shell could be modifiable from the config file.
|
||||||
if cfg!(windows) {
|
if cfg!(windows) {
|
||||||
let mut process = Command::new("cmd");
|
let mut process = CommandSys::new("cmd");
|
||||||
process.arg("/c");
|
process.arg("/c");
|
||||||
process.arg(path);
|
process.arg(path);
|
||||||
|
|
||||||
process
|
process
|
||||||
} else {
|
} else {
|
||||||
let mut process = Command::new("sh");
|
let mut process = CommandSys::new("sh");
|
||||||
process.arg("-c").arg(path);
|
process.arg("-c").arg(path);
|
||||||
|
|
||||||
process
|
process
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PluginDeclaration {
|
||||||
|
name: String,
|
||||||
|
signature: Box<Signature>,
|
||||||
|
filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PluginDeclaration {
|
||||||
|
pub fn new(filename: String, signature: Box<Signature>) -> Self {
|
||||||
|
Self {
|
||||||
|
name: signature.name.clone(),
|
||||||
|
signature,
|
||||||
|
filename,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Command for PluginDeclaration {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> Signature {
|
||||||
|
self.signature.as_ref().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"plugin name plus arguments"
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Both stdout and stdin are piped so we can get the information from the plugin
|
||||||
|
plugin_cmd.stdout(Stdio::piped());
|
||||||
|
plugin_cmd.stdin(Stdio::piped());
|
||||||
|
|
||||||
|
match plugin_cmd.spawn() {
|
||||||
|
Err(err) => Err(ShellError::PluginError(format!("{}", err))),
|
||||||
|
Ok(mut child) => {
|
||||||
|
let input = match input {
|
||||||
|
PipelineData::Value(value) => value,
|
||||||
|
PipelineData::Stream(stream) => {
|
||||||
|
let values = stream.collect::<Vec<Value>>();
|
||||||
|
|
||||||
|
Value::List {
|
||||||
|
vals: values,
|
||||||
|
span: call.head,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// PluginCall information
|
||||||
|
let plugin_call = PluginCall::CallInfo(Box::new(CallInfo {
|
||||||
|
call: call.clone(),
|
||||||
|
input,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 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() {
|
||||||
|
plugin_call::encode_call(&plugin_call, &mut stdin_writer)
|
||||||
|
.map_err(|err| ShellError::PluginError(err.to_string()))?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deserialize response from plugin to extract the resulting value
|
||||||
|
let signature = if let Some(stdout_reader) = child.stdout.take() {
|
||||||
|
let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, stdout_reader);
|
||||||
|
let response = plugin_call::decode_response(&mut buf_read)
|
||||||
|
.map_err(|err| ShellError::PluginError(err.to_string()))?;
|
||||||
|
|
||||||
|
match response {
|
||||||
|
PluginResponse::Value(value) => {
|
||||||
|
Ok(PipelineData::Value(value.as_ref().clone()))
|
||||||
|
}
|
||||||
|
PluginResponse::Error(msg) => Err(PluginError::DecodingError(msg)),
|
||||||
|
_ => Err(PluginError::DecodingError(
|
||||||
|
"result value from plugin not found".into(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(PluginError::UnableToGetStdout)
|
||||||
|
}
|
||||||
|
.map_err(|err| ShellError::PluginError(err.to_string()))?;
|
||||||
|
|
||||||
|
match child.wait() {
|
||||||
|
Err(err) => Err(ShellError::PluginError(format!("{}", err))),
|
||||||
|
Ok(_) => Ok(signature),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_plugin(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `Plugin` trait defines the API which plugins use to "hook" into nushell.
|
||||||
|
pub trait Plugin {
|
||||||
|
fn signature(&self) -> Signature;
|
||||||
|
fn run(&self, call: &Call, input: &Value) -> Result<Value, PluginError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function used in the plugin definition for the communication protocol between
|
||||||
|
// nushell and the external plugin.
|
||||||
|
// If you want to create a new plugin you have to use this function as the main
|
||||||
|
// entry point for the plugin
|
||||||
|
pub fn serve_plugin(plugin: &mut impl Plugin) {
|
||||||
|
let mut stdin_buf = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, std::io::stdin());
|
||||||
|
let plugin_call = decode_call(&mut stdin_buf);
|
||||||
|
|
||||||
|
match plugin_call {
|
||||||
|
Err(err) => {
|
||||||
|
let response = PluginResponse::Error(err.to_string());
|
||||||
|
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(Box::new(plugin.signature()));
|
||||||
|
encode_response(&response, &mut std::io::stdout())
|
||||||
|
.expect("Error encoding response");
|
||||||
|
}
|
||||||
|
PluginCall::CallInfo(call_info) => {
|
||||||
|
let value = plugin.run(&call_info.call, &call_info.input);
|
||||||
|
|
||||||
|
let response = match value {
|
||||||
|
Ok(value) => PluginResponse::Value(Box::new(value)),
|
||||||
|
Err(err) => PluginResponse::Error(err.to_string()),
|
||||||
|
};
|
||||||
|
encode_response(&response, &mut std::io::stdout())
|
||||||
|
.expect("Error encoding response");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -81,6 +81,7 @@ pub fn encode_response(
|
||||||
let mut builder = message.init_root::<plugin_response::Builder>();
|
let mut builder = message.init_root::<plugin_response::Builder>();
|
||||||
|
|
||||||
match &plugin_response {
|
match &plugin_response {
|
||||||
|
PluginResponse::Error(msg) => builder.reborrow().set_error(msg.as_str()),
|
||||||
PluginResponse::Signature(sign) => {
|
PluginResponse::Signature(sign) => {
|
||||||
let signature_builder = builder.reborrow().init_signature();
|
let signature_builder = builder.reborrow().init_signature();
|
||||||
signature::serialize_signature(sign, signature_builder)
|
signature::serialize_signature(sign, signature_builder)
|
||||||
|
@ -105,6 +106,11 @@ pub fn decode_response(reader: &mut impl std::io::BufRead) -> Result<PluginRespo
|
||||||
|
|
||||||
match reader.which() {
|
match reader.which() {
|
||||||
Err(capnp::NotInSchema(_)) => Err(PluginError::DecodingError("value not in schema".into())),
|
Err(capnp::NotInSchema(_)) => Err(PluginError::DecodingError("value not in schema".into())),
|
||||||
|
Ok(plugin_response::Error(reader)) => {
|
||||||
|
let msg = reader.map_err(|e| PluginError::DecodingError(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(PluginResponse::Error(msg.to_string()))
|
||||||
|
}
|
||||||
Ok(plugin_response::Signature(reader)) => {
|
Ok(plugin_response::Signature(reader)) => {
|
||||||
let reader = reader.map_err(|e| PluginError::DecodingError(e.to_string()))?;
|
let reader = reader.map_err(|e| PluginError::DecodingError(e.to_string()))?;
|
||||||
let sign = signature::deserialize_signature(reader)
|
let sign = signature::deserialize_signature(reader)
|
||||||
|
@ -253,6 +259,7 @@ mod tests {
|
||||||
decode_response(&mut buffer.as_slice()).expect("unable to deserialize message");
|
decode_response(&mut buffer.as_slice()).expect("unable to deserialize message");
|
||||||
|
|
||||||
match returned {
|
match returned {
|
||||||
|
PluginResponse::Error(_) => panic!("returned wrong call type"),
|
||||||
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
||||||
PluginResponse::Signature(returned_signature) => {
|
PluginResponse::Signature(returned_signature) => {
|
||||||
assert_eq!(signature.name, returned_signature.name);
|
assert_eq!(signature.name, returned_signature.name);
|
||||||
|
@ -301,10 +308,28 @@ mod tests {
|
||||||
decode_response(&mut buffer.as_slice()).expect("unable to deserialize message");
|
decode_response(&mut buffer.as_slice()).expect("unable to deserialize message");
|
||||||
|
|
||||||
match returned {
|
match returned {
|
||||||
|
PluginResponse::Error(_) => panic!("returned wrong call type"),
|
||||||
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
||||||
PluginResponse::Value(returned_value) => {
|
PluginResponse::Value(returned_value) => {
|
||||||
assert_eq!(&value, returned_value.as_ref())
|
assert_eq!(&value, returned_value.as_ref())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_round_trip_error() {
|
||||||
|
let message = "some error".to_string();
|
||||||
|
let response = PluginResponse::Error(message.clone());
|
||||||
|
|
||||||
|
let mut buffer: Vec<u8> = Vec::new();
|
||||||
|
encode_response(&response, &mut buffer).expect("unable to serialize message");
|
||||||
|
let returned =
|
||||||
|
decode_response(&mut buffer.as_slice()).expect("unable to deserialize message");
|
||||||
|
|
||||||
|
match returned {
|
||||||
|
PluginResponse::Error(msg) => assert_eq!(message, msg),
|
||||||
|
PluginResponse::Signature(_) => panic!("returned wrong call type"),
|
||||||
|
PluginResponse::Value(_) => panic!("returned wrong call type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
2825
crates/nu-plugin/src/plugin_capnp.rs
Normal file
2825
crates/nu-plugin/src/plugin_capnp.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -170,13 +170,8 @@ pub enum ShellError {
|
||||||
#[error("No file to be copied")]
|
#[error("No file to be copied")]
|
||||||
NoFileToBeCopied(),
|
NoFileToBeCopied(),
|
||||||
|
|
||||||
#[error("Unable to serialize message")]
|
#[error("Plugin error")]
|
||||||
#[diagnostic(code(nu::shell::EncodingError), url(docsrs))]
|
PluginError(String),
|
||||||
EncodingError(String),
|
|
||||||
|
|
||||||
#[error("Unable to read message")]
|
|
||||||
#[diagnostic(code(nu::shell::DecodingError), url(docsrs))]
|
|
||||||
DecodingError(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for ShellError {
|
impl From<std::io::Error> for ShellError {
|
||||||
|
|
Loading…
Reference in a new issue