Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugins): plugin run_command api #2862

Merged
merged 5 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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