feat(core): implement env-path cli module on macOS

This commit is contained in:
Federico Terzi 2021-06-26 18:55:05 +02:00
parent 8fb3826298
commit 8f99c6ce16
8 changed files with 307 additions and 56 deletions

View File

@ -0,0 +1,70 @@
/*
* 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 crate::exit_code::{ADD_TO_PATH_FAILURE, ADD_TO_PATH_SUCCESS};
use super::{CliModule, CliModuleArgs};
use log::error;
pub fn new() -> CliModule {
CliModule {
enable_logs: true,
disable_logs_terminal_output: true,
requires_paths: true,
log_mode: super::LogMode::AppendOnly,
subcommand: "env-path".to_string(),
entry: env_path_main,
..Default::default()
}
}
fn env_path_main(args: CliModuleArgs) -> i32 {
let paths = args.paths.expect("missing paths argument");
let cli_args = args.cli_args.expect("missing cli_args");
let elevated_priviledge_prompt = cli_args.is_present("prompt");
if cli_args.subcommand_matches("register").is_some() {
if let Err(error) = crate::path::add_espanso_to_path(elevated_priviledge_prompt) {
error_print_and_log(&format!(
"Unable to add 'espanso' command to PATH: {}",
error
));
return ADD_TO_PATH_FAILURE;
}
} else if cli_args.subcommand_matches("unregister").is_some() {
if let Err(error) = crate::path::remove_espanso_from_path(elevated_priviledge_prompt) {
error_print_and_log(&format!(
"Unable to remove 'espanso' command from PATH: {}",
error
));
return ADD_TO_PATH_FAILURE;
}
} else {
eprintln!("Please specify a subcommand, either `espanso env-path register` to add the 'espanso' command or `espanso env-path unregister` to remove it");
return ADD_TO_PATH_FAILURE;
}
ADD_TO_PATH_SUCCESS
}
fn error_print_and_log(msg: &str) {
error!("{}", msg);
eprintln!("{}", msg);
}

View File

@ -130,6 +130,8 @@ fn launcher_main(args: CliModuleArgs) -> i32 {
preferences.set_completed_wizard(true);
}
// TODO: initialize config directory if not present
0
}

View File

@ -17,16 +17,9 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{path::PathBuf, sync::Mutex};
use std::{path::PathBuf};
use crate::{
exit_code::{
MIGRATE_ALREADY_NEW_FORMAT, MIGRATE_CLEAN_FAILURE, MIGRATE_DIRTY_FAILURE,
MIGRATE_LEGACY_INSTANCE_RUNNING, MIGRATE_SUCCESS, MIGRATE_UNEXPECTED_FAILURE,
MIGRATE_USER_ABORTED,
},
lock::acquire_legacy_lock,
};
use crate::{exit_code::{MIGRATE_ALREADY_NEW_FORMAT, MIGRATE_CLEAN_FAILURE, MIGRATE_DIRTY_FAILURE, MIGRATE_LEGACY_INSTANCE_RUNNING, MIGRATE_SUCCESS, MIGRATE_USER_ABORTED, configure_custom_panic_hook, update_panic_exit_code}, lock::acquire_legacy_lock};
use super::{CliModule, CliModuleArgs};
use colored::*;
@ -35,10 +28,6 @@ use fs_extra::dir::CopyOptions;
use log::{error, info};
use tempdir::TempDir;
lazy_static! {
static ref CURRENT_PANIC_EXIT_CODE: Mutex<i32> = Mutex::new(MIGRATE_UNEXPECTED_FAILURE);
}
pub fn new() -> CliModule {
CliModule {
enable_logs: true,
@ -188,48 +177,6 @@ fn find_available_backup_dir() -> PathBuf {
panic!("could not generate valid backup directory");
}
fn configure_custom_panic_hook() {
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
(*previous_hook)(info);
// Part of this code is taken from the "rust-log-panics" crate
let thread = std::thread::current();
let thread = thread.name().unwrap_or("<unnamed>");
let msg = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &**s,
None => "Box<Any>",
},
};
match info.location() {
Some(location) => {
eprintln!(
"ERROR: '{}' panicked at '{}': {}:{}",
thread,
msg,
location.file(),
location.line(),
);
}
None => eprintln!("ERROR: '{}' panicked at '{}'", thread, msg,),
}
let exit_code = CURRENT_PANIC_EXIT_CODE.lock().unwrap();
std::process::exit(*exit_code);
}));
}
fn update_panic_exit_code(exit_code: i32) {
let mut lock = CURRENT_PANIC_EXIT_CODE
.lock()
.expect("unable to update panic exit code");
*lock = exit_code;
}
fn error_print_and_log(msg: &str) {
error!("{}", msg);
eprintln!("{}", msg);

View File

@ -22,6 +22,7 @@ use espanso_config::{config::ConfigStore, matches::store::MatchStore};
use espanso_path::Paths;
pub mod daemon;
pub mod env_path;
pub mod launcher;
pub mod log;
pub mod migrate;

View File

@ -35,4 +35,56 @@ pub const MIGRATE_LEGACY_INSTANCE_RUNNING: i32 = 2;
pub const MIGRATE_USER_ABORTED: i32 = 3;
pub const MIGRATE_CLEAN_FAILURE: i32 = 50;
pub const MIGRATE_DIRTY_FAILURE: i32 = 51;
pub const MIGRATE_UNEXPECTED_FAILURE: i32 = 101;
pub const MIGRATE_UNEXPECTED_FAILURE: i32 = 101;
pub const ADD_TO_PATH_SUCCESS: i32 = 0;
pub const ADD_TO_PATH_FAILURE: i32 = 1;
use std::sync::Mutex;
lazy_static! {
static ref CURRENT_PANIC_EXIT_CODE: Mutex<i32> = Mutex::new(MIGRATE_UNEXPECTED_FAILURE);
}
pub fn configure_custom_panic_hook() {
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
(*previous_hook)(info);
// Part of this code is taken from the "rust-log-panics" crate
let thread = std::thread::current();
let thread = thread.name().unwrap_or("<unnamed>");
let msg = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &**s,
None => "Box<Any>",
},
};
match info.location() {
Some(location) => {
eprintln!(
"ERROR: '{}' panicked at '{}': {}:{}",
thread,
msg,
location.file(),
location.line(),
);
}
None => eprintln!("ERROR: '{}' panicked at '{}'", thread, msg,),
}
let exit_code = CURRENT_PANIC_EXIT_CODE.lock().unwrap();
std::process::exit(*exit_code);
}));
}
pub fn update_panic_exit_code(exit_code: i32) {
let mut lock = CURRENT_PANIC_EXIT_CODE
.lock()
.expect("unable to update panic exit code");
*lock = exit_code;
}

View File

@ -43,6 +43,7 @@ mod icon;
mod ipc;
mod lock;
mod logging;
mod path;
mod preferences;
mod util;
@ -58,6 +59,7 @@ lazy_static! {
cli::daemon::new(),
cli::modulo::new(),
cli::migrate::new(),
cli::env_path::new(),
];
}
@ -121,6 +123,25 @@ fn main() {
.takes_value(true)
.help("Specify a custom path for the espanso runtime directory"),
)
.subcommand(
SubCommand::with_name("env-path")
.arg(
Arg::with_name("prompt")
.long("prompt")
.required(false)
.takes_value(false)
.help("Prompt for permissions if the operation requires elevated privileges."),
)
.subcommand(
SubCommand::with_name("register")
.about("Add 'espanso' command to PATH"),
)
.subcommand(
SubCommand::with_name("unregister")
.about("Remove 'espanso' command from PATH"),
)
.about("Add or remove the 'espanso' command from the PATH (macOS and Windows only)"),
)
// .subcommand(SubCommand::with_name("cmd")
// .about("Send a command to the espanso daemon.")
// .subcommand(SubCommand::with_name("exit")

128
espanso/src/path/macos.rs Normal file
View File

@ -0,0 +1,128 @@
/*
* 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::io::ErrorKind;
use std::path::PathBuf;
use thiserror::Error;
use anyhow::Result;
use log::{error, warn};
pub fn is_espanso_in_path() -> bool {
PathBuf::from("/usr/local/bin/espanso").is_file()
}
pub fn add_espanso_to_path(prompt_when_necessary: bool) -> Result<()> {
let target_link_dir = PathBuf::from("/usr/local/bin");
let exec_path = std::env::current_exe()?;
if !target_link_dir.is_dir() {
return Err(PathError::UsrLocalBinDirDoesNotExist.into());
}
let target_link_path = target_link_dir.join("espanso");
if let Err(error) = std::os::unix::fs::symlink(&exec_path, &target_link_path) {
match error.kind() {
ErrorKind::PermissionDenied => {
if prompt_when_necessary {
warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript.");
let params = format!(
r##"do shell script "mkdir -p /usr/local/bin && ln -sf '{}' '{}'" with administrator privileges"##,
exec_path.to_string_lossy(),
target_link_path.to_string_lossy(),
);
let mut child = std::process::Command::new("osascript").args(&[
"-e",
&params,
]).spawn()?;
let result = child.wait()?;
if !result.success() {
return Err(PathError::ElevationRequestFailure.into());
}
} else {
return Err(PathError::SymlinkError(error).into());
}
}
other_error => {
return Err(PathError::SymlinkError(error).into());
}
}
}
Ok(())
}
pub fn remove_espanso_from_path(prompt_when_necessary: bool) -> Result<()> {
let target_link_path = PathBuf::from("/usr/local/bin/espanso");
if !target_link_path.is_file() {
return Err(PathError::SymlinkNotFound.into());
}
if let Err(error) = std::fs::remove_file(&target_link_path) {
match error.kind() {
ErrorKind::PermissionDenied => {
if prompt_when_necessary {
warn!("target link file can't be accessed with current permissions, requesting elevated ones through AppleScript.");
let params = format!(
r##"do shell script "rm '{}'" with administrator privileges"##,
target_link_path.to_string_lossy(),
);
let mut child = std::process::Command::new("osascript").args(&[
"-e",
&params,
]).spawn()?;
let result = child.wait()?;
if !result.success() {
return Err(PathError::ElevationRequestFailure.into());
}
} else {
return Err(PathError::SymlinkError(error).into());
}
}
other_error => {
return Err(PathError::SymlinkError(error).into());
}
}
}
Ok(())
}
#[derive(Error, Debug)]
pub enum PathError {
#[error("/usr/local/bin directory doesn't exist")]
UsrLocalBinDirDoesNotExist,
#[error("symlink error: `{0}`")]
SymlinkError(std::io::Error),
#[error("elevation request failed")]
ElevationRequestFailure,
#[error("symlink does not exist, so there is nothing to remove.")]
SymlinkNotFound,
}

30
espanso/src/path/mod.rs Normal file
View File

@ -0,0 +1,30 @@
/*
* 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/>.
*/
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
pub use macos::add_espanso_to_path;
#[cfg(target_os = "macos")]
pub use macos::remove_espanso_from_path;
#[cfg(target_os = "macos")]
pub use macos::is_espanso_in_path;
// TODO: add Linux stub