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
This commit is contained in:
parent
26fcf84702
commit
caaee30179
31 changed files with 1057 additions and 599 deletions
|
|
@ -74,3 +74,7 @@ default = [ "zellij-utils/plugins_from_target" ]
|
||||||
disable_automatic_asset_installation = [ "zellij-utils/disable_automatic_asset_installation" ]
|
disable_automatic_asset_installation = [ "zellij-utils/disable_automatic_asset_installation" ]
|
||||||
unstable = [ "zellij-client/unstable", "zellij-utils/unstable" ]
|
unstable = [ "zellij-client/unstable", "zellij-utils/unstable" ]
|
||||||
singlepass = [ "zellij-server/singlepass" ]
|
singlepass = [ "zellij-server/singlepass" ]
|
||||||
|
|
||||||
|
# uncomment this when developing plugins in the Zellij UI to make plugin compilation faster
|
||||||
|
# [profile.dev.package."*"]
|
||||||
|
# opt-level = 3
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
source: src/tests/e2e/cases.rs
|
source: src/tests/e2e/cases.rs
|
||||||
|
assertion_line: 198
|
||||||
expression: last_snapshot
|
expression: last_snapshot
|
||||||
|
|
||||||
---
|
---
|
||||||
Zellij
|
Zellij
|
||||||
┌──────┐
|
┌──────┐
|
||||||
|
|
|
||||||
|
|
@ -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 --
|
-- INTERFACE LOCKED --
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Tip: UNBOUND => open new pane. UNBOUND => navigate between panes. UNBOUND => increase/decrease pane size.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Tip: Alt + <n> => new pane. Alt + <←↓↑→> or Alt + <hjkl> => navigate. Alt + <+|-> => resize pane.
|
||||||
|
|
|
||||||
|
|
@ -764,7 +764,7 @@ fn init_session(
|
||||||
Some(&to_screen),
|
Some(&to_screen),
|
||||||
Some(&to_pty),
|
Some(&to_pty),
|
||||||
Some(&to_plugin),
|
Some(&to_plugin),
|
||||||
None,
|
Some(&to_server),
|
||||||
Some(&to_pty_writer),
|
Some(&to_pty_writer),
|
||||||
Some(&to_background_jobs),
|
Some(&to_background_jobs),
|
||||||
None,
|
None,
|
||||||
|
|
|
||||||
|
|
@ -1254,7 +1254,7 @@ impl Grid {
|
||||||
let new_row = Row::new(self.width).canonical();
|
let new_row = Row::new(self.width).canonical();
|
||||||
self.viewport.push(new_row);
|
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.scroll_region.is_none() {
|
||||||
if self.alternate_screen_state.is_none() {
|
if self.alternate_screen_state.is_none() {
|
||||||
self.transfer_rows_to_lines_above(1);
|
self.transfer_rows_to_lines_above(1);
|
||||||
|
|
@ -1406,7 +1406,7 @@ impl Grid {
|
||||||
}
|
}
|
||||||
fn line_wrap(&mut self) {
|
fn line_wrap(&mut self) {
|
||||||
self.cursor.x = 0;
|
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() {
|
if self.alternate_screen_state.is_none() {
|
||||||
self.transfer_rows_to_lines_above(1);
|
self.transfer_rows_to_lines_above(1);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -545,6 +545,12 @@ impl Pane for PluginPane {
|
||||||
self.loading_indication.to_string().as_bytes().to_vec(),
|
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) {
|
fn progress_animation_offset(&mut self) {
|
||||||
if self.loading_indication.ended {
|
if self.loading_indication.ended {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
mod start_plugin;
|
mod plugin_loader;
|
||||||
mod wasm_bridge;
|
mod wasm_bridge;
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::{collections::HashMap, fs, path::PathBuf};
|
use std::{collections::HashMap, fs, path::PathBuf};
|
||||||
use wasmer::Store;
|
use wasmer::Store;
|
||||||
|
|
||||||
use crate::screen::ScreenInstruction;
|
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;
|
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
|
Update(Vec<(Option<u32>, Option<ClientId>, Event)>), // Focused plugin / broadcast, client_id, event data
|
||||||
Unload(u32), // plugin_id
|
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),
|
AddClient(ClientId),
|
||||||
RemoveClient(ClientId),
|
RemoveClient(ClientId),
|
||||||
NewTab(
|
NewTab(
|
||||||
|
|
@ -43,7 +51,7 @@ pub enum PluginInstruction {
|
||||||
usize, // tab_index
|
usize, // tab_index
|
||||||
ClientId,
|
ClientId,
|
||||||
),
|
),
|
||||||
ApplyCachedEvents(u32), // u32 is the plugin id
|
ApplyCachedEvents(Vec<u32>), // a list of plugin id
|
||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +61,7 @@ impl From<&PluginInstruction> for PluginContext {
|
||||||
PluginInstruction::Load(..) => PluginContext::Load,
|
PluginInstruction::Load(..) => PluginContext::Load,
|
||||||
PluginInstruction::Update(..) => PluginContext::Update,
|
PluginInstruction::Update(..) => PluginContext::Update,
|
||||||
PluginInstruction::Unload(..) => PluginContext::Unload,
|
PluginInstruction::Unload(..) => PluginContext::Unload,
|
||||||
|
PluginInstruction::Reload(..) => PluginContext::Reload,
|
||||||
PluginInstruction::Resize(..) => PluginContext::Resize,
|
PluginInstruction::Resize(..) => PluginContext::Resize,
|
||||||
PluginInstruction::Exit => PluginContext::Exit,
|
PluginInstruction::Exit => PluginContext::Exit,
|
||||||
PluginInstruction::AddClient(_) => PluginContext::AddClient,
|
PluginInstruction::AddClient(_) => PluginContext::AddClient,
|
||||||
|
|
@ -103,6 +112,42 @@ pub(crate) fn plugin_thread_main(
|
||||||
PluginInstruction::Unload(pid) => {
|
PluginInstruction::Unload(pid) => {
|
||||||
wasm_bridge.unload_plugin(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) => {
|
PluginInstruction::Resize(pid, new_columns, new_rows) => {
|
||||||
wasm_bridge.resize_plugin(pid, new_columns, new_rows)?;
|
wasm_bridge.resize_plugin(pid, new_columns, new_rows)?;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
656
zellij-server/src/plugins/plugin_loader.rs
Normal file
656
zellij-server/src/plugins/plugin_loader.rs
Normal file
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
@ -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))
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
use super::PluginInstruction;
|
use super::PluginInstruction;
|
||||||
use crate::plugins::start_plugin::start_plugin;
|
use crate::plugins::plugin_loader::{PluginLoader, VersionMismatchError};
|
||||||
use log::{debug, info, warn};
|
use log::{debug, info, warn};
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fmt,
|
fmt::Display,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
process,
|
process,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
|
|
@ -33,81 +33,17 @@ use zellij_utils::{
|
||||||
consts::VERSION,
|
consts::VERSION,
|
||||||
data::{Event, EventType, PluginIds},
|
data::{Event, EventType, PluginIds},
|
||||||
errors::prelude::*,
|
errors::prelude::*,
|
||||||
|
errors::ZellijError,
|
||||||
input::{
|
input::{
|
||||||
command::TerminalAction,
|
command::TerminalAction,
|
||||||
layout::RunPlugin,
|
layout::{RunPlugin, RunPluginLocation},
|
||||||
plugins::{PluginConfig, PluginType, PluginsConfig},
|
plugins::{PluginConfig, PluginType, PluginsConfig},
|
||||||
},
|
},
|
||||||
pane_size::Size,
|
pane_size::Size,
|
||||||
serde,
|
serde,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Custom error for plugin version mismatch.
|
type PluginId = u32;
|
||||||
///
|
|
||||||
/// 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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(WasmerEnv, Clone)]
|
#[derive(WasmerEnv, Clone)]
|
||||||
pub struct PluginEnv {
|
pub struct PluginEnv {
|
||||||
|
|
@ -150,7 +86,8 @@ pub struct WasmBridge {
|
||||||
next_plugin_id: u32,
|
next_plugin_id: u32,
|
||||||
cached_events_for_pending_plugins: HashMap<u32, Vec<Event>>, // u32 is the plugin id
|
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)
|
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 {
|
impl WasmBridge {
|
||||||
|
|
@ -176,6 +113,7 @@ impl WasmBridge {
|
||||||
cached_events_for_pending_plugins: HashMap::new(),
|
cached_events_for_pending_plugins: HashMap::new(),
|
||||||
cached_resizes_for_pending_plugins: HashMap::new(),
|
cached_resizes_for_pending_plugins: HashMap::new(),
|
||||||
loading_plugins: HashMap::new(),
|
loading_plugins: HashMap::new(),
|
||||||
|
pending_plugin_reloads: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn load_plugin(
|
pub fn load_plugin(
|
||||||
|
|
@ -196,12 +134,10 @@ impl WasmBridge {
|
||||||
.with_context(err_context)?;
|
.with_context(err_context)?;
|
||||||
let plugin_name = run.location.to_string();
|
let plugin_name = run.location.to_string();
|
||||||
|
|
||||||
self.next_plugin_id += 1;
|
|
||||||
|
|
||||||
self.cached_events_for_pending_plugins
|
self.cached_events_for_pending_plugins
|
||||||
.insert(plugin_id, vec![]);
|
.insert(plugin_id, vec![]);
|
||||||
self.cached_resizes_for_pending_plugins
|
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 load_plugin_task = task::spawn({
|
||||||
let plugin_dir = self.plugin_dir.clone();
|
let plugin_dir = self.plugin_dir.clone();
|
||||||
|
|
@ -214,7 +150,7 @@ impl WasmBridge {
|
||||||
let _ =
|
let _ =
|
||||||
senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id));
|
senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id));
|
||||||
let mut loading_indication = LoadingIndication::new(plugin_name.clone());
|
let mut loading_indication = LoadingIndication::new(plugin_name.clone());
|
||||||
match start_plugin(
|
match PluginLoader::start_plugin(
|
||||||
plugin_id,
|
plugin_id,
|
||||||
client_id,
|
client_id,
|
||||||
&plugin,
|
&plugin,
|
||||||
|
|
@ -228,30 +164,20 @@ impl WasmBridge {
|
||||||
connected_clients.clone(),
|
connected_clients.clone(),
|
||||||
&mut loading_indication,
|
&mut loading_indication,
|
||||||
) {
|
) {
|
||||||
Ok(_) => {
|
Ok(_) => handle_plugin_successful_loading(&senders, plugin_id),
|
||||||
let _ = senders.send_to_background_jobs(
|
Err(e) => handle_plugin_loading_failure(
|
||||||
BackgroundJob::StopPluginLoadingAnimation(plugin_id),
|
&senders,
|
||||||
);
|
plugin_id,
|
||||||
let _ =
|
&mut loading_indication,
|
||||||
senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_id));
|
e,
|
||||||
},
|
),
|
||||||
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(),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
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;
|
self.next_plugin_id += 1;
|
||||||
Ok(plugin_id)
|
Ok(plugin_id)
|
||||||
}
|
}
|
||||||
|
|
@ -267,6 +193,89 @@ impl WasmBridge {
|
||||||
}
|
}
|
||||||
Ok(())
|
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<()> {
|
pub fn add_client(&mut self, client_id: ClientId) -> Result<()> {
|
||||||
let err_context = || format!("failed to add plugins for client {client_id}");
|
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
|
for ((plugin_id, client_id), (instance, plugin_env, (current_rows, current_columns))) in
|
||||||
plugin_map.iter_mut()
|
plugin_map.iter_mut()
|
||||||
{
|
{
|
||||||
|
if self
|
||||||
|
.cached_resizes_for_pending_plugins
|
||||||
|
.contains_key(&plugin_id)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if *plugin_id == pid {
|
if *plugin_id == pid {
|
||||||
*current_rows = new_rows;
|
*current_rows = new_rows;
|
||||||
*current_columns = new_columns;
|
*current_columns = new_columns;
|
||||||
|
|
@ -334,12 +349,10 @@ impl WasmBridge {
|
||||||
plugin_bytes.push((*plugin_id, *client_id, rendered_bytes.as_bytes().to_vec()));
|
plugin_bytes.push((*plugin_id, *client_id, rendered_bytes.as_bytes().to_vec()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (plugin_id, (current_rows, current_columns)) in
|
for (plugin_id, mut current_size) in self.cached_resizes_for_pending_plugins.iter_mut() {
|
||||||
self.cached_resizes_for_pending_plugins.iter_mut()
|
|
||||||
{
|
|
||||||
if *plugin_id == pid {
|
if *plugin_id == pid {
|
||||||
*current_rows = new_rows;
|
current_size.0 = new_rows;
|
||||||
*current_columns = new_columns;
|
current_size.1 = new_columns;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = self
|
let _ = self
|
||||||
|
|
@ -357,6 +370,12 @@ impl WasmBridge {
|
||||||
let mut plugin_bytes = vec![];
|
let mut plugin_bytes = vec![];
|
||||||
for (pid, cid, event) in updates.drain(..) {
|
for (pid, cid, event) in updates.drain(..) {
|
||||||
for (&(plugin_id, client_id), (instance, plugin_env, (rows, columns))) in &*plugin_map {
|
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
|
let subs = plugin_env
|
||||||
.subscriptions
|
.subscriptions
|
||||||
.lock()
|
.lock()
|
||||||
|
|
@ -394,8 +413,42 @@ impl WasmBridge {
|
||||||
.send_to_screen(ScreenInstruction::PluginBytes(plugin_bytes));
|
.send_to_screen(ScreenInstruction::PluginBytes(plugin_bytes));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn apply_cached_events(&mut self, plugin_id: u32) -> Result<()> {
|
pub fn apply_cached_events(&mut self, plugin_ids: Vec<u32>) -> Result<()> {
|
||||||
let err_context = || format!("Failed to apply cached events to plugin {plugin_id}");
|
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) {
|
if let Some(events) = self.cached_events_for_pending_plugins.remove(&plugin_id) {
|
||||||
let mut plugin_map = self.plugin_map.lock().unwrap();
|
let mut plugin_map = self.plugin_map.lock().unwrap();
|
||||||
let all_connected_clients: Vec<ClientId> = self
|
let all_connected_clients: Vec<ClientId> = self
|
||||||
|
|
@ -405,10 +458,10 @@ impl WasmBridge {
|
||||||
.iter()
|
.iter()
|
||||||
.copied()
|
.copied()
|
||||||
.collect();
|
.collect();
|
||||||
for client_id in all_connected_clients {
|
for client_id in &all_connected_clients {
|
||||||
let mut plugin_bytes = vec![];
|
let mut plugin_bytes = vec![];
|
||||||
if let Some((instance, plugin_env, (rows, columns))) =
|
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
|
let subs = plugin_env
|
||||||
.subscriptions
|
.subscriptions
|
||||||
|
|
@ -423,7 +476,7 @@ impl WasmBridge {
|
||||||
}
|
}
|
||||||
apply_event_to_plugin(
|
apply_event_to_plugin(
|
||||||
plugin_id,
|
plugin_id,
|
||||||
client_id,
|
*client_id,
|
||||||
&instance,
|
&instance,
|
||||||
&plugin_env,
|
&plugin_env,
|
||||||
&event,
|
&event,
|
||||||
|
|
@ -441,22 +494,83 @@ impl WasmBridge {
|
||||||
if let Some((rows, columns)) = self.cached_resizes_for_pending_plugins.remove(&plugin_id) {
|
if let Some((rows, columns)) = self.cached_resizes_for_pending_plugins.remove(&plugin_id) {
|
||||||
self.resize_plugin(plugin_id, columns, rows)?;
|
self.resize_plugin(plugin_id, columns, rows)?;
|
||||||
}
|
}
|
||||||
self.loading_plugins.remove(&plugin_id);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
pub fn remove_client(&mut self, client_id: ClientId) {
|
fn plugin_is_currently_being_loaded(&self, plugin_location: &RunPluginLocation) -> bool {
|
||||||
self.connected_clients
|
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()
|
.lock()
|
||||||
.unwrap()
|
.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) {
|
fn size_of_plugin_id(&self, plugin_id: PluginId) -> Option<(usize, usize)> {
|
||||||
for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() {
|
// (rows/colums)
|
||||||
drop(loading_plugin_task.cancel());
|
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<()> {
|
fn load_plugin_instance(instance: &mut Instance) -> Result<()> {
|
||||||
let err_context = || format!("failed to load plugin from instance {instance:#?}");
|
let err_context = || format!("failed to load plugin from instance {instance:#?}");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use zellij_utils::{
|
||||||
actions::{Action, SearchDirection, SearchOption},
|
actions::{Action, SearchDirection, SearchOption},
|
||||||
command::TerminalAction,
|
command::TerminalAction,
|
||||||
get_mode_info,
|
get_mode_info,
|
||||||
|
layout::RunPluginLocation,
|
||||||
},
|
},
|
||||||
ipc::{ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg},
|
ipc::{ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg},
|
||||||
};
|
};
|
||||||
|
|
@ -690,6 +691,18 @@ pub(crate) fn route_action(
|
||||||
))
|
))
|
||||||
.with_context(err_context)?;
|
.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)
|
Ok(should_break)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -258,7 +258,9 @@ pub enum ScreenInstruction {
|
||||||
NextSwapLayout(ClientId),
|
NextSwapLayout(ClientId),
|
||||||
QueryTabNames(ClientId),
|
QueryTabNames(ClientId),
|
||||||
NewTiledPluginPane(RunPluginLocation, Option<String>, ClientId), // Option<String> is
|
NewTiledPluginPane(RunPluginLocation, Option<String>, ClientId), // Option<String> is
|
||||||
|
// optional pane title
|
||||||
NewFloatingPluginPane(RunPluginLocation, Option<String>, ClientId), // Option<String> is an
|
NewFloatingPluginPane(RunPluginLocation, Option<String>, ClientId), // Option<String> is an
|
||||||
|
StartOrReloadPluginPane(RunPluginLocation, Option<String>, ClientId), // Option<String> is
|
||||||
// optional pane title
|
// optional pane title
|
||||||
AddPlugin(
|
AddPlugin(
|
||||||
Option<bool>, // should_float
|
Option<bool>, // should_float
|
||||||
|
|
@ -268,7 +270,9 @@ pub enum ScreenInstruction {
|
||||||
u32, // plugin id
|
u32, // plugin id
|
||||||
),
|
),
|
||||||
UpdatePluginLoadingStage(u32, LoadingIndication), // u32 - plugin_id
|
UpdatePluginLoadingStage(u32, LoadingIndication), // u32 - plugin_id
|
||||||
|
StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id
|
||||||
ProgressPluginLoadingOffset(u32), // u32 - plugin id
|
ProgressPluginLoadingOffset(u32), // u32 - plugin id
|
||||||
|
RequestStateUpdateForPlugins,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&ScreenInstruction> for ScreenContext {
|
impl From<&ScreenInstruction> for ScreenContext {
|
||||||
|
|
@ -415,6 +419,9 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||||
ScreenInstruction::QueryTabNames(..) => ScreenContext::QueryTabNames,
|
ScreenInstruction::QueryTabNames(..) => ScreenContext::QueryTabNames,
|
||||||
ScreenInstruction::NewTiledPluginPane(..) => ScreenContext::NewTiledPluginPane,
|
ScreenInstruction::NewTiledPluginPane(..) => ScreenContext::NewTiledPluginPane,
|
||||||
ScreenInstruction::NewFloatingPluginPane(..) => ScreenContext::NewFloatingPluginPane,
|
ScreenInstruction::NewFloatingPluginPane(..) => ScreenContext::NewFloatingPluginPane,
|
||||||
|
ScreenInstruction::StartOrReloadPluginPane(..) => {
|
||||||
|
ScreenContext::StartOrReloadPluginPane
|
||||||
|
},
|
||||||
ScreenInstruction::AddPlugin(..) => ScreenContext::AddPlugin,
|
ScreenInstruction::AddPlugin(..) => ScreenContext::AddPlugin,
|
||||||
ScreenInstruction::UpdatePluginLoadingStage(..) => {
|
ScreenInstruction::UpdatePluginLoadingStage(..) => {
|
||||||
ScreenContext::UpdatePluginLoadingStage
|
ScreenContext::UpdatePluginLoadingStage
|
||||||
|
|
@ -422,6 +429,12 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||||
ScreenInstruction::ProgressPluginLoadingOffset(..) => {
|
ScreenInstruction::ProgressPluginLoadingOffset(..) => {
|
||||||
ScreenContext::ProgressPluginLoadingOffset
|
ScreenContext::ProgressPluginLoadingOffset
|
||||||
},
|
},
|
||||||
|
ScreenInstruction::StartPluginLoadingIndication(..) => {
|
||||||
|
ScreenContext::StartPluginLoadingIndication
|
||||||
|
},
|
||||||
|
ScreenInstruction::RequestStateUpdateForPlugins => {
|
||||||
|
ScreenContext::RequestStateUpdateForPlugins
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2515,6 +2528,30 @@ pub(crate) fn screen_thread_main(
|
||||||
size,
|
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(
|
ScreenInstruction::AddPlugin(
|
||||||
should_float,
|
should_float,
|
||||||
run_plugin_location,
|
run_plugin_location,
|
||||||
|
|
@ -2548,6 +2585,16 @@ pub(crate) fn screen_thread_main(
|
||||||
}
|
}
|
||||||
screen.render()?;
|
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) => {
|
ScreenInstruction::ProgressPluginLoadingOffset(pid) => {
|
||||||
let all_tabs = screen.get_tabs_mut();
|
let all_tabs = screen.get_tabs_mut();
|
||||||
for tab in all_tabs.values_mut() {
|
for tab in all_tabs.values_mut() {
|
||||||
|
|
@ -2558,6 +2605,14 @@ pub(crate) fn screen_thread_main(
|
||||||
}
|
}
|
||||||
screen.render()?;
|
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(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,7 @@ pub trait Pane {
|
||||||
fn invoked_with(&self) -> &Option<Run>;
|
fn invoked_with(&self) -> &Option<Run>;
|
||||||
fn set_title(&mut self, title: String);
|
fn set_title(&mut self, title: String);
|
||||||
fn update_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins
|
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
|
fn progress_animation_offset(&mut self) {} // only relevant for plugins
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3381,6 +3382,24 @@ impl Tab {
|
||||||
plugin_pane.update_loading_indication(loading_indication);
|
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) {
|
pub fn progress_plugin_loading_offset(&mut self, pid: u32) {
|
||||||
if let Some(plugin_pane) = self
|
if let Some(plugin_pane) = self
|
||||||
.tiled_panes
|
.tiled_panes
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ impl LoadingIndication {
|
||||||
..Default::default()
|
..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 {
|
pub fn with_colors(mut self, terminal_emulator_colors: Palette) -> Self {
|
||||||
self.terminal_emulator_colors = Some(terminal_emulator_colors);
|
self.terminal_emulator_colors = Some(terminal_emulator_colors);
|
||||||
self
|
self
|
||||||
|
|
|
||||||
BIN
zellij-utils/assets/compact-bar.wasm
Normal file → Executable file
BIN
zellij-utils/assets/compact-bar.wasm
Normal file → Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
zellij-utils/assets/status-bar.wasm
Normal file → Executable file
BIN
zellij-utils/assets/status-bar.wasm
Normal file → Executable file
Binary file not shown.
BIN
zellij-utils/assets/strider.wasm
Normal file → Executable file
BIN
zellij-utils/assets/strider.wasm
Normal file → Executable file
Binary file not shown.
BIN
zellij-utils/assets/tab-bar.wasm
Normal file → Executable file
BIN
zellij-utils/assets/tab-bar.wasm
Normal file → Executable file
Binary file not shown.
|
|
@ -7,6 +7,7 @@ use crate::{
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Parser, Default, Debug, Clone, Serialize, Deserialize)]
|
#[derive(Parser, Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
#[clap(version, name = "zellij")]
|
#[clap(version, name = "zellij")]
|
||||||
|
|
@ -373,4 +374,7 @@ pub enum CliAction {
|
||||||
NextSwapLayout,
|
NextSwapLayout,
|
||||||
/// Query all tab names
|
/// Query all tab names
|
||||||
QueryTabNames,
|
QueryTabNames,
|
||||||
|
StartOrReloadPlugin {
|
||||||
|
url: Url,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -326,10 +326,13 @@ pub enum ScreenContext {
|
||||||
NextSwapLayout,
|
NextSwapLayout,
|
||||||
QueryTabNames,
|
QueryTabNames,
|
||||||
NewTiledPluginPane,
|
NewTiledPluginPane,
|
||||||
|
StartOrReloadPluginPane,
|
||||||
NewFloatingPluginPane,
|
NewFloatingPluginPane,
|
||||||
AddPlugin,
|
AddPlugin,
|
||||||
UpdatePluginLoadingStage,
|
UpdatePluginLoadingStage,
|
||||||
ProgressPluginLoadingOffset,
|
ProgressPluginLoadingOffset,
|
||||||
|
StartPluginLoadingIndication,
|
||||||
|
RequestStateUpdateForPlugins,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
|
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
|
||||||
|
|
@ -355,6 +358,7 @@ pub enum PluginContext {
|
||||||
Update,
|
Update,
|
||||||
Render,
|
Render,
|
||||||
Unload,
|
Unload,
|
||||||
|
Reload,
|
||||||
Resize,
|
Resize,
|
||||||
Exit,
|
Exit,
|
||||||
AddClient,
|
AddClient,
|
||||||
|
|
@ -489,6 +493,9 @@ open an issue on GitHub:
|
||||||
|
|
||||||
#[error("Client {client_id} is too slow to handle incoming messages")]
|
#[error("Client {client_id} is too slow to handle incoming messages")]
|
||||||
ClientTooSlow { client_id: u16 },
|
ClientTooSlow { client_id: u16 },
|
||||||
|
|
||||||
|
#[error("The plugin does not exist")]
|
||||||
|
PluginDoesNotExist,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::position::Position;
|
use crate::position::Position;
|
||||||
|
|
||||||
|
|
@ -231,6 +232,7 @@ pub enum Action {
|
||||||
/// Open a new tiled (embedded, non-floating) plugin pane
|
/// Open a new tiled (embedded, non-floating) plugin pane
|
||||||
NewTiledPluginPane(RunPluginLocation, Option<String>), // String is an optional name
|
NewTiledPluginPane(RunPluginLocation, Option<String>), // String is an optional name
|
||||||
NewFloatingPluginPane(RunPluginLocation, Option<String>), // String is an optional name
|
NewFloatingPluginPane(RunPluginLocation, Option<String>), // String is an optional name
|
||||||
|
StartOrReloadPlugin(Url),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Action {
|
impl Action {
|
||||||
|
|
@ -471,6 +473,7 @@ impl Action {
|
||||||
CliAction::PreviousSwapLayout => Ok(vec![Action::PreviousSwapLayout]),
|
CliAction::PreviousSwapLayout => Ok(vec![Action::PreviousSwapLayout]),
|
||||||
CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]),
|
CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]),
|
||||||
CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
|
CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]),
|
||||||
|
CliAction::StartOrReloadPlugin { url } => Ok(vec![Action::StartOrReloadPlugin(url)]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,7 +207,7 @@ impl Run {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct RunPlugin {
|
pub struct RunPlugin {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub _allow_exec_host_cmd: bool,
|
pub _allow_exec_host_cmd: bool,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue