feat(plugins): plugin run_command api (#2862)

* prototype

* add tests

* style(fmt): rustfmt

* update comments

* deprecation warning for execcmd
This commit is contained in:
Aram Drevekenin 2023-10-16 13:42:19 +02:00 committed by GitHub
parent 36237f0414
commit d2b6fb5c5b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 620 additions and 9 deletions

View file

@ -240,6 +240,23 @@ impl ZellijPlugin for State {
Key::Ctrl('1') => { Key::Ctrl('1') => {
request_permission(&[PermissionType::ReadApplicationState]); 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) => { Event::CustomMessage(message, payload) => {

View file

@ -28,6 +28,7 @@ impl ZellijPlugin for State {
EventType::ModeUpdate, EventType::ModeUpdate,
EventType::SessionUpdate, EventType::SessionUpdate,
EventType::Key, EventType::Key,
EventType::RunCommandResult,
]); ]);
} }

View file

@ -3,13 +3,14 @@ use zellij_utils::consts::{
session_info_cache_file_name, session_info_folder_for_session, session_layout_cache_file_name, session_info_cache_file_name, session_info_folder_for_session, session_layout_cache_file_name,
ZELLIJ_SOCK_DIR, ZELLIJ_SOCK_DIR,
}; };
use zellij_utils::data::SessionInfo; use zellij_utils::data::{Event, SessionInfo};
use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType};
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::fs; use std::fs;
use std::io::Write; use std::io::Write;
use std::os::unix::fs::FileTypeExt; use std::os::unix::fs::FileTypeExt;
use std::path::PathBuf;
use std::sync::{ use std::sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Mutex, Arc, Mutex,
@ -17,8 +18,10 @@ use std::sync::{
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use crate::panes::PaneId; use crate::panes::PaneId;
use crate::plugins::{PluginId, PluginInstruction};
use crate::screen::ScreenInstruction; use crate::screen::ScreenInstruction;
use crate::thread_bus::Bus; use crate::thread_bus::Bus;
use crate::ClientId;
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum BackgroundJob { pub enum BackgroundJob {
@ -28,6 +31,15 @@ pub enum BackgroundJob {
ReadAllSessionInfosOnMachine, // u32 - plugin_id ReadAllSessionInfosOnMachine, // u32 - plugin_id
ReportSessionInfo(String, SessionInfo), // String - session name ReportSessionInfo(String, SessionInfo), // String - session name
ReportLayoutInfo((String, BTreeMap<String, String>)), // HashMap<file_name, pane_contents> 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, Exit,
} }
@ -44,6 +56,7 @@ impl From<&BackgroundJob> for BackgroundJobContext {
}, },
BackgroundJob::ReportSessionInfo(..) => BackgroundJobContext::ReportSessionInfo, BackgroundJob::ReportSessionInfo(..) => BackgroundJobContext::ReportSessionInfo,
BackgroundJob::ReportLayoutInfo(..) => BackgroundJobContext::ReportLayoutInfo, BackgroundJob::ReportLayoutInfo(..) => BackgroundJobContext::ReportLayoutInfo,
BackgroundJob::RunCommand(..) => BackgroundJobContext::RunCommand,
BackgroundJob::Exit => BackgroundJobContext::Exit, 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 => { BackgroundJob::Exit => {
for loading_plugin in loading_plugins.values() { for loading_plugin in loading_plugins.values() {
loading_plugin.store(false, Ordering::SeqCst); loading_plugin.store(false, Ordering::SeqCst);

View file

@ -473,6 +473,90 @@ fn create_plugin_thread_with_pty_receiver(
(to_plugin, pty_receiver, screen_receiver, Box::new(teardown)) (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! { lazy_static! {
static ref PLUGIN_FIXTURE: String = format!( static ref PLUGIN_FIXTURE: String = format!(
// to populate this file, make sure to run the build-e2e CI job // 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)); 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));
}

View file

@ -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",
},
),
)

View file

@ -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",
},
),
)

View file

@ -1,4 +1,5 @@
use super::PluginInstruction; use super::PluginInstruction;
use crate::background_jobs::BackgroundJob;
use crate::plugins::plugin_map::{PluginEnv, Subscriptions}; use crate::plugins::plugin_map::{PluginEnv, Subscriptions};
use crate::plugins::wasm_bridge::handle_plugin_crash; use crate::plugins::wasm_bridge::handle_plugin_crash;
use crate::route::route_action; 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::SwitchTabTo(tab_index) => switch_tab_to(env, tab_index),
PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds), PluginCommand::SetTimeout(seconds) => set_timeout(env, seconds),
PluginCommand::ExecCmd(command_line) => exec_cmd(env, command_line), 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) => { PluginCommand::PostMessageTo(plugin_message) => {
post_message_to(env, 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>) { 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 = || { let err_context = || {
format!( format!(
"failed to execute command on host for plugin '{}'", "failed to execute command on host for plugin '{}'",
@ -595,6 +600,38 @@ fn exec_cmd(env: &ForeignFunctionEnv, mut command_line: Vec<String>) {
.non_fatal(); .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<()> { fn post_message_to(env: &ForeignFunctionEnv, plugin_message: PluginMessage) -> Result<()> {
let worker_name = plugin_message let worker_name = plugin_message
.worker_name .worker_name
@ -1159,6 +1196,7 @@ fn check_command_permission(
PluginCommand::OpenCommandPane(..) PluginCommand::OpenCommandPane(..)
| PluginCommand::OpenCommandPaneFloating(..) | PluginCommand::OpenCommandPaneFloating(..)
| PluginCommand::OpenCommandPaneInPlace(..) | PluginCommand::OpenCommandPaneInPlace(..)
| PluginCommand::RunCommand(..)
| PluginCommand::ExecCmd(..) => PermissionType::RunCommands, | PluginCommand::ExecCmd(..) => PermissionType::RunCommands,
PluginCommand::Write(..) | PluginCommand::WriteChars(..) => PermissionType::WriteToStdin, PluginCommand::Write(..) | PluginCommand::WriteChars(..) => PermissionType::WriteToStdin,
PluginCommand::SwitchTabTo(..) PluginCommand::SwitchTabTo(..)

View file

@ -1,6 +1,9 @@
use serde::{de::DeserializeOwned, Serialize}; use serde::{de::DeserializeOwned, Serialize};
use std::collections::HashSet; use std::collections::{BTreeMap, HashSet};
use std::{io, path::Path}; use std::{
io,
path::{Path, PathBuf},
};
use zellij_utils::data::*; use zellij_utils::data::*;
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
pub use zellij_utils::plugin_api; pub use zellij_utils::plugin_api;
@ -171,6 +174,39 @@ pub fn exec_cmd(cmd: &[&str]) {
unsafe { host_run_plugin_command() }; 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 /// Hide the plugin pane (suppress it) from the UI
pub fn hide_self() { pub fn hide_self() {
let plugin_command = PluginCommand::HideSelf; let plugin_command = PluginCommand::HideSelf;

View file

@ -9,7 +9,10 @@ pub struct EventNameList {
pub struct Event { pub struct Event {
#[prost(enumeration = "EventType", tag = "1")] #[prost(enumeration = "EventType", tag = "1")]
pub name: i32, pub name: i32,
#[prost(oneof = "event::Payload", tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13")] #[prost(
oneof = "event::Payload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14"
)]
pub payload: ::core::option::Option<event::Payload>, pub payload: ::core::option::Option<event::Payload>,
} }
/// Nested message and enum types in `Event`. /// Nested message and enum types in `Event`.
@ -41,6 +44,8 @@ pub mod event {
PermissionRequestResultPayload(super::PermissionRequestResultPayload), PermissionRequestResultPayload(super::PermissionRequestResultPayload),
#[prost(message, tag = "13")] #[prost(message, tag = "13")]
SessionUpdatePayload(super::SessionUpdatePayload), SessionUpdatePayload(super::SessionUpdatePayload),
#[prost(message, tag = "14")]
RunCommandResultPayload(super::RunCommandResultPayload),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
@ -51,6 +56,26 @@ pub struct SessionUpdatePayload {
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct RunCommandResultPayload {
#[prost(int32, optional, tag = "1")]
pub exit_code: ::core::option::Option<i32>,
#[prost(bytes = "vec", tag = "2")]
pub stdout: ::prost::alloc::vec::Vec<u8>,
#[prost(bytes = "vec", tag = "3")]
pub stderr: ::prost::alloc::vec::Vec<u8>,
#[prost(message, repeated, tag = "4")]
pub context: ::prost::alloc::vec::Vec<ContextItem>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ContextItem {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub value: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PermissionRequestResultPayload { pub struct PermissionRequestResultPayload {
#[prost(bool, tag = "1")] #[prost(bool, tag = "1")]
pub granted: bool, pub granted: bool,
@ -261,6 +286,7 @@ pub enum EventType {
FileSystemDelete = 14, FileSystemDelete = 14,
PermissionRequestResult = 15, PermissionRequestResult = 15,
SessionUpdate = 16, SessionUpdate = 16,
RunCommandResult = 17,
} }
impl EventType { impl EventType {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -286,6 +312,7 @@ impl EventType {
EventType::FileSystemDelete => "FileSystemDelete", EventType::FileSystemDelete => "FileSystemDelete",
EventType::PermissionRequestResult => "PermissionRequestResult", EventType::PermissionRequestResult => "PermissionRequestResult",
EventType::SessionUpdate => "SessionUpdate", EventType::SessionUpdate => "SessionUpdate",
EventType::RunCommandResult => "RunCommandResult",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -308,6 +335,7 @@ impl EventType {
"FileSystemDelete" => Some(Self::FileSystemDelete), "FileSystemDelete" => Some(Self::FileSystemDelete),
"PermissionRequestResult" => Some(Self::PermissionRequestResult), "PermissionRequestResult" => Some(Self::PermissionRequestResult),
"SessionUpdate" => Some(Self::SessionUpdate), "SessionUpdate" => Some(Self::SessionUpdate),
"RunCommandResult" => Some(Self::RunCommandResult),
_ => None, _ => None,
} }
} }

View file

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32, pub name: i32,
#[prost( #[prost(
oneof = "plugin_command::Payload", oneof = "plugin_command::Payload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42" tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43"
)] )]
pub payload: ::core::option::Option<plugin_command::Payload>, pub payload: ::core::option::Option<plugin_command::Payload>,
} }
@ -96,6 +96,8 @@ pub mod plugin_command {
OpenTerminalInPlacePayload(super::OpenFilePayload), OpenTerminalInPlacePayload(super::OpenFilePayload),
#[prost(message, tag = "42")] #[prost(message, tag = "42")]
OpenCommandPaneInPlacePayload(super::OpenCommandPanePayload), OpenCommandPaneInPlacePayload(super::OpenCommandPanePayload),
#[prost(message, tag = "43")]
RunCommandPayload(super::RunCommandPayload),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
@ -164,6 +166,34 @@ pub struct ExecCmdPayload {
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct RunCommandPayload {
#[prost(string, repeated, tag = "1")]
pub command_line: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
#[prost(message, repeated, tag = "2")]
pub env_variables: ::prost::alloc::vec::Vec<EnvVariable>,
#[prost(string, tag = "3")]
pub cwd: ::prost::alloc::string::String,
#[prost(message, repeated, tag = "4")]
pub context: ::prost::alloc::vec::Vec<ContextItem>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct EnvVariable {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub value: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ContextItem {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub value: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct PluginMessagePayload { pub struct PluginMessagePayload {
#[prost(message, optional, tag = "1")] #[prost(message, optional, tag = "1")]
pub message: ::core::option::Option<super::message::Message>, pub message: ::core::option::Option<super::message::Message>,
@ -263,6 +293,7 @@ pub enum CommandName {
OpenTerminalInPlace = 68, OpenTerminalInPlace = 68,
OpenCommandInPlace = 69, OpenCommandInPlace = 69,
OpenFileInPlace = 70, OpenFileInPlace = 70,
RunCommand = 71,
} }
impl CommandName { impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -342,6 +373,7 @@ impl CommandName {
CommandName::OpenTerminalInPlace => "OpenTerminalInPlace", CommandName::OpenTerminalInPlace => "OpenTerminalInPlace",
CommandName::OpenCommandInPlace => "OpenCommandInPlace", CommandName::OpenCommandInPlace => "OpenCommandInPlace",
CommandName::OpenFileInPlace => "OpenFileInPlace", CommandName::OpenFileInPlace => "OpenFileInPlace",
CommandName::RunCommand => "RunCommand",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -418,6 +450,7 @@ impl CommandName {
"OpenTerminalInPlace" => Some(Self::OpenTerminalInPlace), "OpenTerminalInPlace" => Some(Self::OpenTerminalInPlace),
"OpenCommandInPlace" => Some(Self::OpenCommandInPlace), "OpenCommandInPlace" => Some(Self::OpenCommandInPlace),
"OpenFileInPlace" => Some(Self::OpenFileInPlace), "OpenFileInPlace" => Some(Self::OpenFileInPlace),
"RunCommand" => Some(Self::RunCommand),
_ => None, _ => None,
} }
} }

View file

@ -2,7 +2,7 @@ use crate::input::actions::Action;
use crate::input::config::ConversionError; use crate::input::config::ConversionError;
use clap::ArgEnum; use clap::ArgEnum;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt; use std::fmt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
@ -496,6 +496,8 @@ pub enum Event {
/// A Result of plugin permission request /// A Result of plugin permission request
PermissionRequestResult(PermissionStatus), PermissionRequestResult(PermissionStatus),
SessionUpdate(Vec<SessionInfo>), SessionUpdate(Vec<SessionInfo>),
RunCommandResult(Option<i32>, Vec<u8>, Vec<u8>, BTreeMap<String, String>), // exit_code, STDOUT, STDERR,
// context
} }
#[derive( #[derive(
@ -1064,4 +1066,13 @@ pub enum PluginCommand {
OpenTerminalInPlace(FileToOpen), // only used for the path as cwd OpenTerminalInPlace(FileToOpen), // only used for the path as cwd
OpenFileInPlace(FileToOpen), OpenFileInPlace(FileToOpen),
OpenCommandPaneInPlace(CommandToRun), OpenCommandPaneInPlace(CommandToRun),
RunCommand(
Vec<String>,
BTreeMap<String, String>,
PathBuf,
BTreeMap<String, String>,
), // command,
// env_Variables,
// cwd,
// context
} }

View file

@ -445,6 +445,7 @@ pub enum BackgroundJobContext {
ReadAllSessionInfosOnMachine, ReadAllSessionInfosOnMachine,
ReportSessionInfo, ReportSessionInfo,
ReportLayoutInfo, ReportLayoutInfo,
RunCommand,
Exit, Exit,
} }

View file

@ -40,6 +40,7 @@ enum EventType {
FileSystemDelete = 14; FileSystemDelete = 14;
PermissionRequestResult = 15; PermissionRequestResult = 15;
SessionUpdate = 16; SessionUpdate = 16;
RunCommandResult = 17;
} }
message EventNameList { message EventNameList {
@ -61,6 +62,7 @@ message Event {
FileListPayload file_list_payload = 11; FileListPayload file_list_payload = 11;
PermissionRequestResultPayload permission_request_result_payload = 12; PermissionRequestResultPayload permission_request_result_payload = 12;
SessionUpdatePayload session_update_payload = 13; SessionUpdatePayload session_update_payload = 13;
RunCommandResultPayload run_command_result_payload = 14;
} }
} }
@ -68,6 +70,18 @@ message SessionUpdatePayload {
repeated SessionManifest session_manifests = 1; repeated SessionManifest session_manifests = 1;
} }
message RunCommandResultPayload {
optional int32 exit_code = 1;
bytes stdout = 2;
bytes stderr = 3;
repeated ContextItem context = 4;
}
message ContextItem {
string name = 1;
string value = 2;
}
message PermissionRequestResultPayload { message PermissionRequestResultPayload {
bool granted = 1; bool granted = 1;
} }

View file

@ -183,6 +183,21 @@ impl TryFrom<ProtobufEvent> for Event {
}, },
_ => Err("Malformed payload for the SessionUpdate Event"), _ => Err("Malformed payload for the SessionUpdate Event"),
}, },
Some(ProtobufEventType::RunCommandResult) => match protobuf_event.payload {
Some(ProtobufEventPayload::RunCommandResultPayload(run_command_result_payload)) => {
Ok(Event::RunCommandResult(
run_command_result_payload.exit_code,
run_command_result_payload.stdout,
run_command_result_payload.stderr,
run_command_result_payload
.context
.into_iter()
.map(|c_i| (c_i.name, c_i.value))
.collect(),
))
},
_ => Err("Malformed payload for the RunCommandResult Event"),
},
None => Err("Unknown Protobuf Event"), None => Err("Unknown Protobuf Event"),
} }
} }
@ -338,6 +353,23 @@ impl TryFrom<Event> for ProtobufEvent {
payload: Some(event::Payload::SessionUpdatePayload(session_update_payload)), payload: Some(event::Payload::SessionUpdatePayload(session_update_payload)),
}) })
}, },
Event::RunCommandResult(exit_code, stdout, stderr, context) => {
let run_command_result_payload = RunCommandResultPayload {
exit_code,
stdout,
stderr,
context: context
.into_iter()
.map(|(name, value)| ContextItem { name, value })
.collect(),
};
Ok(ProtobufEvent {
name: ProtobufEventType::RunCommandResult as i32,
payload: Some(event::Payload::RunCommandResultPayload(
run_command_result_payload,
)),
})
},
} }
} }
} }
@ -783,6 +815,7 @@ impl TryFrom<ProtobufEventType> for EventType {
ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete, ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete,
ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult, ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult,
ProtobufEventType::SessionUpdate => EventType::SessionUpdate, ProtobufEventType::SessionUpdate => EventType::SessionUpdate,
ProtobufEventType::RunCommandResult => EventType::RunCommandResult,
}) })
} }
} }
@ -808,6 +841,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete, EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete,
EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult, EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult,
EventType::SessionUpdate => ProtobufEventType::SessionUpdate, EventType::SessionUpdate => ProtobufEventType::SessionUpdate,
EventType::RunCommandResult => ProtobufEventType::RunCommandResult,
}) })
} }
} }

