system settings
git clone https://git.lucas.co/cce-system-interface.git
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"