Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions board/mister/de10nano/rootfs-overlay/etc/network/interfaces
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
# interface file auto-generated by buildroot
#
# MiSTer deviation (v9): each wlan stanza gains a pre-up wait loop before
# launching wpa_supplicant. USB WiFi drivers that initialise asynchronously
# (notably mainline rtw88/rtw89) may not have registered the nl80211 interface
# by the time ifupdown reaches the stanza, so wpa_supplicant would fail with
# "interface not found" on a cold boot. The loop polls `iw dev $IFACE info`
# (which succeeds only once nl80211 knows the device) for up to 20s. It runs
# AFTER the wpa_supplicant.conf existence check, so a system with no WiFi
# configured aborts the stanza first and never waits.
# MiSTer deviation (v9, revised): each wlan stanza gains a device-presence
# guard plus a wait loop before launching wpa_supplicant.
#
# THE RACE THE LOOP EXISTS FOR (v9, commit 4cf2fc7 / ADR 0016). USB WiFi
# drivers that initialise asynchronously -- notably the mainline rtw88/rtw89
# drivers ADR 0016 adopted in place of the out-of-tree morrownr forks -- may
# not have registered the nl80211 interface by the time ifupdown reaches the
# stanza, so wpa_supplicant fails with "interface not found" on a cold boot.
# The `iw dev $IFACE info` poll (which succeeds only once nl80211 knows the
# device) covers that window, for up to 20s.
#
# WHY THE GUARD IN FRONT OF IT. S40network runs `ifup -a`, which processes
# EVERY `auto` stanza -- including `auto wlan1` on a box with a single dongle.
# Unguarded, the poll then runs its full 20 iterations waiting for a device
# that will never appear: a measured 20s added to rcS on every WiFi boot, with
# S41dhcpcd, S49ntp, S50sshd and Main_MiSTer all serialised behind it. This is
# the same waste that made etc/udev/rules.d/70-persistent-net.rules target %k
# instead of stock's `-a` (docs/wifi-parity.md §9) -- that fix covered the
# hotplug path only, and this covers the boot path it left behind.
#
# The guard splits the three cases the way they actually differ:
#
# device absent the stanza aborts instantly. Nothing is lost by
# not waiting: the netdev's own "add" uevent brings
# it up whenever it does appear, via
# 70-persistent-net.rules -> etc/wifi-hotplug.sh,
# and that rule fires on udev's boot-time coldplug
# (S10udevd's `udevadm trigger`) as well as on later
# insertion. Waiting here polls for an event we are
# already subscribed to.
#
# device present, but the v9 poll below still covers it. cfg80211
# nl80211 not ready yet creates the netdev itself, so in practice this
# exits on the first iteration -- it is kept as a
# cheap safety net for the race v9 measured, not
# because it is expected to spin.
#
# device present and falls straight through, as before.
# ready
#
# Both lines run AFTER the wpa_supplicant.conf existence check, so a system
# with no WiFi configured aborts the stanza first and never reaches either.

auto lo
iface lo inet loopback

