This commit is contained in:
2025-10-26 04:27:59 +09:00
committed by GitHub
commit ce4219a86c
2 changed files with 205 additions and 0 deletions

191
wattd Normal file
View File

@@ -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()

14
wattd.service Normal file
View File

@@ -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