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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
|
#!/bin/sh
#
# Runs one LibreSpeed measurement and records the result.
#
# The only thing that starts a measurement: LuCI reaches it through rpcd, the
# scheduler calls it directly. Holding the lock for the whole run means a second
# invocation from either side waits instead of measuring against the first.
# Packaging checks probe every executable for these; a runner that ignored
# them would start a measurement instead of answering.
links=""
case "$1" in
--version)
echo "librespeed-common %%VERSION%%"
exit 0
;;
--help|-h)
cat <<'EOF'
Usage: librespeed-run [INTERFACE]
Runs one LibreSpeed measurement according to /etc/config/librespeed and
records the result for the ubus interface. Started by LuCI or cron.
INTERFACE is a logical interface (default: librespeed.main.interface).
It is measured bound to the device of its link and recorded under the
link (wan6 beside wan counts as wan). It is not measured, and that is
recorded as its failure, when its link is down, netifd does not know it
or netifd does not answer.
--links print each interface's link, the links to offer and the
interfaces whose link is down, as JSON
EOF
exit 0
;;
--links)
[ $# -eq 1 ] || { echo "Usage: librespeed-run --links" >&2; exit 2; }
links=1
shift
;;
esac
# The name reaches jsonfilter expressions and the crontab: UCI's alphabet only.
usage() {
echo "Usage: librespeed-run [INTERFACE]" >&2
exit 2
}
[ $# -le 1 ] || usage
case "$1" in *[!A-Za-z0-9_]*) usage ;; esac
want=$1
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
CLI=/usr/bin/librespeed-cli
STATE_DIR=/tmp/librespeed
STATE="$STATE_DIR/state.json"
RESULT="$STATE_DIR/result.json"
FAILED="$STATE_DIR/failed"
LOCK=/var/lock/librespeed.lock
# A down interface still sits on the device it is configured on only as
# DHCP, DHCPv6 or static: PPP and qmi bring up a device of their own.
DEVP="@.proto='dhcp' || @.proto='dhcpv6' || @.proto='static'"
mkdir -p "$STATE_DIR"
# Writes $2 to $1 without ever leaving a half-written file for a reader.
atomic_write() {
local target="$1" tmp="$1.$$"
printf '%s\n' "$2" > "$tmp" && mv "$tmp" "$target"
}
write_state() {
json_init
json_add_boolean running "$1"
# A run names its link once bound; a failed one, its for_iface.
if [ "$1" = 1 ]; then
[ -n "$state_iface" ] && json_add_string interface "$state_iface"
elif [ -n "$5" ] && [ -n "$for_iface" ]; then
json_add_string interface "$for_iface"
fi
[ -n "$2" ] && json_add_int pid "$2"
[ -n "$3" ] && json_add_int started "$3"
[ -n "$4" ] && json_add_int last_finished "$4"
json_add_string last_error "${5:-}"
json_add_string phase "${6:-}"
[ -n "$7" ] && json_add_double mbps "$7"
[ -n "$8" ] && json_add_int progress "$8"
atomic_write "$STATE" "$(json_dump)"
}
# Approximates progress from the interface byte counters, one sample a second.
#
# The fallback narrator for clients that cannot stream: it reads the whole
# interface, not the test, so it is honest only while the test dominates the
# link -- which is exactly the situation a progress line describes. Whichever
# direction carries the traffic names the phase.
sampler() {
local dev="$1" rx tx prx ptx phase mbps
[ -r "/sys/class/net/$dev/statistics/rx_bytes" ] || return 0
prx=$(cat "/sys/class/net/$dev/statistics/rx_bytes")
ptx=$(cat "/sys/class/net/$dev/statistics/tx_bytes")
while :; do
sleep 1
rx=$(cat "/sys/class/net/$dev/statistics/rx_bytes" 2>/dev/null) || return 0
tx=$(cat "/sys/class/net/$dev/statistics/tx_bytes" 2>/dev/null) || return 0
set -- $(awk -v rx="$rx" -v prx="$prx" -v tx="$tx" -v ptx="$ptx" 'BEGIN {
drx = (rx - prx) * 8 / 1000000
dtx = (tx - ptx) * 8 / 1000000
if (drx >= dtx && drx > 1) printf "download %.1f", drx
else if (dtx > 1) printf "upload %.1f", dtx
else printf "- -"
}')
prx=$rx; ptx=$tx
# The runner may die without reaching its kill (SIGKILL, a trap that
# never ran); without this check the sampler would rewrite state.json
# forever and mask every later run's result.
kill -0 $$ 2>/dev/null || return 0
# An idle second keeps the last phase: the frontend reads an empty
# phase as "not started yet" and would collapse mid-run.
if [ "$1" = "-" ]; then
write_state 1 $$ "$started" "" "" "$phase" ""
else
phase=$1
write_state 1 $$ "$started" "" "" "$1" "$2"
fi
done
}
fail() {
local at
# A run that failed with the cached server drops the cache: the next run
# rediscovers instead of failing against the same dead choice forever.
[ "${used_cache:-0}" = 1 ] && rm -f "$LIST_CACHE" "$LIST_CACHE.src" "$CHOICE_CACHE"
at=$(date +%s)
write_state 0 "" "" "$at" "$1"
# Kept until a result is recorded on this link; other links leave it.
if [ -n "$for_iface" ]; then
mkdir -p "$FAILED"
atomic_write "$FAILED/$for_iface" "$at $1"
fi
logger -t librespeed "measurement of ${for_iface:-$iface} failed: $1"
exit 1
}
# ifdump_get VAR PATH: PATH below @.interface of the netifd snapshot, taken
# once so a run is judged by the state it began with. Matches space-joined.
ifdump_get() {
local __v
[ -n "$ifdump" ] || return 1
__v=$(jsonfilter -s "$ifdump" -e "$1=@.interface$2" 2>/dev/null)
[ -n "$__v" ] || { unset "$1"; return 1; }
eval "$__v"
}
if_device() {
case "$2" in ''|*[!A-Za-z0-9_.-]*) unset "$1"; return 1 ;; esac
ifdump_get "$1" "[@.interface='$2'].l3_device"
}
# The UCI section device $1 is recorded under: wan for wan6, even while wan is
# down. Several are narrowed in turn, a step only where it leaves some: to the
# IPv4 default's holder, then not DHCPv6, then no IPv6 address. A down one
# counts only as DHCP or static (PPP has a device of its own). More left: none.
dev_iface() {
local p q f on="@.dynamic=false && (@.l3_device='$1' || (@.up=false && @.device='$1' && (@.proto='dhcp' || @.proto='static')))"
case "$1" in ''|*[!A-Za-z0-9_.-]*) return 1 ;; esac
ifdump_get p "[$on].interface" || return 1
for f in "!@.ip4table && @.route[@.target='0.0.0.0' && !@.table]" \
"@.proto!='dhcpv6'" "!@['ipv6-address'][0]"; do
case "$p" in *' '*) ;; *) break ;; esac
ifdump_get q "[$on && $f].interface" && on="$on && $f" p=$q
done
case "$p" in *' '*) return 1 ;; esac
echo "$p"
}
# bind_dev VAR NAME: the device a test of NAME is bound to: its l3_device, or
# while it is down the device it sits on, if another interface of its link
# is up there (wan6 beside a down wan). None: its link is down.
bind_dev() {
local d u
case "$2" in ''|*[!A-Za-z0-9_]*) return 1 ;; esac
if_device "$1" "$2" && return 0
ifdump_get d "[@.interface='$2' && @.up=false && ($DEVP)].device" || return 1
dev_iface "$d" > /dev/null || return 1
ifdump_get u "[@.l3_device='$d'].interface" || return 1
eval "$1=\$d"
}
# --links: the link a bound run of each interface records (wan6 as wan), no
# lock taken. A dynamic interface without a link is left out. A static link
# without a gateway (lan) is not offered: no default route in the dump, which
# shows routes only while up, and no gateway or ip6gw in UCI. down lists those
# a run refuses as down: no l3_device, and bind_dev finds none.
print_links() {
local names x d l g4 g6 cache="" map="" nogw="" nol3=""
ifdump_get names "[*].interface"
ifdump_get nol3 "[!@.l3_device].interface"
for x in $names; do
case "$x" in *[!A-Za-z0-9_]*) continue ;; esac
if_device d "$x" ||
ifdump_get d "[@.interface='$x' && @.up=false && ($DEVP)].device" ||
d=""
[ "$d" = lo ] && continue
l=""
if [ -n "$d" ]; then
case " $cache " in
*" $d="*) l=${cache##* "$d"=}; l=${l%% *} ;;
*) l=$(dev_iface "$d"); cache="$cache $d=$l" ;;
esac
fi
[ -n "$l" ] ||
ifdump_get l "[@.interface='$x' && @.dynamic=false].interface" ||
continue
map="$map $x=$l"
done
ifdump_get nogw "[@.proto='static' && !@.route[@.mask=0]].interface" &&
config_load network
json_init
json_add_array links
for l in $(for x in $map; do echo "${x#*=}"; done | sort -u); do
case " $nogw " in
*" $l "*)
config_get g4 "$l" gateway
config_get g6 "$l" ip6gw
[ -n "$g4$g6" ] || continue
;;
esac
json_add_string "" "$l"
done
json_close_array
json_add_object interfaces
for x in $map; do
json_add_string "${x%%=*}" "${x#*=}"
done
json_close_object
json_add_array down
for x in $map; do
x=${x%%=*}
case " $nol3 " in *" $x "*) bind_dev b "$x" || json_add_string "" "$x" ;; esac
done
json_close_array
json_dump
[ -n "$names" ]
}
if [ -n "$links" ]; then
ifdump=$(ubus -t 5 call network.interface dump 2>/dev/null)
print_links
exit
fi
# One measurement at a time. The descriptor stays open for the whole run, so the
# kernel releases the lock even if this script is killed -- there is no stale
# state to time out.
# Schedules fire at the same minute: a run waits up to 3 minutes for the lock
# (BusyBox flock has no -w) and writes nothing meanwhile, then gives up
# (exit 3) and records that as its interface's failure.
exec 9>"$LOCK"
tries=0
until flock -n 9; do
if [ "$tries" = 0 ]; then
echo "waiting for the running measurement to finish" >&2
logger -t librespeed "measurement of ${want:-the default interface} waits for the running one"
elif [ "$tries" -ge 36 ]; then
echo "already running" >&2
logger -t librespeed "skipped measurement of ${want:-the default interface}: another one held the lock for 3 minutes"
# A lost slot is a gap in the data: kept as the interface's failure,
# not in state.json, which is the lock holder's.
skip=$want
[ -n "$skip" ] || { config_load librespeed; config_get skip main interface wan; }
case "$skip" in ''|*[!A-Za-z0-9_]*) ;; *)
mkdir -p "$FAILED"
atomic_write "$FAILED/$skip" "$(date +%s) skipped: another measurement held the lock for 3 minutes"
esac
exit 3
fi
tries=$((tries + 1))
sleep 5
done
# From here status reads state.json as this run's: clear the last interface.
state_iface="" for_iface=""
started=$(date +%s)
write_state 1 $$ "$started"
# stop kills our whole process group, so librespeed-cli dies with us; ash runs
# this trap once the foreground child has exited, and it records that the run
# was stopped rather than pretending the measurement failed.
sampler_pid=
trap '[ -n "$sampler_pid" ] && kill "$sampler_pid" 2>/dev/null
write_state 0 "" "" "$(date +%s)" "stopped"; exit 1' TERM
config_load librespeed
config_get iface main interface wan
[ -n "$want" ] && iface=$want
config_get server main server auto
config_get scheme main scheme auto
config_get server_list main server_list ''
config_get_bool hist_enabled history enabled 1
config_get hist_path history path "$STATE_DIR/history.jsonl"
config_get hist_retention history retention 30d
# --interface takes a device, and UCI carries a logical interface name. A run
# is bound to the device of its link or measures nothing, so its link is never
# a guess. Failures go under the name asked for, once bound under its link.
for_iface=$iface
case "$for_iface" in *[!A-Za-z0-9_]*) for_iface="" ;; esac
ifdump=$(ubus call network.interface dump 2>/dev/null)
[ -n "$ifdump" ] || fail "netifd did not answer"
ifdump_get known "[@.interface='$for_iface'].interface" ||
fail "interface disabled or not configured"
bind_dev dev "$for_iface" || fail "interface is down"
state_iface=$(dev_iface "$dev" 2>/dev/null) || state_iface=$for_iface
for_iface=$state_iface
[ -x "$CLI" ] || fail "librespeed-cli is not installed"
# The Rust client reports progress as NDJSON under --json-stream; the Go one
# does not have the flag yet. Asking --help is one extra exec per measurement
# and keeps one script driving either client.
stream=0
if "$CLI" --help 2>&1 | grep -q -- '--json-stream'; then
stream=1
set -- "$CLI" --json-stream
else
set -- "$CLI" --json
fi
# The router-side counterpart of the web UI remembering its chosen server:
# after an automatic run the picked server's id and the downloaded list are
# cached, and later runs go straight to the same server with --server and
# --local-json instead of fetching the list and pinging everything on it.
LIST_CACHE="$STATE_DIR/servers.json"
CHOICE_CACHE="$STATE_DIR/server-choice"
CACHE_TTL=86400
# Where the list comes from: a self-hosted deployment -- Turris runs
# https://librespeed.turris.cz/servers.json -- replaces the official one.
LIST_URL="${server_list:-https://librespeed.org/backend-servers/servers.php}"
cache_fresh=0
c_id=""
if [ -s "$LIST_CACHE" ] && [ -s "$CHOICE_CACHE" ]; then
read -r c_epoch c_id < "$CHOICE_CACHE"
# Both halves are checked before the arithmetic: a non-numeric value there
# is a fatal error in ash, and the run would die before writing any state.
case "$c_id" in ""|*[!0-9]*) c_id="" ;; esac
case "$c_epoch" in ""|*[!0-9]*) c_id="" ;; esac
[ -n "$c_id" ] && [ $(( $(date +%s) - c_epoch )) -lt "$CACHE_TTL" ] || c_id=""
# A cache fetched from another list is no cache at all.
[ "$(cat "$LIST_CACHE.src" 2>/dev/null)" = "$LIST_URL" ] || c_id=""
fi
[ -n "$c_id" ] && cache_fresh=1
used_cache=0
if [ "$server" != "auto" ]; then
set -- "$@" --server "$server"
# An explicit id still needs the list to resolve it; the cached copy
# saves that download too.
if [ "$cache_fresh" = 1 ]; then
set -- "$@" --local-json "$LIST_CACHE"
used_cache=1
elif [ -n "$server_list" ]; then
set -- "$@" --server-json "$LIST_URL"
fi
elif [ "$cache_fresh" = 1 ]; then
set -- "$@" --server "$c_id" --local-json "$LIST_CACHE"
used_cache=1
elif [ -n "$server_list" ]; then
set -- "$@" --server-json "$LIST_URL"
fi
set -- "$@" --interface "$dev"
# TLS itself can bound the result on routers without AES acceleration, so the
# scheme is a measurement setting, not just a transport detail.
case "$scheme" in
https) set -- "$@" --secure ;;
http) set -- "$@" --insecure ;;
esac
# Never --bytes: it switches the report to MB/s and the history would end up
# holding two units that cannot be told apart afterwards.
# Now with its link.
write_state 1 $$ "$started"
if [ "$stream" = 1 ]; then
# The while runs in a subshell, so the reports cannot come back in a
# variable; they land in a file instead. State updates are throttled to
# nothing -- one arrives a second and state.json lives in tmpfs.
rm -f "$STATE_DIR/reports.json"
"$@" 2>"$STATE_DIR/stderr.log" < /dev/null | while IFS= read -r line; do
case "$line" in
*'"event":"result"'*)
printf '%s' "$line" | jsonfilter -e '@.reports' \
> "$STATE_DIR/reports.json" 2>/dev/null
;;
*'"event":"progress"'*)
write_state 1 $$ "$started" "" "" \
"$(printf '%s' "$line" | jsonfilter -e '@.phase' 2>/dev/null)" \
"$(printf '%s' "$line" | jsonfilter -e '@.mbps' 2>/dev/null)" \
"$(printf '%s' "$line" | jsonfilter -e '@.progress' 2>/dev/null)"
;;
*'"event":"phase"'*)
write_state 1 $$ "$started" "" "" \
"$(printf '%s' "$line" | jsonfilter -e '@.phase' 2>/dev/null)" ""
;;
esac
done
# The pipeline's status is the reader's, so success is judged by what the
# stream delivered: a client that failed never emitted a result event.
out=$(cat "$STATE_DIR/reports.json" 2>/dev/null)
rm -f "$STATE_DIR/reports.json"
if [ -z "$out" ]; then
# tail succeeds on an empty file, so the substitution guards the
# value, not the command: an empty log still reports a failure.
err=$(tail -n 1 "$STATE_DIR/stderr.log" 2>/dev/null)
fail "${err:-measurement failed}"
fi
else
# Not with fd 9: its last sleep would hold the lock after the run.
sampler "$dev" 9>&- &
sampler_pid=$!
out=$("$@" 2>"$STATE_DIR/stderr.log" < /dev/null); rc=$?
[ -n "$sampler_pid" ] && kill "$sampler_pid" 2>/dev/null
if [ "$rc" != 0 ]; then
err=$(tail -n 1 "$STATE_DIR/stderr.log" 2>/dev/null)
fail "${err:-measurement failed (exit $rc)}"
fi
fi
finished=$(date +%s)
[ -n "$out" ] || fail "no output from librespeed-cli"
# Both clients print an array of reports, one per server tested: the Go
# client marshals []report.JSONReport, and the Rust port mirrors that shape.
# jshn cannot load a bare array -- blobmsg wants an object at the top -- so the
# report array is wrapped before parsing. Found the hard way on a router: this
# is exactly the step no macOS test could reach.
json_load "{ \"reports\": $out }" 2>/dev/null || fail "unparseable output from librespeed-cli"
json_select reports 2>/dev/null || fail "unparseable output from librespeed-cli"
# The report is an array with one entry per server tested.
json_select 1 2>/dev/null || fail "empty report"
json_get_var ts timestamp
json_get_var ping ping
json_get_var jitter jitter
json_get_var download download
json_get_var upload upload
json_get_var bsent bytes_sent
json_get_var brecv bytes_received
json_get_var share share
srv_id=""; srv_name=""; srv_url=""
if json_select server 2>/dev/null; then
json_get_var srv_id id
json_get_var srv_name name
json_get_var srv_url url
json_select ..
fi
cli_ip=""; cli_org=""
if json_select client 2>/dev/null; then
json_get_var cli_ip ip
json_get_var cli_org org
json_select ..
fi
# The family of the address the backend saw is the family the test ran over.
# Derived here rather than asked of the CLI, so it works with any client; a
# redacted or missing address simply leaves the field out.
family=""
case "$cli_ip" in
*:*) family="ipv6" ;;
*.*) family="ipv4" ;;
esac
# Whether the run was encrypted is visible from the URL the CLI settled on;
# stored per measurement because the scheme option can change between runs.
proto=""
case "$srv_url" in
https:*) proto="https" ;;
http:*) proto="http" ;;
esac
# result.json -- the last completed measurement, not a database.
json_init
json_add_string timestamp "$ts"
json_add_int started "$started"
json_add_int finished "$finished"
json_add_string interface "$state_iface"
json_add_object server
# Absent until the CLI reports it; consumers treat null as unknown.
[ -n "$srv_id" ] && json_add_int id "$srv_id"
json_add_string name "$srv_name"
json_add_string url "$srv_url"
json_close_object
json_add_object client
json_add_string ip "$cli_ip"
json_add_string org "$cli_org"
json_close_object
[ -n "$family" ] && json_add_string family "$family"
[ -n "$proto" ] && json_add_string proto "$proto"
json_add_double download_mbps "$download"
json_add_double upload_mbps "$upload"
json_add_double ping_ms "$ping"
json_add_double jitter_ms "$jitter"
json_add_int bytes_sent "$bsent"
json_add_int bytes_received "$brecv"
json_add_string share "$share"
result="$(json_dump)"
atomic_write "$RESULT" "$result"
if [ "$hist_enabled" = "1" ]; then
mkdir -p "$(dirname "$hist_path")"
json_init
json_add_string timestamp "$ts"
json_add_int epoch "$finished"
json_add_string interface "$state_iface"
json_add_object server
[ -n "$srv_id" ] && json_add_int id "$srv_id"
json_add_string name "$srv_name"
json_add_string url "$srv_url"
json_close_object
[ -n "$family" ] && json_add_string family "$family"
[ -n "$proto" ] && json_add_string proto "$proto"
json_add_double download_mbps "$download"
json_add_double upload_mbps "$upload"
json_add_double ping_ms "$ping"
json_add_double jitter_ms "$jitter"
printf '%s\n' "$(json_dump)" >> "$hist_path"
# Retention. The epoch above makes this an integer comparison, so no date(1)
# runs here -- a year of history is thousands of lines and forking once per
# line is not something a router should be asked to do.
days=${hist_retention%d}
case "$days" in
''|*[!0-9]*) days=0 ;;
esac
if [ "$days" -gt 0 ]; then
cutoff=$(( finished - days * 86400 ))
# Compacting rewrites the whole file, so it happens in batches rather
# than whenever a single line falls out. Past the retention window every
# run expires something, and rewriting on each of them would push about
# a gigabyte a year through the flash instead of a few megabytes. The
# file therefore holds somewhat more than the window, and is trimmed
# once enough has accumulated to be worth the write.
# jshn writes '"epoch": 175...' with a space, so the match is
# tolerant and the digits are extracted rather than offset-counted.
expired=$(awk -v c="$cutoff" '
match($0, /"epoch":[[:space:]]*[0-9]+/) {
v = substr($0, RSTART, RLENGTH); gsub(/[^0-9]/, "", v)
if (v + 0 < c) n++
}
END { print n + 0 }
' "$hist_path")
total=$(wc -l < "$hist_path")
if [ "$expired" -ge 50 ] || [ "$expired" -ge $(( total / 2 )) ] && [ "$expired" -gt 0 ]; then
tmp="$hist_path.$$"
awk -v c="$cutoff" '
match($0, /"epoch":[[:space:]]*[0-9]+/) {
v = substr($0, RSTART, RLENGTH); gsub(/[^0-9]/, "", v)
if (v + 0 < c) next
}
{ print }
' "$hist_path" > "$tmp" && mv "$tmp" "$hist_path"
fi
fi
fi
write_state 0 "" "" "$finished" ""
# A result clears the failures of all interfaces on its device (down ones
# only as DHCP, DHCPv6 or static).
on=""
case "$dev" in *[!A-Za-z0-9_.-]*) ;; *)
ifdump_get on "[@.l3_device='$dev' || (@.up=false && @.device='$dev' && ($DEVP))].interface"
esac
for k in $state_iface $on; do
case "$k" in *[!A-Za-z0-9_]*) ;; *) rm -f "$FAILED/$k" ;; esac
done
# Refresh the server cache after the run, so the next one starts instantly:
# at most one list download a day, and the choice is the server this run
# actually used, looked up by the name the report carries.
if [ "$cache_fresh" = 0 ]; then
if uclient-fetch -q -T 15 -O "$LIST_CACHE.tmp" "$LIST_URL" 2>/dev/null \
&& [ -s "$LIST_CACHE.tmp" ]; then
mv "$LIST_CACHE.tmp" "$LIST_CACHE"
printf '%s\n' "$LIST_URL" > "$LIST_CACHE.src"
else
rm -f "$LIST_CACHE.tmp"
fi
if [ -s "$LIST_CACHE" ] && [ -n "$srv_name" ]; then
new_id=$(LIST="$LIST_CACHE" NAME="$srv_name" ucode -e '
let fs = require("fs");
let list = json(fs.readfile(getenv("LIST")) || "[]");
for (s in list)
if (s.name == getenv("NAME")) { print(s.id); break; }
' 2>/dev/null)
case "$new_id" in
''|*[!0-9]*) ;;
*) atomic_write "$CHOICE_CACHE" "$(date +%s) $new_id" ;;
esac
fi
fi
logger -t librespeed "measurement of $for_iface done: ${download} Mbps down, ${upload} Mbps up"
exit 0
|