mirror of
https://github.com/AntelopeIO/appbase.git
synced 2026-07-23 14:03:42 +00:00
575 lines
19 KiB
C++
575 lines
19 KiB
C++
#include <appbase/application_base.hpp>
|
|
#include <appbase/version.hpp>
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
#include <boost/filesystem.hpp>
|
|
#include <boost/asio/signal_set.hpp>
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <unordered_map>
|
|
#include <future>
|
|
#include <optional>
|
|
|
|
#include <unistd.h>
|
|
#include <signal.h>
|
|
|
|
namespace appbase {
|
|
|
|
namespace bpo = boost::program_options;
|
|
using bpo::options_description;
|
|
using bpo::variables_map;
|
|
using std::cout;
|
|
|
|
using any_type_compare_map = std::unordered_map<std::type_index, std::function<bool(const boost::any& a, const boost::any& b)>>;
|
|
|
|
class application_impl {
|
|
public:
|
|
|
|
#ifdef _WIN32
|
|
application_impl():_app_options("Application Options"){}
|
|
#else
|
|
|
|
application_impl():_app_options("Application Options"){
|
|
// Create a separate thread to handle signals, so that they don't interrupt I/O.
|
|
// stdio does not recover from EINTR.
|
|
_signal_catching_io_ctx.emplace();
|
|
_signal_catching_thread = std::thread([&ioctx = *_signal_catching_io_ctx]() {
|
|
auto workwork = boost::asio::make_work_guard(ioctx);
|
|
ioctx.run();
|
|
});
|
|
|
|
// after creating the thread for handling signals, we can block signals in the current thread
|
|
sigset_t blocked_signals;
|
|
get_target_sigset(&blocked_signals);
|
|
pthread_sigmask(SIG_BLOCK, &blocked_signals, nullptr);
|
|
}
|
|
|
|
void get_target_sigset(sigset_t* blocked_signals) {
|
|
sigemptyset(blocked_signals);
|
|
sigaddset(blocked_signals, SIGINT);
|
|
sigaddset(blocked_signals, SIGTERM);
|
|
sigaddset(blocked_signals, SIGPIPE);
|
|
sigaddset(blocked_signals, SIGHUP);
|
|
}
|
|
|
|
~application_impl() {
|
|
if(_signal_catching_thread.joinable()) {
|
|
_signal_catching_io_ctx->stop();
|
|
_signal_catching_thread.join();
|
|
}
|
|
|
|
// need to unblock signals, otherwise next thread created will inherit blocked signals
|
|
sigset_t blocked_signals;
|
|
get_target_sigset(&blocked_signals);
|
|
pthread_sigmask(SIG_UNBLOCK, &blocked_signals, nullptr);
|
|
}
|
|
#endif
|
|
|
|
options_description _app_options;
|
|
options_description _cfg_options;
|
|
variables_map _options;
|
|
|
|
bfs::path _data_dir{"data-dir"};
|
|
bfs::path _config_dir{"config-dir"};
|
|
bfs::path _logging_conf{"logging.json"};
|
|
bfs::path _config_file_name;
|
|
|
|
uint64_t _version = 0;
|
|
std::string _version_str = appbase_version_string;
|
|
std::string _full_version_str = appbase_version_string;
|
|
|
|
std::atomic_bool _is_quiting{false};
|
|
|
|
any_type_compare_map _any_compare_map;
|
|
|
|
std::thread _signal_catching_thread;
|
|
std::optional<boost::asio::io_context> _signal_catching_io_ctx;
|
|
};
|
|
|
|
application_base::application_base()
|
|
:my(new application_impl()){
|
|
register_config_type<std::string>();
|
|
register_config_type<bool>();
|
|
register_config_type<unsigned short>();
|
|
register_config_type<unsigned>();
|
|
register_config_type<unsigned long>();
|
|
register_config_type<unsigned long long>();
|
|
register_config_type<short>();
|
|
register_config_type<int>();
|
|
register_config_type<long>();
|
|
register_config_type<long long>();
|
|
register_config_type<double>();
|
|
register_config_type<std::vector<std::string>>();
|
|
register_config_type<boost::filesystem::path>();
|
|
}
|
|
|
|
application_base::~application_base() { }
|
|
|
|
void application_base::set_version(uint64_t version) {
|
|
my->_version = version;
|
|
}
|
|
|
|
uint64_t application_base::version() const {
|
|
return my->_version;
|
|
}
|
|
|
|
string application_base::version_string() const {
|
|
return my->_version_str;
|
|
}
|
|
|
|
void application_base::set_version_string( std::string v ) {
|
|
my->_version_str = std::move( v );
|
|
}
|
|
|
|
string application_base::full_version_string() const {
|
|
return my->_full_version_str;
|
|
}
|
|
|
|
void application_base::set_full_version_string( std::string v ) {
|
|
my->_full_version_str = std::move( v );
|
|
}
|
|
|
|
void application_base::set_default_data_dir(const bfs::path& data_dir) {
|
|
my->_data_dir = data_dir;
|
|
}
|
|
|
|
void application_base::set_default_config_dir(const bfs::path& config_dir) {
|
|
my->_config_dir = config_dir;
|
|
}
|
|
|
|
bfs::path application_base::get_logging_conf() const {
|
|
return my->_logging_conf;
|
|
}
|
|
|
|
void application_base::wait_for_signal(std::shared_ptr<boost::asio::signal_set> ss) {
|
|
ss->async_wait([this, ss](const boost::system::error_code& ec, int) {
|
|
if(ec)
|
|
return;
|
|
quit();
|
|
wait_for_signal(ss);
|
|
});
|
|
}
|
|
|
|
void application_base::setup_signal_handling_on_ios(boost::asio::io_service& ios, bool startup) {
|
|
std::shared_ptr<boost::asio::signal_set> ss = std::make_shared<boost::asio::signal_set>(ios, SIGINT, SIGTERM);
|
|
#ifdef SIGPIPE
|
|
ss->add(SIGPIPE);
|
|
#endif
|
|
#ifdef SIGHUP
|
|
if( startup ) {
|
|
ss->add(SIGHUP);
|
|
}
|
|
#endif
|
|
wait_for_signal(ss);
|
|
}
|
|
|
|
void application_base::startup(boost::asio::io_service& io_serv) {
|
|
//during startup, run a second thread to catch SIGINT/SIGTERM/SIGPIPE/SIGHUP
|
|
boost::asio::io_service startup_thread_ios;
|
|
setup_signal_handling_on_ios(startup_thread_ios, true);
|
|
std::thread startup_thread([&startup_thread_ios]() {
|
|
startup_thread_ios.run();
|
|
});
|
|
auto clean_up_signal_thread = [&startup_thread_ios, &startup_thread]() {
|
|
startup_thread_ios.stop();
|
|
startup_thread.join();
|
|
};
|
|
|
|
try {
|
|
for( auto plugin : initialized_plugins ) {
|
|
if( is_quiting() ) break;
|
|
plugin->startup();
|
|
}
|
|
|
|
} catch( ... ) {
|
|
clean_up_signal_thread();
|
|
shutdown();
|
|
throw;
|
|
}
|
|
|
|
//after startup, shut down the signal handling thread and catch the signals back on main io_service
|
|
clean_up_signal_thread();
|
|
setup_signal_handling_on_ios(io_serv, false);
|
|
|
|
#ifdef SIGHUP
|
|
std::shared_ptr<boost::asio::signal_set> sighup_set(new boost::asio::signal_set(io_serv, SIGHUP));
|
|
start_sighup_handler( sighup_set );
|
|
#endif
|
|
}
|
|
|
|
void application_base::start_sighup_handler( std::shared_ptr<boost::asio::signal_set> sighup_set ) {
|
|
#ifdef SIGHUP
|
|
sighup_set->async_wait([sighup_set, this](const boost::system::error_code& err, int /*num*/) {
|
|
if( err ) return;
|
|
post_cb(priority::medium, [sighup_set, this]() {
|
|
sighup_callback();
|
|
for( auto plugin : initialized_plugins ) {
|
|
if( is_quiting() ) return;
|
|
plugin->handle_sighup();
|
|
}
|
|
});
|
|
start_sighup_handler( sighup_set );
|
|
});
|
|
#endif
|
|
}
|
|
|
|
void application_base::register_config_type_comparison(std::type_index i, config_comparison_f comp) {
|
|
my->_any_compare_map.emplace(i, comp);
|
|
}
|
|
|
|
void application_base::set_program_options()
|
|
{
|
|
for(auto& plug : plugins) {
|
|
boost::program_options::options_description plugin_cli_opts("Command Line Options for " + plug.second->name());
|
|
boost::program_options::options_description plugin_cfg_opts("Config Options for " + plug.second->name());
|
|
plug.second->set_program_options(plugin_cli_opts, plugin_cfg_opts);
|
|
if(plugin_cfg_opts.options().size()) {
|
|
my->_app_options.add(plugin_cfg_opts);
|
|
my->_cfg_options.add(plugin_cfg_opts);
|
|
}
|
|
if(plugin_cli_opts.options().size())
|
|
my->_app_options.add(plugin_cli_opts);
|
|
}
|
|
|
|
options_description app_cfg_opts( "Application Config Options" );
|
|
options_description app_cli_opts( "Application Command Line Options" );
|
|
app_cfg_opts.add_options()
|
|
("plugin", bpo::value< vector<string> >()->composing(), "Plugin(s) to enable, may be specified multiple times");
|
|
|
|
app_cli_opts.add_options()
|
|
("help,h", "Print this help message and exit.")
|
|
("version,v", "Print version information.")
|
|
("full-version", "Print full version information.")
|
|
("print-default-config", "Print default configuration template")
|
|
("data-dir,d", bpo::value<std::string>(), "Directory containing program runtime data")
|
|
("config-dir", bpo::value<std::string>(), "Directory containing configuration files such as config.ini")
|
|
("config,c", bpo::value<std::string>()->default_value( "config.ini" ), "Configuration file name relative to config-dir")
|
|
("logconf,l", bpo::value<std::string>()->default_value( "logging.json" ), "Logging configuration file name/path for library users");
|
|
|
|
my->_cfg_options.add(app_cfg_opts);
|
|
my->_app_options.add(app_cfg_opts);
|
|
my->_app_options.add(app_cli_opts);
|
|
}
|
|
|
|
bool application_base::initialize_impl(int argc, char** argv, vector<abstract_plugin*> autostart_plugins) {
|
|
set_program_options();
|
|
|
|
bpo::variables_map& options = my->_options;
|
|
try {
|
|
bpo::parsed_options parsed = bpo::command_line_parser(argc, argv).options(my->_app_options).run();
|
|
bpo::store(parsed, options);
|
|
vector<string> positionals = bpo::collect_unrecognized(parsed.options, bpo::include_positional);
|
|
if(!positionals.empty())
|
|
BOOST_THROW_EXCEPTION(std::runtime_error("Unknown option '" + positionals[0] + "' passed as command line argument"));
|
|
} catch( const boost::program_options::unknown_option& e ) {
|
|
BOOST_THROW_EXCEPTION(std::runtime_error("Unknown option '" + e.get_option_name() + "' passed as command line argument"));
|
|
}
|
|
|
|
if( options.count( "help" ) ) {
|
|
cout << my->_app_options << std::endl;
|
|
return false;
|
|
}
|
|
|
|
if( options.count( "version" ) ) {
|
|
cout << version_string() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
if( options.count( "full-version" ) ) {
|
|
cout << full_version_string() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
if( options.count( "print-default-config" ) ) {
|
|
print_default_config(cout);
|
|
return false;
|
|
}
|
|
|
|
if( options.count( "data-dir" ) ) {
|
|
// Workaround for 10+ year old Boost defect
|
|
// See https://svn.boost.org/trac10/ticket/8535
|
|
// Should be .as<bfs::path>() but paths with escaped spaces break bpo e.g.
|
|
// std::exception::what: the argument ('/path/with/white\ space') for option '--data-dir' is invalid
|
|
auto workaround = options["data-dir"].as<std::string>();
|
|
bfs::path data_dir = workaround;
|
|
if( data_dir.is_relative() )
|
|
data_dir = bfs::current_path() / data_dir;
|
|
my->_data_dir = data_dir;
|
|
}
|
|
|
|
if( options.count( "config-dir" ) ) {
|
|
auto workaround = options["config-dir"].as<std::string>();
|
|
bfs::path config_dir = workaround;
|
|
if( config_dir.is_relative() )
|
|
config_dir = bfs::current_path() / config_dir;
|
|
my->_config_dir = config_dir;
|
|
}
|
|
|
|
auto workaround = options["logconf"].as<std::string>();
|
|
bfs::path logconf = workaround;
|
|
if( logconf.is_relative() )
|
|
logconf = my->_config_dir / logconf;
|
|
my->_logging_conf = logconf;
|
|
|
|
workaround = options["config"].as<std::string>();
|
|
my->_config_file_name = workaround;
|
|
if( my->_config_file_name.is_relative() )
|
|
my->_config_file_name = my->_config_dir / my->_config_file_name;
|
|
|
|
if(!bfs::exists(my->_config_file_name)) {
|
|
if(my->_config_file_name.compare(my->_config_dir / "config.ini") != 0)
|
|
{
|
|
cout << "Config file " << my->_config_file_name << " missing." << std::endl;
|
|
return false;
|
|
}
|
|
write_default_config(my->_config_file_name);
|
|
}
|
|
|
|
std::vector< bpo::basic_option<char> > opts_from_config;
|
|
try {
|
|
bpo::parsed_options parsed_opts_from_config = bpo::parse_config_file<char>(my->_config_file_name.make_preferred().string().c_str(), my->_cfg_options, false);
|
|
bpo::store(parsed_opts_from_config, options);
|
|
opts_from_config = parsed_opts_from_config.options;
|
|
} catch( const boost::program_options::unknown_option& e ) {
|
|
BOOST_THROW_EXCEPTION(std::runtime_error("Unknown option '" + e.get_option_name() + "' inside the config file " + full_config_file_path().string()));
|
|
}
|
|
|
|
std::vector<string> set_but_default_list;
|
|
|
|
for(const boost::shared_ptr<bpo::option_description>& od_ptr : my->_cfg_options.options()) {
|
|
boost::any default_val, config_val;
|
|
if(!od_ptr->semantic()->apply_default(default_val))
|
|
continue;
|
|
|
|
if(my->_any_compare_map.find(default_val.type()) == my->_any_compare_map.end()) {
|
|
std::cerr << "APPBASE: Developer -- the type " << default_val.type().name() << " is not registered with appbase," << std::endl;
|
|
std::cerr << " add a register_config_type<>() in your plugin's ctor" << std::endl;
|
|
return false;
|
|
}
|
|
|
|
for(const bpo::basic_option<char>& opt : opts_from_config) {
|
|
if(opt.string_key != od_ptr->long_name())
|
|
continue;
|
|
|
|
od_ptr->semantic()->parse(config_val, opt.value, true);
|
|
if(my->_any_compare_map.at(default_val.type())(default_val, config_val))
|
|
set_but_default_list.push_back(opt.string_key);
|
|
break;
|
|
}
|
|
}
|
|
if(set_but_default_list.size()) {
|
|
std::cerr << "APPBASE: Warning: The following configuration items in the config.ini file are redundantly set to" << std::endl;
|
|
std::cerr << " their default value:" << std::endl;
|
|
std::cerr << " ";
|
|
size_t chars_on_line = 0;
|
|
for(auto it = set_but_default_list.cbegin(); it != set_but_default_list.end(); ++it) {
|
|
std::cerr << *it;
|
|
if(it + 1 != set_but_default_list.end())
|
|
std::cerr << ", ";
|
|
if((chars_on_line += it->size()) > 65) {
|
|
std::cerr << std::endl << " ";
|
|
chars_on_line = 0;
|
|
}
|
|
}
|
|
std::cerr << std::endl;
|
|
std::cerr << " Explicit values will override future changes to application defaults. Consider commenting out or" << std::endl;
|
|
std::cerr << " removing these items." << std::endl;
|
|
}
|
|
|
|
if(options.count("plugin") > 0)
|
|
{
|
|
auto plugins = options.at("plugin").as<std::vector<std::string>>();
|
|
for(auto& arg : plugins)
|
|
{
|
|
vector<string> names;
|
|
boost::split(names, arg, boost::is_any_of(" \t,"));
|
|
for(const std::string& name : names)
|
|
get_plugin(name).initialize(options);
|
|
}
|
|
}
|
|
try {
|
|
for (auto plugin : autostart_plugins)
|
|
if (plugin != nullptr && plugin->get_state() == abstract_plugin::registered)
|
|
plugin->initialize(options);
|
|
|
|
bpo::notify(options);
|
|
} catch (...) {
|
|
std::cerr << "Failed to initialize\n";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void application_base::handle_exception(std::exception_ptr eptr, std::string_view origin) {
|
|
try {
|
|
if (eptr)
|
|
std::rethrow_exception(eptr);
|
|
} catch(const std::exception& e) {
|
|
std::cerr << "Caught " << origin << " exception: \"" << e.what() << "\"\n";
|
|
} catch(...) {
|
|
std::cerr << "Caught unknown " << origin << " exception.\n";
|
|
}
|
|
}
|
|
|
|
void application_base::shutdown() {
|
|
std::exception_ptr eptr = nullptr;
|
|
|
|
for(auto ritr = running_plugins.rbegin();
|
|
ritr != running_plugins.rend(); ++ritr) {
|
|
try {
|
|
(*ritr)->shutdown();
|
|
} catch(...) {
|
|
if (!eptr)
|
|
eptr = std::current_exception();
|
|
handle_exception(std::current_exception(), (*ritr)->name());
|
|
}
|
|
}
|
|
for(auto ritr = running_plugins.rbegin();
|
|
ritr != running_plugins.rend(); ++ritr) {
|
|
try {
|
|
plugins.erase((*ritr)->name());
|
|
} catch(...) {
|
|
if (!eptr)
|
|
eptr = std::current_exception();
|
|
std::string origin = (*ritr)->name() + " destructor";
|
|
handle_exception(std::current_exception(), origin);
|
|
}
|
|
}
|
|
try {
|
|
running_plugins.clear();
|
|
initialized_plugins.clear();
|
|
plugins.clear();
|
|
} catch(...) {
|
|
if (!eptr)
|
|
eptr = std::current_exception();
|
|
handle_exception(std::current_exception(), "plugin cleanup");
|
|
}
|
|
quit();
|
|
|
|
// if we caught an exception while shutting down a plugin, rethrow it so that main()
|
|
// can catch it and report the error
|
|
if (eptr)
|
|
std::rethrow_exception(eptr);
|
|
}
|
|
|
|
void application_base::quit() {
|
|
my->_is_quiting = true;
|
|
stop_executor_cb();
|
|
}
|
|
|
|
bool application_base::is_quiting() const {
|
|
return my->_is_quiting;
|
|
}
|
|
|
|
void application_base::set_thread_priority_max() {
|
|
#if __has_include(<pthread.h>)
|
|
pthread_t this_thread = pthread_self();
|
|
struct sched_param params{};
|
|
int policy = 0;
|
|
int ret = pthread_getschedparam(this_thread, &policy, ¶ms);
|
|
if( ret != 0 ) {
|
|
std::cerr << "ERROR: Unable to get thread priority" << std::endl;
|
|
}
|
|
|
|
params.sched_priority = sched_get_priority_max(policy);
|
|
ret = pthread_setschedparam(this_thread, policy, ¶ms);
|
|
if( ret != 0 ) {
|
|
std::cerr << "ERROR: Unable to set thread priority" << std::endl;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void application_base::write_default_config(const bfs::path& cfg_file) {
|
|
if(!bfs::exists(cfg_file.parent_path()))
|
|
bfs::create_directories(cfg_file.parent_path());
|
|
|
|
std::ofstream out_cfg( bfs::path(cfg_file).make_preferred().string());
|
|
print_default_config(out_cfg);
|
|
out_cfg.close();
|
|
}
|
|
|
|
void application_base::print_default_config(std::ostream& os) {
|
|
std::map<std::string, std::string> option_to_plug;
|
|
for(auto& plug : plugins) {
|
|
boost::program_options::options_description plugin_cli_opts;
|
|
boost::program_options::options_description plugin_cfg_opts;
|
|
plug.second->set_program_options(plugin_cli_opts, plugin_cfg_opts);
|
|
|
|
for(const boost::shared_ptr<bpo::option_description>& opt : plugin_cfg_opts.options())
|
|
option_to_plug[opt->long_name()] = plug.second->name();
|
|
}
|
|
|
|
for(const boost::shared_ptr<bpo::option_description>& od : my->_cfg_options.options())
|
|
{
|
|
if(!od->description().empty()) {
|
|
std::string desc = od->description();
|
|
boost::replace_all(desc, "\n", "\n# ");
|
|
os << "# " << desc;
|
|
std::map<std::string, std::string>::iterator it;
|
|
if((it = option_to_plug.find(od->long_name())) != option_to_plug.end())
|
|
os << " (" << it->second << ")";
|
|
os << std::endl;
|
|
}
|
|
boost::any store;
|
|
if(!od->semantic()->apply_default(store))
|
|
os << "# " << od->long_name() << " = " << std::endl;
|
|
else {
|
|
auto example = od->format_parameter();
|
|
if(example.empty())
|
|
// This is a boolean switch
|
|
os << "# " << od->long_name() << " = " << "false" << std::endl;
|
|
else if(store.type() == typeid(bool))
|
|
os << "# " << od->long_name() << " = " << (boost::any_cast<bool&>(store) ? "true" : "false") << std::endl;
|
|
else {
|
|
// The string is formatted "arg (=<interesting part>)"
|
|
auto pos = example.find("(=");
|
|
if(pos != string::npos) example = example.substr(pos+2);
|
|
if(!example.empty()) example.erase(example.length()-1);
|
|
os << "# " << od->long_name() << " = " << example << std::endl;
|
|
}
|
|
}
|
|
os << std::endl;
|
|
}
|
|
}
|
|
|
|
abstract_plugin* application_base::find_plugin(const string& name)const
|
|
{
|
|
auto itr = plugins.find(name);
|
|
if(itr == plugins.end()) {
|
|
return nullptr;
|
|
}
|
|
return itr->second.get();
|
|
}
|
|
|
|
abstract_plugin& application_base::get_plugin(const string& name)const {
|
|
auto ptr = find_plugin(name);
|
|
if(!ptr)
|
|
BOOST_THROW_EXCEPTION(std::runtime_error("unable to find plugin: " + name));
|
|
return *ptr;
|
|
}
|
|
|
|
bfs::path application_base::data_dir() const {
|
|
return my->_data_dir;
|
|
}
|
|
|
|
bfs::path application_base::config_dir() const {
|
|
return my->_config_dir;
|
|
}
|
|
|
|
bfs::path application_base::full_config_file_path() const {
|
|
return bfs::canonical(my->_config_file_name);
|
|
}
|
|
|
|
void application_base::set_sighup_callback(std::function<void()> callback) {
|
|
sighup_callback = callback;
|
|
}
|
|
|
|
const bpo::variables_map& application_base::get_options() const{
|
|
return my->_options;
|
|
}
|
|
|
|
} /// namespace appbase
|