SNode.C
Loading...
Searching...
No Matches
Config.cpp
Go to the documentation of this file.
1/*
2 * SNode.C - A Slim Toolkit for Network Communication
3 * Copyright (C) Volker Christian <me@vchrist.at>
4 * 2020, 2021, 2022, 2023, 2024, 2025
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published
8 * by the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20/*
21 * MIT License
22 *
23 * Permission is hereby granted, free of charge, to any person obtaining a copy
24 * of this software and associated documentation files (the "Software"), to deal
25 * in the Software without restriction, including without limitation the rights
26 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
27 * copies of the Software, and to permit persons to whom the Software is
28 * furnished to do so, subject to the following conditions:
29 *
30 * The above copyright notice and this permission notice shall be included in
31 * all copies or substantial portions of the Software.
32 *
33 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
38 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
39 * THE SOFTWARE.
40 */
41
42#include "utils/Config.h"
43
44#include "utils/Daemon.h"
45#include "utils/Exceptions.h"
46#include "utils/Formatter.h"
47
48#ifndef DOXYGEN_SHOULD_SKIP_THIS
49
50#include "log/Logger.h"
51
52#include <cerrno>
53#include <cstdint>
54#include <cstdlib>
55#include <cstring>
56#include <fcntl.h>
57#include <filesystem>
58#include <fstream>
59#include <functional>
60#include <grp.h>
61#include <iostream>
62#include <memory>
63#include <pwd.h>
64#include <sstream>
65#include <string_view>
66#include <sys/types.h>
67#include <unistd.h>
68#include <unordered_set>
69#include <vector>
70
71#ifdef __GNUC__
72#pragma GCC diagnostic push
73#pragma GCC diagnostic ignored "-Wfloat-equal"
74#ifdef __has_warning
75#if __has_warning("-Wweak-vtables")
76#pragma GCC diagnostic ignored "-Wweak-vtables"
77#endif
78#if __has_warning("-Wcovered-switch-default")
79#pragma GCC diagnostic ignored "-Wcovered-switch-default"
80#endif
81#endif
82#endif
83#include "utils/CLI11.hpp"
84#ifdef __GNUC__
85#pragma GCC diagnostic pop
86#endif
87
88#endif /* DOXYGEN_SHOULD_SKIP_THIS */
89
90#define XSTR(s) STR(s)
91#define STR(s) #s
92
93namespace utils {
94
95 static std::shared_ptr<CLI::App> makeApp() { // NO_LINT
96 const std::shared_ptr<CLI::App> app = std::make_shared<CLI::App>();
97
98 app->configurable(false);
99 app->allow_extras();
100 app->allow_config_extras();
101
102 const std::shared_ptr<CLI::HelpFormatter> helpFormatter = std::make_shared<CLI::HelpFormatter>();
103
104 helpFormatter->label("SUBCOMMAND", "INSTANCE");
105 helpFormatter->label("SUBCOMMANDS", "INSTANCES");
106 helpFormatter->label("PERSISTENT", "");
107 helpFormatter->label("Persistent Options", "Options (persistent)");
108 helpFormatter->label("Nonpersistent Options", "Options (nonpersistent)");
109 helpFormatter->label("Usage", "\nUsage");
110 helpFormatter->label("bool:{true,false}", "{true,false}");
111 helpFormatter->label(":{standard,required,full,default}", "{standard,required,full,default}");
112 helpFormatter->label(":{standard,exact,expanded}", "{standard,exact,expanded}");
113 helpFormatter->column_width(7);
114
115 app->formatter(helpFormatter);
116
117 app->config_formatter(std::make_shared<CLI::ConfigFormatter>());
118 app->get_config_formatter_base()->arrayDelimiter(' ');
119
120 app->option_defaults()->take_last();
121 app->option_defaults()->group(app->get_formatter()->get_label("Nonpersistent Options"));
122
124
125 return app;
126 }
127
128 std::shared_ptr<CLI::App> Config::app = makeApp();
129
130 bool Config::init(int argc, char* argv[]) {
131 bool proceed = true;
132
133 Config::argc = argc;
134 Config::argv = argv;
135
136 applicationName = std::filesystem::path(argv[0]).filename();
137
138 uid_t euid = 0;
139 struct passwd* pw = nullptr;
140 struct group* gr = nullptr;
141
142 if ((pw = getpwuid(getuid())) == nullptr) {
143 proceed = false;
144 } else if ((gr = getgrgid(pw->pw_gid)) == nullptr) {
145 proceed = false;
146 } else if ((euid = geteuid()) == 0) {
147 configDirectory = "/etc/snode.c";
148 logDirectory = "/var/log/snode.c";
149 pidDirectory = "/var/run/snode.c";
150 } else {
151 const char* homedir = nullptr;
152 if ((homedir = std::getenv("XDG_CONFIG_HOME")) == nullptr) {
153 if ((homedir = std::getenv("HOME")) == nullptr) {
154 homedir = pw->pw_dir;
155 }
156 }
157
158 if (homedir != nullptr) {
159 configDirectory = std::string(homedir) + "/.config/snode.c";
160 logDirectory = std::string(homedir) + "/.local/log/snode.c";
161 pidDirectory = std::string(homedir) + "/.local/run/snode.c";
162 } else {
163 proceed = false;
164 }
165 }
166
167 if (proceed && !std::filesystem::exists(configDirectory)) {
168 if (std::filesystem::create_directories(configDirectory)) {
169 std::filesystem::permissions(
170 configDirectory,
171 (std::filesystem::perms::owner_all | std::filesystem::perms::group_read | std::filesystem::perms::group_exec) &
172 ~std::filesystem::perms::others_all);
173 if (geteuid() == 0) {
174 struct group* gr = nullptr;
175 if ((gr = getgrnam(XSTR(GROUP_NAME))) != nullptr) {
176 if (chown(configDirectory.c_str(), euid, gr->gr_gid) < 0) {
177 std::cout << "Warning: Can not set group ownership of '" << configDirectory
178 << "' to 'snodec':" << strerror(errno) << std::endl;
179 }
180 } else {
181 std::cout << "Error: Can not find group 'snodec'. Add it using groupadd or addgroup" << std::endl;
182 std::cout << " and add the current user to this group." << std::endl;
183 std::filesystem::remove(configDirectory);
184 proceed = false;
185 }
186 }
187 } else {
188 std::cout << "Error: Can not create directory '" << configDirectory << "'" << std::endl;
189 proceed = false;
190 }
191 }
192
193 if (proceed && !std::filesystem::exists(logDirectory)) {
194 if (std::filesystem::create_directories(logDirectory)) {
195 std::filesystem::permissions(logDirectory,
196 (std::filesystem::perms::owner_all | std::filesystem::perms::group_all) &
197 ~std::filesystem::perms::others_all);
198 if (geteuid() == 0) {
199 struct group* gr = nullptr;
200 if ((gr = getgrnam(XSTR(GROUP_NAME))) != nullptr) {
201 if (chown(logDirectory.c_str(), euid, gr->gr_gid) < 0) {
202 std::cout << "Warning: Can not set group ownership of '" << logDirectory << "' to 'snodec':" << strerror(errno)
203 << std::endl;
204 }
205 } else {
206 std::cout << "Error: Can not find group 'snodec'. Add it using groupadd or addgroup" << std::endl;
207 std::cout << " and add the current user to this group." << std::endl;
208 std::filesystem::remove(configDirectory);
209 proceed = false;
210 }
211 }
212 } else {
213 std::cout << "Error: Can not create directory '" << logDirectory << "'" << std::endl;
214 proceed = false;
215 }
216 }
217
218 if (proceed && !std::filesystem::exists(pidDirectory)) {
219 if (std::filesystem::create_directories(pidDirectory)) {
220 std::filesystem::permissions(pidDirectory,
221 (std::filesystem::perms::owner_all | std::filesystem::perms::group_all) &
222 ~std::filesystem::perms::others_all);
223 if (geteuid() == 0) {
224 struct group* gr = nullptr;
225 if ((gr = getgrnam(XSTR(GROUP_NAME))) != nullptr) {
226 if (chown(pidDirectory.c_str(), euid, gr->gr_gid) < 0) {
227 std::cout << "Warning: Can not set group ownership of '" << pidDirectory << "' to 'snodec':" << strerror(errno)
228 << std::endl;
229 }
230 } else {
231 std::cout << "Error: Can not find group 'snodec'. Add it using groupadd or addgroup." << std::endl;
232 std::cout << " and add the current user to this group." << std::endl;
233 std::filesystem::remove(configDirectory);
234 proceed = false;
235 }
236 }
237 } else {
238 std::cout << "Error: Can not create directory '" << pidDirectory << "'" << std::endl;
239 proceed = false;
240 }
241 }
242
243 if (proceed) {
244 app->description("Configuration for Application '" + applicationName + "'");
245
246 app->footer("Application '" + applicationName +
247 "' powered by SNode.C\n"
248 "(C) 2020-2025 Volker Christian <me@vchrist.at>\n"
249 "https://github.com/SNodeC/snode.c");
250
251 app->set_config( //
252 "-c,--config-file",
253 configDirectory + "/" + applicationName + ".conf",
254 "Read a config file",
255 false) //
256 ->take_all()
257 ->type_name("configfile")
258 ->check(!CLI::ExistingDirectory);
259
260 app->add_option("-w,--write-config", "Write config file and exit")
261 ->configurable(false)
262 ->default_val(configDirectory + "/" + applicationName + ".conf")
263 ->type_name("[configfile]")
264 ->check(!CLI::ExistingDirectory)
265 ->expected(0, 1);
266
267 app->add_flag( //
268 "-k,--kill",
269 "Kill running daemon") //
270 ->configurable(false)
271 ->disable_flag_override();
272
273 app->add_option( //
274 "-i,--instance-alias",
275 "Make an instance also known as an alias in configuration files")
276 ->configurable(false)
277 ->type_name("instance=instance_alias [instance=instance_alias [...]]")
278 ->each([](const std::string& item) {
279 const auto it = item.find('=');
280 if (it != std::string::npos) {
281 aliases[item.substr(0, it)] = item.substr(it + 1);
282 } else {
283 throw CLI::ConversionError("Can not convert '" + item + "' to a 'instance=instance_alias' pair");
284 }
285 });
286
287 addStandardFlags(app.get());
288
289 logLevelOpt = app->add_option( //
290 "-l,--log-level",
291 "Log level") //
292 ->default_val(4)
293 ->type_name("level")
294 ->check(CLI::Range(0, 6))
295 ->group(app->get_formatter()->get_label("Persistent Options"));
296
297 verboseLevelOpt = app->add_option( //
298 "-v,--verbose-level",
299 "Verbose level") //
300 ->default_val(2)
301 ->type_name("level")
302 ->check(CLI::Range(0, 10))
303 ->group(app->get_formatter()->get_label("Persistent Options"));
304
305 quietOpt = app->add_flag( //
306 "-q{true},!-u,--quiet{true}",
307 "Quiet mode") //
308 ->default_val("false")
309 ->type_name("bool")
310 ->check(CLI::IsMember({"true", "false"}))
311 ->group(app->get_formatter()->get_label("Persistent Options"));
312
313 logFileOpt = app->add_option( //
314 "--log-file",
315 "Log file path") //
316 ->default_val(logDirectory + "/" + applicationName + ".log")
317 ->type_name("logfile")
318 ->check(!CLI::ExistingDirectory)
319 ->group(app->get_formatter()->get_label("Persistent Options"));
320
321 enforceLogFileOpt = app->add_flag( //
322 "-e{true},!-n,--enforce-log-file{true}",
323 "Enforce writing of logs to file for foreground applications") //
324 ->default_val("false")
325 ->type_name("bool")
326 ->check(CLI::IsMember({"true", "false"}))
327 ->group(app->get_formatter()->get_label("Persistent Options"));
328
329 daemonizeOpt = app->add_flag( //
330 "-d{true},!-f,--daemonize{true}",
331 "Start application as daemon") //
332 ->default_val("false")
333 ->type_name("bool")
334 ->check(CLI::IsMember({"true", "false"}))
335 ->group(app->get_formatter()->get_label("Persistent Options"));
336
337 userNameOpt = app->add_option( //
338 "--user-name",
339 "Run daemon under specific user permissions") //
340 ->default_val(pw->pw_name)
341 ->type_name("username")
342 ->needs(daemonizeOpt)
343 ->group(app->get_formatter()->get_label("Persistent Options"));
344
345 groupNameOpt = app->add_option( //
346 "--group-name",
347 "Run daemon under specific group permissions")
348 ->default_val(gr->gr_name)
349 ->type_name("groupname")
350 ->needs(daemonizeOpt)
351 ->group(app->get_formatter()->get_label("Persistent Options"));
352
353 proceed = parse1(); // for stopDaemon and pre init application options
354
355 app->set_version_flag("--version", "1.0-rc1", "Framework version");
356 addHelp(app.get());
357 }
358
359 return proceed;
360 }
361
362 bool Config::bootstrap() {
363 aliases.clear();
364
365 app->final_callback([]() {
366 if (daemonizeOpt->as<bool>() && helpTriggerApp == nullptr && showConfigTriggerApp == nullptr &&
367 (*app)["--write-config"]->count() == 0 && (*app)["--command-line"]->count() == 0) {
368 std::cout << "Running as daemon (double fork)" << std::endl;
369
371 pidDirectory + "/" + applicationName + ".pid", userNameOpt->as<std::string>(), groupNameOpt->as<std::string>());
372
374
375 const std::string logFile = logFileOpt->as<std::string>();
376 if (!logFile.empty()) {
378 }
379 } else if ((*app)["--enforce-log-file"]->as<bool>()) {
380 const std::string logFile = logFileOpt->as<std::string>();
381 if (!logFile.empty()) {
382 std::cout << "Writing logs to file " << logFile << std::endl;
383
385 }
386 }
387 });
388
389 return parse2();
390 }
391
392 bool Config::parse1() {
393 bool proceed = true;
394
395 try {
396 app->parse(argc, argv);
397 } catch (const CLI::ParseError&) {
398 // Do not process ParseError here but on second parse pass
399 }
400
401 if ((*app)["--kill"]->count() > 0) {
402 try {
403 const pid_t daemonPid = utils::Daemon::stopDaemon(pidDirectory + "/" + applicationName + ".pid");
404 std::cout << "Daemon terminated: Pid = " << daemonPid << std::endl;
405 } catch (const DaemonError& e) {
406 std::cout << "DaemonError: " << e.what() << std::endl;
407 } catch (const DaemonFailure& e) {
408 std::cout << "DaemonFailure: " << e.what() << std::endl;
409 }
410
411 proceed = false;
412 } else {
413 app->allow_extras((*app)["--show-config"]->count() != 0);
414
415 if (!quietOpt->as<bool>()) {
416 logger::Logger::setLogLevel(logLevelOpt->as<int>());
417 logger::Logger::setVerboseLevel(verboseLevelOpt->as<int>());
418 }
419
420 logger::Logger::setQuiet(quietOpt->as<bool>());
421 }
422
423 return proceed;
424 }
425
426 // Escape characters with special meaning in Bash (except whitespace).
427 static std::string bash_backslash_escape_no_whitespace(std::string_view s) {
428 static const std::unordered_set<char> special{
429 '\\', '\'', // quoting/escape
430 '`', '$', // substitution
431 '|', '&', ';', '<', '>', '(', ')', '{', '}', // operators
432 '*', '?', '[', ']', '~', '!', '#', '=' // globbing/history/others
433 };
434
435 std::string out;
436 out.reserve(s.size() * 3);
437
438 for (const char c : s) {
439 if (special.contains(c)) {
440 out.push_back('\\');
441 }
442 out.push_back(c);
443 }
444 return out;
445 }
446
447 static void createCommandLineOptions(std::stringstream& out, CLI::App* app, CLI::CallForCommandline::Mode mode) {
448 CLI::Option* disabledOpt = app->get_option_no_throw("--disabled");
449 const bool disabled = disabledOpt != nullptr ? disabledOpt->as<bool>() : false;
450 if (!disabled || mode == CLI::CallForCommandline::Mode::DEFAULT) {
451 for (const CLI::Option* option : app->get_options()) {
452 if (option->get_configurable()) {
453 std::string value;
454
455 switch (mode) {
457 if (option->count() > 0) {
458 value = option->as<std::string>();
459 } else if (option->get_required()) {
460 value = "<REQUIRED>";
461 }
462 break;
464 if (option->get_required()) {
465 if (option->count() > 0) {
466 value = option->as<std::string>();
467 } else {
468 value = "<REQUIRED>";
469 }
470 }
471 break;
473 if (option->count() > 0) {
474 value = option->as<std::string>();
475 } else if (!option->get_default_str().empty()) {
476 value = option->get_default_str();
477 } else if (!option->get_required()) {
478 value = "\"\"";
479 } else {
480 value = "<REQUIRED>";
481 }
482 break;
484 if (!option->get_default_str().empty()) {
485 value = option->get_default_str();
486 } else if (!option->get_required()) {
487 value = "\"\"";
488 } else {
489 value = "<REQUIRED>";
490 }
491 break;
492 }
493 }
494
495 if (!value.empty()) {
496 if (value.starts_with("[") && value.ends_with("]")) {
497 value = value.substr(1, value.size() - 2);
498 }
499
500 if (value != "<REQUIRED>" && value != "\"\"") {
502 }
503 out << "--" << option->get_single_name() << ((option->get_items_expected_max() == 0) ? "=" : " ") << value << " ";
504 }
505 }
506 }
507 } else if (disabledOpt->get_default_str() == "false") {
508 out << "--disabled=true ";
509 }
510 }
511
512 static std::string createCommandLineOptions(CLI::App* app, CLI::CallForCommandline::Mode mode) {
513 std::stringstream out;
514
516
517 std::string optionString = out.str();
518 if (!optionString.empty() && optionString.back() == ' ') {
519 optionString.pop_back();
520 }
521
522 return optionString;
523 }
524
525 static void createCommandLineTemplate(std::stringstream& out, CLI::App* app, CLI::CallForCommandline::Mode mode);
526
527 static std::string createCommandLineSubcommands(CLI::App* app, CLI::CallForCommandline::Mode mode) {
528 std::stringstream out;
529
530 CLI::Option* disabledOpt = app->get_option_no_throw("--disabled");
531 if (disabledOpt == nullptr || !disabledOpt->as<bool>() || mode == CLI::CallForCommandline::Mode::DEFAULT) {
532 for (CLI::App* subcommand : app->get_subcommands({})) {
533 if (!subcommand->get_name().empty()) {
534 createCommandLineTemplate(out, subcommand, mode);
535 }
536 }
537 }
538
539 return out.str();
540 }
541
542 static void createCommandLineTemplate(std::stringstream& out, CLI::App* app, CLI::CallForCommandline::Mode mode) {
543 std::string outString;
544
545 outString = createCommandLineOptions(app, mode);
546 if (!outString.empty()) {
547 outString += " ";
548 }
549
550 outString += createCommandLineSubcommands(app, mode);
551
552 if (!outString.empty()) {
553 out << app->get_name() << " " << outString;
554 }
555 }
556
557 static std::string createCommandLineTemplate(CLI::App* app, CLI::CallForCommandline::Mode mode) {
558 std::stringstream out;
559
561
562 std::string outString = out.str();
563 while (app->get_parent() != nullptr) {
564 app = app->get_parent();
565 std::string parentOptions = createCommandLineOptions(app, mode);
566 outString =
567 std::string(app->get_name()).append(" ").append(!parentOptions.empty() ? parentOptions.append(" ") : "").append(outString);
568 }
569
570 if (outString.empty()) {
571 outString = Config::getApplicationName();
572 }
573
574 return outString;
575 }
576
577 bool Config::parse2() {
578 bool success = false;
579
580 try {
581 try {
582 try {
583 app->parse(argc, argv);
584 success = true;
585 } catch (const CLI::ParseError&) {
586 if (helpTriggerApp == nullptr) {
587 if (showConfigTriggerApp != nullptr) {
588 success = false;
589 throw CLI::CallForShowConfig(showConfigTriggerApp);
590 }
591 if ((*app)["--write-config"]->count() > 0) {
592 success = false;
593 throw CLI::CallForWriteConfig((*app)["--write-config"]->as<std::string>());
594 }
595 }
596
597 throw;
598 }
599 if (helpTriggerApp == nullptr) {
600 if (showConfigTriggerApp != nullptr) {
601 success = false;
602 throw CLI::CallForShowConfig(showConfigTriggerApp);
603 }
604 if ((*app)["--write-config"]->count() > 0) {
605 success = false;
606 throw CLI::CallForWriteConfig((*app)["--write-config"]->as<std::string>());
607 }
608 }
609 } catch (const DaemonError& e) {
610 std::cout << "[" << Color::Code::FG_RED << "Error" << Color::Code::FG_DEFAULT << "] Daemonize: " << e.what()
611 << " ... exiting" << std::endl;
612 } catch (const DaemonFailure& e) {
613 std::cout << "[" << Color::Code::FG_RED << "Failure" << Color::Code::FG_DEFAULT << "] Daemonize: " << e.what()
614 << " ... exiting" << std::endl;
615 } catch (const DaemonSignaled& e) {
616 std::cout << "Pid: " << getpid() << ", child pid: " << e.getPid() << ": " << e.what() << std::endl;
617 } catch (const CLI::CallForHelp&) {
618 const std::string helpMode = helpTriggerApp->get_option("--help")->as<std::string>();
619 const CLI::App* helpApp = nullptr;
620 CLI::AppFormatMode mode = CLI::AppFormatMode::Normal;
621 if (helpMode == "exact") {
622 helpApp = utils::Config::helpTriggerApp;
623 } else if (helpMode == "expanded") {
624 helpApp = utils::Config::helpTriggerApp;
625 mode = CLI::AppFormatMode::All;
626 }
627 std::cout << app->help(helpApp, "", mode) << std::endl;
628 } catch (const CLI::CallForVersion&) {
629 std::cout << app->version() << std::endl << std::endl;
630 } catch (const CLI::CallForCommandline& e) {
631 std::cout << e.what() << std::endl;
632 std::cout << std::endl
633 << Color::Code::FG_GREEN << "command@line" << Color::Code::FG_DEFAULT << ":" << Color::Code::FG_BLUE << "~/> "
635 << std::endl;
636 } catch (const CLI::CallForShowConfig& e) {
637 try {
638 std::cout << e.getApp()->config_to_str(true, true);
639 } catch (const CLI::ParseError& e1) {
640 std::cout << "[" << Color::Code::FG_RED << "Error" << Color::Code::FG_DEFAULT << "] Showing config file: " << e.getApp()
641 << " " << e1.get_name() << " " << e1.what() << std::endl;
642 throw;
643 }
644 } catch (const CLI::CallForWriteConfig& e) {
645 std::cout << e.what() << std::endl;
646 std::ofstream confFile(e.getConfigFile());
647 if (confFile.is_open()) {
648 try {
649 confFile << app->config_to_str(true, true);
650 confFile.close();
651 } catch (const CLI::ParseError& e1) {
652 confFile.close();
653 std::cout << "Error writing config file: " << e1.get_name() << " " << e1.what() << std::endl;
654 throw;
655 }
656 } else {
657 std::cout << "[" << Color::Code::FG_RED << "Error" << Color::Code::FG_DEFAULT
658 << "] Writing config file: " << std::strerror(errno) << std::endl;
659 }
660 } catch (const CLI::ConversionError& e) {
661 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << e.what() << std::endl;
662 throw;
663 } catch (const CLI::ArgumentMismatch& e) {
664 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << e.what() << std::endl;
665 throw;
666 } catch (const CLI::ConfigError& e) {
667 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << e.what() << std::endl;
668 std::cout << " Adding '-w' on the command line may solve this problem" << std::endl;
669 throw;
670 } catch (const CLI::ParseError& e) {
671 const std::string what = e.what();
672 if (what.find("[Option Group: ") != std::string::npos) { // If CLI11 throws that error it means for us there are
673 // unconfigured anonymous instances
674 std::cout << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT
675 << " Anonymous instance(s) not configured in source code " << std::endl;
676 } else {
677 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << what << std::endl;
678 }
679 throw;
680 }
681 } catch ([[maybe_unused]] const CLI::ParseError& e) {
682 std::cout << std::endl << "Append -h or --help to your command line for more information." << std::endl;
683 } catch (const CLI::Error& e) {
684 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << e.what() << std::endl;
685
686 std::cout << std::endl << "Append -h or --help to your command line for more information." << std::endl;
687 }
688
689 return success;
690 }
691
692 void Config::terminate() {
693 if ((*app)["--daemonize"]->as<bool>()) {
694 std::ifstream pidFile(pidDirectory + "/" + applicationName + ".pid", std::ifstream::in);
695
696 if (pidFile.good()) {
697 pid_t pid = 0;
698 pidFile >> pid;
699
700 if (getpid() == pid) {
701 Daemon::erasePidFile(pidDirectory + "/" + applicationName + ".pid");
702 }
703 }
704 } else if (fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK) >= 0) {
705 char buf[1024];
706 while (read(STDIN_FILENO, buf, 1024) > 0) {
707 }
708 }
709 }
710
711 static std::shared_ptr<CLI::HelpFormatter> makeSectionFormatter() {
712 const std::shared_ptr<CLI::HelpFormatter> sectionFormatter = std::make_shared<CLI::HelpFormatter>();
713
714 sectionFormatter->label("SUBCOMMAND", "SECTION");
715 sectionFormatter->label("SUBCOMMANDS", "SECTIONS");
716 sectionFormatter->label("PERSISTENT", "");
717 sectionFormatter->label("Persistent Options", "Options (persistent)");
718 sectionFormatter->label("Nonpersistent Options", "Options (nonpersistent)");
719 sectionFormatter->label("Usage", "\nUsage");
720 sectionFormatter->label("bool:{true,false}", "{true,false}");
721 sectionFormatter->label(":{standard,required,full,default}", "{standard,required,full,default}");
722 sectionFormatter->label(":{standard,exact,expanded}", "{standard,exact,expanded}");
723 sectionFormatter->column_width(7);
724
725 return sectionFormatter;
726 }
727
728 std::shared_ptr<CLI::Formatter> Config::sectionFormatter = makeSectionFormatter();
729
730 CLI::App* Config::addInstance(const std::string& name, const std::string& description, const std::string& group) {
731 CLI::App* instanceSc = app->add_subcommand(name, description) //
732 ->group(group)
733 ->fallthrough()
734 ->formatter(sectionFormatter)
735 ->configurable(false)
736 ->allow_extras(false)
737 ->disabled(name.empty());
738
739 instanceSc //
740 ->option_defaults()
741 ->configurable(!instanceSc->get_disabled());
742
743 if (!instanceSc->get_disabled()) {
744 if (aliases.contains(name)) {
745 instanceSc //
746 ->alias(aliases[name]);
747 }
748 }
749
750 utils::Config::addStandardFlags(instanceSc);
751 utils::Config::addHelp(instanceSc);
752
753 return instanceSc;
754 }
755
756 CLI::App* Config::getInstance(const std::string& name) {
757 return app->get_subcommand(name);
758 }
759
760 CLI::App* Config::addStandardFlags(CLI::App* app) {
761 app //
762 ->add_flag_function(
763 "-s,--show-config",
764 [app](std::size_t) {
765 showConfigTriggerApp = app;
766 },
767 "Show current configuration and exit") //
768 ->configurable(false)
769 ->disable_flag_override();
770
771 app //
772 ->add_flag(
773 "--command-line{standard}",
774 [app]([[maybe_unused]] std::int64_t count) {
775 const std::string& result = app->get_option("--command-line")->as<std::string>();
776 if (result == "standard") {
778 app,
779 "Below is a command line viewing all non-default and required options:\n"
780 "* Options show their configured value\n"
781 "* Required but not yet configured options show <REQUIRED> as value\n"
782 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
784 }
785 if (result == "required") {
787 app,
788 "Below is a command line viewing required options only:\n"
789 "* Options show either their configured or default value\n"
790 "* Required but not yet configured options show <REQUIRED> as value\n"
791 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
793 }
794 if (result == "full") {
796 app,
797 "Below is a command line viewing the full set of options with their default or configured values:\n"
798 "* Options show either their configured or default value\n"
799 "* Required but not yet configured options show <REQUIRED> as value\n"
800 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
802 }
803 if (result == "default") {
805 app,
806 "Below is a command line viewing the full set of options with their default values\n"
807 "* Options show their default value\n"
808 "* Required but not yet configured options show <REQUIRED> as value\n"
809 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
811 }
812 },
813 "Print a command line\n"
814 " standard (default): View all non-default and required options\n"
815 " required: View required options only\n"
816 " full: View the full set of options with their default or configured values\n"
817 " default: View the full set of options with their default values")
818 ->configurable(false)
819 ->check(CLI::IsMember({"standard", "required", "full", "default"}));
820
821 return app;
822 }
823
824 CLI::App* Config::addHelp(CLI::App* app) {
825 app->set_help_flag(
826 "-h{exact},--help{exact}",
827 [app](std::size_t) {
828 helpTriggerApp = app;
829 },
830 "Print help message and exit")
831 ->group(app->get_formatter()->get_label("Nonpersistent Options"))
832 ->check(CLI::IsMember({"standard", "exact", "expanded"}));
833
834 return app;
835 }
836
837 CLI::App* Config::addSimpleHelp(CLI::App* app) {
838 app->set_help_flag(
839 "--help{exact},-h{exact}",
840 [app](std::size_t) {
841 helpTriggerApp = app;
842 },
843 "Print help message and exit")
844 ->group(app->get_formatter()->get_label("Nonpersistent Options"))
845 ->check(CLI::IsMember({"standard", "exact"}));
846
847 return app;
848 }
849
850 void Config::required(CLI::App* instance, bool required) {
851 if (required) {
852 app->needs(instance);
853
854 for (const auto& sub : instance->get_subcommands([](const CLI::App* sc) -> bool {
855 return sc->get_required();
856 })) {
857 instance->needs(sub);
858 }
859 } else {
860 app->remove_needs(instance);
861
862 for (const auto& sub : instance->get_subcommands([](const CLI::App* sc) -> bool {
863 return sc->get_required();
864 })) {
865 instance->remove_needs(sub);
866 }
867 }
868
869 instance->required(required);
870 instance->ignore_case(required);
871 }
872
873 void Config::disabled(CLI::App* instance, bool disabled) {
874 if (disabled) {
875 if (instance->get_ignore_case()) {
876 app->remove_needs(instance);
877 }
878
879 for (const auto& sub : instance->get_subcommands({})) {
880 if (sub->get_ignore_case()) {
881 instance->remove_needs(sub);
882 sub->required(false); // ### must be stored in ConfigInstance
883 }
884 }
885 } else {
886 if (instance->get_ignore_case()) {
887 app->needs(instance);
888 }
889
890 for (const auto& sub : instance->get_subcommands({})) {
891 if (sub->get_ignore_case()) { // ### must be recalled from ConfigInstance
892 instance->needs(sub);
893 sub->required();
894 }
895 }
896 }
897
898 instance->required(disabled ? false : instance->get_ignore_case());
899 }
900
901 bool Config::removeInstance(CLI::App* instance) {
902 Config::required(instance, false);
903
904 return app->remove_subcommand(instance);
905 }
906
907 CLI::Option* Config::addStringOption(const std::string& name, const std::string& description, const std::string& typeName) {
908 applicationOptions[name] = app //
909 ->add_option(name, description)
910 ->type_name(typeName)
911 ->configurable()
912 ->required()
913 ->group("Application Options");
914
915 app->needs(applicationOptions[name]);
916
917 return applicationOptions[name];
918 }
919
920 CLI::Option*
921 Config::addStringOption(const std::string& name, const std::string& description, const std::string& typeName, bool configurable) {
922 addStringOption(name, description, typeName);
923 return applicationOptions[name] //
924 ->configurable(configurable);
925 }
926
927 CLI::Option* Config::addStringOption(const std::string& name,
928 const std::string& description,
929 const std::string& typeName,
930 const std::string& defaultValue) {
931 addStringOption(name, description, typeName);
932
933 applicationOptions[name] //
934 ->required(false)
935 ->default_str(defaultValue);
936
937 app->remove_needs(applicationOptions[name]);
938
939 return applicationOptions[name];
940 }
941
942 CLI::Option* Config::addStringOption(const std::string& name,
943 const std::string& description,
944 const std::string& typeName,
945 const std::string& defaultValue,
946 bool configurable) {
947 addStringOption(name, description, typeName, defaultValue);
948 return applicationOptions[name] //
949 ->configurable(configurable);
950 }
951
952 CLI::Option* Config::addStringOption(const std::string& name,
953 const std::string& description,
954 const std::string& typeName,
955 const char* defaultValue) {
956 return addStringOption(name, description, typeName, std::string(defaultValue));
957 }
958
959 CLI::Option* Config::addStringOption(
960 const std::string& name, const std::string& description, const std::string& typeName, const char* defaultValue, bool configurable) {
961 return addStringOption(name, description, typeName, std::string(defaultValue), configurable);
962 }
963
964 std::string Config::getStringOptionValue(const std::string& name) {
965 if (app->get_option(name) == nullptr) {
966 throw CLI::OptionNotFound(name);
967 }
968
969 return (*app)[name]->as<std::string>();
970 }
971
972 void Config::addFlag(const std::string& name,
973 bool& variable,
974 const std::string& description,
975 bool required,
976 bool configurable,
977 const std::string& groupName) {
978 app->add_flag(name, variable, description) //
979 ->required(required) //
980 ->configurable(configurable) //
981 ->group(groupName);
982 }
983
984 std::string Config::getApplicationName() {
985 return applicationName;
986 }
987
988 int Config::getLogLevel() {
989 return logLevelOpt->as<int>();
990 }
991
992 int Config::getVerboseLevel() {
993 return verboseLevelOpt->as<int>();
994 }
995
996 int Config::argc = 0;
997 char** Config::argv = nullptr;
998
999 std::string Config::applicationName;
1000
1001 std::string Config::configDirectory;
1002 std::string Config::logDirectory;
1003 std::string Config::pidDirectory;
1004
1005 CLI::Option* Config::daemonizeOpt = nullptr;
1006 CLI::Option* Config::logFileOpt = nullptr;
1007 CLI::Option* Config::userNameOpt = nullptr;
1008 CLI::Option* Config::groupNameOpt = nullptr;
1009 CLI::Option* Config::enforceLogFileOpt = nullptr;
1010 CLI::Option* Config::logLevelOpt = nullptr;
1011 CLI::Option* Config::verboseLevelOpt = nullptr;
1012 CLI::Option* Config::quietOpt = nullptr;
1013
1014 CLI::App* Config::helpTriggerApp = nullptr;
1015 CLI::App* Config::showConfigTriggerApp = nullptr;
1016
1017 std::map<std::string, std::string> Config::aliases;
1018 std::map<std::string, CLI::Option*> Config::applicationOptions;
1019
1020} // namespace utils
#define XSTR(s)
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 logToFile(const std::string &logFile)
Definition Logger.cpp:127
static void setVerboseLevel(int level)
Definition Logger.cpp:121
static void setLogLevel(int level)
Definition Logger.cpp:84
static void init()
Definition Logger.cpp:54
static void setQuiet(bool quiet=true)
Definition Logger.cpp:136
pid_t getPid() const
Definition Daemon.cpp:214
static void startDaemon(const std::string &pidFileName, const std::string &userName, const std::string &groupName)
Definition Daemon.cpp:75
static pid_t stopDaemon(const std::string &pidFileName)
Definition Daemon.cpp:158
static void erasePidFile(const std::string &pidFileName)
Definition Daemon.cpp:200
#define STR(a)
Definition clients.h:48
Definition Logger.h:66
Code
Definition Logger.h:68
static std::string createCommandLineTemplate(CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:557
static void createCommandLineTemplate(std::stringstream &out, CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:542
static void createCommandLineOptions(std::stringstream &out, CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:447
static std::shared_ptr< CLI::App > makeApp()
Definition Config.cpp:95
static std::string bash_backslash_escape_no_whitespace(std::string_view s)
Definition Config.cpp:427
static std::string createCommandLineOptions(CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:512
static std::shared_ptr< CLI::HelpFormatter > makeSectionFormatter()
Definition Config.cpp:711
static std::string createCommandLineSubcommands(CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:527