Files
nushell/crates/nu-plugin/src/plugin/mod.rs
WindSoilder 055edd886d Make plugin commands support examples. (#7984)
# Description

As title, we can't provide examples for plugin commands, this pr would
make it possible


# User-Facing Changes

Take plugin `nu-example-1` as example:
```
❯ nu-example-1 -h
PluginSignature test 1 for plugin. Returns Value::Nothing

Usage:
  > nu-example-1 {flags} <a> <b> (opt) ...(rest)

Flags:
  -h, --help - Display the help message for this command
  -f, --flag - a flag for the signature
  -n, --named <String> - named string

Parameters:
  a <int>: required integer value
  b <string>: required string value
  (optional) opt <int>: Optional number
  ...rest <string>: rest value string

Examples:
  running example with an int value and string value
  > nu-example-1 3 bb
```

The examples session is newly added.

## Basic idea behind these changes
when nushell query plugin signatures, plugin just returns it's signature
without any examples, so nushell have no idea about the examples of
plugin commands.
To adding the feature, we just making plugin returns it's signature with
examples.

Before:
```
        1. get signature
         ----------------> 
Nushell ------------------  Plugin
        <-----------------
        2. returns Vec<Signature>
```

After:
```
        1. get signature
        ----------------> 
Nushell ------------------  Plugin
        <-----------------
        2. returns Vec<PluginSignature>
```
        
When writing plugin signature to $nu.plugin-path:
Serialize `<PluginSignature>` rather than `<Signature>`, which would
enable us to serialize examples to `$nu.plugin-path`

## Shortcoming
It's a breaking changes because `Plugin::signature` is changed, and it
requires plugin authors to change their code for new signatures.

Fortunally it should be easy to change, for rust based plugin, we just
need to make a global replace from word `Signature` to word
`PluginSignature` in their plugin project.

Our content of plugin-path is really large, if one plugin have many
examples, it'd results to larger body of $nu.plugin-path, which is not
really scale. A solution would be save register information in other
binary formats rather than `json`. But I think it'd be another story.

# Tests + Formatting

Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A
clippy::needless_collect` to check that you're using the standard code
style
- `cargo test --workspace` to check that all tests pass

# After Submitting

If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
2023-02-08 16:14:18 -06:00

392 lines
14 KiB
Rust

mod declaration;
pub use declaration::PluginDeclaration;
use nu_engine::documentation::get_flags_section;
use std::collections::HashMap;
use crate::protocol::{CallInput, LabeledError, PluginCall, PluginData, PluginResponse};
use crate::EncodingType;
use std::env;
use std::fmt::Write;
use std::io::{BufReader, ErrorKind, Read, Write as WriteTrait};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdout, Command as CommandSys, Stdio};
use nu_protocol::{CustomValue, PluginSignature, ShellError, Span, Value};
use super::EvaluatedCall;
pub(crate) const OUTPUT_BUFFER_SIZE: usize = 8192;
pub trait PluginEncoder: Clone {
fn name(&self) -> &str;
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>;
}
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);
process.arg(path);
process
}
(Some(extension), None) => {
let (shell, separator) = match extension.to_str() {
Some("cmd") | Some("bat") => (Some("cmd"), Some("/c")),
Some("sh") => (Some("sh"), Some("-c")),
Some("py") => (Some("python"), None),
_ => (None, None),
};
match (shell, separator) {
(Some(shell), Some(separator)) => {
let mut process = std::process::Command::new(shell);
process.arg(separator);
process.arg(path);
process
}
(Some(shell), None) => {
let mut process = std::process::Command::new(shell);
process.arg(path);
process
}
_ => std::process::Command::new(path),
}
}
(None, None) => std::process::Command::new(path),
};
// Both stdout and stdin are piped so we can receive information from the plugin
process.stdout(Stdio::piped()).stdin(Stdio::piped());
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();
// If the child process fills its stdout buffer, it may end up waiting until the parent
// reads the stdout, and not be able to read stdin in the meantime, causing a deadlock.
// Writing from another thread ensures that stdout is being read at the same time, avoiding the problem.
std::thread::spawn(move || 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,
shell: &Option<PathBuf>,
current_envs: &HashMap<String, String>,
) -> Result<Vec<PluginSignature>, ShellError> {
let mut plugin_cmd = create_command(path, shell);
let program_name = plugin_cmd.get_program().to_os_string().into_string();
plugin_cmd.envs(current_envs);
let mut child = plugin_cmd.spawn().map_err(|err| {
let error_msg = match err.kind() {
ErrorKind::NotFound => match program_name {
Ok(prog_name) => {
format!("Can't find {prog_name}, please make sure that {prog_name} is in PATH.")
}
_ => {
format!("Error spawning child process: {err}")
}
},
_ => {
format!("Error spawning child process: {err}")
}
};
ShellError::PluginFailedToLoad(error_msg)
})?;
let mut stdin_writer = child
.stdin
.take()
.ok_or_else(|| ShellError::PluginFailedToLoad("plugin missing stdin writer".into()))?;
let mut stdout_reader = child
.stdout
.take()
.ok_or_else(|| ShellError::PluginFailedToLoad("Plugin missing stdout reader".into()))?;
let encoding = get_plugin_encoding(&mut stdout_reader)?;
// Create message to plugin to indicate that signature is required and
// send call to plugin asking for signature
let encoding_clone = encoding.clone();
// If the child process fills its stdout buffer, it may end up waiting until the parent
// reads the stdout, and not be able to read stdin in the meantime, causing a deadlock.
// Writing from another thread ensures that stdout is being read at the same time, avoiding the problem.
std::thread::spawn(move || {
encoding_clone.encode_call(&PluginCall::Signature, &mut stdin_writer)
});
// deserialize response from plugin to extract the signature
let reader = stdout_reader;
let mut buf_read = BufReader::with_capacity(OUTPUT_BUFFER_SIZE, reader);
let response = encoding.decode_response(&mut buf_read)?;
let signatures = match response {
PluginResponse::Signature(sign) => Ok(sign),
PluginResponse::Error(err) => Err(err.into()),
_ => Err(ShellError::PluginFailedToLoad(
"Plugin missing signature".into(),
)),
}?;
match child.wait() {
Ok(_) => Ok(signatures),
Err(err) => Err(ShellError::PluginFailedToLoad(format!("{err}"))),
}
}
// 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<PluginSignature>;
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) {
if env::args().any(|arg| (arg == "-h") || (arg == "--help")) {
print_help(plugin, encoder);
std::process::exit(0)
}
// tell nushell encoding.
//
// 1 byte
// encoding format: | content-length | content |
{
let mut stdout = std::io::stdout();
let encoding = encoder.name();
let length = encoding.len() as u8;
let mut encoding_content: Vec<u8> = encoding.as_bytes().to_vec();
encoding_content.insert(0, length);
stdout
.write_all(&encoding_content)
.expect("Failed to tell nushell my encoding");
stdout
.flush()
.expect("Failed to tell nushell my encoding when flushing stdout");
}
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 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),
};
encoder
.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");
}
}
}
}
}
fn print_help(plugin: &mut impl Plugin, encoder: impl PluginEncoder) {
println!("Nushell Plugin");
println!("Encoder: {}", encoder.name());
let mut help = String::new();
plugin.signature().iter().for_each(|signature| {
let res = write!(help, "\nCommand: {}", signature.sig.name)
.and_then(|_| writeln!(help, "\nUsage:\n > {}", signature.sig.usage))
.and_then(|_| {
if !signature.sig.extra_usage.is_empty() {
writeln!(help, "\nExtra usage:\n > {}", signature.sig.extra_usage)
} else {
Ok(())
}
})
.and_then(|_| {
let flags = get_flags_section(&signature.sig);
write!(help, "{flags}")
})
.and_then(|_| writeln!(help, "\nParameters:"))
.and_then(|_| {
signature
.sig
.required_positional
.iter()
.try_for_each(|positional| {
writeln!(
help,
" {} <{}>: {}",
positional.name, positional.shape, positional.desc
)
})
})
.and_then(|_| {
signature
.sig
.optional_positional
.iter()
.try_for_each(|positional| {
writeln!(
help,
" (optional) {} <{}>: {}",
positional.name, positional.shape, positional.desc
)
})
})
.and_then(|_| {
if let Some(rest_positional) = &signature.sig.rest_positional {
writeln!(
help,
" ...{} <{}>: {}",
rest_positional.name, rest_positional.shape, rest_positional.desc
)
} else {
Ok(())
}
})
.and_then(|_| writeln!(help, "======================"));
if res.is_err() {
println!("{res:?}")
}
});
println!("{help}")
}
pub fn get_plugin_encoding(child_stdout: &mut ChildStdout) -> Result<EncodingType, ShellError> {
let mut length_buf = [0u8; 1];
child_stdout.read_exact(&mut length_buf).map_err(|e| {
ShellError::PluginFailedToLoad(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(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(format!(
"get unsupported plugin encoding: {encoding_for_debug}"
))
})
}