Better defaults management, logger class implementation with logging levels, and write to file configuration implementation

master
Chris Punches 2025-03-03 03:56:51 -05:00
parent e79fa3b89f
commit ee1df1fb0c
14 changed files with 389 additions and 67 deletions

View File

@ -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")
FILES_MATCHING PATTERN "*.so"
)

View File

@ -1,3 +1,4 @@
[logging]
log_file = /var/log/dpm.log
write_to_log = true
write_to_log = true
log_level = INFO

View File

@ -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();

52
include/DPMDefaults.hpp Normal file
View File

@ -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 <chris.punches@silogroup.org>
*
* 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 <https://www.gnu.org/licenses/>.
*
* For bug reports or contributions, please contact the dhlp-contributors
* mailing list at: https://lists.darkhorselinux.org/mailman/listinfo/dhlp-contributors
*/
#pragma once
#include <string>
#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;

46
include/Logger.hpp Normal file
View File

@ -0,0 +1,46 @@
// Logger.hpp
#pragma once
#include <fstream>
#include <iostream>
#include <ctime>
#include <string>
#include <filesystem>
#include <stdexcept>
#include <cstdlib>
#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;

10
include/LoggingLevels.hpp Normal file
View File

@ -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
};

View File

@ -39,6 +39,7 @@
#include "error.hpp"
#include "ModuleLoader.hpp"
#include "dpm_interface_helpers.hpp"
#include "Logger.hpp"
/*
*

View File

@ -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 <iostream>
#include <getopt.h>
#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;
};

View File

@ -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;

View File

@ -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";

149
src/Logger.cpp Normal file
View File

@ -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;
}
}
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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);