2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
42#include "utils/Config.h"
44#include "utils/Daemon.h"
45#include "utils/Exceptions.h"
46#include "utils/Formatter.h"
48#ifndef DOXYGEN_SHOULD_SKIP_THIS
50#include "log/Logger.h"
68#include <unordered_set>
72#pragma GCC diagnostic push
73#pragma GCC diagnostic ignored "-Wfloat-equal"
75#if __has_warning
("-Wweak-vtables")
76#pragma GCC diagnostic ignored "-Wweak-vtables"
78#if __has_warning
("-Wcovered-switch-default")
79#pragma GCC diagnostic ignored "-Wcovered-switch-default"
81#if __has_warning
("-Wmissing-noreturn")
82#pragma GCC diagnostic ignored "-Wmissing-noreturn"
84#if __has_warning
("-Wnrvo")
85#pragma GCC diagnostic ignored "-Wnrvo"
89#include "utils/CLI11.hpp"
91#pragma GCC diagnostic pop
102 const std::shared_ptr<
CLI::App> app = std::make_shared<
CLI::App>();
104 app->configurable(
false);
106 app->allow_config_extras();
110 helpFormatter->label(
"SUBCOMMAND",
"INSTANCE");
111 helpFormatter->label(
"SUBCOMMANDS",
"INSTANCES");
112 helpFormatter->label(
"PERSISTENT",
"");
113 helpFormatter->label(
"Persistent Options",
"Options (persistent)");
114 helpFormatter->label(
"Nonpersistent Options",
"Options (nonpersistent)");
115 helpFormatter->label(
"Usage",
"\nUsage");
116 helpFormatter->label(
"bool:{true,false}",
"{true,false}");
117 helpFormatter->label(
":{standard,required,full,default}",
"{standard,required,full,default}");
118 helpFormatter->label(
":{standard,exact,expanded}",
"{standard,exact,expanded}");
119 helpFormatter->column_width(7);
121 app->formatter(helpFormatter);
124 app->get_config_formatter_base()->arrayDelimiter(
' ');
126 app->option_defaults()->take_last();
127 app->option_defaults()->group(app->get_formatter()->get_label(
"Nonpersistent Options"));
145 struct passwd* pw =
nullptr;
146 struct group* gr =
nullptr;
148 if ((pw = getpwuid(getuid())) ==
nullptr) {
150 }
else if ((gr = getgrgid(pw->pw_gid)) ==
nullptr) {
152 }
else if ((euid = geteuid()) == 0) {
157 const char* homedir =
nullptr;
158 if ((homedir = std::getenv(
"XDG_CONFIG_HOME")) ==
nullptr) {
159 if ((homedir = std::getenv(
"HOME")) ==
nullptr) {
160 homedir = pw->pw_dir;
164 if (homedir !=
nullptr) {
166 logDirectory = std::string(homedir) +
"/.local/log/snode.c";
167 pidDirectory = std::string(homedir) +
"/.local/run/snode.c";
175 std::filesystem::permissions(
177 (std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec) &
178 ~std::filesystem::perms::others_all);
179 if (geteuid() == 0) {
180 struct group* gr =
nullptr;
181 if ((gr = getgrnam(
XSTR(GROUP_NAME))) !=
nullptr) {
183 std::cout <<
"Warning: Can not set group ownership of '" <<
configDirectory
184 <<
"' to 'snodec':" << strerror(errno) << std::endl;
187 std::cout <<
"Error: Can not find group 'snodec'. Add it using groupadd or addgroup" << std::endl;
188 std::cout <<
" and add the current user to this group." << std::endl;
194 std::cout <<
"Error: Can not create directory '" <<
configDirectory <<
"'" << std::endl;
199 if (proceed && !std::filesystem::exists(
logDirectory)) {
200 if (std::filesystem::create_directories(
logDirectory)) {
202 (std::filesystem::perms::owner_all | std::filesystem::perms::group_all) &
203 ~std::filesystem::perms::others_all);
204 if (geteuid() == 0) {
205 struct group* gr =
nullptr;
206 if ((gr = getgrnam(
XSTR(GROUP_NAME))) !=
nullptr) {
207 if (chown(
logDirectory.c_str(), euid, gr->gr_gid) < 0) {
208 std::cout <<
"Warning: Can not set group ownership of '" <<
logDirectory <<
"' to 'snodec':" << strerror(errno)
212 std::cout <<
"Error: Can not find group 'snodec'. Add it using groupadd or addgroup" << std::endl;
213 std::cout <<
" and add the current user to this group." << std::endl;
219 std::cout <<
"Error: Can not create directory '" <<
logDirectory <<
"'" << std::endl;
224 if (proceed && !std::filesystem::exists(
pidDirectory)) {
225 if (std::filesystem::create_directories(
pidDirectory)) {
227 (std::filesystem::perms::owner_all | std::filesystem::perms::group_all) &
228 ~std::filesystem::perms::others_all);
229 if (geteuid() == 0) {
230 struct group* gr =
nullptr;
231 if ((gr = getgrnam(
XSTR(GROUP_NAME))) !=
nullptr) {
232 if (chown(
pidDirectory.c_str(), euid, gr->gr_gid) < 0) {
233 std::cout <<
"Warning: Can not set group ownership of '" <<
pidDirectory <<
"' to 'snodec':" << strerror(errno)
237 std::cout <<
"Error: Can not find group 'snodec'. Add it using groupadd or addgroup." << std::endl;
238 std::cout <<
" and add the current user to this group." << std::endl;
244 std::cout <<
"Error: Can not create directory '" <<
pidDirectory <<
"'" << std::endl;
253 "' powered by SNode.C\n"
254 "(C) 2020-2025 Volker Christian <me@vchrist.at>\n"
255 "https://github.com/SNodeC/snode.c");
260 "Read a config file",
263 ->type_name(
"configfile")
264 ->check(!
CLI::ExistingDirectory);
266 app->add_option(
"-w,--write-config",
"Write config file and exit")
267 ->configurable(
false)
269 ->type_name(
"configfile")
270 ->check(!
CLI::ExistingDirectory)
275 "Kill running daemon")
276 ->configurable(
false)
277 ->disable_flag_override();
280 "-i,--instance-alias",
281 "Make an instance also known as an alias in configuration files")
282 ->configurable(
false)
283 ->type_name(
"instance=instance_alias [instance=instance_alias [...]]")
284 ->each([](
const std::string& item) {
285 const auto it = item.find(
'=');
286 if (it != std::string::npos) {
287 aliases[item.substr(0, it)] = item.substr(it + 1);
289 throw CLI::ConversionError(
"Can not convert '" + item +
"' to a 'instance=instance_alias' pair");
300 ->check(
CLI::Range(0, 6))
301 ->group(
app->get_formatter()->get_label(
"Persistent Options"));
304 "-v,--verbose-level",
308 ->check(
CLI::Range(0, 10))
309 ->group(
app->get_formatter()->get_label(
"Persistent Options"));
312 "-q{true},!-u,--quiet{true}",
314 ->default_val(
"false")
316 ->check(
CLI::IsMember({
"true",
"false"}))
317 ->group(
app->get_formatter()->get_label(
"Persistent Options"));
323 ->type_name(
"logfile")
324 ->check(!
CLI::ExistingDirectory)
325 ->group(
app->get_formatter()->get_label(
"Persistent Options"));
328 "-e{true},!-n,--enforce-log-file{true}",
329 "Enforce writing of logs to file for foreground applications")
330 ->default_val(
"false")
332 ->check(
CLI::IsMember({
"true",
"false"}))
333 ->group(
app->get_formatter()->get_label(
"Persistent Options"));
336 "-d{true},!-f,--daemonize{true}",
337 "Start application as daemon")
338 ->default_val(
"false")
340 ->check(
CLI::IsMember({
"true",
"false"}))
341 ->group(
app->get_formatter()->get_label(
"Persistent Options"));
345 "Run daemon under specific user permissions")
346 ->default_val(pw->pw_name)
347 ->type_name(
"username")
349 ->group(
app->get_formatter()->get_label(
"Persistent Options"));
353 "Run daemon under specific group permissions")
354 ->default_val(gr->gr_name)
355 ->type_name(
"groupname")
357 ->group(
app->get_formatter()->get_label(
"Persistent Options"));
361 app->set_version_flag(
"--version",
"1.0-rc1",
"Framework version");
371 app->final_callback([]() {
373 (*
app)[
"--write-config"]->count() == 0 && (*
app)[
"--command-line"]->count() == 0) {
374 std::cout <<
"Running as daemon (double fork)" << std::endl;
381 const std::string logFile =
logFileOpt->as<std::string>();
382 if (!logFile.empty()) {
385 }
else if ((*
app)[
"--enforce-log-file"]->as<
bool>()) {
386 const std::string logFile =
logFileOpt->as<std::string>();
387 if (!logFile.empty()) {
388 std::cout <<
"Writing logs to file " << logFile << std::endl;
403 }
catch (
const CLI::ParseError&) {
407 if ((*
app)[
"--kill"]->count() > 0) {
410 std::cout <<
"Daemon terminated: Pid = " << daemonPid << std::endl;
412 std::cout <<
"DaemonError: " << e.what() << std::endl;
414 std::cout <<
"DaemonFailure: " << e.what() << std::endl;
419 app->allow_extras((*
app)[
"--show-config"]->count() != 0);
434 static const std::unordered_set<
char> special{
437 '|',
'&',
';',
'<',
'>',
'(',
')',
'{',
'}',
438 '*',
'?',
'[',
']',
'~',
'!',
'#',
'='
442 out.reserve(s.size() * 3);
444 for (
const char c : s) {
445 if (special.contains(c)) {
454 CLI::Option* disabledOpt = app->get_option_no_throw(
"--disabled");
455 const bool disabled = disabledOpt !=
nullptr ? disabledOpt->as<
bool>() :
false;
457 for (
const CLI::Option* option : app->get_options()) {
458 if (option->get_configurable()) {
463 if (option->count() > 0) {
464 value = option->as<std::string>();
465 }
else if (option->get_required()) {
466 value =
"<REQUIRED>";
470 if (option->get_required()) {
471 if (option->count() > 0) {
472 value = option->as<std::string>();
474 value =
"<REQUIRED>";
479 if (option->count() > 0) {
480 value = option->as<std::string>();
481 }
else if (!option->get_default_str().empty()) {
482 value = option->get_default_str();
483 }
else if (!option->get_required()) {
486 value =
"<REQUIRED>";
490 if (!option->get_default_str().empty()) {
491 value = option->get_default_str();
492 }
else if (!option->get_required()) {
495 value =
"<REQUIRED>";
501 if (!value.empty()) {
502 if (value.starts_with(
"[") && value.ends_with(
"]")) {
503 value = value.substr(1, value.size() - 2);
506 if (value !=
"<REQUIRED>" && value !=
"\"\"") {
509 out <<
"--" << option->get_single_name() << ((option->get_items_expected_max() == 0) ?
"=" :
" ") << value <<
" ";
513 }
else if (disabledOpt->get_default_str() ==
"false") {
514 out <<
"--disabled=true ";
519 std::stringstream out;
523 std::string optionString = out.str();
524 if (!optionString.empty() && optionString.back() ==
' ') {
525 optionString.pop_back();
534 std::stringstream out;
536 CLI::Option* disabledOpt = app->get_option_no_throw(
"--disabled");
538 for (
CLI::App* subcommand : app->get_subcommands({})) {
539 if (!subcommand->get_name().empty()) {
549 std::string outString;
552 if (!outString.empty()) {
558 if (!outString.empty()) {
559 out << app->get_name() <<
" " << outString;
564 std::stringstream out;
568 std::string outString = out.str();
569 while (app->get_parent() !=
nullptr) {
570 app = app->get_parent();
573 std::string(app->get_name()).append(
" ").append(!parentOptions.empty() ? parentOptions.append(
" ") :
"").append(outString);
576 if (outString.empty()) {
584 bool success =
false;
591 }
catch (
const CLI::ParseError&) {
597 if ((*
app)[
"--write-config"]->count() > 0) {
610 if ((*
app)[
"--write-config"]->count() > 0) {
617 <<
" ... exiting" << std::endl;
620 <<
" ... exiting" << std::endl;
622 std::cout <<
"Pid: " << getpid() <<
", child pid: " << e
.getPid() <<
": " << e.what() << std::endl;
623 }
catch (
const CLI::CallForHelp&) {
624 const std::string helpMode =
helpTriggerApp->get_option(
"--help")->as<std::string>();
625 const CLI::App* helpApp =
nullptr;
626 CLI::AppFormatMode mode =
CLI::AppFormatMode::Normal;
627 if (helpMode ==
"exact") {
629 }
else if (helpMode ==
"expanded") {
631 mode =
CLI::AppFormatMode::All;
633 std::cout <<
app->help(helpApp,
"", mode) << std::endl;
634 }
catch (
const CLI::CallForVersion&) {
635 std::cout <<
app->version() << std::endl << std::endl;
637 std::cout << e.what() << std::endl;
638 std::cout << std::endl
644 std::cout << e
.getApp()->config_to_str(
true,
true);
645 }
catch (
const CLI::ParseError& e1) {
647 <<
" " << e1.get_name() <<
" " << e1.what() << std::endl;
651 std::cout << e.what() << std::endl;
653 if (confFile.is_open()) {
655 confFile <<
app->config_to_str(
true,
true);
657 }
catch (
const CLI::ParseError& e1) {
659 std::cout <<
"Error writing config file: " << e1.get_name() <<
" " << e1.what() << std::endl;
664 <<
"] Writing config file: " << std::strerror(errno) << std::endl;
666 }
catch (
const CLI::ConversionError& e) {
669 }
catch (
const CLI::ArgumentMismatch& e) {
672 }
catch (
const CLI::ConfigError& e) {
674 std::cout <<
" Adding '-w' on the command line may solve this problem" << std::endl;
676 }
catch (
const CLI::ParseError& e) {
677 const std::string what = e.what();
678 if (what.find(
"[Option Group: ") != std::string::npos) {
681 <<
" Anonymous instance(s) not configured in source code " << std::endl;
687 }
catch ([[maybe_unused]]
const CLI::ParseError& e) {
688 std::cout << std::endl <<
"Append -h or --help to your command line for more information." << std::endl;
689 }
catch (
const CLI::Error& e) {
692 std::cout << std::endl <<
"Append -h or --help to your command line for more information." << std::endl;
699 if ((*
app)[
"--daemonize"]->as<
bool>()) {
702 if (pidFile.good()) {
706 if (getpid() == pid) {
710 }
else if (fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK) >= 0) {
712 while (read(STDIN_FILENO, buf, 1024) > 0) {
720 sectionFormatter->label(
"SUBCOMMAND",
"SECTION");
721 sectionFormatter->label(
"SUBCOMMANDS",
"SECTIONS");
722 sectionFormatter->label(
"PERSISTENT",
"");
723 sectionFormatter->label(
"Persistent Options",
"Options (persistent)");
724 sectionFormatter->label(
"Nonpersistent Options",
"Options (nonpersistent)");
725 sectionFormatter->label(
"Usage",
"\nUsage");
726 sectionFormatter->label(
"bool:{true,false}",
"{true,false}");
727 sectionFormatter->label(
":{standard,required,full,default}",
"{standard,required,full,default}");
728 sectionFormatter->label(
":{standard,exact,expanded}",
"{standard,exact,expanded}");
729 sectionFormatter->column_width(7);
731 return sectionFormatter;
736 CLI::App*
Config::
addInstance(
const std::string& name,
const std::string& description,
const std::string& group,
bool final) {
737 CLI::App* instanceSc =
app->add_subcommand(name, description)
741 ->configurable(
false)
742 ->allow_extras(
false)
743 ->disabled(name.empty());
747 ->configurable(!instanceSc->get_disabled());
749 if (!instanceSc->get_disabled()) {
768 return app->get_subcommand(name);
778 "Show current configuration and exit")
779 ->configurable(
false)
780 ->disable_flag_override();
784 "--command-line{standard}",
785 [app]([[maybe_unused]] std::int64_t count) {
786 const std::string& result = app->get_option(
"--command-line")->as<std::string>();
787 if (result ==
"standard") {
790 "Below is a command line viewing all non-default and required options:\n"
791 "* Options show their configured value\n"
792 "* Required but not yet configured options show <REQUIRED> as value\n"
793 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
796 if (result ==
"active") {
799 "Below is a command line viewing the active set of options with their default or configured values:\n"
800 "* Options show either their configured or default value\n"
801 "* Required but not yet configured options show <REQUIRED> as value\n"
802 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
805 if (result ==
"complete") {
808 "Below is a command line viewing the complete set of options with their default values\n"
809 "* Options show their default value\n"
810 "* Required but not yet configured options show <REQUIRED> as value\n"
811 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
814 if (result ==
"required") {
817 "Below is a command line viewing required options only:\n"
818 "* Options show either their configured or default value\n"
819 "* Required but not yet configured options show <REQUIRED> as value\n"
820 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
824 "Print command-line\n"
825 "* standard (default): Show all non-default and required options\n"
826 "* active: Show all active options\n"
827 "* complete: Show the complete option set with default values\n"
828 "* required: Show only required options")
829 ->configurable(
false)
830 ->check(
CLI::IsMember({
"standard",
"active",
"complete",
"required"}));
837 "-h{exact},--help{exact}",
841 "Print help message and exit\n"
842 "* standard: display help for the last command processed\n"
843 "* exact: display help for the command directly preceding --help\n"
844 "* expanded: print help including all descendant command options")
845 ->group(app->get_formatter()->get_label(
"Nonpersistent Options"))
846 ->check(
CLI::IsMember({
"standard",
"exact",
"expanded"}));
853 "--help{exact},-h{exact}",
857 "Print help message and exit\n"
858 "* standard: display help for the last command processed\n"
859 "* exact: display help for the command directly preceding --help\n")
860 ->group(app->get_formatter()->get_label(
"Nonpersistent Options"))
861 ->check(
CLI::IsMember({
"standard",
"exact"}));
868 app->needs(instance);
870 for (
const auto& sub : instance->get_subcommands([](
const CLI::App* sc) ->
bool {
871 return sc->get_required();
873 instance->needs(sub);
876 app->remove_needs(instance);
878 for (
const auto& sub : instance->get_subcommands([](
const CLI::App* sc) ->
bool {
879 return sc->get_required();
881 instance->remove_needs(sub);
885 instance->required(required);
886 instance->ignore_case(required);
891 if (instance->get_ignore_case()) {
892 app->remove_needs(instance);
895 for (
const auto& sub : instance->get_subcommands({})) {
896 if (sub->get_ignore_case()) {
897 instance->remove_needs(sub);
898 sub->required(
false);
902 if (instance->get_ignore_case()) {
903 app->needs(instance);
906 for (
const auto& sub : instance->get_subcommands({})) {
907 if (sub->get_ignore_case()) {
908 instance->needs(sub);
914 instance->required(disabled ?
false : instance->get_ignore_case());
920 return app->remove_subcommand(instance);
925 ->add_option(name, description)
926 ->type_name(typeName)
929 ->group(
"Application Options");
937 Config::
addStringOption(
const std::string& name,
const std::string& description,
const std::string& typeName,
bool configurable) {
940 ->configurable(configurable);
944 const std::string& description,
945 const std::string& typeName,
946 const std::string& defaultValue) {
951 ->default_str(defaultValue);
959 const std::string& description,
960 const std::string& typeName,
961 const std::string& defaultValue,
965 ->configurable(configurable);
969 const std::string& description,
970 const std::string& typeName,
971 const char* defaultValue) {
976 const std::string& name,
const std::string& description,
const std::string& typeName,
const char* defaultValue,
bool configurable) {
981 if (
app->get_option(name) ==
nullptr) {
982 throw CLI::OptionNotFound(name);
985 return (*
app)[name]->as<std::string>();
990 const std::string& description,
993 const std::string& groupName) {
994 app->add_flag(name, variable, description)
996 ->configurable(configurable)
CLI::App * getApp() const
CallForCommandline(CLI::App *app, const std::string &description, Mode mode)
CLI::App * getApp() const
CallForShowConfig(CLI::App *app)
std::string getConfigFile() const
CallForWriteConfig(const std::string &configFile)
static void setVerboseLevel(int level)
static void setLogLevel(int level)
static void setQuiet(bool quiet=true)
static CLI::Option * enforceLogFileOpt
static CLI::Option * verboseLevelOpt
static CLI::Option * addStringOption(const std::string &name, const std::string &description, const std::string &typeName, const std::string &defaultValue)
static CLI::Option * addStringOption(const std::string &name, const std::string &description, const std::string &typeName, const std::string &defaultValue, bool configurable)
static CLI::Option * groupNameOpt
static bool removeInstance(CLI::App *instance)
static std::string applicationName
static std::map< std::string, std::string > aliases
static CLI::App * addHelp(CLI::App *app)
static void addFlag(const std::string &name, bool &variable, const std::string &description, bool required, bool configurable=true, const std::string &groupName="Application Options")
static CLI::App * helpTriggerApp
static CLI::Option * addStringOption(const std::string &name, const std::string &description, const std::string &typeName, bool configurable)
static CLI::Option * addStringOption(const std::string &name, const std::string &description, const std::string &typeName, const char *defaultValue)
static std::shared_ptr< CLI::Formatter > sectionFormatter
static CLI::Option * logFileOpt
static std::string configDirectory
static std::shared_ptr< CLI::App > app
static std::string pidDirectory
static CLI::App * addSimpleHelp(CLI::App *app)
static CLI::App * getInstance(const std::string &name)
static std::string getApplicationName()
static CLI::App * showConfigTriggerApp
static void disabled(CLI::App *instance, bool disabled=true)
static CLI::App * addInstance(const std::string &name, const std::string &description, const std::string &group, bool final=false)
static CLI::App * addStandardFlags(CLI::App *app)
static CLI::Option * userNameOpt
static std::map< std::string, CLI::Option * > applicationOptions
static CLI::Option * addStringOption(const std::string &name, const std::string &description, const std::string &typeName, const char *defaultValue, bool configurable)
static std::string logDirectory
static CLI::Option * addStringOption(const std::string &name, const std::string &description, const std::string &typeName)
static bool init(int argc, char *argv[])
static void required(CLI::App *instance, bool required=true)
static CLI::Option * quietOpt
static std::string getStringOptionValue(const std::string &name)
static CLI::Option * daemonizeOpt
static CLI::Option * logLevelOpt
static int getVerboseLevel()
static void startDaemon(const std::string &pidFileName, const std::string &userName, const std::string &groupName)
static pid_t stopDaemon(const std::string &pidFileName)
static void erasePidFile(const std::string &pidFileName)
static std::string createCommandLineTemplate(CLI::App *app, CLI::CallForCommandline::Mode mode)
static void createCommandLineTemplate(std::stringstream &out, CLI::App *app, CLI::CallForCommandline::Mode mode)
static void createCommandLineOptions(std::stringstream &out, CLI::App *app, CLI::CallForCommandline::Mode mode)
static std::shared_ptr< CLI::App > makeApp()
static std::string bash_backslash_escape_no_whitespace(std::string_view s)
static std::string createCommandLineOptions(CLI::App *app, CLI::CallForCommandline::Mode mode)
static std::shared_ptr< CLI::HelpFormatter > makeSectionFormatter()
static std::string createCommandLineSubcommands(CLI::App *app, CLI::CallForCommandline::Mode mode)