git.lucas.co / cce-system-interface
system settings
git clone https://git.lucas.co/cce-system-interface.git

commit0c319f2e64fe4186da2bb7374343348c836d6093
parent57e0b836c7
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-02 19:06
Re-apply the power plan after wake and when the source moves mid-run

A Mains change event that lands while cce-power-apply.service is still
running merges into that run instead of starting another. On 2026-10-02
a run started by an unplug as the lid closed was frozen through sleep,
finished on resume, and applied the battery mode it had read two hours
earlier; the plug-in's event merged into it, so the machine stayed in
power-saver (animations off) on AC until the charger was replugged.

`apply` now reads the source again after applying and re-applies if it
changed (up to four passes). cce-power-apply-resume.service runs the
applier after every sleep target, for firmware whose resume event is
missing; it carries X-CceEnable=yes so ccebuild install-system enables it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 cce-power-apply-resume.service | 28 +++++++++++++++++++++++
 src/bin/cce-power-apply.rs     | 50 +++++++++++++++++++++++++++++++++++-------
 udev/90-cce-power-apply.rules  |  8 ++++---
 3 files changed, 75 insertions(+), 11 deletions(-)

diff --git a/cce-power-apply-resume.service b/cce-power-apply-resume.service
new file mode 100644
index 0000000..840b8ea
--- /dev/null
+++ b/cce-power-apply-resume.service
@@ -0,0 +1,28 @@
+[Unit]
+Description=Re-apply the cce Power page's plan after waking
+Documentation=file:/etc/cce/power.kdl
+# The kernel's Mains change event is not a reliable record of a charger
+# plugged or pulled while the machine slept. Some firmware sends none, and
+# one that lands during the thaw merges into a cce-power-apply.service run
+# that sleep froze, which then applies what it read before sleeping. That
+# happened on 2026-10-02: the machine woke on AC in the battery mode, with
+# animations off. Each sleep target is reached only once the machine is
+# awake again, so ordering after them is "on resume".
+#
+# This runs the applier directly, not by starting cce-power-apply.service,
+# because the start would merge into that same frozen run. Two runs at once
+# are harmless: each reads the source again when it finishes.
+#
+# Wanted by the sleep targets, so unlike the udev-started unit this one does
+# nothing until it is enabled. X-CceEnable tells `ccebuild install-system`
+# to enable it (systemd ignores X- keys).
+After=suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target
+ConditionPathExists=/etc/cce/power.kdl
+
+[Service]
+Type=oneshot
+ExecStart=/usr/bin/cce-power-apply apply
+
+[Install]
+WantedBy=suspend.target hibernate.target hybrid-sleep.target suspend-then-hibernate.target
+X-CceEnable=yes
diff --git a/src/bin/cce-power-apply.rs b/src/bin/cce-power-apply.rs
index 041c5b4..080c3c2 100644
--- a/src/bin/cce-power-apply.rs
+++ b/src/bin/cce-power-apply.rs
@@ -10,9 +10,11 @@
 //! - `apply [ac|battery]` — apply the mode assigned to the current (or the
 //!   named) adapter state. Run by `cce-power-apply.service`, which udev
 //!   starts when the Mains supply appears at boot or flips online/offline
-//!   (`udev/90-cce-power-apply.rules`). Per-lever failures are logged and do
-//!   not fail the run: a missing NVIDIA driver must not hide the CPU profile
-//!   that did land.
+//!   (`udev/90-cce-power-apply.rules`), and by
+//!   `cce-power-apply-resume.service` after every wake. It reads the source
+//!   again when it finishes and re-applies if it moved. Per-lever failures
+//!   are logged and do not fail the run: a missing NVIDIA driver must not
+//!   hide the CPU profile that did land.
 //! - `apply-mode <mode>` — apply one mode by name, whatever is plugged in.
 //! - `set <mode> <lever> <value|unset>` — record one lever on a mode and,
 //!   when that mode is the one running, apply it now. Run by the Power page
@@ -69,16 +71,48 @@ fn run_mode(plan: &PowerPlan, mode: Mode, what: &str) -> i32 {
     0
 }
 
+/// How many times one `apply` follows the source changing under it before
+/// it gives up and leaves the next udev event to finish the job. A charger
+/// with a bad contact can flap for as long as it likes.
+const MAX_APPLY_PASSES: usize = 4;
+
 fn cmd_apply(forced: Option<&str>) -> i32 {
-    let source = match forced {
-        None => current_source(),
-        Some(s) => Source::parse(s).unwrap_or_else(|| usage()),
-    };
     let plan = match load_plan() {
         Ok(p) => p,
         Err(code) => return code,
     };
-    run_mode(&plan, plan.assigned(source), source.key())
+    if let Some(s) = forced {
+        let source = Source::parse(s).unwrap_or_else(|| usage());
+        return run_mode(&plan, plan.assigned(source), source.key());
+    }
+    // Read the source again after applying, and go round once more if it
+    // moved. A udev event that lands while this run is still going cannot
+    // start another one: `systemctl start` on a oneshot that is already
+    // activating merges into the running job. Until 2026-10-02 that lost a
+    // plug-in at resume: a run started by an unplug as the lid closed was
+    // frozen with the rest of user space, finished two hours later on
+    // resume, and applied the battery mode it had read before sleeping while
+    // the plug-in's event merged into it. Animations stayed off on AC until
+    // the charger was replugged.
+    let mut source = current_source();
+    let mut pass = 1;
+    loop {
+        let code = run_mode(&plan, plan.assigned(source), source.key());
+        let now = current_source();
+        if now == source {
+            return code;
+        }
+        if pass == MAX_APPLY_PASSES {
+            eprintln!(
+                "cce-power-apply: source still changing after {} passes; applied {}, now {}",
+                pass, source.key(), now.key()
+            );
+            return code;
+        }
+        println!("cce-power-apply: source changed to {} while applying {}; applying again", now.key(), source.key());
+        source = now;
+        pass += 1;
+    }
 }
 
 fn cmd_apply_mode(rest: &[String]) -> i32 {
diff --git a/udev/90-cce-power-apply.rules b/udev/90-cce-power-apply.rules
index 76671dd..1c0a248 100644
--- a/udev/90-cce-power-apply.rules
+++ b/udev/90-cce-power-apply.rules
@@ -1,7 +1,9 @@
 # Apply the cce Power page's per-source plan (/etc/cce/power.kdl) when the
 # AC adapter appears at boot (coldplug add) or changes state. The kernel
-# emits a change event for the Mains supply on plug and unplug, and on
-# resume when the state differs from before suspend. Through systemd rather
-# than a direct RUN so nvidia-smi's few seconds never block udev.
+# emits a change event for the Mains supply on plug and unplug, and usually
+# on resume when the state differs from before suspend -- but not reliably,
+# so cce-power-apply-resume.service re-applies after every wake as well.
+# Through systemd rather than a direct RUN so nvidia-smi's few seconds never
+# block udev.
 # Installed to /etc/udev/rules.d by `ccebuild install-system`.
 SUBSYSTEM=="power_supply", ACTION=="add|change", ENV{POWER_SUPPLY_TYPE}=="Mains", RUN+="/usr/bin/systemctl --no-block start cce-power-apply.service"