forked from extern/nushell
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:
@ -71,8 +71,13 @@ pub use try_::Try;
|
||||
pub use use_::Use;
|
||||
pub use version::Version;
|
||||
pub use while_::While;
|
||||
//#[cfg(feature = "plugin")]
|
||||
|
||||
mod plugin;
|
||||
mod plugin_list;
|
||||
mod plugin_stop;
|
||||
mod register;
|
||||
|
||||
//#[cfg(feature = "plugin")]
|
||||
pub use plugin::PluginCommand;
|
||||
pub use plugin_list::PluginList;
|
||||
pub use plugin_stop::PluginStop;
|
||||
pub use register::Register;
|
||||
|
64
crates/nu-cmd-lang/src/core_commands/plugin.rs
Normal file
64
crates/nu-cmd-lang/src/core_commands/plugin.rs
Normal file
@ -0,0 +1,64 @@
|
||||
use nu_engine::get_full_help;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, IntoPipelineData, PipelineData, ShellError, Signature, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginCommand;
|
||||
|
||||
impl Command for PluginCommand {
|
||||
fn name(&self) -> &str {
|
||||
"plugin"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("plugin")
|
||||
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Commands for managing plugins."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
"To load a plugin, see `register`."
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
Ok(Value::string(
|
||||
get_full_help(
|
||||
&PluginCommand.signature(),
|
||||
&PluginCommand.examples(),
|
||||
engine_state,
|
||||
stack,
|
||||
self.is_parser_keyword(),
|
||||
),
|
||||
call.head,
|
||||
)
|
||||
.into_pipeline_data())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "plugin list",
|
||||
description: "List installed plugins",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "plugin stop inc",
|
||||
description: "Stop the plugin named `inc`.",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
101
crates/nu-cmd-lang/src/core_commands/plugin_list.rs
Normal file
101
crates/nu-cmd-lang/src/core_commands/plugin_list.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use itertools::Itertools;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
record, Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature,
|
||||
Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginList;
|
||||
|
||||
impl Command for PluginList {
|
||||
fn name(&self) -> &str {
|
||||
"plugin list"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("plugin list")
|
||||
.input_output_type(
|
||||
Type::Nothing,
|
||||
Type::Table(vec![
|
||||
("name".into(), Type::String),
|
||||
("is_running".into(), Type::Bool),
|
||||
("pid".into(), Type::Int),
|
||||
("filename".into(), Type::String),
|
||||
("shell".into(), Type::String),
|
||||
("commands".into(), Type::List(Type::String.into())),
|
||||
]),
|
||||
)
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"List installed plugins."
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<nu_protocol::Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "plugin list",
|
||||
description: "List installed plugins.",
|
||||
result: Some(Value::test_list(vec![Value::test_record(record! {
|
||||
"name" => Value::test_string("inc"),
|
||||
"is_running" => Value::test_bool(true),
|
||||
"pid" => Value::test_int(106480),
|
||||
"filename" => if cfg!(windows) {
|
||||
Value::test_string(r"C:\nu\plugins\nu_plugin_inc.exe")
|
||||
} else {
|
||||
Value::test_string("/opt/nu/plugins/nu_plugin_inc")
|
||||
},
|
||||
"shell" => Value::test_nothing(),
|
||||
"commands" => Value::test_list(vec![Value::test_string("inc")]),
|
||||
})])),
|
||||
},
|
||||
Example {
|
||||
example: "ps | where pid in (plugin list).pid",
|
||||
description: "Get process information for running plugins.",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let span = call.span();
|
||||
// Group plugin decls by plugin identity
|
||||
let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
|
||||
decl.plugin_identity()
|
||||
.expect("plugin decl should have identity")
|
||||
});
|
||||
// Build plugins list
|
||||
let list = engine_state.plugins().iter().map(|plugin| {
|
||||
// Find commands that belong to the plugin
|
||||
let commands = decls.get(plugin.identity())
|
||||
.into_iter()
|
||||
.flat_map(|decls| {
|
||||
decls.iter().map(|decl| Value::string(decl.name(), span))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Value::record(record! {
|
||||
"name" => Value::string(plugin.identity().name(), span),
|
||||
"is_running" => Value::bool(plugin.is_running(), span),
|
||||
"pid" => plugin.pid()
|
||||
.map(|p| Value::int(p as i64, span))
|
||||
.unwrap_or(Value::nothing(span)),
|
||||
"filename" => Value::string(plugin.identity().filename().to_string_lossy(), span),
|
||||
"shell" => plugin.identity().shell()
|
||||
.map(|s| Value::string(s.to_string_lossy(), span))
|
||||
.unwrap_or(Value::nothing(span)),
|
||||
"commands" => Value::list(commands, span),
|
||||
}, span)
|
||||
}).collect::<Vec<Value>>();
|
||||
Ok(list.into_pipeline_data(engine_state.ctrlc.clone()))
|
||||
}
|
||||
}
|
75
crates/nu-cmd-lang/src/core_commands/plugin_stop.rs
Normal file
75
crates/nu-cmd-lang/src/core_commands/plugin_stop.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use nu_engine::CallExt;
|
||||
use nu_protocol::{
|
||||
ast::Call,
|
||||
engine::{Command, EngineState, Stack},
|
||||
Category, Example, PipelineData, ShellError, Signature, Spanned, SyntaxShape, Type,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginStop;
|
||||
|
||||
impl Command for PluginStop {
|
||||
fn name(&self) -> &str {
|
||||
"plugin stop"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("plugin stop")
|
||||
.input_output_type(Type::Nothing, Type::Nothing)
|
||||
.required(
|
||||
"name",
|
||||
SyntaxShape::String,
|
||||
"The name of the plugin to stop.",
|
||||
)
|
||||
.category(Category::Core)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Stop an installed plugin if it was running."
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<nu_protocol::Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "plugin stop inc",
|
||||
description: "Stop the plugin named `inc`.",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "plugin list | each { |p| plugin stop $p.name }",
|
||||
description: "Stop all plugins.",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let name: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
|
||||
let mut found = false;
|
||||
for plugin in engine_state.plugins() {
|
||||
if plugin.identity().name() == name.item {
|
||||
plugin.stop()?;
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
Ok(PipelineData::Empty)
|
||||
} else {
|
||||
Err(ShellError::GenericError {
|
||||
error: format!("Failed to stop the `{}` plugin", name.item),
|
||||
msg: "couldn't find a plugin with this name".into(),
|
||||
span: Some(name.span),
|
||||
help: Some("you may need to `register` the plugin first".into()),
|
||||
inner: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -130,11 +130,11 @@ pub fn version(engine_state: &EngineState, call: &Call) -> Result<PipelineData,
|
||||
Value::string(features_enabled().join(", "), call.head),
|
||||
);
|
||||
|
||||
// Get a list of command names and check for plugins
|
||||
// Get a list of plugin names
|
||||
let installed_plugins = engine_state
|
||||
.plugin_decls()
|
||||
.filter(|x| x.is_plugin().is_some())
|
||||
.map(|x| x.name())
|
||||
.plugins()
|
||||
.iter()
|
||||
.map(|x| x.identity().name())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
record.push(
|
||||
|
@ -65,7 +65,7 @@ pub fn create_default_context() -> EngineState {
|
||||
};
|
||||
|
||||
//#[cfg(feature = "plugin")]
|
||||
bind_command!(Register);
|
||||
bind_command!(PluginCommand, PluginList, PluginStop, Register,);
|
||||
|
||||
working_set.render()
|
||||
};
|
||||
|
Reference in New Issue
Block a user