feat(core): implement basic troubleshooting logic
This commit is contained in:
		
							parent
							
								
									dc6b11cfc8
								
							
						
					
					
						commit
						f42c4ef56e
					
				
							
								
								
									
										28
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							|  | @ -105,6 +105,17 @@ version = "0.1.6" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "bstr" | ||||||
|  | version = "0.2.15" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" | ||||||
|  | dependencies = [ | ||||||
|  |  "lazy_static", | ||||||
|  |  "memchr", | ||||||
|  |  "regex-automata", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "byteorder" | name = "byteorder" | ||||||
| version = "1.4.3" | version = "1.4.3" | ||||||
|  | @ -508,6 +519,7 @@ dependencies = [ | ||||||
|  "markdown", |  "markdown", | ||||||
|  "named_pipe", |  "named_pipe", | ||||||
|  "notify", |  "notify", | ||||||
|  |  "opener", | ||||||
|  "regex", |  "regex", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  | @ -1317,6 +1329,16 @@ dependencies = [ | ||||||
|  "objc", |  "objc", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "opener" | ||||||
|  | version = "0.5.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "4ea3ebcd72a54701f56345f16785a6d3ac2df7e986d273eb4395c0b01db17952" | ||||||
|  | dependencies = [ | ||||||
|  |  "bstr", | ||||||
|  |  "winapi 0.3.9", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "ordered-float" | name = "ordered-float" | ||||||
| version = "2.1.1" | version = "2.1.1" | ||||||
|  | @ -1639,6 +1661,12 @@ dependencies = [ | ||||||
|  "regex-syntax", |  "regex-syntax", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "regex-automata" | ||||||
|  | version = "0.1.10" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "regex-syntax" | name = "regex-syntax" | ||||||
| version = "0.6.22" | version = "0.6.22" | ||||||
|  |  | ||||||
|  | @ -55,6 +55,7 @@ dialoguer = "0.8.0" | ||||||
| colored = "2.0.0" | colored = "2.0.0" | ||||||
| tempdir = "0.3.7" | tempdir = "0.3.7" | ||||||
| notify = "4.0.17" | notify = "4.0.17" | ||||||
|  | opener = "0.5.0" | ||||||
| 
 | 
 | ||||||
| [target.'cfg(windows)'.dependencies] | [target.'cfg(windows)'.dependencies] | ||||||
| named_pipe = "0.4.1" | named_pipe = "0.4.1" | ||||||
|  |  | ||||||
|  | @ -27,14 +27,22 @@ use espanso_ipc::IPCClient; | ||||||
| use espanso_path::Paths; | use espanso_path::Paths; | ||||||
| use log::{error, info, warn}; | use log::{error, info, warn}; | ||||||
| 
 | 
 | ||||||
| use crate::{VERSION, exit_code::{ | use crate::{ | ||||||
|     DAEMON_ALREADY_RUNNING, DAEMON_GENERAL_ERROR, DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, |   cli::util::CommandExt, | ||||||
|     WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART, WORKER_SUCCESS, |   exit_code::{ | ||||||
|   }, ipc::{create_ipc_client_to_worker, IPCEvent}, lock::{acquire_daemon_lock, acquire_legacy_lock, acquire_worker_lock}}; |     DAEMON_ALREADY_RUNNING, DAEMON_FATAL_CONFIG_ERROR, DAEMON_GENERAL_ERROR, | ||||||
|  |     DAEMON_LEGACY_ALREADY_RUNNING, DAEMON_SUCCESS, WORKER_EXIT_ALL_PROCESSES, WORKER_RESTART, | ||||||
|  |     WORKER_SUCCESS, | ||||||
|  |   }, | ||||||
|  |   ipc::{create_ipc_client_to_worker, IPCEvent}, | ||||||
|  |   lock::{acquire_daemon_lock, acquire_legacy_lock, acquire_worker_lock}, | ||||||
|  |   VERSION, | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| use super::{CliModule, CliModuleArgs}; | use super::{CliModule, CliModuleArgs, PathsOverrides}; | ||||||
| 
 | 
 | ||||||
| mod ipc; | mod ipc; | ||||||
|  | mod troubleshoot; | ||||||
| mod ui; | mod ui; | ||||||
| mod watcher; | mod watcher; | ||||||
| 
 | 
 | ||||||
|  | @ -42,8 +50,6 @@ pub fn new() -> CliModule { | ||||||
|   #[allow(clippy::needless_update)] |   #[allow(clippy::needless_update)] | ||||||
|   CliModule { |   CliModule { | ||||||
|     requires_paths: true, |     requires_paths: true, | ||||||
|     requires_config: true, |  | ||||||
|     enable_logs: true, |  | ||||||
|     log_mode: super::LogMode::CleanAndAppend, |     log_mode: super::LogMode::CleanAndAppend, | ||||||
|     subcommand: "daemon".to_string(), |     subcommand: "daemon".to_string(), | ||||||
|     entry: daemon_main, |     entry: daemon_main, | ||||||
|  | @ -53,10 +59,11 @@ pub fn new() -> CliModule { | ||||||
| 
 | 
 | ||||||
| fn daemon_main(args: CliModuleArgs) -> i32 { | fn daemon_main(args: CliModuleArgs) -> i32 { | ||||||
|   let paths = args.paths.expect("missing paths in daemon main"); |   let paths = args.paths.expect("missing paths in daemon main"); | ||||||
|   let config_store = args |   let paths_overrides = args | ||||||
|     .config_store |     .paths_overrides | ||||||
|     .expect("missing config store in worker main"); |     .expect("missing paths_overrides in daemon main"); | ||||||
|   let preferences = crate::preferences::get_default(&paths.runtime).expect("unable to obtain preferences"); |   let preferences = | ||||||
|  |     crate::preferences::get_default(&paths.runtime).expect("unable to obtain preferences"); | ||||||
|   let cli_args = args.cli_args.expect("missing cli_args in daemon main"); |   let cli_args = args.cli_args.expect("missing cli_args in daemon main"); | ||||||
| 
 | 
 | ||||||
|   // Make sure only one instance of the daemon is running
 |   // Make sure only one instance of the daemon is running
 | ||||||
|  | @ -77,6 +84,15 @@ fn daemon_main(args: CliModuleArgs) -> i32 { | ||||||
| 
 | 
 | ||||||
|   // TODO: we might need to check preconditions: accessibility on macOS, presence of binaries on Linux, etc
 |   // TODO: we might need to check preconditions: accessibility on macOS, presence of binaries on Linux, etc
 | ||||||
| 
 | 
 | ||||||
|  |   let config_store = | ||||||
|  |     match troubleshoot::load_config_or_troubleshoot(&paths, &paths_overrides, false) { | ||||||
|  |       Ok((load_result, _)) => load_result.config_store, | ||||||
|  |       Err(fatal_err) => { | ||||||
|  |         error!("critical error while loading config: {}", fatal_err); | ||||||
|  |         return DAEMON_FATAL_CONFIG_ERROR; | ||||||
|  |       } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|   info!("espanso version: {}", VERSION); |   info!("espanso version: {}", VERSION); | ||||||
|   // TODO: print os system and version? (with os_info crate)
 |   // TODO: print os system and version? (with os_info crate)
 | ||||||
| 
 | 
 | ||||||
|  | @ -86,7 +102,7 @@ fn daemon_main(args: CliModuleArgs) -> i32 { | ||||||
| 
 | 
 | ||||||
|   // TODO: register signals to terminate the worker if the daemon terminates
 |   // TODO: register signals to terminate the worker if the daemon terminates
 | ||||||
| 
 | 
 | ||||||
|   spawn_worker(&paths, exit_notify.clone()); |   spawn_worker(&paths, &paths_overrides, exit_notify.clone()); | ||||||
| 
 | 
 | ||||||
|   ipc::initialize_and_spawn(&paths.runtime, exit_notify.clone()) |   ipc::initialize_and_spawn(&paths.runtime, exit_notify.clone()) | ||||||
|     .expect("unable to initialize ipc server for daemon"); |     .expect("unable to initialize ipc server for daemon"); | ||||||
|  | @ -135,7 +151,7 @@ fn daemon_main(args: CliModuleArgs) -> i32 { | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if !has_timed_out { |         if !has_timed_out { | ||||||
|           spawn_worker(&paths, exit_notify.clone()); |           spawn_worker(&paths, &paths_overrides, exit_notify.clone()); | ||||||
|         } else { |         } else { | ||||||
|           error!("could not restart worker, as the exit process has timed out"); |           error!("could not restart worker, as the exit process has timed out"); | ||||||
|         } |         } | ||||||
|  | @ -150,7 +166,7 @@ fn daemon_main(args: CliModuleArgs) -> i32 { | ||||||
|               } |               } | ||||||
|               WORKER_RESTART => { |               WORKER_RESTART => { | ||||||
|                 info!("worker requested a restart, spawning a new one..."); |                 info!("worker requested a restart, spawning a new one..."); | ||||||
|                 spawn_worker(&paths, exit_notify.clone()); |                 spawn_worker(&paths, &paths_overrides, exit_notify.clone()); | ||||||
|               } |               } | ||||||
|               _ => { |               _ => { | ||||||
|                 error!("received unexpected exit code from worker {}, exiting", code); |                 error!("received unexpected exit code from worker {}, exiting", code); | ||||||
|  | @ -207,34 +223,33 @@ fn terminate_worker_if_already_running(runtime_dir: &Path) { | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| fn spawn_worker(paths: &Paths, exit_notify: Sender<i32>) { | fn spawn_worker(paths: &Paths, paths_overrides: &PathsOverrides, exit_notify: Sender<i32>) { | ||||||
|   info!("spawning the worker process..."); |   info!("spawning the worker process..."); | ||||||
| 
 | 
 | ||||||
|   let espanso_exe_path = |   let espanso_exe_path = | ||||||
|     std::env::current_exe().expect("unable to obtain espanso executable location"); |     std::env::current_exe().expect("unable to obtain espanso executable location"); | ||||||
| 
 | 
 | ||||||
|  |   // Before starting the worker, check if the configuration has any error, and if so, show the troubleshooting GUI
 | ||||||
|  |   let troubleshoot_guard = match troubleshoot::load_config_or_troubleshoot(paths, paths_overrides, true) { | ||||||
|  |     Ok((_, troubleshoot_guard)) => troubleshoot_guard, | ||||||
|  |     Err(fatal_err) => { | ||||||
|  |       error!("critical configuration error detected, unable to restart worker: {:?}", fatal_err); | ||||||
|  | 
 | ||||||
|  |       // TODO: we should show a "Reload & Retry" button in the troubleshooter to retry
 | ||||||
|  |       // loading the configuration and:
 | ||||||
|  |       //   - if the configuration is good -> start the worker
 | ||||||
|  |       //   - if the configuration is bad -> show the window again
 | ||||||
|  |       // Until we have this logic, we choose the safest option and kill the daemon
 | ||||||
|  |       // (otherwise, we would have a dangling daemon running after closing the troubleshooting).
 | ||||||
|  | 
 | ||||||
|  |       unimplemented!(); | ||||||
|  |       return; | ||||||
|  |     }, | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|   let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string()); |   let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string()); | ||||||
|   command.args(&["worker", "--monitor-daemon"]); |   command.args(&["worker", "--monitor-daemon"]); | ||||||
|   command.env( |   command.with_paths_overrides(paths_overrides); | ||||||
|     "ESPANSO_CONFIG_DIR", |  | ||||||
|     paths.config.to_string_lossy().to_string(), |  | ||||||
|   ); |  | ||||||
|   command.env( |  | ||||||
|     "ESPANSO_PACKAGE_DIR", |  | ||||||
|     paths.packages.to_string_lossy().to_string(), |  | ||||||
|   ); |  | ||||||
|   command.env( |  | ||||||
|     "ESPANSO_RUNTIME_DIR", |  | ||||||
|     paths.runtime.to_string_lossy().to_string(), |  | ||||||
|   ); |  | ||||||
| 
 |  | ||||||
|   // TODO: investigate if this is needed here, especially when invoking a form
 |  | ||||||
|   // // On windows, we need to spawn the process as "Detached"
 |  | ||||||
|   // #[cfg(target_os = "windows")]
 |  | ||||||
|   // {
 |  | ||||||
|   //   use std::os::windows::process::CommandExt;
 |  | ||||||
|   //   //command.creation_flags(0x08000008); // CREATE_NO_WINDOW + DETACHED_PROCESS
 |  | ||||||
|   // }
 |  | ||||||
| 
 | 
 | ||||||
|   let mut child = command.spawn().expect("unable to spawn worker process"); |   let mut child = command.spawn().expect("unable to spawn worker process"); | ||||||
| 
 | 
 | ||||||
|  | @ -243,6 +258,8 @@ fn spawn_worker(paths: &Paths, exit_notify: Sender<i32>) { | ||||||
|   std::thread::Builder::new() |   std::thread::Builder::new() | ||||||
|     .name("worker-status-monitor".to_string()) |     .name("worker-status-monitor".to_string()) | ||||||
|     .spawn(move || { |     .spawn(move || { | ||||||
|  |       let _guard = troubleshoot_guard; | ||||||
|  | 
 | ||||||
|       let result = child.wait(); |       let result = child.wait(); | ||||||
|       if let Ok(status) = result { |       if let Ok(status) = result { | ||||||
|         if let Some(code) = status.code() { |         if let Some(code) = status.code() { | ||||||
|  |  | ||||||
							
								
								
									
										102
									
								
								espanso/src/cli/daemon/troubleshoot.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								espanso/src/cli/daemon/troubleshoot.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,102 @@ | ||||||
|  | /* | ||||||
|  |  * 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::process::{Child, Command}; | ||||||
|  | 
 | ||||||
|  | use anyhow::{Result, bail}; | ||||||
|  | use espanso_path::Paths; | ||||||
|  | 
 | ||||||
|  | use crate::cli::util::CommandExt; | ||||||
|  | use crate::cli::PathsOverrides; | ||||||
|  | use crate::config::ConfigLoadResult; | ||||||
|  | use crate::error_eprintln; | ||||||
|  | use crate::preferences::Preferences; | ||||||
|  | 
 | ||||||
|  | pub fn launch_troubleshoot( | ||||||
|  |   paths_overrides: &PathsOverrides, | ||||||
|  | ) -> Result<TroubleshootGuard> { | ||||||
|  |   let espanso_exe_path = std::env::current_exe()?; | ||||||
|  |   let mut command = Command::new(&espanso_exe_path.to_string_lossy().to_string()); | ||||||
|  |   command.args(&["modulo", "troubleshoot"]); | ||||||
|  |   command.with_paths_overrides(paths_overrides); | ||||||
|  | 
 | ||||||
|  |   let child = command.spawn()?; | ||||||
|  | 
 | ||||||
|  |   Ok(TroubleshootGuard::new(child)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn launch_troubleshoot_blocking(paths_overrides: &PathsOverrides) -> Result<()> { | ||||||
|  |   let mut guard = launch_troubleshoot(paths_overrides)?; | ||||||
|  |   guard.wait()?; | ||||||
|  |   Ok(()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub struct TroubleshootGuard { | ||||||
|  |   child: Child, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl TroubleshootGuard { | ||||||
|  |   pub fn new(child: Child) -> Self { | ||||||
|  |     Self { child } | ||||||
|  |   } | ||||||
|  |   pub fn wait(&mut self) -> Result<()> { | ||||||
|  |     self.child.wait()?; | ||||||
|  |     Ok(()) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Drop for TroubleshootGuard { | ||||||
|  |   fn drop(&mut self) { | ||||||
|  |     let _ = self.child.kill(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn load_config_or_troubleshoot( | ||||||
|  |   paths: &Paths, | ||||||
|  |   paths_overrides: &PathsOverrides, | ||||||
|  |   troubleshoot_non_fatal: bool, | ||||||
|  | ) -> Result<(ConfigLoadResult, Option<TroubleshootGuard>)> { | ||||||
|  |   match crate::load_config(&paths.config, &paths.packages) { | ||||||
|  |     Ok(load_result) => { | ||||||
|  |       let mut troubleshoot_handle = None; | ||||||
|  |       
 | ||||||
|  |       if troubleshoot_non_fatal && !load_result.non_fatal_errors.is_empty() { | ||||||
|  |         let preferences = | ||||||
|  |           crate::preferences::get_default(&paths.runtime).expect("unable to get preferences"); | ||||||
|  | 
 | ||||||
|  |         if preferences.should_display_troubleshoot_for_non_fatal_errors() { | ||||||
|  |           match launch_troubleshoot(paths_overrides) { | ||||||
|  |             Ok(handle) => { | ||||||
|  |               troubleshoot_handle = Some(handle); | ||||||
|  |             }, | ||||||
|  |             Err(err) => { | ||||||
|  |               error_eprintln!("unable to launch troubleshoot GUI: {}", err); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       Ok((load_result, troubleshoot_handle)) | ||||||
|  |     } | ||||||
|  |     Err(fatal_err) => { | ||||||
|  |       launch_troubleshoot_blocking(paths_overrides)?; | ||||||
|  | 
 | ||||||
|  |       bail!("fatal error while loading config: {}", fatal_err); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -17,10 +17,10 @@ | ||||||
|  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 |  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| use log::{error}; |  | ||||||
| use self::util::MigrationError; | use self::util::MigrationError; | ||||||
| use crate::preferences::Preferences; |  | ||||||
| use crate::exit_code::{LAUNCHER_CONFIG_DIR_POPULATION_FAILURE, LAUNCHER_SUCCESS}; | use crate::exit_code::{LAUNCHER_CONFIG_DIR_POPULATION_FAILURE, LAUNCHER_SUCCESS}; | ||||||
|  | use crate::preferences::Preferences; | ||||||
|  | use log::error; | ||||||
| 
 | 
 | ||||||
| use super::{CliModule, CliModuleArgs}; | use super::{CliModule, CliModuleArgs}; | ||||||
| 
 | 
 | ||||||
|  | @ -49,7 +49,9 @@ fn launcher_main(args: CliModuleArgs) -> i32 { | ||||||
|   // TODO: should we create a non-gui wizard? We can also use it for the non-modulo versions of espanso
 |   // TODO: should we create a non-gui wizard? We can also use it for the non-modulo versions of espanso
 | ||||||
| 
 | 
 | ||||||
|   let paths = args.paths.expect("missing paths in launcher main"); |   let paths = args.paths.expect("missing paths in launcher main"); | ||||||
|   let paths_overrides  = args.paths_overrides.expect("missing paths overrides in launcher main"); |   let paths_overrides = args | ||||||
|  |     .paths_overrides | ||||||
|  |     .expect("missing paths overrides in launcher main"); | ||||||
|   let icon_paths = crate::icon::load_icon_paths(&paths.runtime).expect("unable to load icon paths"); |   let icon_paths = crate::icon::load_icon_paths(&paths.runtime).expect("unable to load icon paths"); | ||||||
| 
 | 
 | ||||||
|   let preferences = |   let preferences = | ||||||
|  | @ -103,9 +105,8 @@ fn launcher_main(args: CliModuleArgs) -> i32 { | ||||||
|   } else { |   } else { | ||||||
|     false |     false | ||||||
|   }; |   }; | ||||||
|   let is_accessibility_enabled_handler = Box::new(move || { |   let is_accessibility_enabled_handler = | ||||||
|     accessibility::is_accessibility_enabled() |     Box::new(move || accessibility::is_accessibility_enabled()); | ||||||
|   }); |  | ||||||
|   let enable_accessibility_handler = Box::new(move || { |   let enable_accessibility_handler = Box::new(move || { | ||||||
|     accessibility::prompt_enable_accessibility(); |     accessibility::prompt_enable_accessibility(); | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
| 
 | 
 | ||||||
| use clap::ArgMatches; | use clap::ArgMatches; | ||||||
| use espanso_config::{config::ConfigStore, matches::store::MatchStore}; | use espanso_config::{config::ConfigStore, error::NonFatalErrorSet, matches::store::MatchStore}; | ||||||
| use espanso_path::Paths; | use espanso_path::Paths; | ||||||
| 
 | 
 | ||||||
| pub mod daemon; | pub mod daemon; | ||||||
|  | @ -73,6 +73,7 @@ pub struct CliModuleArgs { | ||||||
|   pub config_store: Option<Box<dyn ConfigStore>>, |   pub config_store: Option<Box<dyn ConfigStore>>, | ||||||
|   pub match_store: Option<Box<dyn MatchStore>>, |   pub match_store: Option<Box<dyn MatchStore>>, | ||||||
|   pub is_legacy_config: bool, |   pub is_legacy_config: bool, | ||||||
|  |   pub non_fatal_errors: Vec<NonFatalErrorSet>, | ||||||
|   pub paths: Option<Paths>, |   pub paths: Option<Paths>, | ||||||
|   pub paths_overrides: Option<PathsOverrides>, |   pub paths_overrides: Option<PathsOverrides>, | ||||||
|   pub cli_args: Option<ArgMatches<'static>>, |   pub cli_args: Option<ArgMatches<'static>>, | ||||||
|  | @ -84,6 +85,7 @@ impl Default for CliModuleArgs { | ||||||
|       config_store: None, |       config_store: None, | ||||||
|       match_store: None, |       match_store: None, | ||||||
|       is_legacy_config: false, |       is_legacy_config: false, | ||||||
|  |       non_fatal_errors: Vec::new(), | ||||||
|       paths: None, |       paths: None, | ||||||
|       paths_overrides: None, |       paths_overrides: None, | ||||||
|       cli_args: None, |       cli_args: None, | ||||||
|  |  | ||||||
|  | @ -25,6 +25,8 @@ mod form; | ||||||
| mod search; | mod search; | ||||||
| #[cfg(feature = "modulo")] | #[cfg(feature = "modulo")] | ||||||
| mod welcome; | mod welcome; | ||||||
|  | #[cfg(feature = "modulo")] | ||||||
|  | mod troubleshoot; | ||||||
| 
 | 
 | ||||||
| pub fn new() -> CliModule { | pub fn new() -> CliModule { | ||||||
|   #[allow(clippy::needless_update)] |   #[allow(clippy::needless_update)] | ||||||
|  | @ -55,6 +57,10 @@ fn modulo_main(args: CliModuleArgs) -> i32 { | ||||||
|     return welcome::welcome_main(&paths, &icon_paths); |     return welcome::welcome_main(&paths, &icon_paths); | ||||||
|   } |   } | ||||||
|   
 |   
 | ||||||
|  |   if let Some(_) = cli_args.subcommand_matches("troubleshoot") { | ||||||
|  |     return troubleshoot::troubleshoot_main(&paths, &icon_paths); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   0 |   0 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										105
									
								
								espanso/src/cli/modulo/troubleshoot.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								espanso/src/cli/modulo/troubleshoot.rs
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,105 @@ | ||||||
|  | /* | ||||||
|  |  * This file is part of espanso. | ||||||
|  |  * | ||||||
|  |  * Copyright (C) 2019-2 file: (), errors: ()  file: (), errors: () 021 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::path::{Path}; | ||||||
|  | 
 | ||||||
|  | use crate::icon::IconPaths; | ||||||
|  | use crate::preferences::Preferences; | ||||||
|  | use espanso_modulo::troubleshooting::{TroubleshootingHandlers, TroubleshootingOptions}; | ||||||
|  | use espanso_path::Paths; | ||||||
|  | 
 | ||||||
|  | pub fn troubleshoot_main(paths: &Paths, icon_paths: &IconPaths) -> i32 { | ||||||
|  |   let preferences = | ||||||
|  |     crate::preferences::get_default(&paths.runtime).expect("unable to initialize preferences"); | ||||||
|  | 
 | ||||||
|  |   let dont_show_again_handler = Box::new(move |dont_show: bool| { | ||||||
|  |     preferences.set_should_display_troubleshoot_for_non_fatal_errors(!dont_show); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   let open_file_handler = Box::new(move |file_path: &Path| { | ||||||
|  |     if let Err(err) = opener::open(file_path) { | ||||||
|  |       eprintln!("error opening file: {}", err); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   let (is_fatal_error, error_sets) = match crate::config::load_config(&paths.config, &paths.packages) | ||||||
|  |   { | ||||||
|  |     Ok(config_result) => { | ||||||
|  |       let error_sets = config_result | ||||||
|  |         .non_fatal_errors | ||||||
|  |         .into_iter() | ||||||
|  |         .map(|error_set| espanso_modulo::troubleshooting::ErrorSet { | ||||||
|  |           file: Some(error_set.file), | ||||||
|  |           errors: error_set | ||||||
|  |             .errors | ||||||
|  |             .into_iter() | ||||||
|  |             .map(|error| espanso_modulo::troubleshooting::ErrorRecord { | ||||||
|  |               level: match error.level { | ||||||
|  |                 espanso_config::error::ErrorLevel::Error => { | ||||||
|  |                   espanso_modulo::troubleshooting::ErrorLevel::Error | ||||||
|  |                 } | ||||||
|  |                 espanso_config::error::ErrorLevel::Warning => { | ||||||
|  |                   espanso_modulo::troubleshooting::ErrorLevel::Warning | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |               message: format!("{:?}", error.error), | ||||||
|  |             }) | ||||||
|  |             .collect(), | ||||||
|  |         }) | ||||||
|  |         .collect(); | ||||||
|  | 
 | ||||||
|  |       (false, error_sets) | ||||||
|  |     } | ||||||
|  |     Err(err) => { | ||||||
|  |       let message = format!("{:?}", err); | ||||||
|  |       let file_path = if message.contains("default.yml") { | ||||||
|  |         let default_file_path = paths.config.join("config").join("default.yml"); | ||||||
|  |         Some(default_file_path) | ||||||
|  |       } else { | ||||||
|  |         None | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       ( | ||||||
|  |         true, | ||||||
|  |         vec![espanso_modulo::troubleshooting::ErrorSet { | ||||||
|  |           file: file_path, | ||||||
|  |           errors: vec![espanso_modulo::troubleshooting::ErrorRecord { | ||||||
|  |             level: espanso_modulo::troubleshooting::ErrorLevel::Error, | ||||||
|  |             message: format!("{:?}", err), | ||||||
|  |           }], | ||||||
|  |         }], | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | 
 | ||||||
|  |   espanso_modulo::troubleshooting::show(TroubleshootingOptions { | ||||||
|  |     window_icon_path: icon_paths | ||||||
|  |       .wizard_icon | ||||||
|  |       .as_ref() | ||||||
|  |       .map(|path| path.to_string_lossy().to_string()), | ||||||
|  |     error_sets, 
 | ||||||
|  |     is_fatal_error, | ||||||
|  |     handlers: TroubleshootingHandlers { | ||||||
|  |       dont_show_again_changed: Some(dont_show_again_handler), | ||||||
|  |       open_file: Some(open_file_handler), | ||||||
|  |     }, | ||||||
|  |   }).expect("troubleshoot GUI returned error"); | ||||||
|  | 
 | ||||||
|  |   0 | ||||||
|  | } | ||||||
|  | @ -17,8 +17,9 @@ | ||||||
|  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 |  * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
| use anyhow::Result; | use anyhow::{Context, Result}; | ||||||
| use log::info; | use espanso_config::{config::ConfigStore, error::{ErrorLevel, NonFatalErrorSet}, matches::store::MatchStore}; | ||||||
|  | use log::{error, info, warn}; | ||||||
| use std::path::Path; | use std::path::Path; | ||||||
| 
 | 
 | ||||||
| const DEFAULT_CONFIG_FILE_CONTENT: &str = include_str!("./res/config/default.yml"); | const DEFAULT_CONFIG_FILE_CONTENT: &str = include_str!("./res/config/default.yml"); | ||||||
|  | @ -26,7 +27,10 @@ const DEFAULT_MATCH_FILE_CONTENT: &str = include_str!("./res/config/base.yml"); | ||||||
| 
 | 
 | ||||||
| pub fn populate_default_config(config_dir: &Path) -> Result<()> { | pub fn populate_default_config(config_dir: &Path) -> Result<()> { | ||||||
|   if !config_dir.is_dir() { |   if !config_dir.is_dir() { | ||||||
|     info!("generating base configuration directory in: {:?}", config_dir); |     info!( | ||||||
|  |       "generating base configuration directory in: {:?}", | ||||||
|  |       config_dir | ||||||
|  |     ); | ||||||
|     std::fs::create_dir_all(config_dir)?; |     std::fs::create_dir_all(config_dir)?; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -46,13 +50,66 @@ pub fn populate_default_config(config_dir: &Path) -> Result<()> { | ||||||
|   let match_file = sub_match_dir.join("base.yml"); |   let match_file = sub_match_dir.join("base.yml"); | ||||||
| 
 | 
 | ||||||
|   if !default_file.is_file() { |   if !default_file.is_file() { | ||||||
|     info!("populating default.yml file with initial content: {:?}", default_file); |     info!( | ||||||
|  |       "populating default.yml file with initial content: {:?}", | ||||||
|  |       default_file | ||||||
|  |     ); | ||||||
|     std::fs::write(default_file, DEFAULT_CONFIG_FILE_CONTENT)?; |     std::fs::write(default_file, DEFAULT_CONFIG_FILE_CONTENT)?; | ||||||
|   } |   } | ||||||
|   if !match_file.is_file() { |   if !match_file.is_file() { | ||||||
|     info!("populating base.yml file with initial content: {:?}", match_file); |     info!( | ||||||
|  |       "populating base.yml file with initial content: {:?}", | ||||||
|  |       match_file | ||||||
|  |     ); | ||||||
|     std::fs::write(match_file, DEFAULT_MATCH_FILE_CONTENT)?; |     std::fs::write(match_file, DEFAULT_MATCH_FILE_CONTENT)?; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Ok(()) |   Ok(()) | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | pub struct ConfigLoadResult { | ||||||
|  |   pub config_store: Box<dyn ConfigStore>, | ||||||
|  |   pub match_store: Box<dyn MatchStore>, | ||||||
|  |   pub is_legacy_config: bool, | ||||||
|  |   pub non_fatal_errors: Vec<NonFatalErrorSet>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn load_config(config_path: &Path, packages_path: &Path) -> Result<ConfigLoadResult> { | ||||||
|  |   if espanso_config::is_legacy_config(&config_path) { | ||||||
|  |     let (config_store, match_store) = espanso_config::load_legacy(&config_path, &packages_path) | ||||||
|  |       .context("unable to load legacy config")?; | ||||||
|  | 
 | ||||||
|  |     Ok(ConfigLoadResult { | ||||||
|  |       config_store, | ||||||
|  |       match_store, | ||||||
|  |       is_legacy_config: true, | ||||||
|  |       non_fatal_errors: Vec::new(), | ||||||
|  |     }) | ||||||
|  |   } else { | ||||||
|  |     let (config_store, match_store, non_fatal_errors) = | ||||||
|  |       espanso_config::load(&config_path).context("unable to load config")?; | ||||||
|  | 
 | ||||||
|  |     // TODO: add an option to avoid dumping the errors in the logs
 | ||||||
|  |     if !non_fatal_errors.is_empty() { | ||||||
|  |       warn!("------- detected some errors in the configuration: -------"); | ||||||
|  |       for non_fatal_error_set in &non_fatal_errors { | ||||||
|  |         warn!(">>> {}", non_fatal_error_set.file.to_string_lossy().to_string()); | ||||||
|  |         for record in &non_fatal_error_set.errors { | ||||||
|  |           if record.level == ErrorLevel::Error { | ||||||
|  |             error!("{:?}", record.error); | ||||||
|  |           } else { | ||||||
|  |             warn!("{:?}", record.error); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       warn!("-----------------------------------------------------------"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     Ok(ConfigLoadResult { | ||||||
|  |       config_store, | ||||||
|  |       match_store, | ||||||
|  |       is_legacy_config: false, | ||||||
|  |       non_fatal_errors, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ pub const DAEMON_SUCCESS: i32 = 0; | ||||||
| pub const DAEMON_ALREADY_RUNNING: i32 = 1; | pub const DAEMON_ALREADY_RUNNING: i32 = 1; | ||||||
| pub const DAEMON_GENERAL_ERROR: i32 = 2; | pub const DAEMON_GENERAL_ERROR: i32 = 2; | ||||||
| pub const DAEMON_LEGACY_ALREADY_RUNNING: i32 = 3; | pub const DAEMON_LEGACY_ALREADY_RUNNING: i32 = 3; | ||||||
|  | pub const DAEMON_FATAL_CONFIG_ERROR: i32 = 4; | ||||||
| 
 | 
 | ||||||
| pub const MIGRATE_SUCCESS: i32 = 0; | pub const MIGRATE_SUCCESS: i32 = 0; | ||||||
| pub const MIGRATE_ALREADY_NEW_FORMAT: i32 = 1; | pub const MIGRATE_ALREADY_NEW_FORMAT: i32 = 1; | ||||||
|  |  | ||||||
|  | @ -33,7 +33,10 @@ use simplelog::{ | ||||||
|   CombinedLogger, ConfigBuilder, LevelFilter, SharedLogger, TermLogger, TerminalMode, WriteLogger, |   CombinedLogger, ConfigBuilder, LevelFilter, SharedLogger, TermLogger, TerminalMode, WriteLogger, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| use crate::cli::{LogMode, PathsOverrides}; | use crate::{ | ||||||
|  |   cli::{LogMode, PathsOverrides}, | ||||||
|  |   config::load_config, | ||||||
|  | }; | ||||||
| 
 | 
 | ||||||
| mod capabilities; | mod capabilities; | ||||||
| mod cli; | mod cli; | ||||||
|  | @ -218,6 +221,7 @@ fn main() { | ||||||
|                 .help("Interpret the input data as JSON"), |                 .help("Interpret the input data as JSON"), | ||||||
|             ), |             ), | ||||||
|         ) |         ) | ||||||
|  |         .subcommand(SubCommand::with_name("troubleshoot").about("Display the troubleshooting GUI")) | ||||||
|         .subcommand(SubCommand::with_name("welcome").about("Display the welcome screen")), |         .subcommand(SubCommand::with_name("welcome").about("Display the welcome screen")), | ||||||
|     ) |     ) | ||||||
|     // .subcommand(SubCommand::with_name("start")
 |     // .subcommand(SubCommand::with_name("start")
 | ||||||
|  | @ -441,23 +445,15 @@ fn main() { | ||||||
|       info!("using runtime dir: {:?}", paths.runtime); |       info!("using runtime dir: {:?}", paths.runtime); | ||||||
| 
 | 
 | ||||||
|       if handler.requires_config { |       if handler.requires_config { | ||||||
|         let (config_store, match_store, is_legacy_config) = |         let config_result = | ||||||
|           if espanso_config::is_legacy_config(&paths.config) { |           load_config(&paths.config, &paths.packages).expect("unable to load config"); | ||||||
|             let (config_store, match_store) = |  | ||||||
|               espanso_config::load_legacy(&paths.config, &paths.packages) |  | ||||||
|                 .expect("unable to load legacy config"); |  | ||||||
|             (config_store, match_store, true) |  | ||||||
|           } else { |  | ||||||
|             let (config_store, match_store) = |  | ||||||
|               espanso_config::load(&paths.config).expect("unable to load config"); |  | ||||||
|             (config_store, match_store, false) |  | ||||||
|           }; |  | ||||||
| 
 | 
 | ||||||
|         cli_args.is_legacy_config = is_legacy_config; |         cli_args.is_legacy_config = config_result.is_legacy_config; | ||||||
|         cli_args.config_store = Some(config_store); |         cli_args.config_store = Some(config_result.config_store); | ||||||
|         cli_args.match_store = Some(match_store); |         cli_args.match_store = Some(config_result.match_store); | ||||||
|  |         cli_args.non_fatal_errors = config_result.non_fatal_errors; | ||||||
| 
 | 
 | ||||||
|         if is_legacy_config { |         if config_result.is_legacy_config { | ||||||
|           warn!("espanso is reading the configuration using compatibility mode, thus some features might not be available"); |           warn!("espanso is reading the configuration using compatibility mode, thus some features might not be available"); | ||||||
|           warn!("you can migrate to the new configuration format by running 'espanso migrate' in a terminal"); |           warn!("you can migrate to the new configuration format by running 'espanso migrate' in a terminal"); | ||||||
|         } |         } | ||||||
|  | @ -497,7 +493,7 @@ fn get_path_override(matches: &ArgMatches, argument: &str, env_var: &str) -> Opt | ||||||
|     if path.is_dir() { |     if path.is_dir() { | ||||||
|       return Some(path); |       return Some(path); | ||||||
|     } else { |     } else { | ||||||
|       error!("{} argument was specified, but it doesn't point to a valid directory. Make sure to create it first.", argument); |       error_eprintln!("{} argument was specified, but it doesn't point to a valid directory. Make sure to create it first.", argument); | ||||||
|       std::process::exit(1); |       std::process::exit(1); | ||||||
|     } |     } | ||||||
|   } else if let Ok(path) = std::env::var(env_var) { |   } else if let Ok(path) = std::env::var(env_var) { | ||||||
|  | @ -505,7 +501,7 @@ fn get_path_override(matches: &ArgMatches, argument: &str, env_var: &str) -> Opt | ||||||
|     if path.is_dir() { |     if path.is_dir() { | ||||||
|       return Some(path); |       return Some(path); | ||||||
|     } else { |     } else { | ||||||
|       error!("{} env variable was specified, but it doesn't point to a valid directory. Make sure to create it first.", env_var); |       error_eprintln!("{} env variable was specified, but it doesn't point to a valid directory. Make sure to create it first.", env_var); | ||||||
|       std::process::exit(1); |       std::process::exit(1); | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|  |  | ||||||
|  | @ -26,6 +26,8 @@ use super::Preferences; | ||||||
| 
 | 
 | ||||||
| const HAS_COMPLETED_WIZARD_KEY: &str = "has_completed_wizard"; | const HAS_COMPLETED_WIZARD_KEY: &str = "has_completed_wizard"; | ||||||
| const SHOULD_DISPLAY_WELCOME_KEY: &str = "should_display_welcome"; | const SHOULD_DISPLAY_WELCOME_KEY: &str = "should_display_welcome"; | ||||||
|  | const SHOULD_DISPLAY_TROUBLESHOOT_FOR_NON_FATAL_ERRORS: &str = | ||||||
|  |   "should_display_troubleshoot_for_non_fatal_errors"; | ||||||
| 
 | 
 | ||||||
| #[derive(Clone)] | #[derive(Clone)] | ||||||
| pub struct DefaultPreferences<KVSType: KVS> { | pub struct DefaultPreferences<KVSType: KVS> { | ||||||
|  | @ -69,4 +71,12 @@ impl<KVSType: KVS> Preferences for DefaultPreferences<KVSType> { | ||||||
|   fn set_should_display_welcome(&self, value: bool) { |   fn set_should_display_welcome(&self, value: bool) { | ||||||
|     self.set(SHOULD_DISPLAY_WELCOME_KEY, value); |     self.set(SHOULD_DISPLAY_WELCOME_KEY, value); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   fn should_display_troubleshoot_for_non_fatal_errors(&self) -> bool { | ||||||
|  |     self.get(SHOULD_DISPLAY_TROUBLESHOOT_FOR_NON_FATAL_ERRORS).unwrap_or(true) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   fn set_should_display_troubleshoot_for_non_fatal_errors(&self, value: bool) { | ||||||
|  |     self.set(SHOULD_DISPLAY_TROUBLESHOOT_FOR_NON_FATAL_ERRORS, value); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -28,6 +28,9 @@ pub trait Preferences: Send + Sync + Clone { | ||||||
| 
 | 
 | ||||||
|   fn should_display_welcome(&self) -> bool; |   fn should_display_welcome(&self) -> bool; | ||||||
|   fn set_should_display_welcome(&self, value: bool); |   fn set_should_display_welcome(&self, value: bool); | ||||||
|  | 
 | ||||||
|  |   fn should_display_troubleshoot_for_non_fatal_errors(&self) -> bool; | ||||||
|  |   fn set_should_display_troubleshoot_for_non_fatal_errors(&self, value: bool); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub fn get_default(runtime_dir: &Path) -> Result<impl Preferences> { | pub fn get_default(runtime_dir: &Path) -> Result<impl Preferences> { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user