Files

172 lines
3.8 KiB
Plaintext

#!/usr/bin/ucode
'use strict';
import * as uci from "uci";
import * as log from "log";
import * as fs from "fs";
const CFG = "smart-reboot";
const SECTION = "settings";
const LOCK_FILE = "/var/lock/smart-reboot.lock";
function shquote(s) {
return "'" + replace(s, /'/g, "'\\''") + "'";
}
function cmd_out(cmd) {
let f = fs.popen(cmd, "r");
if (!f)
return "";
let out = f.read("all") ?? "";
f.close();
return trim(out);
}
let cursor = uci.cursor();
function cfg_get(key, def) {
let val = cursor.get(CFG, SECTION, key);
if (val == null)
return def;
if (type(val) == "string" && !length(val))
return def;
return val;
}
function logger(msg) {
log.syslog(log.LOG_INFO, msg);
}
function set_last_auto_reboot() {
let ts = cmd_out("date '+%Y-%m-%d %H:%M:%S'");
cursor.set(CFG, SECTION, "last_auto_reboot", ts);
cursor.commit(CFG);
}
function list_all_ifaces() {
let out = [];
for (let path in fs.glob("/sys/class/net/*")) {
let name = fs.basename(path);
if (name && name != "lo")
push(out, name);
}
return out;
}
function parse_ifaces(raw) {
if (!raw)
return [];
if (type(raw) == "array")
return raw;
return filter(split(raw, /[ \t\r\n]+/), v => length(v));
}
function sum_iface_bytes(ifaces) {
let total = 0;
for (let iface in ifaces) {
if (!iface)
continue;
let rx = trim(fs.readfile(`/sys/class/net/${iface}/statistics/rx_bytes`) ?? "0");
let tx = trim(fs.readfile(`/sys/class/net/${iface}/statistics/tx_bytes`) ?? "0");
total += int(rx || "0") + int(tx || "0");
}
return total;
}
function parse_time(time_str) {
if (!time_str || index(time_str, ":") < 0)
return null;
let parts = split(time_str, ":");
if (length(parts) != 2)
return null;
let hour = int(parts[0]);
let minute = int(parts[1]);
if (hour < 0 || hour > 23 || minute < 0 || minute > 59)
return null;
return { hour, minute };
}
function main() {
log.openlog("smart-reboot", log.LOG_PID);
/*
* Acquire an exclusive non-blocking lock on the lock file using the
* native ucode fs.file.lock() API instead of shelling out to `lock`. This
* avoids spawning a new process on every invocation and is what the
* reviewer suggested.
*/
let lockfd = fs.open(LOCK_FILE, "w+");
if (!lockfd || !lockfd.lock("xn"))
return 0; /* cannot obtain lock */
/* helper to release and close the handle */
function cleanup() {
if (lockfd) {
lockfd.lock("u");
lockfd.close();
lockfd = null;
}
}
let enabled = cfg_get("enabled", "0");
if (enabled != "1") {
cleanup();
return 0;
}
let parsed = parse_time(cfg_get("time", ""));
let hour = parsed ? parsed.hour : int(cfg_get("hour", "4"));
let minute = parsed ? parsed.minute : int(cfg_get("minute", "0"));
let sample_seconds = int(cfg_get("sample_seconds", "120"));
let byte_threshold = int(cfg_get("byte_threshold", "262144"));
let all_ifaces = cfg_get("all_ifaces", "0");
let ifaces = parse_ifaces(cfg_get("ifaces", []));
if (all_ifaces == "1") {
ifaces = list_all_ifaces();
}
else if (!length(ifaces)) {
logger("No monitored interfaces configured. Set smart-reboot.settings.ifaces or enable all_ifaces.");
cleanup();
return 0;
}
let now = localtime(time());
if (now.hour != hour || now.min != minute) {
cleanup();
return 0;
}
let start_bytes = sum_iface_bytes(ifaces);
/* use built-in sleep (milliseconds) instead of shelling out */
sleep(sample_seconds * 1000);
let end_bytes = sum_iface_bytes(ifaces);
let delta = end_bytes - start_bytes;
if (delta <= byte_threshold) {
logger(`Network idle detected (delta=${delta} bytes, threshold=${byte_threshold}), rebooting now`);
set_last_auto_reboot();
system("/sbin/reboot");
}
else {
logger(`Skip reboot, network is active (delta=${delta} bytes, threshold=${byte_threshold})`);
}
/* release lock before exiting */
cleanup();
exit(main());