From ce4219a86c94d18d5e48c3be7bbfd200e525da50 Mon Sep 17 00:00:00 2001 From: Morgan Date: Sun, 26 Oct 2025 04:27:59 +0900 Subject: [PATCH] Init --- wattd | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++ wattd.service | 14 ++++ 2 files changed, 205 insertions(+) create mode 100644 wattd create mode 100644 wattd.service diff --git a/wattd b/wattd new file mode 100644 index 0000000..c843b14 --- /dev/null +++ b/wattd @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 + +import os +import sys +import time +import signal +import fcntl + +RAPL_BASE = "/sys/class/powercap/intel-rapl" +OUTPUT_DIR = "/run/power" +LOCK_FILE = "/run/wattd.pid" +SAMPLE_INTERVAL = 1.0 + +class RAPL: + def __init__(self, zone_dir): + self.zone_dir = zone_dir + self.energy_file = os.path.join(zone_dir, "energy_uj") + self.name_file = os.path.join(zone_dir, "name") + self.name = self.read_name() + self.prev_energy = None + self.prev_time = None + + def read_name(self): + try: + with open(self.name_file) as f: + return f.read().strip() + except: # noqa: E722 + return os.path.basename(self.zone_dir) + + def read_energy(self): + try: + with open(self.energy_file) as f: + return int(f.read().strip()) + except Exception as e: + print(f"Error reading {self.energy_file}: {e}", file=sys.stderr) + return None + + def measure(self): + energy = self.read_energy() + now = time.monotonic() + + if energy is None: + return None + + if self.prev_energy is None: + self.prev_energy = energy + self.prev_time = now + return None + + dE = energy - self.prev_energy + dt = now - self.prev_time + + if dE < 0: + dE += 2**32 + + if dt <= 0: + return None + + watts = (dE/dt)/1_000_000 + + self.prev_energy = energy + self.prev_time = now + + return watts + + def output_path(self): + name = self.name.replace('/', '-').replace(' ', '-') + return os.path.join(OUTPUT_DIR, name) + +class Monitor: + def __init__(self): + self.zones = [] + self.running = True + self.lock_fd = None + + self.acquire() + self.setup() + self.find() + + if not self.zones: + raise RuntimeError("No RAPL zones found") + + signal.signal(signal.SIGTERM, self.handle_signal) + signal.signal(signal.SIGINT, self.handle_signal) + + def acquire(self): + try: + self.lock_fd = open(LOCK_FILE, 'w') + fcntl.flock(self.lock_fd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + self.lock_fd.write(f"{os.getpid()}\n") + self.lock_fd.flush() + except IOError: + print("already running", file=sys.stderr) + sys.exit(1) + + def release(self): + if self.lock_fd: + try: + fcntl.flock(self.lock_fd.fileno(), fcntl.LOCK_UN) + self.lock_fd.close() + os.remove(LOCK_FILE) + except: # noqa: E722 + pass + + def setup(self): + os.makedirs(OUTPUT_DIR, exist_ok=True) + os.chmod(OUTPUT_DIR, 0o755) + + def find(self): + for root, dirs, files in os.walk(RAPL_BASE): + if 'energy_uj' not in files: + continue + zone_dir = root + energy_file = os.path.join(zone_dir, "energy_uj") + try: + zone = RAPL(zone_dir) + with open(energy_file) as f: + f.read() + self.zones.append(zone) + print(f"{zone.name} -> {zone.output_path()}", file=sys.stderr) + except PermissionError: + print(f"No permission: {energy_file}", file=sys.stderr) + except Exception as e: + print(f"Error: {energy_file}: {e}", file=sys.stderr) + + def handle_signal(self, signum, frame): + print(f"\nSignal {signum}, shutting down", file=sys.stderr) + self.running = False + + def write(self, path, watts): + try: + temp = path + ".tmp" + with open(temp, 'w') as f: + f.write(f"{watts:.2f}\n") + os.rename(temp, path) + os.chmod(path, 0o644) + except Exception as e: + print(f"Write error {path}: {e}", file=sys.stderr) + + def run(self): + print(f"wattd started, {len(self.zones)} zones", file=sys.stderr) + + for zone in self.zones: + zone.measure() + + next_sample = time.monotonic() + SAMPLE_INTERVAL + + while self.running: + try: + sleep_time = next_sample - time.monotonic() + if sleep_time > 0: + time.sleep(sleep_time) + + next_sample += SAMPLE_INTERVAL + + for zone in self.zones: + watts = zone.measure() + + if watts is not None: + self.write(zone.output_path(), watts) + + except KeyboardInterrupt: + break + except Exception as e: + print(f"Loop error: {e}", file=sys.stderr) + next_sample = time.monotonic() + SAMPLE_INTERVAL + + for zone in self.zones: + path = zone.output_path() + try: + os.remove(path) + except: # noqa: E722 + pass + + self.release() + print("wattd stopped", file=sys.stderr) + +def main(): + if os.geteuid() != 0: + print("Please run as root", file=sys.stderr) + sys.exit(1) + + try: + monitor = Monitor() + monitor.run() + except Exception as e: + print(f"Fatal: {e}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/wattd.service b/wattd.service new file mode 100644 index 0000000..255b171 --- /dev/null +++ b/wattd.service @@ -0,0 +1,14 @@ +[Unit] +Description=Power Monitoring Daemon +After=multi-user.target + +[Service] +Type=simple +ExecStart=/usr/bin/wattd +Restart=always +RestartSec=5 +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target