summaryrefslogtreecommitdiffstats
path: root/zellij-server/src
diff options
context:
space:
mode:
Diffstat (limited to 'zellij-server/src')
-rw-r--r--zellij-server/src/lib.rs26
-rw-r--r--zellij-server/src/panes/floating_panes/mod.rs2
-rw-r--r--zellij-server/src/panes/plugin_pane.rs2
-rw-r--r--zellij-server/src/panes/tiled_panes/mod.rs4
-rw-r--r--zellij-server/src/plugins/mod.rs135
-rw-r--r--zellij-server/src/plugins/wasm_bridge.rs737
-rw-r--r--zellij-server/src/pty.rs23
-rw-r--r--zellij-server/src/route.rs4
-rw-r--r--zellij-server/src/screen.rs129
-rw-r--r--zellij-server/src/tab/mod.rs86
-rw-r--r--zellij-server/src/tab/unit/tab_integration_tests.rs42
-rw-r--r--zellij-server/src/tab/unit/tab_tests.rs20
-rw-r--r--zellij-server/src/thread_bus.rs4
-rw-r--r--zellij-server/src/unit/screen_tests.rs155
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_default_params.snap46
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_new_tab_action_with_name_and_layout.snap3
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_rename_tab.snap80
-rw-r--r--zellij-server/src/unit/snapshots/zellij_server__screen__screen_tests__send_cli_undo_rename_tab.snap80
-rw-r--r--zellij-server/src/wasm_vm.rs785
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();
+
+ // ensu