Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a power-profiles-daemon module #2971

Merged
merged 9 commits into from
Mar 4, 2024

Conversation

picnoir
Copy link
Contributor

@picnoir picnoir commented Feb 25, 2024

We introduce a module in charge to display and toggle on click the power profiles via power-profiles-daemon.

https://gitlab.freedesktop.org/upower/power-profiles-daemon

This daemon is pretty widespread. It's the component used by Gnome and KDE to manage the power profiles. The power management daemon is a pretty important software component for laptops and other battery-powered devices.

We're using the daemon DBus interface to:

  • Fetch the available power profiles.
  • Track the active power profile.
  • Change the active power profile.

The original author recently gave up maintenance on the project. The Upower group took over the maintenance burden… …and created a new DBus name for the project. The old name is still advertised for now. We use the old name for compatibility sake: most distributions did not release 0.20, which introduces this new DBus name. We'll likely revisit
this in the future and point to the new bus name. See the inline comment for more details.

Given how widespread this daemon is, I activated the module in the default configuration. I'm not 100% sure about myself on that one though :)

Demo video: https://social.alternativebit.fr/media/514a598a09c6c9047e61af405d188aa7b99a5730015c95ad787f7c023ea1f461.mp4

@picnoir picnoir force-pushed the pic/power-profiles-daemon branch 2 times, most recently from f999681 to 9502940 Compare February 25, 2024 14:12
@picnoir
Copy link
Contributor Author

picnoir commented Feb 25, 2024

Fixes #1539

activeProfile_ = std::find_if(availableProfiles_.begin(), availableProfiles_.end(), pred);
if (activeProfile_ == availableProfiles_.end()) {
spdlog::error("PowerProfilesDaemon: FATAL, can't find the active profile in the available profiles list");
exit(-1);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why an exit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Erf! Two reasons 😅

  1. When I wrote this, I did not know the best way to crash the module to prevent it to do some dangerous dereferences. I made a mental note about that and put this exit in the meantime.
  2. My memory is not so good, I tend to forget my mental notes very fast 😀

Anyway, after looking the current codebase, it seems like throwing a runtime error does the trick. Don't hesitate to point me to another direction if this is not the best way to go.

@picnoir picnoir force-pushed the pic/power-profiles-daemon branch 2 times, most recently from 6cc362b to 7405011 Compare February 26, 2024 08:39
@picnoir
Copy link
Contributor Author

picnoir commented Feb 26, 2024

Rebased, there was a conflict on the config file.


Documentation-wise, I could use some help.

I'm not sure what to do for now. Is the wiki the canonical source for it? How are you dealing with it when adding a new feature like this?

There's not much to document for now, the label ain't configurable: I personally don't really need that. I planned to implement that iteratively in a subsequent PR. Unless you'd prefer this to be done all at the same time in this PR.


Edit: CI is red apparently, I don't have time to fix it RN. I'll do that tonight. Fixed

We introduce a module in charge to display and toggle on click the
power profiles via power-profiles-daemon.

https://gitlab.freedesktop.org/upower/power-profiles-daemon

This daemon is pretty widespread. It's the component used by Gnome and
KDE to manage the power profiles. The power management daemon is a
pretty important software component for laptops and other
battery-powered devices.

We're using the daemon DBus interface to:

- Fetch the available power profiles.
- Track the active power profile.
- Change the active power profile.

The original author recently gave up maintenance on the project. The
Upower group took over the maintenance burden… …and created a new
DBus name for the project. The old name is still advertised for now.
We use the old name for compatibility sake: most distributions did not
release 0.20, which introduces this new DBus name. We'll likely revisit
this in the future and point to the new bus name. See the inline
comment for more details.

Given how widespread this daemon is, I activated the module in the
default configuration.
@picnoir picnoir force-pushed the pic/power-profiles-daemon branch 3 times, most recently from 225f20f to f99f08b Compare February 26, 2024 14:23
@Alexays
Copy link
Owner

Alexays commented Feb 27, 2024

When adding new feature like this, a new entry in the man and wiki with module settings like others is ok :)

@picnoir
Copy link
Contributor Author

picnoir commented Feb 27, 2024

Thanks for the directions. I'll write the manpage section by the end of the week. (and fix the clang-tidy issues)

