Merge pull request #4929 from c4rlo/failed-units-tooltip

systemd-failed-units: add tooltip with list of failed units
This commit is contained in:
Alexis Rouillard
2026-03-18 09:05:54 +01:00
committed by GitHub
3 changed files with 248 additions and 60 deletions

View File

@@ -3,6 +3,7 @@
#include <giomm/dbusproxy.h> #include <giomm/dbusproxy.h>
#include <string> #include <string>
#include <vector>
#include "ALabel.hpp" #include "ALabel.hpp"
@@ -11,23 +12,42 @@ namespace waybar::modules {
class SystemdFailedUnits : public ALabel { class SystemdFailedUnits : public ALabel {
public: public:
SystemdFailedUnits(const std::string&, const Json::Value&); SystemdFailedUnits(const std::string&, const Json::Value&);
virtual ~SystemdFailedUnits(); virtual ~SystemdFailedUnits() = default;
auto update() -> void override; auto update() -> void override;
private: private:
bool hide_on_ok; struct FailedUnit {
std::string format_ok; std::string name;
std::string description;
std::string load_state;
std::string active_state;
std::string sub_state;
std::string scope;
};
bool update_pending; bool hide_on_ok_;
std::string system_state, user_state, overall_state; std::string format_ok_;
uint32_t nr_failed_system, nr_failed_user, nr_failed; std::string tooltip_format_;
std::string last_status; std::string tooltip_format_ok_;
Glib::RefPtr<Gio::DBus::Proxy> system_proxy, user_proxy; std::string tooltip_unit_format_;
bool update_pending_;
std::string system_state_, user_state_, overall_state_;
uint32_t nr_failed_system_, nr_failed_user_, nr_failed_;
std::string last_status_;
Glib::RefPtr<Gio::DBus::Proxy> system_props_proxy_, user_props_proxy_;
Glib::RefPtr<Gio::DBus::Proxy> system_manager_proxy_, user_manager_proxy_;
std::vector<FailedUnit> failed_units_;
void notify_cb(const Glib::ustring& sender_name, const Glib::ustring& signal_name, void notify_cb(const Glib::ustring& sender_name, const Glib::ustring& signal_name,
const Glib::VariantContainerBase& arguments); const Glib::VariantContainerBase& arguments);
void RequestFailedUnits(); void RequestFailedUnits();
void RequestFailedUnitsList();
void RequestSystemState(); void RequestSystemState();
std::vector<FailedUnit> LoadFailedUnitsList(const char* kind,
Glib::RefPtr<Gio::DBus::Proxy>& proxy,
const std::string& scope);
std::string BuildTooltipFailedList() const;
void updateData(); void updateData();
}; };

View File

@@ -19,7 +19,7 @@ Addressed by *systemd-failed-units*
*format-ok*: ++ *format-ok*: ++
typeof: string ++ typeof: string ++
This format is used when there is no failing units. This format is used when there are no failing units.
*user*: ++ *user*: ++
typeof: bool ++ typeof: bool ++
@@ -34,15 +34,30 @@ Addressed by *systemd-failed-units*
*hide-on-ok*: ++ *hide-on-ok*: ++
typeof: bool ++ typeof: bool ++
default: *true* ++ default: *true* ++
Option to hide this module when there is no failing units. Option to hide this module when there are no failed units.
*tooltip-format*: ++
typeof: string ++
default: *System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n{failed_units_list}* ++
Tooltip format shown when there are failed units.
*tooltip-format-ok*: ++
typeof: string ++
default: *System: {system_state}\nUser: {user_state}* ++
Tooltip format used when there are no failed units.
*tooltip-unit-format*: ++
typeof: string ++
default: *{name}: {description}* ++
Format used to render each failed unit inside the tooltip. Each item is prefixed with a bullet.
*menu*: ++ *menu*: ++
typeof: string ++ typeof: string ++
Action that popups the menu. Action that pops up the menu.
*menu-file*: ++ *menu-file*: ++
typeof: string ++ typeof: string ++
Location of the menu descriptor file. There need to be an element of type Location of the menu descriptor file. There needs to be an element of type
GtkMenu with id *menu* GtkMenu with id *menu*
*menu-actions*: ++ *menu-actions*: ++
@@ -62,11 +77,23 @@ Addressed by *systemd-failed-units*
*{nr_failed}*: Number of total failed units. *{nr_failed}*: Number of total failed units.
*{systemd_state}:* State of the systemd system session *{system_state}:* State of the systemd system session.
*{user_state}:* State of the systemd user session *{user_state}:* State of the systemd user session.
*{overall_state}:* Overall state of the systemd and user session. ("Ok" or "Degraded") *{overall_state}:* Overall state of the systemd and user session. ("ok" or "degraded")
*{failed_units_list}:* Bulleted list of failed units using *tooltip-unit-format*. Empty when
there are no failed units.
The *tooltip-unit-format* string supports the following replacements:
*{name}*: Unit name ++
*{description}*: Unit description ++
*{load_state}*: Unit load state ++
*{active_state}*: Unit active state ++
*{sub_state}*: Unit sub state ++
*{scope}*: Either *system* or *user* depending on where the unit originated
# EXAMPLES # EXAMPLES
@@ -77,6 +104,8 @@ Addressed by *systemd-failed-units*
"format-ok": "✓", "format-ok": "✓",
"system": true, "system": true,
"user": false, "user": false,
"tooltip-format": "{nr_failed} failed units:\n{failed_units_list}",
"tooltip-unit-format": "{scope}: {name} ({active_state})",
} }
``` ```

View File

@@ -1,10 +1,15 @@
#include "modules/systemd_failed_units.hpp" #include "modules/systemd_failed_units.hpp"
#include <fmt/format.h>
#include <giomm/dbusproxy.h> #include <giomm/dbusproxy.h>
#include <glibmm/markup.h>
#include <glibmm/variant.h> #include <glibmm/variant.h>
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <cstdint> #include <cstdint>
#include <exception>
#include <stdexcept>
#include <tuple>
static const unsigned UPDATE_DEBOUNCE_TIME_MS = 1000; static const unsigned UPDATE_DEBOUNCE_TIME_MS = 1000;
@@ -12,39 +17,71 @@ namespace waybar::modules {
SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& config) SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value& config)
: ALabel(config, "systemd-failed-units", id, "{nr_failed} failed", 1), : ALabel(config, "systemd-failed-units", id, "{nr_failed} failed", 1),
hide_on_ok(true), hide_on_ok_(true),
update_pending(false), tooltip_format_(
nr_failed_system(0), "System: {system_state}\nUser: {user_state}\nFailed units ({nr_failed}):\n"
nr_failed_user(0), "{failed_units_list}"),
nr_failed(0), tooltip_format_ok_("System: {system_state}\nUser: {user_state}"),
last_status() { tooltip_unit_format_("{name}: {description}"),
update_pending_(false),
nr_failed_system_(0),
nr_failed_user_(0),
nr_failed_(0),
last_status_() {
if (config["hide-on-ok"].isBool()) { if (config["hide-on-ok"].isBool()) {
hide_on_ok = config["hide-on-ok"].asBool(); hide_on_ok_ = config["hide-on-ok"].asBool();
} }
if (config["format-ok"].isString()) { if (config["format-ok"].isString()) {
format_ok = config["format-ok"].asString(); format_ok_ = config["format-ok"].asString();
} else { } else {
format_ok = format_; format_ok_ = format_;
}
if (config["tooltip-format"].isString()) {
tooltip_format_ = config["tooltip-format"].asString();
}
if (config["tooltip-format-ok"].isString()) {
tooltip_format_ok_ = config["tooltip-format-ok"].asString();
}
if (config["tooltip-unit-format"].isString()) {
tooltip_unit_format_ = config["tooltip-unit-format"].asString();
} }
/* Default to enable both "system" and "user". */ /* Default to enable both "system" and "user". */
if (!config["system"].isBool() || config["system"].asBool()) { if (!config["system"].isBool() || config["system"].asBool()) {
system_proxy = Gio::DBus::Proxy::create_for_bus_sync( system_props_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1", Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1",
"/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties"); "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties");
if (!system_proxy) { if (!system_props_proxy_) {
throw std::runtime_error("Unable to connect to systemwide systemd DBus!"); throw std::runtime_error("Unable to connect to systemwide systemd DBus!");
} }
system_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); system_props_proxy_->signal_signal().connect(
sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb));
try {
system_manager_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::BUS_TYPE_SYSTEM, "org.freedesktop.systemd1",
"/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager");
} catch (const Glib::Error& e) {
spdlog::warn("Unable to connect to systemwide systemd Manager interface: {}",
e.what().c_str());
}
} }
if (!config["user"].isBool() || config["user"].asBool()) { if (!config["user"].isBool() || config["user"].asBool()) {
user_proxy = Gio::DBus::Proxy::create_for_bus_sync( user_props_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1", Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1",
"/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties"); "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties");
if (!user_proxy) { if (!user_props_proxy_) {
throw std::runtime_error("Unable to connect to user systemd DBus!"); throw std::runtime_error("Unable to connect to user systemd DBus!");
} }
user_proxy->signal_signal().connect(sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb)); user_props_proxy_->signal_signal().connect(
sigc::mem_fun(*this, &SystemdFailedUnits::notify_cb));
try {
user_manager_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(
Gio::DBus::BusType::BUS_TYPE_SESSION, "org.freedesktop.systemd1",
"/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager");
} catch (const Glib::Error& e) {
spdlog::warn("Unable to connect to user systemd Manager interface: {}", e.what().c_str());
}
} }
updateData(); updateData();
@@ -52,16 +89,11 @@ SystemdFailedUnits::SystemdFailedUnits(const std::string& id, const Json::Value&
dp.emit(); dp.emit();
} }
SystemdFailedUnits::~SystemdFailedUnits() {
if (system_proxy) system_proxy.reset();
if (user_proxy) user_proxy.reset();
}
auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name, auto SystemdFailedUnits::notify_cb(const Glib::ustring& sender_name,
const Glib::ustring& signal_name, const Glib::ustring& signal_name,
const Glib::VariantContainerBase& arguments) -> void { const Glib::VariantContainerBase& arguments) -> void {
if (signal_name == "PropertiesChanged" && !update_pending) { if (signal_name == "PropertiesChanged" && !update_pending_) {
update_pending = true; update_pending_ = true;
/* The fail count may fluctuate due to restarting. */ /* The fail count may fluctuate due to restarting. */
Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &SystemdFailedUnits::updateData), Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &SystemdFailedUnits::updateData),
UPDATE_DEBOUNCE_TIME_MS); UPDATE_DEBOUNCE_TIME_MS);
@@ -88,12 +120,12 @@ void SystemdFailedUnits::RequestSystemState() {
return "unknown"; return "unknown";
}; };
system_state = load("systemwide", system_proxy); system_state_ = load("systemwide", system_props_proxy_);
user_state = load("user", user_proxy); user_state_ = load("user", user_props_proxy_);
if (system_state == "running" && user_state == "running") if (system_state_ == "running" && user_state_ == "running")
overall_state = "ok"; overall_state_ = "ok";
else else
overall_state = "degraded"; overall_state_ = "degraded";
} }
void SystemdFailedUnits::RequestFailedUnits() { void SystemdFailedUnits::RequestFailedUnits() {
@@ -116,46 +148,153 @@ void SystemdFailedUnits::RequestFailedUnits() {
return 0; return 0;
}; };
nr_failed_system = load("systemwide", system_proxy); nr_failed_system_ = load("systemwide", system_props_proxy_);
nr_failed_user = load("user", user_proxy); nr_failed_user_ = load("user", user_props_proxy_);
nr_failed = nr_failed_system + nr_failed_user; nr_failed_ = nr_failed_system_ + nr_failed_user_;
}
void SystemdFailedUnits::RequestFailedUnitsList() {
failed_units_.clear();
if (!tooltipEnabled() || nr_failed_ == 0) {
return;
}
if (system_manager_proxy_) {
auto units = LoadFailedUnitsList("systemwide", system_manager_proxy_, "system");
failed_units_.insert(failed_units_.end(), units.begin(), units.end());
}
if (user_manager_proxy_) {
auto units = LoadFailedUnitsList("user", user_manager_proxy_, "user");
failed_units_.insert(failed_units_.end(), units.begin(), units.end());
}
}
auto SystemdFailedUnits::LoadFailedUnitsList(const char* kind,
Glib::RefPtr<Gio::DBus::Proxy>& proxy,
const std::string& scope) -> std::vector<FailedUnit> {
// org.freedesktop.systemd1.Manager.ListUnits returns
// (name, description, load_state, active_state, sub_state, followed, unit_path, job_id,
// job_type, job_path).
using UnitRow = std::tuple<Glib::ustring, Glib::ustring, Glib::ustring, Glib::ustring,
Glib::ustring, Glib::ustring, Glib::DBusObjectPathString, guint32,
Glib::ustring, Glib::DBusObjectPathString>;
using ListUnitsReply = Glib::Variant<std::tuple<std::vector<UnitRow>>>;
std::vector<FailedUnit> units;
if (!proxy) {
return units;
}
try {
auto data = proxy->call_sync("ListUnits");
if (!data) return units;
if (!data.is_of_type(ListUnitsReply::variant_type())) {
spdlog::warn("Unexpected DBus signature for ListUnits: {}", data.get_type_string());
return units;
}
auto [rows] = Glib::VariantBase::cast_dynamic<ListUnitsReply>(data).get();
for (const auto& row : rows) {
const auto& name = std::get<0>(row);
const auto& description = std::get<1>(row);
const auto& load_state = std::get<2>(row);
const auto& active_state = std::get<3>(row);
const auto& sub_state = std::get<4>(row);
if (active_state == "failed" || sub_state == "failed") {
units.push_back({name, description, load_state, active_state, sub_state, scope});
}
}
} catch (const Glib::Error& e) {
spdlog::error("Failed to list {} units: {}", kind, e.what().c_str());
}
return units;
}
std::string SystemdFailedUnits::BuildTooltipFailedList() const {
if (failed_units_.empty()) {
return "";
}
std::string list;
list.reserve(failed_units_.size() * 16);
bool first = true;
for (const auto& unit : failed_units_) {
try {
auto line = fmt::format(
fmt::runtime(tooltip_unit_format_),
fmt::arg("name", Glib::Markup::escape_text(unit.name).raw()),
fmt::arg("description", Glib::Markup::escape_text(unit.description).raw()),
fmt::arg("load_state", unit.load_state), fmt::arg("active_state", unit.active_state),
fmt::arg("sub_state", unit.sub_state), fmt::arg("scope", unit.scope));
if (!first) {
list += "\n";
}
first = false;
list += "- ";
list += line;
} catch (const std::exception& e) {
spdlog::warn("Failed to format tooltip for unit {}: {}", unit.name, e.what());
}
}
return list;
} }
void SystemdFailedUnits::updateData() { void SystemdFailedUnits::updateData() {
update_pending = false; update_pending_ = false;
RequestSystemState(); RequestSystemState();
if (overall_state == "degraded") RequestFailedUnits(); if (overall_state_ == "degraded") {
RequestFailedUnits();
RequestFailedUnitsList();
} else {
nr_failed_system_ = nr_failed_user_ = nr_failed_ = 0;
failed_units_.clear();
}
dp.emit(); dp.emit();
} }
auto SystemdFailedUnits::update() -> void { auto SystemdFailedUnits::update() -> void {
if (last_status == overall_state) return;
// Hide if needed. // Hide if needed.
if (overall_state == "ok" && hide_on_ok) { if (overall_state_ == "ok" && hide_on_ok_) {
event_box_.set_visible(false); event_box_.set_visible(false);
last_status_ = overall_state_;
return; return;
} }
event_box_.set_visible(true); event_box_.set_visible(true);
// Set state class. // Set state class.
if (!last_status.empty() && label_.get_style_context()->has_class(last_status)) { if (!last_status_.empty() && label_.get_style_context()->has_class(last_status_)) {
label_.get_style_context()->remove_class(last_status); label_.get_style_context()->remove_class(last_status_);
} }
if (!label_.get_style_context()->has_class(overall_state)) { if (!label_.get_style_context()->has_class(overall_state_)) {
label_.get_style_context()->add_class(overall_state); label_.get_style_context()->add_class(overall_state_);
} }
last_status = overall_state; last_status_ = overall_state_;
label_.set_markup(fmt::format( label_.set_markup(fmt::format(
fmt::runtime(nr_failed == 0 ? format_ok : format_), fmt::arg("nr_failed", nr_failed), fmt::runtime(nr_failed_ == 0 ? format_ok_ : format_), fmt::arg("nr_failed", nr_failed_),
fmt::arg("nr_failed_system", nr_failed_system), fmt::arg("nr_failed_user", nr_failed_user), fmt::arg("nr_failed_system", nr_failed_system_), fmt::arg("nr_failed_user", nr_failed_user_),
fmt::arg("system_state", system_state), fmt::arg("user_state", user_state), fmt::arg("system_state", system_state_), fmt::arg("user_state", user_state_),
fmt::arg("overall_state", overall_state))); fmt::arg("overall_state", overall_state_)));
if (tooltipEnabled()) {
std::string failed_list = BuildTooltipFailedList();
auto tooltip_template = overall_state_ == "ok" ? tooltip_format_ok_ : tooltip_format_;
if (!tooltip_template.empty()) {
label_.set_tooltip_markup(fmt::format(
fmt::runtime(tooltip_template), fmt::arg("nr_failed", nr_failed_),
fmt::arg("nr_failed_system", nr_failed_system_),
fmt::arg("nr_failed_user", nr_failed_user_), fmt::arg("system_state", system_state_),
fmt::arg("user_state", user_state_), fmt::arg("overall_state", overall_state_),
fmt::arg("failed_units_list", failed_list)));
} else {
label_.set_tooltip_text("");
}
}
ALabel::update(); ALabel::update();
} }