diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-03-27 19:19:34 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-27 19:19:34 +0200 |
commit | 341f9eb8c8771a59b2e4d238ba49ba88c3720d6d (patch) | |
tree | 51205536dd0789efb770dbe0095af7210a60eed3 /zellij-server | |
parent | 7b609b053f3aaf466258e12be53d57614c8884c7 (diff) |
feat(plugins): async plugin loading (#2327)
* work
* refactor(plugins): break down start plugin async function
* work
* loading messages
* nice ui
* floating panes and error handling
* cleanups and conflicting plugin/direction
* find exact pane when relayouting
* fix plugin pane titles
* kill loading tasks on exit
* refactor: move stuff around
* style(fmt): rustfmt
* various fixes and refactors
Diffstat (limited to 'zellij-server')
-rw-r--r-- | zellij-server/src/background_jobs.rs | 42 | ||||
-rw-r--r-- | zellij-server/src/panes/plugin_pane.rs | 40 | ||||
-rw-r--r-- | zellij-server/src/plugins/mod.rs | 38 | ||||
-rw-r--r-- | zellij-server/src/plugins/start_plugin.rs | 471 | ||||
-rw-r--r-- | zellij-server/src/plugins/wasm_bridge.rs | 510 | ||||
-rw-r--r-- | zellij-server/src/route.rs | 16 | ||||
-rw-r--r-- | zellij-server/src/screen.rs | 112 | ||||
-rw-r--r-- | zellij-server/src/tab/layout_applier.rs | 61 | ||||
-rw-r--r-- | zellij-server/src/tab/mod.rs | 146 | ||||
-rw-r--r-- | zellij-server/src/ui/loading_indication.rs | 260 | ||||
-rw-r--r-- | zellij-server/src/ui/mod.rs | 1 | ||||
-rw-r--r-- | zellij-server/src/unit/screen_tests.rs | 3 |
12 files changed, 1404 insertions, 296 deletions
diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index c612c9d57..8061b4a47 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -2,6 +2,10 @@ use zellij_utils::async_std::task; use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; use std::collections::HashMap; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; use std::time::{Duration, Instant}; use crate::panes::PaneId; @@ -11,6 +15,8 @@ use crate::thread_bus::Bus; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum BackgroundJob { DisplayPaneError(Vec<PaneId>, String), + AnimatePluginLoading(u32), // u32 - plugin_id + StopPluginLoadingAnimation(u32), // u32 - plugin_id Exit, } @@ -18,16 +24,22 @@ impl From<&BackgroundJob> for BackgroundJobContext { fn from(background_job: &BackgroundJob) -> Self { match *background_job { BackgroundJob::DisplayPaneError(..) => BackgroundJobContext::DisplayPaneError, + BackgroundJob::AnimatePluginLoading(..) => BackgroundJobContext::AnimatePluginLoading, + BackgroundJob::StopPluginLoadingAnimation(..) => { + BackgroundJobContext::StopPluginLoadingAnimation + }, BackgroundJob::Exit => BackgroundJobContext::Exit, } } } static FLASH_DURATION_MS: u64 = 1000; +static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500; pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> { let err_context = || "failed to write to pty".to_string(); let mut running_jobs: HashMap<BackgroundJob, Instant> = HashMap::new(); + let mut loading_plugins: HashMap<u32, Arc<AtomicBool>> = HashMap::new(); // u32 - plugin_id loop { let (event, mut err_ctx) = bus.recv().with_context(err_context)?; @@ -54,7 +66,37 @@ pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> { } }); }, + BackgroundJob::AnimatePluginLoading(pid) => { + let loading_plugin = Arc::new(AtomicBool::new(true)); + if job_already_running(job, &mut running_jobs) { + continue; + } + task::spawn({ + let senders = bus.senders.clone(); + let loading_plugin = loading_plugin.clone(); + async move { + while loading_plugin.load(Ordering::SeqCst) { + let _ = senders.send_to_screen( + ScreenInstruction::ProgressPluginLoadingOffset(pid), + ); + task::sleep(std::time::Duration::from_millis( + PLUGIN_ANIMATION_OFFSET_DURATION_MD, + )) + .await; + } + } + }); + loading_plugins.insert(pid, loading_plugin); + }, + BackgroundJob::StopPluginLoadingAnimation(pid) => { + if let Some(loading_plugin) = loading_plugins.remove(&pid) { + loading_plugin.store(false, Ordering::SeqCst); + } + }, BackgroundJob::Exit => { + for loading_plugin in loading_plugins.values() { + loading_plugin.store(false, Ordering::SeqCst); + } return Ok(()); }, } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index c6896f90d..28828907e 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -6,7 +6,10 @@ use crate::panes::{grid::Grid, sixel::SixelImageStore, LinkHandler, PaneId}; use crate::plugins::PluginInstruction; use crate::pty::VteBytes; use crate::tab::Pane; -use crate::ui::pane_boundaries_frame::{FrameParams, PaneFrame}; +use crate::ui::{ + loading_indication::LoadingIndication, + pane_boundaries_frame::{FrameParams, PaneFrame}, +}; use crate::ClientId; use std::cell::RefCell; use std::rc::Rc; @@ -67,6 +70,7 @@ pub(crate) struct PluginPane { borderless: bool, pane_frame_color_override: Option<(PaletteColor, Option<String>)>, invoked_with: Option<Run>, + loading_indication: LoadingIndication, } impl PluginPane { @@ -81,10 +85,13 @@ impl PluginPane { terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>, link_handler: Rc<RefCell<LinkHandler>>, character_cell_size: Rc<RefCell<Option<SizeInPixels>>>, + currently_connected_clients: Vec<ClientId>, style: Style, invoked_with: Option<Run>, ) -> Self { - Self { + let loading_indication = LoadingIndication::new(title.clone()).with_colors(style.colors); + let initial_loading_message = loading_indication.to_string(); + let mut plugin = PluginPane { pid, should_render: HashMap::new(), selectable: true, @@ -108,7 +115,12 @@ impl PluginPane { style, pane_frame_color_override: None, invoked_with, + loading_indication, + }; + for client_id in currently_connected_clients { + plugin.handle_plugin_bytes(client_id, initial_loading_message.as_bytes().to_vec()); } + plugin } } @@ -513,6 +525,24 @@ impl Pane for PluginPane { fn set_title(&mut self, title: String) { self.pane_title = title; } + fn update_loading_indication(&mut self, loading_indication: LoadingIndication) { + if self.loading_indication.ended { + return; + } + self.loading_indication.merge(loading_indication); + self.handle_plugin_bytes_for_all_clients( + self.loading_indication.to_string().as_bytes().to_vec(), + ); + } + fn progress_animation_offset(&mut self) { + if self.loading_indication.ended { + return; + } + self.loading_indication.progress_animation_offset(); + self.handle_plugin_bytes_for_all_clients( + self.loading_indication.to_string().as_bytes().to_vec(), + ); + } } impl PluginPane { @@ -527,4 +557,10 @@ impl PluginPane { fn set_client_should_render(&mut self, client_id: ClientId, should_render: bool) { self.should_render.insert(client_id, should_render); } + fn handle_plugin_bytes_for_all_clients(&mut self, bytes: VteBytes) { + let client_ids: Vec<ClientId> = self.grids.keys().copied().collect(); + for client_id in client_ids { + self.handle_plugin_bytes(client_id, bytes.clone()); + } + } } diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs index 1cd034490..e1884b1e6 100644 --- a/zellij-server/src/plugins/mod.rs +++ b/zellij-server/src/plugins/mod.rs @@ -1,8 +1,10 @@ +mod start_plugin; mod wasm_bridge; use log::info; use std::{collections::HashMap, fs, path::PathBuf}; use wasmer::Store; +use crate::screen::ScreenInstruction; use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId}; use wasm_bridge::WasmBridge; @@ -20,7 +22,14 @@ use zellij_utils::{ #[derive(Clone, Debug)] pub enum PluginInstruction { - Load(RunPlugin, usize, ClientId, Size), // plugin metadata, tab_index, client_ids + Load( + Option<bool>, // should float + Option<String>, // pane title + RunPlugin, + usize, // tab index + ClientId, + Size, + ), Update(Vec<(Option<u32>, Option<ClientId>, Event)>), // Focused plugin / broadcast, client_id, event data Unload(u32), // plugin_id Resize(u32, usize, usize), // plugin_id, columns, rows @@ -33,6 +42,7 @@ pub enum PluginInstruction { usize, // tab_index ClientId, ), + ApplyCachedEvents(u32), // u32 is the plugin id Exit, } @@ -47,6 +57,7 @@ impl From<&PluginInstruction> for PluginContext { PluginInstruction::AddClient(_) => PluginContext::AddClient, PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient, PluginInstruction::NewTab(..) => PluginContext::NewTab, + PluginInstruction::ApplyCachedEvents(..) => PluginContext::ApplyCachedEvents, } } } @@ -69,8 +80,21 @@ pub(crate) fn plugin_thread_main( let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); err_ctx.add_call(ContextType::Plugin((&event).into())); match event { - PluginInstruction::Load(run, tab_index, client_id, size) => { - wasm_bridge.load_plugin(&run, tab_index, size, client_id)?; + PluginInstruction::Load(should_float, pane_title, run, tab_index, client_id, size) => { + 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}"); + }, + } }, PluginInstruction::Update(updates) => { wasm_bridge.update_plugins(updates)?; @@ -126,7 +150,13 @@ pub(crate) fn plugin_thread_main( client_id, ))); }, - PluginInstruction::Exit => break, + PluginInstruction::ApplyCachedEvents(plugin_id) => { + wasm_bridge.apply_cached_events(plugin_id)?; + }, + PluginInstruction::Exit => { + wasm_bridge.cleanup(); + break; + }, } } info!("wasm main thread exits"); diff --git a/zellij-server/src/plugins/start_plugin.rs b/zellij-server/src/plugins/start_plugin.rs new file mode 100644 index 000000000..28000bd15 --- /dev/null +++ b/zellij-server/src/plugins/start_plugin.rs @@ -0,0 +1,471 @@ +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, |