mirror of
https://github.com/nushell/nushell.git
synced 2024-11-25 01:43:47 +01:00
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:
parent
dd8f8861ed
commit
91d44f15c1
@ -344,7 +344,10 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
|
||||
name: identity.name().to_owned(),
|
||||
filename: identity.filename().to_owned(),
|
||||
shell: identity.shell().map(|p| p.to_owned()),
|
||||
data: PluginRegistryItemData::Valid { commands },
|
||||
data: PluginRegistryItemData::Valid {
|
||||
metadata: Default::default(),
|
||||
commands,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -116,11 +116,18 @@ pub fn version(engine_state: &EngineState, span: Span) -> Result<PipelineData, S
|
||||
Value::string(features_enabled().join(", "), span),
|
||||
);
|
||||
|
||||
// Get a list of plugin names
|
||||
// Get a list of plugin names and versions if present
|
||||
let installed_plugins = engine_state
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|x| x.identity().name())
|
||||
.map(|x| {
|
||||
let name = x.identity().name();
|
||||
if let Some(version) = x.metadata().and_then(|m| m.version) {
|
||||
format!("{name} {version}")
|
||||
} else {
|
||||
name.into()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
record.push(
|
||||
|
@ -118,11 +118,12 @@ apparent the next time `nu` is next launched with that plugin registry file.
|
||||
},
|
||||
));
|
||||
let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
|
||||
let metadata = interface.get_metadata()?;
|
||||
let commands = interface.get_signature()?;
|
||||
|
||||
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
||||
// Update the file with the received signatures
|
||||
let item = PluginRegistryItem::new(plugin.identity(), commands);
|
||||
// Update the file with the received metadata and signatures
|
||||
let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
|
||||
contents.upsert_plugin(item);
|
||||
Ok(())
|
||||
})?;
|
||||
|
@ -16,6 +16,7 @@ impl Command for PluginList {
|
||||
Type::Table(
|
||||
[
|
||||
("name".into(), Type::String),
|
||||
("version".into(), Type::String),
|
||||
("is_running".into(), Type::Bool),
|
||||
("pid".into(), Type::Int),
|
||||
("filename".into(), Type::String),
|
||||
@ -43,6 +44,7 @@ impl Command for PluginList {
|
||||
description: "List installed plugins.",
|
||||
result: Some(Value::test_list(vec![Value::test_record(record! {
|
||||
"name" => Value::test_string("inc"),
|
||||
"version" => Value::test_string(env!("CARGO_PKG_VERSION")),
|
||||
"is_running" => Value::test_bool(true),
|
||||
"pid" => Value::test_int(106480),
|
||||
"filename" => if cfg!(windows) {
|
||||
@ -98,8 +100,15 @@ impl Command for PluginList {
|
||||
.map(|s| Value::string(s.to_string_lossy(), head))
|
||||
.unwrap_or(Value::nothing(head));
|
||||
|
||||
let metadata = plugin.metadata();
|
||||
let version = metadata
|
||||
.and_then(|m| m.version)
|
||||
.map(|s| Value::string(s, head))
|
||||
.unwrap_or(Value::nothing(head));
|
||||
|
||||
let record = record! {
|
||||
"name" => Value::string(plugin.identity().name(), head),
|
||||
"version" => version,
|
||||
"is_running" => Value::bool(plugin.is_running(), head),
|
||||
"pid" => pid,
|
||||
"filename" => Value::string(plugin.identity().filename().to_string_lossy(), head),
|
||||
|
@ -3740,28 +3740,37 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
|
||||
)
|
||||
})?;
|
||||
|
||||
let signatures = plugin
|
||||
let metadata_and_signatures = plugin
|
||||
.clone()
|
||||
.get(get_envs)
|
||||
.and_then(|p| p.get_signature())
|
||||
.and_then(|p| {
|
||||
let meta = p.get_metadata()?;
|
||||
let sigs = p.get_signature()?;
|
||||
Ok((meta, sigs))
|
||||
})
|
||||
.map_err(|err| {
|
||||
log::warn!("Error getting signatures: {err:?}");
|
||||
log::warn!("Error getting metadata and signatures: {err:?}");
|
||||
ParseError::LabeledError(
|
||||
"Error getting signatures".into(),
|
||||
"Error getting metadata and signatures".into(),
|
||||
err.to_string(),
|
||||
spans[0],
|
||||
)
|
||||
});
|
||||
|
||||
if let Ok(ref signatures) = signatures {
|
||||
match metadata_and_signatures {
|
||||
Ok((meta, sigs)) => {
|
||||
// Set the metadata on the plugin
|
||||
plugin.set_metadata(Some(meta.clone()));
|
||||
// Add the loaded plugin to the delta
|
||||
working_set.update_plugin_registry(PluginRegistryItem::new(
|
||||
&identity,
|
||||
signatures.clone(),
|
||||
meta,
|
||||
sigs.clone(),
|
||||
));
|
||||
Ok(sigs)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
|
||||
signatures
|
||||
},
|
||||
|sig| sig.map(|sig| vec![sig]),
|
||||
)?;
|
||||
|
@ -252,7 +252,7 @@ pub fn load_plugin_registry_item(
|
||||
})?;
|
||||
|
||||
match &plugin.data {
|
||||
PluginRegistryItemData::Valid { commands } => {
|
||||
PluginRegistryItemData::Valid { metadata, commands } => {
|
||||
let plugin = add_plugin_to_working_set(working_set, &identity)?;
|
||||
|
||||
// Ensure that the plugin is reset. We're going to load new signatures, so we want to
|
||||
@ -260,6 +260,9 @@ pub fn load_plugin_registry_item(
|
||||
// doesn't.
|
||||
plugin.reset()?;
|
||||
|
||||
// Set the plugin metadata from the file
|
||||
plugin.set_metadata(Some(metadata.clone()));
|
||||
|
||||
// Create the declarations from the commands
|
||||
for signature in commands {
|
||||
let decl = PluginDeclaration::new(plugin.clone(), signature.clone());
|
||||
|
@ -11,8 +11,8 @@ use nu_plugin_protocol::{
|
||||
PluginOutput, ProtocolInfo, StreamId, StreamMessage,
|
||||
};
|
||||
use nu_protocol::{
|
||||
ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Span,
|
||||
Spanned, Value,
|
||||
ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginMetadata, PluginSignature,
|
||||
ShellError, Span, Spanned, Value,
|
||||
};
|
||||
use std::{
|
||||
collections::{btree_map, BTreeMap},
|
||||
@ -716,6 +716,7 @@ impl PluginInterface {
|
||||
|
||||
// Convert the call into one with a header and handle the stream, if necessary
|
||||
let (call, writer) = match call {
|
||||
PluginCall::Metadata => (PluginCall::Metadata, Default::default()),
|
||||
PluginCall::Signature => (PluginCall::Signature, Default::default()),
|
||||
PluginCall::CustomValueOp(value, op) => {
|
||||
(PluginCall::CustomValueOp(value, op), Default::default())
|
||||
@ -913,6 +914,17 @@ impl PluginInterface {
|
||||
self.receive_plugin_call_response(result.receiver, context, result.state)
|
||||
}
|
||||
|
||||
/// Get the metadata from the plugin.
|
||||
pub fn get_metadata(&self) -> Result<PluginMetadata, ShellError> {
|
||||
match self.plugin_call(PluginCall::Metadata, None)? {
|
||||
PluginCallResponse::Metadata(meta) => Ok(meta),
|
||||
PluginCallResponse::Error(err) => Err(err.into()),
|
||||
_ => Err(ShellError::PluginFailedToDecode {
|
||||
msg: "Received unexpected response to plugin Metadata call".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the command signatures from the plugin.
|
||||
pub fn get_signature(&self) -> Result<Vec<PluginSignature>, ShellError> {
|
||||
match self.plugin_call(PluginCall::Signature, None)? {
|
||||
@ -1206,6 +1218,7 @@ impl CurrentCallState {
|
||||
source: &PluginSource,
|
||||
) -> Result<(), ShellError> {
|
||||
match call {
|
||||
PluginCall::Metadata => Ok(()),
|
||||
PluginCall::Signature => Ok(()),
|
||||
PluginCall::Run(CallInfo { call, .. }) => self.prepare_call_args(call, source),
|
||||
PluginCall::CustomValueOp(_, op) => {
|
||||
|
@ -18,7 +18,7 @@ use nu_protocol::{
|
||||
ast::{Math, Operator},
|
||||
engine::Closure,
|
||||
ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData,
|
||||
PluginSignature, ShellError, Span, Spanned, Value,
|
||||
PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@ -1019,6 +1019,25 @@ fn start_fake_plugin_call_responder(
|
||||
.expect("failed to spawn thread");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interface_get_metadata() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
let manager = test.plugin("test");
|
||||
let interface = manager.get_interface();
|
||||
|
||||
start_fake_plugin_call_responder(manager, 1, |_| {
|
||||
vec![ReceivedPluginCallMessage::Response(
|
||||
PluginCallResponse::Metadata(PluginMetadata::new().with_version("test")),
|
||||
)]
|
||||
});
|
||||
|
||||
let metadata = interface.get_metadata()?;
|
||||
|
||||
assert_eq!(Some("test"), metadata.version.as_deref());
|
||||
assert!(test.has_unconsumed_write());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interface_get_signature() -> Result<(), ShellError> {
|
||||
let test = TestCase::new();
|
||||
|
@ -7,7 +7,7 @@ use super::{PluginInterface, PluginSource};
|
||||
use nu_plugin_core::CommunicationMode;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
|
||||
PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
@ -31,6 +31,8 @@ pub struct PersistentPlugin {
|
||||
struct MutableState {
|
||||
/// Reference to the plugin if running
|
||||
running: Option<RunningPlugin>,
|
||||
/// Metadata for the plugin, e.g. version.
|
||||
metadata: Option<PluginMetadata>,
|
||||
/// Plugin's preferred communication mode (if known)
|
||||
preferred_mode: Option<PreferredCommunicationMode>,
|
||||
/// Garbage collector config
|
||||
@ -59,6 +61,7 @@ impl PersistentPlugin {
|
||||
identity,
|
||||
mutable: Mutex::new(MutableState {
|
||||
running: None,
|
||||
metadata: None,
|
||||
preferred_mode: None,
|
||||
gc_config,
|
||||
}),
|
||||
@ -268,6 +271,16 @@ impl RegisteredPlugin for PersistentPlugin {
|
||||
self.stop_internal(true)
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<PluginMetadata> {
|
||||
self.mutable.lock().ok().and_then(|m| m.metadata.clone())
|
||||
}
|
||||
|
||||
fn set_metadata(&self, metadata: Option<PluginMetadata>) {
|
||||
if let Ok(mut mutable) = self.mutable.lock() {
|
||||
mutable.metadata = metadata;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_gc_config(&self, gc_config: &PluginGcConfig) {
|
||||
if let Ok(mut mutable) = self.mutable.lock() {
|
||||
// Save the new config for future calls
|
||||
|
@ -23,7 +23,7 @@ pub mod test_util;
|
||||
|
||||
use nu_protocol::{
|
||||
ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData,
|
||||
PluginSignature, ShellError, Span, Spanned, Value,
|
||||
PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
@ -119,6 +119,7 @@ pub struct ByteStreamInfo {
|
||||
/// Calls that a plugin can execute. The type parameter determines the input type.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum PluginCall<D> {
|
||||
Metadata,
|
||||
Signature,
|
||||
Run(CallInfo<D>),
|
||||
CustomValueOp(Spanned<PluginCustomValue>, CustomValueOp),
|
||||
@ -132,6 +133,7 @@ impl<D> PluginCall<D> {
|
||||
f: impl FnOnce(D) -> Result<T, ShellError>,
|
||||
) -> Result<PluginCall<T>, ShellError> {
|
||||
Ok(match self {
|
||||
PluginCall::Metadata => PluginCall::Metadata,
|
||||
PluginCall::Signature => PluginCall::Signature,
|
||||
PluginCall::Run(call) => PluginCall::Run(call.map_data(f)?),
|
||||
PluginCall::CustomValueOp(custom_value, op) => {
|
||||
@ -143,6 +145,7 @@ impl<D> PluginCall<D> {
|
||||
/// The span associated with the call.
|
||||
pub fn span(&self) -> Option<Span> {
|
||||
match self {
|
||||
PluginCall::Metadata => None,
|
||||
PluginCall::Signature => None,
|
||||
PluginCall::Run(CallInfo { call, .. }) => Some(call.head),
|
||||
PluginCall::CustomValueOp(val, _) => Some(val.span),
|
||||
@ -309,6 +312,7 @@ pub enum StreamMessage {
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum PluginCallResponse<D> {
|
||||
Error(LabeledError),
|
||||
Metadata(PluginMetadata),
|
||||
Signature(Vec<PluginSignature>),
|
||||
Ordering(Option<Ordering>),
|
||||
PipelineData(D),
|
||||
@ -323,6 +327,7 @@ impl<D> PluginCallResponse<D> {
|
||||
) -> Result<PluginCallResponse<T>, ShellError> {
|
||||
Ok(match self {
|
||||
PluginCallResponse::Error(err) => PluginCallResponse::Error(err),
|
||||
PluginCallResponse::Metadata(meta) => PluginCallResponse::Metadata(meta),
|
||||
PluginCallResponse::Signature(sigs) => PluginCallResponse::Signature(sigs),
|
||||
PluginCallResponse::Ordering(ordering) => PluginCallResponse::Ordering(ordering),
|
||||
PluginCallResponse::PipelineData(input) => PluginCallResponse::PipelineData(f(input)?),
|
||||
|
@ -6,7 +6,7 @@ use std::{
|
||||
use nu_plugin_engine::{GetPlugin, PluginInterface};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
PluginGcConfig, PluginIdentity, RegisteredPlugin, ShellError,
|
||||
PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, ShellError,
|
||||
};
|
||||
|
||||
pub struct FakePersistentPlugin {
|
||||
@ -42,6 +42,12 @@ impl RegisteredPlugin for FakePersistentPlugin {
|
||||
None
|
||||
}
|
||||
|
||||
fn metadata(&self) -> Option<PluginMetadata> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_metadata(&self, _metadata: Option<PluginMetadata>) {}
|
||||
|
||||
fn set_gc_config(&self, _gc_config: &PluginGcConfig) {
|
||||
// We don't have a GC
|
||||
}
|
||||
|
@ -66,6 +66,10 @@
|
||||
//! }
|
||||
//!
|
||||
//! impl Plugin for LowercasePlugin {
|
||||
//! fn version(&self) -> String {
|
||||
//! env!("CARGO_PKG_VERSION").into()
|
||||
//! }
|
||||
//!
|
||||
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
|
||||
//! vec![Box::new(Lowercase)]
|
||||
//! }
|
||||
|
@ -53,6 +53,10 @@ struct IntoU32;
|
||||
struct IntoIntFromU32;
|
||||
|
||||
impl Plugin for CustomU32Plugin {
|
||||
fn version(&self) -> String {
|
||||
"0.0.0".into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn nu_plugin::PluginCommand<Plugin = Self>>> {
|
||||
vec![Box::new(IntoU32), Box::new(IntoIntFromU32)]
|
||||
}
|
||||
|
@ -8,6 +8,10 @@ struct HelloPlugin;
|
||||
struct Hello;
|
||||
|
||||
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)]
|
||||
}
|
||||
|
@ -59,6 +59,10 @@ impl PluginCommand for Lowercase {
|
||||
}
|
||||
|
||||
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)]
|
||||
}
|
||||
|
@ -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)]
|
||||
//! }
|
||||
|
@ -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)]
|
||||
/// # }
|
||||
|
@ -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()`.
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
38
crates/nu-protocol/src/plugin/metadata.rs
Normal file
38
crates/nu-protocol/src/plugin/metadata.rs
Normal file
@ -0,0 +1,38 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Metadata about the installed plugin. This is cached in the registry file along with the
|
||||
/// signatures. None of the metadata fields are required, and more may be added in the future.
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct PluginMetadata {
|
||||
/// The version of the plugin itself, as self-reported.
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
impl PluginMetadata {
|
||||
/// Create empty metadata.
|
||||
pub const fn new() -> PluginMetadata {
|
||||
PluginMetadata { version: None }
|
||||
}
|
||||
|
||||
/// Set the version of the plugin on the metadata. A suggested way to construct this is:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use nu_protocol::PluginMetadata;
|
||||
/// # fn example() -> PluginMetadata {
|
||||
/// PluginMetadata::new().with_version(env!("CARGO_PKG_VERSION"))
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// which will use the version of your plugin's crate from its `Cargo.toml` file.
|
||||
pub fn with_version(mut self, version: impl Into<String>) -> Self {
|
||||
self.version = Some(version.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PluginMetadata {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
mod identity;
|
||||
mod metadata;
|
||||
mod registered;
|
||||
mod registry_file;
|
||||
mod signature;
|
||||
|
||||
pub use identity::*;
|
||||
pub use metadata::*;
|
||||
pub use registered::*;
|
||||
pub use registry_file::*;
|
||||
pub use signature::*;
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use crate::{PluginGcConfig, PluginIdentity, ShellError};
|
||||
use crate::{PluginGcConfig, PluginIdentity, PluginMetadata, ShellError};
|
||||
|
||||
/// Trait for plugins registered in the [`EngineState`](crate::engine::EngineState).
|
||||
pub trait RegisteredPlugin: Send + Sync {
|
||||
@ -13,6 +13,12 @@ pub trait RegisteredPlugin: Send + Sync {
|
||||
/// Process ID of the plugin executable, if running.
|
||||
fn pid(&self) -> Option<u32>;
|
||||
|
||||
/// Get metadata for the plugin, if set.
|
||||
fn metadata(&self) -> Option<PluginMetadata>;
|
||||
|
||||
/// Set metadata for the plugin.
|
||||
fn set_metadata(&self, metadata: Option<PluginMetadata>);
|
||||
|
||||
/// Set garbage collection config for the plugin.
|
||||
fn set_gc_config(&self, gc_config: &PluginGcConfig);
|
||||
|
||||
|
@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{PluginIdentity, PluginSignature, ShellError, Span};
|
||||
use crate::{PluginIdentity, PluginMetadata, PluginSignature, ShellError, Span};
|
||||
|
||||
// This has a big impact on performance
|
||||
const BUFFER_SIZE: usize = 65536;
|
||||
@ -121,9 +121,10 @@ pub struct PluginRegistryItem {
|
||||
}
|
||||
|
||||
impl PluginRegistryItem {
|
||||
/// Create a [`PluginRegistryItem`] from an identity and signatures.
|
||||
/// Create a [`PluginRegistryItem`] from an identity, metadata, and signatures.
|
||||
pub fn new(
|
||||
identity: &PluginIdentity,
|
||||
metadata: PluginMetadata,
|
||||
mut commands: Vec<PluginSignature>,
|
||||
) -> PluginRegistryItem {
|
||||
// Sort the commands for consistency
|
||||
@ -133,7 +134,7 @@ impl PluginRegistryItem {
|
||||
name: identity.name().to_owned(),
|
||||
filename: identity.filename().to_owned(),
|
||||
shell: identity.shell().map(|p| p.to_owned()),
|
||||
data: PluginRegistryItemData::Valid { commands },
|
||||
data: PluginRegistryItemData::Valid { metadata, commands },
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -144,6 +145,9 @@ impl PluginRegistryItem {
|
||||
#[serde(untagged)]
|
||||
pub enum PluginRegistryItemData {
|
||||
Valid {
|
||||
/// Metadata for the plugin, including its version.
|
||||
#[serde(default)]
|
||||
metadata: PluginMetadata,
|
||||
/// Signatures and examples for each command provided by the plugin.
|
||||
commands: Vec<PluginSignature>,
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
use super::{PluginRegistryFile, PluginRegistryItem, PluginRegistryItemData};
|
||||
use crate::{
|
||||
Category, PluginExample, PluginSignature, ShellError, Signature, SyntaxShape, Type, Value,
|
||||
Category, PluginExample, PluginMetadata, PluginSignature, ShellError, Signature, SyntaxShape,
|
||||
Type, Value,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io::Cursor;
|
||||
@ -11,6 +12,9 @@ fn foo_plugin() -> PluginRegistryItem {
|
||||
filename: "/path/to/nu_plugin_foo".into(),
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid {
|
||||
metadata: PluginMetadata {
|
||||
version: Some("0.1.0".into()),
|
||||
},
|
||||
commands: vec![PluginSignature {
|
||||
sig: Signature::new("foo")
|
||||
.input_output_type(Type::Int, Type::List(Box::new(Type::Int)))
|
||||
@ -36,6 +40,9 @@ fn bar_plugin() -> PluginRegistryItem {
|
||||
filename: "/path/to/nu_plugin_bar".into(),
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid {
|
||||
metadata: PluginMetadata {
|
||||
version: Some("0.2.0".into()),
|
||||
},
|
||||
commands: vec![PluginSignature {
|
||||
sig: Signature::new("bar")
|
||||
.usage("overwrites files with random data")
|
||||
|
@ -42,6 +42,10 @@ impl CustomValuePlugin {
|
||||
}
|
||||
|
||||
impl Plugin for CustomValuePlugin {
|
||||
fn version(&self) -> String {
|
||||
env!("CARGO_PKG_VERSION").into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
vec![
|
||||
Box::new(Generate),
|
||||
|
@ -7,6 +7,10 @@ pub use commands::*;
|
||||
pub use example::ExamplePlugin;
|
||||
|
||||
impl Plugin for ExamplePlugin {
|
||||
fn version(&self) -> String {
|
||||
env!("CARGO_PKG_VERSION").into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
// This is a list of all of the commands you would like Nu to register when your plugin is
|
||||
// loaded.
|
||||
|
@ -10,6 +10,10 @@ pub use from::vcf::FromVcf;
|
||||
pub struct FromCmds;
|
||||
|
||||
impl Plugin for FromCmds {
|
||||
fn version(&self) -> String {
|
||||
env!("CARGO_PKG_VERSION").into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
vec![
|
||||
Box::new(FromEml),
|
||||
|
@ -5,6 +5,10 @@ use nu_protocol::{Category, LabeledError, Signature, Spanned, SyntaxShape, Value
|
||||
pub struct GStatPlugin;
|
||||
|
||||
impl Plugin for GStatPlugin {
|
||||
fn version(&self) -> String {
|
||||
env!("CARGO_PKG_VERSION").into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
vec![Box::new(GStat)]
|
||||
}
|
||||
|
@ -5,6 +5,10 @@ use nu_protocol::{ast::CellPath, LabeledError, Signature, SyntaxShape, Value};
|
||||
pub struct IncPlugin;
|
||||
|
||||
impl Plugin for IncPlugin {
|
||||
fn version(&self) -> String {
|
||||
env!("CARGO_PKG_VERSION").into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
vec![Box::new(Inc::new())]
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
# language without adding any extra dependencies to our tests.
|
||||
|
||||
const NUSHELL_VERSION = "0.94.3"
|
||||
const PLUGIN_VERSION = "0.1.0" # bump if you change commands!
|
||||
|
||||
def main [--stdio] {
|
||||
if ($stdio) {
|
||||
@ -229,6 +230,13 @@ def handle_input []: any -> nothing {
|
||||
}
|
||||
{ Call: [$id, $plugin_call] } => {
|
||||
match $plugin_call {
|
||||
"Metadata" => {
|
||||
write_response $id {
|
||||
Metadata: {
|
||||
version: $PLUGIN_VERSION
|
||||
}
|
||||
}
|
||||
}
|
||||
"Signature" => {
|
||||
write_response $id { Signature: $SIGNATURES }
|
||||
}
|
||||
|
@ -99,6 +99,10 @@ pub struct PolarsPlugin {
|
||||
}
|
||||
|
||||
impl Plugin for PolarsPlugin {
|
||||
fn version(&self) -> String {
|
||||
env!("CARGO_PKG_VERSION").into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
let mut commands: Vec<Box<dyn PluginCommand<Plugin = Self>>> = vec![Box::new(PolarsCmd)];
|
||||
commands.append(&mut eager_commands());
|
||||
|
@ -28,6 +28,7 @@ import json
|
||||
|
||||
|
||||
NUSHELL_VERSION = "0.94.3"
|
||||
PLUGIN_VERSION = "0.1.0" # bump if you change commands!
|
||||
|
||||
|
||||
def signatures():
|
||||
@ -228,7 +229,13 @@ def handle_input(input):
|
||||
exit(0)
|
||||
elif "Call" in input:
|
||||
[id, plugin_call] = input["Call"]
|
||||
if plugin_call == "Signature":
|
||||
if plugin_call == "Metadata":
|
||||
write_response(id, {
|
||||
"Metadata": {
|
||||
"version": PLUGIN_VERSION,
|
||||
}
|
||||
})
|
||||
elif plugin_call == "Signature":
|
||||
write_response(id, signatures())
|
||||
elif "Run" in plugin_call:
|
||||
process_call(id, plugin_call["Run"])
|
||||
|
@ -16,6 +16,10 @@ impl Query {
|
||||
}
|
||||
|
||||
impl Plugin for Query {
|
||||
fn version(&self) -> String {
|
||||
env!("CARGO_PKG_VERSION").into()
|
||||
}
|
||||
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
vec![
|
||||
Box::new(QueryCommand),
|
||||
|
@ -136,7 +136,21 @@ fn handle_message(
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
if let Some(plugin_call) = message.get("Call") {
|
||||
let (id, plugin_call) = (&plugin_call[0], &plugin_call[1]);
|
||||
if plugin_call.as_str() == Some("Signature") {
|
||||
if plugin_call.as_str() == Some("Metadata") {
|
||||
write(
|
||||
output,
|
||||
&json!({
|
||||
"CallResponse": [
|
||||
id,
|
||||
{
|
||||
"Metadata": {
|
||||
"version": env!("CARGO_PKG_VERSION"),
|
||||
}
|
||||
}
|
||||
]
|
||||
}),
|
||||
)
|
||||
} else if plugin_call.as_str() == Some("Signature") {
|
||||
write(
|
||||
output,
|
||||
&json!({
|
||||
|
12
src/main.rs
12
src/main.rs
@ -400,7 +400,7 @@ fn main() -> Result<()> {
|
||||
#[cfg(feature = "plugin")]
|
||||
if let Some(plugins) = &parsed_nu_cli_args.plugins {
|
||||
use nu_plugin_engine::{GetPlugin, PluginDeclaration};
|
||||
use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity};
|
||||
use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity, RegisteredPlugin};
|
||||
|
||||
// Load any plugins specified with --plugins
|
||||
start_time = std::time::Instant::now();
|
||||
@ -419,8 +419,14 @@ fn main() -> Result<()> {
|
||||
// Create the plugin and add it to the working set
|
||||
let plugin = nu_plugin_engine::add_plugin_to_working_set(&mut working_set, &identity)?;
|
||||
|
||||
// Spawn the plugin to get its signatures, and then add the commands to the working set
|
||||
for signature in plugin.clone().get_plugin(None)?.get_signature()? {
|
||||
// Spawn the plugin to get the metadata and signatures
|
||||
let interface = plugin.clone().get_plugin(None)?;
|
||||
|
||||
// Set its metadata
|
||||
plugin.set_metadata(Some(interface.get_metadata()?));
|
||||
|
||||
// Add the commands from the signature to the working set
|
||||
for signature in interface.get_signature()? {
|
||||
let decl = PluginDeclaration::new(plugin.clone(), signature);
|
||||
working_set.add_decl(Box::new(decl));
|
||||
}
|
||||
|
@ -16,6 +16,17 @@ fn plugin_list_shows_installed_plugins() {
|
||||
assert!(out.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_list_shows_installed_plugin_version() {
|
||||
let out = nu_with_plugins!(
|
||||
cwd: ".",
|
||||
plugin: ("nu_plugin_inc"),
|
||||
r#"(plugin list).version.0"#
|
||||
);
|
||||
assert_eq!(env!("CARGO_PKG_VERSION"), out.out);
|
||||
assert!(out.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_keeps_running_after_calling_it() {
|
||||
let out = nu_with_plugins!(
|
||||
|
@ -18,6 +18,13 @@ fn example_plugin_path() -> PathBuf {
|
||||
.expect("nu_plugin_example not found")
|
||||
}
|
||||
|
||||
fn valid_plugin_item_data() -> PluginRegistryItemData {
|
||||
PluginRegistryItemData::Valid {
|
||||
metadata: Default::default(),
|
||||
commands: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_add_then_restart_nu() {
|
||||
let result = nu_with_plugins!(
|
||||
@ -149,7 +156,7 @@ fn plugin_rm_then_restart_nu() {
|
||||
name: "example".into(),
|
||||
filename: example_plugin_path,
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid { commands: vec![] },
|
||||
data: valid_plugin_item_data(),
|
||||
});
|
||||
|
||||
contents.upsert_plugin(PluginRegistryItem {
|
||||
@ -157,7 +164,7 @@ fn plugin_rm_then_restart_nu() {
|
||||
// this doesn't exist, but it should be ok
|
||||
filename: dirs.test().join("nu_plugin_foo"),
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid { commands: vec![] },
|
||||
data: valid_plugin_item_data(),
|
||||
});
|
||||
|
||||
contents
|
||||
@ -225,7 +232,7 @@ fn plugin_rm_from_custom_path() {
|
||||
name: "example".into(),
|
||||
filename: example_plugin_path,
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid { commands: vec![] },
|
||||
data: valid_plugin_item_data(),
|
||||
});
|
||||
|
||||
contents.upsert_plugin(PluginRegistryItem {
|
||||
@ -233,7 +240,7 @@ fn plugin_rm_from_custom_path() {
|
||||
// this doesn't exist, but it should be ok
|
||||
filename: dirs.test().join("nu_plugin_foo"),
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid { commands: vec![] },
|
||||
data: valid_plugin_item_data(),
|
||||
});
|
||||
|
||||
contents
|
||||
@ -273,7 +280,7 @@ fn plugin_rm_using_filename() {
|
||||
name: "example".into(),
|
||||
filename: example_plugin_path.clone(),
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid { commands: vec![] },
|
||||
data: valid_plugin_item_data(),
|
||||
});
|
||||
|
||||
contents.upsert_plugin(PluginRegistryItem {
|
||||
@ -281,7 +288,7 @@ fn plugin_rm_using_filename() {
|
||||
// this doesn't exist, but it should be ok
|
||||
filename: dirs.test().join("nu_plugin_foo"),
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid { commands: vec![] },
|
||||
data: valid_plugin_item_data(),
|
||||
});
|
||||
|
||||
contents
|
||||
@ -331,7 +338,7 @@ fn warning_on_invalid_plugin_item() {
|
||||
name: "example".into(),
|
||||
filename: example_plugin_path,
|
||||
shell: None,
|
||||
data: PluginRegistryItemData::Valid { commands: vec![] },
|
||||
data: valid_plugin_item_data(),
|
||||
});
|
||||
|
||||
contents.upsert_plugin(PluginRegistryItem {
|
||||
|
Loading…
Reference in New Issue
Block a user