From a5f417a8372b030aa2e64d0b20dd5cde39c3d205 Mon Sep 17 00:00:00 2001 From: zeyus Date: Mon, 19 May 2025 09:33:12 +0200 Subject: [PATCH 1/5] Allow setting custom config file path, for platforms that do not support ENV VARS. --- include/lsl/common.h | 9 +++++++++ src/api_config.cpp | 10 ++++++++++ src/api_config.h | 17 +++++++++++++++++ src/common.cpp | 6 ++++++ 4 files changed, 42 insertions(+) diff --git a/include/lsl/common.h b/include/lsl/common.h index 3c81c798f..37ed2f687 100644 --- a/include/lsl/common.h +++ b/include/lsl/common.h @@ -227,3 +227,12 @@ extern LIBLSL_C_API double lsl_local_clock(); * no free() method is available (e.g., in some scripting languages). */ extern LIBLSL_C_API void lsl_destroy_string(char *s); + +/** + * Set the name of the configuration file to be used. + * + * This is a global setting that will be used by all LSL + * after this function is called. If, and only if, this function + * is called before the first call to any other LSL function. + */ +extern LIBLSL_C_API void lsl_set_config_filename(const char *filename); diff --git a/src/api_config.cpp b/src/api_config.cpp index 0d328630e..15c29c240 100644 --- a/src/api_config.cpp +++ b/src/api_config.cpp @@ -55,6 +55,16 @@ api_config::api_config() { // for each config file location under consideration... std::vector filenames; + // NOLINTNEXTLINE(concurrency-mt-unsafe) + if (api_config_filename_ != "") { + // if a config file name was set, use it if it is readable + if (file_is_readable(api_config_filename_)) { + filenames.insert(filenames.begin(), api_config_filename_); + } else { + LOG_F(ERROR, "Config file %s not found", api_config_filename_.c_str()); + } + } + // NOLINTNEXTLINE(concurrency-mt-unsafe) if (auto *cfgpath = getenv("LSLAPICFG")) { std::string envcfg(cfgpath); diff --git a/src/api_config.h b/src/api_config.h index 1c9d3c16b..a66ac2c6d 100644 --- a/src/api_config.h +++ b/src/api_config.h @@ -76,6 +76,21 @@ class api_config { bool allow_ipv6() const { return allow_ipv6_; } bool allow_ipv4() const { return allow_ipv4_; } + /** + * @brief An additional settings path to load configuration from. + */ + const std::string &api_config_filename() const { return api_config_filename_; } + + /** + * @brief Set the config file name used to load the settings. + * + * This MUST be called before the first call to get_instance() to have any effect. + */ + static void set_api_config_filename(const std::string &filename) { + api_config_filename_ = filename; + } + + /** * @brief The range or scope of stream lookup when using multicast-based discovery * @@ -221,6 +236,8 @@ class api_config { */ void load_from_file(const std::string &filename = std::string()); + static std::string api_config_filename_ = ""; + // core parameters bool allow_ipv6_, allow_ipv4_; uint16_t base_port_; diff --git a/src/common.cpp b/src/common.cpp index c8307f208..363b934bd 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -26,6 +26,12 @@ LIBLSL_C_API int32_t lsl_protocol_version() { return lsl::api_config::get_instance()->use_protocol_version(); } +LIBLSL_C_API void lsl_set_config_filename(const char *filename) { + if (filename) { + lsl::api_config::set_api_config_filename(filename); + } +} + LIBLSL_C_API int32_t lsl_library_version() { return LSL_LIBRARY_VERSION; } LIBLSL_C_API double lsl_local_clock() { From 9c204ae13dc268644c57923e0c0181f0b1136da6 Mon Sep 17 00:00:00 2001 From: zeyus Date: Mon, 19 May 2025 09:58:20 +0200 Subject: [PATCH 2/5] Correctly initialize config filename. --- src/api_config.cpp | 2 +- src/api_config.h | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/api_config.cpp b/src/api_config.cpp index 15c29c240..44dc6137b 100644 --- a/src/api_config.cpp +++ b/src/api_config.cpp @@ -56,7 +56,7 @@ api_config::api_config() { std::vector filenames; // NOLINTNEXTLINE(concurrency-mt-unsafe) - if (api_config_filename_ != "") { + if (!api_config_filename_.empty()) { // if a config file name was set, use it if it is readable if (file_is_readable(api_config_filename_)) { filenames.insert(filenames.begin(), api_config_filename_); diff --git a/src/api_config.h b/src/api_config.h index a66ac2c6d..b6d92f2cb 100644 --- a/src/api_config.h +++ b/src/api_config.h @@ -236,7 +236,7 @@ class api_config { */ void load_from_file(const std::string &filename = std::string()); - static std::string api_config_filename_ = ""; + static std::string api_config_filename_; // core parameters bool allow_ipv6_, allow_ipv4_; @@ -275,6 +275,10 @@ class api_config { float smoothing_halftime_; bool force_default_timestamps_; }; + +// initialize configuration file name +inline std::string api_config::api_config_filename_ = ""; + } // namespace lsl #endif From d2be3d6615b9cdd1bb8343412e7931350135d1a0 Mon Sep 17 00:00:00 2001 From: zeyus Date: Mon, 19 May 2025 16:16:46 +0200 Subject: [PATCH 3/5] Added content based config (string). --- include/lsl/common.h | 11 ++++++++ src/api_config.cpp | 63 +++++++++++++++++++++++++++++++++----------- src/api_config.h | 36 ++++++++++++++++++++++--- src/common.cpp | 6 +++++ 4 files changed, 97 insertions(+), 19 deletions(-) diff --git a/include/lsl/common.h b/include/lsl/common.h index 37ed2f687..8eb25e243 100644 --- a/include/lsl/common.h +++ b/include/lsl/common.h @@ -236,3 +236,14 @@ extern LIBLSL_C_API void lsl_destroy_string(char *s); * is called before the first call to any other LSL function. */ extern LIBLSL_C_API void lsl_set_config_filename(const char *filename); + +/** + * Set the content of the configuration file to be used. + * + * This is a global setting that will be used by all LSL + * after this function is called. If, and only if, this function + * is called before the first call to any other LSL function. + * + * @note the configuration content is wiped after LSL has initialized. + */ +extern LIBLSL_C_API void lsl_set_config_content(const char *content); diff --git a/src/api_config.cpp b/src/api_config.cpp index 44dc6137b..88863c07a 100644 --- a/src/api_config.cpp +++ b/src/api_config.cpp @@ -1,7 +1,6 @@ #include "api_config.h" #include "common.h" #include "util/cast.hpp" -#include "util/inireader.hpp" #include "util/strfuns.hpp" #include #include @@ -10,6 +9,7 @@ #include #include #include +#include using namespace lsl; @@ -52,6 +52,23 @@ bool file_is_readable(const std::string &filename) { } api_config::api_config() { + // first check to see if a config content was provided + if (!api_config_content_.empty()) { + try { + // if so, load it from the content + load_from_content(api_config_content_); + // free the content this can only be called once + api_config_content_.clear(); + // config loaded successfully, so return + return; + } catch (std::exception &e) { + LOG_F(ERROR, "Error parsing config content: '%s', rolling back to defaults", e.what()); + // clear the content, it was invalid anyway + api_config_content_.clear(); + } + } + // otherwise, load the config from a file + // for each config file location under consideration... std::vector filenames; @@ -92,7 +109,6 @@ api_config::api_config() { load_from_file(); } - void api_config::load_from_file(const std::string &filename) { try { INI pt; @@ -102,7 +118,35 @@ void api_config::load_from_file(const std::string &filename) { pt.load(infile); } } + api_config::load(pt); + // log config filename only after setting the verbosity level and all config has been read + if (!filename.empty()) + LOG_F(INFO, "Configuration loaded from %s", filename.c_str()); + else + LOG_F(INFO, "Loaded default config"); + + } catch (std::exception &e) { + LOG_F(ERROR, "Error parsing config file '%s': '%s', rolling back to defaults", + filename.c_str(), e.what()); + // any error: assign defaults + load_from_file(); + // and rethrow + throw e; + } +} + +void api_config::load_from_content(const std::string &content) { + // load the content into an INI object + INI pt; + if (!content.empty()) { + std::istringstream content_stream(content); + pt.load(content_stream); + } + api_config::load(pt); + LOG_F(INFO, "Configuration loaded from content"); +} +void api_config::load(INI &pt) { // read the [log] settings #if LOGURU_DEBUG_LOGGING // When built with LSL_DEBUGLOG=ON, default to verbose logging @@ -279,20 +323,7 @@ void api_config::load_from_file(const std::string &filename) { smoothing_halftime_ = pt.get("tuning.SmoothingHalftime", 90.0F); force_default_timestamps_ = pt.get("tuning.ForceDefaultTimestamps", false); - // log config filename only after setting the verbosity level and all config has been read - if (!filename.empty()) - LOG_F(INFO, "Configuration loaded from %s", filename.c_str()); - else - LOG_F(INFO, "Loaded default config"); - - } catch (std::exception &e) { - LOG_F(ERROR, "Error parsing config file '%s': '%s', rolling back to defaults", - filename.c_str(), e.what()); - // any error: assign defaults - load_from_file(); - // and rethrow - throw e; - } + } static std::once_flag api_config_once_flag; diff --git a/src/api_config.h b/src/api_config.h index b6d92f2cb..6cb987cd7 100644 --- a/src/api_config.h +++ b/src/api_config.h @@ -2,6 +2,7 @@ #define API_CONFIG_H #include "netinterfaces.h" +#include "util/inireader.hpp" #include #include #include @@ -14,9 +15,11 @@ namespace lsl { * A configuration object: holds all the configurable settings of liblsl. * These settings can be set via a configuration file that is automatically searched * by stream providers and recipients in a series of locations: - * - lsl_api.cfg - * - ~/lsl_api/lsl_api.cfg - * - /etc/lsl_api/lsl_api.cfg + * - First, the content set via `lsl_set_config_content()` + * - Second, the file set via `lsl_set_config_filename()` + * - Third, the file `lsl_api.cfg` in the current working directory + * - Fourth, the file `lsl_api.cfg` in the home directory (e.g., `~/lsl_api/lsl_api.cfg`) + * - Fifth, the file `lsl_api.cfg` in the system configuration directory (e.g., `/etc/lsl_api/lsl_api.cfg`) * * Note that, while in some cases it might seem sufficient to override configurations * only for a subset of machines involved in a recording session (e.g., the servers), @@ -76,6 +79,18 @@ class api_config { bool allow_ipv6() const { return allow_ipv6_; } bool allow_ipv4() const { return allow_ipv4_; } + + + /** + * @brief Set the configuration directly from a string. + * + * This allows passing in configuration content directly rather than from a file. + * This MUST be called before the first call to get_instance() to have any effect. + */ + static void set_api_config_content(const std::string &content) { + api_config_content_ = content; + } + /** * @brief An additional settings path to load configuration from. */ @@ -236,7 +251,21 @@ class api_config { */ void load_from_file(const std::string &filename = std::string()); + /** + * @brief Load a configuration from a string. + * @param content The configuration content to parse + */ + void load_from_content(const std::string &content); + + /** + * @brief Load the configuration from an INI object. + * @param pt The INI object to load the configuration from + */ + void load(INI &pt); + + // config overrides static std::string api_config_filename_; + static std::string api_config_content_; // core parameters bool allow_ipv6_, allow_ipv4_; @@ -278,6 +307,7 @@ class api_config { // initialize configuration file name inline std::string api_config::api_config_filename_ = ""; +inline std::string api_config::api_config_content_ = ""; } // namespace lsl diff --git a/src/common.cpp b/src/common.cpp index 363b934bd..9914dd372 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -32,6 +32,12 @@ LIBLSL_C_API void lsl_set_config_filename(const char *filename) { } } +LIBLSL_C_API void lsl_set_config_content(const char *content) { + if (content) { + lsl::api_config::set_api_config_content(content); + } +} + LIBLSL_C_API int32_t lsl_library_version() { return LSL_LIBRARY_VERSION; } LIBLSL_C_API double lsl_local_clock() { From b934b07469f58941297c0e3d9ba198659cef119d Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Tue, 21 Apr 2026 16:37:08 -0400 Subject: [PATCH 4/5] Fixup docstrings for lsl_set_config_* functions to be more accurate. --- include/lsl/common.h | 18 +-- src/api_config.cpp | 319 +++++++++++++++++++++---------------------- src/api_config.h | 44 +++--- 3 files changed, 190 insertions(+), 191 deletions(-) diff --git a/include/lsl/common.h b/include/lsl/common.h index 8eb25e243..a1fe2fa49 100644 --- a/include/lsl/common.h +++ b/include/lsl/common.h @@ -229,21 +229,21 @@ extern LIBLSL_C_API double lsl_local_clock(); extern LIBLSL_C_API void lsl_destroy_string(char *s); /** - * Set the name of the configuration file to be used. + * Set the path of the configuration file to be used by liblsl. * - * This is a global setting that will be used by all LSL - * after this function is called. If, and only if, this function - * is called before the first call to any other LSL function. + * This must be called before any other LSL function; otherwise the setting + * has no effect, as the configuration is loaded lazily on first use. + * See also the precedence list in api_config.h. */ extern LIBLSL_C_API void lsl_set_config_filename(const char *filename); /** - * Set the content of the configuration file to be used. + * Set the configuration content (as an INI string) to be used by liblsl. * - * This is a global setting that will be used by all LSL - * after this function is called. If, and only if, this function - * is called before the first call to any other LSL function. + * This must be called before any other LSL function; otherwise the setting + * has no effect, as the configuration is loaded lazily on first use. + * When set, this content takes precedence over any configuration file. * - * @note the configuration content is wiped after LSL has initialized. + * @note The content is discarded after liblsl has initialized. */ extern LIBLSL_C_API void lsl_set_config_content(const char *content); diff --git a/src/api_config.cpp b/src/api_config.cpp index 88863c07a..4aeb43950 100644 --- a/src/api_config.cpp +++ b/src/api_config.cpp @@ -72,7 +72,6 @@ api_config::api_config() { // for each config file location under consideration... std::vector filenames; - // NOLINTNEXTLINE(concurrency-mt-unsafe) if (!api_config_filename_.empty()) { // if a config file name was set, use it if it is readable if (file_is_readable(api_config_filename_)) { @@ -82,6 +81,7 @@ api_config::api_config() { } } + // The LSLAPICFG env var, if set, takes precedence over the API-set filename. // NOLINTNEXTLINE(concurrency-mt-unsafe) if (auto *cfgpath = getenv("LSLAPICFG")) { std::string envcfg(cfgpath); @@ -147,183 +147,180 @@ void api_config::load_from_content(const std::string &content) { } void api_config::load(INI &pt) { - // read the [log] settings + // read the [log] settings #if LOGURU_DEBUG_LOGGING - // When built with LSL_DEBUGLOG=ON, default to verbose logging - int log_level = pt.get("log.level", 1); + // When built with LSL_DEBUGLOG=ON, default to verbose logging + int log_level = pt.get("log.level", 1); #else - int log_level = pt.get("log.level", static_cast(loguru::Verbosity_INFO)); + int log_level = pt.get("log.level", static_cast(loguru::Verbosity_INFO)); #endif - if (log_level < -3 || log_level > 9) - throw std::runtime_error("Invalid log.level (valid range: -3 to 9"); + if (log_level < -3 || log_level > 9) + throw std::runtime_error("Invalid log.level (valid range: -3 to 9"); - std::string log_file = pt.get("log.file", ""); - if (!log_file.empty()) { - loguru::add_file(log_file.c_str(), loguru::Append, log_level); - // don't duplicate log to stderr - loguru::g_stderr_verbosity = -9; - } else - loguru::g_stderr_verbosity = log_level; + std::string log_file = pt.get("log.file", ""); + if (!log_file.empty()) { + loguru::add_file(log_file.c_str(), loguru::Append, log_level); + // don't duplicate log to stderr + loguru::g_stderr_verbosity = -9; + } else + loguru::g_stderr_verbosity = log_level; - // read out the [ports] parameters - multicast_port_ = pt.get("ports.MulticastPort", 16571); - base_port_ = pt.get("ports.BasePort", 16572); - port_range_ = pt.get("ports.PortRange", 32); - allow_random_ports_ = pt.get("ports.AllowRandomPorts", true); - std::string ipv6_str = pt.get("ports.IPv6", "allow"); + // read out the [ports] parameters + multicast_port_ = pt.get("ports.MulticastPort", 16571); + base_port_ = pt.get("ports.BasePort", 16572); + port_range_ = pt.get("ports.PortRange", 32); + allow_random_ports_ = pt.get("ports.AllowRandomPorts", true); + std::string ipv6_str = pt.get("ports.IPv6", "allow"); - allow_ipv4_ = true; + allow_ipv4_ = true; + allow_ipv6_ = true; + // fix some common mis-spellings + if (ipv6_str == "disabled" || ipv6_str == "disable") + allow_ipv6_ = false; + else if (ipv6_str == "allowed" || ipv6_str == "allow") allow_ipv6_ = true; - // fix some common mis-spellings - if (ipv6_str == "disabled" || ipv6_str == "disable") - allow_ipv6_ = false; - else if (ipv6_str == "allowed" || ipv6_str == "allow") - allow_ipv6_ = true; - else if (ipv6_str == "forced" || ipv6_str == "force") - allow_ipv4_ = false; - else - throw std::runtime_error("Unsupported setting for the IPv6 parameter."); - - // read the [multicast] parameters - resolve_scope_ = pt.get("multicast.ResolveScope", "site"); - listen_address_ = pt.get("multicast.ListenAddress", ""); - // Note about multicast addresses: IPv6 multicast addresses should be - // FF0x::1 (see RFC2373, RFC1884) or a predefined multicast group - std::string ipv6_multicast_group = - pt.get("multicast.IPv6MulticastGroup", "113D:6FDD:2C17:A643:FFE2:1BD1:3CD2"); - std::vector machine_group = - parse_set(pt.get("multicast.MachineAddresses", "{127.0.0.1}")); - // 224.0.0.1 is the group for all directly connected hosts (RFC1112) - std::vector link_group = parse_set( - pt.get("multicast.LinkAddresses", "{255.255.255.255, 224.0.0.1, 224.0.0.183}")); - // Multicast groups defined by the organization (and therefore subject - // to filtering / forwarding are in the 239.192.0.0/14 subnet (RFC2365) - std::vector site_group = - parse_set(pt.get("multicast.SiteAddresses", "{239.255.172.215}")); - // Organization groups use the same broadcast addresses (IPv4), but - // have a larger TTL. On the network site, it requires the routers - // to forward the broadcast packets (both IGMP and UDP) - std::vector organization_group = - parse_set(pt.get("multicast.OrganizationAddresses", "{}")); - std::vector global_group = - parse_set(pt.get("multicast.GlobalAddresses", "{}")); - enum { machine = 0, link, site, organization, global } scope; - // construct list of addresses & TTL according to the ResolveScope. - if (resolve_scope_ == "machine") - scope = machine; - else if (resolve_scope_ == "link") - scope = link; - else if (resolve_scope_ == "site") - scope = site; - else if (resolve_scope_ == "organization") - scope = organization; - else if (resolve_scope_ == "global") - scope = global; - else - throw std::runtime_error("This ResolveScope setting is unsupported."); - - std::vector mcasttmp; + else if (ipv6_str == "forced" || ipv6_str == "force") + allow_ipv4_ = false; + else + throw std::runtime_error("Unsupported setting for the IPv6 parameter."); - mcasttmp.insert(mcasttmp.end(), machine_group.begin(), machine_group.end()); - multicast_ttl_ = 0; + // read the [multicast] parameters + resolve_scope_ = pt.get("multicast.ResolveScope", "site"); + listen_address_ = pt.get("multicast.ListenAddress", ""); + // Note about multicast addresses: IPv6 multicast addresses should be + // FF0x::1 (see RFC2373, RFC1884) or a predefined multicast group + std::string ipv6_multicast_group = + pt.get("multicast.IPv6MulticastGroup", "113D:6FDD:2C17:A643:FFE2:1BD1:3CD2"); + std::vector machine_group = + parse_set(pt.get("multicast.MachineAddresses", "{127.0.0.1}")); + // 224.0.0.1 is the group for all directly connected hosts (RFC1112) + std::vector link_group = parse_set( + pt.get("multicast.LinkAddresses", "{255.255.255.255, 224.0.0.1, 224.0.0.183}")); + // Multicast groups defined by the organization (and therefore subject + // to filtering / forwarding are in the 239.192.0.0/14 subnet (RFC2365) + std::vector site_group = + parse_set(pt.get("multicast.SiteAddresses", "{239.255.172.215}")); + // Organization groups use the same broadcast addresses (IPv4), but + // have a larger TTL. On the network site, it requires the routers + // to forward the broadcast packets (both IGMP and UDP) + std::vector organization_group = + parse_set(pt.get("multicast.OrganizationAddresses", "{}")); + std::vector global_group = + parse_set(pt.get("multicast.GlobalAddresses", "{}")); + enum { machine = 0, link, site, organization, global } scope; + // construct list of addresses & TTL according to the ResolveScope. + if (resolve_scope_ == "machine") + scope = machine; + else if (resolve_scope_ == "link") + scope = link; + else if (resolve_scope_ == "site") + scope = site; + else if (resolve_scope_ == "organization") + scope = organization; + else if (resolve_scope_ == "global") + scope = global; + else + throw std::runtime_error("This ResolveScope setting is unsupported."); - if (scope >= link) { - mcasttmp.insert(mcasttmp.end(), link_group.begin(), link_group.end()); - mcasttmp.push_back("FF02:" + ipv6_multicast_group); - multicast_ttl_ = 1; - } - if (scope >= site) { - mcasttmp.insert(mcasttmp.end(), site_group.begin(), site_group.end()); - mcasttmp.push_back("FF05:" + ipv6_multicast_group); - multicast_ttl_ = 24; - } - if (scope >= organization) { - mcasttmp.insert(mcasttmp.end(), organization_group.begin(), organization_group.end()); - mcasttmp.push_back("FF08:" + ipv6_multicast_group); - multicast_ttl_ = 32; - } - if (scope >= global) { - mcasttmp.insert(mcasttmp.end(), global_group.begin(), global_group.end()); - mcasttmp.push_back("FF0E:" + ipv6_multicast_group); - multicast_ttl_ = 255; - } + std::vector mcasttmp; - // apply overrides, if any - int ttl_override = pt.get("multicast.TTLOverride", -1); - std::vector address_override = - parse_set(pt.get("multicast.AddressesOverride", "{}")); - if (ttl_override >= 0) multicast_ttl_ = ttl_override; - if (!address_override.empty()) mcasttmp = address_override; + mcasttmp.insert(mcasttmp.end(), machine_group.begin(), machine_group.end()); + multicast_ttl_ = 0; - // Parse, validate and store multicast addresses - for (auto &it : mcasttmp) { - ip::address addr = ip::make_address(it); - if ((addr.is_v4() && allow_ipv4_) || (addr.is_v6() && allow_ipv6_)) - multicast_addresses_.push_back(addr); - } + if (scope >= link) { + mcasttmp.insert(mcasttmp.end(), link_group.begin(), link_group.end()); + mcasttmp.push_back("FF02:" + ipv6_multicast_group); + multicast_ttl_ = 1; + } + if (scope >= site) { + mcasttmp.insert(mcasttmp.end(), site_group.begin(), site_group.end()); + mcasttmp.push_back("FF05:" + ipv6_multicast_group); + multicast_ttl_ = 24; + } + if (scope >= organization) { + mcasttmp.insert(mcasttmp.end(), organization_group.begin(), organization_group.end()); + mcasttmp.push_back("FF08:" + ipv6_multicast_group); + multicast_ttl_ = 32; + } + if (scope >= global) { + mcasttmp.insert(mcasttmp.end(), global_group.begin(), global_group.end()); + mcasttmp.push_back("FF0E:" + ipv6_multicast_group); + multicast_ttl_ = 255; + } - // The network stack requires the source interfaces for multicast packets to be - // specified as IPv4 address or an IPv6 interface index - // Try getting the interfaces from the configuration files - using namespace asio::ip; - std::vector netifs = parse_set(pt.get("multicast.Interfaces", "{}")); - for (const auto &netifstr : netifs) { - netif if_; - if_.name = std::string("Configured in lslapi.cfg"); - if_.addr = make_address(netifstr); - if (if_.addr.is_v6()) if_.ifindex = if_.addr.to_v6().scope_id(); - multicast_interfaces.push_back(if_); - } - // Try getting the interfaces from the OS - if (multicast_interfaces.empty()) multicast_interfaces = get_local_interfaces(); + // apply overrides, if any + int ttl_override = pt.get("multicast.TTLOverride", -1); + std::vector address_override = + parse_set(pt.get("multicast.AddressesOverride", "{}")); + if (ttl_override >= 0) multicast_ttl_ = ttl_override; + if (!address_override.empty()) mcasttmp = address_override; - // Otherwise, let the OS select an appropriate network interface - if (multicast_interfaces.empty()) { - LOG_F(ERROR, - "No local network interface addresses found, resolving streams will likely " - "only work for devices connected to the main network adapter\n"); - // Add dummy interface with default settings - netif dummy; - dummy.name = "Dummy interface"; - dummy.addr = address_v4::any(); - multicast_interfaces.push_back(dummy); - dummy.name = "IPv6 dummy interface"; - dummy.addr = address_v6::any(); - multicast_interfaces.push_back(dummy); - } + // Parse, validate and store multicast addresses + for (auto &it : mcasttmp) { + ip::address addr = ip::make_address(it); + if ((addr.is_v4() && allow_ipv4_) || (addr.is_v6() && allow_ipv6_)) + multicast_addresses_.push_back(addr); + } + // The network stack requires the source interfaces for multicast packets to be + // specified as IPv4 address or an IPv6 interface index + // Try getting the interfaces from the configuration files + using namespace asio::ip; + std::vector netifs = parse_set(pt.get("multicast.Interfaces", "{}")); + for (const auto &netifstr : netifs) { + netif if_; + if_.name = std::string("Configured in lslapi.cfg"); + if_.addr = make_address(netifstr); + if (if_.addr.is_v6()) if_.ifindex = if_.addr.to_v6().scope_id(); + multicast_interfaces.push_back(if_); + } + // Try getting the interfaces from the OS + if (multicast_interfaces.empty()) multicast_interfaces = get_local_interfaces(); - // read the [lab] settings - known_peers_ = parse_set(pt.get("lab.KnownPeers", "{}")); - session_id_ = pt.get("lab.SessionID", "default"); + // Otherwise, let the OS select an appropriate network interface + if (multicast_interfaces.empty()) { + LOG_F(ERROR, + "No local network interface addresses found, resolving streams will likely " + "only work for devices connected to the main network adapter\n"); + // Add dummy interface with default settings + netif dummy; + dummy.name = "Dummy interface"; + dummy.addr = address_v4::any(); + multicast_interfaces.push_back(dummy); + dummy.name = "IPv6 dummy interface"; + dummy.addr = address_v6::any(); + multicast_interfaces.push_back(dummy); + } - // read the [tuning] settings - use_protocol_version_ = std::min( - LSL_PROTOCOL_VERSION, pt.get("tuning.UseProtocolVersion", LSL_PROTOCOL_VERSION)); - watchdog_check_interval_ = pt.get("tuning.WatchdogCheckInterval", 15.0); - watchdog_time_threshold_ = pt.get("tuning.WatchdogTimeThreshold", 15.0); - multicast_min_rtt_ = pt.get("tuning.MulticastMinRTT", 0.5); - multicast_max_rtt_ = pt.get("tuning.MulticastMaxRTT", 3.0); - unicast_min_rtt_ = pt.get("tuning.UnicastMinRTT", 0.75); - unicast_max_rtt_ = pt.get("tuning.UnicastMaxRTT", 5.0); - continuous_resolve_interval_ = pt.get("tuning.ContinuousResolveInterval", 0.5); - timer_resolution_ = pt.get("tuning.TimerResolution", 1); - max_cached_queries_ = pt.get("tuning.MaxCachedQueries", 100); - time_update_interval_ = pt.get("tuning.TimeUpdateInterval", 2.0); - time_update_minprobes_ = pt.get("tuning.TimeUpdateMinProbes", 6); - time_probe_count_ = pt.get("tuning.TimeProbeCount", 8); - time_probe_interval_ = pt.get("tuning.TimeProbeInterval", 0.064); - time_probe_max_rtt_ = pt.get("tuning.TimeProbeMaxRTT", 0.128); - outlet_buffer_reserve_ms_ = pt.get("tuning.OutletBufferReserveMs", 5000); - outlet_buffer_reserve_samples_ = pt.get("tuning.OutletBufferReserveSamples", 128); - socket_send_buffer_size_ = pt.get("tuning.SendSocketBufferSize", 0); - inlet_buffer_reserve_ms_ = pt.get("tuning.InletBufferReserveMs", 5000); - inlet_buffer_reserve_samples_ = pt.get("tuning.InletBufferReserveSamples", 128); - socket_receive_buffer_size_ = pt.get("tuning.ReceiveSocketBufferSize", 0); - smoothing_halftime_ = pt.get("tuning.SmoothingHalftime", 90.0F); - force_default_timestamps_ = pt.get("tuning.ForceDefaultTimestamps", false); + // read the [lab] settings + known_peers_ = parse_set(pt.get("lab.KnownPeers", "{}")); + session_id_ = pt.get("lab.SessionID", "default"); - + // read the [tuning] settings + use_protocol_version_ = std::min( + LSL_PROTOCOL_VERSION, pt.get("tuning.UseProtocolVersion", LSL_PROTOCOL_VERSION)); + watchdog_check_interval_ = pt.get("tuning.WatchdogCheckInterval", 15.0); + watchdog_time_threshold_ = pt.get("tuning.WatchdogTimeThreshold", 15.0); + multicast_min_rtt_ = pt.get("tuning.MulticastMinRTT", 0.5); + multicast_max_rtt_ = pt.get("tuning.MulticastMaxRTT", 3.0); + unicast_min_rtt_ = pt.get("tuning.UnicastMinRTT", 0.75); + unicast_max_rtt_ = pt.get("tuning.UnicastMaxRTT", 5.0); + continuous_resolve_interval_ = pt.get("tuning.ContinuousResolveInterval", 0.5); + timer_resolution_ = pt.get("tuning.TimerResolution", 1); + max_cached_queries_ = pt.get("tuning.MaxCachedQueries", 100); + time_update_interval_ = pt.get("tuning.TimeUpdateInterval", 2.0); + time_update_minprobes_ = pt.get("tuning.TimeUpdateMinProbes", 6); + time_probe_count_ = pt.get("tuning.TimeProbeCount", 8); + time_probe_interval_ = pt.get("tuning.TimeProbeInterval", 0.064); + time_probe_max_rtt_ = pt.get("tuning.TimeProbeMaxRTT", 0.128); + outlet_buffer_reserve_ms_ = pt.get("tuning.OutletBufferReserveMs", 5000); + outlet_buffer_reserve_samples_ = pt.get("tuning.OutletBufferReserveSamples", 128); + socket_send_buffer_size_ = pt.get("tuning.SendSocketBufferSize", 0); + inlet_buffer_reserve_ms_ = pt.get("tuning.InletBufferReserveMs", 5000); + inlet_buffer_reserve_samples_ = pt.get("tuning.InletBufferReserveSamples", 128); + socket_receive_buffer_size_ = pt.get("tuning.ReceiveSocketBufferSize", 0); + smoothing_halftime_ = pt.get("tuning.SmoothingHalftime", 90.0F); + force_default_timestamps_ = pt.get("tuning.ForceDefaultTimestamps", false); } static std::once_flag api_config_once_flag; diff --git a/src/api_config.h b/src/api_config.h index 6cb987cd7..fe1646725 100644 --- a/src/api_config.h +++ b/src/api_config.h @@ -13,13 +13,18 @@ namespace ip = asio::ip; namespace lsl { /** * A configuration object: holds all the configurable settings of liblsl. - * These settings can be set via a configuration file that is automatically searched - * by stream providers and recipients in a series of locations: - * - First, the content set via `lsl_set_config_content()` - * - Second, the file set via `lsl_set_config_filename()` - * - Third, the file `lsl_api.cfg` in the current working directory - * - Fourth, the file `lsl_api.cfg` in the home directory (e.g., `~/lsl_api/lsl_api.cfg`) - * - Fifth, the file `lsl_api.cfg` in the system configuration directory (e.g., `/etc/lsl_api/lsl_api.cfg`) + * Settings are resolved in the following order of precedence (first match wins): + * 1. Content set via `lsl_set_config_content()` (used directly, no file lookup) + * 2. File named by the `LSLAPICFG` environment variable + * 3. File set via `lsl_set_config_filename()` + * 4. `lsl_api.cfg` in the current working directory + * 5. `lsl_api.cfg` in the user's home directory (e.g. `~/lsl_api/lsl_api.cfg`) + * 6. `lsl_api.cfg` in the system configuration directory (e.g. `/etc/lsl_api/lsl_api.cfg`) + * 7. Built-in defaults + * + * Both `lsl_set_config_content()` and `lsl_set_config_filename()` must be called + * before any other LSL function, since the configuration is loaded lazily on + * first use and not re-read afterwards. * * Note that, while in some cases it might seem sufficient to override configurations * only for a subset of machines involved in a recording session (e.g., the servers), @@ -79,17 +84,15 @@ class api_config { bool allow_ipv6() const { return allow_ipv6_; } bool allow_ipv4() const { return allow_ipv4_; } - - /** - * @brief Set the configuration directly from a string. - * - * This allows passing in configuration content directly rather than from a file. - * This MUST be called before the first call to get_instance() to have any effect. - */ - static void set_api_config_content(const std::string &content) { - api_config_content_ = content; - } + * @brief Set the configuration directly from an INI-formatted string. + * + * Allows passing in configuration content rather than reading from a file. + * MUST be called before the first call to get_instance() to have any effect. + */ + static void set_api_config_content(const std::string &content) { + api_config_content_ = content; + } /** * @brief An additional settings path to load configuration from. @@ -97,15 +100,14 @@ class api_config { const std::string &api_config_filename() const { return api_config_filename_; } /** - * @brief Set the config file name used to load the settings. - * - * This MUST be called before the first call to get_instance() to have any effect. + * @brief Set the config file path used to load the settings. + * + * MUST be called before the first call to get_instance() to have any effect. */ static void set_api_config_filename(const std::string &filename) { api_config_filename_ = filename; } - /** * @brief The range or scope of stream lookup when using multicast-based discovery * From 1bba8e9fd0bac13e0d601dc619649d95229589d2 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Tue, 21 Apr 2026 16:38:01 -0400 Subject: [PATCH 5/5] Add unit test for set_api_config_content --- testing/CMakeLists.txt | 7 ++++++- testing/int/runtime_config.cpp | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 testing/int/runtime_config.cpp diff --git a/testing/CMakeLists.txt b/testing/CMakeLists.txt index 63f2ce92c..90928601f 100644 --- a/testing/CMakeLists.txt +++ b/testing/CMakeLists.txt @@ -102,6 +102,11 @@ endif() # Pretend that lsl_test_internal is part of a shared lib so that it can use the __export symbols lslobj. REQ by MinGW. target_compile_definitions(lsl_test_internal PRIVATE LIBLSL_EXPORTS) +# runtime_config test needs a fresh api_config singleton, so it lives in its own executable. +add_executable(lsl_test_runtime_config int/runtime_config.cpp) +target_link_libraries(lsl_test_runtime_config PRIVATE lslobj lslboost common catch_main) +target_compile_definitions(lsl_test_runtime_config PRIVATE LIBLSL_EXPORTS) + if(LSL_BENCHMARKS) # to get somewhat reproducible performance numbers: # /usr/bin/time -v testing/lsl_test_exported --benchmark-samples 100 bounce @@ -117,7 +122,7 @@ if(LSL_BENCHMARKS) ) endif() -set(LSL_TESTS lsl_test_exported lsl_test_internal) +set(LSL_TESTS lsl_test_exported lsl_test_internal lsl_test_runtime_config) foreach(lsltest ${LSL_TESTS}) add_test(NAME ${lsltest} COMMAND ${lsltest} --wait-for-keypress never) if(LSL_INSTALL) diff --git a/testing/int/runtime_config.cpp b/testing/int/runtime_config.cpp new file mode 100644 index 000000000..86209a3bf --- /dev/null +++ b/testing/int/runtime_config.cpp @@ -0,0 +1,23 @@ +#include "api_config.h" +#include + +// Verifies that configuration provided via the runtime-config API is picked up +// on singleton initialization. Because api_config::get_instance() is guarded by +// std::call_once, only the first configuration wins per-process. This test +// therefore lives in its own executable so the singleton starts fresh. + +TEST_CASE("runtime config content overrides defaults", "[api_config][runtime_config]") { + lsl::api_config::set_api_config_content( + "[lab]\n" + "SessionID = runtime_config_test\n" + "[ports]\n" + "BasePort = 30000\n" + "[tuning]\n" + "UseProtocolVersion = 100\n"); + + const auto *cfg = lsl::api_config::get_instance(); + REQUIRE(cfg != nullptr); + CHECK(cfg->session_id() == "runtime_config_test"); + CHECK(cfg->base_port() == 30000); + CHECK(cfg->use_protocol_version() == 100); +}