From c91e8f56bca312289b32ca0cfcc4932383d5d3e0 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 10 Aug 2021 22:18:39 +0200 Subject: [PATCH] feat(core): implement keyboard layout watcher and refactor worker start flag --- .../src/cli/daemon/keyboard_layout_watcher.rs | 72 ++++++++++++ espanso/src/cli/daemon/mod.rs | 108 ++++++++++-------- espanso/src/cli/worker/engine/mod.rs | 27 ++--- espanso/src/cli/worker/mod.rs | 17 +-- espanso/src/common_flags.rs | 22 ++++ espanso/src/main.rs | 11 +- 6 files changed, 179 insertions(+), 78 deletions(-) create mode 100644 espanso/src/cli/daemon/keyboard_layout_watcher.rs create mode 100644 espanso/src/common_flags.rs diff --git a/espanso/src/cli/daemon/keyboard_layout_watcher.rs b/espanso/src/cli/daemon/keyboard_layout_watcher.rs new file mode 100644 index 0000000..9224352 --- /dev/null +++ b/espanso/src/cli/daemon/keyboard_layout_watcher.rs @@ -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 . + */ + +use anyhow::Result; +use crossbeam::channel::Sender; +use log::{error, debug}; + +const WATCHER_INTERVAL: u64 = 1000; + +pub fn initialize_and_spawn(watcher_notify: Sender<()>) -> Result<()> { + // On Windows and macOS we don't need to restart espanso when the layout changes + if !cfg!(target_os = "linux") { + return Ok(()); + } + + std::thread::Builder::new() + .name("keyboard_layout_watcher".to_string()) + .spawn(move || { + watcher_main(&watcher_notify); + })?; + + Ok(()) +} + +fn watcher_main(watcher_notify: &Sender<()>) { + let layout = espanso_detect::get_active_layout(); + + if layout.is_none() { + error!("unable to start keyboard layout watcher, as espanso couldn't determine active layout."); + return; + } + + let mut layout = layout.expect("missing active layout"); + + loop { + std::thread::sleep(std::time::Duration::from_millis(WATCHER_INTERVAL)); + + if let Some(current_layout) = espanso_detect::get_active_layout() { + if current_layout != layout { + debug!( + "detected keyboard layout change: from {} to {}", + layout, current_layout + ); + + if let Err(error) = watcher_notify.send(()) { + error!("unable to send keyboard layout changed event: {}", error); + } + + layout = current_layout; + } + } else { + error!("keyboard layout watcher couldn't determine active layout"); + break; + } + } +} diff --git a/espanso/src/cli/daemon/mod.rs b/espanso/src/cli/daemon/mod.rs index e212a45..7355a69 100644 --- a/espanso/src/cli/daemon/mod.rs +++ b/espanso/src/cli/daemon/mod.rs @@ -24,10 +24,12 @@ use crossbeam::{ select, }; use espanso_ipc::IPCClient; +use espanso_path::Paths; use log::{error, info, warn}; use crate::{ cli::util::CommandExt, + common_flags::{*}, exit_code::{ DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR, DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART, @@ -41,6 +43,7 @@ use crate::{ use super::{CliModule, CliModuleArgs, PathsOverrides}; mod ipc; +mod keyboard_layout_watcher; mod troubleshoot; mod watcher; @@ -91,6 +94,10 @@ fn daemon_main(args: CliModuleArgs) -> i32 { watcher::initialize_and_spawn(&paths.config, watcher_notify) .expect("unable to initialize config watcher thread"); + let (keyboard_layout_watcher_notify, keyboard_layout_watcher_signal) = unbounded::<()>(); + keyboard_layout_watcher::initialize_and_spawn(keyboard_layout_watcher_notify) + .expect("unable to initialize keyboard layout watcher thread"); + let config_store = match troubleshoot::load_config_or_troubleshoot_until_config_is_correct_or_abort( &paths, @@ -116,13 +123,10 @@ fn daemon_main(args: CliModuleArgs) -> i32 { // TODO: register signals to terminate the worker if the daemon terminates - let mut worker_run_count = 0; - spawn_worker( &paths_overrides, exit_notify.clone(), - &mut worker_run_count, - false, + None ); ipc::initialize_and_spawn(&paths.runtime, exit_notify.clone()) @@ -156,40 +160,13 @@ fn daemon_main(args: CliModuleArgs) -> i32 { }; if should_restart_worker { - match create_ipc_client_to_worker(&paths.runtime) { - Ok(mut worker_ipc) => { - if let Err(err) = worker_ipc.send_async(IPCEvent::Exit) { - error!( - "unable to send termination signal to worker process: {}", - err - ); - } - } - Err(err) => { - error!("could not establish IPC connection with worker: {}", err); - } - } - - // Wait until the worker process has terminated - let start = Instant::now(); - let mut has_timed_out = true; - while start.elapsed() < std::time::Duration::from_secs(30) { - let lock_file = acquire_worker_lock(&paths.runtime); - if lock_file.is_some() { - has_timed_out = false; - break; - } - - std::thread::sleep(std::time::Duration::from_millis(100)); - } - - if !has_timed_out { - spawn_worker(&paths_overrides, exit_notify.clone(), &mut worker_run_count, false); - } else { - error!("could not restart worker, as the exit process has timed out"); - } + restart_worker(&paths, &paths_overrides, exit_notify.clone(), Some(WORKER_START_REASON_CONFIG_CHANGED.to_string())); } } + recv(keyboard_layout_watcher_signal) -> _ => { + info!("keyboard layout change detected, restarting worker..."); + restart_worker(&paths, &paths_overrides, exit_notify.clone(), Some(WORKER_START_REASON_KEYBOARD_LAYOUT_CHANGED.to_string())); + } recv(exit_signal) -> code => { match code { Ok(code) => { @@ -200,7 +177,7 @@ fn daemon_main(args: CliModuleArgs) -> i32 { } WORKER_RESTART => { info!("worker requested a restart, spawning a new one..."); - spawn_worker(&paths_overrides, exit_notify.clone(), &mut worker_run_count, true); + spawn_worker(&paths_overrides, exit_notify.clone(), Some(WORKER_START_REASON_MANUAL.to_string())); } _ => { error!("received unexpected exit code from worker {}, exiting", code); @@ -260,8 +237,7 @@ fn terminate_worker_if_already_running(runtime_dir: &Path) { fn spawn_worker( paths_overrides: &PathsOverrides, exit_notify: Sender, - worker_run_count: &mut i32, - manual_start: bool, + start_reason: Option, ) { info!("spawning the worker process..."); @@ -270,23 +246,19 @@ fn spawn_worker( let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string()); - let string_worker_run_count = format!("{}", worker_run_count); let mut args = vec![ "worker", "--monitor-daemon", - "--run-count", - &string_worker_run_count, ]; - if manual_start { - args.push("--manual"); + if let Some(start_reason) = &start_reason { + args.push("--start-reason"); + args.push(&start_reason); } command.args(&args); command.with_paths_overrides(paths_overrides); let mut child = command.spawn().expect("unable to spawn worker process"); - *worker_run_count += 1; - // Create a monitor thread that will exit with the same non-zero code if // the worker thread exits std::thread::Builder::new() @@ -305,3 +277,47 @@ fn spawn_worker( }) .expect("Unable to spawn worker monitor thread"); } + +fn restart_worker( + paths: &Paths, + paths_overrides: &PathsOverrides, + exit_notify: Sender, + start_reason: Option, +) { + match create_ipc_client_to_worker(&paths.runtime) { + Ok(mut worker_ipc) => { + if let Err(err) = worker_ipc.send_async(IPCEvent::Exit) { + error!( + "unable to send termination signal to worker process: {}", + err + ); + } + } + Err(err) => { + error!("could not establish IPC connection with worker: {}", err); + } + } + + // Wait until the worker process has terminated + let start = Instant::now(); + let mut has_timed_out = true; + while start.elapsed() < std::time::Duration::from_secs(30) { + let lock_file = acquire_worker_lock(&paths.runtime); + if lock_file.is_some() { + has_timed_out = false; + break; + } + + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + if !has_timed_out { + spawn_worker( + &paths_overrides, + exit_notify, + start_reason, + ); + } else { + error!("could not restart worker, as the exit process has timed out"); + } +} diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index b4f53a5..e3cd7bf 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -41,7 +41,7 @@ use crate::{cli::worker::{context::Context, engine::{dispatch::executor::{clipbo extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter}, RendererAdapter, }, - }}, match_cache::{CombinedMatchCache, MatchCache}}, engine::event::ExitMode, preferences::Preferences}; + }}, match_cache::{CombinedMatchCache, MatchCache}}, common_flags::WORKER_START_REASON_CONFIG_CHANGED, engine::event::ExitMode, preferences::Preferences}; use super::secure_input::SecureInputEvent; @@ -60,8 +60,7 @@ pub fn initialize_and_spawn( ui_event_receiver: Receiver, secure_input_receiver: Receiver, use_evdev_backend: bool, - run_count: i32, - has_been_started_manually: bool, + start_reason: Option, ) -> Result> { let handle = std::thread::Builder::new() .name("engine thread".to_string()) @@ -213,22 +212,24 @@ pub fn initialize_and_spawn( } // TODO: check config - match run_count { - 0 => { + match start_reason.as_deref() { + Some(flag) if flag == WORKER_START_REASON_CONFIG_CHANGED => { + ui_remote.show_notification("Configuration reloaded! Espanso automatically loads new changes as soon as you save them."); + } + Some("manual_restart") => { + ui_remote.show_notification("Configuration reloaded!"); + } + Some("keyboard_layout_changed") => { + ui_remote.show_notification("Updated keyboard layout!"); + } + _ => { ui_remote.show_notification("Espanso is running!"); if !preferences.has_displayed_welcome() { super::ui::welcome::show_welcome_screen(); preferences.set_has_displayed_welcome(true); } - }, - n => { - if has_been_started_manually { - ui_remote.show_notification("Configuration reloaded!"); - } else if n == 1 { - ui_remote.show_notification("Configuration reloaded! Espanso automatically loads new changes as soon as you save them."); - } - }, + } } let mut engine = crate::engine::Engine::new(&funnel, &mut processor, &dispatcher); diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs index 0420479..b7c0b26 100644 --- a/espanso/src/cli/worker/mod.rs +++ b/espanso/src/cli/worker/mod.rs @@ -61,15 +61,11 @@ fn worker_main(args: CliModuleArgs) -> i32 { let paths = args.paths.expect("missing paths in worker main"); let cli_args = args.cli_args.expect("missing cli_args in worker main"); - // This number is passed by the daemon and incremented at each worker - // restart. - let run_count = cli_args - .value_of("run-count") - .map(|val| val.parse::().unwrap_or(0)) - .unwrap_or(0); - debug!("starting with run-count = {:?}", cli_args); - - let has_been_started_manually = cli_args.is_present("manual"); + // When restarted, the daemon passes the reason why the worker was restarted (config_change, etc) + let start_reason = cli_args + .value_of("start-reason") + .map(String::from); + debug!("starting with start-reason = {:?}", start_reason); // Avoid running multiple worker instances let lock_file = acquire_worker_lock(&paths.runtime); @@ -132,8 +128,7 @@ fn worker_main(args: CliModuleArgs) -> i32 { engine_ui_event_receiver, engine_secure_input_receiver, use_evdev_backend, - run_count, - has_been_started_manually, + start_reason, ) .expect("unable to initialize engine"); diff --git a/espanso/src/common_flags.rs b/espanso/src/common_flags.rs new file mode 100644 index 0000000..3255d6b --- /dev/null +++ b/espanso/src/common_flags.rs @@ -0,0 +1,22 @@ +/* + * 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 . + */ + +pub const WORKER_START_REASON_MANUAL: &str = "manual_restart"; +pub const WORKER_START_REASON_CONFIG_CHANGED: &str = "config_changed"; +pub const WORKER_START_REASON_KEYBOARD_LAYOUT_CHANGED: &str = "keyboard_layout_changed"; \ No newline at end of file diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 11dce8e..0a1f90c 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -40,6 +40,7 @@ use crate::{ mod capabilities; mod cli; +mod common_flags; mod config; mod engine; mod exit_code; @@ -357,17 +358,11 @@ fn main() { SubCommand::with_name("worker") .setting(AppSettings::Hidden) .arg( - Arg::with_name("run-count") - .long("run-count") + Arg::with_name("start-reason") + .long("start-reason") .required(false) .takes_value(true), ) - .arg( - Arg::with_name("manual") - .long("manual") - .required(false) - .takes_value(false), - ) .arg( Arg::with_name("monitor-daemon") .long("monitor-daemon")