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"
46#ifndef DOXYGEN_SHOULD_SKIP_THIS
48#include "log/Logger.h"
66#include <unordered_set>
82 static const std::unordered_set<
char> special{
85 '|',
'&',
';',
'<',
'>',
'(',
')',
'{',
'}',
86 '*',
'?',
'[',
']',
'~',
'!',
'#',
'='
90 out.reserve(s.size() * 3);
92 for (
const char c : s) {
93 if (special.contains(c)) {
102 const CLI::Option* disabledOpt = app->get_option_no_throw(
"--disabled");
103 const bool disabled = disabledOpt !=
nullptr ? disabledOpt->as<
bool>() :
false;
105 for (
const CLI::Option* option : app->get_options()) {
106 if (option->get_configurable()) {
111 if (option->count() > 0) {
113 for (
const auto& result : option->reduced_results()) {
114 value += (!result.empty() ? result :
"\"\"") +
" ";
117 }
catch (
CLI::ParseError& e) {
121 }
else if (option->get_required()) {
122 value =
"<REQUIRED>";
126 if (option->get_required()) {
127 if (option->count() > 0) {
129 for (
const auto& result : option->reduced_results()) {
130 value += (!result.empty() ? result :
"\"\"") +
" ";
133 }
catch (
CLI::ParseError& e) {
138 value =
"<REQUIRED>";
144 if (option->count() > 0) {
146 for (
const auto& result : option->reduced_results()) {
147 value += (!result.empty() ? result :
"\"\"") +
" ";
150 }
catch (
CLI::ParseError& e) {
154 }
else if (!option->get_default_str().empty()) {
155 value = option->get_default_str();
156 }
else if (!option->get_required()) {
159 value =
"<REQUIRED>";
164 if (!value.empty()) {
165 if (value.starts_with(std::string{
"["}) && value.ends_with(
"]")) {
166 value = value.substr(1, value.size() - 2);
169 if (value !=
"<REQUIRED>" && value !=
"\"\"" && !value.starts_with(
"<[")) {
172 out <<
"--" << option->get_single_name() << ((option->get_items_expected_max() == 0) ?
"=" :
" ") << value <<
" ";
176 }
else if (disabledOpt->get_default_str() ==
"false") {
177 out <<
"--disabled=true ";
182 std::stringstream out;
186 std::string optionString = out.str();
187 if (!optionString.empty() && optionString.back() ==
' ') {
188 optionString.pop_back();
197 std::stringstream out;
199 const CLI::Option* disabledOpt = app->get_option_no_throw(
"--disabled");
201 for (
CLI::App* subcommand : app->get_subcommands({})) {
202 if (!subcommand->get_name().empty()) {
212 std::string outString;
215 if (!outString.empty()) {
221 if (!outString.empty()) {
222 out << app->get_name() <<
" " << outString;
227 std::stringstream out;
231 std::string outString = out.str();
232 while (app->get_parent() !=
nullptr) {
233 app = app->get_parent();
236 std::string(app->get_name()).append(
" ").append(!parentOptions.empty() ? parentOptions.append(
" ") :
"").append(outString);
239 if (outString.empty()) {
247 const std::string modeString = commandlineTriggerApp->get_option(
"--command-line")->as<std::string>();
250 std::ostringstream out;
252 if (modeString ==
"standard") {
253 out <<
"Below is a command line viewing all non-default and required options:\n"
254 "* Options show their configured value\n"
255 "* Required but not yet configured options show <REQUIRED> as value\n"
256 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap"
260 }
else if (modeString ==
"active") {
261 out <<
"Below is a command line viewing the active set of options with their default or configured values:\n"
262 "* Options show either their configured or default value\n"
263 "* Required but not yet configured options show <REQUIRED> as value\n"
264 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap"
267 }
else if (modeString ==
"complete") {
268 out <<
"Below is a command line viewing the complete set of options with their default values\n"
269 "* Options show their default value\n"
270 "* Required but not yet configured options show <REQUIRED> as value\n"
271 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap"
274 }
else if (modeString ==
"required") {
275 out <<
"Below is a command line viewing required options only:\n"
276 "* Options show either their configured or default value\n"
277 "* Required but not yet configured options show <REQUIRED> as value\n"
278 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap"
292 std::stringstream out;
295 out << configTriggeredApp->config_to_str(
true,
true);
296 }
catch (
const CLI::ParseError& e) {
298 <<
"] Showing current config: " << configTriggeredApp->get_name() <<
" " << e.get_name() <<
" " << e.what();
305 std::stringstream out;
307 std::ofstream confFile(subCommand
->getOption("--write-config")->as<std::string>());
308 if (confFile.is_open()) {
313 <<
"] Writing config file: " + subCommand
->getOption("--write-config")->as<std::string>() << std::endl
315 }
catch (
const CLI::ParseError& e) {
318 <<
"] Writing config file: " << e.get_name() <<
" " << e.what() << std::endl;
323 <<
"] Writing config file: " << std::strerror(errno) << std::endl;
330 std::stringstream out;
332 const std::string helpMode =
333 (helpTriggerApp !=
nullptr ? helpTriggerApp->get_option(
"--help") : subCommand
->getOption("--help"))->as<std::string>();
335 const CLI::App* helpApp =
nullptr;
336 CLI::AppFormatMode mode =
CLI::AppFormatMode::Normal;
337 if (helpMode ==
"exact") {
338 helpApp = helpTriggerApp;
339 }
else if (helpMode ==
"expanded") {
340 helpApp = helpTriggerApp;
341 mode =
CLI::AppFormatMode::All;
345 }
catch (
CLI::ParseError& e) {
362 const std::string& userName,
363 const std::string& groupName,
364 const std::string& configDirectory,
365 const std::string& logDirectory,
366 const std::string& pidDirectory) {
370 description("Configuration for Application '" + applicationName +
"'");
372 footer("Application '" + applicationName +
373 "' powered by SNode.C\n"
374 "(C) 2020-2026 Volker Christian <me@vchrist.at>\n"
375 "https://github.com/SNodeC/snode.c");
377 setConfig(configDirectory +
"/" + applicationName +
".conf");
381 "Write config file and exit",
383 configDirectory +
"/" + applicationName +
".conf",
384 !
CLI::ExistingDirectory
),
397 "-m{true},--monochrom-logmonochromLogOption{true}",
398 [&monochromLogOpt
= this->monochromLogOpt]() {
399 if (monochromLogOpt->as<
bool>()) {
405 "Monochrom log output",
408 CLI::IsMember({
"true",
"false"})
),
410 ->trigger_on_parse();
413 "-q{true},--quiet{true}",
417 CLI::IsMember({
"true",
"false"})
),
421 "-e{true},--enforce-log-file{true}",
422 "Enforce writing of logs to file for foreground applications",
425 CLI::IsMember({
"true",
"false"})
),
429 "-d{true},--daemonize{true}",
430 "Start application as daemon",
433 CLI::IsMember({
"true",
"false"})
),
438 "Run daemon under specific user permissions",
441 CLI::TypeValidator<std::string>()
),
446 "Run daemon under specific group permissions",
449 CLI::TypeValidator<std::string>()
),
453 "Make an instance also known as an alias in configuration files",
454 "instance=instance_alias [instance=instance_alias [...]]",
455 CLI::TypeValidator<std::string>()
),
471 const pid_t daemonPid =
473 std::cout <<
"Daemon terminated: Pid = " << daemonPid << std::endl;
475 std::cout <<
"DaemonError: " << e.what() << std::endl;
477 std::cout <<
"DaemonFailure: " << e.what() << std::endl;
501 std::cout <<
"Running as daemon (double fork)" << std::endl;
508 const std::string logFile =
logFileOpt->as<std::string>();
509 if (!logFile.empty()) {
513 const std::string logFile =
logFileOpt->as<std::string>();
514 if (!logFile.empty()) {
515 std::cout <<
"Writing logs to file " << logFile << std::endl;
529 if (pidFile.good()) {
533 if (getpid() == pid) {
537 }
else if (fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK) >= 0) {
539 while (read(STDIN_FILENO, buf, 1024) > 0) {
547 bool proceed =
false;
565 }
else if (
getOption("--write-config")->count() > 0) {
573 }
catch (
const CLI::Success&) {
579 }
catch (
const CLI::ParseError& e) {
587 throw CLI::CallForVersion();
590 throw CLI::CallForHelp();
604 <<
" ... exiting" << std::endl;
607 <<
" ... exiting" << std::endl;
609 std::cout <<
"Pid: " << getpid() <<
", child pid: " << e
.getPid() <<
": " << e.what() << std::endl;
610 }
catch (
const CLI::CallForHelp&) {
612 }
catch (
const CLI::CallForVersion&) {
613 std::cout <<
version() << std::endl << std::endl;
614 }
catch (
const CLI::ConversionError& e) {
618 }
catch (
const CLI::ArgumentMismatch& e) {
622 }
catch (
const CLI::ConfigError& e) {
625 std::cout <<
" Adding '-w' on the command line may solve this problem" << std::endl;
627 }
catch (
const CLI::ParseError& e) {
628 const std::string what = e.what();
629 if (what.find(
"[Option Group: ") != std::string::npos) {
632 <<
" Anonymous instance(s) not configured in source code " << std::endl;
639 }
catch ([[maybe_unused]]
const CLI::ParseError& e) {
640 std::cout << std::endl <<
"Append -h or --help to your command line for more information." << std::endl;
641 }
catch (
const CLI::Error& e) {
645 std::cout << std::endl <<
"Append -h or --help to your command line for more information." << std::endl;
660 const struct passwd* pw =
nullptr;
661 const struct group* gr =
nullptr;
663 if ((pw = getpwuid(getuid())) ==
nullptr) {
665 }
else if ((gr = getgrgid(pw->pw_gid)) ==
nullptr) {
667 }
else if ((euid = geteuid()) == 0) {
672 const char* homedir =
nullptr;
673 if ((homedir = std::getenv(
"XDG_CONFIG_HOME")) ==
nullptr) {
674 if ((homedir = std::getenv(
"HOME")) ==
nullptr) {
675 homedir = pw->pw_dir;
679 if (homedir !=
nullptr) {
681 logDirectory = std::string(homedir) +
"/.local/log/snode.c";
682 pidDirectory = std::string(homedir) +
"/.local/run/snode.c";
690 std::filesystem::permissions(
692 (std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec) &
693 ~std::filesystem::perms::others_all);
694 if (geteuid() == 0) {
695 const struct group* gr =
nullptr;
696 if ((gr = getgrnam(
XSTR(GROUP_NAME))) !=
nullptr) {
698 std::cout <<
"Warning: Can not set group ownership of '" <<
configDirectory
699 <<
"' to 'snodec':" << strerror(errno) << std::endl;
702 std::cout <<
"Error: Can not find group 'snodec'. Add it using groupadd or addgroup" << std::endl;
703 std::cout <<
" and add the current user to this group." << std::endl;
709 std::cout <<
"Error: Can not create directory '" <<
configDirectory <<
"'" << std::endl;
714 if (proceed && !std::filesystem::exists(
logDirectory)) {
715 if (std::filesystem::create_directories(
logDirectory)) {
717 (std::filesystem::perms::owner_all | std::filesystem::perms::group_all) &
718 ~std::filesystem::perms::others_all);
719 if (geteuid() == 0) {
720 const struct group* gr =
nullptr;
721 if ((gr = getgrnam(
XSTR(GROUP_NAME))) !=
nullptr) {
722 if (chown(
logDirectory.c_str(), euid, gr->gr_gid) < 0) {
723 std::cout <<
"Warning: Can not set group ownership of '" <<
logDirectory <<
"' to 'snodec':" << strerror(errno)
727 std::cout <<
"Error: Can not find group 'snodec'. Add it using groupadd or addgroup" << std::endl;
728 std::cout <<
" and add the current user to this group." << std::endl;
734 std::cout <<
"Error: Can not create directory '" <<
logDirectory <<
"'" << std::endl;
739 if (proceed && !std::filesystem::exists(
pidDirectory)) {
740 if (std::filesystem::create_directories(
pidDirectory)) {
742 (std::filesystem::perms::owner_all | std::filesystem::perms::group_all) &
743 ~std::filesystem::perms::others_all);
744 if (geteuid() == 0) {
745 const struct group* gr =
nullptr;
746 if ((gr = getgrnam(
XSTR(GROUP_NAME))) !=
nullptr) {
747 if (chown(
pidDirectory.c_str(), euid, gr->gr_gid) < 0) {
748 std::cout <<
"Warning: Can not set group ownership of '" <<
pidDirectory <<
"' to 'snodec':" << strerror(errno)
752 std::cout <<
"Error: Can not find group 'snodec'. Add it using groupadd or addgroup." << std::endl;
753 std::cout <<
" and add the current user to this group." << std::endl;
759 std::cout <<
"Error: Can not create directory '" <<
pidDirectory <<
"'" << std::endl;
static void setDisableColor(bool disableColorLog=true)
static void logToFile(const std::string &logFile)
static void setVerboseLevel(int level)
static void setLogLevel(int level)
static void setQuiet(bool quiet=true)
static bool getDisableColor()
CLI::Option * enforceLogFileOpt
bool parse1(int argc, char *argv[])
bool parse2(int argc, char *argv[], bool parse1=false)
CLI::Option * verboseLevelOpt
bool bootstrap(int argc, char *argv[])
std::string applicationName
CLI::Option * userNameOpt
CLI::Option * daemonizeOpt
CLI::Option * monochromLogOpt
ConfigRoot * addRootOptions(const std::string &applicationName, const std::string &userName, const std::string &groupName, const std::string &configDirectory, const std::string &logDirectory, const std::string &pidDirectory)
CLI::Option * writeConfigOpt
CLI::Option * groupNameOpt
CLI::Option * logLevelOpt
static std::string applicationName
static const std::string & getApplicationName()
static std::string configDirectory
static std::string pidDirectory
static std::string logDirectory
static bool init(int argc, char *argv[])
static ConfigRoot configRoot
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)
std::string version() const
SubCommand * description(const std::string &description)
SubCommand * allowExtras(bool allow=true)
CLI::Option * addFlagFunction(const std::string &name, const std::function< void()> &callback, const std::string &description, const std::string &typeName, const std::string &defaultValue, const CLI::Validator &validator) const
SubCommand * footer(const std::string &footer)
void parse(int argc, char *argv[]) const
SubCommand(SubCommand *parent, std::shared_ptr< utils::AppWithPtr > appWithPtr, const std::string &group, bool final)
static CLI::App * showConfigTriggerApp
CLI::Option * addFlag(const std::string &name, const std::string &description, const std::string &typeName, ValueTypeT defaultValue, const CLI::Validator &validator) const
CLI::Option * addOption(const std::string &name, const std::string &description, const std::string &typeName, ValueTypeT defaultValue, const CLI::Validator &validator) const
CLI::Option * setVersionFlag(const std::string &version) const
SubCommand * finalCallback(const std::function< void()> &finalCallback)
static CLI::App * helpTriggerApp
CLI::Option * addOption(const std::string &name, const std::string &description, const std::string &typeName, const CLI::Validator &validator) const
CLI::Option * getOption(const std::string &name) const
CLI::Option * setConfig(const std::string &defaultConfigFile) const
static std::map< std::string, std::string > aliases
std::string help(const CLI::App *helpApp, const CLI::AppFormatMode &mode) const
std::string configToStr() const
static CLI::App * commandlineTriggerApp
CLI::Option * setLogFile(const std::string &defaultLogFile) const
CLI::Option * setConfigurable(CLI::Option *option, bool configurable) const
CLI::Option * addFlag(const std::string &name, const std::string &description, const std::string &typeName, const CLI::Validator &validator) const
std::string operator+(const std::string &string, const Code &code)
static std::string createCommandLineTemplate(CLI::App *app, utils::CallForCommandline::Mode mode)
static std::string createCommandLineOptions(CLI::App *app, utils::CallForCommandline::Mode mode)
static std::string doWriteConfig(utils::SubCommand *subCommand)
static std::string createCommandLineSubcommands(CLI::App *app, utils::CallForCommandline::Mode mode)
static std::string getHelp(utils::SubCommand *subCommand, CLI::App *helpTriggerApp)
static std::string getConfig(CLI::App *configTriggeredApp)
static void createCommandLineTemplate(std::stringstream &out, CLI::App *app, utils::CallForCommandline::Mode mode)
static void createCommandLineOptions(std::stringstream &out, CLI::App *app, utils::CallForCommandline::Mode mode)
static std::string bash_backslash_escape_no_whitespace(std::string_view s)
static std::string getCommandLine(CLI::App *commandlineTriggerApp)