diff options
author | Aram Drevekenin <aram@poor.dev> | 2022-12-06 15:34:43 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-12-06 15:34:43 +0100 |
commit | b7adfcc581d40952ec5d1f5a853669322e895bd7 (patch) | |
tree | ba245468ddc56834782604d1829163f8121bd087 /zellij-server | |
parent | c2a6156a6b4ae8cadb1e48cfb8763c0d9233d9f9 (diff) |
refactor(plugins): fix plugin loading data flow (#1995)
Diffstat (limited to 'zellij-server')
19 files changed, 1429 insertions, 934 deletions
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 07e8d0515..63688986a 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -4,6 +4,7 @@ pub mod panes; pub mod tab; mod logging_pipe; +mod plugins; mod pty; mod pty_writer; mod route; @@ -11,7 +12,6 @@ mod screen; mod terminal_bytes; mod thread_bus; mod ui; -mod wasm_vm; use log::info; use pty_writer::{pty_writer_main, PtyWriteInstruction}; @@ -29,10 +29,10 @@ use wasmer::Store; use crate::{ os_input_output::ServerOsApi, + plugins::{plugin_thread_main, PluginInstruction}, pty::{pty_thread_main, Pty, PtyInstruction}, screen::{screen_thread_main, ScreenInstruction}, thread_bus::{Bus, ThreadSenders}, - wasm_vm::{wasm_thread_main, PluginInstruction}, }; use route::route_thread_main; use zellij_utils::{ @@ -108,7 +108,7 @@ pub(crate) struct SessionMetaData { pub default_shell: Option<TerminalAction>, screen_thread: Option<thread::JoinHandle<()>>, pty_thread: Option<thread::JoinHandle<()>>, - wasm_thread: Option<thread::JoinHandle<()>>, + plugin_thread: Option<thread::JoinHandle<()>>, pty_writer_thread: Option<thread::JoinHandle<()>>, } @@ -124,8 +124,8 @@ impl Drop for SessionMetaData { if let Some(pty_thread) = self.pty_thread.take() { let _ = pty_thread.join(); } - if let Some(wasm_thread) = self.wasm_thread.take() { - let _ = wasm_thread.join(); + if let Some(plugin_thread) = self.plugin_thread.take() { + let _ = plugin_thread.join(); } if let Some(pty_writer_thread) = self.pty_writer_thread.take() { let _ = pty_writer_thread.join(); @@ -332,7 +332,7 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) { .as_ref() .unwrap() .senders - .send_to_pty(PtyInstruction::NewTab( + .send_to_screen(ScreenInstruction::NewTab( default_shell.clone(), tab_layout, tab_name, @@ -655,6 +655,7 @@ fn init_session( let pty_thread = thread::Builder::new() .name("pty".to_string()) .spawn({ + let layout = layout.clone(); let pty = Pty::new( Bus::new( vec![pty_receiver], @@ -700,7 +701,7 @@ fn init_session( }) .unwrap(); - let wasm_thread = thread::Builder::new() + let plugin_thread = thread::Builder::new() .name("wasm".to_string()) .spawn({ let plugin_bus = Bus::new( @@ -715,7 +716,14 @@ fn init_session( let store = Store::default(); move || { - wasm_thread_main(plugin_bus, store, data_dir, plugins.unwrap_or_default()).fatal() + plugin_thread_main( + plugin_bus, + store, + data_dir, + plugins.unwrap_or_default(), + layout, + ) + .fatal() } }) .unwrap(); @@ -750,7 +758,7 @@ fn init_session( client_attributes, screen_thread: Some(screen_thread), pty_thread: Some(pty_thread), - wasm_thread: Some(wasm_thread), + plugin_thread: Some(plugin_thread), pty_writer_thread: Some(pty_writer_thread), } } diff --git a/zellij-server/src/panes/floating_panes/mod.rs b/zellij-server/src/panes/floating_panes/mod.rs index 3ca7f5726..bb446e8dc 100644 --- a/zellij-server/src/panes/floating_panes/mod.rs +++ b/zellij-server/src/panes/floating_panes/mod.rs @@ -9,9 +9,9 @@ use crate::{ os_input_output::ServerOsApi, output::{FloatingPanesStack, Output}, panes::{ActivePanes, PaneId}, + plugins::PluginInstruction, thread_bus::ThreadSenders, ui::pane_contents_and_ui::PaneContentsAndUi, - wasm_vm::PluginInstruction, ClientId, }; use std::cell::RefCell; diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index 9353b5114..30624d053 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -3,10 +3,10 @@ use std::time::Instant; use crate::output::{CharacterChunk, SixelImageChunk}; 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::wasm_vm::PluginInstruction; use crate::ClientId; use std::cell::RefCell; use std::rc::Rc; diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index b1665cc97..e49c86543 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -8,11 +8,11 @@ use crate::{ os_input_output::ServerOsApi, output::Output, panes::{ActivePanes, PaneId}, + plugins::PluginInstruction, tab::{Pane, MIN_TERMINAL_HEIGHT, MIN_TERMINAL_WIDTH}, thread_bus::ThreadSenders, ui::boundaries::Boundaries, ui::pane_contents_and_ui::PaneContentsAndUi, - wasm_vm::PluginInstruction, ClientId, }; use zellij_utils::{ @@ -346,9 +346,7 @@ impl TiledPanes { self.reset_boundaries(); } pub fn focus_pane_if_client_not_focused(&mut self, pane_id: PaneId, client_id: ClientId) { - log::info!("inside focus_pane_if_client_not_focused"); if self.active_panes.get(&client_id).is_none() { - log::info!("is none"); self.focus_pane(pane_id, client_id) } } diff --git a/zellij-server/src/plugins/mod.rs b/zellij-server/src/plugins/mod.rs new file mode 100644 index 000000000..c5f3781e7 --- /dev/null +++ b/zellij-server/src/plugins/mod.rs @@ -0,0 +1,135 @@ +mod wasm_bridge; +use log::info; +use std::{collections::HashMap, fs, path::PathBuf}; +use wasmer::Store; + +use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId}; + +use wasm_bridge::WasmBridge; + +use zellij_utils::{ + data::Event, + errors::{prelude::*, ContextType, PluginContext}, + input::{ + command::TerminalAction, + layout::{Layout, PaneLayout, Run, RunPlugin, RunPluginLocation}, + plugins::PluginsConfig, + }, + pane_size::Size, +}; + +#[derive(Clone, Debug)] +pub enum PluginInstruction { + Load(RunPlugin, usize, ClientId, Size), // plugin metadata, tab_index, client_ids + Update(Option<u32>, Option<ClientId>, Event), // Focused plugin / broadcast, client_id, event data + Unload(u32), // plugin_id + Resize(u32, usize, usize), // plugin_id, columns, rows + AddClient(ClientId), + RemoveClient(ClientId), + NewTab( + Option<TerminalAction>, + Option<PaneLayout>, + Option<String>, // tab name + usize, // tab_index + ClientId, + ), + Exit, +} + +impl From<&PluginInstruction> for PluginContext { + fn from(plugin_instruction: &PluginInstruction) -> Self { + match *plugin_instruction { + PluginInstruction::Load(..) => PluginContext::Load, + PluginInstruction::Update(..) => PluginContext::Update, + PluginInstruction::Unload(..) => PluginContext::Unload, + PluginInstruction::Resize(..) => PluginContext::Resize, + PluginInstruction::Exit => PluginContext::Exit, + PluginInstruction::AddClient(_) => PluginContext::AddClient, + PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient, + PluginInstruction::NewTab(..) => PluginContext::NewTab, + } + } +} + +pub(crate) fn plugin_thread_main( + bus: Bus<PluginInstruction>, + store: Store, + data_dir: PathBuf, + plugins: PluginsConfig, + layout: Box<Layout>, +) -> Result<()> { + info!("Wasm main thread starts"); + + let plugin_dir = data_dir.join("plugins/"); + let plugin_global_data_dir = plugin_dir.join("data"); + + let mut wasm_bridge = WasmBridge::new(plugins, bus.senders.clone(), store, plugin_dir); + + loop { + let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); + err_ctx.add_call(ContextType::Plugin((&event).into())); + match event { + // TODO: remove pid_tx from here + PluginInstruction::Load(run, tab_index, client_id, size) => { + wasm_bridge.load_plugin(&run, tab_index, size, client_id)?; + }, + PluginInstruction::Update(pid, cid, event) => { + wasm_bridge.update_plugins(pid, cid, event)?; + }, + PluginInstruction::Unload(pid) => { + wasm_bridge.unload_plugin(pid)?; + }, + PluginInstruction::Resize(pid, new_columns, new_rows) => { + wasm_bridge.resize_plugin(pid, new_columns, new_rows)?; + }, + PluginInstruction::AddClient(client_id) => { + wasm_bridge.add_client(client_id)?; + }, + PluginInstruction::RemoveClient(client_id) => { + wasm_bridge.remove_client(client_id); + }, + PluginInstruction::NewTab( + terminal_action, + tab_layout, + tab_name, + tab_index, + client_id, + ) => { + let mut plugin_ids: HashMap<RunPluginLocation, Vec<u32>> = HashMap::new(); + let extracted_run_instructions = tab_layout + .clone() + .unwrap_or_else(|| layout.new_tab()) + .extract_run_instructions(); + let size = Size::default(); // TODO: is this bad? + for run_instruction in extracted_run_instructions { + if let Some(Run::Plugin(run)) = run_instruction { + let plugin_id = + wasm_bridge.load_plugin(&run, tab_index, size, client_id)?; + plugin_ids.entry(run.location).or_default().push(plugin_id); + } + } + drop(bus.senders.send_to_pty(PtyInstruction::NewTab( + terminal_action, + tab_layout, + tab_name, + tab_index, + plugin_ids, + client_id, + ))); + }, + PluginInstruction::Exit => break, + } + } + info!("wasm main thread exits"); + + fs::remove_dir_all(&plugin_global_data_dir) + .or_else(|err| { + if err.kind() == std::io::ErrorKind::NotFound { + // I don't care... + Ok(()) + } else { + Err(err) + } + }) + .context("failed to cleanup plugin data directory") +} diff --git a/zellij-server/src/plugins/wasm_bridge.rs b/zellij-server/src/plugins/wasm_bridge.rs new file mode 100644 index 000000000..374b1dd48 --- /dev/null +++ b/zellij-server/src/plugins/wasm_bridge.rs @@ -0,0 +1,737 @@ +use super::PluginInstruction; +use highway::{HighwayHash, PortableHash}; +use log::{debug, info, warn}; +use semver::Version; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + collections::{HashMap, HashSet}, + fmt, fs, + path::PathBuf, + process, + str::FromStr, + sync::{Arc, Mutex}, + thread, + time::{Duration, Instant}, +}; +use url::Url; +use wasmer::{ + imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value, + WasmerEnv, +}; +use wasmer_wasi::{Pipe, WasiEnv, WasiState}; + +use crate::{ + logging_pipe::LoggingPipe, + panes::PaneId, + pty::{ClientOrTabIndex, PtyInstruction}, + screen::ScreenInstruction, + thread_bus::ThreadSenders, + ClientId, +}; + +use zellij_utils::{ + consts::{DEBUG_MODE, VERSION, ZELLIJ_CACHE_DIR, ZELLIJ_TMP_DIR}, + data::{Event, EventType, PluginIds}, + errors::prelude::*, + input::{ + command::TerminalAction, + layout::RunPlugin, + plugins::{PluginConfig, PluginType, PluginsConfig}, + }, + pane_size::Size, + serde, +}; + +/// Custom error for plugin version mismatch. +/// +/// This is thrown when, during starting a plugin, it is detected that the plugin version doesn't +/// match the zellij version. This is treated as a fatal error and leads to instantaneous +/// termination. +#[derive(Debug)] +pub struct VersionMismatchError { + zellij_version: String, + plugin_version: String, + plugin_path: PathBuf, + // true for builtin plugins + builtin: bool, +} + +impl std::error::Error for VersionMismatchError {} + +impl VersionMismatchError { + pub fn new( + zellij_version: &str, + plugin_version: &str, + plugin_path: &PathBuf, + builtin: bool, + ) -> Self { + VersionMismatchError { + zellij_version: zellij_version.to_owned(), + plugin_version: plugin_version.to_owned(), + plugin_path: plugin_path.to_owned(), + builtin, + } + } +} + +impl fmt::Display for VersionMismatchError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let first_line = if self.builtin { + "It seems your version of zellij was built with outdated core plugins." + } else { + "If you're seeing this error a plugin version doesn't match the current +zellij version." + }; + + write!( + f, + "{} +Detected versions: + +- Plugin version: {} +- Zellij version: {} +- Offending plugin: {} + +If you're a user: + Please contact the distributor of your zellij version and report this error + to them. + +If you're a developer: + Please run zellij with updated plugins. The easiest way to achieve this + is to build zellij with `cargo make 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)] +pub struct PluginEnv { + pub plugin_id: u32, + pub plugin: PluginConfig, + pub senders: ThreadSenders, + pub wasi_env: WasiEnv, + pub subscriptions: Arc<Mutex<HashSet<EventType>>>, + pub tab_index: usize, + pub client_id: ClientId, + #[allow(dead_code)] + plugin_own_data_dir: PathBuf, +} + +type PluginMap = HashMap<(u32, ClientId), (Instance, PluginEnv, (usize, usize))>; // u32 => + // plugin_id, + // (usize, usize) + // => (rows, + // columns) + +pub struct WasmBridge { + connected_clients: Vec<ClientId>, + plugins: PluginsConfig, + senders: ThreadSenders, + store: Store, + plugin_dir: PathBuf, + plugin_cache: HashMap<PathBuf, Module>, + plugin_map: PluginMap, + next_plugin_id: u32, +} + +impl WasmBridge { + pub fn new( + plugins: PluginsConfig, + senders: ThreadSenders, + store: Store, + plugin_dir: PathBuf, + ) -> Self { + let plugin_map = HashMap::new(); + let connected_clients: Vec<ClientId> = vec![]; + let plugin_cache: HashMap<PathBuf, Module> = HashMap::new(); + WasmBridge { + connected_clients, + plugins, + senders, + store, + plugin_dir, + plugin_cache, + plugin_map, + next_plugin_id: 0, + } + } + pub fn load_plugin( + &mut self, + run: &RunPlugin, + tab_index: usize, + size: Size, + client_id: ClientId, + ) -> Result<u32> { + // returns the plugin id + let err_context = || format!("failed to load plugin for client {client_id}"); + let plugin_id = self.next_plugin_id; + + let plugin = self + .plugins + .get(run) + .with_context(|| format!("failed to resolve plugin {run:?}")) + .with_context(err_context) + .fatal(); + + let (instance, plugin_env) = self + .start_plugin(plugin_id, client_id, &plugin, tab_index) + .with_context(err_context)?; + + let mut main_user_instance = instance.clone(); + let main_user_env = plugin_env.clone(); + load_plugin_instance(&mut main_user_instance).with_context(err_context)?; + + self.plugin_map.insert( + (plugin_id, client_id), + (main_user_instance, main_user_env, (size.rows, size.cols)), + ); + + // clone plugins for the rest of the client ids if they exist + for client_id in self.connected_clients.iter() { + 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(&self.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)?; + self.plugin_map.insert( + (plugin_id, *client_id), + (instance, new_plugin_env, (size.rows, size.cols)), + ); + } + self.next_plugin_id += 1; + Ok(plugin_id) + } + pub fn unload_plugin(&mut self, pid: u32) -> Result<()> { + info!("Bye from plugin {}", &pid); + // TODO: remove plugin's own data directory + let ids_in_plugin_map: Vec<(u32, ClientId)> = self.plugin_map.keys().copied().collect(); + for (plugin_id, client_id) in ids_in_plugin_map { + if pid == plugin_id { + drop(self.plugin_map.remove(&(plugin_id, client_id))); + } + } + Ok(()) + } + #[allow(clippy::too_many_arguments)] + pub fn start_plugin( + &mut self, + plugin_id: u32, + client_id: ClientId, + plugin: &PluginConfig, + tab_index: usize, + ) -> Result<(Instance, PluginEnv)> { + 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()); + let cache_hit = self.plugin_cache.contains_key(&plugin.path); + + // We remove the entry here and repopulate it at the very bottom, if everything went well. + // We must do that because a `get` will only give us a borrow of the Module. This suffices for + // the purpose of setting everything up, but we cannot return a &Module from the "None" match + // arm, because we create the Module from scratch there. Any reference passed outside would + // outlive the Module we create there. Hence, we remove the plugin here and reinsert it + // below... + let module = match self.plugin_cache.remove(&plugin.path) { + Some(module) => { + log::debug!( + "Loaded plugin '{}' from plugin cache", + plugin.path.display() + ); + module + }, + None => { + // 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(&self.plugin_dir) + .with_context(err_context) + .fatal(); + + fs::create_dir_all(&plugin_own_data_dir) + .with_context(|| format!("failed to create datadir in {plugin_own_data_dir:?}")) + .with_context(err_context) + .non_fatal(); + + // ensure tmp dir exists, in case it somehow was deleted (e.g systemd-tmpfiles) + 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) + .non_fatal(); + + let hash: String = PortableHash::default() + .hash256(&wasm_bytes) + .iter() + .map(ToString::to_string) + .collect(); + let cached_path = ZELLIJ_CACHE_DIR.join(&hash); |