Dotfiles_ArchAP/bin/root/usr/local/sbin/batlimit-set

166 lines
5 KiB
Python
Executable file

#!/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 <value>`` on success; stderr
is exactly ``ERROR <reason>`` 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:]))