Skip to content

Commit

Permalink
feat(plugins): plugin run_command api (zellij-org#2862)
Browse files Browse the repository at this point in the history
* prototype

* add tests

* style(fmt): rustfmt

* update comments

* deprecation warning for execcmd
  • Loading branch information
imsnif authored Oct 16, 2023
1 parent 36237f0 commit d2b6fb5
Show file tree
Hide file tree
Showing 16 changed files with 620 additions and 9 deletions.
17 changes: 17 additions & 0 deletions default-plugins/fixture-plugin-for-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions default-plugins/session-manager/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl ZellijPlugin for State {
EventType::ModeUpdate,
EventType::SessionUpdate,
EventType::Key,
EventType::RunCommandResult,
]);
}

Expand Down
61 changes: 60 additions & 1 deletion zellij-server/src/background_jobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,25 @@ 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,
};
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 {
Expand All @@ -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,
}

Expand All @@ -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,
}
}
Expand Down Expand Up @@ -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);
Expand Down
234 changes: 234 additions & 0 deletions zellij-server/src/plugins/unit/plugin_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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));
}
Loading

0 comments on commit d2b6fb5

Please sign in to comment.