mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 20:37:43 +02:00
Keep plugins persistently running in the background (#12064)
# Description This PR uses the new plugin protocol to intelligently keep plugin processes running in the background for further plugin calls. Running plugins can be seen by running the new `plugin list` command, and stopped by running the new `plugin stop` command. This is an enhancement for the performance of plugins, as starting new plugin processes has overhead, especially for plugins in languages that take a significant amount of time on startup. It also enables plugins that have persistent state between commands, making the migration of features like dataframes and `stor` to plugins possible. Plugins are automatically stopped by the new plugin garbage collector, configurable with `$env.config.plugin_gc`: ```nushell $env.config.plugin_gc = { # Configuration for plugin garbage collection default: { enabled: true # true to enable stopping of inactive plugins stop_after: 10sec # how long to wait after a plugin is inactive to stop it } plugins: { # alternate configuration for specific plugins, by name, for example: # # gstat: { # enabled: false # } } } ``` If garbage collection is enabled, plugins will be stopped after `stop_after` passes after they were last active. Plugins are counted as inactive if they have no running plugin calls. Reading the stream from the response of a plugin call is still considered to be activity, but if a plugin holds on to a stream but the call ends without an active streaming response, it is not counted as active even if it is reading it. Plugins can explicitly disable the GC as appropriate with `engine.set_gc_disabled(true)`. The `version` command now lists plugin names rather than plugin commands. The list of plugin commands is accessible via `plugin list`. Recommend doing this together with #12029, because it will likely force plugin developers to do the right thing with mutability and lead to less unexpected behavior when running plugins nested / in parallel. # User-Facing Changes - new command: `plugin list` - new command: `plugin stop` - changed command: `version` (now lists plugin names, rather than commands) - new config: `$env.config.plugin_gc` - Plugins will keep running and be reused, at least for the configured GC period - Plugins that used mutable state in weird ways like `inc` did might misbehave until fixed - Plugins can disable GC if they need to - Had to change plugin signature to accept `&EngineInterface` so that the GC disable feature works. #12029 does this anyway, and I'm expecting (resolvable) conflicts with that # Tests + Formatting - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` Because there is some specific OS behavior required for plugins to not respond to Ctrl-C directly, I've developed against and tested on both Linux and Windows to ensure that works properly. # After Submitting I think this probably needs to be in the book somewhere
This commit is contained in:
@ -13,6 +13,7 @@ pub use self::completer::CompletionAlgorithm;
|
||||
pub use self::helper::extract_value;
|
||||
pub use self::hooks::Hooks;
|
||||
pub use self::output::ErrorStyle;
|
||||
pub use self::plugin_gc::{PluginGcConfig, PluginGcConfigs};
|
||||
pub use self::reedline::{
|
||||
create_menus, EditBindings, HistoryFileFormat, NuCursorShape, ParsedKeybinding, ParsedMenu,
|
||||
};
|
||||
@ -22,6 +23,7 @@ mod completer;
|
||||
mod helper;
|
||||
mod hooks;
|
||||
mod output;
|
||||
mod plugin_gc;
|
||||
mod reedline;
|
||||
mod table;
|
||||
|
||||
@ -96,6 +98,8 @@ pub struct Config {
|
||||
/// match the registered plugin name so `register nu_plugin_example` will be able to place
|
||||
/// its configuration under a `nu_plugin_example` column.
|
||||
pub plugins: HashMap<String, Value>,
|
||||
/// Configuration for plugin garbage collection.
|
||||
pub plugin_gc: PluginGcConfigs,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@ -162,6 +166,7 @@ impl Default for Config {
|
||||
highlight_resolved_externals: false,
|
||||
|
||||
plugins: HashMap::new(),
|
||||
plugin_gc: PluginGcConfigs::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -671,6 +676,9 @@ impl Value {
|
||||
);
|
||||
}
|
||||
}
|
||||
"plugin_gc" => {
|
||||
config.plugin_gc.process(&[key], value, &mut errors);
|
||||
}
|
||||
// Menus
|
||||
"menus" => match create_menus(value) {
|
||||
Ok(map) => config.menus = map,
|
||||
|
252
crates/nu-protocol/src/config/plugin_gc.rs
Normal file
252
crates/nu-protocol/src/config/plugin_gc.rs
Normal file
@ -0,0 +1,252 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{record, ShellError, Span, Value};
|
||||
|
||||
use super::helper::{
|
||||
process_bool_config, report_invalid_key, report_invalid_value, ReconstructVal,
|
||||
};
|
||||
|
||||
/// Configures when plugins should be stopped if inactive
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
pub struct PluginGcConfigs {
|
||||
/// The config to use for plugins not otherwise specified
|
||||
pub default: PluginGcConfig,
|
||||
/// Specific configs for plugins (by name)
|
||||
pub plugins: HashMap<String, PluginGcConfig>,
|
||||
}
|
||||
|
||||
impl PluginGcConfigs {
|
||||
/// Get the plugin GC configuration for a specific plugin name. If not specified by name in the
|
||||
/// config, this is `default`.
|
||||
pub fn get(&self, plugin_name: &str) -> &PluginGcConfig {
|
||||
self.plugins.get(plugin_name).unwrap_or(&self.default)
|
||||
}
|
||||
|
||||
pub(super) fn process(
|
||||
&mut self,
|
||||
path: &[&str],
|
||||
value: &mut Value,
|
||||
errors: &mut Vec<ShellError>,
|
||||
) {
|
||||
if let Value::Record { val, .. } = value {
|
||||
// Handle resets to default if keys are missing
|
||||
if !val.contains("default") {
|
||||
self.default = PluginGcConfig::default();
|
||||
}
|
||||
if !val.contains("plugins") {
|
||||
self.plugins = HashMap::new();
|
||||
}
|
||||
|
||||
val.retain_mut(|key, value| {
|
||||
let span = value.span();
|
||||
match key {
|
||||
"default" => {
|
||||
self.default
|
||||
.process(&join_path(path, &["default"]), value, errors)
|
||||
}
|
||||
"plugins" => process_plugins(
|
||||
&join_path(path, &["plugins"]),
|
||||
value,
|
||||
errors,
|
||||
&mut self.plugins,
|
||||
),
|
||||
_ => {
|
||||
report_invalid_key(&join_path(path, &[key]), span, errors);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
} else {
|
||||
report_invalid_value("should be a record", value.span(), errors);
|
||||
*value = self.reconstruct_value(value.span());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReconstructVal for PluginGcConfigs {
|
||||
fn reconstruct_value(&self, span: Span) -> Value {
|
||||
Value::record(
|
||||
record! {
|
||||
"default" => self.default.reconstruct_value(span),
|
||||
"plugins" => reconstruct_plugins(&self.plugins, span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn process_plugins(
|
||||
path: &[&str],
|
||||
value: &mut Value,
|
||||
errors: &mut Vec<ShellError>,
|
||||
plugins: &mut HashMap<String, PluginGcConfig>,
|
||||
) {
|
||||
if let Value::Record { val, .. } = value {
|
||||
// Remove any plugin configs that aren't in the value
|
||||
plugins.retain(|key, _| val.contains(key));
|
||||
|
||||
val.retain_mut(|key, value| {
|
||||
if matches!(value, Value::Record { .. }) {
|
||||
plugins.entry(key.to_owned()).or_default().process(
|
||||
&join_path(path, &[key]),
|
||||
value,
|
||||
errors,
|
||||
);
|
||||
true
|
||||
} else {
|
||||
report_invalid_value("should be a record", value.span(), errors);
|
||||
if let Some(conf) = plugins.get(key) {
|
||||
// Reconstruct the value if it existed before
|
||||
*value = conf.reconstruct_value(value.span());
|
||||
true
|
||||
} else {
|
||||
// Remove it if it didn't
|
||||
false
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn reconstruct_plugins(plugins: &HashMap<String, PluginGcConfig>, span: Span) -> Value {
|
||||
Value::record(
|
||||
plugins
|
||||
.iter()
|
||||
.map(|(key, val)| (key.to_owned(), val.reconstruct_value(span)))
|
||||
.collect(),
|
||||
span,
|
||||
)
|
||||
}
|
||||
|
||||
/// Configures when a plugin should be stopped if inactive
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PluginGcConfig {
|
||||
/// True if the plugin should be stopped automatically
|
||||
pub enabled: bool,
|
||||
/// When to stop the plugin if not in use for this long (in nanoseconds)
|
||||
pub stop_after: i64,
|
||||
}
|
||||
|
||||
impl Default for PluginGcConfig {
|
||||
fn default() -> Self {
|
||||
PluginGcConfig {
|
||||
enabled: true,
|
||||
stop_after: 10_000_000_000, // 10sec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginGcConfig {
|
||||
fn process(&mut self, path: &[&str], value: &mut Value, errors: &mut Vec<ShellError>) {
|
||||
if let Value::Record { val, .. } = value {
|
||||
// Handle resets to default if keys are missing
|
||||
if !val.contains("enabled") {
|
||||
self.enabled = PluginGcConfig::default().enabled;
|
||||
}
|
||||
if !val.contains("stop_after") {
|
||||
self.stop_after = PluginGcConfig::default().stop_after;
|
||||
}
|
||||
|
||||
val.retain_mut(|key, value| {
|
||||
let span = value.span();
|
||||
match key {
|
||||
"enabled" => process_bool_config(value, errors, &mut self.enabled),
|
||||
"stop_after" => match value {
|
||||
Value::Duration { val, .. } => {
|
||||
if *val >= 0 {
|
||||
self.stop_after = *val;
|
||||
} else {
|
||||
report_invalid_value("must not be negative", span, errors);
|
||||
*val = self.stop_after;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
report_invalid_value("should be a duration", span, errors);
|
||||
*value = Value::duration(self.stop_after, span);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
report_invalid_key(&join_path(path, &[key]), span, errors);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
})
|
||||
} else {
|
||||
report_invalid_value("should be a record", value.span(), errors);
|
||||
*value = self.reconstruct_value(value.span());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReconstructVal for PluginGcConfig {
|
||||
fn reconstruct_value(&self, span: Span) -> Value {
|
||||
Value::record(
|
||||
record! {
|
||||
"enabled" => Value::bool(self.enabled, span),
|
||||
"stop_after" => Value::duration(self.stop_after, span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn join_path<'a>(a: &[&'a str], b: &[&'a str]) -> Vec<&'a str> {
|
||||
a.iter().copied().chain(b.iter().copied()).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_pair() -> (PluginGcConfigs, Value) {
|
||||
(
|
||||
PluginGcConfigs {
|
||||
default: PluginGcConfig {
|
||||
enabled: true,
|
||||
stop_after: 30_000_000_000,
|
||||
},
|
||||
plugins: [(
|
||||
"my_plugin".to_owned(),
|
||||
PluginGcConfig {
|
||||
enabled: false,
|
||||
stop_after: 0,
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
},
|
||||
Value::test_record(record! {
|
||||
"default" => Value::test_record(record! {
|
||||
"enabled" => Value::test_bool(true),
|
||||
"stop_after" => Value::test_duration(30_000_000_000),
|
||||
}),
|
||||
"plugins" => Value::test_record(record! {
|
||||
"my_plugin" => Value::test_record(record! {
|
||||
"enabled" => Value::test_bool(false),
|
||||
"stop_after" => Value::test_duration(0),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process() {
|
||||
let (expected, mut input) = test_pair();
|
||||
let mut errors = vec![];
|
||||
let mut result = PluginGcConfigs::default();
|
||||
result.process(&[], &mut input, &mut errors);
|
||||
assert!(errors.is_empty(), "errors: {errors:#?}");
|
||||
assert_eq!(expected, result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstruct() {
|
||||
let (input, expected) = test_pair();
|
||||
assert_eq!(expected, input.reconstruct_value(Span::test_data()));
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{ast::Call, Alias, BlockId, Example, PipelineData, ShellError, Signature};
|
||||
|
||||
use super::{EngineState, Stack, StateWorkingSet};
|
||||
@ -91,8 +89,14 @@ pub trait Command: Send + Sync + CommandClone {
|
||||
false
|
||||
}
|
||||
|
||||
// Is a plugin command (returns plugin's path, type of shell if the declaration is a plugin)
|
||||
fn is_plugin(&self) -> Option<(&Path, Option<&Path>)> {
|
||||
/// Is a plugin command
|
||||
fn is_plugin(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The identity of the plugin, if this is a plugin command
|
||||
#[cfg(feature = "plugin")]
|
||||
fn plugin_identity(&self) -> Option<&crate::PluginIdentity> {
|
||||
None
|
||||
}
|
||||
|
||||
@ -118,7 +122,7 @@ pub trait Command: Send + Sync + CommandClone {
|
||||
self.is_parser_keyword(),
|
||||
self.is_known_external(),
|
||||
self.is_alias(),
|
||||
self.is_plugin().is_some(),
|
||||
self.is_plugin(),
|
||||
) {
|
||||
(true, false, false, false, false, false) => CommandType::Builtin,
|
||||
(true, true, false, false, false, false) => CommandType::Custom,
|
||||
|
@ -23,6 +23,9 @@ use std::sync::{
|
||||
|
||||
type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::RegisteredPlugin;
|
||||
|
||||
pub static PWD_ENV: &str = "PWD";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@ -113,6 +116,8 @@ pub struct EngineState {
|
||||
pub table_decl_id: Option<usize>,
|
||||
#[cfg(feature = "plugin")]
|
||||
pub plugin_signatures: Option<PathBuf>,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins: Vec<Arc<dyn RegisteredPlugin>>,
|
||||
config_path: HashMap<String, PathBuf>,
|
||||
pub history_enabled: bool,
|
||||
pub history_session_id: i64,
|
||||
@ -171,6 +176,8 @@ impl EngineState {
|
||||
table_decl_id: None,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_signatures: None,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins: vec![],
|
||||
config_path: HashMap::new(),
|
||||
history_enabled: true,
|
||||
history_session_id: 0,
|
||||
@ -255,14 +262,27 @@ impl EngineState {
|
||||
self.scope.active_overlays.append(&mut activated_ids);
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
if delta.plugins_changed {
|
||||
let result = self.update_plugin_file();
|
||||
|
||||
if result.is_ok() {
|
||||
delta.plugins_changed = false;
|
||||
if !delta.plugins.is_empty() {
|
||||
// Replace plugins that overlap in identity.
|
||||
for plugin in std::mem::take(&mut delta.plugins) {
|
||||
if let Some(existing) = self
|
||||
.plugins
|
||||
.iter_mut()
|
||||
.find(|p| p.identity() == plugin.identity())
|
||||
{
|
||||
// Stop the existing plugin, so that the new plugin definitely takes over
|
||||
existing.stop()?;
|
||||
*existing = plugin;
|
||||
} else {
|
||||
self.plugins.push(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
#[cfg(feature = "plugin")]
|
||||
if delta.plugins_changed {
|
||||
// Update the plugin file with the new signatures.
|
||||
self.update_plugin_file()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@ -274,6 +294,8 @@ impl EngineState {
|
||||
stack: &mut Stack,
|
||||
cwd: impl AsRef<Path>,
|
||||
) -> Result<(), ShellError> {
|
||||
let mut config_updated = false;
|
||||
|
||||
for mut scope in stack.env_vars.drain(..) {
|
||||
for (overlay_name, mut env) in scope.drain() {
|
||||
if let Some(env_vars) = self.env_vars.get_mut(&overlay_name) {
|
||||
@ -285,6 +307,7 @@ impl EngineState {
|
||||
let mut new_record = v.clone();
|
||||
let (config, error) = new_record.into_config(&self.config);
|
||||
self.config = config;
|
||||
config_updated = true;
|
||||
env_vars.insert(k, new_record);
|
||||
if let Some(e) = error {
|
||||
return Err(e);
|
||||
@ -303,6 +326,12 @@ impl EngineState {
|
||||
// TODO: better error
|
||||
std::env::set_current_dir(cwd)?;
|
||||
|
||||
if config_updated {
|
||||
// Make plugin GC config changes take effect immediately.
|
||||
#[cfg(feature = "plugin")]
|
||||
self.update_plugin_gc_configs(&self.config.plugin_gc);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -465,6 +494,11 @@ impl EngineState {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn plugins(&self) -> &[Arc<dyn RegisteredPlugin>] {
|
||||
&self.plugins
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn update_plugin_file(&self) -> Result<(), ShellError> {
|
||||
use std::io::Write;
|
||||
@ -490,8 +524,9 @@ impl EngineState {
|
||||
self.plugin_decls().try_for_each(|decl| {
|
||||
// A successful plugin registration already includes the plugin filename
|
||||
// No need to check the None option
|
||||
let (path, shell) = decl.is_plugin().expect("plugin should have file name");
|
||||
let mut file_name = path
|
||||
let identity = decl.plugin_identity().expect("plugin should have identity");
|
||||
let mut file_name = identity
|
||||
.filename()
|
||||
.to_str()
|
||||
.expect("path was checked during registration as a str")
|
||||
.to_string();
|
||||
@ -518,8 +553,8 @@ impl EngineState {
|
||||
serde_json::to_string_pretty(&sig_with_examples)
|
||||
.map(|signature| {
|
||||
// Extracting the possible path to the shell used to load the plugin
|
||||
let shell_str = shell
|
||||
.as_ref()
|
||||
let shell_str = identity
|
||||
.shell()
|
||||
.map(|path| {
|
||||
format!(
|
||||
"-s {}",
|
||||
@ -558,6 +593,14 @@ impl EngineState {
|
||||
})
|
||||
}
|
||||
|
||||
/// Update plugins with new garbage collection config
|
||||
#[cfg(feature = "plugin")]
|
||||
fn update_plugin_gc_configs(&self, plugin_gc: &crate::PluginGcConfigs) {
|
||||
for plugin in &self.plugins {
|
||||
plugin.set_gc_config(plugin_gc.get(plugin.identity().name()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn num_files(&self) -> usize {
|
||||
self.files.len()
|
||||
}
|
||||
@ -650,7 +693,7 @@ impl EngineState {
|
||||
let mut unique_plugin_decls = HashMap::new();
|
||||
|
||||
// Make sure there are no duplicate decls: Newer one overwrites the older one
|
||||
for decl in self.decls.iter().filter(|d| d.is_plugin().is_some()) {
|
||||
for decl in self.decls.iter().filter(|d| d.is_plugin()) {
|
||||
unique_plugin_decls.insert(decl.name(), decl);
|
||||
}
|
||||
|
||||
@ -733,6 +776,12 @@ impl EngineState {
|
||||
}
|
||||
|
||||
pub fn set_config(&mut self, conf: Config) {
|
||||
#[cfg(feature = "plugin")]
|
||||
if conf.plugin_gc != self.config.plugin_gc {
|
||||
// Make plugin GC config changes take effect immediately.
|
||||
self.update_plugin_gc_configs(&conf.plugin_gc);
|
||||
}
|
||||
|
||||
self.config = conf;
|
||||
}
|
||||
|
||||
@ -841,7 +890,7 @@ impl EngineState {
|
||||
(
|
||||
signature,
|
||||
decl.examples(),
|
||||
decl.is_plugin().is_some(),
|
||||
decl.is_plugin(),
|
||||
decl.get_block_id().is_some(),
|
||||
decl.is_parser_keyword(),
|
||||
)
|
||||
|
@ -2,6 +2,12 @@ use super::{usage::Usage, Command, EngineState, OverlayFrame, ScopeFrame, Virtua
|
||||
use crate::ast::Block;
|
||||
use crate::{Module, Variable};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::RegisteredPlugin;
|
||||
|
||||
/// A delta (or change set) between the current global state and a possible future global state. Deltas
|
||||
/// can be applied to the global state to update it to contain both previous state and the state held
|
||||
/// within the delta.
|
||||
@ -17,6 +23,8 @@ pub struct StateDelta {
|
||||
pub scope: Vec<ScopeFrame>,
|
||||
#[cfg(feature = "plugin")]
|
||||
pub(super) plugins_changed: bool, // marks whether plugin file should be updated
|
||||
#[cfg(feature = "plugin")]
|
||||
pub(super) plugins: Vec<Arc<dyn RegisteredPlugin>>,
|
||||
}
|
||||
|
||||
impl StateDelta {
|
||||
@ -40,6 +48,8 @@ impl StateDelta {
|
||||
usage: Usage::new(),
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins_changed: false,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,12 @@ use core::panic;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::{PluginIdentity, RegisteredPlugin};
|
||||
|
||||
/// A temporary extension to the global state. This handles bridging between the global state and the
|
||||
/// additional declarations and scope changes that are not yet part of the global scope.
|
||||
///
|
||||
@ -155,6 +161,28 @@ impl<'a> StateWorkingSet<'a> {
|
||||
self.delta.plugins_changed = true;
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn find_or_create_plugin(
|
||||
&mut self,
|
||||
identity: &PluginIdentity,
|
||||
make: impl FnOnce() -> Arc<dyn RegisteredPlugin>,
|
||||
) -> Arc<dyn RegisteredPlugin> {
|
||||
// Check in delta first, then permanent_state
|
||||
if let Some(plugin) = self
|
||||
.delta
|
||||
.plugins
|
||||
.iter()
|
||||
.chain(self.permanent_state.plugins())
|
||||
.find(|p| p.identity() == identity)
|
||||
{
|
||||
plugin.clone()
|
||||
} else {
|
||||
let plugin = make();
|
||||
self.delta.plugins.push(plugin.clone());
|
||||
plugin
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_predecl(&mut self, name: &[u8]) -> Option<DeclId> {
|
||||
self.move_predecls_to_overlay();
|
||||
|
||||
|
@ -16,7 +16,7 @@ mod parse_error;
|
||||
mod parse_warning;
|
||||
mod pipeline_data;
|
||||
#[cfg(feature = "plugin")]
|
||||
mod plugin_signature;
|
||||
mod plugin;
|
||||
mod shell_error;
|
||||
mod signature;
|
||||
pub mod span;
|
||||
@ -40,7 +40,7 @@ pub use parse_error::{DidYouMean, ParseError};
|
||||
pub use parse_warning::ParseWarning;
|
||||
pub use pipeline_data::*;
|
||||
#[cfg(feature = "plugin")]
|
||||
pub use plugin_signature::*;
|
||||
pub use plugin::*;
|
||||
pub use shell_error::*;
|
||||
pub use signature::*;
|
||||
pub use span::*;
|
||||
|
105
crates/nu-protocol/src/plugin/identity.rs
Normal file
105
crates/nu-protocol/src/plugin/identity.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::{ParseError, Spanned};
|
||||
|
||||
/// Error when an invalid plugin filename was encountered. This can be converted to [`ParseError`]
|
||||
/// if a span is added.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InvalidPluginFilename;
|
||||
|
||||
impl std::fmt::Display for InvalidPluginFilename {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("invalid plugin filename")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Spanned<InvalidPluginFilename>> for ParseError {
|
||||
fn from(error: Spanned<InvalidPluginFilename>) -> ParseError {
|
||||
ParseError::LabeledError(
|
||||
"Invalid plugin filename".into(),
|
||||
"must start with `nu_plugin_`".into(),
|
||||
error.span,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct PluginIdentity {
|
||||
/// The filename used to start the plugin
|
||||
filename: PathBuf,
|
||||
/// The shell used to start the plugin, if required
|
||||
shell: Option<PathBuf>,
|
||||
/// The friendly name of the plugin (e.g. `inc` for `C:\nu_plugin_inc.exe`)
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl PluginIdentity {
|
||||
/// Create a new plugin identity from a path to plugin executable and shell option.
|
||||
pub fn new(
|
||||
filename: impl Into<PathBuf>,
|
||||
shell: Option<PathBuf>,
|
||||
) -> Result<PluginIdentity, InvalidPluginFilename> {
|
||||
let filename = filename.into();
|
||||
|
||||
let name = filename
|
||||
.file_stem()
|
||||
.map(|stem| stem.to_string_lossy().into_owned())
|
||||
.and_then(|stem| stem.strip_prefix("nu_plugin_").map(|s| s.to_owned()))
|
||||
.ok_or(InvalidPluginFilename)?;
|
||||
|
||||
Ok(PluginIdentity {
|
||||
filename,
|
||||
shell,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
/// The filename of the plugin executable.
|
||||
pub fn filename(&self) -> &Path {
|
||||
&self.filename
|
||||
}
|
||||
|
||||
/// The shell command used by the plugin.
|
||||
pub fn shell(&self) -> Option<&Path> {
|
||||
self.shell.as_deref()
|
||||
}
|
||||
|
||||
/// The name of the plugin, determined by the part of the filename after `nu_plugin_` excluding
|
||||
/// the extension.
|
||||
///
|
||||
/// - `C:\nu_plugin_inc.exe` becomes `inc`
|
||||
/// - `/home/nu/.cargo/bin/nu_plugin_inc` becomes `inc`
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Create a fake identity for testing.
|
||||
#[cfg(windows)]
|
||||
#[doc(hidden)]
|
||||
pub fn new_fake(name: &str) -> PluginIdentity {
|
||||
PluginIdentity::new(format!(r"C:\fake\path\nu_plugin_{name}.exe"), None)
|
||||
.expect("fake plugin identity path is invalid")
|
||||
}
|
||||
|
||||
/// Create a fake identity for testing.
|
||||
#[cfg(not(windows))]
|
||||
#[doc(hidden)]
|
||||
pub fn new_fake(name: &str) -> PluginIdentity {
|
||||
PluginIdentity::new(format!(r"/fake/path/nu_plugin_{name}"), None)
|
||||
.expect("fake plugin identity path is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_name_from_path() {
|
||||
assert_eq!("test", PluginIdentity::new_fake("test").name());
|
||||
assert_eq!("test_2", PluginIdentity::new_fake("test_2").name());
|
||||
assert_eq!(
|
||||
"foo",
|
||||
PluginIdentity::new("nu_plugin_foo.sh", Some("sh".into()))
|
||||
.expect("should be valid")
|
||||
.name()
|
||||
);
|
||||
PluginIdentity::new("other", None).expect_err("should be invalid");
|
||||
PluginIdentity::new("", None).expect_err("should be invalid");
|
||||
}
|
7
crates/nu-protocol/src/plugin/mod.rs
Normal file
7
crates/nu-protocol/src/plugin/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod identity;
|
||||
mod registered;
|
||||
mod signature;
|
||||
|
||||
pub use identity::*;
|
||||
pub use registered::*;
|
||||
pub use signature::*;
|
27
crates/nu-protocol/src/plugin/registered.rs
Normal file
27
crates/nu-protocol/src/plugin/registered.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use crate::{PluginGcConfig, PluginIdentity, ShellError};
|
||||
|
||||
/// Trait for plugins registered in the [`EngineState`](crate::EngineState).
|
||||
pub trait RegisteredPlugin: Send + Sync {
|
||||
/// The identity of the plugin - its filename, shell, and friendly name.
|
||||
fn identity(&self) -> &PluginIdentity;
|
||||
|
||||
/// True if the plugin is currently running.
|
||||
fn is_running(&self) -> bool;
|
||||
|
||||
/// Process ID of the plugin executable, if running.
|
||||
fn pid(&self) -> Option<u32>;
|
||||
|
||||
/// Set garbage collection config for the plugin.
|
||||
fn set_gc_config(&self, gc_config: &PluginGcConfig);
|
||||
|
||||
/// Stop the plugin.
|
||||
fn stop(&self) -> Result<(), ShellError>;
|
||||
|
||||
/// Cast the pointer to an [`Any`] so that its concrete type can be retrieved.
|
||||
///
|
||||
/// This is necessary in order to allow `nu_plugin` to handle the implementation details of
|
||||
/// plugins.
|
||||
fn as_any(self: Arc<Self>) -> Arc<dyn Any + Send + Sync>;
|
||||
}
|
Reference in New Issue
Block a user