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,39 +160,12 @@ 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 {
|
||||
|
@ -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