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

src/bin/cce-power-apply.rs (11.9K)

  1 //! cce-power-apply — the root side of the Power page's modes and their
  2 //! adapter-state assignment.
  3 //!
  4 //! The System Interface's Power page keeps a named set of levers per power
  5 //! mode plus a table saying which mode runs plugged in and which on battery
  6 //! (`/etc/cce/power.kdl`, see `cce_settings::power_plan`). Something has to
  7 //! write those levers into sysfs as root whenever the charger comes or goes,
  8 //! with no session and no prompt — that is this binary:
  9 //!
 10 //! - `apply [ac|battery]` — apply the mode assigned to the current (or the
 11 //!   named) adapter state. Run by `cce-power-apply.service`, which udev
 12 //!   starts when the Mains supply appears at boot or flips online/offline
 13 //!   (`udev/90-cce-power-apply.rules`), and by
 14 //!   `cce-power-apply-resume.service` after every wake. It reads the source
 15 //!   again when it finishes and re-applies if it moved. Per-lever failures
 16 //!   are logged and do not fail the run: a missing NVIDIA driver must not
 17 //!   hide the CPU profile that did land. The plan's battery charge limit,
 18 //!   which belongs to no mode, is re-applied by every run.
 19 //! - `sleep` — lower PCIe ASPM to `powersupersave` before the machine
 20 //!   sleeps, when the running mode sets it to anything else. Run by
 21 //!   `cce-power-apply-sleep.service`; the resume unit's `apply` puts the
 22 //!   mode's own value back.
 23 //! - `apply-mode <mode>` — apply one mode by name, whatever is plugged in.
 24 //! - `set <mode> <lever> <value|unset>` — record one lever on a mode and,
 25 //!   when that mode is the one running, apply it now. Run by the Power page
 26 //!   under pkexec, the app's standard privileged path.
 27 //! - `assign <ac|battery> <mode>` — point an adapter state at a mode and,
 28 //!   when that state is the live one, apply the mode now.
 29 //! - `charge-limit <start|unset> <end|unset>` — record the battery's charge
 30 //!   window (whole percent) and apply it now, whatever is plugged in.
 31 //! - `show` — print the plan and the live adapter state.
 32 //!
 33 //! Installed to `/usr/bin` by `ccebuild install-system` (the udev rule and
 34 //! the unit name that path); `ccebuild install` also drops a copy in
 35 //! `~/.local/bin`, which the page falls back to under pkexec before the root
 36 //! side is installed.
 37 
 38 use cce_settings::power_plan::{
 39     apply_charge_limit, apply_lever, apply_mode, current_source, ChargeLimit, Lever, Mode, PowerPlan, Source,
 40     PLAN_PATH,
 41 };
 42 
 43 fn usage() -> ! {
 44     eprintln!(
 45         "usage: cce-power-apply apply [ac|battery]\n       \
 46                 cce-power-apply sleep\n       \
 47                 cce-power-apply apply-mode <mode>\n       \
 48                 cce-power-apply set <mode> <lever> <value|unset>\n       \
 49                 cce-power-apply assign <ac|battery> <mode>\n       \
 50                 cce-power-apply charge-limit <start|unset> <end|unset>\n       \
 51                 cce-power-apply show\n\
 52          modes:  {}\n\
 53          levers: {}",
 54         Mode::ALL.iter().map(|m| m.key()).collect::<Vec<_>>().join(" "),
 55         Lever::ALL.iter().map(|l| l.key()).collect::<Vec<_>>().join(" ")
 56     );
 57     std::process::exit(2)
 58 }
 59 
 60 fn load_plan() -> Result<PowerPlan, i32> {
 61     PowerPlan::load().map_err(|e| {
 62         eprintln!("cce-power-apply: {}", e);
 63         1
 64     })
 65 }
 66 
 67 /// Apply one mode and report each lever. `what` names what asked for it —
 68 /// the adapter state, or the mode itself — so the journal says why.
 69 fn run_mode(plan: &PowerPlan, mode: Mode, what: &str) -> i32 {
 70     let results = apply_mode(plan, mode);
 71     if results.is_empty() {
 72         println!("cce-power-apply: {} runs {}, which sets nothing", what, mode.key());
 73         return 0;
 74     }
 75     for (lever, result) in &results {
 76         match result {
 77             Ok(()) => println!("{} [{}]: {} = {}", what, mode.key(), lever.key(), plan.get(mode, *lever).unwrap_or("")),
 78             Err(e) => eprintln!("cce-power-apply: {} [{}] {}: {}", what, mode.key(), lever.key(), e),
 79         }
 80     }
 81     0
 82 }
 83 
 84 /// Apply the plan's charge window and report it. Nothing to say when the
 85 /// plan has none: the battery's thresholds are then not this binary's.
 86 fn run_charge_limit(plan: &PowerPlan) -> i32 {
 87     let limit = plan.charge_limit();
 88     if limit.is_unset() {
 89         return 0;
 90     }
 91     match apply_charge_limit(limit) {
 92         Ok(()) => {
 93             println!("charge limit: {}", limit);
 94             0
 95         }
 96         Err(e) => {
 97             eprintln!("cce-power-apply: charge limit ({}): {}", limit, e);
 98             1
 99         }
100     }
101 }
102 
103 /// How many times one `apply` follows the source changing under it before
104 /// it gives up and leaves the next udev event to finish the job. A charger
105 /// with a bad contact can flap for as long as it likes.
106 const MAX_APPLY_PASSES: usize = 4;
107 
108 fn cmd_apply(forced: Option<&str>) -> i32 {
109     let plan = match load_plan() {
110         Ok(p) => p,
111         Err(code) => return code,
112     };
113     // Every run, not only when the page sets it: the firmware forgets the
114     // thresholds (a pack that runs flat comes back at 0/100), and this run
115     // is what boot, every plug and unplug, and every wake already start.
116     // Its failure is logged and, like a lever's, does not fail the run.
117     run_charge_limit(&plan);
118     if let Some(s) = forced {
119         let source = Source::parse(s).unwrap_or_else(|| usage());
120         return run_mode(&plan, plan.assigned(source), source.key());
121     }
122     // Read the source again after applying, and go round once more if it
123     // moved. A udev event that lands while this run is still going cannot
124     // start another one: `systemctl start` on a oneshot that is already
125     // activating merges into the running job. Until 2026-10-02 that lost a
126     // plug-in at resume: a run started by an unplug as the lid closed was
127     // frozen with the rest of user space, finished two hours later on
128     // resume, and applied the battery mode it had read before sleeping while
129     // the plug-in's event merged into it. Animations stayed off on AC until
130     // the charger was replugged.
131     let mut source = current_source();
132     let mut pass = 1;
133     loop {
134         let code = run_mode(&plan, plan.assigned(source), source.key());
135         let now = current_source();
136         if now == source {
137             return code;
138         }
139         if pass == MAX_APPLY_PASSES {
140             eprintln!(
141                 "cce-power-apply: source still changing after {} passes; applied {}, now {}",
142                 pass, source.key(), now.key()
143             );
144             return code;
145         }
146         println!("cce-power-apply: source changed to {} while applying {}; applying again", now.key(), source.key());
147         source = now;
148         pass += 1;
149     }
150 }
151 
152 /// The ASPM policy every sleep starts under, whatever mode is running.
153 const SLEEP_ASPM: &str = "powersupersave";
154 
155 /// Ready the machine to sleep. This laptop has only s2idle, where the
156 /// package reaches S0ix only if its PCIe links can drop into L1 substates,
157 /// and the AC mode's `aspm "performance"` forbids that. Nothing re-applies
158 /// the plan while the machine sleeps, so a suspend begun on the charger kept
159 /// that mode through an unplug: the battery history before 2026-10-04 shows
160 /// sleeps begun on battery drawing about 1 W, and those begun on AC up to
161 /// 4 W, enough to empty a full battery in a day and a half closed.
162 ///
163 /// Only ASPM moves. The rest of a mode either has no effect in s2idle or,
164 /// like the NVIDIA power limit, costs seconds of nvidia-smi on every lid
165 /// close. And only when the running mode sets it: the resume unit's `apply`
166 /// then restores that mode's value, so a mode that leaves ASPM alone is not
167 /// woken into a policy it never chose.
168 fn cmd_sleep() -> i32 {
169     let plan = match load_plan() {
170         Ok(p) => p,
171         Err(code) => return code,
172     };
173     let source = current_source();
174     let mode = plan.assigned(source);
175     match plan.get(mode, Lever::Aspm) {
176         None => {
177             println!("cce-power-apply: sleep: {} [{}] sets no aspm; left as is", source.key(), mode.key());
178             0
179         }
180         Some(v) if v == SLEEP_ASPM => 0,
181         Some(v) => match apply_lever(Lever::Aspm, SLEEP_ASPM) {
182             Ok(()) => {
183                 println!("cce-power-apply: sleep: aspm {} -> {} ({} [{}] restores it on wake)", v, SLEEP_ASPM, source.key(), mode.key());
184                 0
185             }
186             Err(e) => {
187                 // Never fail the unit: a sleep that drains faster beats one
188                 // systemd refuses to start.
189                 eprintln!("cce-power-apply: sleep: aspm: {}", e);
190                 0
191             }
192         },
193     }
194 }
195 
196 fn cmd_apply_mode(rest: &[String]) -> i32 {
197     let [mode] = rest else { usage() };
198     let mode = Mode::parse(mode).unwrap_or_else(|| usage());
199     let plan = match load_plan() {
200         Ok(p) => p,
201         Err(code) => return code,
202     };
203     run_mode(&plan, mode, "mode")
204 }
205 
206 fn cmd_set(rest: &[String]) -> i32 {
207     let [mode, lever, value] = rest else { usage() };
208     let mode = Mode::parse(mode).unwrap_or_else(|| usage());
209     let lever = Lever::parse(lever).unwrap_or_else(|| usage());
210     let value: Option<&str> = if value == "unset" { None } else { Some(value.as_str()) };
211     let mut plan = match load_plan() {
212         Ok(p) => p,
213         Err(code) => return code,
214     };
215     if let Err(e) = plan.put(mode, lever, value) {
216         eprintln!("cce-power-apply: {}", e);
217         return 2;
218     }
219     if let Err(e) = plan.save() {
220         eprintln!("cce-power-apply: writing {}: {}", PLAN_PATH, e);
221         return 1;
222     }
223     // Only the mode the machine is running right now touches sysfs; editing
224     // any other mode is a plan change and nothing more.
225     if mode == plan.assigned(current_source()) {
226         if let Some(v) = value {
227             if let Err(e) = apply_lever(lever, v) {
228                 eprintln!("cce-power-apply: {} {}: {}", mode.key(), lever.key(), e);
229                 return 1;
230             }
231         }
232     }
233     0
234 }
235 
236 fn cmd_assign(rest: &[String]) -> i32 {
237     let [source, mode] = rest else { usage() };
238     let source = Source::parse(source).unwrap_or_else(|| usage());
239     let mode = Mode::parse(mode).unwrap_or_else(|| usage());
240     let mut plan = match load_plan() {
241         Ok(p) => p,
242         Err(code) => return code,
243     };
244     plan.assign(source, mode);
245     if let Err(e) = plan.save() {
246         eprintln!("cce-power-apply: writing {}: {}", PLAN_PATH, e);
247         return 1;
248     }
249     // Reassigning the live adapter state hands the machine to another mode
250     // now, not at the next unplug.
251     if source == current_source() {
252         return run_mode(&plan, mode, source.key());
253     }
254     0
255 }
256 
257 fn cmd_charge_limit(rest: &[String]) -> i32 {
258     let [start, end] = rest else { usage() };
259     let pct = |s: &str| -> Option<u32> {
260         if s == "unset" { None } else { Some(s.parse().unwrap_or_else(|_| usage())) }
261     };
262     let limit = ChargeLimit { start: pct(start), end: pct(end) };
263     let mut plan = match load_plan() {
264         Ok(p) => p,
265         Err(code) => return code,
266     };
267     if let Err(e) = plan.set_charge_limit(limit) {
268         eprintln!("cce-power-apply: {}", e);
269         return 2;
270     }
271     if let Err(e) = plan.save() {
272         eprintln!("cce-power-apply: writing {}: {}", PLAN_PATH, e);
273         return 1;
274     }
275     run_charge_limit(&plan)
276 }
277 
278 fn cmd_show() -> i32 {
279     match load_plan() {
280         Ok(plan) => {
281             print!("{}", plan.to_kdl());
282             let source = current_source();
283             println!("// live: {} running {}", source.key(), plan.assigned(source).key());
284             0
285         }
286         Err(code) => code,
287     }
288 }
289 
290 fn main() {
291     let args: Vec<String> = std::env::args().skip(1).collect();
292     let code = match args.first().map(String::as_str) {
293         Some("apply") => cmd_apply(args.get(1).map(String::as_str)),
294         Some("sleep") => cmd_sleep(),
295         Some("apply-mode") => cmd_apply_mode(&args[1..]),
296         Some("set") => cmd_set(&args[1..]),
297         Some("assign") => cmd_assign(&args[1..]),
298         Some("charge-limit") => cmd_charge_limit(&args[1..]),
299         Some("show") => cmd_show(),
300         _ => usage(),
301     };
302     std::process::exit(code);
303 }