1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
|
'use strict';
import { readfile } from 'fs';
import * as uloop from 'uloop';
import { connect } from 'ubus';
const STEP = 5000, SLOTS = 60;
let ring = [], seq = 0, timer;
const bus = connect();
// Observe one main-table default-route device. Prefer IPv4, then route metric;
// break ties by device name so the result does not depend on dump order.
function select_wan_device(interfaces) {
let best = null;
for (let iface in interfaces) {
if (!iface.up)
continue;
let name = iface.device;
if (!name || name == 'lo' || substr(name, 0, 1) == '@')
name = iface.l3_device;
if (!name || name == 'lo' || substr(name, 0, 1) == '@')
continue;
for (let route in iface.route ?? []) {
if (+route.mask != 0)
continue;
const family = route.target == '0.0.0.0' ? 4 : route.target == '::' ? 6 : 0;
if (!family)
continue;
// netifd omits route.table when it is inherited from the interface.
const table = route.table ?? (family == 4 ? iface.ip4table : iface.ip6table);
if (table && table != 254)
continue;
const metric = +(route.metric ?? iface.metric ?? 0);
if (!best || family < best.family ||
(family == best.family && metric < best.metric) ||
(family == best.family && metric == best.metric && name < best.device))
best = { device: name, family, metric };
}
}
// Use the configured carrier (including a VLAN) without walking to its
// physical parent, which may also carry LAN or IPTV traffic.
return best ? best.device : null;
}
function now() {
const c = clock(true);
return c[0] * 1000 + int(c[1] / 1000000);
}
// Tells the instances apart, as the sequence starts over with rpcd.
const id = trim(readfile('/proc/sys/kernel/random/uuid'));
// Counters as the kernel has them. Whoever draws them takes the differences,
// over the time between two samples on the clock they were read by.
function sample() {
const cpus = {}, net = {};
let device = null;
try {
const dump = bus.call('network.interface', 'dump');
if (type(dump?.interface) == 'array')
device = select_wan_device(dump.interface);
}
catch (e) {
warn(`luci.dashboard: traffic discovery: ${e}\n`);
}
for (let line in split(readfile('/proc/stat') ?? '', '\n')) {
if (substr(line, 0, 3) != 'cpu')
break;
const fields = split(line, /\s+/);
cpus[fields[0]] = map(slice(fields, 1, 9), v => +v);
}
const mem = match(readfile('/proc/meminfo', 256) ?? '', /MemAvailable:\s+(\d+)/);
// Only the selected WAN consumes network history. Check its identity
// on both sides of the counter read to detect device recreation.
const before = device ? int(readfile(`/sys/class/net/${device}/ifindex`)) : null;
const t = now();
if (device) {
for (let line in slice(split(readfile('/proc/net/dev') ?? '', '\n'), 2)) {
const fields = split(trim(line), /[: ]+/);
if (length(fields) <= 9 || fields[0] != device)
continue;
const after = int(readfile(`/sys/class/net/${device}/ifindex`));
net[device] = [ +fields[1], +fields[9],
after > 0 && after == before ? after : null ];
break;
}
}
push(ring, [ ++seq, t, cpus, mem ? +mem[1] * 1024 : null, net, device ]);
while (length(ring) > SLOTS)
shift(ring);
}
// Sampling never stops, so the charts are full whenever somebody looks.
function tick() {
timer.set(STEP);
// An exception here would end rpcd's uloop, not just this timer.
try {
sample();
}
catch (e) {
warn(`luci.dashboard: ${e}\n`);
}
}
timer = uloop.timer(0, tick);
const methods = {
history: {
args: { since: 0, id: 'id' },
call: function(request) {
const since = (request.args?.id == id) ? +(request.args?.since ?? 0) : 0;
return {
id,
now: now(),
step: STEP,
slots: SLOTS,
samples: filter(ring, entry => entry[0] > since)
};
}
}
};
return { 'luci.dashboard': methods };
|