mirror of
https://github.com/morgan9e/wattd
synced 2026-04-14 00:14:04 +09:00
Init
This commit is contained in:
191
wattd
Normal file
191
wattd
Normal 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
14
wattd.service
Normal 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
|
||||
Reference in New Issue
Block a user