From 0d785e3ecffbe00bad9fcde4ac17f97ee2ec0d4f Mon Sep 17 00:00:00 2001 From: Daniel Thwaites Date: Mon, 6 Jul 2026 14:27:49 +0100 Subject: [PATCH 1/5] Add missing `sysexit` in `handleComms` --- .../src/process_group_manager/details/process_launcher.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 1b7369578..9bbe9e756 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -118,6 +118,7 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) { LM_LOG_ERROR() << "[New process] fcntl() at line" << __LINE__ << "failed:" << score::lcm::internal::errno_message(errno); + sysexit(EXIT_FAILURE); } break; case CommsType::kLaunchManager: From 51c196c834fb5bdbb98603a7a5dc1104baa4048f Mon Sep 17 00:00:00 2001 From: Daniel Thwaites Date: Tue, 28 Jul 2026 12:18:08 +0100 Subject: [PATCH 2/5] Implement async signal safe logging utility Co-authored-by: Maciej Kaszynski <161459353+MaciejKaszynski@users.noreply.github.com> --- .../src/daemon/src/common/BUILD | 8 ++ .../src/daemon/src/common/signal_safe_log.hpp | 130 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 score/launch_manager/src/daemon/src/common/signal_safe_log.hpp diff --git a/score/launch_manager/src/daemon/src/common/BUILD b/score/launch_manager/src/daemon/src/common/BUILD index 2aac5cfa1..8623fdca2 100644 --- a/score/launch_manager/src/daemon/src/common/BUILD +++ b/score/launch_manager/src/daemon/src/common/BUILD @@ -50,6 +50,14 @@ cc_library( }), ) +cc_library( + name = "signal_safe_log", + hdrs = ["signal_safe_log.hpp"], + include_prefix = "score/mw/launch_manager/common", + strip_include_prefix = "/score/launch_manager/src/daemon/src/common", + visibility = ["//score/launch_manager/src/daemon:__subpackages__"], +) + cc_library( name = "constants", hdrs = ["constants.hpp"], diff --git a/score/launch_manager/src/daemon/src/common/signal_safe_log.hpp b/score/launch_manager/src/daemon/src/common/signal_safe_log.hpp new file mode 100644 index 000000000..e2921ca92 --- /dev/null +++ b/score/launch_manager/src/daemon/src/common/signal_safe_log.hpp @@ -0,0 +1,130 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +#include +#include +#include +#include +#include + +#if (_GNU_SOURCE && __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 32) || __QNXNTO__ +#include +#endif + +#ifndef SIGNAL_SAFE_LOG_HPP_INCLUDED +#define SIGNAL_SAFE_LOG_HPP_INCLUDED + +namespace +{ + +/// @brief Utility for building log messages in an async signal safe context. +template +class signal_safe_buffer +{ + public: + /// @brief Append the given string to the buffer. + template , bool> = true> + void append(const T& value) + { + const auto string = std::string_view(value); + + std::size_t length = std::min(string.length(), buffer_.size() - end_); + + for (std::size_t index = 0; index < length; ++index) + { + buffer_[end_++] = string[index]; + } + } + + /// @brief Append the given integer to the buffer. + template , bool> = true> + void append(const T& value) + { + const auto [pointer, error] = std::to_chars(&buffer_[end_], &buffer_.back(), value); + + if (error == std::errc{}) + { + end_ = pointer - &buffer_.front(); + } + } + + /// @brief Write out the buffer to standard error. + /// @return Whether the write succeeded. True means the whole message was + /// written. False means nothing, or only part was written. + [[nodiscard]] bool flush() + { + while (start_ < end_) + { + const auto written = write(STDERR_FILENO, &buffer_[start_], end_ - start_); + + if (written >= 0) + { + start_ += written; + } + else if (errno != EINTR) + { + return false; + } + } + + // Rewind the indices in case the buffer is used again. + start_ = 0; + end_ = 0; + + return true; + } + + private: + std::array buffer_ = {}; + std::size_t start_ = 0; + std::size_t end_ = 0; +}; + +} // namespace + +namespace score::lcm::internal +{ + +template +[[nodiscard]] bool signal_safe_log(const T&... values) +{ + const int original_errno = errno; // For reentrancy + + signal_safe_buffer<1024U> buffer = {}; + (buffer.append(values), ...); + buffer.append("\n"); + const bool written = buffer.flush(); + + errno = original_errno; // For reentrancy + + return written; +} + +template +[[nodiscard]] bool signal_safe_log_errno(int log_errno, const T&... values) +{ +#if _GNU_SOURCE && __GLIBC__ >= 2 && __GLIBC_MINOR__ >= 32 + // strerrordesc_np is documented as async signal safe. + return signal_safe_log(values..., " (", strerrordesc_np(log_errno), ")"); +#elif __QNXNTO__ + // QNX strerror is documented as async signal safe. + return signal_safe_log(values..., " (", strerror(log_errno), ")"); +#else + // POSIX strerror is not async signal safe. Log the raw number instead. + return signal_safe_log(values..., " (errno ", log_errno, ")"); +#endif +} + +} // namespace score::lcm::internal + +#endif From aa4a7113a951d970b419a86c32f08d17dd33e020 Mon Sep 17 00:00:00 2001 From: Daniel Thwaites Date: Tue, 28 Jul 2026 13:39:10 +0100 Subject: [PATCH 3/5] Replace log calls with signal safe version after forking --- .../src/process_group_manager/details/BUILD | 1 + .../details/process_launcher.cpp | 68 ++++++++++--------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD b/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD index 6b0f00e10..265c792d4 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/BUILD @@ -277,6 +277,7 @@ cc_library( visibility = ["//score/launch_manager/src/daemon/src/process_group_manager:__pkg__"], deps = [ "//score/launch_manager/src/daemon/src/common:log", + "//score/launch_manager/src/daemon/src/common:signal_safe_log", "//score/launch_manager/src/daemon/src/control:control_client_channel", "//score/launch_manager/src/daemon/src/osal:ipc_comms", "//score/launch_manager/src/daemon/src/osal:security_policy", diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 9bbe9e756..5bf4faa8b 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -22,6 +22,7 @@ #include #include +#include "score/mw/launch_manager/common/signal_safe_log.hpp" #include "score/mw/launch_manager/process_group_manager/iprocess.hpp" #include "score/mw/launch_manager/control/control_client_channel.hpp" #include "score/mw/launch_manager/common/log.hpp" @@ -42,6 +43,8 @@ constexpr int kPosixSuccess = 0; namespace { +using score::lcm::internal::signal_safe_log; +using score::lcm::internal::signal_safe_log_errno; using score::lcm::internal::osal::CommsType; using score::lcm::internal::osal::IpcCommsSync; using score::lcm::internal::osal::sysexit; @@ -52,8 +55,7 @@ void applyLimitOrDie(const int resource, const rlimit& limit, const std::string_ { if (::setrlimit(resource, &limit) == -1) { - LM_LOG_FATAL() << "[New process] Failed to set rlimit " << rlimit_name << " " - << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, "Failed to apply rlimit ", rlimit_name)); sysexit(EXIT_FAILURE); } } @@ -101,8 +103,8 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) case CommsType::kReporting: if (-1 == fcntl(IpcCommsSync::sync_fd, F_SETFD, 0)) { - LM_LOG_ERROR() << "[New process] fcntl() at line" << __LINE__ - << "failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, + "fcntl at line ", __LINE__, " failed")); sysexit(EXIT_FAILURE); } close(IpcCommsSync::control_client_handler_nudge_fd); @@ -110,14 +112,14 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) case CommsType::kControlClient: if (-1 == fcntl(IpcCommsSync::sync_fd, F_SETFD, 0)) { - LM_LOG_ERROR() << "[New process] fcntl() at line" << __LINE__ - << "failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, + "fcntl at line ", __LINE__, " failed")); sysexit(EXIT_FAILURE); } if (-1 == fcntl(IpcCommsSync::control_client_handler_nudge_fd, F_SETFD, 0)) { - LM_LOG_ERROR() << "[New process] fcntl() at line" << __LINE__ - << "failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, + "fcntl at line ", __LINE__, " failed")); sysexit(EXIT_FAILURE); } break; @@ -125,8 +127,8 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) // nothing to do here break; default: - LM_LOG_ERROR() << "[New process] at line" << __LINE__ << "unknown CommsType" - << static_cast(param.shared_block->comms_type_); + static_cast(signal_safe_log("at line ", __LINE__, " unknown CommsType ", + static_cast(param.shared_block->comms_type_))); sysexit(EXIT_FAILURE); break; } @@ -142,15 +144,15 @@ void changeCurrentWorkingDirectory(const score::lcm::internal::osal::OsalConfig& if (config.executable_path_.size() >= string_size) { - LM_LOG_ERROR() << "[New process] executable path longer than" << string_size - << "chars:" << config.executable_path_; + static_cast(signal_safe_log("executable path longer than ", + string_size, " chars: ", config.executable_path_)); sysexit(EXIT_FAILURE); } if (-1 == chdir(dirname(strncpy(path_copy, config.executable_path_.c_str(), string_size)))) { - LM_LOG_ERROR() << "[New process] chdir(" << config.executable_path_ - << ") failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, + "chdir(", config.executable_path_, ") failed")); sysexit(EXIT_FAILURE); } } @@ -174,8 +176,8 @@ void changeSecurityPolicy(const score::lcm::internal::osal::OsalConfig& config) { if (score::lcm::internal::osal::setSecurityPolicy(config.security_policy_.c_str()) != 0) { - LM_LOG_ERROR() << "[New process] changeSecurityPolicy(" << config.security_policy_ - << ") failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, + "changeSecurityPolicy(", config.security_policy_, ") failed")); sysexit(EXIT_FAILURE); } } @@ -203,7 +205,8 @@ OsalReturnType IProcess::startProcess(ProcessID* pid, IpcCommsP* block, const Os { if (access(config->executable_path_.c_str(), X_OK) != 0) { - LM_LOG_ERROR() << "File does not exist or is not executable:" << config->executable_path_; + static_cast(signal_safe_log( + "File does not exist or is not executable: ", config->executable_path_)); return result; } @@ -273,9 +276,9 @@ inline bool IProcess::setupComms(IpcCommsP& block, int& fd, const OsalConfig& co } static_cast(snprintf(shm_name, - static_cast(score::lcm::internal::ProcessLimits::maxLocalBuffSize), - "/ipc_shared_mem%u", - shm_name_counter++)); + static_cast(score::lcm::internal::ProcessLimits::maxLocalBuffSize), + "/ipc_shared_mem%u", + shm_name_counter++)); fd = shm_open(shm_name, O_CREAT | O_EXCL | O_RDWR, 0U); @@ -362,7 +365,7 @@ OsalReturnType IProcess::setSchedulingAndSecurity(const OsalConfig& config) // setpgid will fail if called by a session lader (which LCMd is), so skip if (config.comms_type_ != osal::CommsType::kLaunchManager && 0 != setpgid(0, getpid())) { - LM_LOG_ERROR() << "setpgid() failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, "setpgid() failed")); retval = OsalReturnType::kFail; } // Set scheduling policy with sched_setscheduler @@ -373,35 +376,34 @@ OsalReturnType IProcess::setSchedulingAndSecurity(const OsalConfig& config) if (sch_param.sched_priority < sched_get_priority_min(config.scheduling_policy_)) { - LM_LOG_WARN() << "Scheduling priority" << sch_param.sched_priority << "is below minimum for policy" - << config.scheduling_policy_ << ", setting to minimum"; + static_cast(signal_safe_log("Scheduling priority ", sch_param.sched_priority, + " is below minimum for policy ", config.scheduling_policy_, ", setting to minimum")); sch_param.sched_priority = sched_get_priority_min(config.scheduling_policy_); } else if (sch_param.sched_priority > sched_get_priority_max(config.scheduling_policy_)) { - LM_LOG_WARN() << "Scheduling priority" << sch_param.sched_priority << "is above maximum for policy" - << config.scheduling_policy_ << ", setting to maximum"; + static_cast(signal_safe_log("Scheduling priority ", sch_param.sched_priority, + " is above maximum for policy ", config.scheduling_policy_, ", setting to maximum")); sch_param.sched_priority = sched_get_priority_max(config.scheduling_policy_); } if (-1 == sched_setscheduler(0, config.scheduling_policy_, &sch_param)) { - LM_LOG_ERROR() << "sched_setscheduler() failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, "sched_setscheduler() failed")); retval = OsalReturnType::kFail; } // Set core affinity using OS specific functionality in osal if (-1 == osal::setaffinity(config.cpu_mask_)) { - LM_LOG_ERROR() << "setaffinity(" << config.cpu_mask_ - << ") failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, "setaffinity(", config.cpu_mask_, ") failed")); retval = OsalReturnType::kFail; } // Set group ID if (-1 == setgid(config.gid_)) { - LM_LOG_ERROR() << "setgid(" << config.gid_ << ") failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, "setgid(", config.gid_, ") failed")); retval = OsalReturnType::kFail; } // Set supplementary group ids @@ -411,14 +413,14 @@ OsalReturnType IProcess::setSchedulingAndSecurity(const OsalConfig& config) if (supplementary_gids_number > 0 && -1 == osal::setgroups(supplementary_gids_number, config.supplementary_gids_.data())) { - LM_LOG_ERROR() << "setgroups() failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, "setgroups() failed")); retval = OsalReturnType::kFail; } // Set user ID if (-1 == setuid(config.uid_)) { - LM_LOG_ERROR() << "setuid(" << config.uid_ << ") failed:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, "setuid(", config.uid_, ") failed")); retval = OsalReturnType::kFail; } @@ -444,8 +446,8 @@ inline void IProcess::handleChildProcess(ChildProcessConfig& param) // arguments.", true); if (-1 == execve(param.config->argv_[0], const_cast(param.config->argv_.data()), param.config->envp_)) { - LM_LOG_ERROR() << "[New process] execve failed: Unable to execute the" << param.config->executable_path_ - << "app. Error:" << score::lcm::internal::errno_message(errno); + static_cast(signal_safe_log_errno(errno, "execve failed: Unable to execute the ", + param.config->executable_path_, " app.")); sysexit(EXIT_FAILURE); } } From 12e52fdec0c6d16d77a2f8df869437c6cc4e3702 Mon Sep 17 00:00:00 2001 From: Daniel Thwaites Date: Tue, 28 Jul 2026 13:55:11 +0100 Subject: [PATCH 4/5] Add comments about signal safety --- .../details/process_launcher.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp index 5bf4faa8b..9535575eb 100644 --- a/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp +++ b/score/launch_manager/src/daemon/src/process_group_manager/details/process_launcher.cpp @@ -50,6 +50,7 @@ using score::lcm::internal::osal::IpcCommsSync; using score::lcm::internal::osal::sysexit; /// @brief Applies the given limit. +/// @details The implementation should be async signal safe. /// @warning This will sysexit if the set is not succesful. void applyLimitOrDie(const int resource, const rlimit& limit, const std::string_view rlimit_name) noexcept(false) { @@ -61,6 +62,7 @@ void applyLimitOrDie(const int resource, const rlimit& limit, const std::string_ } /// @brief Sets the limit if given a non-zero value, otherwise skips. +/// @details The implementation should be async signal safe. /// @warning This will sysexit if the set is not succesful. void setLimit(const int resource, const std::size_t amount, const std::string_view rlimit_name) noexcept { @@ -77,6 +79,7 @@ void setLimit(const int resource, const std::size_t amount, const std::string_vi applyLimitOrDie(resource, limit, rlimit_name); } +/// @details The implementation should be async signal safe. void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) { // kNoComms !fd3 & !fd4 @@ -134,6 +137,7 @@ void handleComms(score::lcm::internal::osal::ChildProcessConfig& param) } } +/// @details The implementation should be async signal safe. void changeCurrentWorkingDirectory(const score::lcm::internal::osal::OsalConfig& config) { // Change current working directory to the same as the executable @@ -157,6 +161,7 @@ void changeCurrentWorkingDirectory(const score::lcm::internal::osal::OsalConfig& } } +/// @details The implementation should be async signal safe. void implementMemoryResourceLimits(const score::lcm::internal::osal::OsalConfig& config) { setLimit(RLIMIT_DATA, config.resource_limits_.data_, "RLIMIT_DATA"); @@ -170,6 +175,7 @@ void implementMemoryResourceLimits(const score::lcm::internal::osal::OsalConfig& setLimit(RLIMIT_CPU, config.resource_limits_.cpu_, "RLIMIT_CPU"); } +/// @details The implementation should be async signal safe. void changeSecurityPolicy(const score::lcm::internal::osal::OsalConfig& config) { if (config.security_policy_ != "") @@ -229,6 +235,12 @@ OsalReturnType IProcess::startProcess(ProcessID* pid, IpcCommsP* block, const Os if (*pid == kPosixSuccess) { + /* + * From this point on, only async signal safe functions can be + * used. `fork` only copies the current thread, so any locks + * which were held at that time will never be released. + * See `man 2 fork`. + */ ChildProcessConfig param = {config, fd, *block}; handleChildProcess(param); result = OsalReturnType::kSuccess; @@ -357,6 +369,7 @@ inline bool IProcess::initializeSemaphores(IpcCommsP shared_block) return result; } +/// @details The implementation should be async signal safe. OsalReturnType IProcess::setSchedulingAndSecurity(const OsalConfig& config) { OsalReturnType retval = OsalReturnType::kSuccess; @@ -427,6 +440,7 @@ OsalReturnType IProcess::setSchedulingAndSecurity(const OsalConfig& config) return retval; } +/// @details The implementation should be async signal safe. inline void IProcess::handleChildProcess(ChildProcessConfig& param) { handleComms(param); From f7f2f746f6c5180d20f9861c97a2b2d240aeb4c6 Mon Sep 17 00:00:00 2001 From: Daniel Thwaites Date: Wed, 29 Jul 2026 10:51:48 +0100 Subject: [PATCH 5/5] Add unit tests for `signal_safe_log` Co-authored-by: Maciej Kaszynski <161459353+MaciejKaszynski@users.noreply.github.com> --- .../src/daemon/src/common/BUILD | 9 ++ .../daemon/src/common/signal_safe_log_UT.cpp | 111 ++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 score/launch_manager/src/daemon/src/common/signal_safe_log_UT.cpp diff --git a/score/launch_manager/src/daemon/src/common/BUILD b/score/launch_manager/src/daemon/src/common/BUILD index 8623fdca2..737e6eb64 100644 --- a/score/launch_manager/src/daemon/src/common/BUILD +++ b/score/launch_manager/src/daemon/src/common/BUILD @@ -58,6 +58,15 @@ cc_library( visibility = ["//score/launch_manager/src/daemon:__subpackages__"], ) +cc_test( + name = "signal_safe_log_UT", + srcs = ["signal_safe_log_UT.cpp"], + deps = [ + ":signal_safe_log", + "@googletest//:gtest_main", + ], +) + cc_library( name = "constants", hdrs = ["constants.hpp"], diff --git a/score/launch_manager/src/daemon/src/common/signal_safe_log_UT.cpp b/score/launch_manager/src/daemon/src/common/signal_safe_log_UT.cpp new file mode 100644 index 000000000..4559ebfdb --- /dev/null +++ b/score/launch_manager/src/daemon/src/common/signal_safe_log_UT.cpp @@ -0,0 +1,111 @@ +/******************************************************************************** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +#include +#include + +#include "score/mw/launch_manager/common/signal_safe_log.hpp" + +using namespace testing; +using score::lcm::internal::signal_safe_log; +using score::lcm::internal::signal_safe_log_errno; + +class signal_safe_log_test : public ::testing::Test +{ + protected: + void SetUp() override + { + RecordProperty("TestType", "interface-test"); + RecordProperty("DerivationTechnique", "explorative-testing"); + } +}; + +TEST_F(signal_safe_log_test, signal_safe_log_test_simple) +{ + RecordProperty( + "Description", "This test verifies that signal_safe_log successfully writes a simple message to stderr."); + + testing::internal::CaptureStderr(); + ASSERT_TRUE(signal_safe_log("Hello world")); + + EXPECT_EQ(testing::internal::GetCapturedStderr(), "Hello world\n"); +} + +TEST_F(signal_safe_log_test, signal_safe_log_test_simple_with_errno) +{ + RecordProperty( + "Description", + "This test verifies that signal_safe_log successfully writes a simple message and errno to stderr."); + + testing::internal::CaptureStderr(); + ASSERT_TRUE(signal_safe_log_errno(1, "Hello world")); + + EXPECT_THAT(testing::internal::GetCapturedStderr(), ContainsRegex("^Hello world \\(.*\\)")); +} + +TEST_F(signal_safe_log_test, signal_safe_log_test_formatted) +{ + RecordProperty( + "Description", "This test verifies that signal_safe_log successfully writes a formatted message to stderr."); + + testing::internal::CaptureStderr(); + ASSERT_TRUE(signal_safe_log(12, " + ", 76, " = ", 88)); + EXPECT_EQ(testing::internal::GetCapturedStderr(), "12 + 76 = 88\n"); +} + +TEST_F(signal_safe_log_test, signal_safe_log_test_formatted_with_errno) +{ + RecordProperty( + "Description", + "This test verifies that signal_safe_log successfully writes a formatted message and errno to stderr."); + + testing::internal::CaptureStderr(); + ASSERT_TRUE(signal_safe_log_errno(1, 12, " + ", 76, " = ", 88)); + + EXPECT_THAT(testing::internal::GetCapturedStderr(), ContainsRegex("12 \\+ 76 = 88 \\(.*\\)")); +} + +TEST_F(signal_safe_log_test, signal_safe_log_test_minimum_length) +{ + RecordProperty("Description", "This test verifies that signal_safe_log can output at least 1024 characters."); + + testing::internal::CaptureStderr(); + ASSERT_TRUE(signal_safe_log(std::string(1023, 'A'))); + + const auto stderr = testing::internal::GetCapturedStderr(); + +#if __QNXNTO__ + EXPECT_EQ(stderr->size(), 1024); + EXPECT_EQ(stderr->back(), '\n'); +#else + EXPECT_EQ(stderr.size(), 1024); + EXPECT_EQ(stderr.back(), '\n'); +#endif +} + +TEST_F(signal_safe_log_test, signal_safe_log_test_maximum_length) +{ + RecordProperty("Description", "This test verifies that signal_safe_log can output at most 1024 characters."); + + testing::internal::CaptureStderr(); + ASSERT_TRUE(signal_safe_log(std::string(2048, 'A'))); + + const auto stderr = testing::internal::GetCapturedStderr(); + +#if __QNXNTO__ + EXPECT_EQ(stderr->size(), 1024); + EXPECT_EQ(stderr->back(), 'A'); +#else + EXPECT_EQ(stderr.size(), 1024); + EXPECT_EQ(stderr.back(), 'A'); +#endif +}