auto wlan0
iface wlan0 inet manual
pre-up [ -f /media/fat/linux/wpa_supplicant.conf ]
pre-up [ -e /sys/class/net/$IFACE ]
pre-up i=0; while [ $i -lt 20 ] && ! iw dev $IFACE info >/dev/null 2>&1; do sleep 1; i=$((i+1)); done
pre-up wpa_supplicant -s -B -P /run/wpa_supplicant.$IFACE.pid -i $IFACE -D nl80211,wext -c /media/fat/linux/wpa_supplicant.conf
post_up sleep 2
Expand All @@ -23,6 +58,7 @@ iface wlan0 inet manual
auto wlan1
iface wlan1 inet manual
pre-up [ -f /media/fat/linux/wpa_supplicant.conf ]
pre-up [ -e /sys/class/net/$IFACE ]
pre-up i=0; while [ $i -lt 20 ] && ! iw dev $IFACE info >/dev/null 2>&1; do sleep 1; i=$((i+1)); done
pre-up wpa_supplicant -s -B -P /run/wpa_supplicant.$IFACE.pid -i $IFACE -D nl80211,wext -c /media/fat/linux/wpa_supplicant.conf
post_up sleep 2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Restart ntpd once, the first time this box gets an address and ntpd is
# already running. MiSTer -- sourced by dhcpcd-run-hooks, like every hook here.
#
# WHY THIS EXISTS.
# This board has no RTC, so on a cold boot the clock starts at the epoch and
# every timestamp -- save states, screenshots, the OSD clock, syslog, ntpd's own
# log lines -- is wrong until NTP lands. S49ntp starts `ntpd -g` at a fixed
# point in rcS, whether or not a network exists yet, and on the common path it
# does not: WiFi association plus DHCP usually completes well after S49.
#
# ntpd survives that in one respect and not in the other, and the difference is
# the whole reason for this file:
#
# the interface half heals itself. ntpd is built with HAVE_RTNETLINK
# (output/build/ntp-4.2.8p18/config.h:786), so an address appearing is a
# netlink event: ntp_io.c:4874 schedules an endpoint rescan UPDATE_GRACE
# seconds out, and ntp_io.c:1980 -> ntp_peer.c:763 refresh_all_peerinterfaces
# -> peer_refresh_interface reattaches peers to it within ~3s. Nothing to fix.
#
# the DNS half does not. /etc/ntp.conf names 0-3.pool.ntp.org, and for a
# `server <hostname>` line the peer is created ONLY in the DNS callback
# (ntp_config.c:4459-4463 -> peer_name_resolved). At S49 there is no
# /etc/resolv.conf yet -- 20-resolv.conf writes it in this very hook pass --
# so resolution fails and NO PEERS EXIST. The rescan above then has nothing
# to reattach, and peer_xmit returns early on !peer->dstadr
# (ntp_proto.c:4157) regardless.
#
# So the clock waits on ntpd's DNS retry backoff, and nothing else.
# libntp/ntp_intres.c manage_dns_retry_interval walks 2-3-4-6-8-12-16-24-32-48-64
# seconds; config.h:1313 leaves IGNORE_DNS_ERRORS undefined, so DNSFLAGS is 0 and
# retmax is 64, not 1024. Attempts therefore land at t = 2, 5, 9, 15, 23, 35, 51,
# 75, 107, 155, 219, 283 ... seconds after ntpd started, then every 64s. The wait
# is however long until the next slot after the address arrives: a few seconds on
# a wired box, 10-20s on a typical WiFi boot, and 30s to a couple of minutes when
# the network turns up late (slow AP, a cable plugged in after boot, or WiFi
# configured on a card that has already been booted once).
#
# A restart re-resolves immediately against the resolv.conf that now exists.
# /etc/ntp.conf carries `iburst` on all four pool lines, so ntpd gets its samples
# at ~2s intervals rather than at minpoll, and S49ntp's `-g` permits the one
# large step a no-RTC box needs -- correct time within roughly 10-15s of the kick.
#
# WHY A DHCP HOOK, AND NOT AN INIT SCRIPT OR A UDEV RULE.
# Same argument as 90-timezone's, which see: the event we want is "this box now
# has an address", and dhcpcd is what publishes it. An init script can only ask
# whether there is a network a few seconds into boot, which is the wrong question
# -- that is precisely the state that created this problem. A udev rule is wrong
# in the other direction: a net `add` uevent fires when the netdev is CREATED,
# before association, before a lease, before a route, before resolv.conf. Kicking
# a DNS-dependent daemon there would reproduce the bug one layer down.
#
# dhcpcd is already fully event-driven underneath this: it runs in master mode
# and learns about new interfaces from netlink (RTM_NEWLINK ->
# dhcpcd_handleinterface, src/if-linux.c:1137), so a dongle plugged in three
# boots later reaches this hook with no init involvement at all.
#
# WHY sntp IS NOT USED HERE.
# `sntp -Ss` would set the clock in about a second instead of ten, but
# BR2_PACKAGE_NTP_SNTP also installs Buildroot's own /etc/init.d/S48sntp
# (package/ntp/ntp.mk:110-114), which runs sntp at boot -- one script before
# ntpd, with no network -- and would have to be suppressed. Ten seconds is not
# worth a package symbol plus a post-build prune.
#
# WHY NOT service_condcommand, WHICH 50-ntp.conf USES.
# It cannot work on this image, for two independent reasons. dhcpcd's detect_init
# (dhcpcd-run-hooks) finds no systemctl/rc-service/invoke-rc.d/service/sv here and
# falls through to its /etc/init.d branch, which tests `[ -x /etc/init.d/ntpd ]`
# -- ours is S49ntp. And service_status runs `$x/$1 status`, a verb S49ntp has no
# case for. Both fail silently, returning 1. This calls the script directly.
#
# ONCE PER BOOT, AND ONLY FOR AN ntpd THAT IS ALREADY RUNNING.
# The mkdir is the atomic test-and-set, on a tmpfs, so it re-arms every boot and
# cannot be won twice: two interfaces can BOUND at the same moment, and RENEW
# fires again every few hours for the life of the session. Restarting ntpd on
# each renewal would throw away its accumulated clock discipline for no reason,
# which is a real cost rather than a theoretical one.
#
# The running check is what keeps this from starting a daemon nobody asked for --
# if ntpd was disabled by hand, or via /etc/default/ntpd, that decision stands.
# It also handles the ordering case that looks like a bug and is not: on a WIRED
# box, S41dhcpcd blocks until the first lease is bound, so this hook fires DURING
# S41 -- before S49ntp has started ntpd at all. The check fails, the stamp is
# deliberately not claimed (it is taken last, after every gate), and rcS then
# starts ntpd a moment later with resolv.conf already in place, which is the
# outcome we wanted anyway. Nothing is lost and nothing needed kicking.
#
# A static-IP box configured only in /etc/network/interfaces never runs dhcpcd,
# so it never gets here. Accepted, and the same gap ADR 0025 accepts: ntpd's own
# backoff still converges there, just on its own schedule.
#
# DELAYS NOTHING. The work runs in a backgrounded subshell -- S49ntp's `restart`
# has a `sleep 1` in it, and a hook must not hold up dhcpcd's chain. Being
# sourced, this file also must not `exit`: that would end dhcpcd's whole hook run
# and take 20-resolv.conf and 30-hostname with it. The subshell answers both at
# once, and keeps every variable below out of dhcpcd's shell.
#
# ACQUISITION REASONS ONLY -- NOT RENEW OR REBIND.
# 90-timezone matches BOUND|REBOOT|REBIND|RENEW and their *6 forms, which is
# right for a hook gated on a persistent file it writes once. It is WRONG here,
# because this hook's guard is deliberately re-armable: a pass that bails out
# does not spend the stamp (see above), so any later matching event can still
# fire the kick. Match a renewal and that "later event" includes one arriving
# hours into the session, against an ntpd that has been synchronised the whole
# time -- restarting it there would discard its accumulated clock discipline for
# nothing. The wired ordering case makes that reachable rather than theoretical:
# it always bails out first, leaving the kick armed for exactly such an event.
#
# The distinction is in dhcpcd's own selection logic (src/dhcp.c:2499-2513):
# RENEW and REBIND are both chosen only when `state->old` is non-NULL -- we
# already had a lease, and merely extended it (RENEW) or moved it to another
# server or address (REBIND). Neither is an address ARRIVING, which is the event
# this hook exists to catch. BOUND is a new lease and REBOOT is a cached lease
# confirmed at startup (DHS_REBOOT); those two are the acquisitions.
#
# Nothing is lost by narrowing: every case this hook was written for -- WiFi
# associating minutes into a boot, a dongle plugged in later, an AP that comes
# back after the box is up -- is a NEW lease, hence BOUND. And an address that
# changes under a RENEW/REBIND needs no kick anyway: ntpd already holds peers by
# then, and the HAVE_RTNETLINK path above reattaches them within ~3s.
#
# The *6 reasons cannot fire on this kernel -- it is built without IPv6
# (linux.config: "# CONFIG_IPV6 is not set") -- but they cost nothing and are
# correct the day that changes.

