Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0234456af8
|
||
|
|
51e18766a4
|
||
|
|
fa882bb0d7
|
||
|
|
af82b10d3c
|
||
|
|
1b0f2a2a66
|
@@ -3,11 +3,11 @@
|
|||||||
# Maintainer: Daniele Fucini <dfucini [at] gmail [dot] com>
|
# Maintainer: Daniele Fucini <dfucini [at] gmail [dot] com>
|
||||||
|
|
||||||
pkgname=xautolock-inhibit
|
pkgname=xautolock-inhibit
|
||||||
pkgver=0.1
|
pkgver=0.1.2
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc='Honor org.freedesktop.ScreenSaver inhibit requests by disabling xautolock'
|
pkgdesc='Honor org.freedesktop.ScreenSaver inhibit requests by disabling xautolock'
|
||||||
arch=('any')
|
arch=('any')
|
||||||
url="https://git.shouldnt.work/fuxino/${pkgname}"
|
url="https://git.shouldnt.work/fuxino/$pkgname"
|
||||||
license=('GPL-3.0-or-later')
|
license=('GPL-3.0-or-later')
|
||||||
depends=('libx11'
|
depends=('libx11'
|
||||||
'libxss'
|
'libxss'
|
||||||
@@ -15,13 +15,14 @@ depends=('libx11'
|
|||||||
'python-gobject'
|
'python-gobject'
|
||||||
'xautolock')
|
'xautolock')
|
||||||
makedepends=('git')
|
makedepends=('git')
|
||||||
source=(git+${url}?signed#tag=${pkgver})
|
source=("git+$url?signed#tag=$pkgver")
|
||||||
validpgpkeys=('7E12BC1FF3B6EDB2CD8053EB981A2B2A3BBF5514')
|
validpgpkeys=('7E12BC1FF3B6EDB2CD8053EB981A2B2A3BBF5514')
|
||||||
sha256sums=('SKIP')
|
sha256sums=('6713c706e422cea0ae346fc7bd16611dd0e32487acf4d15ec3a4db325eaaa0c7')
|
||||||
|
|
||||||
package()
|
package()
|
||||||
{
|
{
|
||||||
cd "${srcdir}/${pkgname}"
|
cd "$srcdir"/"$pkgname"
|
||||||
install -Dm755 "${pkgname}" "${pkgdir}/usr/bin/${pkgname}"
|
install -Dm755 "$pkgname" "$pkgdir"/usr/bin/"$pkgname"
|
||||||
install -Dm644 "${pkgname}.service" "${pkgdir}/usr/lib/systemd/user/${pkgname}.service"
|
install -Dm644 "$pkgname".service "$pkgdir"/usr/lib/systemd/user/"$pkgname".service
|
||||||
|
install -Dm644 README.md "$pkgdir"/usr/share/doc/"$pkgname"/README.md
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# xautolock-inhibit
|
||||||
|
|
||||||
|
Makes screen locking with `xautolock` respect the "don't lock the screen" requests that apps send, on X11 sessions without a desktop environment (for example a plain window manager like xmonad).
|
||||||
|
|
||||||
|
Browsers, Steam, gamemode and other apps ask the desktop not to lock or blank the screen while a video or game is running, through the D-Bus service `org.freedesktop.ScreenSaver`. Without a desktop environment nothing provides that service, so the requests are dropped and the screen locks in the middle of a video. xautolock-inhibit provides it:
|
||||||
|
|
||||||
|
- While at least one request is active, it runs `xautolock -disable` and pauses X screen blanking and DPMS.
|
||||||
|
- When the last request is released, it waits a few seconds, then runs `xautolock -enable` and resumes blanking.
|
||||||
|
|
||||||
|
xautolock stays your locker; xautolock-inhibit only switches it off and on.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- Owns `org.freedesktop.ScreenSaver` on the session bus at both `/org/freedesktop/ScreenSaver` and `/ScreenSaver` (apps differ on the path), and implements `Inhibit` and `UnInhibit`.
|
||||||
|
- Tracks requests per D-Bus client. If a client disconnects without releasing them (for example a browser crash), its requests are released.
|
||||||
|
- Runs `xautolock` only when the number of active requests goes from 0 to 1 or back to 0. Re-enabling waits a few seconds (`--release-delay`), because browsers send short bursts of requests when a video starts.
|
||||||
|
- Pauses X blanking with `XScreenSaverSuspend`, the same mechanism mpv uses, instead of changing `xset` settings. The X server ends the pause by itself if the daemon exits or crashes.
|
||||||
|
- Requests whose reason is on an ignore list (default: `Playing audio`) are accepted and logged, but don't block locking. Firefox-based browsers send one for audio playback, which would otherwise keep the screen unlocked while music plays with nobody at the computer.
|
||||||
|
- On exit (including SIGTERM), it re-enables xautolock. The systemd unit also runs `xautolock -enable` when it stops, as a fallback.
|
||||||
|
- If another program (a desktop environment) already owns `org.freedesktop.ScreenSaver`, it exits with status 2 instead of taking over.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- `xautolock`, already running in the session
|
||||||
|
- `python`, `python-gobject`
|
||||||
|
- `libx11`, `libxss`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Build and install the package with the included PKGBUILD:
|
||||||
|
|
||||||
|
```
|
||||||
|
makepkg -si
|
||||||
|
```
|
||||||
|
|
||||||
|
Then start it from your window manager's startup, after `xautolock`. For xmonad:
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
spawnOnce "xautolock -time 10 -locker slock -detectsleep"
|
||||||
|
spawnOnce "systemctl --user start xautolock-inhibit"
|
||||||
|
```
|
||||||
|
|
||||||
|
The unit has no `[Install]` section on purpose. Enabling it at login would also start it in a desktop environment, or before X is up; starting it from the window manager limits it to that session. The user service manager needs `DISPLAY` (and `XAUTHORITY`) in its environment, which `/etc/X11/xinit/xinitrc.d/50-systemd-user.sh` sets up for `startx` sessions.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
xautolock-inhibit [-i REASON] [--count-all] [-d SECONDS]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `-i`, `--ignore-reason REASON`: request reason that doesn't block locking. Can be repeated; replaces the default (`Playing audio`).
|
||||||
|
- `--count-all`: every request blocks locking; no reasons are ignored.
|
||||||
|
- `-d`, `--release-delay SECONDS`: wait this long after the last release before re-enabling xautolock (default 5).
|
||||||
|
|
||||||
|
To change the options used by the service:
|
||||||
|
|
||||||
|
```
|
||||||
|
systemctl --user edit xautolock-inhibit.service
|
||||||
|
```
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Service]
|
||||||
|
ExecStart=
|
||||||
|
ExecStart=/usr/bin/xautolock-inhibit --count-all
|
||||||
|
```
|
||||||
|
|
||||||
|
Every request, release and xautolock call is logged to the journal:
|
||||||
|
|
||||||
|
```
|
||||||
|
journalctl --user -u xautolock-inhibit
|
||||||
|
```
|
||||||
|
|
||||||
|
## App notes
|
||||||
|
|
||||||
|
- **Sandboxed apps** may be blocked from reaching the service. For a firejail profile that filters D-Bus (`dbus-user filter`, as the Firefox-based profiles do), allow it in the app's `.local` file, e.g. `zen-browser.local`:
|
||||||
|
|
||||||
|
```
|
||||||
|
dbus-user.talk org.freedesktop.ScreenSaver
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Games**: Steam's overlay and gamemode (`inhibit_screensaver=1`) send requests on their own.
|
||||||
|
- **scrcpy** only sends a request when started with `--disable-screensaver`.
|
||||||
|
- **mpv** talks to the X server directly and doesn't need this daemon.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- Restarting the daemon loses the requests it holds; apps don't send them again. Avoid restarting it while a video or game is running.
|
||||||
|
- `SimulateUserActivity` is accepted but does nothing. Calls are logged once when a client starts sending them and once, with a count, after 5 minutes without any.
|
||||||
|
- `GetActive` always returns `false`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPL-3.0-or-later. See [LICENSE](LICENSE).
|
||||||
+48
-3
@@ -10,6 +10,9 @@ import itertools
|
|||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from gi.repository import Gio, GLib
|
from gi.repository import Gio, GLib
|
||||||
|
|
||||||
@@ -22,6 +25,7 @@ except ImportError:
|
|||||||
BUS_NAME = "org.freedesktop.ScreenSaver"
|
BUS_NAME = "org.freedesktop.ScreenSaver"
|
||||||
OBJECT_PATHS = ("/org/freedesktop/ScreenSaver", "/ScreenSaver") # Vivaldi uses the first, Zen the second
|
OBJECT_PATHS = ("/org/freedesktop/ScreenSaver", "/ScreenSaver") # Vivaldi uses the first, Zen the second
|
||||||
EXIT_NAME_TAKEN = 2
|
EXIT_NAME_TAKEN = 2
|
||||||
|
ACTIVITY_QUIET = 300 # seconds without SimulateUserActivity that end a logged burst
|
||||||
|
|
||||||
INTROSPECTION = """
|
INTROSPECTION = """
|
||||||
<node>
|
<node>
|
||||||
@@ -105,6 +109,14 @@ class XBlanking:
|
|||||||
log(f"X blanking {'paused' if on else 'resumed'}")
|
log(f"X blanking {'paused' if on else 'resumed'}")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ActivityBurst:
|
||||||
|
first: float
|
||||||
|
last: float
|
||||||
|
calls: int = 0
|
||||||
|
timer: int = 0
|
||||||
|
|
||||||
|
|
||||||
class Inhibitor:
|
class Inhibitor:
|
||||||
def __init__(self, ignored_reasons, release_delay, blanking):
|
def __init__(self, ignored_reasons, release_delay, blanking):
|
||||||
self.ignored_reasons = ignored_reasons
|
self.ignored_reasons = ignored_reasons
|
||||||
@@ -113,6 +125,7 @@ class Inhibitor:
|
|||||||
self.cookies = {} # cookie -> (sender, application_name, reason, counts)
|
self.cookies = {} # cookie -> (sender, application_name, reason, counts)
|
||||||
self.next_cookie = itertools.count(1)
|
self.next_cookie = itertools.count(1)
|
||||||
self.proc_names = {} # sender -> "comm[pid]", for logging only
|
self.proc_names = {} # sender -> "comm[pid]", for logging only
|
||||||
|
self.activity: dict[str, ActivityBurst] = {} # sender -> SimulateUserActivity burst
|
||||||
self.disabled = False
|
self.disabled = False
|
||||||
self.release_timer = 0
|
self.release_timer = 0
|
||||||
self.conn = None
|
self.conn = None
|
||||||
@@ -150,7 +163,9 @@ class Inhibitor:
|
|||||||
self.blanking.suspend(False)
|
self.blanking.suspend(False)
|
||||||
self.disabled = False
|
self.disabled = False
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self) -> None:
|
||||||
|
for sender in list(self.activity):
|
||||||
|
self.end_activity(sender)
|
||||||
if self.release_timer:
|
if self.release_timer:
|
||||||
GLib.source_remove(self.release_timer)
|
GLib.source_remove(self.release_timer)
|
||||||
self.release_timer = 0
|
self.release_timer = 0
|
||||||
@@ -192,11 +207,41 @@ class Inhibitor:
|
|||||||
log(f"UnInhibit {cookie}: '{app}' '{reason}'")
|
log(f"UnInhibit {cookie}: '{app}' '{reason}'")
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def release_sender(self, sender):
|
def simulate_activity(self, sender: str) -> None:
|
||||||
|
"""Log a sender's calls once per burst: Steam sends thousands per hour."""
|
||||||
|
now = time.monotonic()
|
||||||
|
burst = self.activity.get(sender)
|
||||||
|
if burst is None:
|
||||||
|
log(f"SimulateUserActivity from {self.proc_name(sender)} (not implemented)")
|
||||||
|
burst = self.activity[sender] = ActivityBurst(first=now, last=now)
|
||||||
|
else:
|
||||||
|
GLib.source_remove(burst.timer)
|
||||||
|
burst.calls += 1
|
||||||
|
burst.last = now
|
||||||
|
burst.timer = GLib.timeout_add_seconds(ACTIVITY_QUIET, self.on_activity_quiet, sender)
|
||||||
|
|
||||||
|
def on_activity_quiet(self, sender: str) -> bool:
|
||||||
|
self.activity[sender].timer = 0
|
||||||
|
self.end_activity(sender)
|
||||||
|
return GLib.SOURCE_REMOVE
|
||||||
|
|
||||||
|
def end_activity(self, sender: str) -> None:
|
||||||
|
burst = self.activity.pop(sender, None)
|
||||||
|
if burst is None:
|
||||||
|
return
|
||||||
|
if burst.timer:
|
||||||
|
GLib.source_remove(burst.timer)
|
||||||
|
if burst.calls == 1:
|
||||||
|
return
|
||||||
|
span = timedelta(seconds=round(burst.last - burst.first))
|
||||||
|
log(f"SimulateUserActivity from {self.proc_name(sender)}: {burst.calls} calls over {span}")
|
||||||
|
|
||||||
|
def release_sender(self, sender: str) -> None:
|
||||||
gone = [c for c, (s, *_) in self.cookies.items() if s == sender]
|
gone = [c for c, (s, *_) in self.cookies.items() if s == sender]
|
||||||
for c in gone:
|
for c in gone:
|
||||||
_, app, reason, _ = self.cookies.pop(c)
|
_, app, reason, _ = self.cookies.pop(c)
|
||||||
log(f"Released {c}: '{app}' '{reason}' ({sender} left the bus)")
|
log(f"Released {c}: '{app}' '{reason}' ({sender} left the bus)")
|
||||||
|
self.end_activity(sender)
|
||||||
self.proc_names.pop(sender, None)
|
self.proc_names.pop(sender, None)
|
||||||
if gone:
|
if gone:
|
||||||
self.update()
|
self.update()
|
||||||
@@ -209,7 +254,7 @@ class Inhibitor:
|
|||||||
self.uninhibit(sender, params.unpack()[0])
|
self.uninhibit(sender, params.unpack()[0])
|
||||||
invocation.return_value(None)
|
invocation.return_value(None)
|
||||||
elif method == "SimulateUserActivity":
|
elif method == "SimulateUserActivity":
|
||||||
log(f"SimulateUserActivity from {self.proc_name(sender)} (not implemented)")
|
self.simulate_activity(sender)
|
||||||
invocation.return_value(None)
|
invocation.return_value(None)
|
||||||
elif method == "Lock":
|
elif method == "Lock":
|
||||||
log(f"Lock from {self.proc_name(sender)}")
|
log(f"Lock from {self.proc_name(sender)}")
|
||||||
|
|||||||
Reference in New Issue
Block a user