Poll the active power profile over D-Bus instead of powerprofilesctl

The battery service ran `powerprofilesctl get` every two seconds to keep the
active profile visible to the wallpaper and lock services. That command is a
PyGObject script, so the shell spawned a Python interpreter for it tens of
thousands of times a day. Roughly once a day one of those exits into a CPython
3.14 finalization race (python/cpython#124619): the GLib D-Bus worker thread
calls PyGILState_Ensure after the interpreter is torn down and the process
dies with SIGSEGV, leaving a core dump and a crash notification behind.

Read the ActiveProfile property straight from power-profiles-daemon with
busctl, the same way omarchy-powerprofiles-set already reads UPower. The
output is JSON, so an empty or malformed reply when the daemon is not running
still reads as no active profile, matching the previous behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gbfh4Mi9dK6SAd1P2xMTi
This commit is contained in:
David Heinemeier HanssonandClaude Fable 5.1 committed 2026-09-13 08:56:39 +02:00
1 parent 31bd80daa4
commit 55799c4ab0
2 files changed
+23 -6

No files matched your search

+21 -5
View File
@@ -59,6 +59,16 @@ Item {
if (!powerProfileReadProcess.running) powerProfileReadProcess.running = true
}
function parseActiveProfile(text) {
// busctl --json=short prints {"type":"s","data":"balanced"}; an empty or
// malformed reply (daemon not running) reads as no active profile.
try {
return String(JSON.parse(text).data || "").trim()
} catch (e) {
return ""
}
}
Process { id: warningProcess }
Process {
@@ -71,17 +81,23 @@ Item {
Process {
id: powerProfileReadProcess
command: ["powerprofilesctl", "get"]
// Read the property straight from the daemon rather than via
// `powerprofilesctl get`. That is a PyGObject script, and spawning a Python
// interpreter for it every two seconds trips a CPython 3.14 shutdown race
// (python/cpython#124619): the GLib D-Bus worker thread re-enters the
// interpreter after finalization and the process dies with SIGSEGV,
// leaving a core dump and a crash notification behind roughly daily.
command: ["busctl", "--json=short", "get-property", "net.hadess.PowerProfiles", "/net/hadess/PowerProfiles", "net.hadess.PowerProfiles", "ActiveProfile"]
stdout: StdioCollector {
waitForEnd: true
onStreamFinished: root.activePowerProfile = String(text || "").trim()
onStreamFinished: root.activePowerProfile = root.parseActiveProfile(text)
}
}
Timer {
// powerprofilesctl has no portable monitor subcommand; keep profile changes
// visible to consumers such as the wallpaper service without requiring the
// power panel to be open.
// There is no portable way to subscribe to profile changes from QML; keep
// them visible to consumers such as the wallpaper service without requiring
// the power panel to be open.
interval: 2000
running: true
repeat: true