use crate::plugins::plugin_map::{ PluginEnv, PluginMap, RunningPlugin, VecDequeInputStream, WriteOutputStream, }; use crate::plugins::plugin_worker::{plugin_worker, RunningWorker}; use crate::plugins::zellij_exports::{wasi_write_object, zellij_exports}; use crate::plugins::PluginId; use highway::{HighwayHash, PortableHash}; use log::info; use std::{ collections::{HashMap, HashSet, VecDeque}, fs, path::PathBuf, sync::{Arc, Mutex}, }; use url::Url; use wasmtime::{Engine, Instance, Linker, Module, Store}; use wasmtime_wasi::{preview1::WasiP1Ctx, DirPerms, FilePerms, WasiCtxBuilder}; use zellij_utils::consts::ZELLIJ_PLUGIN_ARTIFACT_DIR; use zellij_utils::prost::Message; use crate::{ logging_pipe::LoggingPipe, screen::ScreenInstruction, thread_bus::ThreadSenders, ui::loading_indication::LoadingIndication, ClientId, }; use zellij_utils::plugin_api::action::ProtobufPluginConfiguration; use zellij_utils::{ consts::{ZELLIJ_CACHE_DIR, ZELLIJ_SESSION_CACHE_DIR, ZELLIJ_TMP_DIR}, data::{InputMode, PluginCapabilities}, errors::prelude::*, input::command::TerminalAction, input::keybinds::Keybinds, input::layout::Layout, input::plugins::PluginConfig, ipc::ClientAttributes, 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(), )), ); }}; } pub struct PluginLoader<'a> { plugin_cache: Arc>>, plugin_path: PathBuf, loading_indication: &'a mut LoadingIndication, senders: ThreadSenders, plugin_id: PluginId, client_id: ClientId, engine: Engine, plugin: PluginConfig, plugin_dir: &'a PathBuf, tab_index: Option, plugin_own_data_dir: PathBuf, plugin_own_cache_dir: PathBuf, size: Size, wasm_blob_on_hd: Option<(Vec, PathBuf)>, path_to_default_shell: PathBuf, plugin_cwd: PathBuf, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, layout_dir: Option, default_mode: InputMode, keybinds: Keybinds, } impl<'a> PluginLoader<'a> { pub fn reload_plugin_from_memory( plugin_id: PluginId, plugin_dir: PathBuf, plugin_cache: Arc>>, senders: ThreadSenders, engine: Engine, plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, path_to_default_shell: PathBuf, plugin_cwd: PathBuf, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, layout_dir: Option, ) -> Result<()> { let err_context = || format!("failed to reload plugin {plugin_id} from memory"); let mut connected_clients: Vec = 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, engine, &plugin_dir, path_to_default_shell, Some(plugin_cwd), capabilities, client_attributes, default_shell, default_layout, layout_dir, )?; plugin_loader .load_module_from_memory() .and_then(|module| plugin_loader.create_plugin_environment(module)) .and_then(|(store, instance)| { plugin_loader.load_plugin_instance(store, &instance, &plugin_map) }) .and_then(|_| { plugin_loader.clone_instance_for_other_clients(&connected_clients, &plugin_map) }) .with_context(err_context)?; display_loading_stage!(end, loading_indication, senders, plugin_id); Ok(()) } pub fn start_plugin( plugin_id: PluginId, client_id: ClientId, plugin: &PluginConfig, tab_index: Option, plugin_dir: PathBuf, plugin_cache: Arc>>, senders: ThreadSenders, engine: Engine, plugin_map: Arc>, size: Size, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, path_to_default_shell: PathBuf, plugin_cwd: PathBuf, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, skip_cache: bool, layout_dir: Option, default_mode: InputMode, keybinds: Keybinds, ) -> Result<()> { let err_context = || format!("failed to start plugin {plugin_id} for client {client_id}"); let mut plugin_loader = PluginLoader::new( &plugin_cache, loading_indication, &senders, plugin_id, client_id, engine, plugin.clone(), &plugin_dir, tab_index, size, path_to_default_shell, plugin_cwd, capabilities, client_attributes, default_shell, default_layout, layout_dir, default_mode, keybinds, )?; if skip_cache { plugin_loader .compile_module() .and_then(|module| plugin_loader.create_plugin_environment(module)) .and_then(|(store, instance)| { plugin_loader.load_plugin_instance(store, &instance, &plugin_map) }) .and_then(|_| { plugin_loader.clone_instance_for_other_clients( &connected_clients.lock().unwrap(), &plugin_map, ) }) .with_context(err_context)?; } else { 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_environment(module)) .and_then(|(store, instance)| { plugin_loader.load_plugin_instance(store, &instance, &plugin_map) }) .and_then(|_| { plugin_loader.clone_instance_for_other_clients( &connected_clients.lock().unwrap(), &plugin_map, ) }) .with_context(err_context)?; }; display_loading_stage!(end, loading_indication, senders, plugin_id); Ok(()) } pub fn add_client( client_id: ClientId, plugin_dir: PathBuf, plugin_cache: Arc>>, senders: ThreadSenders, engine: Engine, plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, path_to_default_shell: PathBuf, plugin_cwd: PathBuf, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, layout_dir: Option, default_mode: InputMode, keybinds: Keybinds, ) -> Result<()> { let mut new_plugins = HashSet::new(); for plugin_id in plugin_map.lock().unwrap().plugin_ids() { new_plugins.insert((plugin_id, client_id)); } for (plugin_id, existing_client_id) in new_plugins { let mut plugin_loader = PluginLoader::new_from_different_client_id( &plugin_cache, &plugin_map, loading_indication, &senders, plugin_id, existing_client_id, engine.clone(), &plugin_dir, path_to_default_shell.clone(), plugin_cwd.clone(), capabilities.clone(), client_attributes.clone(), default_shell.clone(), default_layout.clone(), layout_dir.clone(), default_mode, keybinds.clone(), )?; plugin_loader .load_module_from_memory() .and_then(|module| plugin_loader.create_plugin_environment(module)) .and_then(|(store, instance)| { plugin_loader.load_plugin_instance(store, &instance, &plugin_map) })? } connected_clients.lock().unwrap().push(client_id); Ok(()) } pub fn reload_plugin( plugin_id: PluginId, plugin_dir: PathBuf, plugin_cache: Arc>>, senders: ThreadSenders, engine: Engine, plugin_map: Arc>, connected_clients: Arc>>, loading_indication: &mut LoadingIndication, path_to_default_shell: PathBuf, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, layout_dir: Option, ) -> Result<()> { let err_context = || format!("failed to reload plugin id {plugin_id}"); let mut connected_clients: Vec = 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, engine, &plugin_dir, path_to_default_shell, None, capabilities, client_attributes, default_shell, default_layout, layout_dir, )?; plugin_loader .compile_module() .and_then(|module| plugin_loader.create_plugin_environment(module)) .and_then(|(store, instance)| { plugin_loader.load_plugin_instance(store, &instance, &plugin_map) }) .and_then(|_| { plugin_loader.clone_instance_for_other_clients(&connected_clients, &plugin_map) }) .with_context(err_context)?; display_loading_stage!(end, loading_indication, senders, plugin_id); Ok(()) } pub fn new( plugin_cache: &Arc>>, loading_indication: &'a mut LoadingIndication, senders: &ThreadSenders, plugin_id: PluginId, client_id: ClientId, engine: Engine, plugin: PluginConfig, plugin_dir: &'a PathBuf, tab_index: Option, size: Size, path_to_default_shell: PathBuf, plugin_cwd: PathBuf, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, layout_dir: Option, default_mode: InputMode, keybinds: Keybinds, ) -> Result { let plugin_own_data_dir = ZELLIJ_SESSION_CACHE_DIR .join(Url::from(&plugin.location).to_string()) .join(format!("{}-{}", plugin_id, client_id)); let plugin_own_cache_dir = ZELLIJ_SESSION_CACHE_DIR .join(Url::from(&plugin.location).to_string()) .join(format!("plugin_cache")); create_plugin_fs_entries(&plugin_own_data_dir, &plugin_own_cache_dir)?; let plugin_path = plugin.path.clone(); Ok(PluginLoader { plugin_cache: plugin_cache.clone(), plugin_path, loading_indication, senders: senders.clone(), plugin_id, client_id, engine, plugin, plugin_dir, tab_index, plugin_own_data_dir, plugin_own_cache_dir, size, wasm_blob_on_hd: None, path_to_default_shell, plugin_cwd, capabilities, client_attributes, default_shell, default_layout, layout_dir, default_mode, keybinds, }) } pub fn new_from_existing_plugin_attributes( plugin_cache: &Arc>>, plugin_map: &Arc>, loading_indication: &'a mut LoadingIndication, senders: &ThreadSenders, plugin_id: PluginId, client_id: ClientId, engine: Engine, plugin_dir: &'a PathBuf, path_to_default_shell: PathBuf, cwd: Option, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, layout_dir: Option, ) -> Result { let err_context = || "Failed to find existing plugin"; let (running_plugin, _subscriptions, _workers) = { let mut plugin_map = plugin_map.lock().unwrap(); plugin_map .remove_single_plugin(plugin_id, client_id) .with_context(err_context)? }; let running_plugin = running_plugin.lock().unwrap(); let tab_index = running_plugin.store.data().tab_index; let size = Size { rows: running_plugin.rows, cols: running_plugin.columns, }; let keybinds = running_plugin.store.data().keybinds.clone(); let default_mode = running_plugin.store.data().default_mode; let plugin_config = running_plugin.store.data().plugin.clone(); // prefer the explicitly given cwd, otherwise copy it from the running plugin // (when reloading a plugin, we want to copy it, when starting a new plugin instance from // meomory, we want to reset it) let plugin_cwd = cwd.unwrap_or_else(|| running_plugin.store.data().plugin_cwd.clone()); loading_indication.set_name(running_plugin.store.data().name()); PluginLoader::new( plugin_cache, loading_indication, senders, plugin_id, client_id, engine, plugin_config, plugin_dir, tab_index, size, path_to_default_shell, plugin_cwd, capabilities, client_attributes, default_shell, default_layout, layout_dir, default_mode, keybinds, ) } pub fn new_from_different_client_id( plugin_cache: &Arc>>, plugin_map: &Arc>, loading_indication: &'a mut LoadingIndication, senders: &ThreadSenders, plugin_id: PluginId, client_id: ClientId, engine: Engine, plugin_dir: &'a PathBuf, path_to_default_shell: PathBuf, plugin_cwd: PathBuf, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, layout_dir: Option, default_mode: InputMode, keybinds: Keybinds, ) -> Result { let err_context = || "Failed to find existing plugin"; let running_plugin = { let plugin_map = plugin_map.lock().unwrap(); plugin_map .get_running_plugin(plugin_id, None) .with_context(err_context)? .clone() }; let running_plugin = running_plugin.lock().unwrap(); let tab_index = running_plugin.store.data().tab_index; let size = Size { rows: running_plugin.rows, cols: running_plugin.columns, }; let plugin_config = running_plugin.store.data().plugin.clone(); loading_indication.set_name(running_plugin.store.data().name()); PluginLoader::new( plugin_cache, loading_indication, senders, plugin_id, client_id, engine, plugin_config, plugin_dir, tab_index, size, path_to_default_shell, plugin_cwd, capabilities, client_attributes, default_shell, default_layout, layout_dir, default_mode, keybinds, ) } pub fn load_module_from_memory(&mut self) -> Result { 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 { 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_file(&self.engine, &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 { self.loading_indication.override_previous_error(); 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_PLUGIN_ARTIFACT_DIR.as_path()) .map_err(anyError::new) .and_then(|_| { // compile module Module::new(&self.engine, &wasm_bytes) }) .and_then(|m| { // serialize module to HD cache for faster loading in the future fs::write(&cached_path, m.serialize()?).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_environment( &mut self, module: Module, ) -> Result<(Store, Instance)> { let (store, instance) = self.create_plugin_instance_env(&module)?; // 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((store, instance)) } pub fn create_plugin_instance_and_wasi_env_for_worker( &mut self, ) -> Result<(Store, Instance)> { let err_context = || { format!( "Failed to create instance and plugin env for worker {}", self.plugin_id ) }; let module = self .plugin_cache .lock() .unwrap() .get(&self.plugin.path) .with_context(err_context)? .clone(); let (store, instance) = self.create_plugin_instance_env(&module)?; Ok((store, instance)) } pub fn load_plugin_instance( &mut self, mut store: Store, instance: &Instance, plugin_map: &Arc>, ) -> Result<()> { let err_context = || format!("failed to load plugin from instance {instance:#?}"); let main_user_instance = instance.clone(); display_loading_stage!( indicate_starting_plugin, self.loading_indication, self.senders, self.plugin_id ); let start_function = instance .get_typed_func::<(), ()>(&mut store, "_start") .with_context(err_context)?; let load_function = instance .get_typed_func::<(), ()>(&mut store, "load") .with_context(err_context)?; let mut workers = HashMap::new(); for function_name in instance .exports(&mut store) .filter_map(|export| export.clone().into_func().map(|_| export.name())) { if function_name.ends_with("_worker") { let (mut store, instance) = self.create_plugin_instance_and_wasi_env_for_worker()?; let start_function_for_worker = instance .get_typed_func::<(), ()>(&mut store, "_start") .with_context(err_context)?; start_function_for_worker .call(&mut store, ()) .with_context(err_context)?; let worker = RunningWorker::new(store, instance, &function_name); let worker_sender = plugin_worker(worker); workers.insert(function_name.into(), worker_sender); } } let subscriptions = store.data().subscriptions.clone(); let plugin = Arc::new(Mutex::new(RunningPlugin::new( store, main_user_instance, self.size.rows, self.size.cols, ))); plugin_map.lock().unwrap().insert( self.plugin_id, self.client_id, plugin.clone(), subscriptions, workers, ); start_function .call(&mut plugin.lock().unwrap().store, ()) .with_context(err_context)?; let protobuf_plugin_configuration: ProtobufPluginConfiguration = self .plugin .userspace_configuration .clone() .try_into() .map_err(|e| anyhow!("Failed to serialize user configuration: {:?}", e))?; let protobuf_bytes = protobuf_plugin_configuration.encode_to_vec(); wasi_write_object(plugin.lock().unwrap().store.data(), &protobuf_bytes) .with_context(err_context)?; load_function .call(&mut plugin.lock().unwrap().store, ()) .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 ); 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, connected_clients: &[ClientId], plugin_map: &Arc>, ) -> Result<()> { if !connected_clients.is_empty() { display_loading_stage!( indicate_cloning_plugin_for_other_clients, self.loading_indication, self.senders, self.plugin_id ); for client_id in connected_clients { if client_id == &self.client_id { // don't reload the plugin once more for ourselves continue; } let mut loading_indication = LoadingIndication::new("".into()); let mut plugin_loader_for_client = PluginLoader::new_from_different_client_id( &self.plugin_cache.clone(), &plugin_map, &mut loading_indication, &self.senders.clone(), self.plugin_id, *client_id, self.engine.clone(), &self.plugin_dir, self.path_to_default_shell.clone(), self.plugin_cwd.clone(), self.capabilities.clone(), self.client_attributes.clone(), self.default_shell.clone(), self.default_layout.clone(), self.layout_dir.clone(), self.default_mode, self.keybinds.clone(), )?; plugin_loader_for_client .load_module_from_memory() .and_then(|module| plugin_loader_for_client.create_plugin_environment(module)) .and_then(|(store, instance)| { plugin_loader_for_client.load_plugin_instance(store, &instance, plugin_map) })? } 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, 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_PLUGIN_ARTIFACT_DIR.join(&hash); self.wasm_blob_on_hd = Some((wasm_bytes.clone(), cached_path.clone())); Ok((wasm_bytes, cached_path)) }, } } pub fn create_wasi_ctx( host_dir: &PathBuf, data_dir: &PathBuf, cache_dir: &PathBuf, tmp_dir: &PathBuf, plugin_url: &String, plugin_id: PluginId, stdin_pipe: Arc>>, stdout_pipe: Arc>>, ) -> Result { let err_context = || format!("Failed to create wasi_ctx"); let dirs = vec![ ("/host".to_owned(), host_dir.clone()), ("/data".to_owned(), data_dir.clone()), ("/cache".to_owned(), cache_dir.clone()), ("/tmp".to_owned(), tmp_dir.clone()), ]; let dirs = dirs.into_iter().filter(|(_dir_name, dir)| { // note that this does not protect against TOCTOU errors // eg. if one or more of these folders existed at the time of check but was deleted // before we mounted in in the wasi environment, we'll crash // when we move to a new wasi environment, we should address this with locking if // there's no built-in solution dir.try_exists().ok().unwrap_or(false) }); let mut wasi_ctx_builder = WasiCtxBuilder::new(); wasi_ctx_builder.env("CLICOLOR_FORCE", "1"); for (guest_path, host_path) in dirs { wasi_ctx_builder .preopened_dir(host_path, guest_path, DirPerms::all(), FilePerms::all()) .with_context(err_context)?; } wasi_ctx_builder .stdin(VecDequeInputStream(stdin_pipe.clone())) .stdout(WriteOutputStream(stdout_pipe.clone())) .stderr(WriteOutputStream(Arc::new(Mutex::new(LoggingPipe::new( plugin_url, plugin_id, ))))); let wasi_ctx = wasi_ctx_builder.build_p1(); Ok(wasi_ctx) } fn create_plugin_instance_env(&self, module: &Module) -> Result<(Store, Instance)> { let err_context = || { format!( "Failed to create instance, plugin env and subscriptions for plugin {}", self.plugin_id ) }; let stdin_pipe = Arc::new(Mutex::new(VecDeque::new())); let stdout_pipe = Arc::new(Mutex::new(VecDeque::new())); let wasi_ctx = PluginLoader::create_wasi_ctx( &self.plugin_cwd, &self.plugin_own_data_dir, &self.plugin_own_cache_dir, &ZELLIJ_TMP_DIR, &self.plugin.location.to_string(), self.plugin_id, stdin_pipe.clone(), stdout_pipe.clone(), )?; let plugin = self.plugin.clone(); let plugin_env = PluginEnv { plugin_id: self.plugin_id, client_id: self.client_id, plugin, permissions: Arc::new(Mutex::new(None)), senders: self.senders.clone(), wasi_ctx, plugin_own_data_dir: self.plugin_own_data_dir.clone(), plugin_own_cache_dir: self.plugin_own_cache_dir.clone(), tab_index: self.tab_index, path_to_default_shell: self.path_to_default_shell.clone(), capabilities: self.capabilities.clone(), client_attributes: self.client_attributes.clone(), default_shell: self.default_shell.clone(), default_layout: self.default_layout.clone(), plugin_cwd: self.plugin_cwd.clone(), input_pipes_to_unblock: Arc::new(Mutex::new(HashSet::new())), input_pipes_to_block: Arc::new(Mutex::new(HashSet::new())), layout_dir: self.layout_dir.clone(), default_mode: self.default_mode.clone(), subscriptions: Arc::new(Mutex::new(HashSet::new())), keybinds: self.keybinds.clone(), stdin_pipe, stdout_pipe, }; let mut store = Store::new(&self.engine, plugin_env); let mut linker = Linker::new(&self.engine); wasmtime_wasi::preview1::add_to_linker_sync(&mut linker, |plugin_env: &mut PluginEnv| { &mut plugin_env.wasi_ctx }) .unwrap(); zellij_exports(&mut linker); let instance = linker .instantiate(&mut store, module) .with_context(err_context)?; if let Some(func) = instance.get_func(&mut store, "_initialize") { func.typed::<(), ()>(&store)?.call(&mut store, ())?; } Ok((store, instance)) } } fn create_plugin_fs_entries( plugin_own_data_dir: &PathBuf, plugin_own_cache_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(&plugin_own_cache_dir) .with_context(|| format!("failed to create cache dir in {plugin_own_cache_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(()) }