Mads
Multi-Agent Distributed System
Loading...
Searching...
No Matches
agent_app.hpp
Go to the documentation of this file.
1/*******************************************************************************
2 _ _ _
3 / \ __ _ ___ _ __ | |_ / \ _ __ _ __
4 / _ \ / _` |/ _ \ '_ \| __| / _ \ | '_ \| '_ \
5 / ___ \ (_| | __/ | | | |_ / ___ \| |_) | |_) |
6 /_/ \_\__, |\___|_| |_|\__/_/ \_\ .__/| .__/
7 |___/ |_| |_|
8
9Executable-oriented helpers for MADS agents.
10Author(s): Paolo Bosetti, 2026
11*******************************************************************************/
12
13#ifndef AGENT_APP_HPP
14#define AGENT_APP_HPP
15
16#include "agent.hpp"
17#include "exec_path.hpp"
18#include "mads.hpp"
19#include "service_discovery.hpp"
20#include <cxxopts.hpp>
21#include <cstdlib>
22#include <filesystem>
23#include <iostream>
24#include <nlohmann/json.hpp>
25#include <optional>
26#include <string>
27#include <type_traits>
28#include <utility>
29
30#if defined(_WIN32)
31#include <process.h>
32#else
33#include <unistd.h>
34#endif
35
36namespace Mads {
37
97template<typename AgentT>
98class AgentAppT : public AgentT {
99 static_assert(std::is_base_of_v<Agent, AgentT>,
100 "AgentT must derive from Mads::Agent");
101 static_assert(std::is_constructible_v<AgentT, std::string, std::string>,
102 "AgentT must be constructible from (std::string, std::string)");
103
104public:
105 using AgentT::init;
106 using AgentT::fetch_settings;
107
113 bool enabled = false;
114
116 std::filesystem::path key_dir = Mads::exec_dir("../etc");
117
119 std::string client_key_name = "client";
120
122 std::string server_key_name = "broker";
123
126 };
127
131 struct CliOptions {
133 std::string settings_uri = SETTINGS_URI;
134
136 std::optional<std::string> agent_name;
137
139 std::optional<std::string> agent_id;
140
143
146 };
147
155 AgentAppT(std::string name, std::string settings_uri)
156 : AgentT(name, std::move(settings_uri)), _options(std::move(name)) {}
157
163 cxxopts::OptionAdder options() { return _options.add_options(); }
164
170 cxxopts::Options &raw_options() { return _options; }
171
177 const cxxopts::Options &raw_options() const { return _options; }
178
183 _options.add_options()
184 // clang-format off
185 ("s,settings", "Settings file path/URI",
186 cxxopts::value<std::string>())
187 ("S,save-settings", "Save settings to ini file",
188 cxxopts::value<std::string>())
189 ("settings-timeout", "Timeout in milliseconds for reading settings from broker ('0' = no timeout)",
190 cxxopts::value<int>()->default_value("0"))
191 ("r,room", "Service discovery room name", cxxopts::value<std::string>()->implicit_value(MADS_SERVICE_ROOM))
192 ("crypto", "Enable CURVE encryption for broker communication")
193 // clang-format on
194 ("keys_dir", "Directory where CURVE keys are stored",
195 cxxopts::value<std::string>()->implicit_value(Mads::exec_dir("../etc")))
196 ("key_broker", "Name of the broker key file (without .key extension)",
197 cxxopts::value<std::string>()->implicit_value("broker"))
198 ("key_client", "Name of the client key file (without .key extension)",
199 cxxopts::value<std::string>()->implicit_value("client"))
200 ("auth_verbose", "Enable verbose authentication messages")
201 ("v,version", "Print version")
202 ("h,help", "Print usage");
203 // clang-format on
204 }
205
210 _options.add_options()
211 // clang-format off
212 ("n,name", "Agent name", cxxopts::value<std::string>())
213 ("i,agent-id", "Agent ID to be added to JSON frames",
214 cxxopts::value<std::string>());
215 // clang-format on
216 }
217
224 void set_agent_name(std::string name) {
225#ifdef _WIN32
226 name = name.substr(name.find_last_of("\\") + 1);
227 name = name.substr(0, name.find("."));
228#else
229 name = name.substr(name.find_last_of("/") + 1);
230#endif
231 const size_t pos = name.rfind('-');
232 if (pos != std::string::npos) {
233 this->_name = name.substr(pos + 1);
234 } else {
235 this->_name = std::move(name);
236 }
237 }
238
243 _options.add_options()
244 ("b,dont-block", "don't block on read");
245 }
246
251 _options.add_options()
252 ("q,queue-size", "ZMQ socket queue size (default 1000)",
253 cxxopts::value<int>());
254 }
255
266 cxxopts::ParseResult &parse_options(int argc, char *argv[]) {
267 try {
268 _parsed_options = _options.parse(argc, argv);
269 } catch (const cxxopts::exceptions::exception &e) {
270 print_parse_error_and_exit(argc, argv, e.what());
271 }
272 const auto unmatched = _parsed_options.unmatched();
273 if (!unmatched.empty()) {
274 std::string message =
275 unmatched.size() == 1 ? "Unexpected CLI argument: "
276 : "Unexpected CLI arguments: ";
277 bool first = true;
278 for (const auto &argument : unmatched) {
279 if (!first) {
280 message += " ";
281 }
282 message += argument;
283 first = false;
284 }
285 print_parse_error_and_exit(argc, argv, message);
286 }
287 return _parsed_options;
288 }
289
307 template<typename SaveAgentT>
309 const cxxopts::ParseResult &parsed, const cxxopts::Options &parser,
310 char *argv[], std::string default_settings_uri = SETTINGS_URI,
311 std::ostream &out = std::cout, std::ostream &err = std::cerr) {
312 static_assert(std::is_base_of_v<Agent, SaveAgentT>,
313 "SaveAgentT must derive from Mads::Agent");
314 static_assert(
315 std::is_constructible_v<SaveAgentT, std::string, std::string>,
316 "SaveAgentT must be constructible from (std::string, std::string)");
317
318 if (parsed.count("help")) {
319 out << argv[0] << " ver. " << LIB_VERSION << std::endl;
320 out << parser.help() << std::endl;
321 return EXIT_SUCCESS;
322 }
323
324 if (parsed.count("version")) {
325 out << LIB_VERSION << std::endl;
326 return EXIT_SUCCESS;
327 }
328
329 if (parsed.count("save-settings")) {
330 auto cli_options =
331 cli_options_from_parse_result(parsed, std::move(default_settings_uri));
332 const auto output_path = parsed["save-settings"].as<std::string>();
333 SaveAgentT obj(argv[0], cli_options.settings_uri);
334 if (cli_options.crypto.enabled) {
335 obj.set_key_dir(cli_options.crypto.key_dir);
336 obj.client_key_name = cli_options.crypto.client_key_name;
337 obj.server_key_name = cli_options.crypto.server_key_name;
338 obj.auth_verbose = cli_options.crypto.auth_verbose;
339 }
340 try {
341 if (cli_options.settings_timeout > 0) {
342 obj.set_settings_timeout(cli_options.settings_timeout);
343 }
344 obj.init(cli_options.crypto.enabled);
345 obj.save_settings(output_path);
346 } catch (const AgentError &e) {
347#ifndef MADS_AGENT_NO_INFO
348 err << fg::red;
349#endif
350 err << "Error saving local settings: " << e.what();
351#ifndef MADS_AGENT_NO_INFO
352 err << fg::reset;
353#endif
354 err << std::endl;
355 return EXIT_FAILURE;
356 } catch (const std::exception &e) {
357#ifndef MADS_AGENT_NO_INFO
358 err << fg::red;
359#endif
360 err << "Error saving settings: " << e.what();
361#ifndef MADS_AGENT_NO_INFO
362 err << fg::reset;
363#endif
364 err << std::endl;
365 return EXIT_FAILURE;
366 }
367#ifndef MADS_AGENT_NO_INFO
368 out << fg::magenta;
369#endif
370 out << "Settings saved to " << output_path;
371#ifndef MADS_AGENT_NO_INFO
372 out << fg::reset;
373#endif
374 out << std::endl;
375 return EXIT_SUCCESS;
376 }
377
378 return -1;
379 }
380
389 void init(const cxxopts::ParseResult &parsed,
390 std::string default_settings_uri = SETTINGS_URI,
391 bool install_watchdog = true) {
392 const auto &cli_options =
393 resolve_cli_options(parsed, std::move(default_settings_uri));
394 apply_cli_options(cli_options);
395 AgentT::init(cli_options.crypto.enabled, install_watchdog);
396 _settings = this->get_settings();
397 }
398
413 void fetch_settings(const cxxopts::ParseResult &parsed,
414 std::string default_settings_uri = SETTINGS_URI) {
415 const auto &cli_options =
416 resolve_cli_options(parsed, std::move(default_settings_uri));
417 apply_cli_options(cli_options);
418 AgentT::fetch_settings(cli_options.crypto.enabled);
419 _settings = this->get_settings();
420 }
421
425 void enable_events(bool enabled = true) { _events_enabled = enabled; }
426
430 void connect(std::chrono::milliseconds delay = std::chrono::milliseconds(250)) {
431 AgentT::connect(delay);
432 if (_events_enabled) {
433 this->register_event(event_type::startup);
434 }
435 }
436
440 void disconnect() {
441 if (_events_enabled && this->is_connected()) {
442 this->register_event(event_type::shutdown);
443 }
444 AgentT::disconnect();
445 }
446
450 const nlohmann::json &settings_json() const { return _settings; }
451
456 if (!_settings.contains("receive_timeout") ||
457 _settings.at("receive_timeout").is_null()) {
458 return;
459 }
460 const auto &receive_timeout = _settings.at("receive_timeout");
461 if (receive_timeout.is_number_integer()) {
462 this->set_receive_timeout(receive_timeout.template get<int>());
463 }
464 }
465
470 if (_parsed_options.count("queue-size") != 0) {
471 const int queue_size = _parsed_options["queue-size"].template as<int>();
472 this->set_high_watermark(queue_size);
473 return;
474 }
475 if (!_settings.contains("queue_size") ||
476 _settings.at("queue_size").is_null()) {
477 return;
478 }
479 this->set_high_watermark(_settings.value("queue_size", 1000));
480 }
481
489 bool restart_if_requested(char *argv[], std::ostream &out = std::cout) {
490 if (!this->restart()) {
491 return false;
492 }
493 auto cmd = std::string(MADS_PREFIX) + argv[0];
494 out << "Restarting " << cmd << "..." << std::endl;
495#if defined(_WIN32)
496 _execvp(cmd.c_str(), argv);
497#else
498 execvp(cmd.c_str(), argv);
499#endif
500 return true;
501 }
502
503private:
504 template<typename T>
505 static std::optional<T> option_value(const cxxopts::ParseResult &parsed,
506 const std::string &name) {
507 if (parsed.count(name) == 0) {
508 return std::nullopt;
509 }
510 return parsed[name].as<T>();
511 }
512
513 static void print_status(std::ostream &out, const std::string &message) {
514#ifndef MADS_AGENT_NO_INFO
515 out << fg::yellow;
516#endif
517 out << message;
518#ifndef MADS_AGENT_NO_INFO
519 out << fg::reset;
520#endif
521 out << std::endl;
522 }
523
524 [[noreturn]] void print_parse_error_and_exit(
525 int argc, char *argv[], const std::string &message) const {
526#ifndef MADS_AGENT_NO_INFO
527 std::cerr << fg::red;
528#endif
529 std::cerr << "Error parsing command line: " << message;
530#ifndef MADS_AGENT_NO_INFO
531 std::cerr << fg::reset;
532#endif
533 std::cerr << std::endl << std::endl;
534 std::cerr << _options.help() << std::endl;
535 if (argc > 0 && argv != nullptr && argv[0] != nullptr) {
536 std::cerr << "Run '" << argv[0] << " --help' for usage." << std::endl;
537 }
538 std::exit(EXIT_FAILURE);
539 }
540
541 static CliOptions cli_options_from_parse_result(
542 const cxxopts::ParseResult &parsed,
543 std::string default_settings_uri = SETTINGS_URI) {
544 CliOptions options;
545 options.settings_uri = std::move(default_settings_uri);
546 options.settings_timeout =
547 option_value<int>(parsed, "settings-timeout").value_or(0);
548
549 if (auto settings = option_value<std::string>(parsed, "settings")) {
550 options.settings_uri = *settings;
551 }
552 if (auto name = option_value<std::string>(parsed, "name")) {
553 options.agent_name = *name;
554 }
555 if (auto agent_id = option_value<std::string>(parsed, "agent-id")) {
556 options.agent_id = *agent_id;
557 }
558
559 if (parsed.count("crypto") != 0) {
560 options.crypto.enabled = true;
561 if (auto key_dir = option_value<std::string>(parsed, "keys_dir")) {
562 options.crypto.key_dir = *key_dir;
563 }
564 if (auto server_key_name =
565 option_value<std::string>(parsed, "key_broker")) {
566 options.crypto.server_key_name = *server_key_name;
567 }
568 if (auto client_key_name =
569 option_value<std::string>(parsed, "key_client")) {
570 options.crypto.client_key_name = *client_key_name;
571 }
572 if (parsed.count("auth_verbose") != 0) {
573 options.crypto.auth_verbose = Mads::auth_verbose::on;
574 }
575 }
576
577 if (parsed.count("room") && !parsed["room"].as<std::string>().empty()) {
578 auto room = parsed["room"].as<std::string>();
579 auto discovery_service = ServiceDiscovery(MADS_SERVICE_PORT);
580 cout << fg::gray << style::italic << "Using "
581 << (options.crypto.enabled ? "encrypted" : "unencrypted")
582 << " service discovery in room '"
583 << parsed["room"].as<std::string>() << "'..." << std::endl;
584 const auto service = discovery_service.discover(room, 5000ms);
585 options.settings_uri = "tcp://" + service.ip + ":" +
586 std::to_string(service.ports.at("settings"));
587 cout << "Found broker on " << service.hostname
588 << " providing settings at: " << options.settings_uri
589 << style::reset << fg::reset << std::endl;
590 if (options.crypto.enabled != service.encrypted) {
591 throw std::runtime_error(
592 "CLI encryption setting does not match discovered service encryption");
593 }
594 }
595
596 return options;
597 }
598
599 // Resolves CliOptions once and caches them, so that a fetch_settings() call
600 // followed by an init() call (or two fetch_settings() calls) only pays for
601 // --room service discovery (see cli_options_from_parse_result) a single
602 // time. Second and later calls ignore their arguments and return the cache.
603 const CliOptions &resolve_cli_options(const cxxopts::ParseResult &parsed,
604 std::string default_settings_uri) {
605 if (!_cli_options) {
606 _cli_options =
607 cli_options_from_parse_result(parsed, std::move(default_settings_uri));
608 }
609 return *_cli_options;
610 }
611
612 void apply_cli_options(const CliOptions &cli_options) {
613 configure_from_cli_options(cli_options);
614 if (cli_options.settings_timeout > 0) {
615 print_status(std::cout, "Using settings timeout of " +
616 std::to_string(cli_options.settings_timeout) +
617 " ms");
618 this->set_settings_timeout(cli_options.settings_timeout);
619 }
620 }
621
622 void configure_from_cli_options(const CliOptions &options) {
623 this->_settings_uri = options.settings_uri;
624
625 if (options.agent_name) {
626 const auto &name = *options.agent_name;
627 const size_t pos = name.rfind('-');
628 if (pos != std::string::npos) {
629 this->_name = name.substr(pos + 1);
630 } else {
631 this->_name = name;
632 }
633 }
634
635 if (options.agent_id) {
636 this->set_agent_id(*options.agent_id);
637 }
638
639 if (options.crypto.enabled) {
640 this->set_key_dir(options.crypto.key_dir);
641 this->client_key_name = options.crypto.client_key_name;
642 this->server_key_name = options.crypto.server_key_name;
643 this->auth_verbose = options.crypto.auth_verbose;
644 }
645 }
646
647 bool _events_enabled = false;
648 nlohmann::json _settings;
649 cxxopts::Options _options;
650 cxxopts::ParseResult _parsed_options;
651 std::optional<CliOptions> _cli_options;
652};
653
655
656template<typename AgentT>
658
659} // namespace Mads
660
661#endif // AGENT_APP_HPP
MADS_EXPORT const char * agent_id(agent_t agent)
Returns the current agent identifier.
Application-facing wrapper for Agent and Agent subclasses.
Definition agent_app.hpp:98
cxxopts::ParseResult & parse_options(int argc, char *argv[])
Parse the owned cxxopts parser.
AgentAppT(std::string name, std::string settings_uri)
Construct an AgentAppT with an owned cxxopts parser.
void connect(std::chrono::milliseconds delay=std::chrono::milliseconds(250))
Connect the wrapped agent and optionally register startup.
void fetch_settings(const cxxopts::ParseResult &parsed, std::string default_settings_uri=SETTINGS_URI)
Apply parsed CLI options and fetch settings (and any broker-served attachment) without binding the wr...
void apply_queue_size()
Apply queue size from CLI or cached settings when present.
const cxxopts::Options & raw_options() const
Access the owned cxxopts parser.
void add_dont_block_option()
Add the non-blocking receive option.
void add_queue_size_option()
Add the ZMQ socket queue size option.
static int handle_standard_exit_options(const cxxopts::ParseResult &parsed, const cxxopts::Options &parser, char *argv[], std::string default_settings_uri=SETTINGS_URI, std::ostream &out=std::cout, std::ostream &err=std::cerr)
Handle standard options that terminate an executable early.
void enable_events(bool enabled=true)
Enable automatic startup and shutdown event registration.
void init(const cxxopts::ParseResult &parsed, std::string default_settings_uri=SETTINGS_URI, bool install_watchdog=true)
Apply parsed CLI options and initialize the wrapped agent.
bool restart_if_requested(char *argv[], std::ostream &out=std::cout)
Restart the current executable if requested by remote control.
void disconnect()
Optionally register shutdown and disconnect the wrapped agent.
void apply_receive_timeout()
Apply receive_timeout from cached settings when present.
cxxopts::OptionAdder options()
Access the common option adder for fluent cxxopts declarations.
const nlohmann::json & settings_json() const
Return cached settings loaded during init().
cxxopts::Options & raw_options()
Access the owned cxxopts parser.
void add_common_options()
Add common MADS executable options to the owned parser.
void set_agent_name(std::string name)
Override the wrapped agent name before initialization.
void add_agent_identity_options()
Add optional agent identity options.
Definition agent.hpp:67
std::string exec_dir(std::string relative="")
Definition exec_path.hpp:62
auth_verbose
Definition curve.hpp:35
AgentAppT< Agent > AgentApp
AgentAppT< AgentT > AgentAppFor
Common options parsed for agent executables.
int settings_timeout
Timeout in milliseconds for reading settings from broker.
std::string settings_uri
Settings path or broker URI.
std::optional< std::string > agent_name
Optional agent name override.
CryptoOptions crypto
CURVE encryption configuration.
std::optional< std::string > agent_id
Optional agent identifier added to outgoing JSON frames.
CURVE encryption options parsed from the command line.
std::string server_key_name
Broker/server public key file name without extension.
std::string client_key_name
Client key file name without extension.
bool enabled
Whether CURVE encryption is enabled.
std::filesystem::path key_dir
Directory containing CURVE key files.