feat(core): implement 'match exec' subcommand. Fix #883, Fix #780

This commit is contained in:
Federico Terzi 2021-10-31 15:44:25 +01:00
parent b38623376e
commit 8edf998e60
11 changed files with 275 additions and 44 deletions

View File

@ -0,0 +1,71 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::collections::HashMap;
use anyhow::{bail, Context, Result};
use clap::ArgMatches;
use espanso_ipc::IPCClient;
use espanso_path::Paths;
use crate::{
ipc::{create_ipc_client_to_worker, IPCEvent, RequestMatchExpansionPayload},
lock::acquire_worker_lock,
};
pub fn exec_main(cli_args: &ArgMatches, paths: &Paths) -> Result<()> {
let trigger = cli_args.value_of("trigger");
let args = cli_args.values_of("arg");
if trigger.is_none() || trigger.map(str::is_empty).unwrap_or(false) {
bail!("You need to specify the --trigger 'trigger' option. Run `espanso match exec --help` for more information.");
}
if acquire_worker_lock(&paths.runtime).is_some() {
bail!("Worker process is not running, please start Espanso first.")
}
let mut client = create_ipc_client_to_worker(&paths.runtime)?;
let mut match_args = HashMap::new();
if let Some(args) = args {
args.for_each(|arg| {
let tokens = arg.split_once('=');
if let Some((key, value)) = tokens {
match_args.insert(key.to_string(), value.to_string());
} else {
eprintln!(
"invalid format for argument '{}', you should follow the 'name=value' format",
arg
);
}
})
}
client
.send_async(IPCEvent::RequestMatchExpansion(
RequestMatchExpansionPayload {
trigger: trigger.map(String::from),
args: match_args,
},
))
.context("unable to send payload to worker process")?;
Ok(())
}

View File

