mirror of
https://github.com/EvernodeXRPL/sashimono.git
synced 2026-07-26 16:40:20 +00:00
Initial project structure. (#1)
This commit is contained in:
committed by
GitHub
parent
5da5d4e303
commit
220d65bd0b
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
*~
|
||||
*.swp
|
||||
*.swo
|
||||
.*.sw?
|
||||
.DS_Store
|
||||
build
|
||||
.vscode
|
||||
**/Makefile
|
||||
**/CMakeCache.txt
|
||||
**/cmake_install.cmake
|
||||
**/CMakeFiles
|
||||
28
CMakeLists.txt
Normal file
28
CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(sagent)
|
||||
|
||||
# Force build type to Release build.
|
||||
set(CMAKE_BUILD_TYPE Release)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY build)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY build)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY build)
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-result -Wreturn-type")
|
||||
|
||||
add_executable(sagent
|
||||
src/util/util.cpp
|
||||
src/conf.cpp
|
||||
src/salog.cpp
|
||||
src/sqlite.cpp
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(sagent
|
||||
sqlite3
|
||||
${CMAKE_DL_LIBS} # Needed for stacktrace support
|
||||
)
|
||||
|
||||
target_precompile_headers(sagent PUBLIC src/pchheader.hpp)
|
||||
39
README.md
39
README.md
@@ -1,20 +1,27 @@
|
||||
# Introduction
|
||||
TODO: Give a short introduction of your project. Let this section explain the objectives or the motivation behind this project.
|
||||
# Sashimono Agent
|
||||
|
||||
# Getting Started
|
||||
TODO: Guide users through getting your code up and running on their own system. In this section you can talk about:
|
||||
1. Installation process
|
||||
2. Software dependencies
|
||||
3. Latest releases
|
||||
4. API references
|
||||
## What's here?
|
||||
*In development*
|
||||
|
||||
# Build and Test
|
||||
TODO: Describe and show how to build your code and run the tests.
|
||||
A C++ version of sashimono agent
|
||||
|
||||
# Contribute
|
||||
TODO: Explain how other users and developers can contribute to make your code better.
|
||||
## Libraries
|
||||
|
||||
If you want to learn more about creating good readme files then refer the following [guidelines](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files:
|
||||
- [ASP.NET Core](https://github.com/aspnet/Home)
|
||||
- [Visual Studio Code](https://github.com/Microsoft/vscode)
|
||||
- [Chakra Core](https://github.com/Microsoft/ChakraCore)
|
||||
## Setting up Sashimono Agent environment
|
||||
Run the setup script located at the repo root (tested on Ubuntu 18.04).
|
||||
```
|
||||
./dev-setup.sh
|
||||
```
|
||||
|
||||
## Build Sashimono Agent
|
||||
1. Run `cmake .` (You only have to do this once)
|
||||
1. Run `make` (Sashimono agent binary will be created as `./build/sagent`)
|
||||
|
||||
## Code structure
|
||||
Code is divided into subsystems via namespaces.
|
||||
|
||||
**conf::** Handles configuration. Loads and holds the central configuration object. Used by most of the subsystems.
|
||||
|
||||
**util::** Contains shared data structures/helper functions used by multiple subsystems.
|
||||
|
||||
**sqlite::** Contains sqlite database management related helper functions.
|
||||
53
dev-setup.sh
Executable file
53
dev-setup.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# Usage ./dev-setup.sh
|
||||
# Sashimono agent build environment setup script.
|
||||
|
||||
set -e # exit on error
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libssl-dev
|
||||
|
||||
workdir=~/sagent-setup
|
||||
|
||||
mkdir $workdir
|
||||
pushd $workdir > /dev/null 2>&1
|
||||
|
||||
# CMAKE
|
||||
cmake=cmake-3.16.0-rc3-Linux-x86_64
|
||||
wget https://github.com/Kitware/CMake/releases/download/v3.16.0-rc3/$cmake.tar.gz
|
||||
tar -zxvf $cmake.tar.gz
|
||||
sudo cp -r $cmake/bin/* /usr/local/bin/
|
||||
sudo cp -r $cmake/share/* /usr/local/share/
|
||||
rm $cmake.tar.gz && rm -r $cmake
|
||||
|
||||
# jsoncons
|
||||
wget https://github.com/danielaparker/jsoncons/archive/v0.153.3.tar.gz
|
||||
tar -zxvf v0.153.3.tar.gz
|
||||
pushd jsoncons-0.153.3 > /dev/null 2>&1
|
||||
sudo cp -r include/jsoncons /usr/local/include/
|
||||
sudo mkdir -p /usr/local/include/jsoncons_ext/
|
||||
sudo cp -r include/jsoncons_ext/bson /usr/local/include/jsoncons_ext/
|
||||
popd > /dev/null 2>&1
|
||||
rm v0.153.3.tar.gz && rm -r jsoncons-0.153.3
|
||||
|
||||
# Sqlite
|
||||
sudo apt-get install -y sqlite3 libsqlite3-dev
|
||||
|
||||
# Plog
|
||||
wget https://github.com/SergiusTheBest/plog/archive/1.1.5.tar.gz
|
||||
tar -zxvf 1.1.5.tar.gz
|
||||
pushd plog-1.1.5 > /dev/null 2>&1
|
||||
sudo cp -r include/plog /usr/local/include/
|
||||
popd > /dev/null 2>&1
|
||||
rm 1.1.5.tar.gz && rm -r plog-1.1.5
|
||||
|
||||
# Update linker library cache.
|
||||
sudo ldconfig
|
||||
|
||||
# Pop workdir
|
||||
popd > /dev/null 2>&1
|
||||
rm -r $workdir
|
||||
|
||||
# Build sagent
|
||||
cmake .
|
||||
make
|
||||
334
src/conf.cpp
Normal file
334
src/conf.cpp
Normal file
@@ -0,0 +1,334 @@
|
||||
#include "conf.hpp"
|
||||
#include "util/util.hpp"
|
||||
|
||||
namespace conf
|
||||
{
|
||||
// Global contract context struct exposed to the application.
|
||||
sa_context ctx;
|
||||
|
||||
// Global configuration struct exposed to the application.
|
||||
sa_config cfg;
|
||||
|
||||
constexpr int FILE_PERMS = 0644;
|
||||
|
||||
bool init_success = false;
|
||||
|
||||
/**
|
||||
* Loads and initializes the config for execution. Must be called once during application startup.
|
||||
* @return 0 for success. -1 for failure.
|
||||
*/
|
||||
int init()
|
||||
{
|
||||
if (validate_dir_paths() == -1 ||
|
||||
read_config(cfg) == -1 ||
|
||||
validate_config(cfg) == -1)
|
||||
return -1;
|
||||
|
||||
init_success = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup any resources.
|
||||
*/
|
||||
void deinit()
|
||||
{
|
||||
if (init_success)
|
||||
{
|
||||
// Deinit here.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create config here.
|
||||
* @return 0 for success. -1 for failure.
|
||||
*/
|
||||
int create()
|
||||
{
|
||||
if (util::is_dir_exists(ctx.config_dir))
|
||||
{
|
||||
if (util::is_file_exists(ctx.config_file))
|
||||
{
|
||||
std::cerr << "Config file already exists. Cannot create config at the same location.\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Recursivly create contract directory. Return an error if unable to create
|
||||
if (util::create_dir_tree_recursive(ctx.config_dir) == -1 ||
|
||||
util::create_dir_tree_recursive(ctx.log_dir) == -1)
|
||||
{
|
||||
std::cerr << "ERROR: unable to create directories.\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
//Create config file with default settings.
|
||||
//We populate the in-memory struct with default settings and then save it to the file.
|
||||
{
|
||||
sa_config cfg = {};
|
||||
|
||||
cfg.version = "0.0.1";
|
||||
cfg.log.max_file_count = 50;
|
||||
cfg.log.max_mbytes_per_file = 10;
|
||||
cfg.log.log_level = "inf";
|
||||
cfg.log.loggers.emplace("console");
|
||||
cfg.log.loggers.emplace("file");
|
||||
|
||||
//Save the default settings into the config file.
|
||||
if (write_config(cfg) != 0)
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::cout << "Config file created at " << ctx.config_file << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the context with directory paths based on provided base directory.
|
||||
* This is called after parsing SA command line arg in order to populate the ctx.
|
||||
* @param basedir Path to base directory.
|
||||
*/
|
||||
void set_dir_paths(std::string basedir)
|
||||
{
|
||||
if (basedir.empty())
|
||||
{
|
||||
// This code branch will never execute the way main is currently coded, but it might change in future
|
||||
std::cerr << "Base directory must be specified\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Resolving the path through realpath will remove any trailing slash if present
|
||||
// Set config directory to the parent of the exec binary.
|
||||
basedir = dirname((char *)util::realpath(basedir).data());
|
||||
ctx.config_dir = basedir + "/cfg";
|
||||
ctx.config_file = ctx.config_dir + "/sa.cfg";
|
||||
ctx.log_dir = basedir + "/log";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the existence of all contract sub directories.
|
||||
* @return 0 for successful validation. -1 for failure.
|
||||
*/
|
||||
int validate_dir_paths()
|
||||
{
|
||||
const std::string paths[2] = {
|
||||
ctx.config_file,
|
||||
ctx.log_dir};
|
||||
|
||||
for (const std::string &path : paths)
|
||||
{
|
||||
if (!util::is_file_exists(path) && !util::is_dir_exists(path))
|
||||
{
|
||||
std::cerr << path << " does not exist.\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the config file on disk and populates the in-memory 'cfg' struct.
|
||||
* @param cfg Config to populate.
|
||||
* @return 0 for successful loading of config. -1 for failure.
|
||||
*/
|
||||
int read_config(sa_config &cfg)
|
||||
{
|
||||
int fd = open(ctx.config_file.data(), O_RDWR, 444);
|
||||
if (fd == -1)
|
||||
return -1;
|
||||
|
||||
// Read the config file into json document object.
|
||||
std::string buf;
|
||||
if (util::read_from_fd(fd, buf) == -1)
|
||||
{
|
||||
std::cerr << "Error reading from the config file. " << errno << '\n';
|
||||
return -1;
|
||||
}
|
||||
|
||||
jsoncons::ojson d;
|
||||
try
|
||||
{
|
||||
d = jsoncons::ojson::parse(buf, jsoncons::strict_json_parsing());
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
std::cerr << "Invalid config file format. " << e.what() << '\n';
|
||||
return -1;
|
||||
}
|
||||
buf.clear();
|
||||
|
||||
try
|
||||
{
|
||||
// Check whether the version is specified.
|
||||
cfg.version = d["version"].as<std::string>();
|
||||
if (cfg.version.empty())
|
||||
{
|
||||
std::cerr << "Config version missing.\n";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
std::cerr << "Required config field version missing at " << ctx.config_file << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::string jpath;
|
||||
|
||||
// log
|
||||
{
|
||||
jpath = "log";
|
||||
|
||||
try
|
||||
{
|
||||
const jsoncons::ojson &log = d["log"];
|
||||
cfg.log.log_level = log["log_level"].as<std::string>();
|
||||
cfg.log.log_level_type = get_loglevel_type(cfg.log.log_level);
|
||||
|
||||
cfg.log.max_mbytes_per_file = log["max_mbytes_per_file"].as<size_t>();
|
||||
cfg.log.max_file_count = log["max_file_count"].as<size_t>();
|
||||
cfg.log.loggers.clear();
|
||||
for (auto &v : log["loggers"].array_range())
|
||||
cfg.log.loggers.emplace(v.as<std::string>());
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
print_missing_field_error(jpath, e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the provided 'cfg' struct into the config file.
|
||||
* @param cfg Config to write.
|
||||
* @return 0 for successful save. -1 for failure.
|
||||
*/
|
||||
int write_config(const sa_config &cfg)
|
||||
{
|
||||
// Popualte json document with 'cfg' values.
|
||||
// ojson is used instead of json to preserve insertion order.
|
||||
jsoncons::ojson d;
|
||||
d.insert_or_assign("version", cfg.version);
|
||||
|
||||
// Log configs.
|
||||
{
|
||||
jsoncons::ojson log_config;
|
||||
log_config.insert_or_assign("log_level", cfg.log.log_level);
|
||||
log_config.insert_or_assign("max_mbytes_per_file", cfg.log.max_mbytes_per_file);
|
||||
log_config.insert_or_assign("max_file_count", cfg.log.max_file_count);
|
||||
|
||||
jsoncons::ojson loggers(jsoncons::json_array_arg);
|
||||
for (std::string_view logger : cfg.log.loggers)
|
||||
{
|
||||
loggers.push_back(logger);
|
||||
}
|
||||
log_config.insert_or_assign("loggers", loggers);
|
||||
d.insert_or_assign("log", log_config);
|
||||
}
|
||||
|
||||
return write_json_file(ctx.config_file, d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given json doc to a file.
|
||||
* @param file_path Path to the file.
|
||||
* @param d Json object.
|
||||
* @return 0 on success. -1 on failure.
|
||||
*/
|
||||
int write_json_file(const std::string &file_path, const jsoncons::ojson &d)
|
||||
{
|
||||
std::string json;
|
||||
// Convert json object to a string.
|
||||
try
|
||||
{
|
||||
jsoncons::json_options options;
|
||||
options.object_array_line_splits(jsoncons::line_split_kind::multi_line);
|
||||
options.spaces_around_comma(jsoncons::spaces_option::no_spaces);
|
||||
std::ostringstream os;
|
||||
os << jsoncons::pretty_print(d, options);
|
||||
json = os.str();
|
||||
os.clear();
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
std::cerr << "Converting json to string failed. " << file_path << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// O_TRUNC flag is used to trucate existing content from the file.
|
||||
const int fd = open(file_path.data(), O_CREAT | O_RDWR | O_TRUNC, FILE_PERMS);
|
||||
if (fd == -1 || write(fd, json.data(), json.size()) == -1)
|
||||
{
|
||||
std::cerr << "Writing file failed. " << file_path << std::endl;
|
||||
if (fd != -1)
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to Log Severity enum type.
|
||||
* @param severity log severity code.
|
||||
* @return log severity type.
|
||||
*/
|
||||
LOG_SEVERITY get_loglevel_type(std::string_view severity)
|
||||
{
|
||||
if (severity == "dbg")
|
||||
return LOG_SEVERITY::DEBUG;
|
||||
else if (severity == "wrn")
|
||||
return LOG_SEVERITY::WARN;
|
||||
else if (severity == "inf")
|
||||
return LOG_SEVERITY::INFO;
|
||||
else
|
||||
return LOG_SEVERITY::ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the config json parsing field missing error.
|
||||
* @param jpath Json path of the feild.
|
||||
* @param e Exception.
|
||||
*/
|
||||
void print_missing_field_error(std::string_view jpath, const std::exception &e)
|
||||
{
|
||||
// Extract field name from jsoncons exception message.
|
||||
std::cerr << "Config validation error: " << e.what() << " in '" << jpath << "' section at " << ctx.config_file << std::endl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the 'cfg' struct for invalid values.
|
||||
* @param cfg Config to validate.
|
||||
* @return 0 for successful validation. -1 for failure.
|
||||
*/
|
||||
int validate_config(const sa_config &cfg)
|
||||
{
|
||||
// Other required fields.
|
||||
|
||||
bool fields_invalid = false;
|
||||
fields_invalid |= cfg.log.log_level.empty() && std::cerr << "Invalid value for loglevel.\n";
|
||||
|
||||
if (fields_invalid)
|
||||
{
|
||||
std::cerr << "Invalid configuration values at " << ctx.config_file << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const std::unordered_set<std::string> valid_loglevels({"dbg", "inf", "wrn", "err"});
|
||||
if (valid_loglevels.count(cfg.log.log_level) != 1)
|
||||
{
|
||||
std::cerr << "Invalid loglevel configured. Valid values: dbg|inf|wrn|err\n";
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
73
src/conf.hpp
Normal file
73
src/conf.hpp
Normal file
@@ -0,0 +1,73 @@
|
||||
#ifndef _SA_CONF_
|
||||
#define _SA_CONF_
|
||||
|
||||
#include "pchheader.hpp"
|
||||
|
||||
namespace conf
|
||||
{
|
||||
// Log severity levels used in Sashimono agent.
|
||||
enum LOG_SEVERITY
|
||||
{
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARN,
|
||||
ERROR
|
||||
};
|
||||
|
||||
struct log_config
|
||||
{
|
||||
std::string log_level; // Log severity level (dbg, inf, wrn, wrr)
|
||||
LOG_SEVERITY log_level_type; // Log severity level enum (debug, info, warn, error)
|
||||
std::unordered_set<std::string> loggers; // List of enabled loggers (console, file)
|
||||
size_t max_mbytes_per_file = 0; // Max MB size of a single log file.
|
||||
size_t max_file_count = 0; // Max no. of log files to keep.
|
||||
};
|
||||
|
||||
struct sa_config
|
||||
{
|
||||
std::string version;
|
||||
log_config log;
|
||||
};
|
||||
|
||||
struct sa_context
|
||||
{
|
||||
std::string command; // The CLI command issued to launch Sashimono agent
|
||||
|
||||
std::string config_dir; // Config dir full path.
|
||||
std::string config_file; // Full path to the config file.
|
||||
std::string log_dir; // Log directory full path.
|
||||
};
|
||||
|
||||
// Global context struct exposed to the application.
|
||||
// Other modules will access context values via this.
|
||||
extern sa_context ctx;
|
||||
|
||||
// Global configuration struct exposed to the application.
|
||||
// Other modules will access config values via this.
|
||||
extern sa_config cfg;
|
||||
|
||||
int init();
|
||||
|
||||
void deinit();
|
||||
|
||||
int create();
|
||||
|
||||
void set_dir_paths(std::string basedir);
|
||||
|
||||
int validate_dir_paths();
|
||||
|
||||
int read_config(sa_config &cfg);
|
||||
|
||||
int write_config(const sa_config &cfg);
|
||||
|
||||
int write_json_file(const std::string &file_path, const jsoncons::ojson &d);
|
||||
|
||||
LOG_SEVERITY get_loglevel_type(std::string_view severity);
|
||||
|
||||
void print_missing_field_error(std::string_view jpath, const std::exception &e);
|
||||
|
||||
int validate_config(const sa_config &cfg);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
104
src/main.cpp
Normal file
104
src/main.cpp
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
Entry point for Sashimono
|
||||
**/
|
||||
#include "pchheader.hpp"
|
||||
#include "conf.hpp"
|
||||
#include "sqlite.hpp"
|
||||
#include "salog.hpp"
|
||||
|
||||
/**
|
||||
* Parses CLI args and extracts sashimono agent command and parameters given.
|
||||
* @param argc Argument count.
|
||||
* @param argv Arguments.
|
||||
* @returns 0 on success, -1 on error.
|
||||
*/
|
||||
int parse_cmd(int argc, char **argv)
|
||||
{
|
||||
conf::ctx.command = argv[1];
|
||||
if (argc == 2 && //We get working dir as an arg anyway. So we need to check for ==2 args.
|
||||
(conf::ctx.command == "new" || conf::ctx.command == "run" || conf::ctx.command == "version"))
|
||||
{
|
||||
// We populate the global contract ctx with the detected command.
|
||||
conf::set_dir_paths(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If all extractions fail display help message.
|
||||
std::cerr << "Arguments mismatch.\n";
|
||||
std::cout << "Usage:\n";
|
||||
std::cout << "sagent version\n";
|
||||
std::cout << "sagent <command> (command = run | new | rekey)\n";
|
||||
std::cout << "Example: sagent run\n";
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs any cleanup on graceful application termination.
|
||||
*/
|
||||
void deinit()
|
||||
{
|
||||
conf::deinit();
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
// Extract the CLI args
|
||||
// This call will populate conf::ctx
|
||||
if (parse_cmd(argc, argv) != 0)
|
||||
return -1;
|
||||
|
||||
if (conf::ctx.command == "new")
|
||||
{
|
||||
// This will create a new config.
|
||||
if (conf::create() != 0)
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (conf::init() != 0)
|
||||
return -1;
|
||||
|
||||
salog::init(); // Initialize logger for SA.
|
||||
|
||||
if (conf::ctx.command == "run")
|
||||
{
|
||||
LOG_INFO << "Sashimono agent started. Version : " << conf::cfg.version << " Log level : " << conf::cfg.log.log_level;
|
||||
|
||||
// Run the program.
|
||||
|
||||
sqlite3 *db = NULL;
|
||||
const char *path = "db.sqlite";
|
||||
|
||||
if (sqlite::open_db(path, &db, true) == -1)
|
||||
{
|
||||
LOG_ERROR << "Error opening database";
|
||||
return -1;
|
||||
}
|
||||
LOG_INFO << "Database " << path << " opened successfully";
|
||||
|
||||
const std::vector<sqlite::table_column_info> column_info{
|
||||
sqlite::table_column_info("VERSION", sqlite::COLUMN_DATA_TYPE::TEXT)};
|
||||
|
||||
if (create_table(db, "SA_VERSION", column_info) == -1)
|
||||
return -1;
|
||||
|
||||
if (sqlite::insert_row(db, "SA_VERSION", "VERSION", "\"0.0.0\"") == -1)
|
||||
return -1;
|
||||
|
||||
if (sqlite::close_db(&db) == -1)
|
||||
{
|
||||
LOG_ERROR << "Error closing database";
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (conf::ctx.command == "version")
|
||||
// Print the version
|
||||
LOG_INFO << "Sashimono Agent " << conf::cfg.version;
|
||||
|
||||
deinit();
|
||||
}
|
||||
|
||||
LOG_INFO << "sashimono agent exited normally.";
|
||||
return 0;
|
||||
}
|
||||
19
src/pchheader.hpp
Normal file
19
src/pchheader.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#ifndef _SA_PCHHEADER_
|
||||
#define _SA_PCHHEADER_
|
||||
|
||||
#include <algorithm>
|
||||
#include <fcntl.h>
|
||||
#include <iostream>
|
||||
#include <jsoncons/json.hpp>
|
||||
#include <libgen.h>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sqlite3.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
#include <plog/Log.h>
|
||||
#include <plog/Appenders/ColorConsoleAppender.h>
|
||||
|
||||
#endif
|
||||
90
src/salog.cpp
Normal file
90
src/salog.cpp
Normal file
@@ -0,0 +1,90 @@
|
||||
#include "pchheader.hpp"
|
||||
#include "conf.hpp"
|
||||
#include "salog.hpp"
|
||||
|
||||
namespace salog
|
||||
{
|
||||
class plog_formatter;
|
||||
|
||||
// Custom formatter adopted from:
|
||||
// https://github.com/SergiusTheBest/plog/blob/master/include/plog/Formatters/TxtFormatter.h
|
||||
class plog_formatter
|
||||
{
|
||||
public:
|
||||
static plog::util::nstring header()
|
||||
{
|
||||
return plog::util::nstring();
|
||||
}
|
||||
|
||||
static inline const char *severity_to_string(plog::Severity severity)
|
||||
{
|
||||
switch (severity)
|
||||
{
|
||||
case plog::Severity::fatal:
|
||||
return "fat";
|
||||
case plog::Severity::error:
|
||||
return "err";
|
||||
case plog::Severity::warning:
|
||||
return "wrn";
|
||||
case plog::Severity::info:
|
||||
return "inf";
|
||||
case plog::Severity::debug:
|
||||
return "dbg";
|
||||
case plog::Severity::verbose:
|
||||
return "ver";
|
||||
default:
|
||||
return "def";
|
||||
}
|
||||
}
|
||||
|
||||
static plog::util::nstring format(const plog::Record &record)
|
||||
{
|
||||
tm t;
|
||||
plog::util::localtime_s(&t, &record.getTime().time); // local time
|
||||
|
||||
plog::util::nostringstream ss;
|
||||
ss << t.tm_year + 1900 << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_mon + 1 << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_mday << PLOG_NSTR(" ");
|
||||
ss << std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_hour << PLOG_NSTR(":")
|
||||
<< std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_min << PLOG_NSTR(":")
|
||||
<< std::setfill(PLOG_NSTR('0')) << std::setw(2) << t.tm_sec << PLOG_NSTR(" ");
|
||||
// Uncomment for millseconds.
|
||||
// << std::setfill(PLOG_NSTR('0')) << std::setw(3) << record.getTime().millitm << PLOG_NSTR(" ");
|
||||
|
||||
ss << PLOG_NSTR("[") << severity_to_string(record.getSeverity()) << PLOG_NSTR("][sa] ");
|
||||
ss << record.getMessage() << PLOG_NSTR("\n");
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
};
|
||||
|
||||
void init()
|
||||
{
|
||||
plog::Severity level;
|
||||
|
||||
if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::DEBUG)
|
||||
level = plog::Severity::debug;
|
||||
else if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::INFO)
|
||||
level = plog::Severity::info;
|
||||
else if (conf::cfg.log.log_level_type == conf::LOG_SEVERITY::WARN)
|
||||
level = plog::Severity::warning;
|
||||
else
|
||||
level = plog::Severity::error;
|
||||
|
||||
const std::string trace_file = conf::ctx.log_dir + "/sa.log";
|
||||
static plog::RollingFileAppender<plog_formatter> fileAppender(trace_file.c_str(), conf::cfg.log.max_mbytes_per_file * 1024 * 1024, conf::cfg.log.max_file_count);
|
||||
static plog::ColorConsoleAppender<plog_formatter> consoleAppender;
|
||||
|
||||
plog::Logger<0> &logger = plog::init(level);
|
||||
|
||||
// Take decision to append logger for file / console or both.
|
||||
if (conf::cfg.log.loggers.count("console") == 1)
|
||||
{
|
||||
logger.addAppender(&consoleAppender);
|
||||
}
|
||||
|
||||
if (conf::cfg.log.loggers.count("file") == 1)
|
||||
{
|
||||
logger.addAppender(&fileAppender);
|
||||
}
|
||||
}
|
||||
} // namespace salog
|
||||
9
src/salog.hpp
Normal file
9
src/salog.hpp
Normal file
@@ -0,0 +1,9 @@
|
||||
#ifndef _SA_SALOG_
|
||||
#define _SA_SALOG_
|
||||
|
||||
namespace salog
|
||||
{
|
||||
void init();
|
||||
} // namespace salog
|
||||
|
||||
#endif
|
||||
257
src/sqlite.cpp
Normal file
257
src/sqlite.cpp
Normal file
@@ -0,0 +1,257 @@
|
||||
#include "sqlite.hpp"
|
||||
#include "salog.hpp"
|
||||
|
||||
namespace sqlite
|
||||
{
|
||||
constexpr const char *COLUMN_DATA_TYPES[]{"INT", "TEXT", "BLOB"};
|
||||
constexpr const char *CREATE_TABLE = "CREATE TABLE IF NOT EXISTS ";
|
||||
constexpr const char *CREATE_INDEX = "CREATE INDEX ";
|
||||
constexpr const char *CREATE_UNIQUE_INDEX = "CREATE UNIQUE INDEX ";
|
||||
constexpr const char *BEGIN_TRANSACTION = "BEGIN TRANSACTION;";
|
||||
constexpr const char *COMMIT_TRANSACTION = "COMMIT;";
|
||||
constexpr const char *ROLLBACK_TRANSACTION = "ROLLBACK;";
|
||||
constexpr const char *INSERT_INTO = "INSERT INTO ";
|
||||
constexpr const char *PRIMARY_KEY = "PRIMARY KEY";
|
||||
constexpr const char *NOT_NULL = "NOT NULL";
|
||||
constexpr const char *VALUES = "VALUES";
|
||||
constexpr const char *SELECT_ALL = "SELECT * FROM ";
|
||||
constexpr const char *SQLITE_MASTER = "sqlite_master";
|
||||
constexpr const char *WHERE = " WHERE ";
|
||||
constexpr const char *AND = " AND ";
|
||||
|
||||
/**
|
||||
* Opens a connection to a given databse and give the db pointer.
|
||||
* @param db_name Database name to be connected.
|
||||
* @param db Pointer to the db pointer which is to be connected and pointed.
|
||||
* @param writable Whether the database must be opened in a writable mode or not.
|
||||
* @returns returns 0 on success, or -1 on error.
|
||||
*/
|
||||
int open_db(std::string_view db_name, sqlite3 **db, const bool writable)
|
||||
{
|
||||
int ret;
|
||||
const int flags = writable ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) : SQLITE_OPEN_READONLY;
|
||||
if ((ret = sqlite3_open_v2(db_name.data(), db, flags, 0)) != SQLITE_OK)
|
||||
{
|
||||
LOG_ERROR << ret << ": Sqlite error when opening database " << db_name;
|
||||
*db = NULL;
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes given sql query.
|
||||
* @param db Pointer to the db.
|
||||
* @param sql Sql query to be executed.
|
||||
* @param callback Callback funcion which is called for each result row.
|
||||
* @param callback_first_arg First data argumat to be parced to the callback (void pointer).
|
||||
* @returns returns 0 on success, or -1 on error.
|
||||
*/
|
||||
int exec_sql(sqlite3 *db, std::string_view sql, int (*callback)(void *, int, char **, char **), void *callback_first_arg)
|
||||
{
|
||||
char *err_msg;
|
||||
if (sqlite3_exec(db, sql.data(), callback, (callback != NULL ? (void *)callback_first_arg : NULL), &err_msg) != SQLITE_OK)
|
||||
{
|
||||
LOG_ERROR << "SQL error occured: " << err_msg;
|
||||
sqlite3_free(err_msg);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int begin_transaction(sqlite3 *db)
|
||||
{
|
||||
return sqlite::exec_sql(db, BEGIN_TRANSACTION);
|
||||
}
|
||||
|
||||
int commit_transaction(sqlite3 *db)
|
||||
{
|
||||
return sqlite::exec_sql(db, COMMIT_TRANSACTION);
|
||||
}
|
||||
|
||||
int rollback_transaction(sqlite3 *db)
|
||||
{
|
||||
return sqlite::exec_sql(db, ROLLBACK_TRANSACTION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a table with given table info.
|
||||
* @param db Pointer to the db.
|
||||
* @param table_name Table name to be created.
|
||||
* @param column_info Column info of the table.
|
||||
* @returns returns 0 on success, or -1 on error.
|
||||
*/
|
||||
int create_table(sqlite3 *db, std::string_view table_name, const std::vector<table_column_info> &column_info)
|
||||
{
|
||||
std::string sql;
|
||||
sql.append(CREATE_TABLE).append(table_name).append(" (");
|
||||
|
||||
for (auto itr = column_info.begin(); itr != column_info.end(); ++itr)
|
||||
{
|
||||
sql.append(itr->name);
|
||||
sql.append(" ");
|
||||
sql.append(COLUMN_DATA_TYPES[itr->column_type]);
|
||||
|
||||
if (itr->is_key)
|
||||
{
|
||||
sql.append(" ");
|
||||
sql.append(PRIMARY_KEY);
|
||||
}
|
||||
|
||||
if (!itr->is_null)
|
||||
{
|
||||
sql.append(" ");
|
||||
sql.append(NOT_NULL);
|
||||
}
|
||||
|
||||
if (itr != column_info.end() - 1)
|
||||
sql.append(",");
|
||||
}
|
||||
sql.append(")");
|
||||
|
||||
const int ret = exec_sql(db, sql);
|
||||
if (ret == -1)
|
||||
LOG_ERROR << "Error when creating sqlite table " << table_name;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int create_index(sqlite3 *db, std::string_view table_name, std::string_view column_names, const bool is_unique)
|
||||
{
|
||||
std::string index_name = std::string("idx_").append(table_name).append("_").append(column_names);
|
||||
std::replace(index_name.begin(), index_name.end(), ',', '_');
|
||||
|
||||
std::string sql;
|
||||
sql.append(is_unique ? CREATE_UNIQUE_INDEX : CREATE_INDEX)
|
||||
.append(index_name)
|
||||
.append(" ON ")
|
||||
.append(table_name)
|
||||
.append("(")
|
||||
.append(column_names)
|
||||
.append(")");
|
||||
|
||||
const int ret = exec_sql(db, sql);
|
||||
if (ret == -1)
|
||||
LOG_ERROR << "Error when creating sqlite index '" << index_name << "' in table " << table_name;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts mulitple rows to a table.
|
||||
* @param db Pointer to the db.
|
||||
* @param table_name Table name to be populated.
|
||||
* @param column_names_string Comma seperated string of colums (eg: "col_1,col_2,...").
|
||||
* @param value_strings Vector of comma seperated values (wrap in single quotes for TEXT type) (eg: ["r1val1,'r1val2',...", "r2val1,'r2val2',..."]).
|
||||
* @returns returns 0 on success, or -1 on error.
|
||||
*/
|
||||
int insert_rows(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, const std::vector<std::string> &value_strings)
|
||||
{
|
||||
std::string sql;
|
||||
|
||||
sql.append(INSERT_INTO);
|
||||
sql.append(table_name);
|
||||
sql.append("(");
|
||||
sql.append(column_names_string);
|
||||
sql.append(") ");
|
||||
sql.append(VALUES);
|
||||
|
||||
for (auto itr = value_strings.begin(); itr != value_strings.end(); ++itr)
|
||||
{
|
||||
sql.append("(");
|
||||
sql.append(*itr);
|
||||
sql.append(")");
|
||||
|
||||
if (itr != value_strings.end() - 1)
|
||||
sql.append(",");
|
||||
}
|
||||
|
||||
/* Execute SQL statement */
|
||||
return exec_sql(db, sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a row to a table.
|
||||
* @param db Pointer to the db.
|
||||
* @param table_name Table name to be populated.
|
||||
* @param column_names_string Comma seperated string of colums (eg: "col_1,col_2,...").
|
||||
* @param value_string comma seperated values as per column order (wrap in single quotes for TEXT type) (eg: "r1val1,'r1val2',...").
|
||||
* @returns returns 0 on success, or -1 on error.
|
||||
*/
|
||||
int insert_row(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, std::string_view value_string)
|
||||
{
|
||||
std::string sql;
|
||||
// Reserving the space for the query before construction.
|
||||
sql.reserve(sizeof(INSERT_INTO) + table_name.size() + column_names_string.size() + sizeof(VALUES) + value_string.size() + 5);
|
||||
|
||||
sql.append(INSERT_INTO);
|
||||
sql.append(table_name);
|
||||
sql.append("(");
|
||||
sql.append(column_names_string);
|
||||
sql.append(") ");
|
||||
sql.append(VALUES);
|
||||
sql.append("(");
|
||||
sql.append(value_string);
|
||||
sql.append(")");
|
||||
|
||||
/* Execute SQL statement */
|
||||
return exec_sql(db, sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether table exist in the database.
|
||||
* @param db Pointer to the db.
|
||||
* @param table_name Table name to be checked.
|
||||
* @returns returns true is exist, otherwise false.
|
||||
*/
|
||||
bool is_table_exists(sqlite3 *db, std::string_view table_name)
|
||||
{
|
||||
std::string sql;
|
||||
// Reserving the space for the query before construction.
|
||||
sql.reserve(sizeof(SELECT_ALL) + sizeof(SQLITE_MASTER) + sizeof(WHERE) + sizeof(AND) + table_name.size() + 19);
|
||||
|
||||
sql.append(SELECT_ALL);
|
||||
sql.append(SQLITE_MASTER);
|
||||
sql.append(WHERE);
|
||||
sql.append("type='table'");
|
||||
sql.append(AND);
|
||||
sql.append("name='");
|
||||
sql.append(table_name);
|
||||
sql.append("'");
|
||||
|
||||
sqlite3_stmt *stmt;
|
||||
|
||||
if (sqlite3_prepare_v2(db, sql.data(), -1, &stmt, 0) == SQLITE_OK &&
|
||||
stmt != NULL && sqlite3_step(stmt) == SQLITE_ROW)
|
||||
{
|
||||
// Finalize and distroys the statement.
|
||||
sqlite3_finalize(stmt);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Finalize and distroys the statement.
|
||||
sqlite3_finalize(stmt);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a connection to a given databse.
|
||||
* @param db Pointer to the db.
|
||||
* @returns returns 0 on success, or -1 on error.
|
||||
*/
|
||||
int close_db(sqlite3 **db)
|
||||
{
|
||||
if (*db == NULL)
|
||||
return 0;
|
||||
|
||||
if (sqlite3_close(*db) != SQLITE_OK)
|
||||
{
|
||||
LOG_ERROR << "Can't close database: " << sqlite3_errmsg(*db);
|
||||
return -1;
|
||||
}
|
||||
|
||||
*db = NULL;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
65
src/sqlite.hpp
Normal file
65
src/sqlite.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
#ifndef _SA_SQLITE_
|
||||
#define _SA_SQLITE_
|
||||
|
||||
#include "pchheader.hpp"
|
||||
|
||||
namespace sqlite
|
||||
{
|
||||
/**
|
||||
* Define an enum and a string array for the column data types.
|
||||
* Any column data type that needs to be supportes should be added to both the 'COLUMN_DATA_TYPE' enum and the 'column_data_type' array in its respective order.
|
||||
*/
|
||||
enum COLUMN_DATA_TYPE
|
||||
{
|
||||
INT,
|
||||
TEXT,
|
||||
BLOB
|
||||
};
|
||||
|
||||
/**
|
||||
* Struct of table column information.
|
||||
* {
|
||||
* string name Name of the column.
|
||||
* column_type Data type of the column.
|
||||
* is_key Whether column is a key.
|
||||
* is_null Whether column is nullable.
|
||||
* }
|
||||
*/
|
||||
struct table_column_info
|
||||
{
|
||||
std::string name;
|
||||
COLUMN_DATA_TYPE column_type;
|
||||
bool is_key;
|
||||
bool is_null;
|
||||
|
||||
table_column_info(std::string_view name, const COLUMN_DATA_TYPE &column_type, const bool is_key = false, const bool is_null = true)
|
||||
: name(name), column_type(column_type), is_key(is_key), is_null(is_null)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
int open_db(std::string_view db_name, sqlite3 **db, const bool writable = false);
|
||||
|
||||
int exec_sql(sqlite3 *db, std::string_view sql, int (*callback)(void *, int, char **, char **) = NULL, void *callback_first_arg = NULL);
|
||||
|
||||
int begin_transaction(sqlite3 *db);
|
||||
|
||||
int commit_transaction(sqlite3 *db);
|
||||
|
||||
int rollback_transaction(sqlite3 *db);
|
||||
|
||||
int create_table(sqlite3 *db, std::string_view table_name, const std::vector<table_column_info> &column_info);
|
||||
|
||||
int create_index(sqlite3 *db, std::string_view table_name, std::string_view column_names, const bool is_unique);
|
||||
|
||||
int insert_rows(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, const std::vector<std::string> &value_strings);
|
||||
|
||||
int insert_row(sqlite3 *db, std::string_view table_name, std::string_view column_names_string, std::string_view value_string);
|
||||
|
||||
bool is_table_exists(sqlite3 *db, std::string_view table_name);
|
||||
|
||||
int close_db(sqlite3 **db);
|
||||
|
||||
|
||||
}
|
||||
#endif
|
||||
100
src/util/util.cpp
Normal file
100
src/util/util.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "../pchheader.hpp"
|
||||
#include "util.hpp"
|
||||
|
||||
namespace util
|
||||
{
|
||||
/**
|
||||
* Check whether given directory exists.
|
||||
* @param path Directory path.
|
||||
* @return Returns true if given directory exists otherwise false.
|
||||
*/
|
||||
bool is_dir_exists(std::string_view path)
|
||||
{
|
||||
struct stat st;
|
||||
return (stat(path.data(), &st) == 0 && S_ISDIR(st.st_mode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether given file exists.
|
||||
* @param path File path.
|
||||
* @return Returns true if give file exists otherwise false.
|
||||
*/
|
||||
bool is_file_exists(std::string_view path)
|
||||
{
|
||||
struct stat st;
|
||||
return (stat(path.data(), &st) == 0 && S_ISREG(st.st_mode));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively creates directories and sub-directories if not exist.
|
||||
* @param path Directory path.
|
||||
* @return Returns 0 operations succeeded otherwise -1.
|
||||
*/
|
||||
int create_dir_tree_recursive(std::string_view path)
|
||||
{
|
||||
if (strcmp(path.data(), "/") == 0) // No need of checking if we are at root.
|
||||
return 0;
|
||||
|
||||
// Check whether this dir exists or not.
|
||||
struct stat st;
|
||||
if (stat(path.data(), &st) != 0 || !S_ISDIR(st.st_mode))
|
||||
{
|
||||
// Check and create parent dir tree first.
|
||||
char *path2 = strdup(path.data());
|
||||
char *parent_dir_path = dirname(path2);
|
||||
bool error_thrown = false;
|
||||
|
||||
if (create_dir_tree_recursive(parent_dir_path) == -1)
|
||||
error_thrown = true;
|
||||
|
||||
free(path2);
|
||||
|
||||
// Create this dir.
|
||||
if (!error_thrown && mkdir(path.data(), S_IRWXU | S_IRWXG | S_IROTH) == -1)
|
||||
{
|
||||
std::cerr << errno << ": Error in recursive dir creation. " << path << std::endl;
|
||||
error_thrown = true;
|
||||
}
|
||||
|
||||
if (error_thrown)
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the entire file from given file discriptor.
|
||||
* @param fd File descriptor to be read.
|
||||
* @param buf String buffer to be populated.
|
||||
* @param offset Begin offset of the file to read.
|
||||
* @return Returns number of bytes read in a successful read and -1 on error.
|
||||
*/
|
||||
int read_from_fd(const int fd, std::string &buf, const off_t offset)
|
||||
{
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) == -1)
|
||||
{
|
||||
std::cerr << errno << ": Error in stat for reading entire file." << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
buf.resize(st.st_size - offset);
|
||||
|
||||
return pread(fd, buf.data(), buf.size(), offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a safe std::string overload for realpath.
|
||||
* @param path Path.
|
||||
* @returns Returns the realpath as string.
|
||||
*/
|
||||
const std::string realpath(std::string_view path)
|
||||
{
|
||||
std::array<char, PATH_MAX> buffer;
|
||||
::realpath(path.data(), buffer.data());
|
||||
buffer[PATH_MAX] = '\0';
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
} // namespace util
|
||||
23
src/util/util.hpp
Normal file
23
src/util/util.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#ifndef _HP_UTIL_UTIL_
|
||||
#define _HP_UTIL_UTIL_
|
||||
|
||||
#include "../pchheader.hpp"
|
||||
|
||||
/**
|
||||
* Contains helper functions and data structures used by multiple other subsystems.
|
||||
*/
|
||||
namespace util
|
||||
{
|
||||
bool is_dir_exists(std::string_view path);
|
||||
|
||||
bool is_file_exists(std::string_view path);
|
||||
|
||||
int create_dir_tree_recursive(std::string_view path);
|
||||
|
||||
int read_from_fd(const int fd, std::string &buf, const off_t offset = 0);
|
||||
|
||||
const std::string realpath(std::string_view path);
|
||||
|
||||
} // namespace util
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user