I'm a bit puzzled by the freebsd CI failure. Is it expected?

@lheckemann
Copy link

lheckemann commented Feb 29, 2024

Thanks for implementing this, I've wanted it for a while!

It would be nice to have the format string be configurable; the full profile names are a bit wide to fit neatly in my already crowded status bar, so I'd like to be able to use just 🌱 / ⚖️ / ⚡ for example.

@alebastr
Copy link
Contributor

I'm a bit puzzled by the freebsd CI failure. Is it expected?

FreeBSD CI is flaky. It works via qemu vm in a macOS virtual machine, and there are random connectivity problems or timeouts. The actual status in the log is fine, so please ignore the failure.

It would be nice to have the format string be configurable; the full profile names are a bit wide to fit neatly in my already crowded status bar, so I'd like to be able to use just 🌱 / ⚖️ / ⚡ for example.

I'd agree with that and even suggest to use the icons by default. For a module enabled in the default config, it takes too much screen space.
Although fontawesome icons would be a better choice due to an ability to set the color with CSS.

@alebastr
Copy link
Contributor

alebastr commented Mar 1, 2024

It appears that this PR crashes instantly if you don't have p-p-d installed.

While investigating, I found a wonderful fact: g_dbus_proxy_new_* family of APIs does not consider missing service an error. I.e. successful return from the constructor doesn't mean that it's safe to touch cached properties (profilesVariant contains nullptr with obvious consequences) or call remote methods. And every public method in the module should check that the initialization was completeld (update() crashes on the first line while attempting to dereference an empty iterator).
Also, create_for_bus_sync or really anything *_sync can block for 25 seconds, but that's not as exciting compared to the above.

So the module init should look like

  • create_for_bus (async version)
  • call("org.freedesktop.DBus.Properties.GetAll") to force peer status check
  • if the chain of async operations succeeded and the necessary properties are available, set the readiness flag and show the module

...I think I need to schedule some time to review existing DBus modules.

We move to a single icon label format to save space on the bar. We
still display the profile name and the driver in the tooltip.
picnoir added a commit to picnoir/Waybar that referenced this pull request Mar 1, 2024
2 changes to address the review feedback:

1. Aleksei pointed out in this
   comment (Alexays#2971 (comment))
   that there's no way to tell if a proxy is alive other than trying to
   call a method on it. We perform a little dance to check whether or
   not power-profiles-daemon is available on the system by calling
   properties.GetAll. If something responds, we assume
   power-profiles-daemon is installed, it's then safe to draw the
   widget and attach the callback to the active profile.
2. We replaced all the synchronous DBus operations by their async
   counterparts.
@picnoir
Copy link
Contributor Author

picnoir commented Mar 1, 2024

Thanks for the detailed feedback @alebastr, it was super useful!

From a (very) occasional C++ developper and Glib newbie perspective, this DBus API is pretty confusing and painful to use. :/

I adressed @lheckemann and @alebastr feedback wrt. the label formatting in 61fed6a. We're now only displaying a small icon in the default config. Everything, can be customized.

I addressed #2971 (comment) in 901314a.

I'll write the man page this EU afternoon.

2 changes to address the review feedback:

1. Aleksei pointed out in this
   comment (Alexays#2971 (comment))
   that there's no way to tell if a proxy is alive other than trying to
   call a method on it. We perform a little dance to check whether or
   not power-profiles-daemon is available on the system by calling
   properties.GetAll. If something responds, we assume
   power-profiles-daemon is installed, it's then safe to draw the
   widget and attach the callback to the active profile.
2. We replaced all the synchronous DBus operations by their async
   counterparts.
@picnoir picnoir force-pushed the pic/power-profiles-daemon branch 2 times, most recently from 29d6eaa to b620eb9 Compare March 1, 2024 14:11
@picnoir
Copy link
Contributor Author

picnoir commented Mar 1, 2024

The manpage is ready.

PTAL

Copy link
Contributor

@alebastr alebastr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the DBus safety comments! Looks correct and works for me both with and without daemon.

I left a few more comments, mostly style nits and redundant code.

// In the 80000 version of fmt library authors decided to optimize imports
// and moved declarations required for fmt::dynamic_format_arg_store in new
// header fmt/args.h
#if (FMT_VERSION >= 80000)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Waybar requires fmt >= 8.1.1, this check is not necessary in a new code.

Comment on lines 20 to 24
if (config_["format"].isString()) {
format_ = config_["format"].asString();
} else {
format_ = "{icon}";
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ALabel::ALabel will do that for you if you pass the correct default format string to the constructor.

// Current CSS class applied to the label
std::string currentStyle_;
// Format strings
std::string labelFormat_;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

labelFormat_ is unused

Comment on lines 62 to 64
if (powerProfileChangeSignal_.connected()) {
powerProfileChangeSignal_.disconnect();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For any connections made with sigc::mem_fun where the object inherits sigc::trackable (base class for the modules does), this will happen automatically. You don't need to track or disconnect the connection.

powerProfileChangeSignal_.disconnect();
}
if (powerProfilesProxy_) {
powerProfilesProxy_.reset();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RefPtr already does that during destruction.

}

void PowerProfilesDaemon::setPropCb(Glib::RefPtr<Gio::AsyncResult>& r) {
auto _ = powerProfilesProxy_->call_finish(r);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, check for exceptions here.


bool PowerProfilesDaemon::handleToggle(GdkEventButton* const& e) {
if (connected_) {
if (e->type == GdkEventType::GDK_BUTTON_PRESS && powerProfilesProxy_) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check for powerProfilesProxy_ could be redundant if you already check connected_

auto pred = [str](Profile const& p) { return p.name == str; };
this->activeProfile_ = std::find_if(availableProfiles_.begin(), availableProfiles_.end(), pred);
if (activeProfile_ == availableProfiles_.end()) {
throw std::runtime_error("FATAL, can't find the active profile in the available profiles list");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure throwing exception is necessary here. It will be caught by Glibmm exception handler anyways, with no consequences.
You can just log the error and hide the module on the next update.

VarStr activeProfileVariant = VarStr::create(activeProfile_->name);
auto callArgs = SetPowerProfileVar::create(
std::make_tuple("net.hadess.PowerProfiles", "ActiveProfile", activeProfileVariant));
auto container = Glib::VariantBase::cast_dynamic<Glib::VariantContainerBase>(callArgs);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual cast might be unnecessary

Comment on lines 124 to 125
profile = {name, driver};
availableProfiles_.push_back(profile);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

availableProfiles_.emplace_back(name, driver); would avoid one temporary object.

And if you move name/driver declaration into the loop (which you might want to do anyways to avoid accidentally reusing values from previous iterations), it'll be safe to write if (!name.empty()) { availableProfiles_.emplace_back(std::move(name), std::move(driver)); }

@picnoir
Copy link
Contributor Author

picnoir commented Mar 1, 2024

Thanks for having taken the time to write these detailed explanations. Learning a lot here :)

Addressed these "nits" in the above commit.

Don't hesitate to squash all these if you feel like it.

Copy link
Contributor

@alebastr alebastr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Thanks for implementing this feature!

include/modules/power_profiles_daemon.hpp Outdated Show resolved Hide resolved
src/modules/power_profiles_daemon.cpp Show resolved Hide resolved
@picnoir
Copy link
Contributor Author

picnoir commented Mar 1, 2024

Loosely related note: there's a quite a few synchronous dbus calls throughout the codebase:

~/code-root/github.com/Alexays/Waybar (pic/power-profiles-daemon) » ag call_sync
src/util/portal.cpp
50:    response = call_sync(std::string(PORTAL_INTERFACE) + ".Read", params);

src/util/backlight_backend.cpp
223:  login_proxy_->call_sync("SetBrightness", call_args);

src/modules/systemd_failed_units.cpp
77:      Glib::VariantContainerBase data = proxy->call_sync("Get", parameters);

src/modules/gamemode.cpp
130:      Glib::VariantContainerBase data = gamemode_proxy->call_sync("Get", parameters);

~/code-root/github.com/Alexays/Waybar (pic/power-profiles-daemon) » ag bus_sync
src/util/backlight_backend.cpp
105:  login_proxy_ = Gio::DBus::Proxy::create_for_bus_sync(

src/modules/systemd_failed_units.cpp
31:    system_proxy = Gio::DBus::Proxy::create_for_bus_sync(
40:    user_proxy = Gio::DBus::Proxy::create_for_bus_sync(

src/modules/bluetooth.cpp
22:  GDBusObjectManager* manager = g_dbus_object_manager_client_new_for_bus_sync(
28:    spdlog::error("g_dbus_object_manager_client_new_for_bus_sync() failed: {}", error->message);

src/modules/gamemode.cpp
90:  gamemode_proxy = Gio::DBus::Proxy::create_for_bus_sync(Gio::DBus::BusType::BUS_TYPE_SESSION,

There was no way to display the default value of format-icons without
breaking the table :(
@picnoir
Copy link
Contributor Author

picnoir commented Mar 2, 2024

I just pushed a small visual fix. The fontawesome icons were not really centered in the box in the default config. I feel like there's something wrong with the calculation of the icon width in the typesetter code.

Added a bit of padding-left to center everything.

@alebastr
Copy link
Contributor

alebastr commented Mar 2, 2024

You also accidentally reverted the last set of the review fixes :)

Would it make sense to use power-profiles-daemon (kebab-case) in the configuration? It's a bit awkward that the CSS element name differs from the config name.

@alebastr
Copy link
Contributor

alebastr commented Mar 2, 2024

Loosely related note: there's a quite a few synchronous dbus calls throughout the codebase:

Yep, I did review all of those.

  • Systemd interfaces aren't affected by DBus activation issues and will not block.
  • Portal interface would break GTK if the DBus service is not available (all those "Waybar takes 25 seconds to start" issues in the tracker). So we already require a correct configuration for this one.
  • bluetooth and gamemode are off by default.

But here's a snippet that simulates a problem with gamemode service activation, blocks startup of waybar for the duration of a DBus timeout and then causes crash on invalid memory access. Fun.

% mkdir -p ~/.local/share/dbus-1/services
% cat >~/.local/share/dbus-1/services/com.feralinteractive.GameMode.service <<EOF
[D-BUS Service]
Name=com.feralinteractive.GameMode
Exec=/usr/bin/sleep 30
EOF

Adding :
- A missing try/catch
- Glib::Error catch
- Remove the useless destructor
- Populate the profiles vector more efficiently
- Numerous nits
The icon is not really centered in the box. This is likely coming from
a bogus glyph width calculation. It's not a big deal, but that's not
really pleasant aesthetically-wise.

Adding a bit of right padding makes it much more pleasant to watch. It
does not really disrupt a wider display form, like one that
explicitely writes the active profile.
@picnoir
Copy link
Contributor Author

picnoir commented Mar 2, 2024

You also accidentally reverted the last set of the review fixes :)

🤦 fixed

Would it make sense to use power-profiles-daemon (kebab-case) in the configuration? It's a bit awkward that the CSS element name differs from the config name.

Yes, I also prefer that. The daemon itself is spelled using kebab cas.

I originally used snake-case seeing it used for idle_inhibitor, thinking it was the local style. But we actually already have some kebab-case names for some other modules (keyboard-state, systemd-failed-units).

power_profiles_daemon => power-profiles-daemon
@Alexays
Copy link
Owner

Alexays commented Mar 4, 2024

LGTM, can you also update the github wiki? :)
Thanks for this nice module.

@Alexays Alexays merged commit 3806075 into Alexays:master Mar 4, 2024
@picnoir picnoir deleted the pic/power-profiles-daemon branch March 4, 2024 13:38
@picnoir
Copy link
Contributor Author

picnoir commented Mar 4, 2024

🎉

I created the module wiki page there: https://github.com/Alexays/Waybar/wiki/Module:-PowerProfilesDaemon

@leiserfg
Copy link
Contributor

@picnoir thanks a lot for this feature! Would it make sense to make it so scrolling up and down cycles up and down? Or right-click cycles down (to keep the symmetry with the current behavior)?

@picnoir
Copy link
Contributor Author

picnoir commented Mar 18, 2024 via email

@leiserfg
Copy link
Contributor

Changing the container is not required, std::vector has a bidirectional iterator as well, I will make a pr for it.

@leiserfg
Copy link
Contributor

#3032

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants