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_function<std::string>(
261 "-w,--write-config",
262 []([[maybe_unused]] const std::string& configFile) {
263 throw CLI::CallForWriteConfig(configFile);
264 },
265 "Write config file and exit")
266 ->configurable(false)
267 ->default_val(configDirectory + "/" + applicationName + ".conf")
268 ->type_name("[configfile]")
269 ->check(!CLI::ExistingDirectory)
270 ->expected(0, 1);
271
272 app->add_flag( //
273 "-k,--kill",
274 "Kill running daemon") //
275 ->configurable(false)
276 ->disable_flag_override();
277
278 app->add_option( //
279 "-i,--instance-alias",
280 "Make an instance also known as an alias in configuration files")
281 ->configurable(false)
282 ->type_name("instance=instance_alias [instance=instance_alias [...]]")
283 ->each([](const std::string& item) {
284 const auto it = item.find('=');
285 if (it != std::string::npos) {
286 aliases[item.substr(0, it)] = item.substr(it + 1);
287 } else {
288 throw CLI::ConversionError("Can not convert '" + item + "' to a 'instance=instance_alias' pair");
289 }
290 });
291
292 addStandardFlags(app.get());
293
294 logLevelOpt = app->add_option( //
295 "-l,--log-level",
296 "Log level") //
297 ->default_val(4)
298 ->type_name("level")
299 ->check(CLI::Range(0, 6))
300 ->group(app->get_formatter()->get_label("Persistent Options"));
301
302 verboseLevelOpt = app->add_option( //
303 "-v,--verbose-level",
304 "Verbose level") //
305 ->default_val(2)
306 ->type_name("level")
307 ->check(CLI::Range(0, 10))
308 ->group(app->get_formatter()->get_label("Persistent Options"));
309
310 quietOpt = app->add_flag( //
311 "-q{true},!-u,--quiet{true}",
312 "Quiet mode") //
313 ->default_val("false")
314 ->type_name("bool")
315 ->check(CLI::IsMember({"true", "false"}))
316 ->group(app->get_formatter()->get_label("Persistent Options"));
317
318 logFileOpt = app->add_option( //
319 "--log-file",
320 "Log file path") //
321 ->default_val(logDirectory + "/" + applicationName + ".log")
322 ->type_name("logfile")
323 ->check(!CLI::ExistingDirectory)
324 ->group(app->get_formatter()->get_label("Persistent Options"));
325
326 enforceLogFileOpt = app->add_flag( //
327 "-e{true},!-n,--enforce-log-file{true}",
328 "Enforce writing of logs to file for foreground applications") //
329 ->default_val("false")
330 ->type_name("bool")
331 ->check(CLI::IsMember({"true", "false"}))
332 ->group(app->get_formatter()->get_label("Persistent Options"));
333
334 daemonizeOpt = app->add_flag( //
335 "-d{true},!-f,--daemonize{true}",
336 "Start application as daemon") //
337 ->default_val("false")
338 ->type_name("bool")
339 ->check(CLI::IsMember({"true", "false"}))
340 ->group(app->get_formatter()->get_label("Persistent Options"));
341
342 userNameOpt = app->add_option( //
343 "--user-name",
344 "Run daemon under specific user permissions") //
345 ->default_val(pw->pw_name)
346 ->type_name("username")
347 ->needs(daemonizeOpt)
348 ->group(app->get_formatter()->get_label("Persistent Options"));
349
350 groupNameOpt = app->add_option( //
351 "--group-name",
352 "Run daemon under specific group permissions")
353 ->default_val(gr->gr_name)
354 ->type_name("groupname")
355 ->needs(daemonizeOpt)
356 ->group(app->get_formatter()->get_label("Persistent Options"));
357
358 proceed = parse1(); // for stopDaemon and pre init application options
359
360 app->set_version_flag("--version", "1.0-rc1", "Framework version");
361 addHelp(app.get());
362 }
363
364 return proceed;
365 }
366
367 bool Config::bootstrap() {
368 aliases.clear();
369
370 app->final_callback([]() {
371 if (daemonizeOpt->as<bool>() && (*app)["--show-config"]->count() == 0 && (*app)["--write-config"]->count() == 0 &&
372 (*app)["--command-line"]->count() == 0) {
373 std::cout << "Running as daemon (double fork)" << std::endl;
374
376 pidDirectory + "/" + applicationName + ".pid", userNameOpt->as<std::string>(), groupNameOpt->as<std::string>());
377
379
380 const std::string logFile = logFileOpt->as<std::string>();
381 if (!logFile.empty()) {
383 }
384 } else if ((*app)["--enforce-log-file"]->as<bool>()) {
385 const std::string logFile = logFileOpt->as<std::string>();
386 if (!logFile.empty()) {
387 std::cout << "Writing logs to file " << logFile << std::endl;
388
390 }
391 }
392 });
393
394 return parse2();
395 }
396
397 bool Config::parse1() {
398 bool proceed = true;
399
400 try {
401 app->parse(argc, argv);
402 } catch (const CLI::ParseError&) {
403 // Do not process ParseError here but on second parse pass
404 }
405
406 if ((*app)["--kill"]->count() > 0) {
407 try {
408 const pid_t daemonPid = utils::Daemon::stopDaemon(pidDirectory + "/" + applicationName + ".pid");
409 std::cout << "Daemon terminated: Pid = " << daemonPid << std::endl;
410 } catch (const DaemonError& e) {
411 std::cout << "DaemonError: " << e.what() << std::endl;
412 } catch (const DaemonFailure& e) {
413 std::cout << "DaemonFailure: " << e.what() << std::endl;
414 }
415
416 proceed = false;
417 } else {
418 if ((*app)["--show-config"]->count() == 0 && (*app)["--write-config"]->count() == 0 && (*app)["--command-line"]->count() == 0) {
419 app->allow_extras(false);
420 }
421
422 if (!quietOpt->as<bool>()) {
423 logger::Logger::setLogLevel(logLevelOpt->as<int>());
424 logger::Logger::setVerboseLevel(verboseLevelOpt->as<int>());
425 }
426
427 logger::Logger::setQuiet(quietOpt->as<bool>());
428 }
429
430 return proceed;
431 }
432
433 // Escape characters with special meaning in Bash (except whitespace).
434 static std::string bash_backslash_escape_no_whitespace(std::string_view s) {
435 static const std::unordered_set<char> special{
436 '\\', '\'', // quoting/escape
437 '`', '$', // substitution
438 '|', '&', ';', '<', '>', '(', ')', '{', '}', // operators
439 '*', '?', '[', ']', '~', '!', '#', '=' // globbing/history/others
440 };
441
442 std::string out;
443 out.reserve(s.size() * 2);
444
445 for (char c : s) {
446 if (special.contains(c)) {
447 out.push_back('\\');
448 }
449 out.push_back(c);
450 }
451 return out;
452 }
453
454 static void createCommandLineOptions(std::stringstream& out, CLI::App* app, CLI::CallForCommandline::Mode mode) {
455 CLI::Option* disabledOpt = app->get_option_no_throw("--disabled");
456 const bool disabled = disabledOpt != nullptr ? disabledOpt->as<bool>() : false;
457 if (!disabled || mode == CLI::CallForCommandline::Mode::DEFAULT) {
458 for (const CLI::Option* option : app->get_options()) {
459 if (option->get_configurable()) {
460 std::string value;
461
462 switch (mode) {
464 if (option->count() > 0) {
465 value = option->as<std::string>();
466 } else if (option->get_required()) {
467 value = "<REQUIRED>";
468 }
469 break;
471 if (option->get_required()) {
472 if (option->count() > 0) {
473 value = option->as<std::string>();
474 } else {
475 value = "<REQUIRED>";
476 }
477 }
478 break;
480 if (option->count() > 0) {
481 value = option->as<std::string>();
482 } else if (!option->get_default_str().empty()) {
483 value = option->get_default_str();
484 } else if (!option->get_required()) {
485 value = "\"\"";
486 } else {
487 value = "<REQUIRED>";
488 }
489 break;
491 if (!option->get_default_str().empty()) {
492 value = option->get_default_str();
493 } else if (!option->get_required()) {
494 value = "\"\"";
495 } else {
496 value = "<REQUIRED>";
497 }
498 break;
499 }
500 }
501
502 if (!value.empty()) {
503 if (value.starts_with("[") && value.ends_with("]")) {
504 value = value.substr(1, value.size() - 2);
505 }
506
507 if (value != "<REQUIRED>" && value != "\"\"") {
509 }
510 out << "--" << option->get_single_name() << ((option->get_items_expected_max() == 0) ? "=" : " ") << value << " ";
511 }
512 }
513 }
514 } else if (disabledOpt->get_default_str() == "false") {
515 out << "--disabled=true ";
516 }
517 }
518
519 static std::string createCommandLineOptions(CLI::App* app, CLI::CallForCommandline::Mode mode) {
520 std::stringstream out;
521
523
524 std::string optionString = out.str();
525 if (optionString.back() == ' ') {
526 optionString.pop_back();
527 }
528
529 return optionString;
530 }
531
532 static void createCommandLineTemplate(std::stringstream& out, CLI::App* app, CLI::CallForCommandline::Mode mode);
533
534 static std::string createCommandLineSubcommands(CLI::App* app, CLI::CallForCommandline::Mode mode) {
535 std::stringstream out;
536
537 CLI::Option* disabledOpt = app->get_option_no_throw("--disabled");
538 if (disabledOpt == nullptr || !disabledOpt->as<bool>() || mode == CLI::CallForCommandline::Mode::DEFAULT) {
539 for (CLI::App* subcommand : app->get_subcommands({})) {
540 if (!subcommand->get_name().empty()) {
541 createCommandLineTemplate(out, subcommand, mode);
542 }
543 }
544 }
545
546 return out.str();
547 }
548
549 static void createCommandLineTemplate(std::stringstream& out, CLI::App* app, CLI::CallForCommandline::Mode mode) {
550 std::string outString;
551
552 outString = createCommandLineOptions(app, mode);
553 if (!outString.empty()) {
554 outString += " ";
555 }
556
557 outString += createCommandLineSubcommands(app, mode);
558
559 if (!outString.empty()) {
560 out << app->get_name() << " " << outString;
561 }
562 }
563
564 static std::string createCommandLineTemplate(CLI::App* app, CLI::CallForCommandline::Mode mode) {
565 std::stringstream out;
566
568
569 std::string outString = out.str();
570 while (app->get_parent() != nullptr) {
571 app = app->get_parent();
572 std::string parentOptions = createCommandLineOptions(app, mode);
573 outString =
574 std::string(app->get_name()).append(" ").append(!parentOptions.empty() ? parentOptions.append(" ") : "").append(outString);
575 }
576
577 if (outString.empty()) {
578 outString = Config::getApplicationName();
579 }
580
581 return outString;
582 }
583
584 bool Config::parse2() {
585 bool success = false;
586
587 try {
588 try {
589 app->parse(argc, argv);
590 success = true;
591 } catch (const DaemonError& e) {
592 std::cout << "Daemon error: " << e.what() << " ... exiting" << std::endl;
593 } catch (const DaemonFailure& e) {
594 std::cout << "Daemon failure: " << e.what() << " ... exiting" << std::endl;
595 } catch (const DaemonSignaled& e) {
596 std::cout << "Pid: " << getpid() << ", child pid: " << e.getPid() << ": " << e.what() << std::endl;
597 } catch (const CLI::CallForHelp&) {
598 const std::string helpMode = helpApp->get_option("--help")->as<std::string>();
599 const CLI::App* helpApp = nullptr;
600 CLI::AppFormatMode mode = CLI::AppFormatMode::Normal;
601 if (helpMode == "exact") {
602 helpApp = utils::Config::helpApp;
603 } else if (helpMode == "expanded") {
604 helpApp = utils::Config::helpApp;
605 mode = CLI::AppFormatMode::All;
606 }
607 std::cout << app->help(helpApp, "", mode) << std::endl;
608 } catch (const CLI::CallForVersion&) {
609 std::cout << app->version() << std::endl << std::endl;
610 } catch (const CLI::CallForCommandline& e) {
611 std::cout << e.what() << std::endl;
612 std::cout << std::endl
613 << Color::Code::FG_GREEN << "command@line" << Color::Code::FG_DEFAULT << ":" << Color::Code::FG_BLUE << "~/> "
615 << std::endl;
616 } catch (const CLI::CallForShowConfig& e) {
617 try {
618 std::cout << e.getApp()->config_to_str(true, true);
619 } catch (const CLI::ParseError& e1) {
620 std::cout << "Error showing config file: " << e.getApp() << " " << e1.get_name() << " " << e1.what() << std::endl;
621 throw;
622 }
623 } catch (const CLI::CallForWriteConfig& e) {
624 std::cout << e.what() << std::endl;
625 std::ofstream confFile(e.getConfigFile());
626 if (confFile.is_open()) {
627 try {
628 confFile << app->config_to_str(true, true);
629 confFile.close();
630 } catch (const CLI::ParseError& e1) {
631 confFile.close();
632 std::cout << "Error writing config file: " << e1.get_name() << " " << e1.what() << std::endl;
633 throw;
634 }
635 } else {
636 std::cout << "Error writing config file: " << std::strerror(errno) << std::endl;
637 }
638 } catch (const CLI::ConversionError& e) {
639 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << e.what() << std::endl;
640 throw;
641 } catch (const CLI::ArgumentMismatch& e) {
642 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << e.what() << std::endl;
643 throw;
644 } catch (const CLI::ConfigError& e) {
645 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << e.what() << std::endl;
646 std::cout << " Adding '-w' on the command line may solve this problem" << std::endl;
647 throw;
648 } catch (const CLI::ParseError& e) {
649 const std::string what = e.what();
650 if (what.find("[Option Group: ") != std::string::npos) { // If CLI11 throws that error it means for us there are
651 // unconfigured anonymous instances
652 std::cout << Color::Code::FG_RED << "[BootstrapError]" << Color::Code::FG_DEFAULT
653 << " Anonymous instance(s) not configured in source code " << std::endl;
654 } else {
655 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << what << std::endl;
656 }
657 throw;
658 }
659 } catch ([[maybe_unused]] const CLI::ParseError& e) {
660 std::cout << std::endl << "Append -h or --help to your command line for more information." << std::endl;
661 } catch (const CLI::Error& e) {
662 std::cout << "[" << Color::Code::FG_RED << e.get_name() << Color::Code::FG_DEFAULT << "] " << e.what() << std::endl;
663 std::cout << "Append -h or --help to your command line for more information." << std::endl;
664 }
665
666 return success;
667 }
668
669 void Config::terminate() {
670 if ((*app)["--daemonize"]->as<bool>()) {
671 std::ifstream pidFile(pidDirectory + "/" + applicationName + ".pid", std::ifstream::in);
672
673 if (pidFile.good()) {
674 pid_t pid = 0;
675 pidFile >> pid;
676
677 if (getpid() == pid) {
678 Daemon::erasePidFile(pidDirectory + "/" + applicationName + ".pid");
679 }
680 }
681 } else if (fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK) >= 0) {
682 char buf[1024];
683 while (read(STDIN_FILENO, buf, 1024) > 0) {
684 }
685 }
686 }
687
688 static std::shared_ptr<CLI::HelpFormatter> makeSectionFormatter() {
689 const std::shared_ptr<CLI::HelpFormatter> sectionFormatter = std::make_shared<CLI::HelpFormatter>();
690
691 sectionFormatter->label("SUBCOMMAND", "SECTION");
692 sectionFormatter->label("SUBCOMMANDS", "SECTIONS");
693 sectionFormatter->label("PERSISTENT", "");
694 sectionFormatter->label("Persistent Options", "Options (persistent)");
695 sectionFormatter->label("Nonpersistent Options", "Options (nonpersistent)");
696 sectionFormatter->label("Usage", "\nUsage");
697 sectionFormatter->label("bool:{true,false}", "{true,false}");
698 sectionFormatter->label(":{standard,required,full,default}", "{standard,required,full,default}");
699 sectionFormatter->label(":{standard,exact,expanded}", "{standard,exact,expanded}");
700 sectionFormatter->column_width(7);
701
702 return sectionFormatter;
703 }
704
705 std::shared_ptr<CLI::Formatter> Config::sectionFormatter = makeSectionFormatter();
706
707 CLI::App* Config::addInstance(const std::string& name, const std::string& description, const std::string& group) {
708 CLI::App* instanceSc = app->add_subcommand(name, description) //
709 ->group(group)
710 ->fallthrough()
711 ->formatter(sectionFormatter)
712 ->configurable(false)
713 ->allow_extras(false)
714 ->disabled(name.empty());
715
716 instanceSc //
717 ->option_defaults()
718 ->configurable(!instanceSc->get_disabled());
719
720 if (!instanceSc->get_disabled()) {
721 if (aliases.contains(name)) {
722 instanceSc //
723 ->alias(aliases[name]);
724 }
725 }
726
727 utils::Config::addStandardFlags(instanceSc);
728 utils::Config::addHelp(instanceSc);
729
730 return instanceSc;
731 }
732
733 CLI::App* Config::getInstance(const std::string& name) {
734 return app->get_subcommand(name);
735 }
736
737 CLI::App* Config::addStandardFlags(CLI::App* app) {
738 app //
739 ->add_flag_callback(
740 "-s,--show-config",
741 [app]() {
742 throw CLI::CallForShowConfig(app);
743 },
744 "Show current configuration and exit") //
745 ->configurable(false)
746 ->disable_flag_override();
747
748 app //
749 ->add_flag(
750 "--command-line{standard}",
751 [app]([[maybe_unused]] std::int64_t count) {
752 const std::string& result = app->get_option("--command-line")->as<std::string>();
753 if (result == "standard") {
755 app,
756 "Below is a command line viewing all non-default and required options:\n"
757 "* Options show their configured value\n"
758 "* Required but not yet configured options show <REQUIRED> as value\n"
759 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
761 }
762 if (result == "required") {
764 app,
765 "Below is a command line viewing required options only:\n"
766 "* Options show either their configured or default value\n"
767 "* Required but not yet configured options show <REQUIRED> as value\n"
768 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
770 }
771 if (result == "full") {
773 app,
774 "Below is a command line viewing the full set of options with their default or configured values:\n"
775 "* Options show either their configured or default value\n"
776 "* Required but not yet configured options show <REQUIRED> as value\n"
777 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
779 }
780 if (result == "default") {
782 app,
783 "Below is a command line viewing the full set of options with their default values\n"
784 "* Options show their default value\n"
785 "* Required but not yet configured options show <REQUIRED> as value\n"
786 "* Options marked as <REQUIRED> need to be configured for a successful bootstrap",
788 }
789 },
790 "Print a command line\n"
791 " standard (default): View all non-default and required options\n"
792 " required: View required options only\n"
793 " full: View the full set of options with their default or configured values\n"
794 " default: View the full set of options with their default values")
795 ->configurable(false)
796 ->check(CLI::IsMember({"standard", "required", "full", "default"}));
797
798 return app;
799 }
800
801 CLI::App* Config::addHelp(CLI::App* app) {
802 app->set_help_flag(
803 "-h{standard},--help{standard}",
804 [app](std::size_t) {
805 helpApp = app;
806 },
807 "Print help message and exit")
808 ->group(app->get_formatter()->get_label("Nonpersistent Options"))
809 ->check(CLI::IsMember({"standard", "exact", "expanded"}));
810
811 return app;
812 }
813
814 CLI::App* Config::addSimpleHelp(CLI::App* app) {
815 app->set_help_flag(
816 "--help,-h",
817 [app](std::size_t) {
818 helpApp = app;
819 },
820 "Print help message and exit")
821 ->group(app->get_formatter()->get_label("Nonpersistent Options"))
822 ->disable_flag_override();
823
824 return app;
825 }
826
827 void Config::required(CLI::App* instance, bool required) {
828 if (required) {
829 app->needs(instance);
830
831 for (const auto& sub : instance->get_subcommands([](const CLI::App* sc) -> bool {
832 return sc->get_required();
833 })) {
834 instance->needs(sub);
835 }
836 } else {
837 app->remove_needs(instance);
838
839 for (const auto& sub : instance->get_subcommands([](const CLI::App* sc) -> bool {
840 return sc->get_required();
841 })) {
842 instance->remove_needs(sub);
843 }
844 }
845
846 instance->required(required);
847 instance->ignore_case(required);
848 }
849
850 void Config::disabled(CLI::App* instance, bool disabled) {
851 if (disabled) {
852 app->remove_needs(instance);
853
854 for (const auto& sub : instance->get_subcommands({})) {
855 // sub->disabled();
856
857 if (sub->get_ignore_case()) {
858 instance->remove_needs(sub);
859 sub->required(false); // ### must be stored in ConfigInstance
860 }
861 }
862 } else {
863 app->needs(instance);
864
865 for (const auto& sub : instance->get_subcommands({})) {
866 // sub->disabled(false);
867
868 if (sub->get_ignore_case()) { // ### must be recalled from ConfigInstance
869 instance->needs(sub);
870 sub->required();
871 }
872 }
873 }
874
875 instance->required(disabled ? false : instance->get_ignore_case());
876 }
877
878 bool Config::removeInstance(CLI::App* instance) {
879 Config::required(instance, false);
880
881 return app->remove_subcommand(instance);
882 }
883
884 CLI::Option* Config::addStringOption(const std::string& name, const std::string& description, const std::string& typeName) {
885 applicationOptions[name] = app //
886 ->add_option(name, description)
887 ->type_name(typeName)
888 ->configurable()
889 ->required()
890 ->group("Application Options");
891
892 app->needs(applicationOptions[name]);
893
894 return applicationOptions[name];
895 }
896
897 CLI::Option*
898 Config::addStringOption(const std::string& name, const std::string& description, const std::string& typeName, bool configurable) {
899 addStringOption(name, description, typeName);
900 return applicationOptions[name] //
901 ->configurable(configurable);
902 }
903
904 CLI::Option* Config::addStringOption(const std::string& name,
905 const std::string& description,
906 const std::string& typeName,
907 const std::string& defaultValue) {
908 addStringOption(name, description, typeName);
909
910 applicationOptions[name] //
911 ->required(false)
912 ->default_str(defaultValue);
913
914 app->remove_needs(applicationOptions[name]);
915
916 return applicationOptions[name];
917 }
918
919 CLI::Option* Config::addStringOption(const std::string& name,
920 const std::string& description,
921 const std::string& typeName,
922 const std::string& defaultValue,
923 bool configurable) {
924 addStringOption(name, description, typeName, defaultValue);
925 return applicationOptions[name] //
926 ->configurable(configurable);
927 }
928
929 CLI::Option* Config::addStringOption(const std::string& name,
930 const std::string& description,
931 const std::string& typeName,
932 const char* defaultValue) {
933 return addStringOption(name, description, typeName, std::string(defaultValue));
934 }
935
936 CLI::Option* Config::addStringOption(
937 const std::string& name, const std::string& description, const std::string& typeName, const char* defaultValue, bool configurable) {
938 return addStringOption(name, description, typeName, std::string(defaultValue), configurable);
939 }
940
941 std::string Config::getStringOptionValue(const std::string& name) {
942 if (app->get_option(name) == nullptr) {
943 throw CLI::OptionNotFound(name);
944 }
945
946 return (*app)[name]->as<std::string>();
947 }
948
949 void Config::addFlag(const std::string& name,
950 bool& variable,
951 const std::string& description,
952 bool required,
953 bool configurable,
954 const std::string& groupName) {
955 app->add_flag(name, variable, description) //
956 ->required(required) //
957 ->configurable(configurable) //
958 ->group(groupName);
959 }
960
961 std::string Config::getApplicationName() {
962 return applicationName;
963 }
964
965 int Config::getLogLevel() {
966 return logLevelOpt->as<int>();
967 }
968
969 int Config::getVerboseLevel() {
970 return verboseLevelOpt->as<int>();
971 }
972
973 int Config::argc = 0;
974 char** Config::argv = nullptr;
975
976 std::string Config::applicationName;
977
978 std::string Config::configDirectory;
979 std::string Config::logDirectory;
980 std::string Config::pidDirectory;
981
982 CLI::Option* Config::daemonizeOpt = nullptr;
983 CLI::Option* Config::logFileOpt = nullptr;
984 CLI::Option* Config::userNameOpt = nullptr;
985 CLI::Option* Config::groupNameOpt = nullptr;
986 CLI::Option* Config::enforceLogFileOpt = nullptr;
987 CLI::Option* Config::logLevelOpt = nullptr;
988 CLI::Option* Config::verboseLevelOpt = nullptr;
989 CLI::Option* Config::quietOpt = nullptr;
990
991 CLI::App* Config::helpApp = nullptr;
992
993 std::map<std::string, std::string> Config::aliases;
994 std::map<std::string, CLI::Option*> Config::applicationOptions;
995
996} // 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:64
Code
Definition Logger.h:66
static std::string createCommandLineTemplate(CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:564
static void createCommandLineTemplate(std::stringstream &out, CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:549
static void createCommandLineOptions(std::stringstream &out, CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:454
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:434
static std::string createCommandLineOptions(CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:519
static std::shared_ptr< CLI::HelpFormatter > makeSectionFormatter()
Definition Config.cpp:688
static std::string createCommandLineSubcommands(CLI::App *app, CLI::CallForCommandline::Mode mode)
Definition Config.cpp:534