diff --git a/CMakeLists.txt b/CMakeLists.txt index e9f2993..f11813e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,8 @@ set(CMAKE_CXX_STANDARD 20) # Create modules directory file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/modules) -add_executable(dpm +add_executable( + dpm src/dpm.cpp src/ModuleLoader.cpp src/dpm_interface.cpp @@ -15,6 +16,7 @@ add_executable(dpm src/handlers.cpp src/module_interface.cpp src/ConfigManager.cpp + src/Logger.cpp ) target_include_directories(dpm PRIVATE include) @@ -25,7 +27,8 @@ target_link_options(dpm PRIVATE -rdynamic) # Add the info module add_library(info MODULE modules/info.cpp) -set_target_properties(info PROPERTIES +set_target_properties( + info PROPERTIES PREFIX "" SUFFIX ".so" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/modules" @@ -34,13 +37,16 @@ set_target_properties(info PROPERTIES # Installation rules install(TARGETS dpm DESTINATION bin) install(DIRECTORY DESTINATION /etc/dpm/conf.d) -install(DIRECTORY "${CMAKE_SOURCE_DIR}/data/" +install( + DIRECTORY "${CMAKE_SOURCE_DIR}/data/" DESTINATION /etc/dpm/conf.d FILES_MATCHING PATTERN "*.conf" ) # Install all .so files from build/modules to the module path -install(DIRECTORY ${CMAKE_BINARY_DIR}/modules/ +install( + DIRECTORY ${CMAKE_BINARY_DIR}/modules/ DESTINATION /usr/lib/dpm/modules - FILES_MATCHING PATTERN "*.so") \ No newline at end of file + FILES_MATCHING PATTERN "*.so" +) \ No newline at end of file diff --git a/data/logging.conf b/data/logging.conf index 95a70c5..c18ec07 100644 --- a/data/logging.conf +++ b/data/logging.conf @@ -1,3 +1,4 @@ [logging] log_file = /var/log/dpm.log -write_to_log = true \ No newline at end of file +write_to_log = true +log_level = INFO \ No newline at end of file diff --git a/include/ConfigManager.hpp b/include/ConfigManager.hpp index fc71db6..c329757 100644 --- a/include/ConfigManager.hpp +++ b/include/ConfigManager.hpp @@ -48,7 +48,13 @@ class ConfigManager { public: // Constructor - ConfigManager( const std::string& config_dir = DPMDefaultPaths::CONFIG_DIR ); + ConfigManager(); + + // Set the configuration directory + void setConfigDir(const std::string& config_dir); + + // Get the current configuration directory + std::string getConfigDir() const; // Load all configuration files from the config directory bool loadConfigurations(); diff --git a/include/DPMDefaults.hpp b/include/DPMDefaults.hpp new file mode 100644 index 0000000..aa1302f --- /dev/null +++ b/include/DPMDefaults.hpp @@ -0,0 +1,52 @@ +/** + * @file DPMDefaults.hpp + * @brief Default configuration values for the DPM utility + * + * Defines the DPMDefaults structure which provides default configuration values + * for paths, logging settings, and other system-wide defaults used by the DPM + * utility when explicit configuration is not provided. + * + * @copyright Copyright (c) 2025 SILO GROUP LLC + * @author Chris Punches + * + * Part of the Dark Horse Linux Package Manager (DPM) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * For bug reports or contributions, please contact the dhlp-contributors + * mailing list at: https://lists.darkhorselinux.org/mailman/listinfo/dhlp-contributors + */ + +#pragma once + +#include +#include "LoggingLevels.hpp" + +// default system configuration +struct DPMDefaults { + static const char* const MODULE_PATH; + static const char* const CONFIG_DIR; + static const char* const LOG_FILE; + static const bool write_to_log; + static const LoggingLevels LOG_LEVEL; +}; + +// Initialize static constants +inline const char * const DPMDefaults::MODULE_PATH = "/usr/lib/dpm/modules/"; + +inline const char * const DPMDefaults::CONFIG_DIR = "/etc/dpm/conf.d/"; + +inline const char * const DPMDefaults::LOG_FILE = "/var/log/dpm.log"; +inline const bool DPMDefaults::write_to_log = false; +inline const LoggingLevels DPMDefaults::LOG_LEVEL = LoggingLevels::INFO; \ No newline at end of file diff --git a/include/Logger.hpp b/include/Logger.hpp new file mode 100644 index 0000000..84587d8 --- /dev/null +++ b/include/Logger.hpp @@ -0,0 +1,46 @@ +// Logger.hpp +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "LoggingLevels.hpp" +#include "DPMDefaults.hpp" + +class Logger { +public: + // constructor + Logger(); + + // destructor + ~Logger(); + + // Log method that accepts a string + void log(LoggingLevels log_level, const std::string& message); + + // Configuration setters + void setLogFile(const std::string& log_file); + void setWriteToLog(bool write_to_log); + void setLogLevel(LoggingLevels log_level); + + // String to LoggingLevels conversion + static LoggingLevels stringToLogLevel(const std::string& level_str, LoggingLevels default_level = LoggingLevels::INFO); + +private: + // the logging level to stay initialized to + LoggingLevels log_level; + + // whether or not to log to file + bool log_to_file; + + // log file path + std::string log_file; +}; + +// Global logger instance +extern Logger g_logger; diff --git a/include/LoggingLevels.hpp b/include/LoggingLevels.hpp new file mode 100644 index 0000000..c9e8a60 --- /dev/null +++ b/include/LoggingLevels.hpp @@ -0,0 +1,10 @@ +#pragma once + +// Log level enum that will be accessible to modules +enum LoggingLevels { + FATAL = 0, + ERROR = 1, + WARN = 2, + INFO = 3, + DEBUG = 4 +}; diff --git a/include/dpm_interface.hpp b/include/dpm_interface.hpp index eece612..7dfd368 100644 --- a/include/dpm_interface.hpp +++ b/include/dpm_interface.hpp @@ -39,6 +39,7 @@ #include "error.hpp" #include "ModuleLoader.hpp" #include "dpm_interface_helpers.hpp" +#include "Logger.hpp" /* * diff --git a/include/dpm_interface_helpers.hpp b/include/dpm_interface_helpers.hpp index b7d95cd..d1c21eb 100644 --- a/include/dpm_interface_helpers.hpp +++ b/include/dpm_interface_helpers.hpp @@ -1,5 +1,5 @@ /** - * @file dpm_interface_helpers.hpp +* @file dpm_interface_helpers.hpp * @brief Helper functions for DPM command-line interface * * Provides utility functions for command-line argument parsing and @@ -34,18 +34,17 @@ #include #include +#include "Logger.hpp" +#include "LoggingLevels.hpp" +#include "DPMDefaults.hpp" + // data structure for supplied arguments struct CommandArgs { std::string module_path; + std::string config_dir; std::string module_name; - std::string command; // All arguments combined into a single command string + std::string command; }; // parse dpm cli arguments into a serialized structure CommandArgs parse_args( int argc, char * argv[] ); - -// default system paths -struct DPMDefaultPaths { - static const std::string MODULE_PATH; - static const std::string CONFIG_DIR; -}; diff --git a/modules/info.cpp b/modules/info.cpp index dec1370..e252e87 100644 --- a/modules/info.cpp +++ b/modules/info.cpp @@ -39,14 +39,6 @@ #define MODULE_VERSION "0.1.0" #define DPM_VERSION "0.1.0" -// Command enum for switch case -enum Command { - CMD_UNKNOWN, - CMD_HELP, - CMD_VERSION, - CMD_SYSTEM, - CMD_CONFIG -}; // Declaration of the DPM config function we want to call extern "C" const char* dpm_get_config(const char* section, const char* key); @@ -64,6 +56,15 @@ extern "C" const char* dpm_get_description(void) { return "DPM Info Module - Provides information about the DPM system"; } +// Command enum for switch case +enum Command { + CMD_UNKNOWN, + CMD_HELP, + CMD_VERSION, + CMD_SYSTEM, + CMD_CONFIG +}; + // Function to detect architecture using uname std::string detect_architecture() { struct utsname system_info; diff --git a/src/ConfigManager.cpp b/src/ConfigManager.cpp index 836c81a..b12f006 100644 --- a/src/ConfigManager.cpp +++ b/src/ConfigManager.cpp @@ -34,8 +34,8 @@ // Global configuration manager instance ConfigManager g_config_manager; -ConfigManager::ConfigManager(const std::string& config_dir) - : _config_dir(config_dir) +ConfigManager::ConfigManager() + : _config_dir(DPMDefaults::CONFIG_DIR) { // Ensure the config directory ends with a slash if (!_config_dir.empty() && _config_dir.back() != '/') { @@ -43,6 +43,21 @@ ConfigManager::ConfigManager(const std::string& config_dir) } } +void ConfigManager::setConfigDir(const std::string& config_dir) +{ + _config_dir = config_dir; + + // Ensure the config directory ends with a slash + if (!_config_dir.empty() && _config_dir.back() != '/') { + _config_dir += '/'; + } +} + +std::string ConfigManager::getConfigDir() const +{ + return _config_dir; +} + std::string ConfigManager::trimWhitespace(const std::string& str) const { const std::string whitespace = " \t\n\r\f\v"; diff --git a/src/Logger.cpp b/src/Logger.cpp new file mode 100644 index 0000000..a1cad88 --- /dev/null +++ b/src/Logger.cpp @@ -0,0 +1,149 @@ +// Logger.cpp +#include "Logger.hpp" + +// Global logger instance +Logger g_logger; + +Logger::Logger() + : log_level(DPMDefaults::LOG_LEVEL), + log_to_file(DPMDefaults::write_to_log), + log_file(DPMDefaults::LOG_FILE) +{ +} + +Logger::~Logger() +{ +} + +void Logger::setLogFile(const std::string& new_log_file) +{ + log_file = new_log_file; + + // If logging to file is enabled, ensure the log directory exists and is writable + if (log_to_file) { + std::filesystem::path log_path(log_file); + std::filesystem::path log_dir = log_path.parent_path(); + + // Check if the directory exists, create if not + if (!log_dir.empty() && !std::filesystem::exists(log_dir)) { + try { + if (!std::filesystem::create_directories(log_dir)) { + std::cerr << "FATAL: Failed to create log directory: " << log_dir.string() << std::endl; + exit(1); + } + } catch (const std::filesystem::filesystem_error& e) { + std::cerr << "FATAL: Error creating log directory: " << e.what() << std::endl; + exit(1); + } + } + + // Verify we can write to the log file + try { + std::ofstream test_log_file(log_file, std::ios::app); + if (!test_log_file.is_open()) { + std::cerr << "FATAL: Cannot open log file for writing: " << log_file << std::endl; + exit(1); + } + test_log_file.close(); + } catch (const std::exception& e) { + std::cerr << "FATAL: Error validating log file access: " << e.what() << std::endl; + exit(1); + } + } +} + +void Logger::setWriteToLog(bool new_write_to_log) +{ + log_to_file = new_write_to_log; + + // If logging was just enabled, validate the log file + if (log_to_file) { + setLogFile(log_file); + } +} + +void Logger::setLogLevel(LoggingLevels new_log_level) +{ + log_level = new_log_level; +} + +LoggingLevels Logger::stringToLogLevel(const std::string& level_str, LoggingLevels default_level) +{ + if (level_str == "FATAL") { + return LoggingLevels::FATAL; + } else if (level_str == "ERROR") { + return LoggingLevels::ERROR; + } else if (level_str == "WARN") { + return LoggingLevels::WARN; + } else if (level_str == "INFO") { + return LoggingLevels::INFO; + } else if (level_str == "DEBUG") { + return LoggingLevels::DEBUG; + } + + // Return default if no match + return default_level; +} + +void Logger::log(LoggingLevels message_level, const std::string& message) +{ + // Only process if the message level is less than or equal to the configured level + if (message_level <= log_level) { + // Convert log level to string + std::string level_str; + switch (message_level) { + case LoggingLevels::FATAL: + level_str = "FATAL"; + break; + case LoggingLevels::ERROR: + level_str = "ERROR"; + break; + case LoggingLevels::WARN: + level_str = "WARN"; + break; + case LoggingLevels::INFO: + level_str = "INFO"; + break; + case LoggingLevels::DEBUG: + level_str = "DEBUG"; + break; + default: + level_str = "UNKNOWN"; + break; + } + + // Console output without timestamp + if (message_level == LoggingLevels::FATAL || + message_level == LoggingLevels::ERROR || + message_level == LoggingLevels::WARN) { + // Send to stderr + std::cerr << level_str << ": " << message << std::endl; + } else { + // Send to stdout + std::cout << message << std::endl; + } + + // Write to log file if enabled (with timestamp) + if (log_to_file) { + try { + // Get current time for timestamp (only for log file) + std::time_t now = std::time(nullptr); + char timestamp[32]; + std::strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", std::localtime(&now)); + + // Full formatted message with timestamp for log file + std::string formatted_message = std::string(timestamp) + " [" + level_str + "] " + message; + + std::ofstream log_stream(log_file, std::ios::app); + if (log_stream.is_open()) { + log_stream << formatted_message << std::endl; + log_stream.close(); + } else { + std::cerr << "Failed to write to log file: " << log_file << std::endl; + } + } catch (const std::exception& e) { + std::cerr << "Error writing to log file: " << e.what() << std::endl; + } + } + } +} diff --git a/src/dpm.cpp b/src/dpm.cpp index f883f5f..ef4c640 100644 --- a/src/dpm.cpp +++ b/src/dpm.cpp @@ -36,6 +36,7 @@ #include "dpm_interface_helpers.hpp" #include "error.hpp" #include "ConfigManager.hpp" +#include "Logger.hpp" /* * DPM serves three functions: @@ -60,63 +61,93 @@ int default_behavior(const ModuleLoader& loader) * @param argv Array of C-style strings containing the arguments * @return Exit code indicating success (0) or failure (non-zero) */ -int main(int argc, char* argv[]) +int main( int argc, char* argv[] ) { - // Load configuration files - if (!g_config_manager.loadConfigurations()) { - std::cerr << "Warning: No configuration files present or loaded from '" << DPMDefaultPaths::CONFIG_DIR << "*.conf', reverting to defaults." << std::endl; - // Continue execution, as we might be able to use default values + // process the arguments supplied to DPM and provide + // an object that contains them for command and routing + // processing + CommandArgs args = parse_args( argc, argv ); + + // Set the configuration directory path (CLI argument takes precedence over defaults) + if ( !args.config_dir.empty() ) + { + // args.config_dir was supplied so set it + g_config_manager.setConfigDir( args.config_dir ); + } else { + // args.config_dir was not supplied, so fall back to default path + g_config_manager.setConfigDir( DPMDefaults::CONFIG_DIR ); } - // process the arguments suppplied to DPM and provide - // an object that contains them for command and routing - // processing - this will include any module_path from CLI - auto args = parse_args(argc, argv); + // Load configuration files + if ( !g_config_manager.loadConfigurations() ) + { + // failed to load any configuration files, so alert the user + std::cerr << "Warning: No configuration files present or loaded from '" + << g_config_manager.getConfigDir() << "*.conf', reverting to defaults." << std::endl; + } + + // Configure logger (CLI args > config > defaults) + // Check configuration for log settings + bool config_write_to_log = g_config_manager.getConfigBool("logging", "write_to_log", DPMDefaults::write_to_log); + std::string config_log_file = g_config_manager.getConfigString("logging", "log_file", DPMDefaults::LOG_FILE); + + // Parse log_level from config using the new method + std::string log_level_str = g_config_manager.getConfigString("logging", "log_level", "INFO"); + LoggingLevels config_log_level = Logger::stringToLogLevel(log_level_str, DPMDefaults::LOG_LEVEL); + + // Configure global logger instance + g_logger.setLogLevel(config_log_level); + g_logger.setWriteToLog(config_write_to_log); + g_logger.setLogFile(config_log_file); // Determine the module path (CLI arg > config > default) std::string module_path; // If CLI argument was provided, use it - if (!args.module_path.empty()) { + if ( !args.module_path.empty() ) + { module_path = args.module_path; } else { // Otherwise, check configuration file - const char* config_module_path = g_config_manager.getConfigValue("modules", "module_path"); - if (config_module_path) { + const char * config_module_path = g_config_manager.getConfigValue( "modules", "module_path" ); + if ( config_module_path ) + { module_path = config_module_path; - } - // Finally, use default if nothing else is available - else { - module_path = DPMDefaultPaths::MODULE_PATH; + } else { + // use default if nothing else is available + module_path = DPMDefaults::MODULE_PATH; } } // create a module loader object with the determined path - ModuleLoader loader(module_path); + ModuleLoader loader( module_path ); // check the module path for the loader object - int path_check_result = main_check_module_path(loader); - if (path_check_result != 0) { + int path_check_result = main_check_module_path( loader ); + if ( path_check_result != 0 ) + { // exit if there's an error and ensure // it has an appropriate return code - return 1; + return path_check_result; } // if no module is provided to execute, then trigger the default // dpm behaviour - if (args.module_name.empty()) { - return default_behavior(loader); + if ( args.module_name.empty() ) + { + return default_behavior( loader ); } // execute the module - DPMErrorCategory execute_error = loader.execute_module(args.module_name, args.command); + DPMErrorCategory execute_error = loader.execute_module( args.module_name, args.command ); - std::string extracted_path; - loader.get_module_path(extracted_path); + std::string absolute_modules_path; + loader.get_module_path( absolute_modules_path ); - FlexDPMError result = make_error(execute_error); + // construct an error object + FlexDPMError result = make_error( execute_error ); result.module_name = args.module_name.c_str(); - result.module_path = extracted_path.c_str(); + result.module_path = absolute_modules_path.c_str(); // pair result with a message and exit with the appropriate error code return handle_error(result); diff --git a/src/dpm_interface.cpp b/src/dpm_interface.cpp index f391763..72b61a3 100644 --- a/src/dpm_interface.cpp +++ b/src/dpm_interface.cpp @@ -65,23 +65,23 @@ int main_check_module_path(const ModuleLoader& loader) loader.get_module_path(path); if (!std::filesystem::exists(path)) { - std::cerr << "FATAL: modules.modules_path does not exist: " << path << std::endl; + g_logger.log(LoggingLevels::FATAL, "modules.modules_path does not exist: " + path); return 1; } if (!std::filesystem::is_directory(path)) { - std::cerr << "FATAL: modules.modules_path is not a directory: " << path << std::endl; + g_logger.log(LoggingLevels::FATAL, "modules.modules_path is not a directory: " + path); return 1; } try { auto perms = std::filesystem::status(path).permissions(); if ((perms & std::filesystem::perms::owner_read) == std::filesystem::perms::none) { - std::cerr << "FATAL: Permission denied: " << path << std::endl; + g_logger.log(LoggingLevels::FATAL, "Permission denied: " + path); return 1; } } catch (const std::filesystem::filesystem_error&) { - std::cerr << "FATAL: Permission denied: " << path << std::endl; + g_logger.log(LoggingLevels::FATAL, "Permission denied: " + path); return 1; } @@ -122,18 +122,18 @@ int main_list_modules(const ModuleLoader& loader) // set the module path DPMErrorCategory get_path_error = loader.get_module_path(path); if ( get_path_error != DPMErrorCategory::SUCCESS ) { - std::cerr << "Failed to get modules.modules_path" << std::endl; + g_logger.log(LoggingLevels::FATAL, "Failed to get modules.modules_path"); return 1; } DPMErrorCategory list_error = loader.list_available_modules(modules); if (list_error != DPMErrorCategory::SUCCESS) { - std::cerr << "FATAL: No modules found in modules.modules_path: " << path << std::endl; + g_logger.log(LoggingLevels::FATAL, "No modules found in modules.modules_path: " + path); return 1; } if (modules.empty()) { - std::cerr << "FATAL: No modules found in modules.modules_path: '" << path << "'." << std::endl; + g_logger.log(LoggingLevels::FATAL, "No modules found in modules.modules_path: '" + path + "'."); return 0; } @@ -154,8 +154,8 @@ int main_list_modules(const ModuleLoader& loader) dlclose(handle); } - if (valid_modules.empty()) { - std::cerr << "FATAL: No valid modules found in modules.modules_path: '" << path << "'." << std::endl; + if ( valid_modules.empty() ) { + g_logger.log(LoggingLevels::FATAL, "No valid modules found in modules.modules_path: '" + path + "'."); return 0; } @@ -193,7 +193,7 @@ int main_list_modules(const ModuleLoader& loader) const int column_spacing = 4; // Display the table header - std::cout << "\nAvailable modules in modules.modules_path: '" << path << "':" << std::endl << std::endl; + g_logger.log(LoggingLevels::DEBUG, "\nAvailable modules in modules.modules_path: '" + path + "':\n"); std::cout << std::left << std::setw(max_name_length + column_spacing) << "MODULE" << std::setw(max_version_length + column_spacing) << "VERSION" << "DESCRIPTION" << std::endl; diff --git a/src/dpm_interface_helpers.cpp b/src/dpm_interface_helpers.cpp index f8a8bd7..3c97bfc 100644 --- a/src/dpm_interface_helpers.cpp +++ b/src/dpm_interface_helpers.cpp @@ -30,9 +30,7 @@ #include "dpm_interface_helpers.hpp" -// Define the static constants -const std::string DPMDefaultPaths::MODULE_PATH = "/usr/lib/dpm/modules/"; -const std::string DPMDefaultPaths::CONFIG_DIR = "/etc/dpm/conf.d/"; + /** * Parse command line arguments for DPM. @@ -47,6 +45,7 @@ const std::string DPMDefaultPaths::CONFIG_DIR = "/etc/dpm/conf.d/"; * * The function handles the following arguments: * - ``-m, --module-path PATH``: Sets the directory path where DPM modules are located + * - ``-c, --config-dir PATH``: Sets the directory path where DPM configuration files are located * - ``-h, --help``: Displays a help message and exits * * Additional arguments are processed as follows: @@ -61,25 +60,31 @@ CommandArgs parse_args(int argc, char* argv[]) { CommandArgs args; args.module_path = ""; + args.config_dir = ""; static struct option long_options[] = { {"module-path", required_argument, 0, 'm'}, + {"config-dir", required_argument, 0, 'c'}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} }; int opt; int option_index = 0; - while ((opt = getopt_long(argc, argv, "m:h", long_options, &option_index)) != -1) { + while ((opt = getopt_long(argc, argv, "m:c:h", long_options, &option_index)) != -1) { switch (opt) { case 'm': args.module_path = optarg; break; + case 'c': + args.config_dir = optarg; + break; case 'h': std::cout << "Usage: dpm [options] [module-name] [module args...] [module-command] [command-args]\n\n" << "Options:\n\n" << " -m, --module-path PATH Path to DPM modules (overrides modules.modules_path in config)\n" - << " -h, --help Show this help message\n\n"; + << " -c, --config-dir PATH Path to DPM configuration directory\n" + << " -h, --help Show this help message\n\n"; exit(0); case '?': exit(1);