diff options
-rw-r--r-- | default-plugins/fixture-plugin-for-tests/src/main.rs | 17 | ||||
-rw-r--r-- | default-plugins/session-manager/src/main.rs | 1 | ||||
-rw-r--r-- | zellij-server/src/background_jobs.rs | 61 | ||||
-rw-r--r-- | zellij-server/src/plugins/unit/plugin_tests.rs | 234 | ||||
-rw-r--r-- | zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_plugin_command.snap | 20 | ||||
-rw-r--r-- | zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_with_env_vars_and_cwd_plugin_command.snap | 22 | ||||
-rw-r--r-- | zellij-server/src/plugins/zellij_exports.rs | 38 | ||||
-rw-r--r-- | zellij-tile/src/shim.rs | 40 | ||||
-rw-r--r-- | zellij-utils/assets/prost/api.event.rs | 30 | ||||
-rw-r--r-- | zellij-utils/assets/prost/api.plugin_command.rs | 35 | ||||
-rw-r--r-- | zellij-utils/src/data.rs | 13 | ||||
-rw-r--r-- | zellij-utils/src/errors.rs | 1 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/event.proto | 14 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/event.rs | 34 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/plugin_command.proto | 19 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/plugin_command.rs | 50 |
16 files changed, 620 insertions, 9 deletions
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs index 811a4507e..cdbdc9f2a 100644 --- a/default-plugins/fixture-plugin-for-tests/src/main.rs +++ b/default-plugins/fixture-plugin-for-tests/src/main.rs @@ -240,6 +240,23 @@ impl ZellijPlugin for State { Key::Ctrl('1') => { request_permission(&[PermissionType::ReadApplicationState]); }, + Key::Ctrl('2') => { + let mut context = BTreeMap::new(); + context.insert("user_key_1".to_owned(), "user_value_1".to_owned()); + run_command(&["ls", "-l"], context); + }, + Key::Ctrl('3') => { + let mut context = BTreeMap::new(); + context.insert("user_key_2".to_owned(), "user_value_2".to_owned()); + let mut env_vars = BTreeMap::new(); + env_vars.insert("VAR1".to_owned(), "some_value".to_owned()); + run_command_with_env_variables_and_cwd( + &["ls", "-l"], + env_vars, + std::path::PathBuf::from("/some/custom/folder"), + context, + ); + }, _ => {}, }, Event::CustomMessage(message, payload) => { diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs index b65986319..bd0b1f2b7 100644 --- a/default-plugins/session-manager/src/main.rs +++ b/default-plugins/session-manager/src/main.rs @@ -28,6 +28,7 @@ impl ZellijPlugin for State { EventType::ModeUpdate, EventType::SessionUpdate, EventType::Key, + EventType::RunCommandResult, ]); } diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs index 375ba4ba3..57e13364c 100644 --- a/zellij-server/src/background_jobs.rs +++ b/zellij-server/src/background_jobs.rs @@ -3,13 +3,14 @@ use zellij_utils::consts::{ session_info_cache_file_name, session_info_folder_for_session, session_layout_cache_file_name, ZELLIJ_SOCK_DIR, }; -use zellij_utils::data::SessionInfo; +use zellij_utils::data::{Event, SessionInfo}; use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; use std::collections::{BTreeMap, HashMap}; use std::fs; use std::io::Write; use std::os::unix::fs::FileTypeExt; +use std::path::PathBuf; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, Mutex, @@ -17,8 +18,10 @@ use std::sync::{ use std::time::{Duration, Instant}; use crate::panes::PaneId; +use crate::plugins::{PluginId, PluginInstruction}; use crate::screen::ScreenInstruction; use crate::thread_bus::Bus; +use crate::ClientId; #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub enum BackgroundJob { @@ -28,6 +31,15 @@ pub enum BackgroundJob { ReadAllSessionInfosOnMachine, // u32 - plugin_id ReportSessionInfo(String, SessionInfo), // String - session name ReportLayoutInfo((String, BTreeMap<String, String>)), // HashMap<file_name, pane_contents> + RunCommand( + PluginId, + ClientId, + String, + Vec<String>, + BTreeMap<String, String>, + PathBuf, + BTreeMap<String, String>, + ), // command, args, env_variables, cwd, context Exit, } @@ -44,6 +56,7 @@ impl From<&BackgroundJob> for BackgroundJobContext { }, BackgroundJob::ReportSessionInfo(..) => BackgroundJobContext::ReportSessionInfo, BackgroundJob::ReportLayoutInfo(..) => BackgroundJobContext::ReportLayoutInfo, + BackgroundJob::RunCommand(..) => BackgroundJobContext::RunCommand, BackgroundJob::Exit => BackgroundJobContext::Exit, } } @@ -226,6 +239,52 @@ pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> { } }); }, + BackgroundJob::RunCommand( + plugin_id, + client_id, + command, + args, + env_variables, + cwd, + context, + ) => { + // when async_std::process stabilizes, we should change this to be async + std::thread::spawn({ + let senders = bus.senders.clone(); + move || { + let output = std::process::Command::new(&command) + .args(&args) + .envs(env_variables) + .current_dir(cwd) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .output(); + match output { + Ok(output) => { + let stdout = output.stdout.to_vec(); + let stderr = output.stderr.to_vec(); + let exit_code = output.status.code(); + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + Some(plugin_id), + Some(client_id), + Event::RunCommandResult(exit_code, stdout, stderr, context), + )])); + }, + Err(e) => { + log::error!("Failed to run command: {}", e); + let stdout = vec![]; + let stderr = format!("{}", e).as_bytes().to_vec(); + let exit_code = Some(2); + let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( + Some(plugin_id), + Some(client_id), + Event::RunCommandResult(exit_code, stdout, stderr, context), + )])); + }, + } + } + }); + }, BackgroundJob::Exit => { for loading_plugin in loading_plugins.values() { loading_plugin.store(false, Ordering::SeqCst); diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs index 652623ccc..e866987d7 100644 --- a/zellij-server/src/plugins/unit/plugin_tests.rs +++ b/zellij-server/src/plugins/unit/plugin_tests.rs @@ -473,6 +473,90 @@ fn create_plugin_thread_with_pty_receiver( (to_plugin, pty_receiver, screen_receiver, Box::new(teardown)) } +fn create_plugin_thread_with_background_jobs_receiver( + zellij_cwd: Option<PathBuf>, +) -> ( + SenderWithContext<PluginInstruction>, + Receiver<(BackgroundJob, ErrorContext)>, + Receiver<(ScreenInstruction, ErrorContext)>, + Box<dyn FnOnce()>, +) { + let zellij_cwd = zellij_cwd.unwrap_or_else(|| PathBuf::from(".")); + let (to_server, _server_receiver): ChannelWithContext<ServerInstruction> = + channels::bounded(50); + let to_server = SenderWithContext::new(to_server); + + let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channels::unbounded(); + let to_screen = SenderWithContext::new(to_screen); + + let (to_plugin, plugin_receiver): ChannelWithContext<PluginInstruction> = channels::unbounded(); + let to_plugin = SenderWithContext::new(to_plugin); + let (to_pty, _pty_receiver): ChannelWithContext<PtyInstruction> = channels::unbounded(); + let to_pty = SenderWithContext::new(to_pty); + + let (to_pty_writer, _pty_writer_receiver): ChannelWithContext<PtyWriteInstruction> = + channels::unbounded(); + let to_pty_writer = SenderWithContext::new(to_pty_writer); + + let (to_background_jobs, background_jobs_receiver): ChannelWithContext<BackgroundJob> = + channels::unbounded(); + let to_background_jobs = SenderWithContext::new(to_background_jobs); + + let plugin_bus = Bus::new( + vec![plugin_receiver], + Some(&to_screen), + Some(&to_pty), + Some(&to_plugin), + Some(&to_server), + Some(&to_pty_writer), + Some(&to_background_jobs), + None, + ) + .should_silently_fail(); + let store = Store::new(wasmer::Singlepass::default()); + let data_dir = PathBuf::from(tempdir().unwrap().path()); + let default_shell = PathBuf::from("."); + let plugin_capabilities = PluginCapabilities::default(); + let client_attributes = ClientAttributes::default(); + let default_shell_action = None; // TODO: change me + let plugin_thread = std::thread::Builder::new() + .name("plugin_thread".to_string()) + .spawn(move || { + set_var("ZELLIJ_SESSION_NAME", "zellij-test"); + plugin_thread_main( + plugin_bus, + store, + data_dir, + PluginsConfig::default(), + Box::new(Layout::default()), + default_shell, + zellij_cwd, + plugin_capabilities, + client_attributes, + default_shell_action, + ) + .expect("TEST") + }) + .unwrap(); + let teardown = { + let to_plugin = to_plugin.clone(); + move || { + let _ = to_pty.send(PtyInstruction::Exit); + let _ = to_pty_writer.send(PtyWriteInstruction::Exit); + let _ = to_screen.send(ScreenInstruction::Exit); + let _ = to_server.send(ServerInstruction::KillSession); + let _ = to_plugin.send(PluginInstruction::Exit); + let _ = plugin_thread.join(); + } + }; + ( + to_plugin, + background_jobs_receiver, + screen_receiver, + Box::new(teardown), + ) +} + lazy_static! { static ref PLUGIN_FIXTURE: String = format!( // to populate this file, make sure to run the build-e2e CI job @@ -5184,3 +5268,153 @@ pub fn denied_permission_request_result() { assert_snapshot!(format!("{:#?}", permissions)); } + +#[test] +#[ignore] +pub fn run_command_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, background_jobs_receiver, screen_receiver, teardown) = + create_plugin_thread_with_background_jobs_receiver(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_background_jobs_instructions = Arc::new(Mutex::new(vec![])); + let background_jobs_thread = log_actions_in_thread!( + received_background_jobs_instructions, + BackgroundJob::RunCommand, + background_jobs_receiver, + 1 + ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + tab_index, + None, + client_id, + size, + )); + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('2')), // this triggers the enent in the fixture plugin + )])); + background_jobs_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_background_jobs_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let BackgroundJob::RunCommand(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} + +#[test] +#[ignore] +pub fn run_command_with_env_vars_and_cwd_plugin_command() { + let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its + // destructor removes the directory + let plugin_host_folder = PathBuf::from(temp_folder.path()); + let cache_path = plugin_host_folder.join("permissions_test.kdl"); + let (plugin_thread_sender, background_jobs_receiver, screen_receiver, teardown) = + create_plugin_thread_with_background_jobs_receiver(Some(plugin_host_folder)); + let plugin_should_float = Some(false); + let plugin_title = Some("test_plugin".to_owned()); + let run_plugin = RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)), + configuration: Default::default(), + }; + let tab_index = 1; + let client_id = 1; + let size = Size { + cols: 121, + rows: 20, + }; + let received_background_jobs_instructions = Arc::new(Mutex::new(vec![])); + let background_jobs_thread = log_actions_in_thread!( + received_background_jobs_instructions, + BackgroundJob::RunCommand, + background_jobs_receiver, + 1 + ); + let received_screen_instructions = Arc::new(Mutex::new(vec![])); + let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!( + received_screen_instructions, + ScreenInstruction::Exit, + screen_receiver, + 1, + &PermissionType::ChangeApplicationState, + cache_path, + plugin_thread_sender, + client_id + ); + + let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id)); + let _ = plugin_thread_sender.send(PluginInstruction::Load( + plugin_should_float, + false, + plugin_title, + run_plugin, + tab_index, + None, + client_id, + size, + )); + std::thread::sleep(std::time::Duration::from_millis(500)); + let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( + None, + Some(client_id), + Event::Key(Key::Ctrl('3')), // this triggers the enent in the fixture plugin + )])); + background_jobs_thread.join().unwrap(); // this might take a while if the cache is cold + teardown(); + let new_tab_event = received_background_jobs_instructions + .lock() + .unwrap() + .iter() + .find_map(|i| { + if let BackgroundJob::RunCommand(..) = i { + Some(i.clone()) + } else { + None + } + }) + .clone(); + assert_snapshot!(format!("{:#?}", new_tab_event)); +} diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_plugin_command.snap new file mode 100644 index 000000000..a98f85681 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_plugin_command.snap @@ -0,0 +1,20 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 5339 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + RunCommand( + 0, + 1, + "ls", + [ + "-l", + ], + {}, + ".", + { + "user_key_1": "user_value_1", + }, + ), +) diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_with_env_vars_and_cwd_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_with_env_vars_and_cwd_plugin_command.snap new file mode 100644 index 000000000..5c5d03769 --- /dev/null +++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_with_env_vars_and_cwd_plugin_command.snap @@ -0,0 +1,22 @@ +--- +source: zellij-server/src/plugins/./unit/plugin_tests.rs +assertion_line: 5414 +expression: "format!(\"{:#?}\", new_tab_event)" +--- +Some( + RunCommand( + 0, + 1, + "ls", + [ + "-l", + ], + { + "VAR1": "some_value", + }, + "/some/custom/folder", + { + "user_key_2": "user_value_2", + }, + ), +) diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs index 1608df8fb..496c7d64a 100644 --- a/zellij-server/src/plugins/zellij_exports.rs +++ b/zellij-server/src/plugins/zellij_exports.rs @@ -1,4 +1,5 @@ use super::PluginInstruction; +use crate::background_jobs::BackgroundJob; use crate::plugins::plugin_map::{PluginEnv, Subscriptions}; use crate::plugins::wasm_bridge::handle_plugin_crash; use crate::route::route_action; @@ -126,6 +127,9 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) { PluginCommand::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index), PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds), PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line), + PluginCommand::RunCommand(command_line, env_variables, cwd, context) => { + run_command(env, command_line, env_variables, cwd, context) + }, PluginCommand::PostMessageTo(plugin_message) => { post_message_to(env, plugin_message)? }, @@ -572,6 +576,7 @@ fn set_timeout(env: &ForeignFunctionEnv, secs: f64) { } fn exec_cmd(env: &ForeignFunctionEnv, mut command_line: Vec<String>) { + log::warn!("The ExecCmd plugin command is deprecated and will be removed in a future version. Please use RunCmd instead (it has all the things and can even show you STDOUT/STDERR and an exit code!)"); let err_context = || { format!( "failed to execute command on host for plugin '{}'", @@ -595,6 +600,38 @@ fn exec_cmd(env: &ForeignFunctionEnv, mut command_line: Vec<String>) { .non_fatal(); } +fn run_command( + env: &ForeignFunctionEnv, + mut command_line: Vec<String>, + env_variables: BTreeMap<String, String>, + cwd: PathBuf, + context: BTreeMap<String, String>, +) { + let err_context = || { + format!( + "failed to execute command on host for plugin '{}'", + env.plugin_env.name() + ) + }; + if command_line.is_empty() { + log::error!("Command cannot be empty"); + } else { + let command = command_line.remove(0); + let _ = env + .plugin_env + .senders + .send_to_background_jobs(BackgroundJob::RunCommand( + env.plugin_env.plugin_id, + env.plugin_env.client_id, + command, + command_line, + env_variables, + cwd, + context, + )); + } +} + fn post_message_to(env: &ForeignFunctionEnv, plugin_message: PluginMessage) -> Result<()> { let worker_name = plugin_message .worker_name @@ -1159,6 +1196,7 @@ fn check_command_permission( PluginCommand::OpenCommandPane(..) | PluginCommand::OpenCommandPaneFloating(..) | PluginCommand::OpenCommandPaneInPlace(..) + | PluginCommand::RunCommand(..) | PluginCommand::ExecCmd(..) => PermissionType::RunCommands, PluginCommand::Write(..) | PluginCommand::WriteChars(..) => PermissionType::WriteToStdin, PluginCommand::SwitchTabTo(..) diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index 09bb7f597..97065a2a3 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -1,6 +1,9 @@ use serde::{de::DeserializeOwned, Serialize}; -use std::collections::HashSet; -use std::{io, path::Path}; +use std::collections::{BTreeMap, HashSet}; +use std::{ + io, + path::{Path, PathBuf}, +}; use zellij_utils::data::*; use zellij_utils::errors::prelude::*; pub use zellij_utils::plugin_api; @@ -171,6 +174,39 @@ pub fn exec_cmd(cmd: &[&str]) { unsafe { host_run_plugin_command() }; } +/// Run this command in the background on the host machine, optionally being notified of its output +/// if subscribed to the `RunCommandResult` Event +pub fn run_command(cmd: &[&str], context: BTreeMap<String, String>) { + let plugin_command = PluginCommand::RunCommand( + cmd.iter().cloned().map(|s| s.to_owned()).collect(), + BTreeMap::new(), + PathBuf::from("."), + context, + ); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + +/// Run this command in the background on the host machine, providing environment variables and a +/// cwd. Optionally being notified of its output if subscribed to the `RunCommandResult` Event +pub fn run_command_with_env_variables_and_cwd( + cmd: &[&str], + env_variables: BTreeMap<String, String>, + cwd: PathBuf, + context: BTreeMap<String, String>, +) { + let plugin_command = PluginCommand::RunCommand( + cmd.iter().cloned().map(|s| s.to_owned()).collect(), + env_variables, + cwd, + context, + ); + let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); + object_to_stdout(&protobuf_plugin_command.encode_to_vec()); + unsafe { host_run_plugin_command() }; +} + /// Hide the plugin pane (suppress it) from the UI pub fn hide_self() { let plugin_command = PluginCommand::HideSelf; diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs index 39bc97705..9b0097a5d 100644< |