#!/usr/bin/env python3 """Apply one validated BAT1 charge limit and persist it through TLP. Protocol marker: LABFY_TLP_PERSISTENT_V1. QuickShell checks the installed root-owned helper for this marker before allowing a write. Deployment: install this source as root:root, mode 0755, at /usr/local/sbin/batlimit-set. The sudoers rule grants only that installed path. Exit codes: 0 success, 2 invalid argument, 1 unavailable/apply/readback error, 3 rollback failure. stdout is exactly ``SUCCESS `` on success; stderr is exactly ``ERROR `` on failure. """ import fcntl import os from pathlib import Path import re import stat import subprocess import sys import tempfile CONFIG = Path("/etc/tlp.d/90-labfy-charge-limit.conf") SYSFS = Path("/sys/class/power_supply/BAT1/charge_control_end_threshold") LOCK = Path("/run/lock/labfy-batlimit-set.lock") TLP = "/usr/bin/tlp" def fail(reason: str, code: int = 1) -> int: print(f"ERROR {reason}", file=sys.stderr) return code def read_threshold() -> int: raw = SYSFS.read_text(encoding="ascii").strip() if not re.fullmatch(r"[0-9]{1,3}", raw, re.ASCII): raise ValueError("invalid kernel threshold") return int(raw) def prior_config() -> bytes | None: try: metadata = CONFIG.lstat() except FileNotFoundError: return None # CONTRACT: never follow or replace a link supplied at this privileged path. if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0 or metadata.st_size > 4096: raise ValueError("unsafe TLP config") return CONFIG.read_bytes() def sync_directory() -> None: fd = os.open(CONFIG.parent, os.O_RDONLY | os.O_DIRECTORY) try: os.fsync(fd) finally: os.close(fd) def replace_config(content: bytes) -> None: # INVARIANT: TLP must see either the entire old file or the entire new file. fd, temporary = tempfile.mkstemp(prefix=".90-labfy-charge-limit.", dir=CONFIG.parent) try: os.fchmod(fd, 0o644) with os.fdopen(fd, "wb") as output: output.write(content) output.flush() os.fsync(output.fileno()) os.replace(temporary, CONFIG) sync_directory() finally: if os.path.exists(temporary): os.unlink(temporary) def restore_config(previous: bytes | None) -> None: if previous is None: try: CONFIG.unlink() except FileNotFoundError: return sync_directory() else: replace_config(previous) def apply_runtime(value: int) -> bool: # TLP's setcharge command is temporary; the atomically replaced file owns boot persistence. try: result = subprocess.run( [TLP, "setcharge", "0", str(value), "BAT1"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=20, check=False, ) except (OSError, subprocess.TimeoutExpired): return False return result.returncode == 0 def locked_operation(value: int) -> int: try: old_value = read_threshold() old_config = prior_config() except (OSError, ValueError): return fail("unavailable") try: replace_config( f"START_CHARGE_THRESH_BAT1=0\nSTOP_CHARGE_THRESH_BAT1={value}\n".encode("ascii") ) except OSError: # Even a directory fsync failure can happen after rename; restore the old bytes. try: restore_config(old_config) except OSError: return fail("rollback_failed", 3) return fail("unavailable") try: applied = apply_runtime(value) if applied and read_threshold() == value: print(f"SUCCESS {value}") return 0 except (OSError, ValueError): pass # CONTRACT: a failed apply must not leave the new preference for the next boot. try: restore_config(old_config) restored = apply_runtime(old_value) and read_threshold() == old_value except (OSError, ValueError): restored = False return fail("apply_failed" if restored else "rollback_failed", 1 if restored else 3) def main(argv: list[str]) -> int: if len(argv) != 1 or not re.fullmatch(r"[1-9][0-9]{1,2}", argv[0], re.ASCII): return fail("invalid_value", 2) value = int(argv[0]) if not 40 <= value <= 100: return fail("invalid_value", 2) if os.geteuid() != 0: return fail("root_required") # One fixed root-owned lock serializes callers; QuickShell also limits itself to one Process. try: lock_fd = os.open(LOCK, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600) try: metadata = os.fstat(lock_fd) if not stat.S_ISREG(metadata.st_mode) or metadata.st_uid != 0: return fail("unavailable") fcntl.flock(lock_fd, fcntl.LOCK_EX) return locked_operation(value) finally: os.close(lock_fd) except OSError: return fail("unavailable") if __name__ == "__main__": sys.exit(main(sys.argv[1:]))