From 461389c9503fff4107a427da3f54d30e4d1840b8 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Mon, 15 Jul 2024 15:09:12 +0200 Subject: [PATCH 01/13] feature: When file not found for alumet exec => better explanation --- app-agent/src/main.rs | 189 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 app-agent/src/main.rs diff --git a/app-agent/src/main.rs b/app-agent/src/main.rs new file mode 100644 index 0000000..afa817b --- /dev/null +++ b/app-agent/src/main.rs @@ -0,0 +1,189 @@ +use std::{env, path::PathBuf, process, time::Duration}; + +use alumet::{ + agent::{static_plugins, Agent, AgentBuilder, AgentConfig}, + plugin::{ + event::{self, StartConsumerMeasurement}, + rust::InvalidConfig, + }, + resources::ResourceConsumer, +}; + +use clap::{Args, Parser, Subcommand}; +use env_logger::Env; + +use plugin_csv::CsvPlugin; +use plugin_perf::PerfPlugin; +use plugin_rapl::RaplPlugin; +use plugin_socket_control::SocketControlPlugin; +use serde::{Deserialize, Serialize}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +fn main() { + env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + log::info!("Starting ALUMET agent v{VERSION}"); + // Print a warning if we are running in debug mode. + #[cfg(debug_assertions)] + { + log::warn!("DEBUG assertions are enabled, this build of Alumet is fine for debugging, but not for production."); + } + + // Parse command-line arguments. + let args = Cli::parse(); + + // Specifies the plugins that we want to load. + let plugins = static_plugins![RaplPlugin, CsvPlugin, PerfPlugin, SocketControlPlugin]; + + // Build the measurement agent. + let mut agent = AgentBuilder::new(plugins) + .config_path(&args.config) + .default_app_config(AppConfig::default()) + .build(); + + // CLI option + let cmd = args.command.clone().unwrap_or(Commands::Run); + + if matches!(cmd, Commands::RegenConfig) { + // Regenerate config and stop. + agent + .write_default_config() + .expect("failed to (re)generate the configuration file"); + log::info!("Configuration file (re)generated."); + return; + } + + // Load the config. + let mut agent_config = agent.load_config().unwrap(); + apply_config(&mut agent, &mut agent_config, args); + + // Start the measurement. + let running_agent = agent.start(agent_config).unwrap_or_else(|err| { + log::error!("{err:?}"); + if let Some(_) = err.downcast_ref::() { + log::error!("HINT: You could try to regenerate the configuration by running `{} regen-config` (use --help to get more information).", env!("CARGO_BIN_NAME")); + } + panic!("ALUMET agent failed to start: {err}"); + }); + + // Keep the pipeline running until... + match cmd { + Commands::Run => { + // ...the program stops (on SIGTERM or on a "stop" command). + running_agent.wait_for_shutdown(Duration::MAX).unwrap(); + } + Commands::Exec(ExecArgs { + program: external_command, + args, + }) => { + // ...another process, that we'll launch now, exits. + + // Spawn the process. + let p_unw = process::Command::new(external_command.clone()) + .args(args) + .spawn(); + let mut p = match p_unw { + Ok(val) => val, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => { + let current_dir = match env::current_dir() { + Ok(current_dir) => current_dir, + Err(_) => PathBuf::from("Unable to retrieve current dir"), + }; + log::error!("File: {:?} not found in current directory: {:?}", external_command, current_dir); + panic!("Maybe you could change the path to match with the correct one") + } + _ => { + panic!("Error in child process"); + } + }, + }; + + // Notify the plugins that there is a process to observe. + let pid = p.id(); + log::info!("Child process '{external_command}' spawned with pid {pid}."); + event::start_consumer_measurement() + .publish(StartConsumerMeasurement(vec![ResourceConsumer::Process { pid }])); + + // Wait for the process to terminate. + let status = p.wait().expect("failed to wait for child process"); + log::info!("Child process exited with status {status}, Alumet will now stop."); + + // Stop the pipeline. + running_agent.pipeline.control_handle().shutdown(); + running_agent.wait_for_shutdown(Duration::MAX).unwrap(); + } + Commands::RegenConfig => unreachable!(), + } + log::info!("ALUMET agent has stopped.") +} + +/// Applies the configuration (file + arguments). +fn apply_config(agent: &mut Agent, global_config: &mut AgentConfig, cli_args: Cli) { + // Apply the config file + let app_config: AppConfig = global_config.take_app_config().try_into().unwrap(); + agent.sources_max_update_interval(app_config.max_update_interval); + + // Apply the CLI args (they override the file) + if let Some(max_update_interval) = cli_args.max_update_interval { + agent.sources_max_update_interval(max_update_interval); + } +} + +/// Structure of the config file, excluding plugin configs. +#[derive(Deserialize, Serialize)] +struct AppConfig { + #[serde(with = "humantime_serde")] + max_update_interval: Duration, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + max_update_interval: Duration::from_millis(500), + } + } +} + +/// Command line arguments. +#[derive(Parser)] +struct Cli { + #[command(subcommand)] + command: Option, + + /// Path to the config file. + #[arg(long, default_value = "alumet-config.toml")] + config: String, + /// Maximum amount of time between two updates of the sources' commands. + /// + /// A lower value means that the latency of source commands will be lower, + /// i.e. commands will be applied faster, at the cost of a higher overhead. + #[arg(long, value_parser = humantime_serde::re::humantime::parse_duration)] + max_update_interval: Option, +} + +#[derive(Subcommand, Clone)] +enum Commands { + /// Run the agent and monitor the system. + /// + /// This is the default command. + Run, + + /// Execute a command and observe its process. + Exec(ExecArgs), + + /// Regenerate the configuration file and stop. + /// + /// If the file exists, it will be overwritten. + RegenConfig, +} + +#[derive(Args, Clone)] +struct ExecArgs { + /// The program to run. + program: String, + + /// Arguments to the program. + #[arg(trailing_var_arg = true)] + args: Vec, +} From 71536753d3e481234c5649ed46522bdd34269c0c Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Mon, 15 Jul 2024 17:20:39 +0200 Subject: [PATCH 02/13] feature: Modify app-agent --- app-agent/src/main.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app-agent/src/main.rs b/app-agent/src/main.rs index afa817b..62add7f 100644 --- a/app-agent/src/main.rs +++ b/app-agent/src/main.rs @@ -1,4 +1,4 @@ -use std::{env, path::PathBuf, process, time::Duration}; +use std::{process, time::Duration}; use alumet::{ agent::{static_plugins, Agent, AgentBuilder, AgentConfig}, @@ -79,18 +79,26 @@ fn main() { // ...another process, that we'll launch now, exits. // Spawn the process. - let p_unw = process::Command::new(external_command.clone()) + let p_result = process::Command::new(external_command.clone()) .args(args) .spawn(); - let mut p = match p_unw { + let mut p = match p_result { Ok(val) => val, Err(e) => match e.kind() { std::io::ErrorKind::NotFound => { - let current_dir = match env::current_dir() { - Ok(current_dir) => current_dir, - Err(_) => PathBuf::from("Unable to retrieve current dir"), - }; - log::error!("File: {:?} not found in current directory: {:?}", external_command, current_dir); + let external_cmd_vec = external_command.split(" "); + let mut final_cmd: String = String::new(); + for elem in external_cmd_vec{ + if std::fs::metadata(elem).is_ok(){ + if !elem.starts_with("./") { + let good_path_file = format!("./{}", elem); + final_cmd.push_str(&good_path_file); + } + }else{ + final_cmd.push_str(&format!("{} ",elem)); + } + } + log::error!("Error not found, maybe try with the following: {}", final_cmd); panic!("Maybe you could change the path to match with the correct one") } _ => { From 14576ce892a76fb8168dfdeeb833a62e4254ed90 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Wed, 27 Nov 2024 12:33:37 +0100 Subject: [PATCH 03/13] feat(exec_process): Improve the error message when using exec mode --- app-agent/src/exec_process.rs | 73 ++++++++++++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index 3d49be6..a57b26f 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -1,6 +1,8 @@ //! Tie Alumet to a running process. -use std::process::ExitStatus; +use std::{ + fs, os::unix::fs::PermissionsExt, process::ExitStatus +}; use alumet::{ pipeline::{ @@ -17,10 +19,71 @@ use anyhow::Context; /// Spawns a child process and waits for it to exit. pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result { // Spawn the process. - let mut p = std::process::Command::new(external_command.clone()) - .args(args) - .spawn() - .expect("error in child process"); + let p_result = std::process::Command::new(external_command.clone()) + .args(args.clone()) + .spawn(); + + let mut p = match p_result { + Ok(val) => val, + Err(e) => match e.kind() { + std::io::ErrorKind::NotFound => { + let directory_entries_iter = fs::read_dir("./").unwrap(); + let mut entry_list = String::from("Available files in the current directory:\n"); + for entry_result in directory_entries_iter { + let entry = entry_result.unwrap(); + let entry_type = entry.file_type().unwrap(); + if entry_type.is_file() { + let entry_string = entry.file_name().into_string().unwrap(); + if external_command == entry_string { + if let Ok(canonical_path) = fs::canonicalize(entry.path()) { + if let Some(parent_path) = canonical_path.parent() { + log::info!("Found corresponding file in the current directory: {:?}", parent_path); + } else { + log::info!("Found corresponding file in the current directory."); + } + } else { + log::info!("Found corresponding file in the current directory."); + } + // Check for execution permissions + let mut checkup_message = String::from(format!("Who can execute the file: {}:\n", external_command)); + if entry.metadata().unwrap().permissions().mode() & 0o100 != 0 { + checkup_message = format!("{} - User: YES\n", checkup_message); + } else { + checkup_message = format!("{} - User: NO\n", checkup_message); + } + if entry.metadata().unwrap().permissions().mode() & 0o010 != 0 { + checkup_message = format!("{} - Group: YES\n", checkup_message); + } else { + checkup_message = format!("{} - Group: NO\n", checkup_message); + } + if entry.metadata().unwrap().permissions().mode() & 0o001 != 0 { + checkup_message = format!("{} - Others: YES\n", checkup_message); + } else { + checkup_message = format!("{} - Others: NO\n", checkup_message); + } + if !external_command.starts_with("./") { + checkup_message = format!("{}\n Please try again using the following syntax:\n", checkup_message); + checkup_message = format!("{} [...] exec ./{}", checkup_message, external_command); + for argument in &args { + checkup_message = format!("{} {}", checkup_message, argument) + } + } + log::error!("{}",checkup_message); + panic!("Maybe you could change the path to match with the correct one") + } else { + // Unable to find a corresponding things to execute + entry_list = format!("{} -{}\n", entry_list, entry_string) + } + } + } + log::error!("Executable not found in the current directory, found:\n{}", entry_list); + panic!("Maybe you could change the path") + } + _ => { + panic!("Error in child process"); + } + }, + }; // Notify the plugins that there is a process to observe. let pid = p.id(); From 59c3bba507959aeae24ae708a50b52534bb34a1e Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Wed, 27 Nov 2024 12:43:24 +0100 Subject: [PATCH 04/13] feat(exec_process): Better lint --- app-agent/src/exec_process.rs | 12 +-- app-agent/src/main.rs | 197 ---------------------------------- 2 files changed, 6 insertions(+), 203 deletions(-) delete mode 100644 app-agent/src/main.rs diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index a57b26f..3eaf2cf 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -1,8 +1,6 @@ //! Tie Alumet to a running process. -use std::{ - fs, os::unix::fs::PermissionsExt, process::ExitStatus -}; +use std::{fs, os::unix::fs::PermissionsExt, process::ExitStatus}; use alumet::{ pipeline::{ @@ -45,7 +43,8 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result log::info!("Found corresponding file in the current directory."); } // Check for execution permissions - let mut checkup_message = String::from(format!("Who can execute the file: {}:\n", external_command)); + let mut checkup_message = + String::from(format!("Who can execute the file: {}:\n", external_command)); if entry.metadata().unwrap().permissions().mode() & 0o100 != 0 { checkup_message = format!("{} - User: YES\n", checkup_message); } else { @@ -62,13 +61,14 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result checkup_message = format!("{} - Others: NO\n", checkup_message); } if !external_command.starts_with("./") { - checkup_message = format!("{}\n Please try again using the following syntax:\n", checkup_message); + checkup_message = + format!("{}\n Please try again using the following syntax:\n", checkup_message); checkup_message = format!("{} [...] exec ./{}", checkup_message, external_command); for argument in &args { checkup_message = format!("{} {}", checkup_message, argument) } } - log::error!("{}",checkup_message); + log::error!("{}", checkup_message); panic!("Maybe you could change the path to match with the correct one") } else { // Unable to find a corresponding things to execute diff --git a/app-agent/src/main.rs b/app-agent/src/main.rs deleted file mode 100644 index 62add7f..0000000 --- a/app-agent/src/main.rs +++ /dev/null @@ -1,197 +0,0 @@ -use std::{process, time::Duration}; - -use alumet::{ - agent::{static_plugins, Agent, AgentBuilder, AgentConfig}, - plugin::{ - event::{self, StartConsumerMeasurement}, - rust::InvalidConfig, - }, - resources::ResourceConsumer, -}; - -use clap::{Args, Parser, Subcommand}; -use env_logger::Env; - -use plugin_csv::CsvPlugin; -use plugin_perf::PerfPlugin; -use plugin_rapl::RaplPlugin; -use plugin_socket_control::SocketControlPlugin; -use serde::{Deserialize, Serialize}; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -fn main() { - env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); - log::info!("Starting ALUMET agent v{VERSION}"); - // Print a warning if we are running in debug mode. - #[cfg(debug_assertions)] - { - log::warn!("DEBUG assertions are enabled, this build of Alumet is fine for debugging, but not for production."); - } - - // Parse command-line arguments. - let args = Cli::parse(); - - // Specifies the plugins that we want to load. - let plugins = static_plugins![RaplPlugin, CsvPlugin, PerfPlugin, SocketControlPlugin]; - - // Build the measurement agent. - let mut agent = AgentBuilder::new(plugins) - .config_path(&args.config) - .default_app_config(AppConfig::default()) - .build(); - - // CLI option - let cmd = args.command.clone().unwrap_or(Commands::Run); - - if matches!(cmd, Commands::RegenConfig) { - // Regenerate config and stop. - agent - .write_default_config() - .expect("failed to (re)generate the configuration file"); - log::info!("Configuration file (re)generated."); - return; - } - - // Load the config. - let mut agent_config = agent.load_config().unwrap(); - apply_config(&mut agent, &mut agent_config, args); - - // Start the measurement. - let running_agent = agent.start(agent_config).unwrap_or_else(|err| { - log::error!("{err:?}"); - if let Some(_) = err.downcast_ref::() { - log::error!("HINT: You could try to regenerate the configuration by running `{} regen-config` (use --help to get more information).", env!("CARGO_BIN_NAME")); - } - panic!("ALUMET agent failed to start: {err}"); - }); - - // Keep the pipeline running until... - match cmd { - Commands::Run => { - // ...the program stops (on SIGTERM or on a "stop" command). - running_agent.wait_for_shutdown(Duration::MAX).unwrap(); - } - Commands::Exec(ExecArgs { - program: external_command, - args, - }) => { - // ...another process, that we'll launch now, exits. - - // Spawn the process. - let p_result = process::Command::new(external_command.clone()) - .args(args) - .spawn(); - let mut p = match p_result { - Ok(val) => val, - Err(e) => match e.kind() { - std::io::ErrorKind::NotFound => { - let external_cmd_vec = external_command.split(" "); - let mut final_cmd: String = String::new(); - for elem in external_cmd_vec{ - if std::fs::metadata(elem).is_ok(){ - if !elem.starts_with("./") { - let good_path_file = format!("./{}", elem); - final_cmd.push_str(&good_path_file); - } - }else{ - final_cmd.push_str(&format!("{} ",elem)); - } - } - log::error!("Error not found, maybe try with the following: {}", final_cmd); - panic!("Maybe you could change the path to match with the correct one") - } - _ => { - panic!("Error in child process"); - } - }, - }; - - // Notify the plugins that there is a process to observe. - let pid = p.id(); - log::info!("Child process '{external_command}' spawned with pid {pid}."); - event::start_consumer_measurement() - .publish(StartConsumerMeasurement(vec![ResourceConsumer::Process { pid }])); - - // Wait for the process to terminate. - let status = p.wait().expect("failed to wait for child process"); - log::info!("Child process exited with status {status}, Alumet will now stop."); - - // Stop the pipeline. - running_agent.pipeline.control_handle().shutdown(); - running_agent.wait_for_shutdown(Duration::MAX).unwrap(); - } - Commands::RegenConfig => unreachable!(), - } - log::info!("ALUMET agent has stopped.") -} - -/// Applies the configuration (file + arguments). -fn apply_config(agent: &mut Agent, global_config: &mut AgentConfig, cli_args: Cli) { - // Apply the config file - let app_config: AppConfig = global_config.take_app_config().try_into().unwrap(); - agent.sources_max_update_interval(app_config.max_update_interval); - - // Apply the CLI args (they override the file) - if let Some(max_update_interval) = cli_args.max_update_interval { - agent.sources_max_update_interval(max_update_interval); - } -} - -/// Structure of the config file, excluding plugin configs. -#[derive(Deserialize, Serialize)] -struct AppConfig { - #[serde(with = "humantime_serde")] - max_update_interval: Duration, -} - -impl Default for AppConfig { - fn default() -> Self { - Self { - max_update_interval: Duration::from_millis(500), - } - } -} - -/// Command line arguments. -#[derive(Parser)] -struct Cli { - #[command(subcommand)] - command: Option, - - /// Path to the config file. - #[arg(long, default_value = "alumet-config.toml")] - config: String, - /// Maximum amount of time between two updates of the sources' commands. - /// - /// A lower value means that the latency of source commands will be lower, - /// i.e. commands will be applied faster, at the cost of a higher overhead. - #[arg(long, value_parser = humantime_serde::re::humantime::parse_duration)] - max_update_interval: Option, -} - -#[derive(Subcommand, Clone)] -enum Commands { - /// Run the agent and monitor the system. - /// - /// This is the default command. - Run, - - /// Execute a command and observe its process. - Exec(ExecArgs), - - /// Regenerate the configuration file and stop. - /// - /// If the file exists, it will be overwritten. - RegenConfig, -} - -#[derive(Args, Clone)] -struct ExecArgs { - /// The program to run. - program: String, - - /// Arguments to the program. - #[arg(trailing_var_arg = true)] - args: Vec, -} From 08b06ec1105472b2fc81aa79f4df0b60b9898d53 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Thu, 28 Nov 2024 15:07:38 +0100 Subject: [PATCH 05/13] feat(exec_process): Added a better error handling --- app-agent/src/exec_process.rs | 139 +++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 54 deletions(-) diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index 3eaf2cf..c3f5179 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -1,6 +1,6 @@ //! Tie Alumet to a running process. -use std::{fs, os::unix::fs::PermissionsExt, process::ExitStatus}; +use std::{fs::{self, File}, os::unix::fs::PermissionsExt, path::PathBuf, process::ExitStatus}; use alumet::{ pipeline::{ @@ -25,59 +25,12 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result Ok(val) => val, Err(e) => match e.kind() { std::io::ErrorKind::NotFound => { - let directory_entries_iter = fs::read_dir("./").unwrap(); - let mut entry_list = String::from("Available files in the current directory:\n"); - for entry_result in directory_entries_iter { - let entry = entry_result.unwrap(); - let entry_type = entry.file_type().unwrap(); - if entry_type.is_file() { - let entry_string = entry.file_name().into_string().unwrap(); - if external_command == entry_string { - if let Ok(canonical_path) = fs::canonicalize(entry.path()) { - if let Some(parent_path) = canonical_path.parent() { - log::info!("Found corresponding file in the current directory: {:?}", parent_path); - } else { - log::info!("Found corresponding file in the current directory."); - } - } else { - log::info!("Found corresponding file in the current directory."); - } - // Check for execution permissions - let mut checkup_message = - String::from(format!("Who can execute the file: {}:\n", external_command)); - if entry.metadata().unwrap().permissions().mode() & 0o100 != 0 { - checkup_message = format!("{} - User: YES\n", checkup_message); - } else { - checkup_message = format!("{} - User: NO\n", checkup_message); - } - if entry.metadata().unwrap().permissions().mode() & 0o010 != 0 { - checkup_message = format!("{} - Group: YES\n", checkup_message); - } else { - checkup_message = format!("{} - Group: NO\n", checkup_message); - } - if entry.metadata().unwrap().permissions().mode() & 0o001 != 0 { - checkup_message = format!("{} - Others: YES\n", checkup_message); - } else { - checkup_message = format!("{} - Others: NO\n", checkup_message); - } - if !external_command.starts_with("./") { - checkup_message = - format!("{}\n Please try again using the following syntax:\n", checkup_message); - checkup_message = format!("{} [...] exec ./{}", checkup_message, external_command); - for argument in &args { - checkup_message = format!("{} {}", checkup_message, argument) - } - } - log::error!("{}", checkup_message); - panic!("Maybe you could change the path to match with the correct one") - } else { - // Unable to find a corresponding things to execute - entry_list = format!("{} -{}\n", entry_list, entry_string) - } - } - } - log::error!("Executable not found in the current directory, found:\n{}", entry_list); - panic!("Maybe you could change the path") + let return_error: String = handle_not_found(external_command, args); + panic!("{}", return_error); + }, + std::io::ErrorKind::PermissionDenied => { + let return_error: String = handle_permission_denied(external_command); + panic!("{}", return_error); } _ => { panic!("Error in child process"); @@ -96,6 +49,84 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result Ok(status) } +fn handle_permission_denied(external_command: String) -> String { + let file_permission_denied = match File::open(external_command.clone()) { + Ok(file) => { + file + }, + Err(err) => { + panic!("Error when try to read the file: {}", err); + }, + }; + let metadata_file = file_permission_denied.metadata().expect(format!("Unable to retrieve metadata for: {}", external_command).as_str()); + // Check for user permissions. + let user_perm = match metadata_file.permissions().mode() & 0o500 { + 0 => "rx", + 1 => "r", + 4 => "x", + _ => "", + }; + // Check for group permissions. + let group_perm: &str = match metadata_file.permissions().mode() & 0o050 { + 0 => "rx", + 1 => "r", + 4 => "x", + _ => "", + }; + // Check for other permissions. + let other_perm = match metadata_file.permissions().mode() & 0o005 { + 0 => "rx", + 1 => "r", + 4 => "x", + _ => "", + }; + if user_perm == "rx" || group_perm == "rx" || other_perm == "rx" { + log::error!("file '{}' is missing the following permissions: 'rx'", external_command); + log::info!("💡 Hint: try 'chmod +rx {}", external_command) + } else if user_perm == "r" || group_perm == "r" || other_perm == "r" { + log::error!("file '{}' is missing the following permissions: 'r'", external_command); + log::info!("💡 Hint: try 'chmod +r {}", external_command) + } else if user_perm == "x" || group_perm == "x" || other_perm == "x" { + log::error!("file '{}' is missing the following permissions: 'x'", external_command); + log::info!("💡 Hint: try 'chmod +x {}", external_command) + } else { + log::warn!("Can't determine right issue about the file: {}", external_command); + } + "Issue happened about file's permission".to_string() +} + +fn handle_not_found(external_command: String, args: Vec) -> String { + fn resolve_application_path() -> std::io::Result { + std::env::current_exe()?.canonicalize() + } + log::error!("Command '{}' not found", external_command); + let directory_entries_iter = match fs::read_dir(".") { + Ok(directory) => { + directory + }, + Err(err) => { + panic!("Error when try to read current directory: {}", err); + }, + }; + let app_path = resolve_application_path() + .ok() + .and_then(|p| p.to_str().map(|s| s.to_owned())) + .unwrap_or(String::from("path/to/agent")); + for entry_result in directory_entries_iter { + let entry = entry_result.unwrap(); + let entry_type = entry.file_type().unwrap(); + if entry_type.is_file() { + let entry_string = entry.file_name().into_string().unwrap(); + if external_command == entry_string { + log::info!("💡Hint: A file named '{}' exists in the current directory. Prepend ./ to execute it.", entry_string); + log::info!("Example: {} exec ./{} {}",app_path, entry_string, args.join(" ")); + } + } + } + + "Issue happened because the file was not found".to_string() +} + // Triggers one measurement (on all sources that support manual trigger). pub fn trigger_measurement_now(pipeline: &MeasurementPipeline) -> anyhow::Result<()> { let control_handle = pipeline.control_handle(); From 78f8b35a8326c71244ff14429c02df8902d06ebf Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Thu, 28 Nov 2024 16:39:40 +0100 Subject: [PATCH 06/13] =?UTF-8?q?feat(exec=5Fprocess):=20first=20implement?= =?UTF-8?q?ation=20of=20the=20Damerau=E2=80=93Levenshtein=20distance=20wit?= =?UTF-8?q?h=20adjacent=20transpositions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app-agent/src/exec_process.rs | 48 ++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index c3f5179..bbb828f 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -1,6 +1,6 @@ //! Tie Alumet to a running process. -use std::{fs::{self, File}, os::unix::fs::PermissionsExt, path::PathBuf, process::ExitStatus}; +use std::{collections::HashMap, fs::{self, File}, os::unix::fs::PermissionsExt, path::PathBuf, process::ExitStatus}; use alumet::{ pipeline::{ @@ -140,3 +140,49 @@ pub fn trigger_measurement_now(pipeline: &MeasurementPipeline) -> anyhow::Result .block_on(send_task) .context("failed to send TriggerMessage") } + + +fn distance_with_adjacent_transposition(a: String, b: String) -> usize { + let a_len = a.chars().count(); + let b_len = b.chars().count(); + + let mut da: HashMap = HashMap::new(); + let mut d: Vec> = vec![vec![0; b_len+2]; a_len+2]; + + let max_dist = a_len + b_len; + d[0][0] = max_dist; + + for i in 0..(a_len+1){ + d[i+1][0] = max_dist; + d[i+1][1] = i; + } + for j in 0..(b_len+1){ + d[0][j+1] = max_dist; + d[1][j+1] = j; + } + + for i in 1..(a_len + 1){ + let mut db = 0; + for j in 1..(b_len + 1){ + let k = *da.get(&b.chars().nth(j-1).unwrap()).unwrap_or(&0); + let l = db; + let cost: usize; + if a.chars().nth(i-1).unwrap() == b.chars().nth(j-1).unwrap() { + cost = 0; + db = j; + } else { + cost = 1; + } + let operations = [ + d[i][j] + cost, // substitution + d[i + 1][j] + 1, // insertion + d[i][j + 1] + 1, // deletion + d[k][l] + (i - k - 1) + 1 + (j - l - 1), // transposition + ]; + d[i + 1][j + 1] = *operations.iter().min().unwrap(); + } + da.insert(a.chars().nth(i-1).unwrap(), i); + } + d[a_len+1][b_len+1] + +} \ No newline at end of file From 968ba251e5bdcdb11605ef4a35c24e3321af37c8 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Fri, 29 Nov 2024 12:10:27 +0100 Subject: [PATCH 07/13] feat(exec_process): Add test for the Damerau-Levenshtein distance function and use it when file not found in exec mode --- .cspell/project-words.txt | 8 ++ app-agent/src/exec_process.rs | 116 +++++++++++------------ app-agent/src/lib.rs | 1 + app-agent/src/utils.rs | 168 ++++++++++++++++++++++++++++++++++ 4 files changed, 228 insertions(+), 65 deletions(-) create mode 100644 app-agent/src/utils.rs diff --git a/.cspell/project-words.txt b/.cspell/project-words.txt index 64b747f..6b41565 100644 --- a/.cspell/project-words.txt +++ b/.cspell/project-words.txt @@ -2,6 +2,7 @@ alumet ASCIINEMA astring Atos +capaldi cbindgen cdylib Cgroups @@ -21,7 +22,10 @@ ITLB Mispredicted monitorable mpoint +necron +Neron NVML +olle PERFMON POWERCAP psys @@ -39,5 +43,9 @@ SYSFS thiserror timerfd Trystram +tuut +Tweety UCUM uncore +круто +Привет diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index bbb828f..6eb524a 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -1,6 +1,11 @@ //! Tie Alumet to a running process. -use std::{collections::HashMap, fs::{self, File}, os::unix::fs::PermissionsExt, path::PathBuf, process::ExitStatus}; +use std::{ + fs::{self, File}, + os::unix::fs::PermissionsExt, + path::PathBuf, + process::ExitStatus, +}; use alumet::{ pipeline::{ @@ -27,7 +32,7 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result std::io::ErrorKind::NotFound => { let return_error: String = handle_not_found(external_command, args); panic!("{}", return_error); - }, + } std::io::ErrorKind::PermissionDenied => { let return_error: String = handle_permission_denied(external_command); panic!("{}", return_error); @@ -51,14 +56,14 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result fn handle_permission_denied(external_command: String) -> String { let file_permission_denied = match File::open(external_command.clone()) { - Ok(file) => { - file - }, + Ok(file) => file, Err(err) => { panic!("Error when try to read the file: {}", err); - }, + } }; - let metadata_file = file_permission_denied.metadata().expect(format!("Unable to retrieve metadata for: {}", external_command).as_str()); + let metadata_file = file_permission_denied + .metadata() + .expect(format!("Unable to retrieve metadata for: {}", external_command).as_str()); // Check for user permissions. let user_perm = match metadata_file.permissions().mode() & 0o500 { 0 => "rx", @@ -81,7 +86,10 @@ fn handle_permission_denied(external_command: String) -> String { _ => "", }; if user_perm == "rx" || group_perm == "rx" || other_perm == "rx" { - log::error!("file '{}' is missing the following permissions: 'rx'", external_command); + log::error!( + "file '{}' is missing the following permissions: 'rx'", + external_command + ); log::info!("💡 Hint: try 'chmod +rx {}", external_command) } else if user_perm == "r" || group_perm == "r" || other_perm == "r" { log::error!("file '{}' is missing the following permissions: 'r'", external_command); @@ -101,29 +109,53 @@ fn handle_not_found(external_command: String, args: Vec) -> String { } log::error!("Command '{}' not found", external_command); let directory_entries_iter = match fs::read_dir(".") { - Ok(directory) => { - directory - }, + Ok(directory) => directory, Err(err) => { panic!("Error when try to read current directory: {}", err); - }, + } }; let app_path = resolve_application_path() - .ok() - .and_then(|p| p.to_str().map(|s| s.to_owned())) - .unwrap_or(String::from("path/to/agent")); + .ok() + .and_then(|p| p.to_str().map(|s| s.to_owned())) + .unwrap_or(String::from("path/to/agent")); + + let mut lowest_distance = usize::MAX; + let mut best_element = None; + for entry_result in directory_entries_iter { let entry = entry_result.unwrap(); let entry_type = entry.file_type().unwrap(); if entry_type.is_file() { let entry_string = entry.file_name().into_string().unwrap(); - if external_command == entry_string { - log::info!("💡Hint: A file named '{}' exists in the current directory. Prepend ./ to execute it.", entry_string); - log::info!("Example: {} exec ./{} {}",app_path, entry_string, args.join(" ")); + let distance = super::utils::distance_with_adjacent_transposition( + external_command + .strip_prefix("./") + .unwrap_or(&external_command) + .to_string(), + entry_string.clone(), + ); + if distance < 3 && distance < lowest_distance { + lowest_distance = distance; + best_element = Some((entry_string, distance)); } } } - + match best_element { + Some((element, distance)) => { + if distance == 0 { + log::info!( + "💡 Hint: A file named '{}' exists in the current directory. Prepend ./ to execute it.", + element + ); + log::info!("Example: {} exec ./{} {}", app_path, element, args.join(" ")); + } else { + log::info!("💡 Hint: Did you mean ./{} {}", element, args.join(" ")); + } + } + None => { + log::warn!("💡 Hint: No matching file exists in the current directory. Prepend ./ to execute it."); + } + } "Issue happened because the file was not found".to_string() } @@ -140,49 +172,3 @@ pub fn trigger_measurement_now(pipeline: &MeasurementPipeline) -> anyhow::Result .block_on(send_task) .context("failed to send TriggerMessage") } - - -fn distance_with_adjacent_transposition(a: String, b: String) -> usize { - let a_len = a.chars().count(); - let b_len = b.chars().count(); - - let mut da: HashMap = HashMap::new(); - let mut d: Vec> = vec![vec![0; b_len+2]; a_len+2]; - - let max_dist = a_len + b_len; - d[0][0] = max_dist; - - for i in 0..(a_len+1){ - d[i+1][0] = max_dist; - d[i+1][1] = i; - } - for j in 0..(b_len+1){ - d[0][j+1] = max_dist; - d[1][j+1] = j; - } - - for i in 1..(a_len + 1){ - let mut db = 0; - for j in 1..(b_len + 1){ - let k = *da.get(&b.chars().nth(j-1).unwrap()).unwrap_or(&0); - let l = db; - let cost: usize; - if a.chars().nth(i-1).unwrap() == b.chars().nth(j-1).unwrap() { - cost = 0; - db = j; - } else { - cost = 1; - } - let operations = [ - d[i][j] + cost, // substitution - d[i + 1][j] + 1, // insertion - d[i][j + 1] + 1, // deletion - d[k][l] + (i - k - 1) + 1 + (j - l - 1), // transposition - ]; - d[i + 1][j + 1] = *operations.iter().min().unwrap(); - } - da.insert(a.chars().nth(i-1).unwrap(), i); - } - d[a_len+1][b_len+1] - -} \ No newline at end of file diff --git a/app-agent/src/lib.rs b/app-agent/src/lib.rs index 698a80a..2152837 100644 --- a/app-agent/src/lib.rs +++ b/app-agent/src/lib.rs @@ -6,6 +6,7 @@ pub mod agent_util; pub mod config_ops; pub mod exec_process; pub mod options; +pub mod utils; /// Returns the path to the Alumet agent that is being executed. pub fn resolve_application_path() -> std::io::Result { diff --git a/app-agent/src/utils.rs b/app-agent/src/utils.rs new file mode 100644 index 0000000..de52c40 --- /dev/null +++ b/app-agent/src/utils.rs @@ -0,0 +1,168 @@ +use std::collections::HashMap; + +// Damerau-Levenshtein distance +pub fn distance_with_adjacent_transposition(a: String, b: String) -> usize { + let a_len = a.chars().count(); + let b_len = b.chars().count(); + + let mut da: HashMap = HashMap::new(); + let mut d: Vec> = vec![vec![0; b_len + 2]; a_len + 2]; + + let max_dist = a_len + b_len; + d[0][0] = max_dist; + + for i in 0..(a_len + 1) { + d[i + 1][0] = max_dist; + d[i + 1][1] = i; + } + for j in 0..(b_len + 1) { + d[0][j + 1] = max_dist; + d[1][j + 1] = j; + } + + for i in 1..(a_len + 1) { + let mut db = 0; + for j in 1..(b_len + 1) { + let k = *da.get(&b.chars().nth(j - 1).unwrap()).unwrap_or(&0); + let l = db; + let cost: usize; + if a.chars().nth(i - 1).unwrap() == b.chars().nth(j - 1).unwrap() { + cost = 0; + db = j; + } else { + cost = 1; + } + let operations = [ + d[i][j] + cost, // substitution + d[i + 1][j] + 1, // insertion + d[i][j + 1] + 1, // deletion + d[k][l] + (i - k - 1) + 1 + (j - l - 1), // transposition + ]; + d[i + 1][j + 1] = *operations.iter().min().unwrap(); + } + da.insert(a.chars().nth(i - 1).unwrap(), i); + } + d[a_len + 1][b_len + 1] +} + +#[cfg(test)] +mod tests { + use super::distance_with_adjacent_transposition; + + #[test] + fn basic_cases() { + assert_eq!(0, distance_with_adjacent_transposition("".to_string(), "".to_string())); + assert_eq!(1, distance_with_adjacent_transposition("".to_string(), " ".to_string())); + assert_eq!( + 5, + distance_with_adjacent_transposition("".to_string(), "Neron".to_string()) + ); + assert_eq!( + 5, + distance_with_adjacent_transposition("Neron".to_string(), "".to_string()) + ); + assert_eq!( + 1, + distance_with_adjacent_transposition("Neron".to_string(), "Necron".to_string()) + ); + assert_eq!( + 1, + distance_with_adjacent_transposition("necron".to_string(), "neron".to_string()) + ); + assert_eq!( + 5, + distance_with_adjacent_transposition("bread".to_string(), "butter".to_string()) + ); + assert_eq!( + 1, + distance_with_adjacent_transposition("giggle".to_string(), "wiggle".to_string()) + ); + assert_eq!( + 2, + distance_with_adjacent_transposition("sparkle".to_string(), "darkle".to_string()) + ); + assert_eq!( + 6, + distance_with_adjacent_transposition("Amelia".to_string(), "Pond".to_string()) + ); + assert_eq!( + 2, + distance_with_adjacent_transposition("Song".to_string(), "Pond".to_string()) + ); + assert_eq!( + 4, + distance_with_adjacent_transposition("Donna".to_string(), "noble".to_string()) + ); + assert_eq!( + 7, + distance_with_adjacent_transposition("Tweety".to_string(), "Sylvester".to_string()) + ); + assert_eq!( + 6, + distance_with_adjacent_transposition("abacus".to_string(), "flower".to_string()) + ); + } + + #[test] + fn intermediate_cases() { + assert_eq!( + 1, + distance_with_adjacent_transposition("Toto".to_string(), "toto".to_string()) + ); + assert_eq!( + 1, + distance_with_adjacent_transposition("Peter".to_string(), "peter".to_string()) + ); + assert_eq!( + 1, + distance_with_adjacent_transposition("capaldi".to_string(), "Capaldi".to_string()) + ); + assert_eq!( + 0, + distance_with_adjacent_transposition("tutu".to_string(), "tutu".to_string()) + ); + assert_eq!( + 1, + distance_with_adjacent_transposition("tuut".to_string(), "tutu".to_string()) + ); + assert_eq!( + 1, + distance_with_adjacent_transposition("tutu".to_string(), "tuut".to_string()) + ); + assert_eq!( + 1, + distance_with_adjacent_transposition("tutu".to_string(), "tuut".to_string()) + ); + assert_eq!( + 4, + distance_with_adjacent_transposition("hello".to_string(), "olleH".to_string()) + ); + } + + #[test] + fn complexe_cases() { + assert_eq!( + 0, + distance_with_adjacent_transposition( + "Hello, is it me you're looking for? I can see it in your eyes I can see it in your smile".to_string(), + "Hello, is it me you're looking for? I can see it in your eyes I can see it in your smile".to_string() + ) + ); + assert_eq!( + 0, + distance_with_adjacent_transposition("&é'(-è_çà)=".to_string(), "&é'(-è_çà)=".to_string()) + ); + assert_eq!( + 10, + distance_with_adjacent_transposition("&é'(-è_çà)=".to_string(), "=)àç_è-('é&".to_string()) + ); + assert_eq!( + 0, + distance_with_adjacent_transposition("Привет, это круто".to_string(), "Привет, это круто".to_string()) + ); + assert_eq!( + 6, + distance_with_adjacent_transposition("Привет, круто это".to_string(), "Привет, это круто".to_string()) + ); + } +} From df9027b60efc0999db6d024519594d883a6984b0 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Wed, 11 Dec 2024 10:33:23 +0100 Subject: [PATCH 08/13] feat(exec): improved Damerau-Levenshtein distance algo and error handling on exec mode --- app-agent/src/exec_process.rs | 41 +++++++++++++++++++++++++++++------ app-agent/src/utils.rs | 12 +++++----- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index 6eb524a..fcb5489 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -2,7 +2,7 @@ use std::{ fs::{self, File}, - os::unix::fs::PermissionsExt, + os::unix::{fs::PermissionsExt, process::ExitStatusExt}, path::PathBuf, process::ExitStatus, }; @@ -17,7 +17,7 @@ use alumet::{ plugin::event::StartConsumerMeasurement, resources::ResourceConsumer, }; -use anyhow::Context; +use anyhow::{anyhow, Context}; /// Spawns a child process and waits for it to exit. pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result { @@ -31,11 +31,11 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result Err(e) => match e.kind() { std::io::ErrorKind::NotFound => { let return_error: String = handle_not_found(external_command, args); - panic!("{}", return_error); + return Err(anyhow!(return_error)); } std::io::ErrorKind::PermissionDenied => { let return_error: String = handle_permission_denied(external_command); - panic!("{}", return_error); + return Err(anyhow!(return_error)); } _ => { panic!("Error in child process"); @@ -58,7 +58,7 @@ fn handle_permission_denied(external_command: String) -> String { let file_permission_denied = match File::open(external_command.clone()) { Ok(file) => file, Err(err) => { - panic!("Error when try to read the file: {}", err); + panic!("Error when trying to read the file: {}", err); } }; let metadata_file = file_permission_denied @@ -147,9 +147,36 @@ fn handle_not_found(external_command: String, args: Vec) -> String { "💡 Hint: A file named '{}' exists in the current directory. Prepend ./ to execute it.", element ); - log::info!("Example: {} exec ./{} {}", app_path, element, args.join(" ")); + log::info!( + "Example: {} exec ./{} {}", + app_path, + element, + args.iter() + .map(|arg| { + if arg.contains(' ') { + format!("\"{}\"", arg) + } else { + arg.to_string() + } + }) + .collect::>() + .join(" ") + ); } else { - log::info!("💡 Hint: Did you mean ./{} {}", element, args.join(" ")); + log::info!( + "💡 Hint: Did you mean ./{} {}", + element, + args.iter() + .map(|arg| { + if arg.contains(' ') { + format!("\"{}\"", arg) + } else { + arg.to_string() + } + }) + .collect::>() + .join(" ") + ); } } None => { diff --git a/app-agent/src/utils.rs b/app-agent/src/utils.rs index de52c40..b48650a 100644 --- a/app-agent/src/utils.rs +++ b/app-agent/src/utils.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; // Damerau-Levenshtein distance pub fn distance_with_adjacent_transposition(a: String, b: String) -> usize { - let a_len = a.chars().count(); - let b_len = b.chars().count(); + let a_chars: Vec = a.chars().collect(); + let b_chars: Vec = b.chars().collect(); + let a_len = a_chars.len(); + let b_len = b_chars.len(); let mut da: HashMap = HashMap::new(); let mut d: Vec> = vec![vec![0; b_len + 2]; a_len + 2]; @@ -23,10 +25,10 @@ pub fn distance_with_adjacent_transposition(a: String, b: String) -> usize { for i in 1..(a_len + 1) { let mut db = 0; for j in 1..(b_len + 1) { - let k = *da.get(&b.chars().nth(j - 1).unwrap()).unwrap_or(&0); + let k = *da.get(&b_chars[j - 1]).unwrap_or(&0); let l = db; let cost: usize; - if a.chars().nth(i - 1).unwrap() == b.chars().nth(j - 1).unwrap() { + if a_chars[i - 1] == b_chars[j - 1] { cost = 0; db = j; } else { @@ -40,7 +42,7 @@ pub fn distance_with_adjacent_transposition(a: String, b: String) -> usize { ]; d[i + 1][j + 1] = *operations.iter().min().unwrap(); } - da.insert(a.chars().nth(i - 1).unwrap(), i); + da.insert(a_chars[i - 1], i); } d[a_len + 1][b_len + 1] } From 264ef6fafe63d992c06ede5bfe61225688adaec1 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Wed, 11 Dec 2024 17:44:31 +0100 Subject: [PATCH 09/13] feat(exec): Check for parent rights if an issue happen and suggest command to correct --- app-agent/src/exec_process.rs | 78 +++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 9 deletions(-) diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index fcb5489..2c8b192 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -1,7 +1,7 @@ //! Tie Alumet to a running process. use std::{ - fs::{self, File}, + fs::{self, File, Metadata}, os::unix::{fs::PermissionsExt, process::ExitStatusExt}, path::PathBuf, process::ExitStatus, @@ -58,28 +58,88 @@ fn handle_permission_denied(external_command: String) -> String { let file_permission_denied = match File::open(external_command.clone()) { Ok(file) => file, Err(err) => { + // Current parent can change if a parent of the parent don't have the correct rights + let mut current_parent = match std::path::Path::new(&external_command).parent() { + Some(parent) => parent, + None => return "".to_string(), + }; + // Trough this loop I will iterate over parent of parent until I can retrieve metadata, it will show the first folder + // that I can't execute and suggest to the user to grant execution rights. + let metadata: Metadata; + loop { + match current_parent.metadata() { + Ok(metadata_parent) => { + metadata = metadata_parent; + break; + } + Err(_) => { + current_parent = match current_parent.parent() { + Some(parent) => parent, + None => { + panic!("Unable to retrieve a parent for your file"); + } + } + } + } + } + let user_perm_parent = match metadata.permissions().mode() & 0o500 { + 0o100 => 1, + _ => 0, + }; + let group_perm_parent = match metadata.permissions().mode() & 0o050 { + 0o010 => 1, + _ => 0, + }; + let other_perm_parent = match metadata.permissions().mode() & 0o005 { + 0o001 => 1, + _ => 0, + }; + // Print warn message when parent folder's file has a missing execute rights + if user_perm_parent == 0 { + log::warn!( + "folder '{}' is missing the following permissions for user owner: 'x'", + current_parent.display() + ) + } + if group_perm_parent == 0 { + log::warn!( + "folder '{}' is missing the following permissions for group owner: 'x'", + current_parent.display() + ) + } + if other_perm_parent == 0 { + log::warn!( + "folder '{}' is missing the following permissions for other: 'x'", + current_parent.display() + ) + } + if user_perm_parent == 0 || group_perm_parent == 0 || other_perm_parent == 0 { + log::info!("💡 Hint: try 'chmod +x {}'", current_parent.display()) + } panic!("Error when trying to read the file: {}", err); } }; - let metadata_file = file_permission_denied + + // Get file metadata + let file_metadata = file_permission_denied .metadata() .expect(format!("Unable to retrieve metadata for: {}", external_command).as_str()); // Check for user permissions. - let user_perm = match metadata_file.permissions().mode() & 0o500 { + let user_perm = match file_metadata.permissions().mode() & 0o500 { 0 => "rx", 1 => "r", 4 => "x", _ => "", }; // Check for group permissions. - let group_perm: &str = match metadata_file.permissions().mode() & 0o050 { + let group_perm: &str = match file_metadata.permissions().mode() & 0o050 { 0 => "rx", 1 => "r", 4 => "x", _ => "", }; // Check for other permissions. - let other_perm = match metadata_file.permissions().mode() & 0o005 { + let other_perm = match file_metadata.permissions().mode() & 0o005 { 0 => "rx", 1 => "r", 4 => "x", @@ -90,13 +150,13 @@ fn handle_permission_denied(external_command: String) -> String { "file '{}' is missing the following permissions: 'rx'", external_command ); - log::info!("💡 Hint: try 'chmod +rx {}", external_command) + log::info!("💡 Hint: try 'chmod +rx {}'", external_command) } else if user_perm == "r" || group_perm == "r" || other_perm == "r" { log::error!("file '{}' is missing the following permissions: 'r'", external_command); - log::info!("💡 Hint: try 'chmod +r {}", external_command) + log::info!("💡 Hint: try 'chmod +r {}'", external_command) } else if user_perm == "x" || group_perm == "x" || other_perm == "x" { log::error!("file '{}' is missing the following permissions: 'x'", external_command); - log::info!("💡 Hint: try 'chmod +x {}", external_command) + log::info!("💡 Hint: try 'chmod +x {}'", external_command) } else { log::warn!("Can't determine right issue about the file: {}", external_command); } @@ -111,7 +171,7 @@ fn handle_not_found(external_command: String, args: Vec) -> String { let directory_entries_iter = match fs::read_dir(".") { Ok(directory) => directory, Err(err) => { - panic!("Error when try to read current directory: {}", err); + panic!("Error when trying to read current directory: {}", err); } }; let app_path = resolve_application_path() From 4fc1d61ac78aae5c7e9ddc02e4946cd848736a1f Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Thu, 9 Jan 2025 17:02:21 +0100 Subject: [PATCH 10/13] feat(exec_process): work on PR review, separation of function,... --- app-agent/src/exec_process.rs | 248 +++++++++---------- app-agent/src/lib.rs | 2 +- app-agent/src/{utils.rs => word_distance.rs} | 35 ++- 3 files changed, 146 insertions(+), 139 deletions(-) rename app-agent/src/{utils.rs => word_distance.rs} (82%) diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index 2c8b192..379d508 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -17,7 +17,7 @@ use alumet::{ plugin::event::StartConsumerMeasurement, resources::ResourceConsumer, }; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, Context, Error}; /// Spawns a child process and waits for it to exit. pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result { @@ -38,7 +38,7 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result return Err(anyhow!(return_error)); } _ => { - panic!("Error in child process"); + return Err(anyhow!("Error in child process")); } }, }; @@ -55,112 +55,48 @@ pub fn exec_child(external_command: String, args: Vec) -> anyhow::Result } fn handle_permission_denied(external_command: String) -> String { - let file_permission_denied = match File::open(external_command.clone()) { - Ok(file) => file, - Err(err) => { - // Current parent can change if a parent of the parent don't have the correct rights - let mut current_parent = match std::path::Path::new(&external_command).parent() { - Some(parent) => parent, - None => return "".to_string(), - }; - // Trough this loop I will iterate over parent of parent until I can retrieve metadata, it will show the first folder - // that I can't execute and suggest to the user to grant execution rights. - let metadata: Metadata; - loop { - match current_parent.metadata() { - Ok(metadata_parent) => { - metadata = metadata_parent; - break; - } - Err(_) => { - current_parent = match current_parent.parent() { - Some(parent) => parent, - None => { - panic!("Unable to retrieve a parent for your file"); - } - } - } - } - } - let user_perm_parent = match metadata.permissions().mode() & 0o500 { - 0o100 => 1, - _ => 0, - }; - let group_perm_parent = match metadata.permissions().mode() & 0o050 { - 0o010 => 1, - _ => 0, - }; - let other_perm_parent = match metadata.permissions().mode() & 0o005 { - 0o001 => 1, - _ => 0, - }; - // Print warn message when parent folder's file has a missing execute rights - if user_perm_parent == 0 { - log::warn!( - "folder '{}' is missing the following permissions for user owner: 'x'", - current_parent.display() - ) - } - if group_perm_parent == 0 { - log::warn!( - "folder '{}' is missing the following permissions for group owner: 'x'", - current_parent.display() - ) - } - if other_perm_parent == 0 { - log::warn!( - "folder '{}' is missing the following permissions for other: 'x'", - current_parent.display() - ) - } - if user_perm_parent == 0 || group_perm_parent == 0 || other_perm_parent == 0 { - log::info!("💡 Hint: try 'chmod +x {}'", current_parent.display()) - } - panic!("Error when trying to read the file: {}", err); + let file_open_result = File::open(external_command.clone()); + let file_correctly_opened = if let Err(err) = file_open_result { + // Can't open the file, let's check it's parent + let current_parent = match find_a_parent_with_perm_issue(external_command.clone()) { + Ok(parent) => parent, + Err(err) => return err, + }; + let metadata: Metadata = current_parent.metadata().expect(&format!("Unable to retrieve metadata of file: {}", current_parent.display())); + let missing_permissions = check_missing_permissions(metadata.permissions().mode(), 0o555); + if missing_permissions & 0o500 != 0 || missing_permissions & 0o050 != 0 || missing_permissions & 0o005 != 0 { + log::warn!( + "folder '{}' is missing the following permissions: 'rx'", + current_parent.display() + ); + log::info!("💡 Hint: try 'chmod +rx {}'", current_parent.display()); } + return format!("Error when trying to read the file: {}", external_command.clone()); + } else if let Ok(file) = file_open_result { + // Can open the file + file + } else { + return "Error when trying to read the file".to_owned(); }; - // Get file metadata - let file_metadata = file_permission_denied + // Get file metadata to see missing permissions + let file_metadata = file_correctly_opened .metadata() .expect(format!("Unable to retrieve metadata for: {}", external_command).as_str()); - // Check for user permissions. - let user_perm = match file_metadata.permissions().mode() & 0o500 { - 0 => "rx", - 1 => "r", - 4 => "x", - _ => "", - }; - // Check for group permissions. - let group_perm: &str = match file_metadata.permissions().mode() & 0o050 { - 0 => "rx", - 1 => "r", - 4 => "x", - _ => "", - }; - // Check for other permissions. - let other_perm = match file_metadata.permissions().mode() & 0o005 { - 0 => "rx", - 1 => "r", - 4 => "x", - _ => "", - }; - if user_perm == "rx" || group_perm == "rx" || other_perm == "rx" { - log::error!( - "file '{}' is missing the following permissions: 'rx'", - external_command - ); - log::info!("💡 Hint: try 'chmod +rx {}'", external_command) - } else if user_perm == "r" || group_perm == "r" || other_perm == "r" { - log::error!("file '{}' is missing the following permissions: 'r'", external_command); - log::info!("💡 Hint: try 'chmod +r {}'", external_command) - } else if user_perm == "x" || group_perm == "x" || other_perm == "x" { - log::error!("file '{}' is missing the following permissions: 'x'", external_command); - log::info!("💡 Hint: try 'chmod +x {}'", external_command) + let missing_permissions = check_missing_permissions(file_metadata.permissions().mode(), 0o505); + let missing_right_str; + if missing_permissions & 0o500 != 0 || missing_permissions & 0o050 != 0 || missing_permissions & 0o005 != 0 { + missing_right_str = "rx" + } else if missing_permissions & 0o400 != 0 || missing_permissions & 0o040 != 0 || missing_permissions & 0o004 != 0 { + missing_right_str = "r" + } else if missing_permissions & 0o100 != 0 || missing_permissions & 0o010 != 0 || missing_permissions & 0o001 != 0 { + missing_right_str = "x" } else { - log::warn!("Can't determine right issue about the file: {}", external_command); + missing_right_str = "rx" } - "Issue happened about file's permission".to_string() + log::error!("file '{}' is missing the following permissions: 'x'", external_command); + log::info!("💡 Hint: try 'chmod +{} {}'",missing_right_str, external_command); + "Error happened about file's permission".to_string() } fn handle_not_found(external_command: String, args: Vec) -> String { @@ -187,11 +123,11 @@ fn handle_not_found(external_command: String, args: Vec) -> String { let entry_type = entry.file_type().unwrap(); if entry_type.is_file() { let entry_string = entry.file_name().into_string().unwrap(); - let distance = super::utils::distance_with_adjacent_transposition( + let distance = super::word_distance::distance_with_adjacent_transposition( external_command - .strip_prefix("./") - .unwrap_or(&external_command) - .to_string(), + .strip_prefix("./") + .unwrap_or(&external_command) + .to_string(), entry_string.clone(), ); if distance < 3 && distance < lowest_distance { @@ -202,48 +138,88 @@ fn handle_not_found(external_command: String, args: Vec) -> String { } match best_element { Some((element, distance)) => { + let argument_list = args.iter().map(|arg| { + if arg.contains(' ') { + format!("\"{}\"", arg) + } else { + arg.to_string() + } + }).collect::>().join(" "); if distance == 0 { log::info!( "💡 Hint: A file named '{}' exists in the current directory. Prepend ./ to execute it.", element ); - log::info!( - "Example: {} exec ./{} {}", - app_path, - element, - args.iter() - .map(|arg| { - if arg.contains(' ') { - format!("\"{}\"", arg) - } else { - arg.to_string() - } - }) - .collect::>() - .join(" ") - ); + log::info!("Example: {} exec ./{} {}", app_path, element, argument_list); } else { - log::info!( - "💡 Hint: Did you mean ./{} {}", - element, - args.iter() - .map(|arg| { - if arg.contains(' ') { - format!("\"{}\"", arg) - } else { - arg.to_string() - } - }) - .collect::>() - .join(" ") - ); + log::info!("💡 Hint: Did you mean ./{} {}", element, argument_list); } } None => { - log::warn!("💡 Hint: No matching file exists in the current directory. Prepend ./ to execute it."); + log::warn!("💡 Hint: No matching file exists in the current directory. Please try again we a different one."); + } + } + "Sorry but the file was not found".to_string() +} + +fn check_missing_permissions(current_permissions: u32, required_permissions: u32) -> u32 { + let mut missing = 0; + + // Check read, write and execute permissions for the user + if (required_permissions & 0o400) != 0 && (current_permissions & 0o400) == 0 { + missing |= 0o400; + } + if (required_permissions & 0o200) != 0 && (current_permissions & 0o200) == 0 { + missing |= 0o200; + } + if (required_permissions & 0o100) != 0 && (current_permissions & 0o100) == 0 { + missing |= 0o100; + } + + // Check read, write and execute permissions for the group + if (required_permissions & 0o040) != 0 && (current_permissions & 0o040) == 0 { + missing |= 0o040; + } + if (required_permissions & 0o020) != 0 && (current_permissions & 0o020) == 0 { + missing |= 0o020; + } + if (required_permissions & 0o010) != 0 && (current_permissions & 0o010) == 0 { + missing |= 0o010; + } + + // Check read, write and execute permissions for others + if (required_permissions & 0o004) != 0 && (current_permissions & 0o004) == 0 { + missing |= 0o004; + } + if (required_permissions & 0o002) != 0 && (current_permissions & 0o002) == 0 { + missing |= 0o002; + } + if (required_permissions & 0o001) != 0 && (current_permissions & 0o001) == 0 { + missing |= 0o001; + } + + missing +} + +fn find_a_parent_with_perm_issue(path: String) -> anyhow::Result { + // Current parent can change if a parent of the parent don't have the correct rights + let mut current_parent = match std::path::Path::new(&path).parent() { + Some(parent) => parent, + None => return Err("".to_string()), + }; + // Through this loop I will iterate over parent of parent until I can retrieve metadata, it will show the first folder + // that I can't execute and suggest to the user to grant execution rights. + loop { + match current_parent.metadata() { + Ok(_) => return Ok(current_parent.to_path_buf()), + Err(_) => { + current_parent = match current_parent.parent() { + Some(parent) => parent, + None => return Err("Unable to retrieve a parent for your file".to_string()), + } + } } } - "Issue happened because the file was not found".to_string() } // Triggers one measurement (on all sources that support manual trigger). diff --git a/app-agent/src/lib.rs b/app-agent/src/lib.rs index 2152837..8678ed2 100644 --- a/app-agent/src/lib.rs +++ b/app-agent/src/lib.rs @@ -6,7 +6,7 @@ pub mod agent_util; pub mod config_ops; pub mod exec_process; pub mod options; -pub mod utils; +pub mod word_distance; /// Returns the path to the Alumet agent that is being executed. pub fn resolve_application_path() -> std::io::Result { diff --git a/app-agent/src/utils.rs b/app-agent/src/word_distance.rs similarity index 82% rename from app-agent/src/utils.rs rename to app-agent/src/word_distance.rs index b48650a..d7b0ec6 100644 --- a/app-agent/src/utils.rs +++ b/app-agent/src/word_distance.rs @@ -1,7 +1,38 @@ use std::collections::HashMap; -// Damerau-Levenshtein distance -pub fn distance_with_adjacent_transposition(a: String, b: String) -> usize { +/// Computes the Damerau-Levenshtein distance between two strings. +/// +/// The Damerau-Levenshtein distance is a measure of the similarity between two strings, +/// which is defined as the minimum number of operations needed to transform one string +/// into the other. The allowed operations are: +/// +/// - Insertion of a single character +/// - Deletion of a single character +/// - Substitution of a single character +/// - Transposition of two adjacent characters +/// +/// # Arguments +/// +/// * `a` - The first string. +/// * `b` - The second string. +/// +/// # Returns +/// +/// The Damerau-Levenshtein distance between the two strings. +/// +/// # Examples +/// +/// ``` +/// let distance = distance_with_adjacent_transposition("kitten".to_string(), "sitting".to_string()); +/// assert_eq!(distance, 3); +/// +/// let distance = distance_with_adjacent_transposition("flaw".to_string(), "lawn".to_string()); +/// assert_eq!(distance, 2); +/// +/// let distance = distance_with_adjacent_transposition("ca".to_string(), "abc".to_string()); +/// assert_eq!(distance, 2); +/// ``` + pub fn distance_with_adjacent_transposition(a: String, b: String) -> usize { let a_chars: Vec = a.chars().collect(); let b_chars: Vec = b.chars().collect(); let a_len = a_chars.len(); From ad2ffb7909eaaf018f039dbfc63e8aff315a0825 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Fri, 10 Jan 2025 09:49:14 +0100 Subject: [PATCH 11/13] feat(exec_process): lint files --- app-agent/src/exec_process.rs | 35 +++++++++++++++++++++------------- app-agent/src/word_distance.rs | 22 ++++++++++----------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index 379d508..9dc4f99 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -62,7 +62,10 @@ fn handle_permission_denied(external_command: String) -> String { Ok(parent) => parent, Err(err) => return err, }; - let metadata: Metadata = current_parent.metadata().expect(&format!("Unable to retrieve metadata of file: {}", current_parent.display())); + let metadata: Metadata = current_parent.metadata().expect(&format!( + "Unable to retrieve metadata of file: {}", + current_parent.display() + )); let missing_permissions = check_missing_permissions(metadata.permissions().mode(), 0o555); if missing_permissions & 0o500 != 0 || missing_permissions & 0o050 != 0 || missing_permissions & 0o005 != 0 { log::warn!( @@ -95,7 +98,7 @@ fn handle_permission_denied(external_command: String) -> String { missing_right_str = "rx" } log::error!("file '{}' is missing the following permissions: 'x'", external_command); - log::info!("💡 Hint: try 'chmod +{} {}'",missing_right_str, external_command); + log::info!("💡 Hint: try 'chmod +{} {}'", missing_right_str, external_command); "Error happened about file's permission".to_string() } @@ -125,9 +128,9 @@ fn handle_not_found(external_command: String, args: Vec) -> String { let entry_string = entry.file_name().into_string().unwrap(); let distance = super::word_distance::distance_with_adjacent_transposition( external_command - .strip_prefix("./") - .unwrap_or(&external_command) - .to_string(), + .strip_prefix("./") + .unwrap_or(&external_command) + .to_string(), entry_string.clone(), ); if distance < 3 && distance < lowest_distance { @@ -138,13 +141,17 @@ fn handle_not_found(external_command: String, args: Vec) -> String { } match best_element { Some((element, distance)) => { - let argument_list = args.iter().map(|arg| { - if arg.contains(' ') { - format!("\"{}\"", arg) - } else { - arg.to_string() - } - }).collect::>().join(" "); + let argument_list = args + .iter() + .map(|arg| { + if arg.contains(' ') { + format!("\"{}\"", arg) + } else { + arg.to_string() + } + }) + .collect::>() + .join(" "); if distance == 0 { log::info!( "💡 Hint: A file named '{}' exists in the current directory. Prepend ./ to execute it.", @@ -156,7 +163,9 @@ fn handle_not_found(external_command: String, args: Vec) -> String { } } None => { - log::warn!("💡 Hint: No matching file exists in the current directory. Please try again we a different one."); + log::warn!( + "💡 Hint: No matching file exists in the current directory. Please try again we a different one." + ); } } "Sorry but the file was not found".to_string() diff --git a/app-agent/src/word_distance.rs b/app-agent/src/word_distance.rs index d7b0ec6..84756ad 100644 --- a/app-agent/src/word_distance.rs +++ b/app-agent/src/word_distance.rs @@ -1,38 +1,38 @@ use std::collections::HashMap; /// Computes the Damerau-Levenshtein distance between two strings. -/// +/// /// The Damerau-Levenshtein distance is a measure of the similarity between two strings, /// which is defined as the minimum number of operations needed to transform one string /// into the other. The allowed operations are: -/// +/// /// - Insertion of a single character /// - Deletion of a single character /// - Substitution of a single character /// - Transposition of two adjacent characters -/// +/// /// # Arguments -/// +/// /// * `a` - The first string. /// * `b` - The second string. -/// +/// /// # Returns -/// +/// /// The Damerau-Levenshtein distance between the two strings. -/// +/// /// # Examples -/// +/// /// ``` /// let distance = distance_with_adjacent_transposition("kitten".to_string(), "sitting".to_string()); /// assert_eq!(distance, 3); -/// +/// /// let distance = distance_with_adjacent_transposition("flaw".to_string(), "lawn".to_string()); /// assert_eq!(distance, 2); -/// +/// /// let distance = distance_with_adjacent_transposition("ca".to_string(), "abc".to_string()); /// assert_eq!(distance, 2); /// ``` - pub fn distance_with_adjacent_transposition(a: String, b: String) -> usize { +pub fn distance_with_adjacent_transposition(a: String, b: String) -> usize { let a_chars: Vec = a.chars().collect(); let b_chars: Vec = b.chars().collect(); let a_len = a_chars.len(); From 49138b51960ac8d6c9f39b4bb72a50bbf19da8f4 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Fri, 10 Jan 2025 10:02:16 +0100 Subject: [PATCH 12/13] feat(exec_process): import corrected in documentation --- app-agent/src/word_distance.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/app-agent/src/word_distance.rs b/app-agent/src/word_distance.rs index 84756ad..438622d 100644 --- a/app-agent/src/word_distance.rs +++ b/app-agent/src/word_distance.rs @@ -23,6 +23,7 @@ use std::collections::HashMap; /// # Examples /// /// ``` +/// use app_agent::word_distance::distance_with_adjacent_transposition; /// let distance = distance_with_adjacent_transposition("kitten".to_string(), "sitting".to_string()); /// assert_eq!(distance, 3); /// From 6460b9ac959ec31e10b4aa0d88a2f3d03dd80976 Mon Sep 17 00:00:00 2001 From: Cyprien Pelisse-Verdoux Date: Fri, 10 Jan 2025 15:45:47 +0100 Subject: [PATCH 13/13] feat(exec_process): Added some unitary tests --- app-agent/src/exec_process.rs | 272 +++++++++++++++++++++++++++++++++- 1 file changed, 267 insertions(+), 5 deletions(-) diff --git a/app-agent/src/exec_process.rs b/app-agent/src/exec_process.rs index 9dc4f99..3055454 100644 --- a/app-agent/src/exec_process.rs +++ b/app-agent/src/exec_process.rs @@ -3,7 +3,7 @@ use std::{ fs::{self, File, Metadata}, os::unix::{fs::PermissionsExt, process::ExitStatusExt}, - path::PathBuf, + path::{Path, PathBuf}, process::ExitStatus, }; @@ -99,7 +99,7 @@ fn handle_permission_denied(external_command: String) -> String { } log::error!("file '{}' is missing the following permissions: 'x'", external_command); log::info!("💡 Hint: try 'chmod +{} {}'", missing_right_str, external_command); - "Error happened about file's permission".to_string() + format!("Error happened about file's permission {}", external_command) } fn handle_not_found(external_command: String, args: Vec) -> String { @@ -126,11 +126,22 @@ fn handle_not_found(external_command: String, args: Vec) -> String { let entry_type = entry.file_type().unwrap(); if entry_type.is_file() { let entry_string = entry.file_name().into_string().unwrap(); - let distance = super::word_distance::distance_with_adjacent_transposition( - external_command + let path = Path::new(&external_command); + let external_command_corrected_name: String; + if path.is_absolute() { + external_command_corrected_name = path + .file_name() + .and_then(|os_str| os_str.to_str().map(|s| s.to_string())) + .unwrap_or_else(|| external_command.to_string()); + } else { + external_command_corrected_name = external_command .strip_prefix("./") .unwrap_or(&external_command) - .to_string(), + .to_string(); + } + + let distance = super::word_distance::distance_with_adjacent_transposition( + external_command_corrected_name.clone(), entry_string.clone(), ); if distance < 3 && distance < lowest_distance { @@ -161,6 +172,7 @@ fn handle_not_found(external_command: String, args: Vec) -> String { } else { log::info!("💡 Hint: Did you mean ./{} {}", element, argument_list); } + return "File not found but another appears to match".to_string(); } None => { log::warn!( @@ -244,3 +256,253 @@ pub fn trigger_measurement_now(pipeline: &MeasurementPipeline) -> anyhow::Result .block_on(send_task) .context("failed to send TriggerMessage") } + +#[cfg(test)] +mod tests { + use fs::Permissions; + + use super::*; + + fn reset_permissions(folder_a: PathBuf, folder_b: PathBuf, file: &File) { + fs::set_permissions(&folder_a, Permissions::from_mode(0o777)).expect("Can't change folder's permissions"); + fs::set_permissions(&folder_b, Permissions::from_mode(0o777)).expect("Can't change folder's permissions"); + file.set_permissions(Permissions::from_mode(0o777)) + .expect("Can't change file's permissions"); + } + + #[test] + fn test_check_missing_permissions() { + // Check user perms + assert_eq!(0o000, check_missing_permissions(0o700, 0o700)); + assert_eq!(0o000, check_missing_permissions(0o400, 0o400)); + assert_eq!(0o000, check_missing_permissions(0o200, 0o200)); + assert_eq!(0o000, check_missing_permissions(0o100, 0o100)); + assert_eq!(0o100, check_missing_permissions(0o600, 0o700)); + assert_eq!(0o200, check_missing_permissions(0o500, 0o700)); + assert_eq!(0o400, check_missing_permissions(0o300, 0o700)); + assert_eq!(0o500, check_missing_permissions(0o200, 0o700)); + assert_eq!(0o600, check_missing_permissions(0o100, 0o700)); + assert_eq!(0o700, check_missing_permissions(0o000, 0o700)); + assert_eq!(0o100, check_missing_permissions(0o200, 0o300)); + assert_eq!(0o100, check_missing_permissions(0o000, 0o100)); + assert_eq!(0o200, check_missing_permissions(0o000, 0o200)); + assert_eq!(0o400, check_missing_permissions(0o000, 0o400)); + + // Check group perms + assert_eq!(0o000, check_missing_permissions(0o070, 0o070)); + assert_eq!(0o000, check_missing_permissions(0o070, 0o070)); + assert_eq!(0o000, check_missing_permissions(0o040, 0o040)); + assert_eq!(0o000, check_missing_permissions(0o020, 0o020)); + assert_eq!(0o000, check_missing_permissions(0o010, 0o010)); + assert_eq!(0o010, check_missing_permissions(0o060, 0o070)); + assert_eq!(0o020, check_missing_permissions(0o050, 0o070)); + assert_eq!(0o040, check_missing_permissions(0o030, 0o070)); + assert_eq!(0o050, check_missing_permissions(0o020, 0o070)); + assert_eq!(0o060, check_missing_permissions(0o010, 0o070)); + assert_eq!(0o070, check_missing_permissions(0o000, 0o070)); + assert_eq!(0o010, check_missing_permissions(0o020, 0o030)); + assert_eq!(0o010, check_missing_permissions(0o000, 0o010)); + assert_eq!(0o020, check_missing_permissions(0o000, 0o020)); + assert_eq!(0o040, check_missing_permissions(0o000, 0o040)); + + // Check other perms + assert_eq!(0o000, check_missing_permissions(0o007, 0o007)); + assert_eq!(0o000, check_missing_permissions(0o004, 0o004)); + assert_eq!(0o000, check_missing_permissions(0o002, 0o002)); + assert_eq!(0o000, check_missing_permissions(0o001, 0o001)); + assert_eq!(0o001, check_missing_permissions(0o006, 0o007)); + assert_eq!(0o002, check_missing_permissions(0o005, 0o007)); + assert_eq!(0o004, check_missing_permissions(0o003, 0o007)); + assert_eq!(0o005, check_missing_permissions(0o002, 0o007)); + assert_eq!(0o006, check_missing_permissions(0o001, 0o007)); + assert_eq!(0o007, check_missing_permissions(0o000, 0o007)); + assert_eq!(0o001, check_missing_permissions(0o002, 0o003)); + assert_eq!(0o001, check_missing_permissions(0o000, 0o001)); + assert_eq!(0o002, check_missing_permissions(0o000, 0o002)); + assert_eq!(0o004, check_missing_permissions(0o000, 0o004)); + } + + #[test] + fn test_handle_permission_denied() { + let tmp = std::env::temp_dir(); + let root: std::path::PathBuf = tmp.join("river_folder/"); + if root.exists() { + std::fs::remove_dir_all(&root).unwrap(); + } + let river_song_folder = root.join("song_folder/"); + std::fs::create_dir_all(&river_song_folder).unwrap(); + + let path_file = river_song_folder.join("script.sh"); + let path_file_string = path_file.clone().into_os_string().into_string().unwrap(); + std::fs::write( + path_file.clone(), + format!( + "#!/bin/sh\n + echo \"Hello\"\n + sleep 2" + ), + ) + .unwrap(); + + let file = match File::open(&path_file) { + Err(why) => panic!("couldn't open {}: {}", path_file.display(), why), + Ok(file) => file, + }; + + let message_expect_folder = "Can't change folder's permissions"; + let message_expect_file = "Can't change file's permissions"; + + file.set_permissions(Permissions::from_mode(0o555)) + .expect(message_expect_file); + fs::set_permissions(&river_song_folder, Permissions::from_mode(0o555)).expect(message_expect_folder); + fs::set_permissions(&root, Permissions::from_mode(0o555)).expect(message_expect_folder); + assert_eq!( + format!("Error happened about file's permission {}", path_file_string.clone()), + handle_permission_denied(path_file_string.clone()) + ); + reset_permissions(root.clone(), river_song_folder.clone(), &file); + + file.set_permissions(Permissions::from_mode(0o444)) + .expect(message_expect_file); + fs::set_permissions(&river_song_folder, Permissions::from_mode(0o555)).expect(message_expect_folder); + fs::set_permissions(&root, Permissions::from_mode(0o555)).expect(message_expect_folder); + assert_eq!( + format!("Error happened about file's permission {}", path_file_string.clone()), + handle_permission_denied(path_file_string.clone()) + ); + reset_permissions(root.clone(), river_song_folder.clone(), &file); + + file.set_permissions(Permissions::from_mode(0o555)) + .expect(message_expect_file); + fs::set_permissions(&river_song_folder, Permissions::from_mode(0o555)).expect(message_expect_folder); + fs::set_permissions(&root, Permissions::from_mode(0o444)).expect(message_expect_folder); + assert_eq!( + format!("Error when trying to read the file: {}", path_file_string.clone()), + handle_permission_denied(path_file_string.clone()) + ); + reset_permissions(root.clone(), river_song_folder.clone(), &file); + + file.set_permissions(Permissions::from_mode(0o555)) + .expect(message_expect_file); + fs::set_permissions(&river_song_folder, Permissions::from_mode(0o555)).expect(message_expect_folder); + fs::set_permissions(&root, Permissions::from_mode(0o111)).expect(message_expect_folder); + assert_eq!( + format!("Error happened about file's permission {}", path_file_string.clone()), + handle_permission_denied(path_file_string.clone()) + ); + reset_permissions(root.clone(), river_song_folder.clone(), &file); + + file.set_permissions(Permissions::from_mode(0o555)) + .expect(message_expect_file); + fs::set_permissions(&river_song_folder, Permissions::from_mode(0o111)).expect(message_expect_folder); + fs::set_permissions(&root, Permissions::from_mode(0o111)).expect(message_expect_folder); + assert_eq!( + format!("Error happened about file's permission {}", path_file_string.clone()), + handle_permission_denied(path_file_string.clone()) + ); + reset_permissions(root.clone(), river_song_folder.clone(), &file); + + file.set_permissions(Permissions::from_mode(0o555)) + .expect(message_expect_file); + fs::set_permissions(&river_song_folder, Permissions::from_mode(0o111)).expect(message_expect_folder); + fs::set_permissions(&root, Permissions::from_mode(0o555)).expect(message_expect_folder); + assert_eq!( + format!("Error happened about file's permission {}", path_file_string.clone()), + handle_permission_denied(path_file_string.clone()) + ); + reset_permissions(root.clone(), river_song_folder.clone(), &file); + + file.set_permissions(Permissions::from_mode(0o555)) + .expect(message_expect_file); + fs::set_permissions(&river_song_folder, Permissions::from_mode(0o000)).expect(message_expect_folder); + fs::set_permissions(&root, Permissions::from_mode(0o555)).expect(message_expect_folder); + assert_eq!( + format!("Error when trying to read the file: {}", path_file_string.clone()), + handle_permission_denied(path_file_string.clone()) + ); + reset_permissions(root.clone(), river_song_folder.clone(), &file); + + file.set_permissions(Permissions::from_mode(0o555)) + .expect(message_expect_file); + fs::set_permissions(&river_song_folder, Permissions::from_mode(0o555)).expect(message_expect_folder); + fs::set_permissions(&root, Permissions::from_mode(0o000)).expect(message_expect_folder); + assert_eq!( + format!("Error when trying to read the file: {}", path_file_string.clone()), + handle_permission_denied(path_file_string.clone()) + ); + reset_permissions(root.clone(), river_song_folder.clone(), &file); + } + + #[test] + fn test_handle_not_found() { + let tmp = std::env::temp_dir(); + let root: std::path::PathBuf = tmp.join("river_folder/"); + if root.exists() { + std::fs::remove_dir_all(&root).unwrap(); + } + std::fs::create_dir_all(&root).unwrap(); + fs::set_permissions(&root, Permissions::from_mode(0o777)).expect("Can't change folder's permissions"); + std::env::set_current_dir(&root).expect("Error when trying to modify working directory"); + + let path_file = root.join("script.sh"); + let path_non_existing_file = root.join("scripts.sh"); + let path_file_string = path_file.clone().into_os_string().into_string().unwrap(); + let path_non_existing_file_string = path_non_existing_file.clone().into_os_string().into_string().unwrap(); + std::fs::write( + path_file.clone(), + format!( + "#!/bin/sh\n + echo \"Hello\"\n + sleep 2" + ), + ) + .unwrap(); + let file = match File::open(&path_file) { + Err(why) => panic!("couldn't open {}: {}", path_file.display(), why), + Ok(file) => file, + }; + file.set_permissions(Permissions::from_mode(0o777)) + .expect("Can't modify file's permissions"); + let args: Vec = vec![]; + + assert_eq!( + "File not found but another appears to match", + handle_not_found(path_file_string.clone(), args.clone()) + ); + assert_eq!( + "File not found but another appears to match", + handle_not_found(path_non_existing_file_string.clone(), args.clone()) + ); + assert_eq!( + "File not found but another appears to match", + handle_not_found("scripts.sh".to_owned(), args.clone()) + ); + assert_eq!( + "File not found but another appears to match", + handle_not_found("./script.sh".to_owned(), args.clone()) + ); + assert_eq!( + "Sorry but the file was not found", + handle_not_found("./scriptAAAAAAAAAA.sh".to_owned(), args.clone()) + ); + + let path_file_distance2 = root.join("scriptAA.sh"); + std::fs::write( + path_file_distance2.clone(), + format!( + "#!/bin/sh\n + echo \"Bye\"\n + sleep 2" + ), + ) + .unwrap(); + assert_eq!( + "File not found but another appears to match", + handle_not_found("./scripts.sh".to_owned(), args.clone()) + ); + assert_eq!( + "File not found but another appears to match", + handle_not_found("./script.sh".to_owned(), args.clone()) + ); + } +}