feat(core): implement keyboard layout watcher and refactor worker start flag
This commit is contained in:
		
							parent
							
								
									fb93754f90
								
							
						
					
					
						commit
						c91e8f56bc
					
				
							
								
								
									
										72
									
								
								espanso/src/cli/daemon/keyboard_layout_watcher.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								espanso/src/cli/daemon/keyboard_layout_watcher.rs
									
									
									
									
									
										Normal 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 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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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<i32>,
 | 
			
		||||
  worker_run_count: &mut i32,
 | 
			
		||||
  manual_start: bool,
 | 
			
		||||
  start_reason: Option<String>,
 | 
			
		||||
) {
 | 
			
		||||
  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<i32>,
 | 
			
		||||
  start_reason: Option<String>,
 | 
			
		||||
) {
 | 
			
		||||
  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");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<UIEvent>,
 | 
			
		||||
  secure_input_receiver: Receiver<SecureInputEvent>,
 | 
			
		||||
  use_evdev_backend: bool,
 | 
			
		||||
  run_count: i32,
 | 
			
		||||
  has_been_started_manually: bool,
 | 
			
		||||
  start_reason: Option<String>,
 | 
			
		||||
) -> Result<JoinHandle<ExitMode>> {
 | 
			
		||||
  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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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::<i32>().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");
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								espanso/src/common_flags.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								espanso/src/common_flags.rs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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 <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
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";
 | 
			
		||||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user