zellij/zellij-server/src/plugins/wasm_bridge.rs
Jae-Heon Ji c8ddb23297
feat: add plugin permission system (#2624)
* WIP: add exaple of permission ui

* feat: add request permission ui

* feat: add caching permission in memory

* feat: add permission check

* feat: add file caching

* fix: changes request

* feat(ui): new status bar mode (#2619)

* supermode prototype

* fix integration tests

* fix tests

* style(fmt): rustfmt

* docs(changelog): status-bar supermode

* fix(rendering): occasional glitches while resizing (#2621)

* docs(changelog): resize glitches fix

* chore(version): bump development version

* Fix colored pane frames in mirrored sessions (#2625)

* server/panes/tiled: Fix colored frames

in mirrored sessions. Colored frames were previously ignored because
they were treated like floating panes when rendering tiled panes.

* CHANGELOG: Add PR #2625

* server/tab/unit: Fix unit tests for server.

* fix(sessions): use custom lists of adjectives and nouns for generating session names (#2122)

* Create custom lists of adjectives and nouns for generating session names

* move word lists to const slices

* add logic to retry name generation

* refactor

 - reuse the name generator
 - iterator instead of for loop

---------

Co-authored-by: Thomas Linford <linford.t@gmail.com>

* docs(changelog): generate session names with custom words list

* feat(plugins): make plugins configurable (#2646)

* work

* make every plugin entry point configurable

* make integration tests pass

* make e2e tests pass

* add test for plugin configuration

* add test snapshot

* add plugin config parsing test

* cleanups

* style(fmt): rustfmt

* style(comment): remove commented code

* docs(changelog): configurable plugins

* style(fmt): rustfmt

* touch up ui

* fix: don't save permission data in memory

* feat: load cached permission

* test: add example test (WIP)

* fix: issue event are always denied

* test: update snapshot

* apply formatting

* refactor: update default cache function

* test: add more new test

* apply formatting

* Revert "apply formatting"

This reverts commit a4e93703fbfdb6865131daa1c8b90fc5c36ab25e.

* apply format

* fix: update cache path

* apply format

* fix: cache path

* fix: update log level

* test for github workflow

* Revert "test for github workflow"

This reverts commit 01eff3bc5d1627a4e60bc6dac8ebe5500bc5b56e.

* refactor: permission cache

* fix(test): permission grant/deny race condition

* style(fmt): rustfmt

* style(fmt): rustfmt

* configure permissions

* permission denied test

* snapshot

* add ui for small plugins

* style(fmt): rustfmt

* some cleanups

---------

Co-authored-by: Aram Drevekenin <aram@poor.dev>
Co-authored-by: har7an <99636919+har7an@users.noreply.github.com>
Co-authored-by: Kyle Sutherland-Cash <kyle.sutherlandcash@gmail.com>
Co-authored-by: Thomas Linford <linford.t@gmail.com>
Co-authored-by: Thomas Linford <tlinford@users.noreply.github.com>
2023-08-12 15:35:42 +02:00

865 lines
35 KiB
Rust

use super::{PluginId, PluginInstruction};
use crate::plugins::plugin_loader::PluginLoader;
use crate::plugins::plugin_map::{AtomicEvent, PluginEnv, PluginMap, RunningPlugin, Subscriptions};
use crate::plugins::plugin_worker::MessageToWorker;
use crate::plugins::watch_filesystem::watch_filesystem;
use crate::plugins::zellij_exports::{wasi_read_string, wasi_write_object};
use log::info;
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
str::FromStr,
sync::{Arc, Mutex},
};
use wasmer::{Instance, Module, Store, Value};
use zellij_utils::async_std::task::{self, JoinHandle};
use zellij_utils::data::{PermissionStatus, PermissionType};
use zellij_utils::input::permission::PermissionCache;
use zellij_utils::notify_debouncer_full::{notify::RecommendedWatcher, Debouncer, FileIdMap};
use zellij_utils::plugin_api::event::ProtobufEvent;
use zellij_utils::prost::Message;
use crate::{
background_jobs::BackgroundJob, screen::ScreenInstruction, thread_bus::ThreadSenders,
ui::loading_indication::LoadingIndication, ClientId,
};
use zellij_utils::{
data::{Event, EventType, PluginCapabilities},
errors::prelude::*,
input::{
command::TerminalAction,
layout::{Layout, RunPlugin, RunPluginLocation},
plugins::PluginsConfig,
},
ipc::ClientAttributes,
pane_size::Size,
};
pub struct WasmBridge {
connected_clients: Arc<Mutex<Vec<ClientId>>>,
plugins: PluginsConfig,
senders: ThreadSenders,
store: Store,
plugin_dir: PathBuf,
plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>>,
plugin_map: Arc<Mutex<PluginMap>>,
next_plugin_id: PluginId,
cached_events_for_pending_plugins: HashMap<PluginId, Vec<Event>>,
cached_resizes_for_pending_plugins: HashMap<PluginId, (usize, usize)>, // (rows, columns)
cached_worker_messages: HashMap<PluginId, Vec<(ClientId, String, String, String)>>, // Vec<clientid,
// worker_name,
// message,
// payload>
loading_plugins: HashMap<(PluginId, RunPlugin), JoinHandle<()>>, // plugin_id to join-handle
pending_plugin_reloads: HashSet<RunPlugin>,
path_to_default_shell: PathBuf,
watcher: Option<Debouncer<RecommendedWatcher, FileIdMap>>,
zellij_cwd: PathBuf,
capabilities: PluginCapabilities,
client_attributes: ClientAttributes,
default_shell: Option<TerminalAction>,
default_layout: Box<Layout>,
}
impl WasmBridge {
pub fn new(
plugins: PluginsConfig,
senders: ThreadSenders,
store: Store,
plugin_dir: PathBuf,
path_to_default_shell: PathBuf,
zellij_cwd: PathBuf,
capabilities: PluginCapabilities,
client_attributes: ClientAttributes,
default_shell: Option<TerminalAction>,
default_layout: Box<Layout>,
) -> Self {
let plugin_map = Arc::new(Mutex::new(PluginMap::default()));
let connected_clients: Arc<Mutex<Vec<ClientId>>> = Arc::new(Mutex::new(vec![]));
let plugin_cache: Arc<Mutex<HashMap<PathBuf, Module>>> =
Arc::new(Mutex::new(HashMap::new()));
let watcher = None;
WasmBridge {
connected_clients,
plugins,
senders,
store,
plugin_dir,
plugin_cache,
plugin_map,
path_to_default_shell,
watcher,
next_plugin_id: 0,
cached_events_for_pending_plugins: HashMap::new(),
cached_resizes_for_pending_plugins: HashMap::new(),
cached_worker_messages: HashMap::new(),
loading_plugins: HashMap::new(),
pending_plugin_reloads: HashSet::new(),
zellij_cwd,
capabilities,
client_attributes,
default_shell,
default_layout,
}
}
pub fn load_plugin(
&mut self,
run: &RunPlugin,
tab_index: usize,
size: Size,
client_id: Option<ClientId>,
) -> Result<PluginId> {
// returns the plugin id
let err_context = move || format!("failed to load plugin");
let client_id = client_id
.or_else(|| {
self.connected_clients
.lock()
.unwrap()
.iter()
.next()
.copied()
})
.with_context(|| {
"Plugins must have a client id, none was provided and none are connected"
})?;
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)?;
let plugin_name = run.location.to_string();
self.cached_events_for_pending_plugins
.insert(plugin_id, vec![]);
self.cached_resizes_for_pending_plugins
.insert(plugin_id, (size.rows, size.cols));
let load_plugin_task = task::spawn({
let plugin_dir = self.plugin_dir.clone();
let plugin_cache = self.plugin_cache.clone();
let senders = self.senders.clone();
let store = self.store.clone();
let plugin_map = self.plugin_map.clone();
let connected_clients = self.connected_clients.clone();
let path_to_default_shell = self.path_to_default_shell.clone();
let zellij_cwd = self.zellij_cwd.clone();
let capabilities = self.capabilities.clone();
let client_attributes = self.client_attributes.clone();
let default_shell = self.default_shell.clone();
let default_layout = self.default_layout.clone();
async move {
let _ =
senders.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(plugin_id));
let mut loading_indication = LoadingIndication::new(plugin_name.clone());
match PluginLoader::start_plugin(
plugin_id,
client_id,
&plugin,
tab_index,
plugin_dir,
plugin_cache,
senders.clone(),
store,
plugin_map,
size,
connected_clients.clone(),
&mut loading_indication,
path_to_default_shell,
zellij_cwd.clone(),
capabilities,
client_attributes,
default_shell,
default_layout,
) {
Ok(_) => handle_plugin_successful_loading(&senders, plugin_id),
Err(e) => handle_plugin_loading_failure(
&senders,
plugin_id,
&mut loading_indication,
e,
),
}
let _ =
senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(vec![plugin_id]));
}
});
self.loading_plugins
.insert((plugin_id, run.clone()), load_plugin_task);
self.next_plugin_id += 1;
Ok(plugin_id)
}
pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> {
info!("Bye from plugin {}", &pid);
let mut plugin_map = self.plugin_map.lock().unwrap();
for (running_plugin, _, workers) in plugin_map.remove_plugins(pid) {
for (_worker_name, worker_sender) in workers {
drop(worker_sender.send(MessageToWorker::Exit));
}
let running_plugin = running_plugin.lock().unwrap();
let cache_dir = running_plugin.plugin_env.plugin_own_data_dir.clone();
if let Err(e) = std::fs::remove_dir_all(cache_dir) {
log::error!("Failed to remove cache dir for plugin: {:?}", e);
}
}
Ok(())
}
pub fn reload_plugin(&mut self, run_plugin: &RunPlugin) -> Result<()> {
if self.plugin_is_currently_being_loaded(&run_plugin.location) {
self.pending_plugin_reloads.insert(run_plugin.clone());
return Ok(());
}
let plugin_ids = self.all_plugin_ids_for_plugin_location(&run_plugin.location)?;
for plugin_id in &plugin_ids {
let (rows, columns) = self.size_of_plugin_id(*plugin_id).unwrap_or((0, 0));
self.cached_events_for_pending_plugins
.insert(*plugin_id, vec![]);
self.cached_resizes_for_pending_plugins
.insert(*plugin_id, (rows, columns));
}
let first_plugin_id = *plugin_ids.get(0).unwrap(); // this is safe becaise the above
// methods always returns at least 1 id
let mut loading_indication = LoadingIndication::new(run_plugin.location.to_string());
self.start_plugin_loading_indication(&plugin_ids, &loading_indication);
let load_plugin_task = task::spawn({
let plugin_dir = self.plugin_dir.clone();
let plugin_cache = self.plugin_cache.clone();
let senders = self.senders.clone();
let store = self.store.clone();
let plugin_map = self.plugin_map.clone();
let connected_clients = self.connected_clients.clone();
let path_to_default_shell = self.path_to_default_shell.clone();
let zellij_cwd = self.zellij_cwd.clone();
let capabilities = self.capabilities.clone();
let client_attributes = self.client_attributes.clone();
let default_shell = self.default_shell.clone();
let default_layout = self.default_layout.clone();
async move {
match PluginLoader::reload_plugin(
first_plugin_id,
plugin_dir.clone(),
plugin_cache.clone(),
senders.clone(),
store.clone(),
plugin_map.clone(),
connected_clients.clone(),
&mut loading_indication,
path_to_default_shell.clone(),
zellij_cwd.clone(),
capabilities.clone(),
client_attributes.clone(),
default_shell.clone(),
default_layout.clone(),
) {
Ok(_) => {
handle_plugin_successful_loading(&senders, first_plugin_id);
for plugin_id in &plugin_ids {
if plugin_id == &first_plugin_id {
// no need to reload the plugin we just reloaded
continue;
}
let mut loading_indication = LoadingIndication::new("".into());
match PluginLoader::reload_plugin_from_memory(
*plugin_id,
plugin_dir.clone(),
plugin_cache.clone(),
senders.clone(),
store.clone(),
plugin_map.clone(),
connected_clients.clone(),
&mut loading_indication,
path_to_default_shell.clone(),
zellij_cwd.clone(),
capabilities.clone(),
client_attributes.clone(),
default_shell.clone(),
default_layout.clone(),
) {
Ok(_) => handle_plugin_successful_loading(&senders, *plugin_id),
Err(e) => handle_plugin_loading_failure(
&senders,
*plugin_id,
&mut loading_indication,
e,
),
}
}
},
Err(e) => {
for plugin_id in &plugin_ids {
handle_plugin_loading_failure(
&senders,
*plugin_id,
&mut loading_indication,
&e,
);
}
},
}
let _ = senders.send_to_plugin(PluginInstruction::ApplyCachedEvents(plugin_ids));
}
});
self.loading_plugins
.insert((first_plugin_id, run_plugin.clone()), load_plugin_task);
Ok(())
}
pub fn add_client(&mut self, client_id: ClientId) -> Result<()> {
let mut loading_indication = LoadingIndication::new("".into());
match PluginLoader::add_client(
client_id,
self.plugin_dir.clone(),
self.plugin_cache.clone(),
self.senders.clone(),
self.store.clone(),
self.plugin_map.clone(),
self.connected_clients.clone(),
&mut loading_indication,
self.path_to_default_shell.clone(),
self.zellij_cwd.clone(),
self.capabilities.clone(),
self.client_attributes.clone(),
self.default_shell.clone(),
self.default_layout.clone(),
) {
Ok(_) => {
let _ = self
.senders
.send_to_screen(ScreenInstruction::RequestStateUpdateForPlugins);
Ok(())
},
Err(e) => Err(e),
}
}
pub fn resize_plugin(
&mut self,
pid: PluginId,
new_columns: usize,
new_rows: usize,
) -> Result<()> {
let err_context = move || format!("failed to resize plugin {pid}");
let plugins_to_resize: Vec<(PluginId, ClientId, Arc<Mutex<RunningPlugin>>)> = self
.plugin_map
.lock()
.unwrap()
.running_plugins()
.iter()
.cloned()
.filter(|(plugin_id, _client_id, _running_plugin)| {
!self
.cached_resizes_for_pending_plugins
.contains_key(&plugin_id)
})
.collect();
for (plugin_id, client_id, running_plugin) in plugins_to_resize {
if plugin_id == pid {
let event_id = running_plugin
.lock()
.unwrap()
.next_event_id(AtomicEvent::Resize);
task::spawn({
let senders = self.senders.clone();
let running_plugin = running_plugin.clone();
let plugin_id = plugin_id;
let client_id = client_id;
async move {
let mut running_plugin = running_plugin.lock().unwrap();
if running_plugin.apply_event_id(AtomicEvent::Resize, event_id) {
running_plugin.rows = new_rows;
running_plugin.columns = new_columns;
let rendered_bytes = running_plugin
.instance
.exports
.get_function("render")
.map_err(anyError::new)
.and_then(|render| {
render
.call(&[
Value::I32(running_plugin.rows as i32),
Value::I32(running_plugin.columns as i32),
])
.map_err(anyError::new)
})
.and_then(|_| wasi_read_string(&running_plugin.plugin_env.wasi_env))
.with_context(err_context);
match rendered_bytes {
Ok(rendered_bytes) => {
let plugin_bytes = vec![(
plugin_id,
client_id,
rendered_bytes.as_bytes().to_vec(),
)];
senders
.send_to_screen(ScreenInstruction::PluginBytes(
plugin_bytes,
))
.unwrap();
},
Err(e) => log::error!("{}", e),
}
}
}
});
}
}
for (plugin_id, mut current_size) in self.cached_resizes_for_pending_plugins.iter_mut() {
if *plugin_id == pid {
current_size.0 = new_rows;
current_size.1 = new_columns;
}
}
Ok(())
}
pub fn update_plugins(
&mut self,
mut updates: Vec<(Option<PluginId>, Option<ClientId>, Event)>,
) -> Result<()> {
let err_context = || "failed to update plugin state".to_string();
let plugins_to_update: Vec<(
PluginId,
ClientId,
Arc<Mutex<RunningPlugin>>,
Arc<Mutex<Subscriptions>>,
)> = self
.plugin_map
.lock()
.unwrap()
.running_plugins_and_subscriptions()
.iter()
.cloned()
.filter(|(plugin_id, _client_id, _running_plugin, _subscriptions)| {
!&self
.cached_events_for_pending_plugins
.contains_key(&plugin_id)
})
.collect();
for (pid, cid, event) in updates.drain(..) {
for (plugin_id, client_id, running_plugin, subscriptions) in &plugins_to_update {
let subs = subscriptions.lock().unwrap().clone();
// FIXME: This is very janky... Maybe I should write my own macro for Event -> EventType?
let event_type =
EventType::from_str(&event.to_string()).with_context(err_context)?;
if subs.contains(&event_type)
&& ((pid.is_none() && cid.is_none())
|| (pid.is_none() && cid == Some(*client_id))
|| (cid.is_none() && pid == Some(*plugin_id))
|| (cid == Some(*client_id) && pid == Some(*plugin_id)))
{
task::spawn({
let senders = self.senders.clone();
let running_plugin = running_plugin.clone();
let event = event.clone();
let plugin_id = *plugin_id;
let client_id = *client_id;
async move {
let running_plugin = running_plugin.lock().unwrap();
let mut plugin_bytes = vec![];
match apply_event_to_plugin(
plugin_id,
client_id,
&running_plugin.instance,
&running_plugin.plugin_env,
&event,
running_plugin.rows,
running_plugin.columns,
&mut plugin_bytes,
) {
Ok(()) => {
let _ = senders.send_to_screen(ScreenInstruction::PluginBytes(
plugin_bytes,
));
},
Err(e) => {
log::error!("{:?}", e);
// https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c
let stringified_error =
format!("{:?}", e).replace("\n", "\n\r");
handle_plugin_crash(
plugin_id,
stringified_error,
senders.clone(),
);
},
}
}
});
}
}
for (plugin_id, cached_events) in self.cached_events_for_pending_plugins.iter_mut() {
if pid.is_none() || pid.as_ref() == Some(plugin_id) {
cached_events.push(event.clone());
}
}
}
Ok(())
}
pub fn apply_cached_events(&mut self, plugin_ids: Vec<PluginId>) -> Result<()> {
let mut applied_plugin_paths = HashSet::new();
for plugin_id in plugin_ids {
self.apply_cached_events_and_resizes_for_plugin(plugin_id)?;
if let Some(run_plugin) = self.run_plugin_of_plugin_id(plugin_id) {
applied_plugin_paths.insert(run_plugin.clone());
}
self.loading_plugins
.retain(|(p_id, _run_plugin), _| p_id != &plugin_id);
}
for run_plugin in applied_plugin_paths.drain() {
if self.pending_plugin_reloads.remove(&run_plugin) {
let _ = self.reload_plugin(&run_plugin);
}
}
Ok(())
}
pub fn remove_client(&mut self, client_id: ClientId) {
self.connected_clients
.lock()
.unwrap()
.retain(|c| c != &client_id);
}
pub fn cleanup(&mut self) {
for (_plugin_id, loading_plugin_task) in self.loading_plugins.drain() {
drop(loading_plugin_task.cancel());
}
let plugin_ids = self.plugin_map.lock().unwrap().plugin_ids();
for plugin_id in &plugin_ids {
drop(self.unload_plugin(*plugin_id));
}
if let Some(watcher) = self.watcher.take() {
watcher.stop_nonblocking();
}
}
fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<&RunPlugin> {
self.loading_plugins
.iter()
.find(|((p_id, _run_plugin), _)| p_id == &plugin_id)
.map(|((_p_id, run_plugin), _)| run_plugin)
}
fn apply_cached_events_and_resizes_for_plugin(&mut self, plugin_id: PluginId) -> Result<()> {
let err_context = || format!("Failed to apply cached events to plugin");
if let Some(events) = self.cached_events_for_pending_plugins.remove(&plugin_id) {
let all_connected_clients: Vec<ClientId> = self
.connected_clients
.lock()
.unwrap()
.iter()
.copied()
.collect();
for client_id in &all_connected_clients {
if let Some((running_plugin, subscriptions)) = self
.plugin_map
.lock()
.unwrap()
.get_running_plugin_and_subscriptions(plugin_id, *client_id)
{
let subs = subscriptions.lock().unwrap().clone();
for event in events.clone() {
let event_type =
EventType::from_str(&event.to_string()).with_context(err_context)?;
if !subs.contains(&event_type) {
continue;
}
task::spawn({
let senders = self.senders.clone();
let running_plugin = running_plugin.clone();
let client_id = *client_id;
async move {
let running_plugin = running_plugin.lock().unwrap();
let mut plugin_bytes = vec![];
match apply_event_to_plugin(
plugin_id,
client_id,
&running_plugin.instance,
&running_plugin.plugin_env,
&event,
running_plugin.rows,
running_plugin.columns,
&mut plugin_bytes,
) {
Ok(()) => {
let _ = senders.send_to_screen(
ScreenInstruction::PluginBytes(plugin_bytes),
);
},
Err(e) => {
log::error!("{}", e);
},
}
}
});
}
}
}
}
if let Some((rows, columns)) = self.cached_resizes_for_pending_plugins.remove(&plugin_id) {
self.resize_plugin(plugin_id, columns, rows)?;
}
self.apply_cached_worker_messages(plugin_id)?;
Ok(())
}
pub fn apply_cached_worker_messages(&mut self, plugin_id: PluginId) -> Result<()> {
if let Some(mut messages) = self.cached_worker_messages.remove(&plugin_id) {
let mut worker_messages: HashMap<(ClientId, String), Vec<(String, String)>> =
HashMap::new();
for (client_id, worker_name, message, payload) in messages.drain(..) {
worker_messages
.entry((client_id, worker_name))
.or_default()
.push((message, payload));
}
for ((client_id, worker_name), messages) in worker_messages.drain() {
self.post_messages_to_plugin_worker(plugin_id, client_id, worker_name, messages)?;
}
}
Ok(())
}
fn plugin_is_currently_being_loaded(&self, plugin_location: &RunPluginLocation) -> bool {
self.loading_plugins
.iter()
.find(|((_plugin_id, run_plugin), _)| &run_plugin.location == plugin_location)
.is_some()
}
fn all_plugin_ids_for_plugin_location(
&self,
plugin_location: &RunPluginLocation,
) -> Result<Vec<PluginId>> {
self.plugin_map
.lock()
.unwrap()
.all_plugin_ids_for_plugin_location(plugin_location)
}
fn size_of_plugin_id(&self, plugin_id: PluginId) -> Option<(usize, usize)> {
// (rows/colums)
self.plugin_map
.lock()
.unwrap()
.get_running_plugin(plugin_id, None)
.map(|r| {
let r = r.lock().unwrap();
(r.rows, r.columns)
})
}
fn start_plugin_loading_indication(
&self,
plugin_ids: &[PluginId],
loading_indication: &LoadingIndication,
) {
for plugin_id in plugin_ids {
let _ = self
.senders
.send_to_screen(ScreenInstruction::StartPluginLoadingIndication(
*plugin_id,
loading_indication.clone(),
));
let _ = self
.senders
.send_to_background_jobs(BackgroundJob::AnimatePluginLoading(*plugin_id));
}
}
pub fn post_messages_to_plugin_worker(
&mut self,
plugin_id: PluginId,
client_id: ClientId,
worker_name: String,
mut messages: Vec<(String, String)>,
) -> Result<()> {
let worker =
self.plugin_map
.lock()
.unwrap()
.worker_sender(plugin_id, client_id, &worker_name);
match worker {
Some(worker) => {
for (message, payload) in messages.drain(..) {
if let Err(e) = worker.try_send(MessageToWorker::Message(message, payload)) {
log::error!("Failed to send message to worker: {:?}", e);
}
}
},
None => {
log::warn!("Worker {worker_name} not found, caching messages");
for (message, payload) in messages.drain(..) {
self.cached_worker_messages
.entry(plugin_id)
.or_default()
.push((client_id, worker_name.clone(), message, payload));
}
},
}
Ok(())
}
pub fn start_fs_watcher_if_not_started(&mut self) {
if self.watcher.is_none() {
self.watcher = match watch_filesystem(self.senders.clone(), &self.zellij_cwd) {
Ok(watcher) => Some(watcher),
Err(e) => {
log::error!("Failed to watch filesystem: {:?}", e);
None
},
};
}
}
pub fn cache_plugin_permissions(
&mut self,
plugin_id: PluginId,
client_id: Option<ClientId>,
permissions: Vec<PermissionType>,
status: PermissionStatus,
cache_path: Option<PathBuf>,
) -> Result<()> {
if let Some(running_plugin) = self
.plugin_map
.lock()
.unwrap()
.get_running_plugin(plugin_id, client_id)
{
let err_context = || format!("Failed to write plugin permission {plugin_id}");
let mut running_plugin = running_plugin.lock().unwrap();
let permissions = if status == PermissionStatus::Granted {
permissions
} else {
vec![]
};
running_plugin
.plugin_env
.set_permissions(HashSet::from_iter(permissions.clone()));
let mut permission_cache = PermissionCache::from_path_or_default(cache_path);
permission_cache.cache(
running_plugin.plugin_env.plugin.location.to_string(),
permissions,
);
permission_cache.write_to_file().with_context(err_context)?;
}
Ok(())
}
}
fn handle_plugin_successful_loading(senders: &ThreadSenders, plugin_id: PluginId) {
let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id));
let _ = senders.send_to_screen(ScreenInstruction::RequestStateUpdateForPlugins);
}
fn handle_plugin_loading_failure(
senders: &ThreadSenders,
plugin_id: PluginId,
loading_indication: &mut LoadingIndication,
error: impl std::fmt::Debug,
) {
log::error!("{:?}", error);
let _ = senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(plugin_id));
loading_indication.indicate_loading_error(format!("{:?}", error));
let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
plugin_id,
loading_indication.clone(),
));
}
// TODO: move to permissions?
fn check_event_permission(
plugin_env: &PluginEnv,
event: &Event,
) -> (PermissionStatus, Option<PermissionType>) {
if plugin_env.plugin.is_builtin() {
// built-in plugins can do all the things because they're part of the application and
// there's no use to deny them anything
return (PermissionStatus::Granted, None);
}
let permission = match event {
Event::ModeUpdate(..)
| Event::TabUpdate(..)
| Event::PaneUpdate(..)
| Event::CopyToClipboard(..)
| Event::SystemClipboardFailure
| Event::InputReceived => PermissionType::ReadApplicationState,
_ => return (PermissionStatus::Granted, None),
};
if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
if permissions.contains(&permission) {
return (PermissionStatus::Granted, None);
}
}
(PermissionStatus::Denied, Some(permission))
}
pub fn apply_event_to_plugin(
plugin_id: PluginId,
client_id: ClientId,
instance: &Instance,
plugin_env: &PluginEnv,
event: &Event,
rows: usize,
columns: usize,
plugin_bytes: &mut Vec<(PluginId, ClientId, Vec<u8>)>,
) -> Result<()> {
let err_context = || format!("Failed to apply event to plugin {plugin_id}");
match check_event_permission(plugin_env, event) {
(PermissionStatus::Granted, _) => {
let protobuf_event: ProtobufEvent = event
.clone()
.try_into()
.map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
let update = instance
.exports
.get_function("update")
.with_context(err_context)?;
wasi_write_object(&plugin_env.wasi_env, &protobuf_event.encode_to_vec())
.with_context(err_context)?;
let update_return = update.call(&[]).with_context(err_context)?;
let should_render = match update_return.get(0) {
Some(Value::I32(n)) => *n == 1,
_ => false,
};
if rows > 0 && columns > 0 && should_render {
let rendered_bytes = instance
.exports
.get_function("render")
.map_err(anyError::new)
.and_then(|render| {
render
.call(&[Value::I32(rows as i32), Value::I32(columns as i32)])
.map_err(anyError::new)
})
.and_then(|_| wasi_read_string(&plugin_env.wasi_env))
.with_context(err_context)?;
plugin_bytes.push((plugin_id, client_id, rendered_bytes.as_bytes().to_vec()));
}
},
(PermissionStatus::Denied, permission) => {
log::error!(
"PluginId '{}' permission '{}' is not allowed - Event '{:?}' denied",
plugin_id,
permission
.map(|p| p.to_string())
.unwrap_or("UNKNOWN".to_owned()),
EventType::from_str(&event.to_string()).with_context(err_context)?
);
},
}
Ok(())
}
pub fn handle_plugin_crash(plugin_id: PluginId, message: String, senders: ThreadSenders) {
let mut loading_indication = LoadingIndication::new("Panic!".to_owned());
loading_indication.indicate_loading_error(message);
let _ = senders.send_to_screen(ScreenInstruction::UpdatePluginLoadingStage(
plugin_id,
loading_indication,
));
let _ = senders.send_to_plugin(PluginInstruction::Unload(plugin_id));
}