case "$reason" in
BOUND|REBOOT|BOUND6|REBOOT6)
# if_up is compared as DATA, not executed. dhcpcd's own hooks write
# `if $if_up`, which works because dhcpcd sets it to the literal string
# true or false -- but this is sourced into dhcpcd's shell, where
# anything else in that variable would then run as a command there.
if [ "${if_up:-false}" = true ]; then
(
NTP_INIT=/etc/init.d/S49ntp
# S49ntp passes `-p $PIDFILE` to ntpd, so ntpd itself writes
# this. /var/run is a usr-merge-era symlink to /run (a tmpfs),
# so the file cannot outlive a boot. If /etc/default/ntpd has
# moved PIDFILE, the check below simply fails and this hook does
# nothing -- the safe direction.
NTPD_PID=/var/run/ntpd.pid
# Matched against the pidfile's process so a stale pidfile whose
# pid has been REUSED cannot make us restart -- see below.
NTPD_EXEC=/usr/sbin/ntpd
# tmpfs, so the once-per-boot stamp re-arms on every boot.
STAMPDIR=/run/ntp-kick

# Cheapest checks first; the stamp is claimed LAST, so a pass
# that bails out early leaves the one kick still available.
[ -x "$NTP_INIT" ] || exit 0
[ -s "$NTPD_PID" ] || exit 0
ntpd_pid="$(cat "$NTPD_PID" 2>/dev/null)"
# Reject empty, non-numeric, and 0. 0 matters on its own: `kill -0
# 0` signals the whole process GROUP, and start-stop-daemon -t
# against a pidfile containing 0 was measured to return success
# on this image -- either would make a garbage pidfile look live.
case "$ntpd_pid" in
''|0|*[!0-9]*) exit 0 ;;
esac
# Liveness AND identity, in one probe. Liveness alone is not
# enough: the pidfile cannot outlive a boot (tmpfs), but a crashed
# ntpd within one boot leaves one behind, and a long-running box
# can wrap pid_max and reuse that pid for something unrelated.
# We would then restart "ntpd" -- and S49ntp's stop() is stock's,
# which does `start-stop-daemon -K -p "$PIDFILE"` with no -x, so
# it would SIGTERM that unrelated process before starting a daemon
# that was not running. This hook is the first automatic caller of
# that script (50-ntp.conf's service_condcommand never fires here,
# see above), so the exposure is ours to avoid.
#
# -x makes start-stop-daemon match the process as well as the
# pidfile. Measured on both BusyBox vintages that matter -- ours
# (1.38.0) and stock's (1.33.1), the latter extracted from
# Linux_Image_creator_MiSTer's rootfs.tar.bz2 and run under
# qemu-arm -- against a live decoy process: without -x the probe
# returns 0, with `-x /usr/sbin/ntpd` it returns 1, and with the
# decoy's own name it returns 0. Note the match is by name, not by
# resolved path (a decoy whose exe was /usr/lib/.../sleep still
# matched `-x /usr/bin/sleep`), which is ample here: the case being
# excluded is a reused pid belonging to something that is not ntpd.
start-stop-daemon -K -t -q -p "$NTPD_PID" -x "$NTPD_EXEC" 2>/dev/null || exit 0

mkdir "$STAMPDIR" 2>/dev/null || exit 0

echo "ntpd: address acquired, restarting to re-resolve the NTP pool"
"$NTP_INIT" restart
) & # NTP-KICK-BACKGROUND -- scripts/test-ntp-kick.sh keys on this
# marker to run the body synchronously; keep it if the line moves.
fi
;;
esac
Loading
Loading