GCC Code Coverage Report


Directory: ./
File: boot/FPPINIT_Network.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 767 917 83.6%
Functions: 36 37 97.3%
Branches: 1354 2872 47.1%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2024 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include <arpa/inet.h>
14 #include <atomic>
15 #include <chrono>
16 #include <cstdio>
17 #include <ctime>
18 #include <filesystem>
19 #include <ifaddrs.h>
20 #include <list>
21 #include <map>
22 #include <net/if.h>
23 #include <netdb.h>
24 #include <netinet/in.h>
25 #include <set>
26 #include <sys/socket.h>
27 #include <sys/stat.h>
28 #include <sys/time.h>
29 #include <systemd/sd-daemon.h>
30 #include <thread>
31 #include <unistd.h>
32 #include <vector>
33
34 #include "FPPINIT.h"
35
36 // One single-quoted shell word: nothing inside single quotes is expanded.
37 // Every name below that reaches a shell goes through this, as well as the
38 // isShellSafeWord() checks the settings among them (TetherInterface,
39 // WifiRegulatoryDomain) already get: this runs as root, and one missed
40 // check should not be all that stands between a setting and the shell.
41 292 static std::string ShellSingleQuote(const std::string& v) {
42
1/2
✓ Branch 0 taken 292 times.
✗ Branch 1 not taken.
292 std::string out = "'";
43
2/2
✓ Branch 0 taken 2080 times.
✓ Branch 1 taken 292 times.
2372 for (char ch : v) {
44
5/16
✗ Branch 0 not taken.
✓ Branch 1 taken 2080 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 2080 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2080 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2080 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 2080 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
6240 out += (ch == '\'') ? std::string("'\\''") : std::string(1, ch);
45 }
46
1/2
✓ Branch 0 taken 292 times.
✗ Branch 1 not taken.
292 out += "'";
47 292 return out;
48 ✗ }
49
50 576 std::string FindTetherWIFIAdapater() {
51 576 std::string ret;
52
9/18
✓ Branch 0 taken 576 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 576 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✓ Branch 5 taken 562 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 14 times.
✓ Branch 8 taken 576 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 576 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 562 times.
✓ Branch 13 taken 14 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
1728 if (!getRawSetting("TetherInterface", ret) || ret.empty()) {
53 // see if we can find a non-associated interface
54
5/8
✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 562 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1124 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1124 times.
✓ Branch 7 taken 562 times.
1686 for (const auto& entry : std::filesystem::directory_iterator("/sys/class/net/")) {
55
2/6
✓ Branch 0 taken 1124 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1124 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
1124 std::string dev = entry.path().filename();
56
4/6
✓ Branch 0 taken 1124 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1124 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 205 times.
✓ Branch 5 taken 919 times.
2248 if (startsWith(dev, "wl")) {
57
4/8
✓ Branch 0 taken 205 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 205 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 205 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 205 times.
✗ Branch 7 not taken.
205 std::string output = execAndReturn("/usr/sbin/iw dev " + ShellSingleQuote(dev) + " link");
58
3/6
✓ Branch 0 taken 205 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 205 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 205 times.
410 if (contains(output, "Not connected")) {
59 ✗ return dev;
60 }
61
1/2
✓ Branch 0 taken 205 times.
✗ Branch 1 not taken.
205 }
62
3/6
✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 562 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 562 times.
✗ Branch 5 not taken.
1686 }
63 }
64 // Used unquoted in root commands; the UI only offers interface names.
65
6/8
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 562 times.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 14 times.
✓ Branch 6 taken 562 times.
✓ Branch 7 taken 14 times.
576 if (ret.empty() || !isShellSafeWord(ret)) {
66
1/2
✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
562 ret = "wlan0";
67 }
68 576 return ret;
69 576 }
70
71 91 static int getIntFromMap(const std::map<std::string, std::string>& m, const std::string& setting, int def) {
72
3/4
✓ Branch 0 taken 91 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 83 times.
91 if (m.find(setting) != m.end()) {
73
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 std::string v = m.at(setting);
74
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (v.empty()) {
75 ✗ return def;
76 }
77
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 return std::stoi(v);
78 8 }
79 83 return def;
80 }
81 // Boolean flags in interface.<if> are written by the UI as "1" (checked) or
82 // "" (unchecked); treat an API/hand-written "0"/"false" as off too rather than
83 // as "non-empty, therefore on".
84 50 static bool settingEnabled(const std::map<std::string, std::string>& m, const std::string& setting) {
85
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 auto it = m.find(setting);
86
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 17 times.
50 if (it == m.end()) {
87 33 return false;
88 }
89
7/10
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✓ Branch 5 taken 5 times.
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✓ Branch 9 taken 4 times.
17 return !it->second.empty() && it->second != "0" && it->second != "false";
90 }
91 4 static std::string netmaskToSubnet(const std::string& nm) {
92 const std::map<std::string, std::string> NETMASK2SUBNET = {
93 { "255.255.255.252", "30" },
94 { "255.255.255.248", "29" },
95 { "255.255.255.240", "28" },
96 { "255.255.255.224", "27" },
97 { "255.255.255.192", "26" },
98 { "255.255.255.128", "25" },
99 { "255.255.255.0", "24" },
100 { "255.255.254.0", "23" },
101 { "255.255.252.0", "22" },
102 { "255.255.248.0", "21" },
103 { "255.255.240.0", "20" },
104 { "255.255.224.0", "19" },
105 { "255.255.192.0", "18" },
106 { "255.255.128.0", "17" },
107 { "255.255.0.0", "16" }
108
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✓ Branch 3 taken 4 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
68 };
109
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if (NETMASK2SUBNET.contains(nm)) {
110
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 return NETMASK2SUBNET.at(nm);
111 }
112 ✗ return "24";
113
15/34
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 4 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 4 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 4 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 4 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 4 times.
✗ Branch 23 not taken.
✓ Branch 24 taken 4 times.
✗ Branch 25 not taken.
✓ Branch 26 taken 4 times.
✗ Branch 27 not taken.
✓ Branch 28 taken 4 times.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
8 }
114
115 25 static bool compareAndCopyFiles(std::map<std::string, std::string> filesNeeded) {
116 25 bool changed = false;
117
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 25 times.
61 for (auto& nf : filesNeeded) {
118
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 36 times.
36 if (FileExists(nf.first)) {
119 ✗ std::string c = GetFileContents(nf.first);
120 ✗ if (c != nf.second) {
121 ✗ PutFileContents(nf.first, nf.second);
122 ✗ changed = true;
123 }
124 ✗ } else {
125
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 PutFileContents(nf.first, nf.second);
126 36 changed = true;
127 }
128 }
129 25 return changed;
130 }
131 // A value for a WIFI: QR code field, where \ ; , : and " are escaped.
132 16 static std::string WifiQREscape(const std::string& v) {
133 16 std::string out;
134
2/2
✓ Branch 0 taken 148 times.
✓ Branch 1 taken 16 times.
164 for (char ch : v) {
135
5/10
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 148 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 148 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 148 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 148 times.
148 if (ch == '\\' || ch == ';' || ch == ',' || ch == ':' || ch == '"') {
136 ✗ out += '\\';
137 }
138
1/2
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
148 out += ch;
139 }
140 16 return out;
141 ✗ }
142 8 static std::string CreateHostAPDConfig(const std::string& interface) {
143
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 std::string ssid = "FPP";
144
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 std::string psk = "Christmas";
145
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
16 std::string WifiRegulatoryDomain = "US";
146
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
16 getRawSetting("WifiRegulatoryDomain", WifiRegulatoryDomain);
147
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
16 getRawSetting("TetherSSID", ssid);
148
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 getRawSetting("TetherPSK", psk);
149
150 8 std::string content;
151
5/10
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
8 content.append("interface=").append(interface).append("\ndriver=nl80211\nwpa_passphrase=").append(psk).append("\nssid=");
152
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
8 content.append(ssid).append("\ncountry_code=").append(WifiRegulatoryDomain);
153
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 content.append("\nhw_mode=g\nchannel=11\nmacaddr_acl=0\nauth_algs=3\nwpa=2\n");
154
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 content.append("wpa_key_mgmt=WPA-PSK\nwpa_pairwise=TKIP\nrsn_pairwise=CCMP\nwmm_enabled=0\nignore_broadcast_ssid=0\nwpa_group_rekey=3600\nwpa_gmk_rekey=86400\n");
155
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 content.append("reassociation_deadline=3000\npmk_r1_push=1\nft_over_ds=0\nbss_transition=1\n");
156
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 if (FileExists("/usr/bin/qrencode")) {
157 // The SSID and PSK are single-quoted for the shell: inside the double
158 // quotes they used to sit in, "$(...)" in either setting ran as root.
159
7/14
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 8 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 8 times.
✗ Branch 13 not taken.
8 std::string payload = ShellSingleQuote("WIFI:T:WPA;S:" + WifiQREscape(ssid) + ";P:" + WifiQREscape(psk) + ";;");
160 // Low error correction MAY result in a QR that can be pixel doubled which generally works better for phones to pick up
161
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::string cmd = "/usr/bin/qrencode -t ASCII -m 0 -s 6 -l L -o ";
162
5/10
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
8 cmd.append(ShellSingleQuote(FPP_MEDIA_DIR + "/tmp/wifi-L.ascii")).append(" ").append(payload);
163
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 exec(cmd);
164 // Generate a High error correction as well. If we cannot pixel double, this will give more info to the phone/device
165
166
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 cmd = "/usr/bin/qrencode -t ASCII -m 3 -s 6 -l H -o ";
167
5/10
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
8 cmd.append(ShellSingleQuote(FPP_MEDIA_DIR + "/tmp/wifi-H.ascii")).append(" ").append(payload);
168
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 exec(cmd);
169 8 }
170
171 8 return content;
172 8 }
173 // The [DHCPServer] block for the tethering AP. The pool comes from
174 // config/interface.<iface> (the DHCP Pool Offset / Size fields on the Network
175 // page) whenever that file exists: maybeEnableTethering() rewrites this
176 // .network file *after* setupNetwork() has already generated it from the
177 // interface config, so hardcoding the pool here silently reverted whatever the
178 // user had configured -- a box set to offset 100 kept handing out addresses
179 // from .10 because the second write won. 10/100 remains the default for a
180 // board that has no interface config at all (fresh install, no /config dir).
181 6 static void AppendTetherDHCPServerSection(std::string& content, const std::string& interface) {
182 6 int offset = 10;
183 6 int poolSize = 100;
184
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 const std::string interfaceConfigFile = FPP_MEDIA_DIR + "/config/interface." + interface;
185
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 3 times.
6 if (FileExists(interfaceConfigFile)) {
186
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 auto interfaceSettings = loadSettingsFile(interfaceConfigFile);
187
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 offset = getIntFromMap(interfaceSettings, "DHCPOFFSET", offset);
188
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 poolSize = getIntFromMap(interfaceSettings, "DHCPPOOLSIZE", poolSize);
189 3 }
190 // PersistLeases=yes is already the default on Debian 13/trixie (systemd
191 // >= 256, via systemd-networkd-persistent-storage.service); set explicitly
192 // here for clarity and so behavior doesn't silently depend on an OS-level
193 // default we don't control. Harmlessly ignored as an unknown key on older
194 // systemd (verified: an unrecognized [DHCPServer] key logs a warning but
195 // doesn't break parsing).
196
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 content.append("[DHCPServer]\n"
197 "PersistLeases=yes\n"
198 "PoolOffset=")
199
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 .append(std::to_string(offset))
200
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 .append("\nPoolSize=")
201
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 .append(std::to_string(poolSize))
202
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 .append("\nEmitDNS=no\n\n");
203
5/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 5 times.
6 if (FileExists(FPP_MEDIA_DIR + "/config/leases." + interface)) {
204
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
1 content.append(GetFileContents(FPP_MEDIA_DIR + "/config/leases." + interface));
205
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 content.append("\n");
206 }
207 6 }
208 37 static void unblockWifi() {
209
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 35 times.
37 if (FileExists("/usr/sbin/rfkill")) {
210
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 exec("/usr/sbin/rfkill unblock wifi");
211
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 exec("/usr/sbin/rfkill unblock all");
212 }
213 37 }
214
215 // Consumes the marker resetConfig.php drops for a "network" area reset. Must
216 // only be called from the boot "start" path (FPPINIT.cpp), NOT from
217 // setupNetwork() -- that function is also invoked live via `fppinit
218 // setupNetwork <iface>` (network.php's interface-save path) while
219 // systemd-networkd is already running, which would reintroduce the exact
220 // live-daemon race this is meant to avoid. Boot's "start" action runs before
221 // systemd-networkd.service is (re)started for this boot -- fppinit.service is
222 // ordered Before= it -- so calling this only from there is race-free by
223 // construction: the daemon that owns /var/lib/systemd/network/dhcp-server-lease/
224 // isn't running yet.
225 //
226 // TODO(FPP11): this is one of several ad-hoc "do this on next boot" marker
227 // files scattered around FPP (see also /fpp_kiosk, fpp_expand_rootfs,
228 // /fppos_upgraded). Standardize these on a single settings-key convention
229 // instead of bare marker files.
230 3 void consumePendingDhcpLeaseReset() {
231
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string marker = FPP_MEDIA_DIR + "/config/dhcpLeaseResetPending";
232
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
3 if (!FileExists(marker)) {
233 2 return;
234 }
235 // One JSON file per interface (not a per-lease directory), e.g.
236 // /var/lib/systemd/network/dhcp-server-lease/eth0 -- confirmed against a
237 // live device (systemd 257). Wipe all of them.
238
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (DirectoryExists("/var/lib/systemd/network/dhcp-server-lease")) {
239
5/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 1 times.
3 for (const auto& entry : std::filesystem::directory_iterator("/var/lib/systemd/network/dhcp-server-lease")) {
240 2 std::error_code ec;
241 2 std::filesystem::remove(entry.path(), ec);
242 1 }
243 }
244 1 unlink(marker.c_str());
245
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 }
246
247 // hostapd is up or on its way up or down. `systemctl is-active` prints one
248 // word; "inactive" contains "active", so compare the whole word.
249 18 static bool hostapdRunning() {
250
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 std::string state = execAndReturn("/usr/bin/systemctl is-active hostapd");
251
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 TrimWhiteSpace(state);
252
9/16
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 15 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 15 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 15 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 15 times.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✓ Branch 15 taken 15 times.
36 return state == "active" || state == "activating" || state == "deactivating" || state == "reloading";
253 18 }
254
255 25 void setupNetwork(bool fullReload) {
256
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 unblockWifi();
257
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 auto dnsSettings = loadSettingsFile(FPP_MEDIA_DIR + "/config/dns");
258
259 // Load global gateway configuration with migration support
260
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 auto gatewaySettings = loadSettingsFile(FPP_MEDIA_DIR + "/config/gateway");
261
3/6
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
25 std::string globalGateway = gatewaySettings["GATEWAY"];
262
263 // Migration: If no global gateway exists, check for old per-interface gateways
264
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 if (globalGateway.empty()) {
265 25 std::error_code cfgEc;
266
6/10
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 25 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 25 times.
✓ Branch 9 taken 25 times.
50 for (const auto& entry : std::filesystem::directory_iterator(FPP_MEDIA_DIR + "/config", cfgEc)) {
267
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 std::string filename = entry.path().filename();
268
4/6
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✓ Branch 5 taken 8 times.
50 if (startsWith(filename, "interface.")) {
269
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 auto interfaceSettings = loadSettingsFile(entry.path());
270
3/6
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
17 std::string oldGateway = interfaceSettings["GATEWAY"];
271
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 17 times.
17 if (!oldGateway.empty() && oldGateway != "\"\"") {
272 // Found an old gateway setting - migrate it to global
273 ✗ printf("FPP - Migrating gateway %s from %s to global configuration\n",
274 oldGateway.c_str(), filename.c_str());
275
276 // Save to global gateway file
277 ✗ std::string gatewayContent = "GATEWAY=\"" + oldGateway + "\"\n";
278 ✗ PutFileContents(FPP_MEDIA_DIR + "/config/gateway", gatewayContent);
279 ✗ globalGateway = oldGateway;
280
281 // Remove old GATEWAY setting from interface file (optional cleanup)
282 ✗ std::string interfaceContent = GetFileContents(entry.path());
283 ✗ size_t pos = interfaceContent.find("GATEWAY=");
284 ✗ if (pos != std::string::npos) {
285 ✗ size_t endPos = interfaceContent.find('\n', pos);
286 ✗ if (endPos != std::string::npos) {
287 ✗ interfaceContent.erase(pos, endPos - pos + 1);
288 ✗ PutFileContents(entry.path(), interfaceContent);
289 }
290 }
291 ✗ break; // Use first gateway found
292 ✗ }
293
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 }
294
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
50 }
295 }
296
297
3/6
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
50 std::string dns = dnsSettings["DNS1"];
298
4/8
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 25 times.
50 if (dnsSettings["DNS2"] != "") {
299 ✗ dns += ",";
300 ✗ dns += dnsSettings["DNS2"];
301 }
302
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
50 std::string WifiRegulatoryDomain = "US";
303
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 getRawSetting("WifiRegulatoryDomain", WifiRegulatoryDomain);
304
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 if (isShellSafeWord(WifiRegulatoryDomain)) {
305
3/6
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
25 exec("/usr/sbin/iw reg set " + ShellSingleQuote(WifiRegulatoryDomain));
306 }
307
308
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 int tetherEnabled = getRawSettingInt("EnableTethering", 0);
309
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 std::string tetherInterface = FindTetherWIFIAdapater();
310
311
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 const std::string dhcpProxyFile = FPP_MEDIA_DIR + "/config/dhcp-proxy-config.conf";
312 25 std::string dhcpProxies;
313
314 25 bool ipForward = false;
315 25 bool hostapd = false;
316 25 std::map<std::string, std::string> filesNeeded;
317 25 std::list<std::string> filesToConsider;
318 25 std::list<std::string> commandsToRun;
319 25 std::list<std::string> postCommandsToRun;
320 // error_code forms: a directory that isn't there (no wpasupplicant
321 // package, say) is simply empty, not an uncaught exception that ends
322 // fppinit before any of the network is configured.
323 25 std::error_code dirEc;
324
5/8
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 29 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 29 times.
✓ Branch 7 taken 25 times.
54 for (const auto& entry : std::filesystem::directory_iterator("/etc/systemd/network", dirEc)) {
325
2/4
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
✗ Branch 3 not taken.
29 std::string dev = entry.path().filename();
326
8/30
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 29 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 29 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 29 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 29 times.
✓ Branch 16 taken 29 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 29 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✓ Branch 21 taken 29 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
87 if (startsWith(dev, "10-") && endsWith(dev, ".network")) {
327 ✗ filesToConsider.emplace_back("/etc/systemd/network/" + dev);
328 }
329 54 }
330
5/8
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 25 times.
✓ Branch 7 taken 25 times.
50 for (const auto& entry : std::filesystem::directory_iterator("/etc/wpa_supplicant", dirEc)) {
331
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 std::string dev = entry.path().filename();
332
8/30
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 25 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 25 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 25 times.
✓ Branch 16 taken 25 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 25 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✓ Branch 21 taken 25 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
75 if (startsWith(dev, "wpa_supplicant-wl") && endsWith(dev, ".conf")) {
333 ✗ filesToConsider.emplace_back("/etc/wpa_supplicant/" + dev);
334 }
335 50 }
336
3/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 24 times.
25 if (FileExists("/etc/hostapd/hostapd.conf")) {
337
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 filesToConsider.emplace_back("/etc/hostapd/hostapd.conf");
338 }
339
4/8
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 25 times.
✗ Branch 7 not taken.
25 if (std::filesystem::exists(FPP_MEDIA_DIR + "/config")) {
340
6/10
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 25 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 25 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 25 times.
✓ Branch 9 taken 25 times.
50 for (const auto& entry : std::filesystem::directory_iterator(FPP_MEDIA_DIR + "/config")) {
341
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 std::string dev = entry.path().filename();
342
4/6
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✓ Branch 5 taken 8 times.
50 if (startsWith(dev, "interface.")) {
343 17 bool validConfig = true;
344
3/6
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
17 auto interfaceSettings = loadSettingsFile(FPP_MEDIA_DIR + "/config/" + dev);
345
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 std::string interface = dev.substr(10);
346
347
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 std::string content = "[Match]\nName=";
348 17 std::string addressLines;
349
350
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
34 int DHCPSERVER = getIntFromMap(interfaceSettings, "DHCPSERVER", 0);
351
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
34 int DHCPOFFSET = getIntFromMap(interfaceSettings, "DHCPOFFSET", 100);
352
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
34 int DHCPPOOLSIZE = getIntFromMap(interfaceSettings, "DHCPPOOLSIZE", 50);
353
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
34 int ROUTEMETRIC = getIntFromMap(interfaceSettings, "ROUTEMETRIC", 0);
354
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 int IPFORWARDING = getIntFromMap(interfaceSettings, "IPFORWARDING", 0);
355
356
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 16 times.
17 if (DHCPSERVER == 1) {
357
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 std::string address = interfaceSettings["ADDRESS"];
358
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 address = address.substr(0, address.find_last_of("."));
359
5/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
1 dhcpProxies += "RewriteRule ^" + address + ".([0-9:]*)$ http://" + address + ".$1/ [P,L]\n";
360
5/10
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
1 dhcpProxies += "RewriteRule ^" + address + ".([0-9:]*)/(.*)$ http://" + address + ".$1/$2 [P,L]\n\n";
361 1 }
362
363
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 content.append(interface).append("\nType=");
364
4/6
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✓ Branch 5 taken 2 times.
34 if (startsWith(interface, "wl")) {
365
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 content.append("wlan\n\n");
366 } else {
367
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 content.append("ether\n\n");
368 }
369
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 content.append("[Network]\n");
370
4/8
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 17 times.
34 if (dnsSettings["DNS1"] != "") {
371 ✗ content.append("DNS=").append(dnsSettings["DNS1"]).append("\n");
372 }
373
4/8
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 17 times.
34 if (dnsSettings["DNS2"] != "") {
374 ✗ content.append("DNS=").append(dnsSettings["DNS2"]).append("\n");
375 }
376
5/8
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✓ Branch 7 taken 4 times.
34 if (interfaceSettings["PROTO"] == "dhcp") {
377
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 addressLines = "DHCP=yes\n";
378 13 DHCPSERVER = 0;
379 } else {
380
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 addressLines = "Address=";
381
9/18
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 4 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 4 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 4 times.
✗ Branch 17 not taken.
16 addressLines.append(interfaceSettings["ADDRESS"]).append("/").append(netmaskToSubnet(interfaceSettings["NETMASK"])).append("\n");
382 }
383
4/6
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✓ Branch 5 taken 2 times.
34 if (startsWith(interface, "wl")) {
384 // wifi settings
385
5/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 13 times.
15 if (tetherEnabled == 1 && interface == tetherInterface) {
386
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
6 filesNeeded["/etc/hostapd/hostapd.conf"] = CreateHostAPDConfig(interface);
387 2 DHCPSERVER = 1;
388 2 hostapd = true;
389
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 addressLines.append("Address=192.168.8.1/24\n");
390
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
26 } else if (!interfaceSettings["SSID"].empty()) {
391
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
26 bool wpa3 = settingEnabled(interfaceSettings, "WPA3");
392
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
26 bool backupWpa3 = settingEnabled(interfaceSettings, "BACKUPWPA3");
393
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 std::string wpa = "ctrl_interface=/var/run/wpa_supplicant\nctrl_interface_group=0\nupdate_config=1\ncountry=";
394
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 wpa.append(WifiRegulatoryDomain);
395 // Protected Management Frames (802.11w) as a GLOBAL "optional"
396 // (pmf=1), never as a per-network ieee80211w=1. The difference
397 // matters: for the global default wpa_supplicant checks whether
398 // the driver advertises a BIP cipher and silently skips PMF if
399 // not, but a per-network ieee80211w is honoured blindly. The
400 // brcmfmac firmware on the Pi Zero W (BCM43430; the Pi 3B shares
401 // the chip) has no MFP support, so with ieee80211w=1 it negotiated
402 // PMF with any PMF-capable AP (most Wi-Fi 6 routers), failed to
403 // install the IGTK ("WPA: Failed to configure IGTK to the driver")
404 // and dropped an otherwise-good association - looked like a bad
405 // password. On boards whose driver does advertise a BIP cipher
406 // (BCM43455, rtw88, ...) nothing changes: SAE still negotiates
407 // PMF and WPA2 APs set to PMF-required still work.
408 // This mirrors what NetworkManager does on stock Raspberry Pi
409 // OS. GitHub issue #2953.
410
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 wpa.append("\npmf=1\n");
411
4/4
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 6 times.
13 if (wpa3 || backupWpa3) {
412 // 2 = hash-to-element AND hunting-and-pecking. With 1 (H2E only)
413 // wpa_supplicant skips every BSS of an AP that lacks H2E ("skip -
414 // SAE H2E required, but not supported by the AP"), so the WPA3
415 // checkbox could never connect to such an AP on any board.
416
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 wpa.append("sae_pwe=2\n");
417 }
418
4/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
39 wpa.append("\nnetwork={\n ssid=\"").append(interfaceSettings["SSID"]);
419
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
26 if (!interfaceSettings["PSK"].empty()) {
420
4/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
39 wpa.append("\"\n psk=\"").append(interfaceSettings["PSK"]);
421 }
422
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 wpa.append("\"\n key_mgmt=");
423
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 9 times.
13 if (wpa3) {
424
5/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
12 wpa.append("SAE WPA-PSK\n sae_password=\"").append(interfaceSettings["PSK"]).append("\"");
425 } else {
426
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 wpa.append("WPA-PSK");
427 }
428
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 13 times.
26 if (settingEnabled(interfaceSettings, "HIDDEN")) {
429 ✗ wpa.append("\n scan_ssid=1");
430 }
431
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 wpa.append("\n priority=100\n}\n\n");
432
16/32
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 11 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 11 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 11 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 11 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 11 times.
✓ Branch 15 taken 2 times.
✓ Branch 16 taken 11 times.
✓ Branch 17 taken 2 times.
✓ Branch 18 taken 13 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 13 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 11 times.
✓ Branch 23 taken 2 times.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
61 if (!interfaceSettings["BACKUPSSID"].empty() && interfaceSettings["BACKUPSSID"] != "\"\"") {
433
4/8
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 11 times.
✗ Branch 7 not taken.
33 wpa.append("\nnetwork={\n ssid=\"").append(interfaceSettings["BACKUPSSID"]);
434
3/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
22 if (!interfaceSettings["BACKUPPSK"].empty()) {
435
4/8
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 11 times.
✗ Branch 7 not taken.
33 wpa.append("\"\n psk=\"").append(interfaceSettings["BACKUPPSK"]);
436 }
437
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 wpa.append("\"\n key_mgmt=");
438
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 7 times.
11 if (backupWpa3) {
439
5/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
12 wpa.append("SAE WPA-PSK\n sae_password=\"").append(interfaceSettings["BACKUPPSK"]).append("\"");
440 } else {
441
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 wpa.append("WPA-PSK");
442 }
443
3/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 11 times.
22 if (settingEnabled(interfaceSettings, "BACKUPHIDDEN")) {
444 ✗ wpa.append("\n scan_ssid=1");
445 }
446
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 wpa.append("\n priority=90\n}\n\n");
447 }
448
4/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
13 filesNeeded["/etc/wpa_supplicant/wpa_supplicant-" + interface + ".conf"] = wpa;
449 // Queue these unconditionally rather than gating them on
450 // `systemctl is-active`/`is-enabled` queries: during early
451 // boot the systemd manager is saturated (dbus etc. take many
452 // seconds to come up), so those synchronous queries block for
453 // ~9s and stall fppinit -- and this wpa_supplicant@ branch
454 // only runs once a wifi interface exists. The commands run
455 // only if the wpa config actually changed (see commandsToRun
456 // handling below), and `enable` + `reload-or-restart` are both
457 // idempotent, so the pre-checks bought nothing but the stall.
458
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
13 commandsToRun.emplace_back("systemctl enable \"wpa_supplicant@" + interface + ".service\" &");
459
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
13 commandsToRun.emplace_back("systemctl reload-or-restart \"wpa_supplicant@" + interface + ".service\" &");
460
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
13 postCommandsToRun.emplace_back("systemctl reload-or-restart \"wpa_supplicant@" + interface + ".service\" &");
461 13 } else {
462 ✗ validConfig = false;
463 }
464 }
465
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 14 times.
17 if (DHCPSERVER) {
466
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 content.append("DHCPServer=yes\n");
467 }
468
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (IPFORWARDING == 1) {
469 ✗ content.append("IPForward=yes\n");
470 ✗ ipForward = true;
471
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 16 times.
17 } else if (IPFORWARDING == 2) {
472 // systemd-networkd might not have masquerade support compiled in, we'll just do it manually
473 1 ipForward = true;
474
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 exec("/usr/sbin/nft add table nat");
475
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 exec("/usr/sbin/nft 'add chain nat postrouting { type nat hook postrouting priority 100 ; }'");
476
4/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
1 exec("/usr/sbin/nft add rule nat postrouting oif " + ShellSingleQuote(interface) + " masquerade");
477 }
478
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 content.append(addressLines);
479 // some of the FPP7 images don't support this setting. They use older gcc
480 // Pi Zero 2 W has issues with IgnoreCarrierLoss causing network visibility problems (Issue #2487)
481 #ifdef PLATFORM_PI
482 ✗ if (!isPiZero2W()) {
483 ✗ content.append("IgnoreCarrierLoss=5s\n");
484 }
485 #else
486
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 content.append("IgnoreCarrierLoss=5s\n");
487 #endif
488
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 content.append("\n");
489
490 // Apply global gateway to static interfaces only
491
4/20
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 17 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 17 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 17 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
17 if (!globalGateway.empty() && interfaceSettings["PROTO"] != "dhcp") {
492 ✗ content.append("[Route]\nGateway=").append(globalGateway).append("\n");
493 ✗ if (ROUTEMETRIC != 0) {
494 ✗ content.append("Metric=").append(std::to_string(ROUTEMETRIC)).append("\n");
495 }
496 }
497
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 content.append("\n");
498
5/8
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✓ Branch 7 taken 4 times.
34 if (interfaceSettings["PROTO"] == "dhcp") {
499
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 content.append("[DHCPv4]\nClientIdentifier=mac\nUseDomains=true\n");
500 // FPP owns the hostname (checkHostName writes /etc/hostname), so
501 // there is nothing to learn from the lease. Leaving this at its
502 // default of yes makes networkd call org.freedesktop.hostname1 the
503 // moment the lease lands, which D-Bus-activates systemd-hostnamed,
504 // which in turn activates polkit to authorize the call. Measured on
505 // a single-core AM335x board that chain cost ~21s of boot (4.3s
506 // hostnamed + 16.8s polkitd) for a hostname that was already
507 // correct; nothing else on an FPP box uses polkit at all.
508
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 content.append("UseHostname=false\n");
509 // Only use NTP from DHCP if explicitly enabled
510 13 std::string useNTPFromDHCP;
511
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 getRawSetting("UseNTPFromDHCP", useNTPFromDHCP);
512
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 if (useNTPFromDHCP != "1") {
513
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 content.append("UseNTP=no\n");
514 }
515
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
17 } else if (ROUTEMETRIC != 0) {
516 ✗ content.append("[DHCPv4]\n");
517 }
518
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 17 times.
17 if (ROUTEMETRIC != 0) {
519 ✗ content.append("RouteMetric=").append(interfaceSettings["ROUTEMETRIC"]).append("\n\n");
520 ✗ content.append("[IPv6AcceptRA]\nRouteMetric=").append(interfaceSettings["ROUTEMETRIC"]).append("\n\n");
521 } else {
522
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 content.append("\n");
523 }
524
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 14 times.
17 if (DHCPSERVER == 1) {
525 // PersistLeases=yes is already the default on Debian 13/trixie
526 // (systemd >= 256, via systemd-networkd-persistent-storage.service);
527 // set explicitly here for clarity and so behavior doesn't silently
528 // depend on an OS-level default we don't control. Harmlessly ignored
529 // as an unknown key on older systemd (verified: an unrecognized
530 // [DHCPServer] key logs a warning but doesn't break parsing).
531
5/10
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
3 content.append("[DHCPServer]\nPersistLeases=yes\nDefaultLeaseTimeSec=8h\nMaxLeaseTimeSec=8h\nPoolOffset=").append(std::to_string(DHCPOFFSET)).append("\nPoolSize=").append(std::to_string(DHCPPOOLSIZE)).append("\n");
532
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
6 if (!dnsSettings["DNS1"].empty()) {
533 ✗ content.append("EmitDNS=yes\nDNS=").append(dnsSettings["DNS1"]).append("\n");
534 }
535
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 content.append("\n");
536
5/8
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 1 times.
3 if (FileExists(FPP_MEDIA_DIR + "/config/leases." + interface)) {
537
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
2 content.append(GetFileContents(FPP_MEDIA_DIR + "/config/leases." + interface));
538 }
539
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 content.append("\n");
540 }
541
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 if (validConfig) {
542
4/8
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 17 times.
✗ Branch 7 not taken.
17 filesNeeded["/etc/systemd/network/10-" + interface + ".network"] = content;
543 }
544 17 }
545 50 }
546 }
547
548 // If tethering is explicitly enabled (==1) but no interface configs set it up,
549 // configure it now (handles fresh installs with no /config directory)
550
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 21 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
25 if (tetherEnabled == 1 && !hostapd) {
551
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
6 filesNeeded["/etc/hostapd/hostapd.conf"] = CreateHostAPDConfig(tetherInterface);
552
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::string content = "[Match]\nName=";
553
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 content.append(tetherInterface).append("\nType=wlan\n\n"
554 "[Network]\n"
555 "DHCP=no\n"
556 "Address=192.168.8.1/24\n"
557 "DHCPServer=yes\n\n");
558
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 AppendTetherDHCPServerSection(content, tetherInterface);
559
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
2 filesNeeded["/etc/systemd/network/10-" + tetherInterface + ".network"] = content;
560 2 hostapd = true;
561 2 }
562
563 25 bool reloadApache = false;
564
5/8
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 24 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 25 times.
25 if (dhcpProxies.empty() && FileExists(dhcpProxyFile)) {
565 ✗ unlink(dhcpProxyFile.c_str());
566 ✗ reloadApache = true;
567
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 24 times.
25 } else if (!dhcpProxies.empty()) {
568
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 PutFileContents(dhcpProxyFile, dhcpProxies);
569 1 reloadApache = true;
570 }
571
572 // FPP's NTP daemon is chrony. Large-offset stepping (ntpd's "-g") is handled
573 // by the "makestep" directive in /etc/chrony/chrony.conf, so there is nothing
574 // to inject per-boot. Here we only reconcile DHCP-provided NTP servers with
575 // the UseNTPFromDHCP setting: the networkd-dispatcher chrony hook drops any
576 // DHCP servers into /run/chrony-dhcp (consumed via "sourcedir" in
577 // chrony.conf) when the setting is enabled. When it's disabled, make sure
578 // none linger from a previous configuration. chrony is started after
579 // fpp_postnetwork, so clearing the dir now is enough -- no reload needed.
580 25 std::string useNTPFromDHCP;
581
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 getRawSetting("UseNTPFromDHCP", useNTPFromDHCP);
582
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 if (useNTPFromDHCP != "1") {
583
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
50 exec("/usr/bin/rm -f /run/chrony-dhcp/*.sources 2>/dev/null");
584 }
585
586 // Auto-detect default gateway as a fallback NTP server so that chrony has
587 // a reachable peer when internet pool servers are blocked (e.g. behind
588 // double NAT / a gateway that drops NTP replies -- see issue #2736). LAN
589 // traffic to the gateway does not cross any NAT boundary. chrony gracefully
590 // ignores non-responsive servers, so this is safe even if the gateway does
591 // not run NTP.
592 {
593
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
50 std::string gw = execAndReturn(
594
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 "/usr/bin/ip route show default 2>/dev/null | /usr/bin/awk '{print $3}'");
595
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 TrimWhiteSpace(gw);
596
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 7 times.
25 if (!gw.empty()) {
597 struct sockaddr_in sa;
598
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 if (inet_pton(AF_INET, gw.c_str(), &(sa.sin_addr)) == 1) {
599
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 std::string chronyConf = GetFileContents("/etc/chrony/chrony.conf");
600
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 if (chronyConf.find("server " + gw) == std::string::npos) {
601 chronyConf += "\n# Auto-detected default gateway fallback\nserver "
602
3/6
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
18 + gw + " minpoll 10 maxpoll 12 iburst\n";
603
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
36 PutFileContents("/etc/chrony/chrony.conf", chronyConf);
604
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 execbg("/usr/bin/systemctl reload-or-restart chrony.service &");
605
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 printf("FPP - Added default gateway %s as fallback NTP server\n",
606 gw.c_str());
607 }
608 18 }
609 }
610 25 }
611 25 bool changed = false;
612
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 25 times.
26 for (auto& ftc : filesToConsider) {
613
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (filesNeeded.find(ftc) == filesNeeded.end()) {
614 // No longer used/needed, remove it
615 1 unlink(ftc.c_str());
616 1 changed = true;
617
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
2 if (startsWith(ftc, "/etc/wpa_supplicant/")) {
618 ✗ std::string intf = ftc.substr(ftc.find("-") + 1);
619 ✗ intf = intf.substr(0, intf.find("."));
620 ✗ execbg("/usr/bin/systemctl disable wpa_supplicant@" + intf + ".service &");
621 ✗ }
622 }
623 }
624
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 changed |= compareAndCopyFiles(filesNeeded);
625
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 5 times.
25 if (changed) {
626 20 bool localFullReload = false;
627
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 printf("Need to restart/reload stuff.\n");
628
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 4 times.
20 if (!hostapd) {
629 // Out of AP mode before the Wi-Fi client starts: while hostapd
630 // still holds the radio, a wpa_supplicant started beside it often
631 // fails to associate, and leaving AP mode from the UI then needed
632 // a power cycle (#2825).
633
6/10
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 16 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 14 times.
64 if (contains(execAndReturn("/usr/bin/systemctl is-enabled hostapd"), "enabled")) {
634
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 execbg("/usr/bin/systemctl disable hostapd.service &");
635 2 localFullReload = true;
636 }
637
3/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 14 times.
16 if (hostapdRunning()) {
638 // Bounded: an unresponsive hostapd (driver/dbus hiccup) must not be
639 // able to wedge fppinit indefinitely - this runs synchronously in
640 // the interactive "Restart Network" request (network_apply_interface),
641 // so a hang here blocks that whole HTTP request and can make the
642 // rest of the appliance look unresponsive until it clears.
643
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 exec("/usr/bin/timeout 10 /usr/bin/systemctl stop hostapd.service");
644
3/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
2 if (hostapdRunning()) {
645
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - hostapd did not stop within 10s, killing it\n");
646
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 exec("/usr/bin/systemctl kill --signal=SIGKILL hostapd.service");
647 }
648 2 localFullReload = true;
649 }
650
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
32 exec("rm -f /home/fpp/media/tmp/wifi-*.ascii");
651 }
652 // These end in "&" and must not be waited for: through exec()'s popen the
653 // backgrounded systemctl keeps the pipe open, so exec() blocks until it
654 // exits. A start job for an adapter that is not plugged in then held
655 // fppinit -- and systemd-networkd, ordered after it -- until the job
656 // timed out.
657
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 20 times.
46 for (auto& c : commandsToRun) {
658
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 printf(" %s\n", c.c_str());
659
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 execbg(c);
660 }
661
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 if (fullReload) {
662
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
40 exec("/usr/bin/systemctl daemon-reload");
663 }
664
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 execbg("/usr/bin/systemctl reload-or-restart systemd-networkd.service &");
665
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 16 times.
20 if (hostapd) {
666
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 unblockWifi();
667
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 execbg("/usr/bin/systemctl reload-or-restart hostapd.service &");
668 }
669
3/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 18 times.
20 if (fullReload && localFullReload) {
670
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 exec("/usr/bin/systemctl daemon-reload");
671
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 exec("/usr/bin/systemctl reload-or-restart systemd-networkd.service");
672
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
3 for (auto& c : postCommandsToRun) {
673
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf(" %s\n", c.c_str());
674
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 execbg(c);
675 }
676 }
677 }
678
679
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 printf("FPP - Setting max IGMP memberships\n");
680 // rmem_max is the ceiling a plain SO_RCVBUF request is clamped to. The e131
681 // bridge input sockets ask for far more than the old 384KB when many
682 // universes are configured (and use SO_RCVBUFFORCE where possible), so raise
683 // the ceiling to 8MB to cover the burst of a several-hundred-universe frame.
684
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
25 exec("/usr/sbin/sysctl -w net/core/rmem_max=8388608 net/core/wmem_max=393216 net/ipv4/igmp_max_memberships=512 > /dev/null 2>&1");
685
686
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 printf("FPP - Configuring ethernet interfaces for show traffic\n");
687
5/8
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 49 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 49 times.
✓ Branch 7 taken 25 times.
74 for (const auto& entry : std::filesystem::directory_iterator("/sys/class/net/")) {
688
2/4
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
49 std::string dev = entry.path().filename();
689
3/4
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 27 times.
49 if (IsShowEthernetInterface(dev)) {
690
3/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✓ Branch 3 taken 4 times.
22 if (InterfaceHasRoutableIPv4(dev)) {
691 // fq is required for the per-socket pacing the UDP channel
692 // outputs use, and is a good general-purpose default even
693 // when pacing is off (whether to pace is decided per
694 // destination at runtime). Only useful on interfaces that
695 // can carry routed UDP, so an address-less interface
696 // dedicated to a raw layer-2 output (ColorLight) keeps its
697 // default qdisc. Not applied to wifi (mac80211 has its own
698 // internal queueing), and the helper refuses to replace an
699 // admin-installed custom qdisc.
700
1/2
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
18 InstallShowTrafficQdisc(dev);
701 }
702 // Energy Efficient Ethernet causes latency spikes and drops with
703 // bursty traffic on several NICs (including the Pi's)
704
4/8
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 22 times.
✗ Branch 7 not taken.
22 exec("/usr/sbin/ethtool --set-eee " + ShellSingleQuote(dev) + " eee off > /dev/null 2>&1");
705 }
706 74 }
707
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 24 times.
25 if (ipForward) {
708
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 exec("/usr/sbin/sysctl net.ipv4.ip_forward=1");
709 } else {
710
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
48 exec("/usr/sbin/sysctl net.ipv4.ip_forward=0");
711 }
712
18/34
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 24 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 1 times.
✓ Branch 13 taken 24 times.
✓ Branch 14 taken 1 times.
✓ Branch 15 taken 24 times.
✓ Branch 16 taken 1 times.
✓ Branch 17 taken 24 times.
✓ Branch 18 taken 1 times.
✓ Branch 19 taken 24 times.
✓ Branch 20 taken 1 times.
✓ Branch 21 taken 24 times.
✗ Branch 22 not taken.
✓ Branch 23 taken 25 times.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
29 if (reloadApache && (!contains(execAndReturn("/usr/bin/systemctl is-active apache2"), "inactive"))) {
713 ✗ exec("/usr/bin/systemctl reload-or-restart apache2");
714 }
715 25 }
716
717 // Whether an IPv4 address is one that reaches a real network. Excludes the
718 // board's own USB gadget and tether addresses, and 169.254/16: networkd falls
719 // back to link-local while DHCP is still pending (LinkLocalAddressing=yes), and
720 // the probe can finish before a slow DHCP server answers, so treating it as
721 // "the address" started fppd on it ahead of the real lease.
722 102 static bool isUpstreamAddress(const std::string& addr) {
723
11/26
✓ Branch 0 taken 102 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 102 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 102 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 102 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 102 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 102 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 102 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 102 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 102 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 102 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 102 times.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
306 return !startsWith(addr, "169.254.") && addr != "192.168.6.2" && addr != "192.168.7.2" && addr != "192.168.8.1";
724 }
725
726 // Check if there are any network interfaces that could potentially receive NTP time
727 // Returns true if there's at least one interface with a "real" IP address that could reach NTP servers
728 // Excludes loopback, USB gadget, and tethering interfaces
729 711 static bool hasNetworkInterfaceForNTP() {
730 // If EnableTethering is explicitly set to 1, user expects to use tethering
731 // which means no external network - skip NTP wait
732
2/4
✓ Branch 0 taken 711 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 711 times.
✗ Branch 3 not taken.
711 int tetheringEnabled = getRawSettingInt("EnableTethering", 0);
733
2/2
✓ Branch 0 taken 165 times.
✓ Branch 1 taken 546 times.
711 if (tetheringEnabled == 1) {
734
1/2
✓ Branch 0 taken 165 times.
✗ Branch 1 not taken.
165 printf("FPP - Tethering is enabled, skipping NTP time wait\n");
735 165 return false;
736 }
737
738 546 struct ifaddrs* ifAddrStruct = NULL;
739 546 struct ifaddrs* ifa = NULL;
740 546 void* tmpAddrPtr = NULL;
741 546 bool hasValidIP = false;
742
743
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 546 times.
546 if (getifaddrs(&ifAddrStruct) != 0) {
744 // If we can't get interface info, assume we might have network
745 ✗ return true;
746 }
747
748
2/2
✓ Branch 0 taken 781 times.
✓ Branch 1 taken 503 times.
1284 for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
749
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 781 times.
781 if (!ifa->ifa_addr) {
750 589 continue;
751 }
752
753
1/2
✓ Branch 0 taken 781 times.
✗ Branch 1 not taken.
781 std::string nm = ifa->ifa_name;
754 // Skip loopback and USB gadget interfaces (usb0, usb1, etc.)
755
12/20
✓ Branch 0 taken 781 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 192 times.
✓ Branch 3 taken 589 times.
✓ Branch 4 taken 192 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 192 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 192 times.
✓ Branch 10 taken 192 times.
✓ Branch 11 taken 589 times.
✓ Branch 12 taken 192 times.
✓ Branch 13 taken 589 times.
✓ Branch 14 taken 589 times.
✓ Branch 15 taken 192 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
1165 if (nm == "lo" || startsWith(nm, "usb")) {
756 589 continue;
757 }
758
759 // Only check IPv4 addresses
760
2/2
✓ Branch 0 taken 43 times.
✓ Branch 1 taken 149 times.
192 if (ifa->ifa_addr->sa_family == AF_INET) {
761 43 tmpAddrPtr = &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr;
762 char addressBuffer[INET_ADDRSTRLEN];
763 43 inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN);
764
1/2
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
43 std::string addr = addressBuffer;
765
766 // Skip USB gadget, tethering and link-local addresses
767
2/4
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 43 times.
43 if (!isUpstreamAddress(addr)) {
768 ✗ continue;
769 }
770
771 // Found a valid IP that could potentially reach NTP
772 43 hasValidIP = true;
773 43 break;
774
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 43 times.
43 }
775
3/3
✓ Branch 0 taken 149 times.
✓ Branch 1 taken 589 times.
✓ Branch 2 taken 43 times.
781 }
776
777
1/2
✓ Branch 0 taken 546 times.
✗ Branch 1 not taken.
546 if (ifAddrStruct != NULL) {
778 546 freeifaddrs(ifAddrStruct);
779 }
780
781 // If no valid IP found, check if any "real" interfaces exist with link up
782 // An interface must have carrier (link) to potentially get DHCP
783
2/2
✓ Branch 0 taken 503 times.
✓ Branch 1 taken 43 times.
546 if (!hasValidIP) {
784 // Re-scan to check if interfaces exist with carrier
785
1/2
✓ Branch 0 taken 503 times.
✗ Branch 1 not taken.
503 if (getifaddrs(&ifAddrStruct) == 0) {
786
1/2
✓ Branch 0 taken 503 times.
✗ Branch 1 not taken.
503 std::string tetherInterface = FindTetherWIFIAdapater();
787
2/2
✓ Branch 0 taken 607 times.
✓ Branch 1 taken 503 times.
1110 for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
788
1/2
✓ Branch 0 taken 607 times.
✗ Branch 1 not taken.
607 std::string nm = ifa->ifa_name;
789 // Skip loopback and USB gadget interfaces
790
12/20
✓ Branch 0 taken 607 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 104 times.
✓ Branch 3 taken 503 times.
✓ Branch 4 taken 104 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 104 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 104 times.
✓ Branch 10 taken 104 times.
✓ Branch 11 taken 503 times.
✓ Branch 12 taken 104 times.
✓ Branch 13 taken 503 times.
✓ Branch 14 taken 503 times.
✓ Branch 15 taken 104 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
815 if (nm == "lo" || startsWith(nm, "usb")) {
791 503 continue;
792 }
793 // Skip the interface designated for tethering
794
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 104 times.
104 if (nm == tetherInterface) {
795 ✗ continue;
796 }
797 // Check for "real" network interface names
798
5/24
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 104 times.
✓ Branch 10 taken 104 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 104 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 104 times.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
312 if (startsWith(nm, "eth") || startsWith(nm, "wlan") ||
799
4/28
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 104 times.
✗ Branch 14 not taken.
✓ Branch 15 taken 104 times.
✗ Branch 16 not taken.
✓ Branch 17 taken 104 times.
✗ Branch 18 not taken.
✓ Branch 19 taken 104 times.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
104 startsWith(nm, "en") || startsWith(nm, "wl") ||
800
7/34
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 104 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✓ Branch 17 taken 104 times.
✗ Branch 18 not taken.
✓ Branch 19 taken 104 times.
✗ Branch 20 not taken.
✓ Branch 21 taken 104 times.
✗ Branch 22 not taken.
✓ Branch 23 taken 104 times.
✗ Branch 24 not taken.
✓ Branch 25 taken 104 times.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
208 startsWith(nm, "br") || startsWith(nm, "bond")) {
801 // Check carrier state via sysfs - most reliable across all drivers
802 // /sys/class/net/<iface>/carrier returns 1 if link, 0 if no link
803
2/4
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 104 times.
✗ Branch 3 not taken.
104 std::string carrierPath = "/sys/class/net/" + nm + "/carrier";
804
1/2
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
104 std::string carrier = GetFileContents(carrierPath);
805
1/2
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
104 TrimWhiteSpace(carrier);
806
2/4
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 104 times.
104 if (carrier == "1") {
807 ✗ printf("FPP - Interface %s has carrier, will wait for NTP\n", nm.c_str());
808 ✗ hasValidIP = true;
809 ✗ break;
810 }
811
2/4
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 104 times.
✗ Branch 3 not taken.
104 }
812
2/3
✓ Branch 0 taken 104 times.
✓ Branch 1 taken 503 times.
✗ Branch 2 not taken.
607 }
813
1/2
✓ Branch 0 taken 503 times.
✗ Branch 1 not taken.
503 if (ifAddrStruct != NULL) {
814 503 freeifaddrs(ifAddrStruct);
815 }
816 503 }
817 }
818 546 return hasValidIP;
819 }
820
821 23 void handleBootDelay() {
822
2/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
23 int i = getRawSettingInt("bootDelay", -1);
823
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 const std::string delayFile = FPP_MEDIA_DIR + "/tmp/boot_delay";
824
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 const std::string skipFile = FPP_MEDIA_DIR + "/tmp/boot_delay_skip";
825
826 // bootDelay=0 means no delay at all - clean up any flag file and return immediately
827
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 17 times.
23 if (i == 0) {
828 6 unlink(delayFile.c_str());
829 6 unlink(skipFile.c_str());
830 6 return;
831 }
832
833
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 16 times.
17 if (i > 0) {
834
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - Sleeping for %d seconds\n", i);
835 // Create flag file with start time and duration for UI countdown
836 1 time_t startTime = time(nullptr);
837
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 std::string flagContent = std::to_string(startTime) + "," + std::to_string(i);
838
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 PutFileContents(delayFile, flagContent);
839 // Notify systemd we're starting the delay and extend timeout
840
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 sd_notify(0, "STATUS=Boot delay in progress");
841
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 sd_notifyf(0, "EXTEND_TIMEOUT_USEC=%llu", (unsigned long long)(i + 30) * 1000000);
842
843 // Sleep in 100ms increments to allow UI to show countdown and respond to skip
844 1 int remainingMs = i * 1000;
845 1 int watchdogCounter = 0;
846
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 while (remainingMs > 0) {
847 // Check for skip request from UI every iteration
848
3/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 11 times.
12 if (FileExists(skipFile)) {
849
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - Boot delay skip requested by user\n");
850 1 unlink(skipFile.c_str());
851 1 break;
852 }
853
854
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 std::this_thread::sleep_for(std::chrono::milliseconds(100));
855 11 remainingMs -= 100;
856 11 watchdogCounter++;
857
858 // Notify systemd watchdog every 10 seconds (100 iterations)
859
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (watchdogCounter >= 100) {
860 ✗ sd_notify(0, "WATCHDOG=1");
861 ✗ watchdogCounter = 0;
862 }
863 }
864 // Remove flag files when delay completes
865 1 unlink(delayFile.c_str());
866 1 unlink(skipFile.c_str());
867
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
17 } else if (i == -1) {
868 // Auto mode: nothing to wait on here. The is-there-an-NTP-capable-network
869 // decision and the quick SNTP set both happen in handleTimeSyncWait(),
870 // which runs on the network thread after waitForInterfacesUp() has
871 // already waited for link/IP properly. This used to pre-poll up to 5s
872 // for an interface, but that only serialized boot ahead of the
873 // concurrent audio/network threads and its verdict was recomputed
874 // later anyway. Just clean up any stale flag files.
875 16 unlink(delayFile.c_str());
876 16 unlink(skipFile.c_str());
877 }
878
4/4
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 6 times.
29 }
879
880 // Minimal SNTP (RFC 4330) client: query an NTP server over UDP and step the
881 // system clock to its transmit timestamp (second precision). This one-shot set
882 // runs during boot so we never block ~20s waiting for the chrony daemon to step
883 // the clock; chrony refines it to sub-millisecond afterwards. Note the socket is
884 // left unbound, so the query sources from a random ephemeral port -- which (like
885 // chrony itself) dodges routers/ISP gateways that drop NTP replies to source
886 // port 123. Must run as root (clock_settime needs CAP_SYS_TIME). Returns true if
887 // the clock was set.
888 28 static bool quickSntpSet(const std::string& host, int timeoutSecs) {
889 28 struct addrinfo hints {};
890 28 hints.ai_family = AF_INET;
891 28 hints.ai_socktype = SOCK_DGRAM;
892 28 struct addrinfo* res = nullptr;
893
3/8
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 28 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 28 times.
✗ Branch 7 not taken.
28 if (getaddrinfo(host.c_str(), "123", &hints, &res) != 0 || res == nullptr) {
894 28 return false;
895 }
896 ✗ bool ok = false;
897 ✗ int sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
898 ✗ if (sock >= 0) {
899 ✗ struct timeval tv { timeoutSecs, 0 };
900 ✗ setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
901 ✗ setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
902 ✗ unsigned char pkt[48] = { 0 };
903 ✗ pkt[0] = 0x1b; // LI=0, VN=3, Mode=3 (client)
904 ✗ if (sendto(sock, pkt, sizeof(pkt), 0, res->ai_addr, res->ai_addrlen) == (ssize_t)sizeof(pkt)
905 ✗ && recv(sock, pkt, sizeof(pkt), 0) == (ssize_t)sizeof(pkt)) {
906 ✗ int stratum = pkt[1];
907 // Transmit timestamp: 32-bit seconds since 1900, big-endian, at offset 40.
908 ✗ uint32_t ntpSecs = ((uint32_t)pkt[40] << 24) | ((uint32_t)pkt[41] << 16) | ((uint32_t)pkt[42] << 8) | (uint32_t)pkt[43];
909 // stratum 0 == "kiss o' death"/invalid; 16+ == unsynchronized.
910 // 2208988800 = seconds between the 1900 (NTP) and 1970 (Unix) epochs.
911 ✗ if (stratum > 0 && stratum < 16 && ntpSecs > 2208988800U) {
912 ✗ struct timespec ts { (time_t)(ntpSecs - 2208988800U), 0 };
913 ✗ ok = (clock_settime(CLOCK_REALTIME, &ts) == 0);
914 }
915 }
916 ✗ close(sock);
917 }
918 ✗ freeaddrinfo(res);
919 ✗ return ok;
920 }
921
922 // Ensure the clock is sane before fppd starts - called AFTER waitForInterfacesUp
923 // so we know network is available. On RTC-less boards the clock boots stale (at
924 // the image build date), which would make schedules and logs wrong. Rather than
925 // block boot for ~20s waiting for the chrony daemon to step the clock, do a
926 // quick one-shot SNTP set now and continue; the daemon refines it afterwards.
927 21 void handleTimeSyncWait() {
928
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
21 int i = getRawSettingInt("bootDelay", -1);
929
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 16 times.
21 if (i != -1) {
930 // Only do time sync wait in auto mode
931 7 return;
932 }
933
934
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 const std::string delayFile = FPP_MEDIA_DIR + "/tmp/boot_delay";
935
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 const std::string skipFile = FPP_MEDIA_DIR + "/tmp/boot_delay_skip";
936
937 // Check if there are any network interfaces that could get NTP time
938 // If not, skip - no point trying NTP on a device with no network
939
3/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 14 times.
16 if (!hasNetworkInterfaceForNTP()) {
940
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("FPP - No network interface found, skipping NTP time set\n");
941 2 unlink(delayFile.c_str());
942 2 unlink(skipFile.c_str());
943 2 return;
944 }
945
946 struct stat attr;
947 14 stat("/etc/fpp/rfs_version", &attr);
948 14 time_t fileTime = attr.st_ctime;
949 14 time_t currentTime = time(nullptr);
950
951
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (difftime(fileTime, currentTime) <= 0) {
952 // Clock is already at or past the image build date - nothing to do.
953 ✗ unlink(delayFile.c_str());
954 ✗ unlink(skipFile.c_str());
955 ✗ return;
956 }
957
958 // Clock is stale (no RTC). Do a fast SNTP set against the configured server
959 // and move on -- do NOT block on full NTP convergence. Use the first
960 // pool/server from chrony.conf so a site-local time server is honoured; fall
961 // back to FPP's pool.
962
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
28 std::string ntpHost = execAndReturn(
963
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 "/usr/bin/awk '/^(pool|server)[[:space:]]/ { print $2; exit }' /etc/chrony/chrony.conf 2>/dev/null");
964
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 TrimWhiteSpace(ntpHost);
965
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 if (ntpHost.empty()) {
966
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 ntpHost = "falconplayer.pool.ntp.org";
967 }
968
969
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 printf("FPP - System clock is stale; doing a quick SNTP set from %s\n", ntpHost.c_str());
970
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 sd_notify(0, "STATUS=Setting system clock via SNTP");
971 14 bool set = false;
972
3/4
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 14 times.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
42 for (int attempt = 0; attempt < 2 && !set; ++attempt) {
973
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 set = quickSntpSet(ntpHost, 2);
974 }
975
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 14 times.
14 if (set) {
976 ✗ currentTime = time(nullptr);
977 struct tm tmNow;
978 ✗ localtime_r(&currentTime, &tmNow);
979 char buffer[26];
980 ✗ strftime(buffer, 26, "%Y-%m-%d %H:%M:%S", &tmNow);
981 ✗ printf("FPP - Clock set via SNTP to %s; chrony will refine it in the background\n", buffer);
982 } else {
983
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 printf("FPP - Quick SNTP set failed (no time server reachable yet); continuing, chrony will sync when available\n");
984 }
985 14 unlink(delayFile.c_str());
986 14 unlink(skipFile.c_str());
987
4/4
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 2 times.
18 }
988
989 23 void checkWLANInterface() {
990
16/62
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 23 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 23 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 23 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 23 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✓ Branch 21 taken 23 times.
✗ Branch 22 not taken.
✓ Branch 23 taken 23 times.
✗ Branch 24 not taken.
✓ Branch 25 taken 23 times.
✗ Branch 26 not taken.
✓ Branch 27 taken 23 times.
✗ Branch 28 not taken.
✓ Branch 29 taken 23 times.
✓ Branch 30 taken 23 times.
✗ Branch 31 not taken.
✓ Branch 32 taken 23 times.
✗ Branch 33 not taken.
✓ Branch 34 taken 23 times.
✗ Branch 35 not taken.
✓ Branch 36 taken 23 times.
✗ Branch 37 not taken.
✓ Branch 38 taken 23 times.
✗ Branch 39 not taken.
✗ Branch 40 not taken.
✓ Branch 41 taken 23 times.
✗ Branch 42 not taken.
✗ Branch 43 not taken.
✗ Branch 44 not taken.
✗ Branch 45 not taken.
✗ Branch 46 not taken.
✗ Branch 47 not taken.
✗ Branch 48 not taken.
✗ Branch 49 not taken.
✗ Branch 50 not taken.
✗ Branch 51 not taken.
✗ Branch 52 not taken.
✗ Branch 53 not taken.
✗ Branch 54 not taken.
✗ Branch 55 not taken.
✗ Branch 56 not taken.
✗ Branch 57 not taken.
✗ Branch 58 not taken.
✗ Branch 59 not taken.
✗ Branch 60 not taken.
✗ Branch 61 not taken.
115 if (contains(execAndReturn("/usr/bin/systemctl is-enabled wpa_supplicant@wlan0"), "enabled") && contains(execAndReturn("/usr/bin/systemctl is-active wpa_supplicant@wlan0"), "inactive")) {
991 ✗ printf("Need to restart wpa_supplicant@wlan0\n");
992 ✗ execbg("systemctl restart \"wpa_supplicant@wlan0.service\" &");
993 }
994 23 }
995
996 // Build the spoken "I Have Found The Following I P Addresses, ..." string from
997 // the current non-tether IPv4 addresses. Returns "" if none are present yet.
998 71 static std::string buildIPAnnounceString() {
999
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 std::string announce = "I Have Found The Following I P Addresses";
1000 71 bool found = false;
1001 71 struct ifaddrs* ifAddrStruct = NULL;
1002 71 struct ifaddrs* ifa = NULL;
1003 71 void* tmpAddrPtr = NULL;
1004 71 getifaddrs(&ifAddrStruct);
1005
2/2
✓ Branch 0 taken 195 times.
✓ Branch 1 taken 71 times.
266 for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
1006
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 195 times.
195 if (!ifa->ifa_addr) {
1007 129 continue;
1008 }
1009
1/2
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
195 std::string nm = ifa->ifa_name;
1010
13/30
✓ Branch 0 taken 195 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 195 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 195 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 195 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 195 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 129 times.
✓ Branch 11 taken 66 times.
✓ Branch 12 taken 195 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 195 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 195 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 195 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 129 times.
✓ Branch 21 taken 66 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
975 if (startsWith(nm, "usb") || startsWith(nm, "lo")) {
1011 129 continue;
1012 }
1013
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 35 times.
66 if (ifa->ifa_addr->sa_family == AF_INET) { // check it is IP4
1014 // is a valid IP4 Address
1015 31 tmpAddrPtr = &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr;
1016 char addressBuffer[INET_ADDRSTRLEN];
1017 31 inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN);
1018
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 std::string addr = addressBuffer;
1019
2/4
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
31 if (isUpstreamAddress(addr)) {
1020
1/2
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
31 printf("FPP - Found %s IP Address %s\n", ifa->ifa_name, addressBuffer);
1021
3/6
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
31 announce += ", " + std::string(addressBuffer);
1022 31 found = true;
1023 }
1024 31 }
1025
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 129 times.
195 }
1026
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 if (ifAddrStruct != NULL) {
1027 71 freeifaddrs(ifAddrStruct);
1028 }
1029
6/10
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 42 times.
✓ Branch 2 taken 29 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 42 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 42 times.
✓ Branch 7 taken 29 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
184 return found ? announce : "";
1030 71 }
1031
1032 // A USB network adapter can enumerate, bind, and bring up its netdev, and then
1033 // have the USB link itself fail at the protocol layer: every register access
1034 // returns -EPROTO, the radio never associates, and the board runs perfectly
1035 // while being completely unreachable. Measured at roughly 5% of boots on one
1036 // adapter/controller pairing, and the box stays dark until someone walks out
1037 // to it.
1038 //
1039 // Nothing recovers this in software. Tried on hardware and rejected: unbind and
1040 // rebind on the usb driver (re-enumerates, then fails its descriptor reads and
1041 // never returns), disabling and re-enabling the root-hub port (leaves it "not
1042 // attached"), and unbinding the host controller (brings the controller back but
1043 // not the device). Even a physical replug does not reliably fix it. A reboot
1044 // does, so that is what this does.
1045 //
1046 // The whole difficulty is firing only for this failure and nothing else. All of
1047 // the following must hold:
1048 // - a USB-attached network interface exists and has no carrier
1049 // - no other interface has an address, so we are not already reachable
1050 // - tethering is not the configured intent
1051 // - this boot's kernel log shows USB protocol errors
1052 // That last condition is what separates a wedged adapter from an ordinary "no
1053 // access point in range", "wrong passphrase", or "AP is down" -- none of which a
1054 // reboot would fix, and all of which it would hide.
1055 static constexpr int USB_WEDGE_MAX_REBOOTS = 2;
1056
1057 33 static std::string usbWedgeStateFile() {
1058 // /var/tmp, not media/tmp (fppinit.service wipes that on stop, and this has
1059 // to survive exactly the reboot it is counting) and not media/config (this
1060 // is transient state, and a restored config backup must not carry a stale
1061 // count into an unrelated box).
1062
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
66 return "/var/tmp/fpp-usbnet-wedge-reboots";
1063 }
1064
1065 // Attempts made recently. Deliberately expires: a box power-cycled hours later
1066 // should get a fresh pair of tries rather than inheriting a stale count.
1067 2 static int usbWedgeRebootCount() {
1068
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string c = GetFileContents(usbWedgeStateFile());
1069 2 int count = 0;
1070 2 long long when = 0;
1071
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 if (c.empty() || sscanf(c.c_str(), "%d %lld", &count, &when) != 2) {
1072 2 return 0;
1073 }
1074 ✗ if ((((long long)time(nullptr)) - when) > 3600) {
1075 ✗ return 0;
1076 }
1077 ✗ return count;
1078 2 }
1079
1080 29 static void clearUsbWedgeRebootCount() {
1081
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 unlink(usbWedgeStateFile().c_str());
1082 29 }
1083
1084 // USB port ids (e.g. "1-1.2") of USB-attached network interfaces that have no
1085 // carrier. An interface that has carrier is working even if DHCP has not
1086 // finished, and is none of our business.
1087 7 static std::set<std::string> carrierlessUsbNetDeviceIds() {
1088 7 std::set<std::string> ids;
1089 7 std::error_code dirEc;
1090
5/8
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 14 times.
✓ Branch 7 taken 7 times.
21 for (const auto& entry : std::filesystem::directory_iterator("/sys/class/net", dirEc)) {
1091
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 std::string ifName = entry.path().filename();
1092
12/20
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 7 times.
✓ Branch 10 taken 7 times.
✓ Branch 11 taken 7 times.
✓ Branch 12 taken 7 times.
✓ Branch 13 taken 7 times.
✓ Branch 14 taken 7 times.
✓ Branch 15 taken 7 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
28 if (ifName == "lo" || startsWith(ifName, "usb")) {
1093 7 continue; // usb0/usb1 are the board's own gadget interfaces, not adapters
1094 }
1095
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
7 std::string carrier = GetFileContents(entry.path().string() + "/carrier");
1096
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 TrimWhiteSpace(carrier);
1097
3/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 4 times.
7 if (carrier == "1") {
1098 3 continue;
1099 }
1100 4 std::error_code ec;
1101
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
4 std::filesystem::path dev = std::filesystem::canonical(entry.path() / "device", ec);
1102
8/12
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 3 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
4 if (ec || dev.string().find("/usb") == std::string::npos) {
1103 1 continue; // onboard MAC; a reboot is not indicated for it
1104 }
1105 // .../usb1/1-1/1-1.2/1-1.2:1.0 -- the interface node's parent is the USB
1106 // device, and its name is the port id the kernel logs errors against.
1107
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 std::string usbId = dev.parent_path().filename();
1108
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 if (!usbId.empty() && usbId.find(':') == std::string::npos) {
1109
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 ids.insert(usbId);
1110 }
1111
6/6
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 11 times.
26 }
1112 7 return ids;
1113 ✗ }
1114
1115 3 static bool anyInterfaceHasAddress() {
1116 3 struct ifaddrs* addrs = nullptr;
1117
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (getifaddrs(&addrs) != 0) {
1118 ✗ return true; // cannot tell: assume reachable and do not reboot
1119 }
1120 3 bool found = false;
1121
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 for (struct ifaddrs* ifa = addrs; ifa && !found; ifa = ifa->ifa_next) {
1122
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != AF_INET) {
1123 3 continue;
1124 }
1125 ✗ std::string nm = ifa->ifa_name;
1126 ✗ if (nm == "lo" || startsWith(nm, "usb")) {
1127 ✗ continue;
1128 }
1129 ✗ uint32_t a = ntohl(((struct sockaddr_in*)ifa->ifa_addr)->sin_addr.s_addr);
1130 ✗ if ((a >> 16) == 0xA9FE) {
1131 ✗ continue; // 169.254/16 link-local is not a usable address
1132 }
1133 ✗ found = true;
1134 ✗ }
1135 3 freeifaddrs(addrs);
1136 3 return found;
1137 }
1138
1139 // The signature of a link that has failed below the driver: -EPROTO/-ETIMEDOUT
1140 // against the adapter's own USB port id.
1141 //
1142 // Anchoring on the port id is what keeps a flaky USB stick from rebooting a box
1143 // whose WiFi is merely out of range -- both conditions would otherwise be true
1144 // at once. Every line the kernel and the driver emit for this failure carries
1145 // the id, in both forms:
1146 // usb 1-1.2: device descriptor read/64, error -71
1147 // rtw88_8822bu 1-1.2:1.0: write register 0x5 failed with -71
1148 // Thresholded because one retried transfer is normal; a real wedge produces
1149 // thousands.
1150 3 static bool kernelLogShowsUsbProtocolFailure(const std::set<std::string>& usbIds) {
1151
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (usbIds.empty()) {
1152 ✗ return false;
1153 }
1154 3 std::string ids;
1155
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
6 for (const auto& id : usbIds) {
1156
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!ids.empty()) {
1157 ✗ ids += "|";
1158 }
1159
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 3 times.
12 for (char c : id) { // the ids contain '.', which must not be a wildcard
1160
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 9 times.
9 if (c == '.') {
1161 ✗ ids += "\\.";
1162 } else {
1163
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 ids += c;
1164 }
1165 }
1166 }
1167 std::string out = execAndReturn(
1168
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 "/bin/dmesg 2>/dev/null | /bin/grep -cE '(" + ids + "):.*(error -(71|110)" +
1169
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 "|failed with -(71|110)|fail, status: -(71|110))'");
1170
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 TrimWhiteSpace(out);
1171 3 return atoi(out.c_str()) >= 4;
1172 3 }
1173
1174 // Set once a reboot has been asked for, so the rest of postNetwork can stop
1175 // doing work for a boot that is about to be thrown away.
1176 static std::atomic<bool> s_usbWedgeRebootPending{false};
1177
1178 23 bool usbWedgeRebootPending() {
1179 23 return s_usbWedgeRebootPending.load();
1180 }
1181
1182 // Returns true if a reboot was requested. Does NOT block waiting for it.
1183 //
1184 // fppd's start depends on fpp_postnetwork, so anything that stalls this service
1185 // past its TimeoutStartSec takes fppd down with it. /usr/sbin/reboot is a
1186 // symlink to systemctl, which asks logind over D-Bus to set a wall message;
1187 // on an early-boot box that activation can time out. Calling it synchronously
1188 // hung this service for 37+ seconds until systemd killed it -- the reboot never
1189 // happened AND fppd never started, which is worse than the wedge being fixed.
1190 // So: go straight to the system manager (no logind), don't wait for the job,
1191 // and background the call so it cannot stall this thread no matter what.
1192 8 static bool rebootIfUsbNetWedged() {
1193
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
16 if (getRawSettingInt("AutoRebootOnUSBNetworkFailure", 1) == 0) {
1194 ✗ return false;
1195 }
1196
4/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 7 times.
16 if (getRawSettingInt("EnableTethering", 0) == 1) {
1197 1 return false; // no upstream network is expected; being without one is the plan
1198 }
1199
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::set<std::string> stalled = carrierlessUsbNetDeviceIds();
1200
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 3 times.
7 if (stalled.empty()) {
1201 4 return false; // no USB adapter in trouble, so a reboot cannot be the answer
1202 }
1203
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
3 if (anyInterfaceHasAddress()) {
1204 ✗ return false; // reachable some other way; never reboot a box that is fine
1205 }
1206
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
3 if (!kernelLogShowsUsbProtocolFailure(stalled)) {
1207
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - USB network adapter has no carrier, but its USB link is healthy "
1208 "(no access point in range?); not rebooting\n");
1209 1 return false;
1210 }
1211
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 int count = usbWedgeRebootCount();
1212
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (count >= USB_WEDGE_MAX_REBOOTS) {
1213 ✗ printf("FPP - USB network adapter is wedged, but %d reboots have not cleared it; "
1214 "staying up rather than looping\n",
1215 count);
1216 ✗ return false;
1217 }
1218
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("FPP - USB network adapter is wedged: USB protocol errors, no carrier, and no "
1219 "other network. Rebooting (attempt %d of %d).\n",
1220 count + 1, USB_WEDGE_MAX_REBOOTS);
1221
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 PutFileContents(usbWedgeStateFile(),
1222
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
4 std::to_string(count + 1) + " " + std::to_string((long long)time(nullptr)));
1223 2 sync();
1224 2 s_usbWedgeRebootPending = true;
1225 // Forced reboot, not a graceful one. The condition being recovered from is a
1226 // wedged USB stack, which is precisely what stops a graceful shutdown from
1227 // finishing: tasks block in uninterruptible sleep on the dead device, so
1228 // neither SIGTERM nor SIGKILL clears them. Measured on hardware, an ordinary
1229 // "systemctl start reboot.target" began stopping units 8s after this point
1230 // and then sat there for 37 minutes, the controller logging
1231 // "musb-hdrc: VBUS_ERROR in a_wait_bcon" while it churned -- the board was
1232 // unreachable that whole time, which is the outage this is supposed to end.
1233 // sync() above is what makes skipping the unmounts acceptable; the sysrq
1234 // fallback covers systemctl itself being unable to run.
1235
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 execbg("( /bin/systemctl --force --force reboot"
1236 " || { echo 1 > /proc/sys/kernel/sysrq; echo b > /proc/sysrq-trigger; } ) &");
1237 2 return true;
1238 7 }
1239
1240 // WiFi interfaces with client settings, which may still associate and get an
1241 // address. Read from the config, so it includes adapters that are not present.
1242 47 static std::set<std::string> wifiClientInterfaces() {
1243 47 std::set<std::string> ret;
1244
2/4
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✗ Branch 3 not taken.
47 int tetherEnabled = getRawSettingInt("EnableTethering", 0);
1245 47 std::string tetherInterface;
1246
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 42 times.
47 if (tetherEnabled == 1) {
1247
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 tetherInterface = FindTetherWIFIAdapater(); // its settings describe the AP, not a client
1248 }
1249 47 std::error_code ec;
1250
6/10
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 47 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 34 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 34 times.
✓ Branch 9 taken 47 times.
81 for (const auto& entry : std::filesystem::directory_iterator(FPP_MEDIA_DIR + "/config", ec)) {
1251
2/4
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
34 std::string name = entry.path().filename();
1252
13/24
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✓ Branch 5 taken 13 times.
✓ Branch 6 taken 21 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 18 times.
✓ Branch 10 taken 21 times.
✓ Branch 11 taken 13 times.
✓ Branch 12 taken 34 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 34 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 16 times.
✓ Branch 17 taken 18 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
102 if (!startsWith(name, "interface.wl") || name.substr(10) == tetherInterface) {
1253 16 continue;
1254 }
1255
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 auto settings = loadSettingsFile(entry.path());
1256
12/32
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 18 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 18 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 18 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 18 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 18 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 18 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 18 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 18 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 18 times.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
90 if (!settings["SSID"].empty() && settings["SSID"] != "\"\"") {
1257
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
18 ret.insert(name.substr(10));
1258 }
1259
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 16 times.
81 }
1260 // A hand-written supplicant config counts too
1261
5/8
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 30 times.
✓ Branch 7 taken 47 times.
77 for (const auto& entry : std::filesystem::directory_iterator("/etc/wpa_supplicant", ec)) {
1262
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 std::string name = entry.path().filename();
1263
15/30
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✓ Branch 5 taken 18 times.
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 12 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 12 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 12 times.
✓ Branch 13 taken 18 times.
✓ Branch 14 taken 12 times.
✓ Branch 15 taken 18 times.
✓ Branch 16 taken 30 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 30 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 12 times.
✓ Branch 21 taken 18 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
114 if (startsWith(name, "wpa_supplicant-wl") && endsWith(name, ".conf")) {
1264
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 ret.insert(name.substr(15, name.size() - 15 - 5));
1265 }
1266 77 }
1267 47 return ret;
1268 47 }
1269
1270 // An address can only come from a wired interface once it has carrier (static
1271 // configs are not applied without it), or from a present WiFi adapter with
1272 // client settings. With neither, a first boot would sit out the whole wait
1273 // before tethering comes up, so for a board with no wired port and no
1274 // configured WiFi adapter, the wait ends once the set of interfaces has held
1275 // still for a moment -- enough for USB adapters enumerating one after another
1276 // behind a hub (seen ~1s apart).
1277 //
1278 // A wired port without carrier is never written off early: carrier cannot tell
1279 // "no cable" from "still autonegotiating", which took 5.1s on a Pi 5 against a
1280 // gigabit switch. Giving up there started tethering, then tore it down when
1281 // DHCP landed a few seconds later.
1282 //
1283 // A configured WiFi adapter that is absent is not worth waiting for: postNetwork
1284 // runs after network.target, which wpa_supplicant@<if> orders itself before, so
1285 // by now its start job has either seen the adapter appear or given up on it
1286 // (JobTimeoutSec in etc/systemd/wpa_supplicant@.service.d). If it turns up
1287 // later, udev starts its supplicant then.
1288 class UpstreamLinkWatch {
1289 public:
1290 static constexpr int SETTLE_ITERATIONS = 20; // x 200ms
1291
1292 enum class State {
1293 NONE, // nothing can get an address
1294 POSSIBLE, // something may still get link
1295 LINK // something has link; its address may be about to land
1296 };
1297
1298 47 explicit UpstreamLinkWatch(std::set<std::string> wifiClients) :
1299 47 wifiClients(std::move(wifiClients)) {}
1300
1301 666 State check(int count) {
1302 666 std::set<std::string> state;
1303 666 bool possible = false;
1304 666 std::error_code ec;
1305
5/8
✓ Branch 0 taken 666 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 666 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1366 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1367 times.
✓ Branch 7 taken 665 times.
2032 for (const auto& entry : std::filesystem::directory_iterator("/sys/class/net", ec)) {
1306
2/4
✓ Branch 0 taken 1367 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1367 times.
✗ Branch 3 not taken.
1367 std::string dev = entry.path().filename();
1307
21/36
✓ Branch 0 taken 1367 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 702 times.
✓ Branch 3 taken 665 times.
✓ Branch 4 taken 702 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 702 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 702 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 702 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 702 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 702 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 456 times.
✓ Branch 17 taken 246 times.
✓ Branch 18 taken 702 times.
✓ Branch 19 taken 665 times.
✓ Branch 20 taken 702 times.
✓ Branch 21 taken 665 times.
✓ Branch 22 taken 702 times.
✓ Branch 23 taken 665 times.
✓ Branch 24 taken 702 times.
✓ Branch 25 taken 665 times.
✓ Branch 26 taken 1121 times.
✓ Branch 27 taken 246 times.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✗ Branch 35 not taken.
2771 if (dev == "lo" || startsWith(dev, "usb") || !FileExists(entry.path().string() + "/device")) {
1308 1121 continue; // loopback, the board's own gadget links, virtual devices
1309 }
1310
3/6
✓ Branch 0 taken 246 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 246 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 246 times.
✗ Branch 5 not taken.
246 std::string flags = GetFileContents(entry.path().string() + "/flags");
1311 246 bool up = strtol(flags.c_str(), nullptr, 16) & IFF_UP;
1312
3/6
✓ Branch 0 taken 246 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 246 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 246 times.
✗ Branch 5 not taken.
246 state.insert(dev + (up ? ":up" : ":down"));
1313
12/22
✓ Branch 0 taken 246 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 246 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 246 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 217 times.
✓ Branch 7 taken 29 times.
✓ Branch 8 taken 217 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 42 times.
✓ Branch 11 taken 175 times.
✓ Branch 12 taken 246 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 246 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 42 times.
✓ Branch 17 taken 204 times.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
246 if (FileExists(entry.path().string() + "/wireless") && !wifiClients.count(dev)) {
1314 42 continue; // no client config, so it can only ever be the tether AP
1315 }
1316
3/6
✓ Branch 0 taken 204 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 204 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 204 times.
✗ Branch 5 not taken.
204 std::string carrier = GetFileContents(entry.path().string() + "/carrier");
1317
1/2
✓ Branch 0 taken 204 times.
✗ Branch 1 not taken.
204 TrimWhiteSpace(carrier);
1318
3/4
✓ Branch 0 taken 204 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 203 times.
204 if (carrier == "1") {
1319 1 return State::LINK;
1320 }
1321 203 possible = true;
1322
14/14
✓ Branch 0 taken 203 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 203 times.
✓ Branch 3 taken 42 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 203 times.
✓ Branch 6 taken 1163 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 665 times.
✓ Branch 9 taken 1 times.
✓ Branch 10 taken 665 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 665 times.
✓ Branch 13 taken 1 times.
2079 }
1323
3/4
✓ Branch 0 taken 665 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 657 times.
665 if (state != lastState) {
1324
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 lastState = state;
1325 8 lastChange = count;
1326 }
1327
8/8
✓ Branch 0 taken 462 times.
✓ Branch 1 taken 203 times.
✓ Branch 2 taken 42 times.
✓ Branch 3 taken 420 times.
✓ Branch 4 taken 40 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 663 times.
✓ Branch 7 taken 2 times.
665 if (possible || lastState.empty() || (count - lastChange) < SETTLE_ITERATIONS) {
1328 663 return State::POSSIBLE;
1329 }
1330 2 return State::NONE;
1331 666 }
1332
1333 private:
1334 std::set<std::string> wifiClients;
1335 std::set<std::string> lastState;
1336 int lastChange = 0;
1337 };
1338
1339 47 bool waitForInterfacesUp(int timeOut, bool allowUsbRecovery) {
1340 47 int count = 0;
1341
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 UpstreamLinkWatch linkWatch(wifiClientInterfaces());
1342 // If no network interfaces have carrier/link, don't wait for IP address - likely no network available and no point waiting for DHCP/NTP
1343
3/4
✓ Branch 0 taken 695 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 666 times.
✓ Branch 3 taken 29 times.
695 while (!hasNetworkInterfaceForNTP()) {
1344
1/2
✓ Branch 0 taken 666 times.
✗ Branch 1 not taken.
666 UpstreamLinkWatch::State link = linkWatch.check(count);
1345
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 665 times.
666 if (link == UpstreamLinkWatch::State::LINK) {
1346 // Has link but no address yet: DHCP gets the rest of the timeOut
1347 // below rather than being cut off at the halfway mark.
1348 1 break;
1349 }
1350 665 bool giveUp = count >= (timeOut / 2); // spend half the timeOut waiting for interfaces to have link, then give up
1351
4/4
✓ Branch 0 taken 650 times.
✓ Branch 1 taken 15 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 648 times.
665 if (!giveUp && link == UpstreamLinkWatch::State::NONE) {
1352
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("FPP - No wired port and no configured WiFi adapter present; nothing can get an address\n");
1353 2 giveUp = true;
1354 }
1355
2/2
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 648 times.
665 if (giveUp) {
1356 // Before writing the network off, check for a USB adapter whose USB
1357 // link has died, which only a reboot clears.
1358 //
1359 // Opt-in, and only the boot path opts in. The tether callers below
1360 // are driven by networkd-dispatcher on carrier changes, and rebooting
1361 // from a carrier-change handler is a boot loop with extra steps.
1362
7/8
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 9 times.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 6 times.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 15 times.
17 if (allowUsbRecovery && rebootIfUsbNetWedged()) {
1363 // Reboot is queued. Return at once and let the caller skip the
1364 // rest of postNetwork: this service is on fppd's dependency
1365 // chain, so overrunning its start timeout here would fail fppd
1366 // for the few seconds of life this boot has left.
1367 2 return false;
1368 }
1369
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 printf("FPP - No network interfaces with link detected after waiting for %d ms, skipping IP wait\n", count * 200);
1370 15 return false;
1371 }
1372
1/2
✓ Branch 0 taken 648 times.
✗ Branch 1 not taken.
648 std::this_thread::sleep_for(std::chrono::milliseconds(200));
1373 648 ++count;
1374 }
1375
1/2
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
30 printf("FPP - Waited for %0.1f seconds for network interfaces to have link...\n", (((float)count) * 0.2));
1376 30 std::string announce;
1377 do {
1378
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 announce = buildIPAnnounceString();
1379
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 29 times.
71 if (announce.empty()) {
1380
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 std::this_thread::sleep_for(std::chrono::milliseconds(200));
1381 42 ++count;
1382 }
1383
6/6
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 29 times.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 41 times.
✓ Branch 5 taken 30 times.
71 } while (announce.empty() && (count < timeOut));
1384
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 29 times.
30 if (announce.empty()) {
1385
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - Could not get a valid IP address\n");
1386 1 return false;
1387 }
1388
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 printf("FPP - Waited for %0.1f seconds for IP address\n", (((float)count) * 0.2f));
1389 // Network came up, so whatever the last wedge was, it is over. Clearing here
1390 // rather than letting the count expire keeps the next occurrence entitled to
1391 // its full pair of attempts.
1392
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 clearUsbWedgeRebootCount();
1393 29 return true;
1394 47 }
1395
1396 // Announce the device's IP address(es) over audio with flite + pw-play. Invoked
1397 // from fpp-announce-ip.service (ordered after fpp_postnetwork and the PipeWire
1398 // services) rather than inline in postNetwork, so the CPU-heavy flite synthesis
1399 // doesn't compete with the tail of postNetwork / fppd startup on single-core
1400 // boards. Network is already up by the time the service runs.
1401 ✗ void announceIPAddresses() {
1402 ✗ if (getRawSettingInt("disableIPAnnouncement", 0) || !FileExists("/usr/bin/flite")) {
1403 ✗ return;
1404 }
1405 ✗ std::string announce = buildIPAnnounceString();
1406 // postNetwork's IP wait can time out with the DHCP lease landing moments
1407 // later; this service is off fppd's critical path, so give the address a
1408 // bounded chance to appear rather than silently skipping the announcement.
1409 ✗ for (int i = 0; announce.empty() && i < 75; ++i) {
1410 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(200));
1411 ✗ announce = buildIPAnnounceString();
1412 }
1413 ✗ if (announce.empty()) {
1414 ✗ printf("FPP - announceIP: no usable IP address to announce\n");
1415 ✗ return;
1416 }
1417 // flite is an ALSA client, so playing directly (flite -t) would route
1418 // through the pipewire-alsa plugin + /root/.asoundrc -- the fragile ALSA
1419 // chain we're retiring. Instead render to a WAV and play it through PipeWire
1420 // natively with pw-play. pw-play needs the runtime env vars to find FPP's
1421 // PipeWire instance at /run/pipewire-fpp. /run/fppd is created by setupAudio.
1422 // Both halves are hard-capped. A PipeWire graph that cannot reach a working
1423 // sink does not fail pw-play -- the node parks in error and the stream never
1424 // starts *or* ends, so pw-play blocks in poll() indefinitely. Unbounded, that
1425 // turns a cosmetic audio problem into an unbootable box. The real fix is
1426 // upstream (FPP now stops WirePlumber from creating a second node fighting
1427 // for the same PCM), but nothing about announcing an IP address is worth
1428 // risking a hung boot service over, so cap it here too.
1429 ✗ const std::string announceWav = "/run/fppd/ip-announce.wav";
1430 std::string fliteCmd =
1431 ✗ "/usr/bin/timeout 60 /usr/bin/flite -voice awb -t \"" + announce + "\" -o " + announceWav +
1432 " && PIPEWIRE_RUNTIME_DIR=/run/pipewire-fpp XDG_RUNTIME_DIR=/run/pipewire-fpp"
1433 ✗ " /usr/bin/timeout 60 /usr/bin/pw-play " +
1434 ✗ announceWav;
1435 ✗ exec(fliteCmd);
1436 ✗ unlink(announceWav.c_str()); // don't leave the rendered WAV behind in /run
1437 ✗ }
1438 // Turn off 802.11 power saving on every wireless interface. A show controller
1439 // wants its network responsive, not its radio dozing between beacons; the power
1440 // saved is irrelevant on a mains-powered board.
1441 //
1442 // Was dead code until now -- written but never called, from the commit that
1443 // introduced it -- so this had simply never run. The drivers in use happen to
1444 // default power_save off, which is why nobody noticed; this makes it true by
1445 // intent rather than by luck, and covers an adapter whose driver defaults the
1446 // other way.
1447 21 void disableWLANPowerManagement() {
1448 21 struct ifaddrs* ifAddrStruct = NULL;
1449 21 getifaddrs(&ifAddrStruct);
1450 // getifaddrs yields one entry per address family, so a single interface shows
1451 // up as AF_PACKET plus AF_INET plus AF_INET6. Collect names first: otherwise
1452 // this forks iw two or three times per adapter for no reason.
1453 21 std::set<std::string> wireless;
1454
2/2
✓ Branch 0 taken 91 times.
✓ Branch 1 taken 21 times.
112 for (struct ifaddrs* ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
1455
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 91 times.
91 if (!ifa->ifa_addr) {
1456 ✗ continue;
1457 }
1458
1/2
✓ Branch 0 taken 91 times.
✗ Branch 1 not taken.
182 std::string nm = ifa->ifa_name;
1459
4/6
✓ Branch 0 taken 91 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 91 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✓ Branch 5 taken 83 times.
182 if (startsWith(nm, "wl")) {
1460
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 wireless.insert(nm);
1461 }
1462 91 }
1463
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 if (ifAddrStruct != NULL) {
1464 21 freeifaddrs(ifAddrStruct);
1465 }
1466
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 21 times.
23 for (const auto& nm : wireless) {
1467
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 printf("FPP - Disabling power saving for %s\n", nm.c_str());
1468
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
2 exec("/usr/sbin/iw dev " + ShellSingleQuote(nm) + " set power_save off 2>/dev/null > /dev/null");
1469 }
1470 21 }
1471
1472 45 void maybeEnableTethering() {
1473
2/4
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
45 int te = getRawSettingInt("EnableTethering", 0);
1474
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 9 times.
45 if (te == 0) {
1475 36 bool found = false;
1476 36 struct ifaddrs* ifAddrStruct = NULL;
1477 36 struct ifaddrs* ifa = NULL;
1478 36 void* tmpAddrPtr = NULL;
1479 36 getifaddrs(&ifAddrStruct);
1480
2/2
✓ Branch 0 taken 150 times.
✓ Branch 1 taken 36 times.
186 for (ifa = ifAddrStruct; ifa != NULL; ifa = ifa->ifa_next) {
1481
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 150 times.
150 if (!ifa->ifa_addr) {
1482 92 continue;
1483 }
1484
1/2
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
150 std::string nm = ifa->ifa_name;
1485
13/30
✓ Branch 0 taken 150 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 150 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 150 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 150 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 150 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 92 times.
✓ Branch 11 taken 58 times.
✓ Branch 12 taken 150 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 150 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 150 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 150 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 92 times.
✓ Branch 21 taken 58 times.
✗ Branch 22 not taken.
✗ Branch 23 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
750 if (startsWith(nm, "usb") || startsWith(nm, "lo")) {
1486 92 continue;
1487 }
1488
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 30 times.
58 if (ifa->ifa_addr->sa_family == AF_INET) { // check it is IP4
1489 // is a valid IP4 Address
1490 28 tmpAddrPtr = &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr;
1491 char addressBuffer[INET_ADDRSTRLEN];
1492 28 inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN);
1493
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 std::string addr = addressBuffer;
1494
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
28 if (isUpstreamAddress(addr)) {
1495 28 found = true;
1496 }
1497 28 }
1498
2/2
✓ Branch 0 taken 58 times.
✓ Branch 1 taken 92 times.
150 }
1499
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 if (ifAddrStruct != NULL) {
1500 36 freeifaddrs(ifAddrStruct);
1501 }
1502
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 28 times.
36 if (!found) {
1503 // Before enabling tethering, check if any wired interface has carrier/link.
1504 // If a cable is connected, DHCP will likely succeed shortly - don't enable tethering.
1505 // This matches the original bash MaybeEnableTethering logic that checked ethtool link.
1506 8 bool hasWiredLink = false;
1507
5/8
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 15 times.
✓ Branch 7 taken 3 times.
18 for (const auto& entry : std::filesystem::directory_iterator("/sys/class/net/")) {
1508
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 std::string dev = entry.path().filename();
1509
29/58
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✓ Branch 5 taken 6 times.
✓ Branch 6 taken 9 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 9 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 9 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 9 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 9 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 9 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 9 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 9 times.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✓ Branch 23 taken 9 times.
✓ Branch 24 taken 9 times.
✓ Branch 25 taken 6 times.
✓ Branch 26 taken 9 times.
✓ Branch 27 taken 6 times.
✓ Branch 28 taken 9 times.
✓ Branch 29 taken 6 times.
✓ Branch 30 taken 9 times.
✓ Branch 31 taken 6 times.
✓ Branch 32 taken 9 times.
✓ Branch 33 taken 6 times.
✓ Branch 34 taken 9 times.
✓ Branch 35 taken 6 times.
✓ Branch 36 taken 15 times.
✗ Branch 37 not taken.
✓ Branch 38 taken 15 times.
✗ Branch 39 not taken.
✓ Branch 40 taken 6 times.
✓ Branch 41 taken 9 times.
✗ Branch 42 not taken.
✗ Branch 43 not taken.
✗ Branch 44 not taken.
✗ Branch 45 not taken.
✗ Branch 46 not taken.
✗ Branch 47 not taken.
✗ Branch 48 not taken.
✗ Branch 49 not taken.
✗ Branch 50 not taken.
✗ Branch 51 not taken.
✗ Branch 52 not taken.
✗ Branch 53 not taken.
✗ Branch 54 not taken.
✗ Branch 55 not taken.
✗ Branch 56 not taken.
✗ Branch 57 not taken.
99 if (startsWith(dev, "eth") || startsWith(dev, "en") || startsWith(dev, "br") || startsWith(dev, "bond")) {
1510
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 std::string carrierPath = "/sys/class/net/" + dev + "/carrier";
1511
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 if (FileExists(carrierPath)) {
1512
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 std::string carrier = GetFileContents(carrierPath);
1513
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 TrimWhiteSpace(carrier);
1514
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 1 times.
6 if (carrier == "1") {
1515
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 printf("FPP - Wired interface %s has link, not enabling tethering\n", dev.c_str());
1516 5 hasWiredLink = true;
1517 5 break;
1518 }
1519
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
6 }
1520
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 5 times.
6 }
1521
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
23 }
1522
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3 times.
8 if (hasWiredLink) {
1523 // A wired interface has link - DHCP should complete soon.
1524 // Don't enable tethering; the routable.d/check_tether dispatcher
1525 // will clean up tethering if it was enabled from a previous boot.
1526 5 return;
1527 }
1528 // Check if the tether interface has a wifi client configuration
1529
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 std::string tetherInterface = FindTetherWIFIAdapater();
1530
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::string interfaceConfigFile = FPP_MEDIA_DIR + "/config/interface." + tetherInterface;
1531
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
3 if (FileExists(interfaceConfigFile)) {
1532
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 auto interfaceSettings = loadSettingsFile(interfaceConfigFile);
1533
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
2 if (!interfaceSettings["SSID"].empty()) {
1534 // WiFi client config exists - give it time to connect and get DHCP
1535
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - %s has WiFi SSID configured, waiting up to 12s for connection and DHCP...\n", tetherInterface.c_str());
1536 1 int waitCount = 0;
1537 1 bool gotIP = false;
1538 1 bool connected = false;
1539
4/6
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 24 times.
✗ Branch 5 not taken.
25 while (waitCount < 24 && !gotIP && !connected) { // 24 * 500ms = 12 seconds
1540
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 std::this_thread::sleep_for(std::chrono::milliseconds(500));
1541 // Check for IP address
1542 24 struct ifaddrs* ifAddrCheck = NULL;
1543
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 if (getifaddrs(&ifAddrCheck) == 0) {
1544
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 24 times.
48 for (struct ifaddrs* ifa = ifAddrCheck; ifa != NULL; ifa = ifa->ifa_next) {
1545
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 24 times.
24 if (ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET) {
1546 ✗ std::string ifname = ifa->ifa_name;
1547 ✗ if (ifname == tetherInterface) {
1548 ✗ void* tmpAddrPtr = &((struct sockaddr_in*)ifa->ifa_addr)->sin_addr;
1549 char addressBuffer[INET_ADDRSTRLEN];
1550 ✗ inet_ntop(AF_INET, tmpAddrPtr, addressBuffer, INET_ADDRSTRLEN);
1551 ✗ std::string addr = addressBuffer;
1552 ✗ if (!contains(addr, "169.254.")) { // Ignore link-local
1553 ✗ printf("FPP - %s got IP address %s\n", tetherInterface.c_str(), addr.c_str());
1554 ✗ gotIP = true;
1555 ✗ break;
1556 }
1557 ✗ }
1558 ✗ }
1559 }
1560 24 freeifaddrs(ifAddrCheck);
1561 }
1562 // Also check if connected to AP (even without IP yet)
1563
3/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 11 times.
24 if (!gotIP && waitCount > 10) { // After 5 seconds, start checking connection status
1564
4/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
13 std::string iwOutput = execAndReturn("/usr/sbin/iw " + ShellSingleQuote(tetherInterface) + " link");
1565
3/6
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 13 times.
26 if (contains(iwOutput, "Connected to")) {
1566 ✗ printf("FPP - %s connected to WiFi, keeping WiFi config active\n", tetherInterface.c_str());
1567 ✗ connected = true;
1568 }
1569 13 }
1570
2/4
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
24 if (gotIP || connected)
1571 break;
1572 24 waitCount++;
1573 }
1574
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (!gotIP && !connected) {
1575
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 printf("FPP - %s no IP or connection after 12s, enabling tethering\n", tetherInterface.c_str());
1576 1 te = 1;
1577 }
1578 } else {
1579 // Interface config exists but no SSID - enable tethering
1580 ✗ te = 1;
1581 }
1582 1 } else {
1583 // did not find an ip address and no wifi client config exists
1584 2 te = 1;
1585 }
1586 3 }
1587 }
1588
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 std::string tetherInterface = FindTetherWIFIAdapater();
1589
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 32 times.
40 if (te == 1) {
1590
4/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 4 times.
8 if (!FileExists("/sys/class/net/" + tetherInterface)) {
1591
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 printf("No interface for tethering.\n");
1592 4 te = 2;
1593 }
1594 }
1595
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 36 times.
40 if (te == 1) {
1596
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string c = CreateHostAPDConfig(tetherInterface);
1597
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 PutFileContents("/etc/hostapd/hostapd.conf", c);
1598
1599 // Remove wpa_supplicant config if it exists (switching from client to AP mode)
1600
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::string wpaConfig = "/etc/wpa_supplicant/wpa_supplicant-" + tetherInterface + ".conf";
1601
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 4 times.
4 if (FileExists(wpaConfig)) {
1602 ✗ printf("FPP - Removing wpa_supplicant config for %s to enable tethering\n", tetherInterface.c_str());
1603 ✗ unlink(wpaConfig.c_str());
1604 ✗ exec("/usr/bin/systemctl stop " + ShellSingleQuote("wpa_supplicant@" + tetherInterface + ".service"));
1605 ✗ exec("/usr/bin/systemctl disable " + ShellSingleQuote("wpa_supplicant@" + tetherInterface + ".service"));
1606 }
1607
1608
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string content = "[Match]\nName=";
1609
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 content.append(tetherInterface).append("\nType=wlan\n\n"
1610 "[Network]\n"
1611 "DHCP=no\n"
1612 "Address=192.168.8.1/24\n"
1613 "DHCPServer=yes\n\n");
1614
1615
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 AppendTetherDHCPServerSection(content, tetherInterface);
1616
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
4 PutFileContents("/etc/systemd/network/10-" + tetherInterface + ".network", content);
1617
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 unblockWifi();
1618
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 exec("/usr/bin/systemctl reload-or-restart systemd-networkd.service");
1619
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 unblockWifi();
1620
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 exec("/usr/bin/systemctl reload-or-restart hostapd.service");
1621 // Only the boot-time symlink is wanted here. Plain "enable" also syncs
1622 // hostapd's SysV init script through update-rc.d and then reloads the
1623 // manager: measured 4.75s on a PocketBeagle2 even when already enabled,
1624 // on fppd's startup path. systemd ignores the init script since a native
1625 // unit exists, and nothing needs a reload to see a new wants link.
1626
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 exec("SYSTEMCTL_SKIP_SYSV=1 /usr/bin/systemctl enable --no-reload hostapd.service");
1627 4 }
1628 40 }
1629 45 void detectNetworkModules() {
1630 45 std::string content;
1631
5/8
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 93 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 93 times.
✓ Branch 7 taken 45 times.
138 for (const auto& entry : std::filesystem::directory_iterator("/sys/class/net/")) {
1632
2/4
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 93 times.
✗ Branch 3 not taken.
93 const std::string filename = entry.path().filename();
1633
2/4
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 93 times.
✗ Branch 3 not taken.
93 std::string modName = "/sys/class/net/" + filename + "/device/driver/module";
1634
2/4
✓ Branch 0 taken 93 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 93 times.
93 if (FileExists(modName)) {
1635 char buf[256];
1636 ✗ size_t l = readlink(modName.c_str(), buf, sizeof(buf));
1637 ✗ buf[l] = 0;
1638 ✗ std::string mod = buf;
1639 ✗ mod = mod.substr(mod.rfind("/") + 1);
1640 ✗ content += mod;
1641 ✗ content += "\n";
1642 ✗ }
1643 138 }
1644
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 45 times.
45 if (!content.empty()) {
1645 ✗ PutFileContents("/etc/modules-load.d/fpp-network.conf", content);
1646 // PutFileContents("/home/fpp/media/config/fpp-network-modules.conf", content);
1647 }
1648 45 }
1649 46 void removeDummyInterface() {
1650
2/4
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 46 times.
✗ Branch 3 not taken.
46 exec("ip link delete dummy0 > /dev/null 2>&1");
1651 46 }
1652