From 2355486cb3c4afc47676671e49c27a49b3231002 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:30:14 -0500 Subject: [PATCH 1/6] feat(util): add GLib command stream for nonblocking line reads Add a small GLib-backed helper for command stdout that integrates with the main loop instead of blocking on getline() in a worker thread. The helper keeps the existing child setup semantics used by Waybar commands, including process groups, parent-death signaling, and WAYBAR_OUTPUT_NAME propagation. This is the foundation for moving long-running custom commands away from manual poll/read logic in the module itself. Signed-off-by: Austin Horstman --- include/util/command_line_stream.hpp | 40 +++++ meson.build | 3 +- src/util/command_line_stream.cpp | 209 +++++++++++++++++++++++++++ 3 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 include/util/command_line_stream.hpp create mode 100644 src/util/command_line_stream.cpp diff --git a/include/util/command_line_stream.hpp b/include/util/command_line_stream.hpp new file mode 100644 index 00000000..05c82fc4 --- /dev/null +++ b/include/util/command_line_stream.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include +#include + +namespace waybar::util::command { + +class LineStream { + public: + using OutputCallback = std::function; + using ExitCallback = std::function; + + LineStream(std::string output_name, OutputCallback on_output, ExitCallback on_exit); + ~LineStream(); + + void start(const std::string& cmd); + void stop(); + bool running() const; + + private: + bool handleStdout(Glib::IOCondition condition); + void handleExit(Glib::Pid pid, int status); + void closeStdout(); + void drainStdout(bool flush_trailing_line); + static int statusToExitCode(int status); + + std::string output_name_; + OutputCallback on_output_; + ExitCallback on_exit_; + std::string buffer_; + Glib::Pid pid_; + int stdout_fd_; + sigc::connection stdout_connection_; + sigc::connection child_connection_; +}; + +} // namespace waybar::util::command diff --git a/meson.build b/meson.build index f96139a0..e3839cf5 100644 --- a/meson.build +++ b/meson.build @@ -196,7 +196,8 @@ src_files = files( 'src/util/regex_collection.cpp', 'src/util/css_reload_helper.cpp', 'src/util/transform_8bit_to_rgba.cpp', - 'src/util/utf8_string.cpp' + 'src/util/utf8_string.cpp', + 'src/util/command_line_stream.cpp' ) man_files = files( diff --git a/src/util/command_line_stream.cpp b/src/util/command_line_stream.cpp new file mode 100644 index 00000000..b13cc19b --- /dev/null +++ b/src/util/command_line_stream.cpp @@ -0,0 +1,209 @@ +#include "util/command_line_stream.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#endif +#ifdef __FreeBSD__ +#include +#endif + +#include "util/command.hpp" + +namespace { + +void prepareChild(const std::string& output_name) { + sigset_t mask; + sigfillset(&mask); + + const auto err = pthread_sigmask(SIG_UNBLOCK, &mask, nullptr); + if (err != 0) { + spdlog::error("pthread_sigmask in LineStream failed: {}", std::strerror(err)); + } + + int deathsig = SIGTERM; +#ifdef __linux__ + if (prctl(PR_SET_PDEATHSIG, deathsig) != 0) { + spdlog::error("prctl(PR_SET_PDEATHSIG) in LineStream failed: {}", std::strerror(errno)); + } +#endif +#ifdef __FreeBSD__ + if (procctl(P_PID, 0, PROC_PDEATHSIG_CTL, reinterpret_cast(&deathsig)) == -1) { + spdlog::error("procctl(PROC_PDEATHSIG_CTL) in LineStream failed: {}", std::strerror(errno)); + } +#endif + + if (setpgid(0, 0) != 0) { + spdlog::error("setpgid in LineStream failed: {}", std::strerror(errno)); + } + if (!output_name.empty()) { + setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); + } +} + +void emitBufferedLines(std::string& buffer, + const waybar::util::command::LineStream::OutputCallback& on_output, + bool flush_trailing_line) { + for (auto newline_pos = buffer.find('\n'); newline_pos != std::string::npos; + newline_pos = buffer.find('\n')) { + auto line = buffer.substr(0, newline_pos); + buffer.erase(0, newline_pos + 1); + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + on_output(line); + } + + if (flush_trailing_line && !buffer.empty()) { + auto line = std::move(buffer); + buffer.clear(); + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + on_output(line); + } +} + +} // namespace + +waybar::util::command::LineStream::LineStream(std::string output_name, OutputCallback on_output, + ExitCallback on_exit) + : output_name_(std::move(output_name)), + on_output_(std::move(on_output)), + on_exit_(std::move(on_exit)), + pid_(0), + stdout_fd_(-1) {} + +waybar::util::command::LineStream::~LineStream() { stop(); } + +void waybar::util::command::LineStream::start(const std::string& cmd) { + stop(); + + std::vector argv{"/bin/sh", "-c", cmd}; + Glib::spawn_async_with_pipes( + "", argv, Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, + sigc::bind(sigc::ptr_fun(&prepareChild), output_name_), &pid_, nullptr, &stdout_fd_, nullptr); + + const auto flags = fcntl(stdout_fd_, F_GETFL, 0); + if (flags == -1 || fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK) == -1) { + const auto saved_errno = errno; + stop(); + throw std::runtime_error("Unable to configure child stdout: " + + std::string(std::strerror(saved_errno))); + } + + stdout_connection_ = + Glib::signal_io().connect(sigc::mem_fun(*this, &LineStream::handleStdout), stdout_fd_, + Glib::IO_IN | Glib::IO_HUP | Glib::IO_ERR | Glib::IO_NVAL); + child_connection_ = + Glib::signal_child_watch().connect(sigc::mem_fun(*this, &LineStream::handleExit), pid_); +} + +void waybar::util::command::LineStream::stop() { + stdout_connection_.disconnect(); + child_connection_.disconnect(); + + if (pid_ != 0) { + killpg(pid_, SIGTERM); + waitpid(pid_, nullptr, 0); + Glib::spawn_close_pid(pid_); + pid_ = 0; + } + + closeStdout(); + buffer_.clear(); +} + +bool waybar::util::command::LineStream::running() const { return pid_ != 0; } + +bool waybar::util::command::LineStream::handleStdout(Glib::IOCondition condition) { + const auto should_flush = + static_cast(condition & (Glib::IO_HUP | Glib::IO_ERR | Glib::IO_NVAL)); + drainStdout(should_flush); + + if (!running() || should_flush) { + closeStdout(); + return false; + } + + return true; +} + +void waybar::util::command::LineStream::handleExit(Glib::Pid pid, int status) { + child_connection_.disconnect(); + + if (stdout_fd_ != -1) { + drainStdout(true); + stdout_connection_.disconnect(); + closeStdout(); + } + + if (pid_ == pid) { + Glib::spawn_close_pid(pid_); + pid_ = 0; + } else { + Glib::spawn_close_pid(pid); + } + + on_exit_(statusToExitCode(status)); +} + +void waybar::util::command::LineStream::closeStdout() { + if (stdout_fd_ != -1) { + ::close(stdout_fd_); + stdout_fd_ = -1; + } +} + +void waybar::util::command::LineStream::drainStdout(bool flush_trailing_line) { + if (stdout_fd_ == -1) { + return; + } + + std::array chunk = {}; + while (true) { + const auto bytes_read = ::read(stdout_fd_, chunk.data(), chunk.size()); + if (bytes_read > 0) { + buffer_.append(chunk.data(), static_cast(bytes_read)); + emitBufferedLines(buffer_, on_output_, false); + continue; + } + + if (bytes_read == 0) { + emitBufferedLines(buffer_, on_output_, flush_trailing_line); + return; + } + + if (errno == EINTR) { + continue; + } + if (errno == EAGAIN || errno == EWOULDBLOCK) { + return; + } + + spdlog::error("Reading command stdout failed: {}", std::strerror(errno)); + emitBufferedLines(buffer_, on_output_, flush_trailing_line); + return; + } +} + +int waybar::util::command::LineStream::statusToExitCode(int status) { + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + if (WIFSIGNALED(status)) { + return 128 + WTERMSIG(status); + } + return 1; +} From 560f02509b0b3bd09824d0aabe5ea29763ffbc7f Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:30:14 -0500 Subject: [PATCH 2/6] test(util): cover command stream line delivery and EOF flushing Add focused coverage for the new GLib command stream helper. These tests verify that complete lines are emitted as they arrive and that EOF flushes a final unterminated line without duplicating a newline-terminated one. That behavior is the contract the custom module will rely on when its continuous command handling moves onto this helper. Signed-off-by: Austin Horstman --- test/utils/command_line_stream.cpp | 69 ++++++++++++++++++++++++ test/utils/fixtures/GlibTestsFixture.hpp | 11 +++- test/utils/meson.build | 2 + 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 test/utils/command_line_stream.cpp diff --git a/test/utils/command_line_stream.cpp b/test/utils/command_line_stream.cpp new file mode 100644 index 00000000..8c1639e3 --- /dev/null +++ b/test/utils/command_line_stream.cpp @@ -0,0 +1,69 @@ +#if __has_include() +#include +#else +#include +#endif + +#include + +#include +#include +#include + +#include "util/command_line_stream.hpp" + +namespace { + +struct StreamResult { + std::vector lines; + std::optional exit_code; + bool timed_out = false; +}; + +auto run_stream_command(const std::string& cmd) -> StreamResult { + StreamResult result; + auto loop = Glib::MainLoop::create(); + + auto timeout = Glib::signal_timeout().connect( + [&]() { + result.timed_out = true; + loop->quit(); + return false; + }, + 3000); + + waybar::util::command::LineStream stream( + "", + [&](const std::string& line) { result.lines.push_back(line); }, + [&](int exit_code) { + result.exit_code = exit_code; + loop->quit(); + }); + + stream.start(cmd); + loop->run(); + timeout.disconnect(); + return result; +} + +} // namespace + +TEST_CASE("command::LineStream emits complete lines and flushes trailing output", + "[util][command_line_stream]") { + const auto result = run_stream_command("printf 'first\\nsecond'"); + + REQUIRE_FALSE(result.timed_out); + REQUIRE(result.exit_code.has_value()); + REQUIRE(*result.exit_code == 0); + REQUIRE(result.lines == std::vector{"first", "second"}); +} + +TEST_CASE("command::LineStream does not emit an extra line after newline-terminated output", + "[util][command_line_stream]") { + const auto result = run_stream_command("printf 'first\\nsecond\\n'"); + + REQUIRE_FALSE(result.timed_out); + REQUIRE(result.exit_code.has_value()); + REQUIRE(*result.exit_code == 0); + REQUIRE(result.lines == std::vector{"first", "second"}); +} diff --git a/test/utils/fixtures/GlibTestsFixture.hpp b/test/utils/fixtures/GlibTestsFixture.hpp index a21c8e07..95ce7f95 100644 --- a/test/utils/fixtures/GlibTestsFixture.hpp +++ b/test/utils/fixtures/GlibTestsFixture.hpp @@ -6,10 +6,16 @@ class GlibTestsFixture : public sigc::trackable { public: GlibTestsFixture() : main_loop_{Glib::MainLoop::create()} {} + ~GlibTestsFixture() { timeout_.disconnect(); } void setTimeout(int timeout) { - Glib::signal_timeout().connect_once([]() { throw std::runtime_error("Test timed out"); }, - timeout); + timeout_.disconnect(); + timeout_ = Glib::signal_timeout().connect( + []() { + throw std::runtime_error("Test timed out"); + return false; + }, + timeout); } void run(std::function fn) { @@ -21,4 +27,5 @@ class GlibTestsFixture : public sigc::trackable { protected: Glib::RefPtr main_loop_; + sigc::connection timeout_; }; diff --git a/test/utils/meson.build b/test/utils/meson.build index e8dd37fa..0d9130de 100644 --- a/test/utils/meson.build +++ b/test/utils/meson.build @@ -15,8 +15,10 @@ test_src = files( 'SafeSignal.cpp', 'sleeper_thread.cpp', 'command.cpp', + 'command_line_stream.cpp', 'css_reload_helper.cpp', '../../src/util/css_reload_helper.cpp', + '../../src/util/command_line_stream.cpp', ) if tz_dep.found() From 5f4b96ad1d8a34a819581a117b733a6ea097e288 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:32:15 -0500 Subject: [PATCH 3/6] refactor(custom): move continuous exec onto GLib command stream Replace the custom module's continuous getline() worker with the new GLib-backed command stream helper. This moves line delivery, child exit handling, and restart scheduling onto the main loop so continuous commands no longer depend on a blocking FILE* read inside SleeperThread. The behavior is kept aligned with the old module semantics: stdout lines still emit updates, non-zero exits still surface as errors, and restart-interval still respawns the command. Signed-off-by: Austin Horstman --- include/modules/custom.hpp | 9 ++- src/modules/custom.cpp | 117 +++++++++++++++++++------------------ 2 files changed, 67 insertions(+), 59 deletions(-) diff --git a/include/modules/custom.hpp b/include/modules/custom.hpp index 442b1c37..dbdcb698 100644 --- a/include/modules/custom.hpp +++ b/include/modules/custom.hpp @@ -3,10 +3,12 @@ #include #include +#include #include #include "AIconLabel.hpp" #include "util/command.hpp" +#include "util/command_line_stream.hpp" #include "util/json.hpp" #include "util/sleeper_thread.hpp" @@ -22,6 +24,9 @@ class Custom : public AIconLabel { private: void delayWorker(); void continuousWorker(); + void startContinuousProcess(bool throw_on_failure); + void handleContinuousProcessExit(int exit_code); + void scheduleContinuousRestart(); void waitingWorker(); void parseOutputRaw(); void parseOutputJson(); @@ -42,10 +47,10 @@ class Custom : public AIconLabel { const bool tooltip_format_enabled_; std::vector class_; int percentage_; - FILE* fp_; - int pid_; util::command::res output_; util::JsonParser parser_; + std::unique_ptr continuous_stream_; + sigc::connection restart_connection_; util::SleeperThread thread_; }; diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 83f36797..54fda760 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -2,10 +2,9 @@ #include +#include #include -#include "util/scope_guard.hpp" - waybar::modules::Custom::Custom(const std::string& name, const std::string& id, const Json::Value& config, const std::string& output_name) : AIconLabel(config, "custom-" + name, id, "{}"), @@ -13,9 +12,7 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, output_name_(output_name), id_(id), tooltip_format_enabled_{config_["tooltip-format"].isString()}, - percentage_(0), - fp_(nullptr), - pid_(-1) { + percentage_(0) { if (config.isNull()) { spdlog::warn("There is no configuration for 'custom/{}', element will be hidden", name); } @@ -40,10 +37,9 @@ waybar::modules::Custom::Custom(const std::string& name, const std::string& id, } waybar::modules::Custom::~Custom() { - if (pid_ != -1) { - killpg(pid_, SIGTERM); - waitpid(pid_, NULL, 0); - pid_ = -1; + restart_connection_.disconnect(); + if (continuous_stream_) { + continuous_stream_->stop(); } } @@ -80,57 +76,64 @@ void waybar::modules::Custom::delayWorker() { } void waybar::modules::Custom::continuousWorker() { - auto cmd = config_["exec"].asString(); - pid_ = -1; - fp_ = util::command::open(cmd, pid_, output_name_); - if (!fp_) { - throw std::runtime_error("Unable to open " + cmd); - } - thread_ = [this, cmd] { - char* buff = nullptr; - waybar::util::ScopeGuard buff_deleter([&buff]() { - if (buff) { - free(buff); - } - }); - size_t len = 0; - if (getline(&buff, &len, fp_) == -1) { - int exit_code = 1; - if (fp_) { - exit_code = WEXITSTATUS(util::command::close(fp_, pid_)); - fp_ = nullptr; - } - if (exit_code != 0) { - output_ = {exit_code, ""}; + continuous_stream_ = std::make_unique( + output_name_, + [this](const std::string& output) { + output_ = {0, output}; dp.emit(); - spdlog::error("{} stopped unexpectedly, is it endless?", name_); - } - if (config_["restart-interval"].isNumeric() && config_["restart-interval"].asDouble() > 0) { - pid_ = -1; - thread_.sleep_for(std::chrono::milliseconds( - std::max(1L, // Minimum 1ms due to millisecond precision - static_cast(config_["restart-interval"].asDouble() * 1000)))); - fp_ = util::command::open(cmd, pid_, output_name_); - if (!fp_) { - throw std::runtime_error("Unable to open " + cmd); - } - } else { - // A non-positive restart-interval must not busy-respawn the script - // (that starves the GTK main loop); treat it as "do not restart". - thread_.stop(); - return; - } - } else { - std::string output = buff; + }, + [this](int exit_code) { handleContinuousProcessExit(exit_code); }); + startContinuousProcess(true); +} - // Remove last newline - if (!output.empty() && output[output.length() - 1] == '\n') { - output.erase(output.length() - 1); - } - output_ = {0, output}; - dp.emit(); +void waybar::modules::Custom::startContinuousProcess(bool throw_on_failure) { + const auto cmd = config_["exec"].asString(); + + try { + continuous_stream_->start(cmd); + } catch (const Glib::SpawnError& e) { + if (throw_on_failure) { + throw std::runtime_error("Unable to open " + cmd + ": " + e.what().raw()); } - }; + output_ = {1, ""}; + dp.emit(); + spdlog::error("Unable to restart {}: {}", name_, e.what().raw()); + scheduleContinuousRestart(); + } catch (const std::exception& e) { + if (throw_on_failure) { + throw; + } + output_ = {1, ""}; + dp.emit(); + spdlog::error("Unable to restart {}: {}", name_, e.what()); + scheduleContinuousRestart(); + } +} + +void waybar::modules::Custom::handleContinuousProcessExit(int exit_code) { + if (exit_code != 0) { + output_ = {exit_code, ""}; + dp.emit(); + spdlog::error("{} stopped unexpectedly, is it endless?", name_); + } + + scheduleContinuousRestart(); +} + +void waybar::modules::Custom::scheduleContinuousRestart() { + restart_connection_.disconnect(); + if (!config_["restart-interval"].isNumeric() || config_["restart-interval"].asDouble() <= 0) { + // A non-positive restart-interval must not busy-respawn the script + // (that starves the GTK main loop); treat it as "do not restart". + return; + } + + restart_connection_ = Glib::signal_timeout().connect( + [this] { + startContinuousProcess(false); + return false; + }, + std::max(1U, static_cast(config_["restart-interval"].asDouble() * 1000))); } void waybar::modules::Custom::waitingWorker() { From fc01c03a202ed84688ed900ed78048db57bf20ef Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:33:12 -0500 Subject: [PATCH 4/6] fix(custom): avoid blocking child reaps in interval worker Stop the interval worker from waiting synchronously on every pid in pid_children_ before it refreshes the module. Switching this reap pass to waitpid(..., WNOHANG) keeps the worker responsive when an older event-triggered child is still running, while still removing children that have already exited. Signed-off-by: Austin Horstman --- src/modules/custom.cpp | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index 54fda760..f010209e 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -50,13 +51,21 @@ void waybar::modules::Custom::delayWorker() { } thread_ = [this] { - for (int i : this->pid_children_) { - int status; - waitpid(i, &status, 0); + for (auto it = this->pid_children_.begin(); it != this->pid_children_.end();) { + int status = 0; + const auto pid = static_cast(*it); + const auto waited = waitpid(pid, &status, WNOHANG); + if (waited == 0) { + ++it; + continue; + } + if (waited == -1 && errno != ECHILD) { + ++it; + continue; + } + it = this->pid_children_.erase(it); } - this->pid_children_.clear(); - bool can_update = true; if (config_["exec-if"].isString()) { output_ = util::command::execNoRead(config_["exec-if"].asString()); From f987c24144272afc5976e9385b81fbb19f59b928 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Sun, 8 Mar 2026 22:36:15 -0500 Subject: [PATCH 5/6] style(custom): apply format and tidy cleanups Run clang-format on the changed C++ files and fix the clang-tidy findings introduced by the custom command migration. The only codegen-relevant change here is switching the new res assignments in custom.cpp to designated initializers. The rest is formatting only. Signed-off-by: Austin Horstman --- src/modules/custom.cpp | 8 ++++---- src/util/command_line_stream.cpp | 6 +++--- test/utils/command_line_stream.cpp | 3 +-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/modules/custom.cpp b/src/modules/custom.cpp index f010209e..64577509 100644 --- a/src/modules/custom.cpp +++ b/src/modules/custom.cpp @@ -88,7 +88,7 @@ void waybar::modules::Custom::continuousWorker() { continuous_stream_ = std::make_unique( output_name_, [this](const std::string& output) { - output_ = {0, output}; + output_ = {.exit_code = 0, .out = output}; dp.emit(); }, [this](int exit_code) { handleContinuousProcessExit(exit_code); }); @@ -104,7 +104,7 @@ void waybar::modules::Custom::startContinuousProcess(bool throw_on_failure) { if (throw_on_failure) { throw std::runtime_error("Unable to open " + cmd + ": " + e.what().raw()); } - output_ = {1, ""}; + output_ = {.exit_code = 1, .out = ""}; dp.emit(); spdlog::error("Unable to restart {}: {}", name_, e.what().raw()); scheduleContinuousRestart(); @@ -112,7 +112,7 @@ void waybar::modules::Custom::startContinuousProcess(bool throw_on_failure) { if (throw_on_failure) { throw; } - output_ = {1, ""}; + output_ = {.exit_code = 1, .out = ""}; dp.emit(); spdlog::error("Unable to restart {}: {}", name_, e.what()); scheduleContinuousRestart(); @@ -121,7 +121,7 @@ void waybar::modules::Custom::startContinuousProcess(bool throw_on_failure) { void waybar::modules::Custom::handleContinuousProcessExit(int exit_code) { if (exit_code != 0) { - output_ = {exit_code, ""}; + output_ = {.exit_code = exit_code, .out = ""}; dp.emit(); spdlog::error("{} stopped unexpectedly, is it endless?", name_); } diff --git a/src/util/command_line_stream.cpp b/src/util/command_line_stream.cpp index b13cc19b..aeeedfaf 100644 --- a/src/util/command_line_stream.cpp +++ b/src/util/command_line_stream.cpp @@ -91,9 +91,9 @@ void waybar::util::command::LineStream::start(const std::string& cmd) { stop(); std::vector argv{"/bin/sh", "-c", cmd}; - Glib::spawn_async_with_pipes( - "", argv, Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, - sigc::bind(sigc::ptr_fun(&prepareChild), output_name_), &pid_, nullptr, &stdout_fd_, nullptr); + Glib::spawn_async_with_pipes("", argv, Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, + sigc::bind(sigc::ptr_fun(&prepareChild), output_name_), &pid_, + nullptr, &stdout_fd_, nullptr); const auto flags = fcntl(stdout_fd_, F_GETFL, 0); if (flags == -1 || fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK) == -1) { diff --git a/test/utils/command_line_stream.cpp b/test/utils/command_line_stream.cpp index 8c1639e3..b852a6e8 100644 --- a/test/utils/command_line_stream.cpp +++ b/test/utils/command_line_stream.cpp @@ -33,8 +33,7 @@ auto run_stream_command(const std::string& cmd) -> StreamResult { 3000); waybar::util::command::LineStream stream( - "", - [&](const std::string& line) { result.lines.push_back(line); }, + "", [&](const std::string& line) { result.lines.push_back(line); }, [&](int exit_code) { result.exit_code = exit_code; loop->quit(); From 1b6173ddda9b2571ec0a754534fb8564d79aa1d1 Mon Sep 17 00:00:00 2001 From: Austin Horstman Date: Mon, 9 Mar 2026 08:00:27 -0500 Subject: [PATCH 6/6] fix(util): keep GLib child setup fork-safe Move WAYBAR_OUTPUT_NAME injection into the parent-provided spawn environment and strip logging and setenv() out of the GLib child-setup hook. That keeps the helper's post-fork path limited to the process setup it actually needs, which is a safer fit for sanitizer-heavy platforms such as FreeBSD. Signed-off-by: Austin Horstman --- src/util/command_line_stream.cpp | 50 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/src/util/command_line_stream.cpp b/src/util/command_line_stream.cpp index aeeedfaf..7abb79b8 100644 --- a/src/util/command_line_stream.cpp +++ b/src/util/command_line_stream.cpp @@ -23,33 +23,42 @@ namespace { -void prepareChild(const std::string& output_name) { +auto buildChildEnvironment(const std::string& output_name) -> std::vector { + auto names = Glib::listenv(); + std::vector envp; + envp.reserve(names.size() + 1); + + for (const auto& name : names) { + bool found = false; + auto value = Glib::getenv(name, found); + if (!found || name == "WAYBAR_OUTPUT_NAME") { + continue; + } + envp.push_back(name + "=" + value); + } + + if (!output_name.empty()) { + envp.push_back("WAYBAR_OUTPUT_NAME=" + output_name); + } + + return envp; +} + +void prepareChild() { sigset_t mask; sigfillset(&mask); - const auto err = pthread_sigmask(SIG_UNBLOCK, &mask, nullptr); - if (err != 0) { - spdlog::error("pthread_sigmask in LineStream failed: {}", std::strerror(err)); - } + (void)pthread_sigmask(SIG_UNBLOCK, &mask, nullptr); int deathsig = SIGTERM; #ifdef __linux__ - if (prctl(PR_SET_PDEATHSIG, deathsig) != 0) { - spdlog::error("prctl(PR_SET_PDEATHSIG) in LineStream failed: {}", std::strerror(errno)); - } + (void)prctl(PR_SET_PDEATHSIG, deathsig); #endif #ifdef __FreeBSD__ - if (procctl(P_PID, 0, PROC_PDEATHSIG_CTL, reinterpret_cast(&deathsig)) == -1) { - spdlog::error("procctl(PROC_PDEATHSIG_CTL) in LineStream failed: {}", std::strerror(errno)); - } + (void)procctl(P_PID, 0, PROC_PDEATHSIG_CTL, reinterpret_cast(&deathsig)); #endif - if (setpgid(0, 0) != 0) { - spdlog::error("setpgid in LineStream failed: {}", std::strerror(errno)); - } - if (!output_name.empty()) { - setenv("WAYBAR_OUTPUT_NAME", output_name.c_str(), 1); - } + (void)setpgid(0, 0); } void emitBufferedLines(std::string& buffer, @@ -91,9 +100,10 @@ void waybar::util::command::LineStream::start(const std::string& cmd) { stop(); std::vector argv{"/bin/sh", "-c", cmd}; - Glib::spawn_async_with_pipes("", argv, Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, - sigc::bind(sigc::ptr_fun(&prepareChild), output_name_), &pid_, - nullptr, &stdout_fd_, nullptr); + auto envp = buildChildEnvironment(output_name_); + Glib::spawn_async_with_pipes("", argv, envp, + Glib::SPAWN_DO_NOT_REAP_CHILD | Glib::SPAWN_CLOEXEC_PIPES, + sigc::ptr_fun(&prepareChild), &pid_, nullptr, &stdout_fd_, nullptr); const auto flags = fcntl(stdout_fd_, F_GETFL, 0); if (flags == -1 || fcntl(stdout_fd_, F_SETFL, flags | O_NONBLOCK) == -1) {