feat(core): implement keyboard layout watcher and refactor worker start flag

This commit is contained in:
Federico Terzi 2021-08-10 22:18:39 +02:00
parent fb93754f90
commit c91e8f56bc
6 changed files with 179 additions and 78 deletions

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 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;
}
}
}

View File

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

View File

@ -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);

View File

@ -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");

View 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";

View File

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