@ -19,10 +19,12 @@
use super::{CliModule, CliModuleArgs};
mod exec;
mod list;
pub fn new() -> CliModule {
CliModule {
requires_paths: true,
requires_config: true,
subcommand: "match".to_string(),
entry: match_main,
@ -34,14 +36,18 @@ fn match_main(args: CliModuleArgs) -> i32 {
let cli_args = args.cli_args.expect("missing cli_args");
let config_store = args.config_store.expect("missing config_store");
let match_store = args.match_store.expect("missing match_store");
let paths = args.paths.expect("missing paths");
if let Some(sub_args) = cli_args.subcommand_matches("list") {
if let Err(err) = list::list_main(sub_args, config_store, match_store) {
eprintln!("unable to list matches: {:?}", err);
return 1;
}
} else if let Some(_sub_args) = cli_args.subcommand_matches("exec") {
todo!();
} else if let Some(sub_args) = cli_args.subcommand_matches("exec") {
if let Err(err) = exec::exec_main(sub_args, &paths) {
eprintln!("unable to exec match: {:?}", err);
return 1;
}
} else {
eprintln!("Invalid use, please run 'espanso match --help' to get more information.");
return 1;

View File

@ -0,0 +1,72 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use crossbeam::channel::{Receiver, Select, SelectedOperation};
use espanso_engine::{
event::{Event, EventType},
funnel,
};
use log::warn;
use super::sequencer::Sequencer;
pub struct IpcEventSource<'a> {
pub ipc_event_receiver: Receiver<EventType>,
pub sequencer: &'a Sequencer,
}
impl<'a> IpcEventSource<'a> {
pub fn new(ipc_event_receiver: Receiver<EventType>, sequencer: &'a Sequencer) -> Self {
IpcEventSource {
ipc_event_receiver,
sequencer,
}
}
}
impl<'a> funnel::Source<'a> for IpcEventSource<'a> {
fn register(&'a self, select: &mut Select<'a>) -> usize {
select.recv(&self.ipc_event_receiver)
}
fn receive(&self, op: SelectedOperation) -> Option<Event> {
let ipc_event = op
.recv(&self.ipc_event_receiver)
.expect("unable to select data from IpcEventSource receiver");
// Execute only events that have been whitelisted
if !is_event_type_allowed(&ipc_event) {
warn!(
"received black-listed event from IPC stream, blocking it: {:?}",
ipc_event
);
return None;
}
Some(Event {
source_id: self.sequencer.next_id(),
etype: ipc_event,
})
}
}
fn is_event_type_allowed(event: &EventType) -> bool {
matches!(event, EventType::MatchExecRequest(_))
}

View File

@ -35,6 +35,7 @@ use self::{
pub mod detect;
pub mod exit;
pub mod ipc;
pub mod key_state;
pub mod modifier;
pub mod secure_input;

View File

@ -43,33 +43,22 @@ impl<'a> SecureInputSource<'a> {
impl<'a> funnel::Source<'a> for SecureInputSource<'a> {
fn register(&'a self, select: &mut Select<'a>) -> usize {
if cfg!(target_os = "macos") {
select.recv(&self.receiver)
} else {
999999
}
select.recv(&self.receiver)
}
fn receive(&self, op: SelectedOperation) -> Option<Event> {
if cfg!(target_os = "macos") {
let si_event = op
.recv(&self.receiver)
.expect("unable to select data from SecureInputSource receiver");
let si_event = op
.recv(&self.receiver)
.expect("unable to select data from SecureInputSource receiver");
Some(Event {
source_id: self.sequencer.next_id(),
etype: match si_event {
SecureInputEvent::Disabled => EventType::SecureInputDisabled,
SecureInputEvent::Enabled { app_name, app_path } => {
EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
}
},
})
} else {
Some(Event {
source_id: self.sequencer.next_id(),
etype: EventType::NOOP,
})
}
Some(Event {
source_id: self.sequencer.next_id(),
etype: match si_event {
SecureInputEvent::Disabled => EventType::SecureInputDisabled,
SecureInputEvent::Enabled { app_name, app_path } => {
EventType::SecureInputEnabled(SecureInputEnabledEvent { app_name, app_path })
}
},
})
}
}

View File

@ -23,7 +23,7 @@ use anyhow::Result;
use crossbeam::channel::Receiver;
use espanso_config::{config::ConfigStore, matches::store::MatchStore};
use espanso_detect::SourceCreationOptions;
use espanso_engine::event::ExitMode;
use espanso_engine::event::{EventType, ExitMode};
use espanso_inject::{InjectorCreationOptions, KeyboardStateProvider};
use espanso_path::Paths;
use espanso_ui::{event::UIEvent, UIRemote};
@ -82,6 +82,7 @@ pub fn initialize_and_spawn(
secure_input_receiver: Receiver<SecureInputEvent>,
use_evdev_backend: bool,
start_reason: Option<String>,
ipc_event_receiver: Receiver<EventType>,
) -> Result<JoinHandle<ExitMode>> {
let handle = std::thread::Builder::new()
.name("engine thread".to_string())
@ -131,17 +132,18 @@ pub fn initialize_and_spawn(
})
.expect("failed to initialize detector module");
let exit_source = super::engine::funnel::exit::ExitSource::new(exit_signal, &sequencer);
let ipc_event_source =
super::engine::funnel::ipc::IpcEventSource::new(ipc_event_receiver, &sequencer);
let ui_source = super::engine::funnel::ui::UISource::new(ui_event_receiver, &sequencer);
let secure_input_source = super::engine::funnel::secure_input::SecureInputSource::new(
secure_input_receiver,
&sequencer,
);
let sources: Vec<&dyn espanso_engine::funnel::Source> = vec![
&detect_source,
&exit_source,
&ui_source,
&secure_input_source,
];
let mut sources: Vec<&dyn espanso_engine::funnel::Source> =
vec![&detect_source, &exit_source, &ui_source, &ipc_event_source];
if cfg!(target_os = "macos") {
sources.push(&secure_input_source);
}
let funnel = espanso_engine::funnel::default(&sources);
let rolling_matcher = RollingMatcherAdapter::new(
@ -226,6 +228,7 @@ pub fn initialize_and_spawn(
&config_manager,
&config_manager,
&modifier_state_store,
&combined_match_cache,
);
let event_injector = EventInjectorAdapter::new(&*injector, &config_manager);

View File

@ -21,13 +21,17 @@ use std::path::Path;
use anyhow::Result;
use crossbeam::channel::Sender;
use espanso_engine::event::ExitMode;
use espanso_engine::event::{external::MatchExecRequestEvent, EventType, ExitMode};
use espanso_ipc::{EventHandlerResponse, IPCServer};
use log::{error, warn};
use crate::ipc::IPCEvent;
pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<ExitMode>) -> Result<()> {
pub fn initialize_and_spawn(
runtime_dir: &Path,
exit_notify: Sender<ExitMode>,
event_notify: Sender<EventType>,
) -> Result<()> {
let server = crate::ipc::create_worker_ipc_server(runtime_dir)?;
std::thread::Builder::new()
@ -55,6 +59,21 @@ pub fn initialize_and_spawn(runtime_dir: &Path, exit_notify: Sender<ExitMode>) -
EventHandlerResponse::NoResponse
}
IPCEvent::RequestMatchExpansion(payload) => {
if let Err(err) =
event_notify.send(EventType::MatchExecRequest(MatchExecRequestEvent {
trigger: payload.trigger,
args: payload.args,
}))
{
error!(
"experienced error while sending event signal from worker ipc handler: {}",
err
);
}
EventHandlerResponse::NoResponse
}
#[allow(unreachable_patterns)]
unexpected_event => {
warn!(

View File

@ -21,8 +21,9 @@ use std::collections::HashMap;
use espanso_config::{
config::ConfigStore,
matches::{store::MatchStore, Match, MatchEffect},
matches::{store::MatchStore, Match, MatchCause, MatchEffect},
};
use espanso_engine::event::internal::DetectedMatch;
use super::{builtin::BuiltInMatch, engine::process::middleware::match_select::MatchSummary};
@ -164,3 +165,50 @@ impl<'a> espanso_engine::process::MatchProvider for CombinedMatchCache<'a> {
ids
}
}
impl<'a> espanso_engine::process::MatchResolver for CombinedMatchCache<'a> {
fn find_matches_from_trigger(&self, trigger: &str) -> Vec<DetectedMatch> {
let user_matches: Vec<DetectedMatch> = self
.user_match_cache
.cache
.values()
.filter_map(|m| {
if let MatchCause::Trigger(trigger_cause) = &m.cause {
if trigger_cause.triggers.iter().any(|t| t == trigger) {
Some(DetectedMatch {
id: m.id,
trigger: Some(trigger.to_string()),
..Default::default()
})
} else {
None
}
} else {
None
}
})
.collect();
let builtin_matches: Vec<DetectedMatch> = self
.builtin_match_cache
.values()
.filter_map(|m| {
if m.triggers.iter().any(|t| t == trigger) {
Some(DetectedMatch {
id: m.id,
trigger: Some(trigger.to_string()),
..Default::default()
})
} else {
None
}
})
.collect();
let mut matches = Vec::with_capacity(user_matches.len() + builtin_matches.len());
matches.extend(user_matches);
matches.extend(builtin_matches);
matches
}
}

View File

@ -112,6 +112,7 @@ fn worker_main(args: CliModuleArgs) -> i32 {
.expect("unable to initialize UI module");
let (engine_exit_notify, engine_exit_receiver) = unbounded();
let (ipc_event_notify, ipc_event_receiver) = unbounded();
let (engine_ui_event_sender, engine_ui_event_receiver) = unbounded();
let (engine_secure_input_sender, engine_secure_input_receiver) = unbounded();
@ -126,11 +127,12 @@ fn worker_main(args: CliModuleArgs) -> i32 {
engine_secure_input_receiver,
use_evdev_backend,
start_reason,
ipc_event_receiver,
)
.expect("unable to initialize engine");
// Setup the IPC server
ipc::initialize_and_spawn(&paths.runtime, engine_exit_notify.clone())
ipc::initialize_and_spawn(&paths.runtime, engine_exit_notify.clone(), ipc_event_notify)
.expect("unable to initialize IPC server");
// If specified, automatically monitor the daemon status and

View File

@ -20,12 +20,20 @@
use anyhow::Result;
use espanso_ipc::{IPCClient, IPCServer};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::{collections::HashMap, path::Path};
#[derive(Debug, Serialize, Deserialize)]
pub enum IPCEvent {
Exit,
ExitAllProcesses,
RequestMatchExpansion(RequestMatchExpansionPayload),
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RequestMatchExpansionPayload {
pub trigger: Option<String>,
pub args: HashMap<String, String>,
}
pub fn create_daemon_ipc_server(runtime_dir: &Path) -> Result<impl IPCServer<IPCEvent>> {

View File

@ -398,12 +398,24 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#))
.takes_value(true)
)
)
// .subcommand(SubCommand::with_name("exec")
// .about("Triggers the expansion of the given match")
// .arg(Arg::with_name("trigger")
// .help("The trigger of the match to be expanded")
// )
// )
.subcommand(SubCommand::with_name("exec")
.about("Triggers the expansion of a match")
.arg(Arg::with_name("trigger")
.short("t")
.long("trigger")
.help("The trigger of the match to be expanded")
.required(false)
.takes_value(true)
)
.arg(Arg::with_name("arg")
.long("arg")
.help("Specify also an argument for the expansion, following the --arg 'name=value' format. You can specify multiple ones.")
.required(false)
.takes_value(true)
.multiple(true)
.number_of_values(1)
)
)
)
.subcommand(
SubCommand::with_name("package")