View file

@ -82,6 +82,7 @@ enum CommandName {
OpenTerminalInPlace = 68; OpenTerminalInPlace = 68;
OpenCommandInPlace = 69; OpenCommandInPlace = 69;
OpenFileInPlace = 70; OpenFileInPlace = 70;
RunCommand = 71;
} }
message PluginCommand { message PluginCommand {
@ -128,6 +129,7 @@ message PluginCommand {
OpenFilePayload open_file_in_place_payload = 40; OpenFilePayload open_file_in_place_payload = 40;
OpenFilePayload open_terminal_in_place_payload = 41; OpenFilePayload open_terminal_in_place_payload = 41;
OpenCommandPanePayload open_command_pane_in_place_payload = 42; OpenCommandPanePayload open_command_pane_in_place_payload = 42;
RunCommandPayload run_command_payload = 43;
} }
} }
@ -170,6 +172,23 @@ message ExecCmdPayload {
repeated string command_line = 1; repeated string command_line = 1;
} }
message RunCommandPayload {
repeated string command_line = 1;
repeated EnvVariable env_variables = 2;
string cwd = 3;
repeated ContextItem context = 4;
}
message EnvVariable {
string name = 1;
string value = 2;
}
message ContextItem {
string name = 1;
string value = 2;
}
message PluginMessagePayload { message PluginMessagePayload {
api.message.Message message = 1; api.message.Message message = 1;
} }

