summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--default-plugins/fixture-plugin-for-tests/src/main.rs17
-rw-r--r--default-plugins/session-manager/src/main.rs1
-rw-r--r--zellij-server/src/background_jobs.rs61
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs234
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_plugin_command.snap20
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__run_command_with_env_vars_and_cwd_plugin_command.snap22
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs38
-rw-r--r--zellij-tile/src/shim.rs40
-rw-r--r--zellij-utils/assets/prost/api.event.rs30
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs35
-rw-r--r--zellij-utils/src/data.rs13
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/plugin_api/event.proto14
-rw-r--r--zellij-utils/src/plugin_api/event.rs34
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto19
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs50
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<