aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2023-04-19 10:09:39 +0200
committerGitHub <noreply@github.com>2023-04-19 10:09:39 +0200
commitcaaee30179cf8e3d20ad103b898e4bad9ebf648b (patch)
tree6348896d80031befd553d71340e99c02f8f0d078
parentdocs(changelog): default cwd (diff)
downloadsl0ppij-caaee30179cf8e3d20ad103b898e4bad9ebf648b.tar.gz
sl0ppij-caaee30179cf8e3d20ad103b898e4bad9ebf648b.tar.xz
sl0ppij-caaee30179cf8e3d20ad103b898e4bad9ebf648b.zip
feat(plugins): reload plugin at runtime (#2372)
* fix(plugins): proper error when wasm file does not exist * reload working * race condition handling * refactor(plugins): start plugin * refactor(plugins): plugin-loader * refactor(plugins): load/reload plugin * refactor(plugins): apply cached events * fix(plugins): gittery loading * chore(plugins): rename reload-plugin to start-or-reload-plugin * chore(styling): small cleanups * style(fmt): rustfmt * style(fmt): cleanups * style(fmt): cleanups * test(e2e): update snapshots * test(e2e): update snapshots * chore(repo): comment plugin optimization because it doubles the CI time
-rw-r--r--Cargo.toml4
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap2
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap4
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap2
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap2
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap2
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap2
-rw-r--r--src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap2
-rw-r--r--zellij-server/src/lib.rs2
-rw-r--r--zellij-server/src/panes/grid.rs4
-rw-r--r--zellij-server/src/panes/plugin_pane.rs6
-rw-r--r--zellij-server/src/plugins/mod.rs53
-rw-r--r--zellij-server/src/plugins/plugin_loader.rs656
-rw-r--r--zellij-server/src/plugins/start_plugin.rs471
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs338
-rw-r--r--zellij-server/src/route.rs13
-rw-r--r--zellij-server/src/screen.rs55
-rw-r--r--zellij-server/src/tab/mod.rs19
-rw-r--r--zellij-server/src/ui/loading_indication.rs3
-rwxr-xr-x[-rw-r--r--]zellij-utils/assets/compact-bar.wasmbin397489 -> 488843 bytes
-rwxr-xr-xzellij-utils/assets/plugins/compact-bar.wasmbin488843 -> 794980 bytes
-rwxr-xr-xzellij-utils/assets/plugins/status-bar.wasmbin621120 -> 921339 bytes
-rwxr-xr-xzellij-utils/assets/plugins/strider.wasmbin502993 -> 809895 bytes
-rwxr-xr-xzellij-utils/assets/plugins/tab-bar.wasmbin459471 -> 765705 bytes
-rwxr-xr-x[-rw-r--r--]zellij-utils/assets/status-bar.wasmbin485616 -> 621120 bytes
-rwxr-xr-x[-rw-r--r--]zellij-utils/assets/strider.wasmbin413509 -> 502993 bytes
-rwxr-xr-x[-rw-r--r--]zellij-utils/assets/tab-bar.wasmbin373679 -> 459471 bytes
-rw-r--r--zellij-utils/src/cli.rs4
-rw-r--r--zellij-utils/src/errors.rs7
-rw-r--r--zellij-utils/src/input/actions.rs3
-rw-r--r--zellij-utils/src/input/layout.rs2
31 files changed, 1057 insertions, 599 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 56c855f0..f01e8f90 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -74,3 +74,7 @@ default = [ "zellij-utils/plugins_from_target" ]
disable_automatic_asset_installation = [ "zellij-utils/disable_automatic_asset_installation" ]
unstable = [ "zellij-client/unstable", "zellij-utils/unstable" ]
singlepass = [ "zellij-server/singlepass" ]
+
+# uncomment this when developing plugins in the Zellij UI to make plugin compilation faster
+# [profile.dev.package."*"]
+# opt-level = 3
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap
index 59bdf1ff..45d5bc9f 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__bracketed_paste.snap
@@ -25,5 +25,5 @@ expression: last_snapshot
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  BASE 
+ Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap
index a5e456d2..713fac4d 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap
@@ -1,7 +1,7 @@
---
source: src/tests/e2e/cases.rs
+assertion_line: 198
expression: last_snapshot
-
---
Zellij
┌──────┐
@@ -21,5 +21,5 @@ expression: last_snapshot
│ │
│ │
└──────┘
-
+
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap
index 70fa42f4..a0881f67 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__lock_mode.snap
@@ -25,5 +25,5 @@ expression: last_snapshot
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- Ctrl + <g> LOCK  <> PANE  <> TAB  <> RESIZE  <> MOVE  <> SEARCH  <> SESSION  <> QUIT   BASE 
+ Ctrl + <g> LOCK  <> PANE  <> TAB  <> RESIZE  <> MOVE  <> SEARCH  <> SESSION  <> QUIT 
-- INTERFACE LOCKED --
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap
index b45f0114..29b78eb2 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__starts_with_one_terminal.snap
@@ -25,5 +25,5 @@ expression: last_snapshot
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  BASE 
+ Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap
index 065bbfe5..2d8a5042 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__status_bar_loads_custom_keybindings.snap
@@ -25,5 +25,5 @@ expression: last_snapshot
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- <F1> LOCK  <F2> PANE  <F3> TAB  <F4> RESIZE  <F5> MOVE  <F6> SEARCH  <F7> SESSION  <F8> QUIT   BASE 
+ <F1> LOCK  <F2> PANE  <F3> TAB  <F4> RESIZE  <F5> MOVE  <F6> SEARCH  <F7> SESSION  <F8> QUIT 
Tip: UNBOUND => open new pane. UNBOUND => navigate between panes. UNBOUND => increase/decrease pane size.
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap
index 76d6ee12..7bc4b49f 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_pane.snap
@@ -25,5 +25,5 @@ expression: last_snapshot
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  BASE 
+ Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.
diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap
index 6f84144f..b817373e 100644
--- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap
+++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__undo_rename_tab.snap
@@ -25,5 +25,5 @@ expression: last_snapshot
│ │
│ │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
- Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT  BASE 
+ Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <h> MOVE  <s> SEARCH  <o> SESSION  <q> QUIT 
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index bf0f1a82..f82c9d68 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -764,7 +764,7 @@ fn init_session(
Some(&to_screen),
Some(&to_pty),
Some(&to_plugin),
- None,
+ Some(&to_server),
Some(&to_pty_writer),
Some(&to_background_jobs),
None,
diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs
index b76d21ec..6f44f6eb 100644
--- a/zellij-server/src/panes/grid.rs
+++ b/zellij-server/src/panes/grid.rs
@@ -1254,7 +1254,7 @@ impl Grid {
let new_row = Row::new(self.width).canonical();
self.viewport.push(new_row);
}
- if self.cursor.y == self.height - 1 {
+ if self.cursor.y == self.height.saturating_sub(1) {
if self.scroll_region.is_none() {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
@@ -1406,7 +1406,7 @@ impl Grid {
}
fn line_wrap(&mut self) {
self.cursor.x = 0;
- if self.cursor.y == self.height - 1 {
+ if self.cursor.y == self.height.saturating_sub(1) {
if self.alternate_screen_state.is_none() {
self.transfer_rows_to_lines_above(1);
} else {
diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs
index 8a45c3c1..7915069c 100644
--- a/zellij-server/src/panes/plugin_pane.rs
+++ b/zellij-server/src/panes/plugin_pane.rs
@@ -545,6 +545,12 @@ impl Pane for PluginPane {
self.loading_indication.to_string().as_bytes().to_vec(),
);
}
+ fn start_loading_indication(&mut self, loading_indication: LoadingIndication) {
+ self.loading_indication.merge(loading_indication);
+ self.handle_plugin_bytes_for_all_clients(
+ self.loading_indication.to_string().as_bytes().to_vec(),
+ );
+ }
fn progress_animation_offset(&mut self) {
if self.loading_indication.ended {
return;
diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs
index 2aca679a..eeb19c20 100644
--- a/zellij-server/src/plugins/mod.rs
+++ b/zellij-server/src/plugins/mod.rs
@@ -1,11 +1,11 @@
-mod start_plugin;
+mod plugin_loader;
mod wasm_bridge;
use log::info;
use std::{collections::HashMap, fs, path::PathBuf};
use wasmer::Store;
use crate::screen::ScreenInstruction;
-use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId};
+use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction};
use wasm_bridge::WasmBridge;
@@ -32,7 +32,15 @@ pub enum PluginInstruction {
),
Update(Vec<(Option<u32>, Option<ClientId>, Event)>), // Focused plugin / broadcast, client_id, event data
Unload(u32), // plugin_id
- Resize(u32, usize, usize), // plugin_id, columns, rows
+ Reload(
+ Option<bool>, // should float
+ Option<String>, // pane title
+ RunPlugin,
+ usize, // tab index
+ ClientId,
+ Size,
+ ),
+ Resize(u32, usize, usize), // plugin_id, columns, rows
AddClient(ClientId),
RemoveClient(ClientId),
NewTab(
@@ -43,7 +51,7 @@ pub enum PluginInstruction {
usize, // tab_index
ClientId,
),
- ApplyCachedEvents(u32), // u32 is the plugin id
+ ApplyCachedEvents(Vec<u32>), // a list of plugin id
Exit,
}
@@ -53,6 +61,7 @@ impl From<&PluginInstruction> for PluginContext {
PluginInstruction::Load(..) => PluginContext::Load,
PluginInstruction::Update(..) => PluginContext::Update,
PluginInstruction::Unload(..) => PluginContext::Unload,
+ PluginInstruction::Reload(..) => PluginContext::Reload,
PluginInstruction::Resize(..) => PluginContext::Resize,
PluginInstruction::Exit => PluginContext::Exit,
PluginInstruction::AddClient(_) => PluginContext::AddClient,
@@ -103,6 +112,42 @@ pub(crate) fn plugin_thread_main(
PluginInstruction::Unload(pid) => {
wasm_bridge.unload_plugin(pid)?;
},
+ PluginInstruction::Reload(
+ should_float,
+ pane_title,
+ run,
+ tab_index,
+ client_id,
+ size,
+ ) => match wasm_bridge.reload_plugin(&run) {
+ Ok(_) => {
+ let _ = bus
+ .senders
+ .send_to_server(ServerInstruction::UnblockInputThread);
+ },
+ Err(err) => match err.downcast_ref::<ZellijError>() {
+ Some(ZellijError::PluginDoesNotExist) => {
+ log::warn!("Plugin {} not found, starting it instead", run.location);
+ match wasm_bridge.load_plugin(&run, tab_index, size, client_id) {
+ Ok(plugin_id) => {
+ drop(bus.senders.send_to_screen(ScreenInstruction::AddPlugin(
+ should_float,
+ run,
+ pane_title,
+ tab_index,
+ plugin_id,
+ )));
+ },
+ Err(e) => {
+ log::error!("Failed to load plugin: {e}");
+ },
+ };
+ },
+ _ => {
+ return Err(err);
+ },
+ },
+ },
PluginInstruction::Resize(pid, new_columns, new_rows) => {
wasm_bridge.resize_plugin(pid, new_columns, new_rows)?;
},
diff --git a/zellij-server/src/plugins/plugin_loader.rs b/zellij-server/src/plugins/plugin_loader.rs
new file mode 100644
index 00000000..c5413388
--- /dev/null
+++ b/zellij-server/src/plugins/plugin_loader.rs
@@ -0,0 +1,656 @@
+use crate::plugins::wasm_bridge::{wasi_read_string, zellij_exports, PluginEnv, PluginMap};
+use highway::{HighwayHash, PortableHash};
+use log::info;
+use semver::Version;
+use std::{
+ collections::{HashMap, HashSet},
+ fmt, fs,
+ path::PathBuf,
+ sync::{Arc, Mutex},
+};
+use url::Url;
+use wasmer::{ChainableNamedResolver, Instance, Module, Store};
+use wasmer_wasi::{Pipe, WasiState};
+
+use crate::{
+ logging_pipe::LoggingPipe, screen::ScreenInstruction, thread_bus::ThreadSenders,
+ ui::loading_indication::LoadingIndication, ClientId,
+};
+
+use zellij_utils::{
+ consts::{VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR},
+ errors::prelude::*,
+ input::plugins::PluginConfig,
+ pane_size::Size,
+};
+
+macro_rules! display_loading_stage {
+ ($loading_stage:ident, $loading_indication:expr, $senders:expr, $plugin_id:expr) => {{
+ $loading_indication.$loading_stage();
+ drop(
+ $senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
+ $plugin_id,
+ $loading_indication.clone(),
+ )),
+ );
+ }};
+}
+
+/// Custom error for plugin version mismatch.
+///
+/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't
+/// match the zellij version. This is treated as a fatal error and leads to instantaneous
+/// termination.
+#[derive(Debug)]
+pub struct VersionMismatchError {
+ zellij_version: String,
+ plugin_version: String,
+ plugin_path: PathBuf,
+ // true for builtin plugins
+ builtin: bool,
+}
+
+impl std::error::Error for VersionMismatchError {}
+
+impl VersionMismatchError {
+ pub fn new(
+ zellij_version: &str,
+ plugin_version: &str,
+ plugin_path: &PathBuf,
+ builtin: bool,
+ ) -> Self {
+ VersionMismatchError {
+ zellij_version: zellij_version.to_owned(),
+ plugin_version: plugin_version.to_owned(),
+ plugin_path: plugin_path.to_owned(),
+ builtin,
+ }
+ }
+}
+
+impl fmt::Display for VersionMismatchError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let first_line = if self.builtin {
+ "It seems your version of zellij was built with outdated core plugins."
+ } else {
+ "If you're seeing this error a plugin version doesn't match the current
+zellij version."
+ };
+
+ write!(
+ f,
+ "{}
+Detected versions:
+
+- Plugin version: {}
+- Zellij version: {}
+- Offending plugin: {}
+
+If you're a user:
+ Please contact the distributor of your zellij version and report this error
+ to them.
+
+If you're a developer:
+ Please run zellij with updated plugins. The easiest way to achieve this
+ is to build zellij with `cargo xtask install`. Also refer to the docs:
+ https://github.com/zellij-org/zellij/blob/main/CONTRIBUTING.md#building
+",
+ first_line,
+ self.plugin_version.trim_end(),
+ self.zellij_version.trim_end(),
+ self.plugin_path.display()
+ )
+ }
+}
+
+// Returns `Ok` if the plugin version matches the zellij version.
+// Returns an `Err` otherwise.
+fn assert_plugin_version(instance: &Instance, plugin_env: &PluginEnv) -> Result<()> {
+ let err_context = || {
+ format!(
+ "failed to determine plugin version for plugin {}",
+ plugin_env.plugin.path.display()
+ )
+ };
+
+ let plugin_version_func = match instance.exports.get_function("plugin_version") {
+ Ok(val) => val,
+ Err(_) => {
+ return Err(anyError::new(VersionMismatchError::new(
+ VERSION,
+ "Unavailable",
+ &plugin_env.plugin.path,
+ plugin_env.plugin.is_builtin(),
+ )))
+ },
+ };
+
+ let plugin_version = plugin_version_func
+ .call(&[])
+ .map_err(anyError::new)
+ .and_then(|_| wasi_read_string(&plugin_env.wasi_env))
+ .and_then(|string| Version::parse(&string).context("failed to parse plugin version"))
+ .with_context(err_context)?;
+ let zellij_version = Version::parse(VERSION)
+ .context("failed to parse zellij version")
+ .with_context(err_context)?;
+ if plugin_version != zellij_version {
+ return Err(anyError::new(VersionMismatchError::new(
+ VERSION,
+ &plugin_version.to_string(),
+ &plugin_env.plugin.path,
+ plugin_env.plugin.is_builtin(),
+ )));
+ }
+
+ Ok(())
+}
+
+fn load_plugin_instance(instance: &mut Instance) -> Result<()> {
+ let err_context = || format!("failed to load plugin from instance {instance:#?}");
+
+ let load_function = instance
+ .exports
+ .get_function("_start")
+ .with_context(err_context)?;
+ // This eventually calls the `.load()` method
+ load_function.call(&[]).with_context(err_context)?;
+ Ok(())
+}
+
+pub struct PluginLoader<'a> {
+ plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
+ plugin_map: Arc<Mutex<PluginMap>>,
+ plugin_path: PathBuf,
+ loading_indication: &'a mut LoadingIndication,
+ senders: ThreadSenders,
+ plugin_id: u32,
+ client_id: ClientId,
+ store: Store,
+ plugin: PluginConfig,
+ plugin_dir: &'a PathBuf,
+ tab_index: usize,
+ plugin_own_data_dir: PathBuf,
+ size: Size,
+ wasm_blob_on_hd: Option<(Vec<u8>, PathBuf)>,
+}
+
+impl<'a> PluginLoader<'a> {
+ pub fn reload_plugin_from_memory(
+ plugin_id: u32,
+ plugin_dir: PathBuf,
+ plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
+ senders: ThreadSenders,
+ store: Store,
+ plugin_map: Arc<Mutex<PluginMap>>,
+ connected_clients: Arc<Mutex<Vec<ClientId>>>,
+ loading_indication: &mut LoadingIndication,
+ ) -> Result<()> {
+ let err_context = || format!("failed to reload plugin {plugin_id} from memory");
+ let mut connected_clients: Vec<ClientId> =
+ connected_clients.lock().unwrap().iter().copied().collect();
+ if connected_clients.is_empty() {
+ return Err(anyhow!("No connected clients, cannot reload plugin"));
+ }
+ let first_client_id = connected_clients.remove(0);
+
+ let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes(
+ &plugin_cache,
+ &plugin_map,
+ loading_indication,
+ &senders,
+ plugin_id,
+ first_client_id,
+ &store,
+ &plugin_dir,
+ )?;
+ plugin_loader
+ .load_module_from_memory()
+ .and_then(|module| plugin_loader.create_plugin_instance_and_environment(module))
+ .and_then(|(instance, plugin_env)| {
+ plugin_loader.load_plugin_instance(&instance, &plugin_env)?;
+ plugin_loader.clone_instance_for_other_clients(
+ &instance,
+ &plugin_env,
+ &connected_clients,
+ )
+ })
+ .with_context(err_context)?;
+ display_loading_stage!(end, loading_indication, senders, plugin_id);
+ Ok(())
+ }
+
+ pub fn start_plugin(
+ plugin_id: u32,
+ client_id: ClientId,
+ plugin: &PluginConfig,
+ tab_index: usize,
+ plugin_dir: PathBuf,
+ plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
+ senders: ThreadSenders,
+ store: Store,
+ plugin_map: Arc<Mutex<PluginMap>>,
+ size: Size,
+ connected_clients: Arc<Mutex<Vec<ClientId>>>,
+ loading_indication: &mut LoadingIndication,
+ ) -> Result<()> {
+ let err_context = || format!("failed to start plugin {plugin:#?} for client {client_id}");
+ let mut plugin_loader = PluginLoader::new(
+ &plugin_cache,
+ &plugin_map,
+ loading_indication,
+ &senders,
+ plugin_id,
+ client_id,
+ &store,
+ plugin.clone(),
+ &plugin_dir,
+ tab_index,
+ size,
+ )?;
+ plugin_loader
+ .load_module_from_memory()
+ .or_else(|_e| plugin_loader.load_module_from_hd_cache())
+ .or_else(|_e| plugin_loader.compile_module())
+ .and_then(|module| plugin_loader.create_plugin_instance_and_environment(module))
+ .and_then(|(instance, plugin_env)| {
+ plugin_loader.load_plugin_instance(&instance, &plugin_env)?;
+ plugin_loader.clone_instance_for_other_clients(
+ &instance,
+ &plugin_env,
+ &connected_clients.lock().unwrap(),
+ )
+ })
+ .with_context(err_context)?;
+ display_loading_stage!(end, loading_indication, senders, plugin_id);
+ Ok(())
+ }
+
+ pub fn reload_plugin(
+ plugin_id: u32,
+ plugin_dir: PathBuf,
+ plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
+ senders: ThreadSenders,
+ store: Store,
+ plugin_map: Arc<Mutex<PluginMap>>,
+ connected_clients: Arc<Mutex<Vec<ClientId>>>,
+ loading_indication: &mut LoadingIndication,
+ ) -> Result<()> {
+ let err_context = || format!("failed to reload plugin id {plugin_id}");
+
+ let mut connected_clients: Vec<ClientId> =
+ connected_clients.lock().unwrap().iter().copied().collect();
+ if connected_clients.is_empty() {
+ return Err(anyhow!("No connected clients, cannot reload plugin"));
+ }
+ let first_client_id = connected_clients.remove(0);
+
+ let mut plugin_loader = PluginLoader::new_from_existing_plugin_attributes(
+ &plugin_cache,
+ &plugin_map,
+ loading_indication,
+ &senders,
+ plugin_id,
+ first_client_id,
+ &store,
+ &plugin_dir,
+ )?;
+ plugin_loader
+ .compile_module()
+ .and_then(|module| plugin_loader.create_plugin_instance_and_environment(module))
+ .and_then(|(instance, plugin_env)| {
+ plugin_loader.load_plugin_instance(&instance, &plugin_env)?;
+ plugin_loader.clone_instance_for_other_clients(
+ &instance,
+ &plugin_env,
+ &connected_clients,
+ )
+ })
+ .with_context(err_context)?;
+ display_loading_stage!(end, loading_indication, senders, plugin_id);
+ Ok(())
+ }
+ pub fn new(
+ plugin_cache: &Arc<Mutex<HashMap<PathBuf, Module>>>,
+ plugin_map: &Arc<Mutex<PluginMap>>,
+ loading_indication: &'a mut LoadingIndication,
+ senders: &ThreadSenders,
+ plugin_id: u32,
+ client_id: ClientId,
+ store: &Store,
+ plugin: PluginConfig,
+ plugin_dir: &'a PathBuf,
+ tab_index: usize,
+ size: Size,
+ ) -> Result<Self> {
+ let plugin_own_data_dir = ZELLIJ_CACHE_DIR.join(Url::from(&plugin.location).to_string());
+ create_plugin_fs_entries(&plugin_own_data_dir)?;
+ let plugin_path = plugin.path.clone();
+ Ok(PluginLoader {
+ plugin_cache: plugin_cache.clone(),
+ plugin_map: plugin_map.clone(),
+ plugin_path,
+ loading_indication,
+ senders: senders.clone(),
+ plugin_id,
+ client_id,
+ store: store.clone(),
+ plugin,
+ plugin_dir,
+ tab_index,
+ plugin_own_data_dir,
+ size,
+ wasm_blob_on_hd: None,
+ })
+ }
+ pub fn new_from_existing_plugin_attributes(
+ plugin_cache: &Arc<Mutex<HashMap<PathBuf, Module>>>,
+ plugin_map: &Arc<Mutex<PluginMap>>,
+ loading_indication: &'a mut LoadingIndication,
+ senders: &ThreadSenders,
+ plugin_id: u32,
+ client_id: ClientId,
+ store: &Store,
+ plugin_dir: &'a PathBuf,
+ ) -> Result<Self> {
+ let err_context = || "Failed to find existing plugin";
+ let (_old_instance, old_user_env, (rows, cols)) = {
+ let mut plugin_map = plugin_map.lock().unwrap();
+ plugin_map
+ .remove(&(plugin_id, client_id))
+ .with_context(err_context)?
+ };
+ let tab_index = old_user_env.tab_index;
+ let size = Size { rows, cols };
+ let plugin_config = old_user_env.plugin.clone();
+ loading_indication.set_name(old_user_env.name());
+ PluginLoader::new(
+ plugin_cache,
+ plugin_map,
+ loading_indication,
+ senders,
+ plugin_id,
+ client_id,
+ store,
+ plugin_config,
+ plugin_dir,
+ tab_index,
+ size,
+ )
+ }
+ pub fn load_module_from_memory(&mut self) -> Result<Module> {
+ display_loading_stage!(
+ indicate_loading_plugin_from_memory,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ let module = self
+ .plugin_cache
+ .lock()
+ .unwrap()
+ .remove(&self.plugin_path)
+ .ok_or(anyhow!("Plugin is not stored in memory"))?;
+ display_loading_stage!(
+ indicate_loading_plugin_from_memory_success,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ Ok(module)
+ }
+ pub fn load_module_from_hd_cache(&mut self) -> Result<Module> {
+ display_loading_stage!(
+ indicate_loading_plugin_from_memory_notfound,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ display_loading_stage!(
+ indicate_loading_plugin_from_hd_cache,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ let (_wasm_bytes, cached_path) = self.plugin_bytes_and_cache_path()?;
+ let timer = std::time::Instant::now();
+ let module = unsafe { Module::deserialize_from_file(&self.store, &cached_path)? };
+ log::info!(
+ "Loaded plugin '{}' from cache folder at '{}' in {:?}",
+ self.plugin_path.display(),
+ ZELLIJ_CACHE_DIR.display(),
+ timer.elapsed(),
+ );
+ display_loading_stage!(
+ indicate_loading_plugin_from_hd_cache_success,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ Ok(module)
+ }
+ pub fn compile_module(&mut self) -> Result<Module> {
+ display_loading_stage!(
+ indicate_loading_plugin_from_hd_cache_notfound,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ display_loading_stage!(
+ indicate_compiling_plugin,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ let (wasm_bytes, cached_path) = self.plugin_bytes_and_cache_path()?;
+ let timer = std::time::Instant::now();
+ let err_context = || "failed to recover cache dir";
+ let module = fs::create_dir_all(ZELLIJ_CACHE_DIR.to_owned())
+ .map_err(anyError::new)
+ .and_then(|_| {
+ // compile module
+ Module::new(&self.store, &wasm_bytes).map_err(anyError::new)
+ })
+ .and_then(|m| {
+ // serialize module to HD cache for faster loading in the future
+ m.serialize_to_file(&cached_path).map_err(anyError::new)?;
+ log::info!(
+ "Compiled plugin '{}' in {:?}",
+ self.plugin_path.display(),
+ timer.elapsed()
+ );
+ Ok(m)
+ })
+ .with_context(err_context)?;
+ Ok(module)
+ }
+ pub fn create_plugin_instance_and_environment(
+ &mut self,
+ module: Module,
+ ) -> Result<(Instance, PluginEnv)> {
+ let err_context = || {
+ format!(
+ "Failed to create instance and plugin env for plugin {}",
+ self.plugin_id
+ )
+ };
+ let mut wasi_env = WasiState::new("Zellij")
+ .env("CLICOLOR_FORCE", "1")
+ .map_dir("/host", ".")
+ .and_then(|wasi| wasi.map_dir("/data", &self.plugin_own_data_dir))
+ .and_then(|wasi| wasi.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path()))
+ .and_then(|wasi| {
+ wasi.stdin(Box::new(Pipe::new()))
+ .stdout(Box::new(Pipe::new()))
+ .stderr(Box::new(LoggingPipe::new(
+ &self.plugin.location.to_string(),
+ self.plugin_id,
+ )))
+ .finalize()
+ })
+ .with_context(err_context)?;
+ let wasi = wasi_env.import_object(&module).with_context(err_context)?;
+
+ let mut mut_plugin = self.plugin.clone();
+ mut_plugin.set_tab_index(self.tab_index);
+ let plugin_env = PluginEnv {
+ plugin_id: self.plugin_id,
+ client_id: self.client_id,
+ plugin: mut_plugin,
+ senders: self.senders.clone(),
+ wasi_env,
+ subscriptions: Arc::new(Mutex::new(HashSet::new())),
+ plugin_own_data_dir: self.plugin_own_data_dir.clone(),
+ tab_index: self.tab_index,
+ };
+
+ let zellij = zellij_exports(&self.store, &plugin_env);
+ let instance =
+ Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?;
+ assert_plugin_version(&instance, &plugin_env).with_context(err_context)?;
+ // Only do an insert when everything went well!
+ let cloned_plugin = self.plugin.clone();
+ self.plugin_cache
+ .lock()
+ .unwrap()
+ .insert(cloned_plugin.path, module);
+ Ok((instance, plugin_env))
+ }
+ pub fn load_plugin_instance(
+ &mut self,
+ instance: &Instance,
+ plugin_env: &PluginEnv,
+ ) -> Result<()> {
+ let err_context = || format!("failed to load plugin from instance {instance:#?}");
+ let main_user_instance = instance.clone();
+ let main_user_env = plugin_env.clone();
+ display_loading_stage!(
+ indicate_starting_plugin,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ let load_function = instance
+ .exports
+ .get_function("_start")
+ .with_context(err_context)?;
+ // This eventually calls the `.load()` method
+ load_function.call(&[]).with_context(err_context)?;
+ display_loading_stage!(
+ indicate_starting_plugin_success,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ display_loading_stage!(
+ indicate_writing_plugin_to_cache,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ let mut plugin_map = self.plugin_map.lock().unwrap();
+ plugin_map.insert(
+ (self.plugin_id, self.client_id),
+ (
+ main_user_instance,
+ main_user_env,
+ (self.size.rows, self.size.cols),
+ ),
+ );
+ display_loading_stage!(
+ indicate_writing_plugin_to_cache_success,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ Ok(())
+ }
+ pub fn clone_instance_for_other_clients(
+ &mut self,
+ instance: &Instance,
+ plugin_env: &PluginEnv,
+ connected_clients: &[ClientId],
+ ) -> Result<()> {
+ if !connected_clients.is_empty() {
+ display_loading_stage!(
+ indicate_cloning_plugin_for_other_clients,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ let mut plugin_map = self.plugin_map.lock().unwrap();
+ for client_id in connected_clients {
+ let (instance, new_plugin_env) =
+ clone_plugin_for_client(&plugin_env, *client_id, &instance, &self.store)?;
+ plugin_map.insert(
+ (self.plugin_id, *client_id),
+ (instance, new_plugin_env, (self.size.rows, self.size.cols)),
+ );
+ }
+ display_loading_stage!(
+ indicate_cloning_plugin_for_other_clients_success,
+ self.loading_indication,
+ self.senders,
+ self.plugin_id
+ );
+ }
+ Ok(())
+ }
+ fn plugin_bytes_and_cache_path(&mut self) -> Result<(Vec<u8>, PathBuf)> {
+ match self.wasm_blob_on_hd.as_ref() {
+ Some((wasm_bytes, cached_path)) => Ok((wasm_bytes.clone(), cached_path.clone())),
+ None => {
+ if self.plugin._allow_exec_host_cmd {
+ info!(
+ "Plugin({:?}) is able to run any host command, this may lead to some security issues!",
+ self.plugin.path
+ );
+ }
+ // The plugins blob as stored on the filesystem
+ let wasm_bytes = self.plugin.resolve_wasm_bytes(&self.plugin_dir)?;
+ let hash: String = PortableHash::default()
+ .hash256(&wasm_bytes)
+ .iter()
+ .map(ToString::to_string)
+ .collect();
+ let cached_path = ZELLIJ_CACHE_DIR.join(&hash);
+ self.wasm_blob_on_hd = Some((wasm_bytes.clone(), cached_path.clone()));
+ Ok((wasm_bytes, cached_path))
+ },
+ }
+ }
+}
+
+fn create_plugin_fs_entries(plugin_own_data_dir: &PathBuf) -> Result<()> {
+ let err_context = || "failed to create plugin fs entries";
+ // Create filesystem entries mounted into WASM.
+ // We create them here to get expressive error messages in case they fail.
+ fs::create_dir_all(&plugin_own_data_dir)
+ .with_context(|| format!("failed to create datadir in {plugin_own_data_dir:?}"))
+ .with_context(err_context)?;
+ fs::create_dir_all(ZELLIJ_TMP_DIR.as_path())
+ .with_context(|| format!("failed to create tmpdir at {:?}", &ZELLIJ_TMP_DIR.as_path()))
+ .with_context(err_context)?;
+ Ok(())
+}
+
+fn clone_plugin_for_client(
+ plugin_env: &PluginEnv,
+ client_id: ClientId,
+ instance: &Instance,
+ store: &Store,
+) -> Result<(Instance, PluginEnv)> {
+ let err_context = || format!("Failed to clone plugin for client {client_id}");
+ let mut new_plugin_env = plugin_env.clone();
+ new_plugin_env.client_id = client_id;
+ let module = instance.module().clone();
+ let wasi = new_plugin_env
+ .wasi_env
+ .import_object(&module)
+ .with_context(err_context)?;
+ let zellij = zellij_exports(store, &new_plugin_env);
+ let mut instance =
+ Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?;
+ load_plugin_instance(&mut instance).with_context(err_context)?;
+ Ok((instance, new_plugin_env))
+}
diff --git a/zellij-server/src/plugins/start_plugin.rs b/zellij-server/src/plugins/start_plugin.rs
deleted file mode 100644
index 28000bd1..00000000
--- a/zellij-server/src/plugins/start_plugin.rs
+++ /dev/null
@@ -1,471 +0,0 @@
-use crate::plugins::wasm_bridge::{wasi_read_string, zellij_exports, PluginEnv, PluginMap};
-use highway::{HighwayHash, PortableHash};
-use log::info;
-use semver::Version;
-use std::{
- collections::{HashMap, HashSet},
- fmt, fs,
- path::PathBuf,
- sync::{Arc, Mutex},
- time::Instant,
-};
-use url::Url;
-use wasmer::{ChainableNamedResolver, Instance, Module, Store};
-use wasmer_wasi::{Pipe, WasiState};
-
-use crate::{
- logging_pipe::LoggingPipe, screen::ScreenInstruction, thread_bus::ThreadSenders,
- ui::loading_indication::LoadingIndication, ClientId,
-};
-
-use zellij_utils::{
- consts::{VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR},
- errors::prelude::*,
- input::plugins::PluginConfig,
- pane_size::Size,
-};
-
-/// Custom error for plugin version mismatch.
-///
-/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't
-/// match the zellij version. This is treated as a fatal error and leads to instantaneous
-/// termination.
-#[derive(Debug)]
-pub struct VersionMismatchError {
- zellij_version: String,
- plugin_version: String,
- plugin_path: PathBuf,
- // true for builtin plugins
- builtin: bool,
-}
-
-impl std::error::Error for VersionMismatchError {}
-
-impl VersionMismatchError {
- pub fn new(
- zellij_version: &str,
- plugin_version: &str,
- plugin_path: &PathBuf,
- builtin: bool,
- ) -> Self {
- VersionMismatchError {
- zellij_version: zellij_version.to_owned(),
- plugin_version: plugin_version.to_owned(),
- plugin_path: plugin_path.to_owned(),
- builtin,
- }
- }
-}
-
-impl fmt::Display for VersionMismatchError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let first_line = if self.builtin {
- "It seems your version of zellij was built with outdated core plugins."
- } else {
- "If you're seeing this error a plugin version doesn't match the current
-zellij version."
- };
-
- write!(
- f,
- "{}
-Detected versions:
-
-- Plugin version: {}
-- Zellij version: {}
-- Offending plugin: {}
-
-If you're a user:
- Please contact the distributor of your zellij version and report this error
- to them.
-
-If you're a developer:
- Please run zellij with updated plugins. The easiest way to achieve this
- is to build zellij with `cargo xtask install`. Also refer to the docs:
- https://github.com/zellij-org/zellij/blob/main/CONTRIBUTING.md#building
-",
- first_line,
- self.plugin_version.trim_end(),
- self.zellij_version.trim_end(),
- self.plugin_path.display()
- )
- }
-}
-
-// Returns `Ok` if the plugin version matches the zellij version.
-// Returns an `Err` otherwise.
-fn assert_plugin_version(instance: &Instance, plugin_env: &PluginEnv) -> Result<()> {
- let err_context = || {
- format!(
- "failed to determine plugin version for plugin {}",
- plugin_env.plugin.path.display()
- )
- };
-
- let plugin_version_func = match instance.exports.get_function("plugin_version") {
- Ok(val) => val,
- Err(_) => {
- return Err(anyError::new(VersionMismatchError::new(
- VERSION,
- "Unavailable",
- &plugin_env.plugin.path,
- plugin_env.plugin.is_builtin(),
- )))
- },
- };
-
- let plugin_version = plugin_version_func
- .call(&[])
- .map_err(anyError::new)
- .and_then(|_| wasi_read_string(&plugin_env.wasi_env))
- .and_then(|string| Version::parse(&string).context("failed to parse plugin version"))
- .with_context(err_context)?;
- let zellij_version = Version::parse(VERSION)
- .context("failed to parse zellij version")
- .with_context(err_context)?;
- if plugin_version != zellij_version {
- return Err(anyError::new(VersionMismatchError::new(
- VERSION,
- &plugin_version.to_string(),
- &plugin_env.plugin.path,
- plugin_env.plugin.is_builtin(),
- )));
- }
-
- Ok(())
-}
-
-fn load_plugin_instance(instance: &mut Instance) -> Result<()> {
- let err_context = || format!("failed to load plugin from instance {instance:#?}");
-
- let load_function = instance
- .exports
- .get_function("_start")
- .with_context(err_context)?;
- // This eventually calls the `.load()` method
- load_function.call(&[]).with_context(err_context)?;
- Ok(())
-}
-
-pub fn start_plugin(
- plugin_id: u32,
- client_id: ClientId,
- plugin: &PluginConfig,
- tab_index: usize,
- plugin_dir: PathBuf,
- plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
- senders: ThreadSenders,
- mut store: Store,
- plugin_map: Arc<Mutex<PluginMap>>,
- size: Size,
- connected_clients: Arc<Mutex<Vec<ClientId>>>,
- loading_indication: &mut LoadingIndication,
-) -> Result<()> {
- let err_context = || format!("failed to start plugin {plugin:#?} for client {client_id}");
- let plugin_own_data_dir = ZELLIJ_CACHE_DIR.join(Url::from(&plugin.location).to_string());
- create_plugin_fs_entries(&plugin_own_data_dir)?;
-
- loading_indication.indicate_loading_plugin_from_memory();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- let (module, cache_hit) = {
- let mut plugin_cache = plugin_cache.lock().unwrap();
- let (module, cache_hit) = load_module_from_memory(&mut *plugin_cache, &plugin.path);
- (module, cache_hit)
- };
-
- let module = match module {
- Some(module) => {
- loading_indication.indicate_loading_plugin_from_memory_success();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- module
- },
- None => {
- loading_indication.indicate_loading_plugin_from_memory_notfound();
- loading_indication.indicate_loading_plugin_from_hd_cache();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
-
- let (wasm_bytes, cached_path) = plugin_bytes_and_cache_path(&plugin, &plugin_dir);
- let timer = std::time::Instant::now();
- match load_module_from_hd_cache(&mut store, &plugin.path, &timer, &cached_path) {
- Ok(module) => {
- loading_indication.indicate_loading_plugin_from_hd_cache_success();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- module
- },
- Err(_e) => {
- loading_indication.indicate_loading_plugin_from_hd_cache_notfound();
- loading_indication.indicate_compiling_plugin();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- let module =
- compile_module(&mut store, &plugin.path, &timer, &cached_path, wasm_bytes)?;
- loading_indication.indicate_compiling_plugin_success();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- module
- },
- }
- },
- };
-
- let (instance, plugin_env) = create_plugin_instance_and_environment(
- plugin_id,
- client_id,
- plugin,
- &module,
- tab_index,
- plugin_own_data_dir,
- senders.clone(),
- &mut store,
- )?;
-
- if !cache_hit {
- // Check plugin version
- assert_plugin_version(&instance, &plugin_env).with_context(err_context)?;
- }
-
- // Only do an insert when everything went well!
- let cloned_plugin = plugin.clone();
- {
- let mut plugin_cache = plugin_cache.lock().unwrap();
- plugin_cache.insert(cloned_plugin.path, module);
- }
-
- let mut main_user_instance = instance.clone();
- let main_user_env = plugin_env.clone();
- loading_indication.indicate_starting_plugin();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- load_plugin_instance(&mut main_user_instance).with_context(err_context)?;
- loading_indication.indicate_starting_plugin_success();
- loading_indication.indicate_writing_plugin_to_cache();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
-
- {
- let mut plugin_map = plugin_map.lock().unwrap();
- plugin_map.insert(
- (plugin_id, client_id),
- (main_user_instance, main_user_env, (size.rows, size.cols)),
- );
- }
-
- loading_indication.indicate_writing_plugin_to_cache_success();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
-
- let connected_clients: Vec<ClientId> =
- connected_clients.lock().unwrap().iter().copied().collect();
- if !connected_clients.is_empty() {
- loading_indication.indicate_cloning_plugin_for_other_clients();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- let mut plugin_map = plugin_map.lock().unwrap();
- for client_id in connected_clients {
- let (instance, new_plugin_env) =
- clone_plugin_for_client(&plugin_env, client_id, &instance, &mut store)?;
- plugin_map.insert(
- (plugin_id, client_id),
- (instance, new_plugin_env, (size.rows, size.cols)),
- );
- }
- loading_indication.indicate_cloning_plugin_for_other_clients_success();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- }
- loading_indication.end();
- let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- Ok(())
-}
-
-fn create_plugin_fs_entries(plugin_own_data_dir: &PathBuf) -> Result<()> {
- let err_context = || "failed to create plugin fs entries";
- // Create filesystem entries mounted into WASM.
- // We create them here to get expressive error messages in case they fail.
- fs::create_dir_all(&plugin_own_data_dir)
- .with_context(|| format!("failed to create datadir in {plugin_own_data_dir:?}"))
- .with_context(err_context)?;
- fs::create_dir_all(ZELLIJ_TMP_DIR.as_path())
- .with_context(|| format!("failed to create tmpdir at {:?}", &ZELLIJ_TMP_DIR.as_path()))
- .with_context(err_context)?;
- Ok(())
-}
-
-fn compile_module(
- store: &mut Store,
- plugin_path: &PathBuf,
- timer: &Instant,
- cached_path: &PathBuf,
- wasm_bytes: Vec<u8>,
-) -> Result<Module> {
- let err_context = || "failed to recover cache dir";
- fs::create_dir_all(ZELLIJ_CACHE_DIR.to_owned())
- .map_err(anyError::new)
- .and_then(|_| {
- // compile module
- Module::new(&*store, &wasm_bytes).map_err(anyError::new)
- })
- .map(|m| {
- // serialize module to HD cache for faster loading in the future
- m.serialize_to_file(&cached_path).map_err(anyError::new)?;
- log::info!(
- "Compiled plugin '{}' in {:?}",
- plugin_path.display(),
- timer.elapsed()
- );
- Ok(m)
- })
- .with_context(err_context)?
-}
-
-fn load_module_from_hd_cache(
- store: &mut Store,
- plugin_path: &PathBuf,
- timer: &Instant,
- cached_path: &PathBuf,
-) -> Result<Module> {
- let module = unsafe { Module::deserialize_from_file(&*store, &cached_path)? };
- log::info!(
- "Loaded plugin '{}' from cache folder at '{}' in {:?}",
- plugin_path.display(),
- ZELLIJ_CACHE_DIR.display(),
- timer.elapsed(),
- );
- Ok(module)
-}
-
-fn plugin_bytes_and_cache_path(plugin: &PluginConfig, plugin_dir: &PathBuf) -> (Vec<u8>, PathBuf) {
- let err_context = || "failed to get plugin bytes and cached path";
- // Populate plugin module cache for this plugin!
- // Is it in the cache folder already?
- if plugin._allow_exec_host_cmd {
- info!(
- "Plugin({:?}) is able to run any host command, this may lead to some security issues!",
- plugin.path
- );
- }
- // The plugins blob as stored on the filesystem
- let wasm_bytes = plugin
- .resolve_wasm_bytes(&plugin_dir)
- .with_context(err_context)
- .fatal();
- let hash: String = PortableHash::default()
- .hash256(&wasm_bytes)
- .iter()
- .map(ToString::to_string)
- .collect();
- let cached_path = ZELLIJ_CACHE_DIR.join(&hash);
- (wasm_bytes, cached_path)
-}
-
-fn load_module_from_memory(
- plugin_cache: &mut HashMap<PathBuf, Module>,
- plugin_path: &PathBuf,
-) -> (Option<Module>, bool) {
- let module = plugin_cache.remove(plugin_path);
- let mut cache_hit = false;
- if module.is_some() {
- cache_hit = true;
- log::debug!(
- "Loaded plugin '{}' from plugin cache",
- plugin_path.display()
- );
- }
- (module, cache_hit)
-}
-
-fn create_plugin_instance_and_environment(
- plugin_id: u32,
- client_id: ClientId,
- plugin: &PluginConfig,
- module: &Module,
- tab_index: usize,
- plugin_own_data_dir: PathBuf,
- senders: ThreadSenders,
- store: &mut Store,
-) -> Result<(Instance, PluginEnv)> {
- let err_context = || format!("Failed to create instance and plugin env for plugin {plugin_id}");
- let mut wasi_env = WasiState::new("Zellij")
- .env("CLICOLOR_FORCE", "1")
- .map_dir("/host", ".")
- .and_then(|wasi| wasi.map_dir("/data", &plugin_own_data_dir))
- .and_then(|wasi| wasi.map_dir("/tmp", ZELLIJ_TMP_DIR.as_path()))
- .and_then(|wasi| {
- wasi.stdin(Box::new(Pipe::new()))
- .stdout(Box::new(Pipe::new()))
- .stderr(Box::new(LoggingPipe::new(
- &plugin.location.to_string(),
- plugin_id,
- )))
- .finalize()
- })
- .with_context(err_context)?;
- let wasi = wasi_env.import_object(&module).with_context(err_context)?;
-
- let mut mut_plugin = plugin.clone();
- mut_plugin.set_tab_index(tab_index);
- let plugin_env = PluginEnv {
- plugin_id,
- client_id,
- plugin: mut_plugin,
- senders: senders.clone(),
- wasi_env,
- subscriptions: Arc::new(Mutex::new(HashSet::new())),
- plugin_own_data_dir,
- tab_index,
- };
-
- let zellij = zellij_exports(&store, &plugin_env);
- let instance = Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?;
- Ok((instance, plugin_env))
-}
-
-fn clone_plugin_for_client(
- plugin_env: &PluginEnv,
- client_id: ClientId,
- instance: &Instance,
- store: &Store,
-) -> Result<(Instance, PluginEnv)> {
- let err_context = || format!("Failed to clone plugin for client {client_id}");
- let mut new_plugin_env = plugin_env.clone();
- new_plugin_env.client_id = client_id;
- let module = instance.module().clone();
- let wasi = new_plugin_env
- .wasi_env
- .import_object(&module)
- .with_context(err_context)?;
- let zellij = zellij_exports(store, &new_plugin_env);
- let mut instance =
- Instance::new(&module, &zellij.chain_back(wasi)).with_context(err_context)?;
- load_plugin_instance(&mut instance).with_context(err_context)?;
- Ok((instance, new_plugin_env))
-}
diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs
index 68f8bc6d..119a1d24 100644
--- a/zellij-server/src/plugins/wasm_bridge.rs
+++ b/zellij-server/src/plugins/wasm_bridge.rs
@@ -1,10 +1,10 @@
use super::PluginInstruction;
-use crate::plugins::start_plugin::start_plugin;
+use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError};
use log::{debug, info, warn};
use serde::{de::DeserializeOwned, Serialize};
use std::{
collections::{HashMap, HashSet},
- fmt,
+ fmt::Display,
path::PathBuf,
process,
str::FromStr,
@@ -33,81 +33,17 @@ use zellij_utils::{
consts::VERSION,
data::{Event, EventType, PluginIds},
errors::prelude::*,
+ errors::ZellijError,
input::{
command::TerminalAction,
- layout::RunPlugin,
+ layout::{RunPlugin, RunPluginLocation},
plugins::{PluginConfig, PluginType, PluginsConfig},
},
pane_size::Size,
serde,
};
-/// Custom error for plugin version mismatch.
-///
-/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't
-/// match the zellij version. This is treated as a fatal error and leads to instantaneous
-/// termination.
-#[derive(Debug)]
-pub struct VersionMismatchError {
- zellij_version: String,
- plugin_version: String,
- plugin_path: PathBuf,
- // true for builtin plugins
- builtin: bool,
-}
-
-impl std::error::Error for VersionMismatchError {}
-
-impl VersionMismatchError {
- pub fn new(
- zellij_version: &str,
- plugin_version: &str,
- plugin_path: &PathBuf,
- builtin: bool,
- ) -> Self {
- VersionMismatchError {
- zellij_version: zellij_version.to_owned(),
- plugin_version: plugin_version.to_owned(),
- plugin_path: plugin_path.to_owned(),
- builtin,
- }
- }
-}
-
-impl fmt::Display for VersionMismatchError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let first_line = if self.builtin {
- "It seems your version of zellij was built with outdated core plugins."
- } else {
- "If you're seeing this error a plugin version doesn't match the current
-zellij version."
- };
-
- write!(
- f,
- "{}
-Detected versions:
-
-- Plugin version: {}
-- Zellij version: {}
-- Offending plugin: {}
-
-If you're a user:
- Please contact the distributor of your zellij version and report this error
- to them.
-
-If you're a developer:
- Please run zellij with updated plugins. The easiest way to achieve this
- is to build zellij with `cargo xtask install`. Also refer to the docs:
- https://github.com/zellij-org/zellij/blob/main/CONTRIBUTING.md#building
-",
- first_line,
- self.plugin_version.trim_end(),
- self.zellij_version.trim_end(),
- self.plugin_path.display()
- )
- }
-}
+type PluginId = u32;
#[derive(WasmerEnv, Clone)]
pub struct PluginEnv {
@@ -150,7 +86,8 @@ pub struct WasmBridge {
next_plugin_id: u32,
cached_events_for_pending_plugins: HashMap<u32, Vec<Event>>, // u32 is the plugin id
cached_resizes_for_pending_plugins: HashMap<u32, (usize, usize)>, // (rows, columns)
- loading_plugins: HashMap<u32, JoinHandle<()>>, // plugin_id to join-handle
+ loading_plugins: HashMap<(u32, RunPlugin), JoinHandle<()>>, // plugin_id to join-handle
+ pending_plugin_reloads: HashSet<RunPlugin>,
}
impl WasmBridge {
@@ -176,6 +113,7 @@ impl WasmBridge {
cached_events_for_pending_plugins: HashMap::new(),
cached_resizes_for_pending_plugins: HashMap::new(),
loading_plugins: HashMap::new(),
+ pending_plugin_reloads: HashSet::new(),
}
}
pub fn load_plugin(
@@ -196,12 +134,10 @@ impl WasmBridge {
.with_context(err_context)?;
let plugin_name = run.location.to_string();
- self.next_plugin_id += 1;
-
self.cached_events_for_pending_plugins
.insert(plugin_id, vec![]);
self.cached_resizes_for_pending_plugins
- .insert(plugin_id, (0, 0));
+ .insert(plugin_id, (size.rows, size.cols));
let load_plugin_task = task::spawn({
let plugin_dir = self.plugin_dir.clone();
@@ -214,7 +150,7 @@ impl WasmBridge {
let _ =
senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id));
let mut loading_indication = LoadingIndication::new(plugin_name.clone());
- match start_plugin(
+ match PluginLoader::start_plugin(
plugin_id,
client_id,
&plugin,
@@ -228,30 +164,20 @@ impl WasmBridge {
connected_clients.clone(),
&mut loading_indication,
) {
- Ok(_) => {
- let _ = senders.send_to_background_jobs(
- BackgroundJob::StopPluginLoadingAnimation(plugin_id),
- );
- let _ =
- senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_id));
- },
- Err(e) => {
- let _ = senders.send_to_background_jobs(
- BackgroundJob::StopPluginLoadingAnimation(plugin_id),
- );
- let _ =
- senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_id));
- loading_indication.indicate_loading_error(e.to_string());
- let _ =
- senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
- plugin_id,
- loading_indication.clone(),
- ));
- },
+ Ok(_) => handle_plugin_successful_loading(&senders, plugin_id),
+ Err(e) => handle_plugin_loading_failure(
+ &senders,
+ plugin_id,
+ &mut loading_indication,
+ e,
+ ),
}
+ let _ =
+ senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(vec![plugin_id]));
}
});
- self.loading_plugins.insert(plugin_id, load_plugin_task);
+ self.loading_plugins
+ .insert((plugin_id, run.clone()), load_plugin_task);
self.next_plugin_id += 1;
Ok(plugin_id)
}
@@ -267,6 +193,89 @@ impl WasmBridge {
}
Ok(())
}
+ pub fn reload_plugin(&mut self, run_plugin: &RunPlugin) -> Result<()> {
+ if self.plugin_is_currently_being_loaded(&run_plugin.location) {
+ self.pending_plugin_reloads.insert(run_plugin.clone());
+ return Ok(());
+ }
+
+ let plugin_ids = self.all_plugin_ids_for_plugin_location(&run_plugin.location)?;
+ for plugin_id in &plugin_ids {
+ let (rows, columns) = self.size_of_plugin_id(*plugin_id).unwrap_or((0, 0));
+ self.cached_events_for_pending_plugins
+ .insert(*plugin_id, vec![]);
+ self.cached_resizes_for_pending_plugins
+ .insert(*plugin_id, (rows, columns));
+ }
+
+ let first_plugin_id = *plugin_ids.get(0).unwrap(); // this is safe becaise the above
+ // methods always returns at least 1 id
+ let mut loading_indication = LoadingIndication::new(run_plugin.location.to_string());
+ self.start_plugin_loading_indication(&plugin_ids, &loading_indication);
+ let load_plugin_task = task::spawn({
+ let plugin_dir = self.plugin_dir.clone();
+ let plugin_cache = self.plugin_cache.clone();
+ let senders = self.senders.clone();
+ let store = self.store.clone();
+ let plugin_map = self.plugin_map.clone();
+ let connected_clients = self.connected_clients.clone();
+ async move {
+ match PluginLoader::reload_plugin(
+ first_plugin_id,
+ plugin_dir.clone(),
+ plugin_cache.clone(),
+ senders.clone(),
+ store.clone(),
+ plugin_map.clone(),
+ connected_clients.clone(),
+ &mut loading_indication,
+ ) {
+ Ok(_) => {
+ handle_plugin_successful_loading(&senders, first_plugin_id);
+ for plugin_id in &plugin_ids {
+ if plugin_id == &first_plugin_id {
+ // no need to reload the plugin we just reloaded
+ continue;
+ }
+ let mut loading_indication = LoadingIndication::new("".into());
+ match PluginLoader::reload_plugin_from_memory(
+ *plugin_id,
+ plugin_dir.clone(),
+ plugin_cache.clone(),
+ senders.clone(),
+ store.clone(),
+ plugin_map.clone(),
+ connected_clients.clone(),
+ &mut loading_indication,
+ ) {
+ Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id),
+ Err(e) => handle_plugin_loading_failure(
+ &senders,
+ *plugin_id,
+ &mut loading_indication,
+ e,
+ ),
+ }
+ }
+ },
+ Err(e) => {
+ for plugin_id in &plugin_ids {
+ handle_plugin_loading_failure(
+ &senders,
+ *plugin_id,
+ &mut loading_indication,
+ &e,
+ );
+ }
+ },
+ }
+ let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_ids));
+ }
+ });
+ self.loading_plugins
+ .insert((first_plugin_id, run_plugin.clone()), load_plugin_task);
+ Ok(())
+ }
pub fn add_client(&mut self, client_id: ClientId) -> Result<()> {
let err_context = || format!("failed to add plugins for client {client_id}");
@@ -311,6 +320,12 @@ impl WasmBridge {
for ((plugin_id, client_id), (instance, plugin_env, (current_rows, current_columns))) in
plugin_map.iter_mut()
{
+ if self
+ .cached_resizes_for_pending_plugins
+ .contains_key(&plugin_id)
+ {
+ continue;
+ }
if *plugin_id == pid {
*current_rows = new_rows;
*current_columns = new_columns;
@@ -334,12 +349,10 @@ impl WasmBridge {
plugin_bytes.push((*plugin_id, *client_id, rendered_bytes.as_bytes().to_vec()));
}
}
- for (plugin_id, (current_rows, current_columns)) in
- self.cached_resizes_for_pending_plugins.iter_mut()
- {
+ for (plugin_id, mut current_size) in self.cached_resizes_for_pending_plugins.iter_mut() {
if *plugin_id == pid {
- *current_rows = new_rows;
- *current_columns = new_columns;
+ current_size.0 = new_rows;
+ current_size.1 = new_columns;
}
}
let _ = self
@@ -357,6 +370,12 @@ impl WasmBridge {
let mut plugin_bytes = vec![];
for (pid, cid, event) in updates.drain(..) {
for (&(plugin_id, client_id), (instance, plugin_env, (rows, columns))) in &*plugin_map {
+ if self
+ .cached_events_for_pending_plugins
+ .contains_key(&plugin_id)
+ {
+ continue;
+ }
let subs = plugin_env
.subscriptions
.lock()
@@ -394,8 +413,42 @@ impl WasmBridge {
.send_to_screen(ScreenInstruction::PluginBytes(plugin_bytes));
Ok(())
}
- pub fn apply_cached_events(&mut self, plugin_id: u32) -> Result<()> {
- let err_context = || format!("Failed to apply cached events to plugin {plugin_id}");
+ pub fn apply_cached_events(&mut self, plugin_ids: Vec<u32>) -> Result<()> {
+ let mut applied_plugin_paths = HashSet::new();
+ for plugin_id in plugin_ids {
+ self.apply_cached_events_and_resizes_for_plugin(plugin_id)?;
+ if let Some(run_plugin) = self.run_plugin_of_plugin_id(plugin_id) {
+ applied_plugin_paths.insert(run_plugin.clone());
+ }
+ self.loading_plugins
+ .retain(|(p_id, _run_plugin), _| p_id != &plugin_id);
+ }
+ for run_plugin in applied_plugin_paths.drain() {
+ if self.pending_plugin_reloads.remove(&run_plugin) {
+ let _ = self.reload_plugin(&run_plugin);
+ }
+ }
+ Ok(())
+ }
+ pub fn remove_client(&mut self, client_id: ClientId) {
+ self.connected_clients
+ .lock()
+ .unwrap()
+ .retain(|c| c != &client_id);
+ }
+ pub fn cleanup(&mut self) {
+ for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() {
+ drop(loading_plugin_task.cancel());
+ }
+ }
+ fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<&RunPlugin> {
+ self.loading_plugins
+ .iter()
+ .find(|((p_id, _run_plugin), _)| p_id == &plugin_id)
+ .map(|((_p_id, run_plugin), _)| run_plugin)
+ }
+ fn apply_cached_events_and_resizes_for_plugin(&mut self, plugin_id: PluginId) -> Result<()> {
+ let err_context = || format!("Failed to apply cached events to plugin");
if let Some(events) = self.cached_events_for_pending_plugins.remove(&plugin_id) {
let mut plugin_map = self.plugin_map.lock().unwrap();
let all_connected_clients: Vec<ClientId> = self
@@ -405,10 +458,10 @@ impl WasmBridge {
.iter()
.copied()
.collect();
- for client_id in all_connected_clients {
+ for client_id in &all_connected_clients {
let mut plugin_bytes = vec![];
if let Some((instance, plugin_env, (rows, columns))) =
- plugin_map.get_mut(&(plugin_id, client_id))
+ plugin_map.get_mut(&(plugin_id, *client_id))
{
let subs = plugin_env
.subscriptions
@@ -423,7 +476,7 @@ impl WasmBridge {
}
apply_event_to_plugin(
plugin_id,
- client_id,
+ *client_id,
&instance,
&plugin_env,
&event,
@@ -441,22 +494,83 @@ impl WasmBridge {
if let Some((rows, columns)) = self.cached_resizes_for_pending_plugins.remove(&plugin_id) {
self.resize_plugin(plugin_id, columns, rows)?;
}
- self.loading_plugins.remove(&plugin_id);
Ok(())
}
- pub fn remove_client(&mut self, client_id: ClientId) {
- self.connected_clients
+ fn plugin_is_currently_being_loaded(&self, plugin_location: &RunPluginLocation) -> bool {
+ self.loading_plugins
+ .iter()
+ .find(|((_plugin_id, run_plugin), _)| &run_plugin.location == plugin_location)
+ .is_some()
+ }
+ fn all_plugin_ids_for_plugin_location(
+ &self,
+ plugin_location: &RunPluginLocation,
+ ) -> Result<Vec<PluginId>> {
+ let err_context = || format!("Failed to get plugin ids for location {plugin_location}");
+ let plugin_ids: Vec<PluginId> = self
+ .plugin_map
.lock()
.unwrap()
- .retain(|c| c != &client_id);
+ .iter()
+ .filter(
+ |((_plugin_id, _client_id), (_instance, plugin_env, _size))| {
+ &plugin_env.plugin.location == plugin_location
+ },
+ )
+ .map(|((plugin_id, _client_id), _)| *plugin_id)
+ .collect();
+ if plugin_ids.is_empty() {
+ return Err(ZellijError::PluginDoesNotExist).with_context(err_context);
+ }
+ Ok(plugin_ids)
}
- pub fn cleanup(&mut self) {
- for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() {
- drop(loading_plugin_task.cancel());
+ fn size_of_plugin_id(&self, plugin_id: PluginId) -> Option<(usize, usize)> {
+ // (rows/colums)
+ self.plugin_map
+ .lock()
+ .unwrap()
+ .iter()
+ .find(|((p_id, _client_id), (_instance, _plugin_env, _size))| *p_id == plugin_id)
+ .map(|((_p_id, _client_id), (_instance, _plugin_env, size))| *size)
+ }
+ fn start_plugin_loading_indication(
+ &self,
+ plugin_ids: &[PluginId],
+ loading_indication: &LoadingIndication,
+ ) {
+ for plugin_id in plugin_ids {
+ let _ = self
+ .senders
+ .send_to_screen(ScreenInstruction::StartPluginLoadingIndication(
+ *plugin_id,
+ loading_indication.clone(),
+ ));
+ let _ = self
+ .senders
+ .send_to_background_jobs(BackgroundJob::AnimatePluginLoading(*plugin_id));
}
}
}
+fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) {
+ let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id));
+ let _ = senders.send_to_screen(ScreenInstruction::RequestStateUpdateForPlugins);
+}
+
+fn handle_plugin_loading_failure(
+ senders: &ThreadSenders,
+ plugin_id: PluginId,
+ loading_indication: &mut LoadingIndication,
+ error: impl Display,
+) {
+ let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id));
+ loading_indication.indicate_loading_error(error.to_string());
+ let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
+ plugin_id,
+ loading_indication.clone(),
+ ));
+}
+
fn load_plugin_instance(instance: &mut Instance) -> Result<()> {
let err_context = || format!("failed to load plugin from instance {instance:#?}");
diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs
index c14d0c14..cf56a3b1 100644
--- a/zellij-server/src/route.rs
+++ b/zellij-server/src/route.rs
@@ -16,6 +16,7 @@ use zellij_utils::{
actions::{Action, SearchDirection, SearchOption},
command::TerminalAction,
get_mode_info,
+ layout::RunPluginLocation,
},
ipc::{ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg},
};
@@ -690,6 +691,18 @@ pub(crate) fn route_action(
))
.with_context(err_context)?;
},
+ Action::StartOrReloadPlugin(url) => {
+ let run_plugin_location =
+ RunPluginLocation::parse(url.as_str()).with_context(err_context)?;
+ session
+ .senders
+ .send_to_screen(ScreenInstruction::StartOrReloadPluginPane(
+ run_plugin_location,
+ None,
+ client_id,
+ ))
+ .with_context(err_context)?;
+ },
}
Ok(should_break)
}
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index 8b5cc5d6..7c698d65 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -258,7 +258,9 @@ pub enum ScreenInstruction {
NextSwapLayout(ClientId),
QueryTabNames(ClientId),
NewTiledPluginPane(RunPluginLocation, Option<String>, ClientId), // Option<String> is
+ // optional pane title
NewFloatingPluginPane(RunPluginLocation, Option<String>, ClientId), // Option<String> is an
+ StartOrReloadPluginPane(RunPluginLocation, Option<String>, ClientId), // Option<String> is
// optional pane title
AddPlugin(
Option<bool>, // should_float
@@ -268,7 +270,9 @@ pub enum ScreenInstruction {
u32, // plugin id
),
UpdatePluginLoadingStage(u32, LoadingIndication), // u32 - plugin_id
+ StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id
ProgressPluginLoadingOffset(u32), // u32 - plugin id
+ RequestStateUpdateForPlugins,
}
impl From<&ScreenInstruction> for ScreenContext {
@@ -415,6 +419,9 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::QueryTabNames(..) => ScreenContext::QueryTabNames,
ScreenInstruction::NewTiledPluginPane(..) => ScreenContext::NewTiledPluginPane,
ScreenInstruction::NewFloatingPluginPane(..) => ScreenContext::NewFloatingPluginPane,
+ ScreenInstruction::StartOrReloadPluginPane(..) => {
+ ScreenContext::StartOrReloadPluginPane
+ },
ScreenInstruction::AddPlugin(..) => ScreenContext::AddPlugin,
ScreenInstruction::UpdatePluginLoadingStage(..) => {
ScreenContext::UpdatePluginLoadingStage
@@ -422,6 +429,12 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::ProgressPluginLoadingOffset(..) => {
ScreenContext::ProgressPluginLoadingOffset
},
+ ScreenInstruction::StartPluginLoadingIndication(..) => {
+ ScreenContext::StartPluginLoadingIndication
+ },
+ ScreenInstruction::RequestStateUpdateForPlugins => {
+ ScreenContext::RequestStateUpdateForPlugins
+ },
}
}
}
@@ -2515,6 +2528,30 @@ pub(crate) fn screen_thread_main(
size,
))?;
},
+ ScreenInstruction::StartOrReloadPluginPane(
+ run_plugin_location,
+ pane_title,
+ client_id,
+ ) => {
+ let tab_index = screen.active_tab_indices.values().next().unwrap_or(&1);
+ let size = Size::default();
+ let should_float = Some(false);
+ let run_plugin = RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: run_plugin_location,
+ };
+ screen
+ .bus
+ .senders
+ .send_to_plugin(PluginInstruction::Reload(
+ should_float,
+ pane_title,
+ run_plugin,
+ *tab_index,
+ client_id,
+ size,
+ ))?;
+ },
ScreenInstruction::AddPlugin(
should_float,
run_plugin_location,
@@ -2548,6 +2585,16 @@ pub(crate) fn screen_thread_main(
}
screen.render()?;
},
+ ScreenInstruction::StartPluginLoadingIndication(pid, loading_indication) => {
+ let all_tabs = screen.get_tabs_mut();
+ for tab in all_tabs.values_mut() {
+ if tab.has_plugin(pid) {
+ tab.start_plugin_loading_indication(pid, loading_indication);
+ break;
+ }
+ }
+ screen.render()?;
+ },
ScreenInstruction::ProgressPluginLoadingOffset(pid) => {
let all_tabs = screen.get_tabs_mut();
for tab in all_tabs.values_mut() {
@@ -2558,6 +2605,14 @@ pub(crate) fn screen_thread_main(
}
screen.render()?;
},
+ ScreenInstruction::RequestStateUpdateForPlugins => {
+ let all_tabs = screen.get_tabs_mut();
+ for tab in all_tabs.values_mut() {
+ tab.update_input_modes()?;
+ }
+ screen.update_tabs()?;
+ screen.render()?;
+ },
}
}
Ok(())
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 65228b83..4a37a090 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -443,6 +443,7 @@ pub trait Pane {
fn invoked_with(&self) -> &Option<Run>;
fn set_title(&mut self, title: String);
fn update_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins
+ fn start_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins
fn progress_animation_offset(&mut self) {} // only relevant for plugins
}
@@ -3381,6 +3382,24 @@ impl Tab {
plugin_pane.update_loading_indication(loading_indication);
}
}
+ pub fn start_plugin_loading_indication(
+ &mut self,
+ pid: u32,
+ loading_indication: LoadingIndication,
+ ) {
+ if let Some(plugin_pane) = self
+ .tiled_panes
+ .get_pane_mut(PaneId::Plugin(pid))
+ .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid)))
+ .or_else(|| {
+ self.suppressed_panes
+ .values_mut()
+ .find(|s_p| s_p.pid() == PaneId::Plugin(pid))
+ })
+ {
+ plugin_pane.start_loading_indication(loading_indication);
+ }
+ }
pub fn progress_plugin_loading_offset(&mut self, pid: u32) {
if let Some(plugin_pane) = self
.tiled_panes
diff --git a/zellij-server/src/ui/loading_indication.rs b/zellij-server/src/ui/loading_indication.rs
index 26bf299d..78af2c10 100644
--- a/zellij-server/src/ui/loading_indication.rs
+++ b/zellij-server/src/ui/loading_indication.rs
@@ -35,6 +35,9 @@ impl LoadingIndication {
..Default::default()
}
}
+ pub fn set_name(&mut self, plugin_name: String) {
+ self.plugin_name = plugin_name;
+ }
pub fn with_colors(mut self, terminal_emulator_colors: Palette) -> Self {
self.terminal_emulator_colors = Some(terminal_emulator_colors);
self
diff --git a/zellij-utils/assets/compact-bar.wasm b/zellij-utils/assets/compact-bar.wasm
index 46267f5e..00d14d0e 100644..100755
--- a/zellij-utils/assets/compact-bar.wasm
+++ b/zellij-utils/assets/compact-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm
index 00d14d0e..0e3598d0 100755
--- a/zellij-utils/assets/plugins/compact-bar.wasm
+++ b/zellij-utils/assets/plugins/compact-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm
index 66e6b10d..a1d0c817 100755
--- a/zellij-utils/assets/plugins/status-bar.wasm
+++ b/zellij-utils/assets/plugins/status-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm
index 287385a5..37da81bc 100755
--- a/zellij-utils/assets/plugins/strider.wasm
+++ b/zellij-utils/assets/plugins/strider.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm
index b22bcf10..38f17dfc 100755
--- a/zellij-utils/assets/plugins/tab-bar.wasm
+++ b/zellij-utils/assets/plugins/tab-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/status-bar.wasm b/zellij-utils/assets/status-bar.wasm
index 5b4f6eb0..66e6b10d 100644..100755
--- a/zellij-utils/assets/status-bar.wasm
+++ b/zellij-utils/assets/status-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/strider.wasm b/zellij-utils/assets/strider.wasm
index 0af68b2f..287385a5 100644..100755
--- a/zellij-utils/assets/strider.wasm
+++ b/zellij-utils/assets/strider.wasm
Binary files differ
diff --git a/zellij-utils/assets/tab-bar.wasm b/zellij-utils/assets/tab-bar.wasm
index 1166de83..b22bcf10 100644..100755
--- a/zellij-utils/assets/tab-bar.wasm
+++ b/zellij-utils/assets/tab-bar.wasm
Binary files differ
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs
index aa8a27ea..14bcfae7 100644
--- a/zellij-utils/src/cli.rs
+++ b/zellij-utils/src/cli.rs
@@ -7,6 +7,7 @@ use crate::{
use clap::{Parser, Subcommand};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
+use url::Url;
#[derive(Parser, Default, Debug, Clone, Serialize, Deserialize)]
#[clap(version, name = "zellij")]
@@ -373,4 +374,7 @@ pub enum CliAction {
NextSwapLayout,
/// Query all tab names
QueryTabNames,
+ StartOrReloadPlugin {
+ url: Url,
+ },
}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index e23c22bb..8b49112b 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -326,10 +326,13 @@ pub enum ScreenContext {
NextSwapLayout,
QueryTabNames,
NewTiledPluginPane,
+ StartOrReloadPluginPane,
NewFloatingPluginPane,
AddPlugin,
UpdatePluginLoadingStage,
ProgressPluginLoadingOffset,
+ StartPluginLoadingIndication,
+ RequestStateUpdateForPlugins,
}
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
@@ -355,6 +358,7 @@ pub enum PluginContext {
Update,
Render,
Unload,
+ Reload,
Resize,
Exit,
AddClient,
@@ -489,6 +493,9 @@ open an issue on GitHub:
#[error("Client {client_id} is too slow to handle incoming messages")]
ClientTooSlow { client_id: u16 },
+
+ #[error("The plugin does not exist")]
+ PluginDoesNotExist,
}
#[cfg(not(target_family = "wasm"))]
diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs
index d03fd120..c288b8d2 100644
--- a/zellij-utils/src/input/actions.rs
+++ b/zellij-utils/src/input/actions.rs
@@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::str::FromStr;
+use url::Url;
use crate::position::Position;
@@ -231,6 +232,7 @@ pub enum Action {
/// Open a new tiled (embedded, non-floating) plugin pane
NewTiledPluginPane(RunPluginLocation, Option<String>), // String is an optional name
NewFloatingPluginPane(RunPluginLocation, Option<String>), // String is an optional name
+ StartOrReloadPlugin(Url),
}
impl Action {
@@ -471,6 +473,7 @@ impl Action {
CliAction::PreviousSwapLayout => Ok(vec![Action::PreviousSwapLayout]),
CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]),
CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
+ CliAction::StartOrReloadPlugin { url } => Ok(vec![Action::StartOrReloadPlugin(url)]),
}
}
}
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
index 830f7c02..713b2775 100644
--- a/zellij-utils/src/input/layout.rs
+++ b/zellij-utils/src/input/layout.rs
@@ -207,7 +207,7 @@ impl Run {
}
}
-#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct RunPlugin {
#[serde(default)]
pub _allow_exec_host_cmd: bool,