aboutsummaryrefslogtreecommitdiffstats
path: root/zellij-server/src/plugins/plugin_loader.rs
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 /zellij-server/src/plugins/plugin_loader.rs
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
Diffstat (limited to 'zellij-server/src/plugins/plugin_loader.rs')
-rw-r--r--zellij-server/src/plugins/plugin_loader.rs656
1 files changed, 656 insertions, 0 deletions
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))
+}