Allow plugins to report their own version and store it in the registry (#12883)

# Description

This allows plugins to report their version (and potentially other
metadata in the future). The version is shown in `plugin list` and in
`version`.

The metadata is stored in the registry file, and reflects whatever was
retrieved on `plugin add`, not necessarily the running binary. This can
help you to diagnose if there's some kind of mismatch with what you
expect. We could potentially use this functionality to show a warning or
error if a plugin being run does not have the same version as what was
in the cache file, suggesting `plugin add` be run again, but I haven't
done that at this point.

It is optional, and it requires the plugin author to make some code
changes if they want to provide it, since I can't automatically
determine the version of the calling crate or anything tricky like that
to do it.

Example:

```
> plugin list | select name version is_running pid
╭───┬────────────────┬─────────┬────────────┬─────╮
│ # │      name      │ version │ is_running │ pid │
├───┼────────────────┼─────────┼────────────┼─────┤
│ 0 │ example        │ 0.93.1  │ false      │     │
│ 1 │ gstat          │ 0.93.1  │ false      │     │
│ 2 │ inc            │ 0.93.1  │ false      │     │
│ 3 │ python_example │ 0.1.0   │ false      │     │
╰───┴────────────────┴─────────┴────────────┴─────╯
```

cc @maxim-uvarov (he asked for it)

# User-Facing Changes

- `plugin list` gets a `version` column
- `version` shows plugin versions when available
- plugin authors *should* add `fn metadata()` to their `impl Plugin`,
but don't have to

# Tests + Formatting

Tested the low level stuff and also the `plugin list` column.

# After Submitting
- [ ] update plugin guide docs
- [ ] update plugin protocol docs (`Metadata` call & response)
- [ ] update plugin template (`fn metadata()` should be easy)
- [ ] release notes
This commit is contained in:
Devyn Cairns
2024-06-21 04:27:09 -07:00
committed by GitHub
parent dd8f8861ed
commit 91d44f15c1
38 changed files with 360 additions and 46 deletions

View File

@ -24,6 +24,10 @@
//! struct MyCommand;
//!
//! impl Plugin for MyPlugin {
//! fn version(&self) -> String {
//! env!("CARGO_PKG_VERSION").into()
//! }
//!
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
//! vec![Box::new(MyCommand)]
//! }

View File

@ -60,6 +60,9 @@ use crate::{EngineInterface, EvaluatedCall, Plugin};
/// }
///
/// # impl Plugin for LowercasePlugin {
/// # fn version(&self) -> String {
/// # "0.0.0".into()
/// # }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Lowercase)]
/// # }
@ -195,6 +198,9 @@ pub trait PluginCommand: Sync {
/// }
///
/// # impl Plugin for HelloPlugin {
/// # fn version(&self) -> String {
/// # "0.0.0".into()
/// # }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Hello)]
/// # }

View File

@ -11,8 +11,8 @@ use nu_plugin_protocol::{
ProtocolInfo,
};
use nu_protocol::{
engine::Closure, Config, LabeledError, PipelineData, PluginSignature, ShellError, Span,
Spanned, Value,
engine::Closure, Config, LabeledError, PipelineData, PluginMetadata, PluginSignature,
ShellError, Span, Spanned, Value,
};
use std::{
collections::{btree_map, BTreeMap, HashMap},
@ -29,6 +29,9 @@ use std::{
#[derive(Debug)]
#[doc(hidden)]
pub enum ReceivedPluginCall {
Metadata {
engine: EngineInterface,
},
Signature {
engine: EngineInterface,
},
@ -280,8 +283,11 @@ impl InterfaceManager for EngineInterfaceManager {
}
};
match call {
// We just let the receiver handle it rather than trying to store signature here
// or something
// Ask the plugin for metadata
PluginCall::Metadata => {
self.send_plugin_call(ReceivedPluginCall::Metadata { engine: interface })
}
// Ask the plugin for signatures
PluginCall::Signature => {
self.send_plugin_call(ReceivedPluginCall::Signature { engine: interface })
}
@ -416,6 +422,13 @@ impl EngineInterface {
}
}
/// Write a call response of plugin metadata.
pub(crate) fn write_metadata(&self, metadata: PluginMetadata) -> Result<(), ShellError> {
let response = PluginCallResponse::Metadata(metadata);
self.write(PluginOutput::CallResponse(self.context()?, response))?;
self.flush()
}
/// Write a call response of plugin signatures.
///
/// Any custom values in the examples will be rendered using `to_base_value()`.

View File

@ -322,6 +322,26 @@ fn manager_consume_goodbye_closes_plugin_call_channel() -> Result<(), ShellError
Ok(())
}
#[test]
fn manager_consume_call_metadata_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();
set_default_protocol_info(&mut manager)?;
let rx = manager
.take_plugin_call_receiver()
.expect("couldn't take receiver");
manager.consume(PluginInput::Call(0, PluginCall::Metadata))?;
match rx.try_recv().expect("call was not forwarded to receiver") {
ReceivedPluginCall::Metadata { engine } => {
assert_eq!(Some(0), engine.context);
Ok(())
}
call => panic!("wrong call type: {call:?}"),
}
}
#[test]
fn manager_consume_call_signature_forwards_to_receiver_with_context() -> Result<(), ShellError> {
let mut manager = TestCase::new().engine();

View File

@ -16,7 +16,8 @@ use nu_plugin_core::{
};
use nu_plugin_protocol::{CallInfo, CustomValueOp, PluginCustomValue, PluginInput, PluginOutput};
use nu_protocol::{
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, ShellError, Spanned, Value,
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginMetadata,
ShellError, Spanned, Value,
};
use thiserror::Error;
@ -52,6 +53,10 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// struct Hello;
///
/// impl Plugin for HelloPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
///
/// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// vec![Box::new(Hello)]
/// }
@ -89,6 +94,23 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384;
/// # }
/// ```
pub trait Plugin: Sync {
/// The version of the plugin.
///
/// The recommended implementation, which will use the version from your crate's `Cargo.toml`
/// file:
///
/// ```no_run
/// # use nu_plugin::{Plugin, PluginCommand};
/// # struct MyPlugin;
/// # impl Plugin for MyPlugin {
/// fn version(&self) -> String {
/// env!("CARGO_PKG_VERSION").into()
/// }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> { vec![] }
/// # }
/// ```
fn version(&self) -> String;
/// The commands supported by the plugin
///
/// Each [`PluginCommand`] contains both the signature of the command and the functionality it
@ -216,6 +238,7 @@ pub trait Plugin: Sync {
/// # struct MyPlugin;
/// # impl MyPlugin { fn new() -> Self { Self }}
/// # impl Plugin for MyPlugin {
/// # fn version(&self) -> String { "0.0.0".into() }
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();}
/// # }
/// fn main() {
@ -504,6 +527,12 @@ where
}
match plugin_call {
// Send metadata back to nushell so it can be stored with the plugin signatures
ReceivedPluginCall::Metadata { engine } => {
engine
.write_metadata(PluginMetadata::new().with_version(plugin.version()))
.try_to_report(&engine)?;
}
// Sending the signature back to nushell to create the declaration definition
ReceivedPluginCall::Signature { engine } => {
let sigs = commands