View file

@ -3,9 +3,10 @@ pub use super::generated_api::api::{
event::EventNameList as ProtobufEventNameList, event::EventNameList as ProtobufEventNameList,
input_mode::InputMode as ProtobufInputMode, input_mode::InputMode as ProtobufInputMode,
plugin_command::{ plugin_command::{
plugin_command::Payload, CommandName, ExecCmdPayload, IdAndNewName, MovePayload, plugin_command::Payload, CommandName, ContextItem, EnvVariable, ExecCmdPayload,
OpenCommandPanePayload, OpenFilePayload, PluginCommand as ProtobufPluginCommand, IdAndNewName, MovePayload, OpenCommandPanePayload, OpenFilePayload,
PluginMessagePayload, RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload,
SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload,
}, },
plugin_permission::PermissionType as ProtobufPermissionType, plugin_permission::PermissionType as ProtobufPermissionType,
@ -14,7 +15,9 @@ pub use super::generated_api::api::{
use crate::data::{ConnectToSession, PermissionType, PluginCommand}; use crate::data::{ConnectToSession, PermissionType, PluginCommand};
use std::collections::BTreeMap;
use std::convert::TryFrom; use std::convert::TryFrom;
use std::path::PathBuf;
impl TryFrom<ProtobufPluginCommand> for PluginCommand { impl TryFrom<ProtobufPluginCommand> for PluginCommand {
type Error = &'static str; type Error = &'static str;
@ -553,6 +556,27 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}, },
_ => Err("Mismatched payload for OpenCommandPaneInPlace"), _ => Err("Mismatched payload for OpenCommandPaneInPlace"),
}, },
Some(CommandName::RunCommand) => match protobuf_plugin_command.payload {
Some(Payload::RunCommandPayload(run_command_payload)) => {
let env_variables: BTreeMap<String, String> = run_command_payload
.env_variables
.into_iter()
.map(|e| (e.name, e.value))
.collect();
let context: BTreeMap<String, String> = run_command_payload
.context
.into_iter()
.map(|e| (e.name, e.value))
.collect();
Ok(PluginCommand::RunCommand(
run_command_payload.command_line,
env_variables,
PathBuf::from(run_command_payload.cwd),
context,
))
},
_ => Err("Mismatched payload for RunCommand"),
},
None => Err("Unrecognized plugin command"), None => Err("Unrecognized plugin command"),
} }
} }
@ -928,6 +952,26 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
}, },
)), )),
}), }),
PluginCommand::RunCommand(command_line, env_variables, cwd, context) => {
let env_variables: Vec<_> = env_variables
.into_iter()
.map(|(name, value)| EnvVariable { name, value })
.collect();
let context: Vec<_> = context
.into_iter()
.map(|(name, value)| ContextItem { name, value })
.collect();
let cwd = cwd.display().to_string();
Ok(ProtobufPluginCommand {
name: CommandName::RunCommand as i32,
payload: Some(Payload::RunCommandPayload(RunCommandPayload {
command_line,
env_variables,
cwd,
context,
})),
})
},
} }
} }
} }