nushell/crates/nu-cmd-plugin/src/util.rs
Devyn Cairns 3af575cce7 Make plugin list read state from plugin registry file as well (#14085)
# Description

[Context on
Discord](https://discord.com/channels/601130461678272522/855947301380947968/1292279795035668583)

**This is a breaking change, due to the removal of `is_running`.**

Some users find the `plugin list` command confusing, because it doesn't
show anything different after running `plugin add` or `plugin rm`. This
modifies the `plugin list` command to also look at the plugin registry
file to give some idea of how the plugins in engine state differ from
those in the plugin registry file.

The following values of `status` are now produced instead of
`is_running`:

- `added`: The plugin is present in the plugin registry file, but not in
the engine.
- `loaded`: The plugin is present both in the plugin registry file and
in the engine, but is not running.
- `running`: The plugin is currently running, and the `pid` column
should contain its process ID.
- `modified`: The plugin state present in the plugin registry file is
different from the state in the engine.
- `removed`: The plugin is still loaded in the engine, but is not
present in the plugin registry file.
- `invalid`: The data in the plugin registry file couldn't be
deserialized, and the plugin most likely needs to be added again.

Example (`commands` omitted):

```
╭──────┬─────────────────────┬────────────┬───────────┬──────────┬─────────────────────────────────────────────────────┬─────────╮
│    # │        name         │  version   │  status   │   pid    │                      filename                       │  shell  │
├──────┼─────────────────────┼────────────┼───────────┼──────────┼─────────────────────────────────────────────────────┼─────────┤
│    0 │ custom_values       │ 0.1.0      │ loaded    │          │ /home/devyn/.cargo/bin/nu_plugin_custom_values      │         │
│    1 │ dbus                │ 0.11.0     │ loaded    │          │ /home/devyn/.cargo/bin/nu_plugin_dbus               │         │
│    2 │ example             │ 0.98.1     │ loaded    │          │ /home/devyn/.cargo/bin/nu_plugin_example            │         │
│    3 │ explore_ir          │ 0.3.0      │ loaded    │          │ /home/devyn/.cargo/bin/nu_plugin_explore_ir         │         │
│    4 │ formats             │ 0.98.1     │ loaded    │          │ /home/devyn/.cargo/bin/nu_plugin_formats            │         │
│    5 │ gstat               │ 0.98.1     │ running   │   236662 │ /home/devyn/.cargo/bin/nu_plugin_gstat              │         │
│    6 │ inc                 │ 0.98.1     │ loaded    │          │ /home/devyn/.cargo/bin/nu_plugin_inc                │         │
│    7 │ polars              │ 0.98.1     │ added     │          │ /home/devyn/.cargo/bin/nu_plugin_polars             │         │
│    8 │ query               │ 0.98.1     │ removed   │          │ /home/devyn/.cargo/bin/nu_plugin_query              │         │
│    9 │ stress_internals    │ 0.98.1     │ loaded    │          │ /home/devyn/.cargo/bin/nu_plugin_stress_internals   │         │
╰──────┴─────────────────────┴────────────┴───────────┴──────────┴─────────────────────────────────────────────────────┴─────────╯

```

# User-Facing Changes

To `plugin list`:

* **Breaking:** The `is_running` column is removed and replaced with
`status`. Use `status == running` to filter equivalently.
* The `--plugin-config` from other plugin management commands is now
supported.
* Added an `--engine` flag which behaves more or less like before, and
doesn't load the plugin registry file at all.
* Added a `--registry` flag which only checks the plugin registry file.
All plugins appear as `added` since there is no state to compare with.

Because the default is to check both, the `plugin list` command might be
a little bit slower. If you don't need to check the plugin registry
file, the `--engine` flag does not load the plugin registry file at all,
so it should be just as fast as before.

# Tests + Formatting

Added tests for `added` and `removed` statuses. `modified` and `invalid`
are a bit more tricky so I didn't try.

# After Submitting

- [ ] update documentation that references the `plugin list` command
- [ ] release notes
2024-10-20 23:12:57 +02:00

153 lines
4.9 KiB
Rust

#[allow(deprecated)]
use nu_engine::{command_prelude::*, current_dir};
use nu_protocol::{engine::StateWorkingSet, PluginRegistryFile};
use std::{
fs::{self, File},
path::PathBuf,
};
fn get_plugin_registry_file_path(
engine_state: &EngineState,
stack: &mut Stack,
span: Span,
custom_path: &Option<Spanned<String>>,
) -> Result<PathBuf, ShellError> {
#[allow(deprecated)]
let cwd = current_dir(engine_state, stack)?;
if let Some(ref custom_path) = custom_path {
Ok(nu_path::expand_path_with(&custom_path.item, cwd, true))
} else {
engine_state
.plugin_path
.clone()
.ok_or_else(|| ShellError::GenericError {
error: "Plugin registry file not set".into(),
msg: "pass --plugin-config explicitly here".into(),
span: Some(span),
help: Some("you may be running `nu` with --no-config-file".into()),
inner: vec![],
})
}
}
pub(crate) fn read_plugin_file(
engine_state: &EngineState,
stack: &mut Stack,
span: Span,
custom_path: &Option<Spanned<String>>,
) -> Result<PluginRegistryFile, ShellError> {
let plugin_registry_file_path =
get_plugin_registry_file_path(engine_state, stack, span, custom_path)?;
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
// Try to read the plugin file if it exists
if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) {
PluginRegistryFile::read_from(
File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
msg: format!(
"failed to read `{}`: {}",
plugin_registry_file_path.display(),
err
),
span: file_span,
})?,
Some(file_span),
)
} else if let Some(path) = custom_path {
Err(ShellError::FileNotFound {
file: path.item.clone(),
span: path.span,
})
} else {
Ok(PluginRegistryFile::default())
}
}
pub(crate) fn modify_plugin_file(
engine_state: &EngineState,
stack: &mut Stack,
span: Span,
custom_path: &Option<Spanned<String>>,
operate: impl FnOnce(&mut PluginRegistryFile) -> Result<(), ShellError>,
) -> Result<(), ShellError> {
let plugin_registry_file_path =
get_plugin_registry_file_path(engine_state, stack, span, custom_path)?;
let file_span = custom_path.as_ref().map(|p| p.span).unwrap_or(span);
// Try to read the plugin file if it exists
let mut contents = if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) {
PluginRegistryFile::read_from(
File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
msg: format!(
"failed to read `{}`: {}",
plugin_registry_file_path.display(),
err
),
span: file_span,
})?,
Some(file_span),
)?
} else {
PluginRegistryFile::default()
};
// Do the operation
operate(&mut contents)?;
// Save the modified file on success
contents.write_to(
File::create(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned {
msg: format!(
"failed to create `{}`: {}",
plugin_registry_file_path.display(),
err
),
span: file_span,
})?,
Some(span),
)?;
Ok(())
}
pub(crate) fn canonicalize_possible_filename_arg(
engine_state: &EngineState,
stack: &Stack,
arg: &str,
) -> PathBuf {
// This results in the best possible chance of a match with the plugin item
#[allow(deprecated)]
if let Ok(cwd) = nu_engine::current_dir(engine_state, stack) {
let path = nu_path::expand_path_with(arg, &cwd, true);
// Try to canonicalize
nu_path::locate_in_dirs(&path, &cwd, || get_plugin_dirs(engine_state, stack))
// If we couldn't locate it, return the expanded path alone
.unwrap_or(path)
} else {
arg.into()
}
}
pub(crate) fn get_plugin_dirs(
engine_state: &EngineState,
stack: &Stack,
) -> impl Iterator<Item = String> {
// Get the NU_PLUGIN_DIRS constant or env var
let working_set = StateWorkingSet::new(engine_state);
let value = working_set
.find_variable(b"$NU_PLUGIN_DIRS")
.and_then(|var_id| working_set.get_constant(var_id).ok())
.or_else(|| stack.get_env_var(engine_state, "NU_PLUGIN_DIRS"))
.cloned(); // TODO: avoid this clone
// Get all of the strings in the list, if possible
value
.into_iter()
.flat_map(|value| value.into_list().ok())
.flatten()
.flat_map(|list_item| list_item.coerce_into_string().ok())
}