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

Add KDE support (org.freedesktop.PowerManagement.Inhibit) #324

Merged
merged 22 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/source/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## wakepy 0.9.0
🗓️ unreleased

### ✨ Features
- Support keep.running mode in KDE Plasma 5.12.90 and newer through the [org.freedesktop.PowerManagement](#keep-running-org-freedesktop-powermanagement) method. It may also be used on other DEs which implement this older freedesktop.org D-Bus interface (but not Xcfe). ([#324](https://github.com/fohrloop/wakepy/pull/324))

### 📖 Documentation
- Document that the [org.freedesktop.ScreenSaver](keep-presenting-org-freedesktop-screensaver) method for keep.presenting mode also supports KDE Plasma. ([#324](https://github.com/fohrloop/wakepy/pull/324))

## wakepy 0.8.0
🗓️ 2024-05-26

Expand Down
14 changes: 14 additions & 0 deletions docs/source/methods-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ Methods are different ways of entering in (or keeping a) Mode. A Method may supp
If used hundreds or thousands of times, may slow down system. See: [wakepy/#277](https://github.com/fohrloop/wakepy/issues/277)
````


(keep-running-org-freedesktop-powermanagement)=
### org.freedesktop.PowerManagement
- **Name**: `org.freedesktop.PowerManagement`
- **Introduced in**: wakepy 0.9.0
- **How it works**: Uses the Inhibit method of the org.freedesktop.PowerManagement D-Bus service when activating and saves the returned cookie on the Method instance. Uses UnInhibit method of the same service with the cookie when deactivating. The org.freedesktop.PowerManagement is an obsolete spec, but certain Desktop Environments provide that as the only option for inhibiting the suspend action. The documentation was previously hosted on [freedesktop.org](https://www.freedesktop.org/wiki/Specifications/power-management-spec/) but the links on the page are dead. The spec has three versions: 0.1, 0.2 and 0.3. The 0.3 is not found anywhere but the version 0.2 of the spec can be read in the [Internet Archive](https://web.archive.org/web/20090417010057/http://people.freedesktop.org/~hughsient/temp/power-management-spec-0.2.html)
- **Multiprocess safe?**: Yes
- **What if the process holding the lock dies?**: The lock is automatically removed.
- **How to check it?**: You may check if there are *any* inhibitors using the HasInhibit method of the `/org/freedesktop/PowerManagement/Inhibit` object on the `org.freedesktop.PowerManagement.Inhibit` interface. Note that updating the inhibit flag from `false` to `true` may take a few seconds. A good tool for this is [D-Spy](https://apps.gnome.org/Dspy/). Alternatively, you could monitor your inhibit call with [`dbus-monitor`](https://dbus.freedesktop.org/doc/dbus-monitor.1.html).
- **Requirements**: D-Bus + KDE Plasma >=5.12.90 or other DE which implements this older freedesktop D-Bus interface. Exception: Xfce is not supported.
- **About unsupported DEs** Older versions of KDE Plasma had a bug which also prevented the screenlock/screesaver activation. The bug was fixed in [D11182](https://phabricator.kde.org/D11182), commit [152400c1b688](https://phabricator.kde.org/R122:152400c1b6880506ee1395011686c2b191f419a0) and was part of the KDE Plasma to 5.12.90 ( = 5.13 Beta) [release](https://kde.org/announcements/changelogs/plasma/5/5.12.5-5.12.90/). This Method is not supported either on Xfce as it has similar bug which prevents also the automatic screenlock/screensaver (See: [xfce4-power-manager/#65](https://gitlab.xfce.org/xfce/xfce4-power-manager/-/issues/65)), hence, wakepy refuses to use org.freedesktop.PowerManagement as method for keep.running mode on KDE < 5.12.90 and on any version of Xfce.
- **Tested on**: openSUSE 15.5 with KDE Plasma 5.27.9 ([Comment in #310](https://github.com/fohrloop/wakepy/issues/310#issuecomment-2140156882) by [fohrloop](https://github.com/fohrloop/)).


(keep-running-windows-stes)=
### SetThreadExecutionState

Expand Down
219 changes: 190 additions & 29 deletions src/wakepy/methods/freedesktop.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from __future__ import annotations

import os
import re
import subprocess
import typing

from wakepy.core import (
Expand All @@ -15,39 +18,29 @@
)

if typing.TYPE_CHECKING:
from typing import Optional
from typing import Optional, Tuple

XDG_SESSION_DESKTOP = "XDG_SESSION_DESKTOP"
"""The environment variable for the xdg desktop of the current session. Defined
in pam_systemd manual[1].

class FreedesktopScreenSaverInhibit(Method):
"""Method using org.freedesktop.ScreenSaver D-Bus API
Note that there's also XDG_CURRENT_DESKTOP which contains a colon-separated
list of DEs, defined in [2]

https://people.freedesktop.org/~hadess/idle-inhibition-spec/re01.html
"""
[1]: https://www.freedesktop.org/software/systemd/man/pam_systemd.html
[2]: https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html
"""

name = "org.freedesktop.ScreenSaver"
mode_name = ModeName.KEEP_PRESENTING
KDE = "KDE"
"""Constant for KDE desktop environment / KDE Plasma"""
XFCE = "XFCE"
"""Constant for Xfce desktop environemtn"""

screen_saver = DBusAddress(
bus=BusType.SESSION,
service="org.freedesktop.ScreenSaver",
path="/org/freedesktop/ScreenSaver",
interface="org.freedesktop.ScreenSaver",
)

method_inhibit = DBusMethod(
name="Inhibit",
signature="ss",
params=("application_name", "reason_for_inhibit"),
output_signature="u",
output_params=("cookie",),
).of(screen_saver)

method_uninhibit = DBusMethod(
name="UnInhibit",
signature="u",
params=("cookie",),
).of(screen_saver)
class FreedesktopInhibitorWithCookieMethod(Method):
"""Base class for freedesktop.org D-Bus based methods."""

service_dbus_address: DBusAddress
supported_platforms = (PlatformName.LINUX,)

def __init__(self, **kwargs: object) -> None:
Expand All @@ -65,9 +58,7 @@ def enter_mode(self) -> None:

retval = self.process_dbus_call(call)
if retval is None:
raise RuntimeError(
"Could not get inhibit cookie from org.freedesktop.ScreenSaver"
)
raise RuntimeError(f"Could not get inhibit cookie from {self.name}")
self.inhibit_cookie = retval[0]

def exit_mode(self) -> None:
Expand All @@ -81,3 +72,173 @@ def exit_mode(self) -> None:
)
self.process_dbus_call(call)
self.inhibit_cookie = None

@property
def method_inhibit(self) -> DBusMethod:
return DBusMethod(
name="Inhibit",
signature="ss",
params=("application_name", "reason_for_inhibit"),
output_signature="u",
output_params=("cookie",),
).of(self.service_dbus_address)

@property
def method_uninhibit(self) -> DBusMethod:
return DBusMethod(
name="UnInhibit",
signature="u",
params=("cookie",),
).of(self.service_dbus_address)


class FreedesktopScreenSaverInhibit(FreedesktopInhibitorWithCookieMethod):
"""Method using org.freedesktop.ScreenSaver D-Bus API

https://people.freedesktop.org/~hadess/idle-inhibition-spec/re01.html
"""

name = "org.freedesktop.ScreenSaver"
mode_name = ModeName.KEEP_PRESENTING

service_dbus_address = DBusAddress(
bus=BusType.SESSION,
service="org.freedesktop.ScreenSaver",
path="/org/freedesktop/ScreenSaver",
interface="org.freedesktop.ScreenSaver",
)


class FreedesktopPowerManagementInhibit(FreedesktopInhibitorWithCookieMethod):
"""Method using org.freedesktop.PowerManagement D-Bus API

According to [1] and [2] this might be obsolete. The spec itself can be
read in the internet arhives[3]. Part of the spec (v0.2.0) copied here for
convenience:

DBUS Interface: org.freedesktop.PowerManagement.Inhibit
DBUS Path: /org/freedesktop/PowerManagement/Inhibit

When the power manager detects an idle session and system, it can perform a
system suspend or hibernate, known as an idle sleep action. We can prevent
the session power manager daemon from doing this action using the inhibit
interface.

An automatic inhibit should be taken by the file manager if there is a slow
network copy that will take many minutes to complete.

A cookie is a randomly assigned 32bit unsigned integer used to identify the
inhibit. It is required as the same application may want to call inhibit
multiple times, without managing the inhibit calls itself.

Name Input Output Error Description
------- ------------------ ------- ------ -----------
Inhibit string application uint cookie PermissionDenied [D1]
string reason
UnInhibit uint cookie CookieNotFound [D2]
HasInhibit bool has_inhibit [D3]

[D1] Inhibits the computer from performing an idle sleep action. Useful if
you want to do an operation of long duration without the computer
suspending. Reason and application should be translated strings where
possible.

[D2] Allows the computer to perform the idle sleep or user action if the
number of inhibit calls is zero. If there are multiple cookies outstanding,
clearing one cookie does not allow the action to happen. If the program
holding the cookie exits from the session bus without calling UnInhibit()
then it is automatically removed.

[D3] Returns false if we have no valid inhibits. This will return true if
the number of inhibit cookies is greater than zero.

[1] https://code.videolan.org/videolan/vlc/-/issues/25785
[2] https://www.freedesktop.org/wiki/Specifications/power-management-spec/
[3] https://web.archive.org/web/20090417010057/http://people.freedesktop.org/~hughsient/temp/power-management-spec-0.2.html

"""

name = "org.freedesktop.PowerManagement"
mode_name = ModeName.KEEP_RUNNING

service_dbus_address = DBusAddress(
bus=BusType.SESSION,
service="org.freedesktop.PowerManagement",
path="/org/freedesktop/PowerManagement/Inhibit",
interface="org.freedesktop.PowerManagement.Inhibit",
)

_min_kde_plasma_version = (5, 12, 90)
"""The minimum KDE Plasma version which supports this method.

In earlier versions of KDE Plasma, there was a bug which caused the
PowerManagement.Inhibit to behave similarly to the
org.freedesktop.ScreenSaver interface. This was fixed in commit
152400c1b6880506ee1395011686c2b191f419a0 which was part of KDE Plasma
5.12.90.
"""

def caniuse(self) -> bool | None | str:

current_de = _get_current_desktop_environment()

if current_de == KDE:
kde_version = _get_kde_plasma_version()

if kde_version is None:
raise RuntimeError(
"Running on KDE but could not detect KDE Plasma version."
)

if kde_version < self._min_kde_plasma_version:
min_version_str = ".".join(str(x) for x in self._min_kde_plasma_version)
raise RuntimeError(
(f"{self.name} only supports KDE >= {min_version_str}")
)
# KDE Plasma with a supported version
return True

elif current_de == XFCE:
raise RuntimeError(
"org.freedesktop.PowerManagemen does not support XFCE as it has a bug "
"which prevents automatic screenlock / screensaver. See: "
"https://gitlab.xfce.org/xfce/xfce4-power-manager/-/issues/65"
)

# Other DEs
return True


def _get_current_desktop_environment() -> str | None:
"""Get the desktop environment of the current session. If the DE cannot be
determined, return None"""
if XDG_SESSION_DESKTOP not in os.environ:
return None

de_from_env_var = os.environ[XDG_SESSION_DESKTOP]
if de_from_env_var.upper() == KDE:
return KDE
elif de_from_env_var.upper() == XFCE:
return XFCE
return de_from_env_var


def _get_kde_plasma_version() -> Optional[Tuple[int, ...]]:
"""Get the KDE Plasma version as tuple.

Returns
-------
versiontuple:
The detected KDE Plasma version. For example: (5,27,9)
If no KDE Plasma is found, returns None.

"""
# returns for example 'plasmashell 5.27.9'
out = subprocess.getoutput("plasmashell --version")
mtch = re.match("plasmashell ([0-9][0-9.]*)$", out)
if mtch is None:
return None

versionstring = mtch.group(1)
versiontuple = tuple(int(x) for x in versionstring.split("."))
return versiontuple
Loading