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