Harden USB logging and persist watchdog events in management reports
This commit is contained in:
1 parent
bd55369aa9
commit
3add4695cc
98 files changed
+7207
-372
No files matched your search
@@ -0,0 +1,358 @@
|
||||
"""Exercise actual CommonCLI USB debug commands, images, and import commits."""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
HARNESS = r'''
|
||||
#include <cassert>
|
||||
#include <math.h>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <helpers/IdentityStore.h>
|
||||
class TestFS : public MemoryFS {
|
||||
public:
|
||||
using MemoryFS::open;
|
||||
File open(const char* path, uint8_t mode) {
|
||||
return MemoryFS::open(path, mode == FILE_O_WRITE ? "w" : "r");
|
||||
}
|
||||
};
|
||||
#undef FILESYSTEM
|
||||
#define FILESYSTEM TestFS
|
||||
#include "ContactFileTransaction.h"
|
||||
#define ATOMIC_FILE_WRITER_IMPLEMENTATION
|
||||
#include <helpers/AtomicFileWriter.h>
|
||||
#include <helpers/PrefsSaveRouting.h>
|
||||
#include <helpers/CommonPrefsRecovery.h>
|
||||
#include <helpers/MQTTPrefsAtomicStore.h>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/RepeaterRadioTiming.h>
|
||||
#include <helpers/bridges/ESPNowBridgeFormat.h>
|
||||
#include <helpers/radiolib/LR2021SideDetectorConfig.h>
|
||||
#define MIN_LORA_TX_POWER -9
|
||||
#define MAX_LORA_TX_POWER 22
|
||||
#include <helpers/radiolib/RadioPowerLimits.h>
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define MESH_USB_LOGGING_AVAILABLE 1
|
||||
#define DEFAULT_POWERSAVING_ENABLED 1
|
||||
#define DEFAULT_CAD_ENABLED 0
|
||||
#define BRIDGE_MAX_BAUD 115200
|
||||
#define RX_POWERSAVING_DEFAULT_RX_US 65625UL
|
||||
#define RX_POWERSAVING_DEFAULT_SLEEP_US 60000UL
|
||||
#define FLOOD_RETRY_PATH_GATE_DISABLED 0xFF
|
||||
#define MIN_LOCAL_ADVERT_INTERVAL 60
|
||||
#define constrain(value, low, high) ((value) < (low) ? (low) : ((value) > (high) ? (high) : (value)))
|
||||
@CONSTANTS@
|
||||
namespace mesh {
|
||||
struct RadioProfiles { static constexpr unsigned MaxPreamble = 65535; };
|
||||
static bool usb_logging = true, usb_debug = false;
|
||||
static bool prohibit_stale_debug = false;
|
||||
static unsigned usb_updates = 0, debug_updates = 0;
|
||||
void setUsbLoggingEnabled(bool enabled) {
|
||||
if (enabled && prohibit_stale_debug) assert(!usb_debug);
|
||||
usb_logging = enabled; ++usb_updates;
|
||||
}
|
||||
void setUsbDebugEnabled(bool enabled) { usb_debug = enabled; ++debug_updates; }
|
||||
bool isUsbLoggingEnabled() { return usb_logging; }
|
||||
bool isUsbDebugEnabled() { return usb_debug; }
|
||||
bool isUsbDebugLoggingEnabled() { return usb_logging && usb_debug; }
|
||||
}
|
||||
struct NodePrefs {
|
||||
@FIELDS@
|
||||
bool dirty = false;
|
||||
void clearDirty() { dirty = false; }
|
||||
void markUnsaved() { dirty = true; }
|
||||
};
|
||||
@HELPERS@
|
||||
void ensureRxPowerSavingDefaults(uint32_t*, uint32_t*) {}
|
||||
void recalcRxPowerSavingFromLevel(uint8_t, uint8_t, float, uint8_t,
|
||||
uint32_t*, uint32_t*) {}
|
||||
@LEGACY@
|
||||
@SERIALIZER@
|
||||
@STORE@;
|
||||
struct CommonCLI;
|
||||
struct Callbacks {
|
||||
CommonCLI* cli;
|
||||
void savePrefs(PrefsSaveRouting::Scope scope);
|
||||
void updateAdvertTimer() {}
|
||||
};
|
||||
struct CommonCLI {
|
||||
FILESYSTEM fs;
|
||||
NodePrefs prefs;
|
||||
NodePrefs* _prefs = &prefs;
|
||||
LegacyObserverTail _legacy_tail{};
|
||||
Callbacks callbacks{this};
|
||||
Callbacks* _callbacks = &callbacks;
|
||||
bool _com_prefs_needs_upgrade = false;
|
||||
bool _common_save_result_known = false, _common_save_succeeded = false;
|
||||
bool _observer_save_result_known = false, _observer_save_succeeded = false;
|
||||
uint32_t _prefs_save_failures = 0;
|
||||
CommonCLI() {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
fs.rename_replaces = true;
|
||||
#endif
|
||||
prefs.freq = 910; prefs.bw = 62.5f; prefs.sf = 7; prefs.cr = 5;
|
||||
prefs.primary_radio_preamble = 48;
|
||||
}
|
||||
void savePrefs(FILESYSTEM*, PrefsSaveRouting::Scope);
|
||||
void savePrefs(PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common);
|
||||
bool trySavePrefs(PrefsSaveRouting::Scope scope = PrefsSaveRouting::Scope::Common);
|
||||
void loadPrefsInt(FILESYSTEM*, const char*);
|
||||
bool recoverCommonPrefsFiles(FILESYSTEM*);
|
||||
bool saveCommonPrefsImageAtomically(FILESYSTEM*);
|
||||
bool saveMQTTPrefs(FILESYSTEM*) { return true; }
|
||||
void set(const char* config, char* reply) {
|
||||
@SET_DEBUG@
|
||||
@SET_MASTER@
|
||||
strcpy(reply, "unknown");
|
||||
}
|
||||
void get(const char* config, char* reply) {
|
||||
@GET_DEBUG@
|
||||
@GET_MASTER@
|
||||
strcpy(reply, "unknown");
|
||||
}
|
||||
};
|
||||
void Callbacks::savePrefs(PrefsSaveRouting::Scope scope) {
|
||||
cli->savePrefs(&cli->fs, scope);
|
||||
}
|
||||
@METHODS@
|
||||
|
||||
// Execute the production dynamic schema, replacing only its generic codec.
|
||||
// Registered uint8_t references emulate imported configuration values.
|
||||
class ConfigSerializer {
|
||||
protected:
|
||||
virtual void structure() = 0;
|
||||
template<typename T> void def(const char*, T&) {}
|
||||
void def(const char* key, uint8_t& value) { bytes[key] = &value; }
|
||||
void def(const char*, char*, size_t) {}
|
||||
public:
|
||||
std::map<std::string, uint8_t*> bytes;
|
||||
bool import(const char* key, uint8_t value) {
|
||||
structure();
|
||||
if (!bytes.count(key)) return false;
|
||||
*bytes[key] = value;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@DYNAMIC@;
|
||||
struct Capture {
|
||||
std::vector<uint8_t> bytes;
|
||||
size_t write(const uint8_t* data, size_t n) {
|
||||
bytes.insert(bytes.end(), data, data + n);
|
||||
return n;
|
||||
}
|
||||
};
|
||||
int main() {
|
||||
CommonCLI cli;
|
||||
char reply[160] = {};
|
||||
assert(cli.prefs.usb_debug_enabled == 0);
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded);
|
||||
const auto baseline = cli.fs.files["/com_prefs"];
|
||||
assert(baseline.size() == 870);
|
||||
const size_t debug_offset = baseline.size() - 1;
|
||||
const size_t logging_offset = baseline.size() - 7;
|
||||
const size_t preamble_offset = baseline.size() - 4;
|
||||
const size_t espnow_offset = baseline.size() - 2;
|
||||
assert(baseline[debug_offset] == 0 && baseline[logging_offset] == 1);
|
||||
assert(baseline[preamble_offset] == 48 && baseline[espnow_offset] == 1);
|
||||
Capture capture;
|
||||
assert(writeCommonPrefsImage(capture, &cli.prefs));
|
||||
assert(capture.bytes == baseline); // Checked and ordinary writers agree.
|
||||
|
||||
cli.set("usb.debug on", reply);
|
||||
assert(strcmp(reply, "OK - USB debug on (saved)") == 0);
|
||||
assert(cli.prefs.usb_debug_enabled == 1 && mesh::isUsbDebugLoggingEnabled());
|
||||
auto enabled = cli.fs.files["/com_prefs"];
|
||||
assert(enabled.size() == baseline.size() && enabled[debug_offset] == 1);
|
||||
assert(std::equal(baseline.begin(), baseline.begin() + debug_offset, enabled.begin()));
|
||||
cli.set("usb.logging off", reply);
|
||||
assert(!mesh::isUsbLoggingEnabled() && mesh::isUsbDebugEnabled());
|
||||
assert(!mesh::isUsbDebugLoggingEnabled());
|
||||
cli.get("usb.debug", reply);
|
||||
assert(strcmp(reply, "> on") == 0); // Saved intent is visible with master OFF.
|
||||
cli.get("usb.logging", reply);
|
||||
assert(strcmp(reply, "> off") == 0);
|
||||
|
||||
// Malformed tokens and suffixes never mutate either gate or the image.
|
||||
const auto unchanged = cli.fs.files["/com_prefs"];
|
||||
for (const char* command : {"usb.debug", "usb.debug true", "usb.debug 1",
|
||||
"usb.debug ON", "usb.debug off junk", "usb.debug off ",
|
||||
"usb.debug on reboot", "usb.debug off reboot"}) {
|
||||
const unsigned updates = mesh::debug_updates;
|
||||
cli.set(command, reply);
|
||||
assert(strcmp(reply, "Error: usage set usb.debug on|off") == 0);
|
||||
assert(cli.prefs.usb_debug_enabled == 1 && mesh::isUsbDebugEnabled());
|
||||
assert(mesh::debug_updates == updates);
|
||||
assert(cli.fs.files["/com_prefs"] == unchanged);
|
||||
}
|
||||
cli.set("usb.debugger off", reply);
|
||||
assert(strcmp(reply, "unknown") == 0);
|
||||
cli.set("usb.debug\t \toff", reply);
|
||||
assert(strcmp(reply, "OK - USB debug off (saved)") == 0);
|
||||
assert(cli.prefs.usb_debug_enabled == 0 && !mesh::isUsbDebugEnabled());
|
||||
const auto debug_off = cli.fs.files["/com_prefs"];
|
||||
|
||||
// Failed first/write/verification/publication stages cannot enable debug.
|
||||
for (int fault : {0, 1, 2, 3}) {
|
||||
if (fault == 0) cli.fs.fail_write = true;
|
||||
if (fault == 1) cli.fs.fail_write_after = debug_offset;
|
||||
if (fault == 2) cli.fs.fail_read_open = true;
|
||||
if (fault == 3) cli.fs.fail_rename = 1;
|
||||
const unsigned updates = mesh::debug_updates;
|
||||
cli.set("usb.debug on", reply);
|
||||
assert(strcmp(reply, "Error: USB debug not saved; unchanged") == 0);
|
||||
assert(cli.prefs.usb_debug_enabled == 0 && !mesh::isUsbDebugEnabled());
|
||||
assert(mesh::debug_updates == updates && !mesh::isUsbLoggingEnabled());
|
||||
assert(cli.fs.files["/com_prefs"] == debug_off);
|
||||
cli.fs.fail_write = false; cli.fs.fail_write_after = -1;
|
||||
cli.fs.fail_read_open = false; cli.fs.fail_rename = 0;
|
||||
}
|
||||
|
||||
// Imported schemas apply gates only after a successful common commit.
|
||||
BridgePrefs dynamic(&cli.prefs);
|
||||
assert(dynamic.import("usb_dbg", 1));
|
||||
assert(!mesh::isUsbDebugEnabled());
|
||||
const unsigned observer_updates = mesh::debug_updates;
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Observer);
|
||||
assert(!mesh::isUsbDebugEnabled() && mesh::debug_updates == observer_updates);
|
||||
assert(cli.fs.files["/com_prefs"] == debug_off);
|
||||
cli.fs.fail_write = true;
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(!cli._common_save_succeeded && !mesh::isUsbDebugEnabled());
|
||||
cli.fs.fail_write = false;
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded && mesh::isUsbDebugEnabled());
|
||||
assert(!mesh::isUsbDebugLoggingEnabled());
|
||||
assert(dynamic.import("usb_log", 1));
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(mesh::isUsbDebugLoggingEnabled());
|
||||
// A debug-OFF import must take effect before the master is enabled again.
|
||||
mesh::setUsbLoggingEnabled(false);
|
||||
assert(dynamic.import("usb_dbg", 0));
|
||||
mesh::prohibit_stale_debug = true;
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
mesh::prohibit_stale_debug = false;
|
||||
assert(mesh::isUsbLoggingEnabled() && !mesh::isUsbDebugEnabled());
|
||||
for (uint8_t bad : {2, 255}) {
|
||||
assert(dynamic.import("usb_dbg", bad));
|
||||
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
|
||||
assert(cli._common_save_succeeded && !mesh::isUsbDebugEnabled());
|
||||
assert(cli.prefs.usb_debug_enabled == 0 && cli.fs.files["/com_prefs"][debug_offset] == 0);
|
||||
}
|
||||
|
||||
// Every older/torn length inherits debug OFF, even after loading debug ON.
|
||||
auto image = baseline;
|
||||
image[logging_offset] = 0; image[debug_offset] = 1;
|
||||
for (size_t size = 0; size < image.size(); ++size) {
|
||||
CommonCLI reader;
|
||||
reader.prefs.usb_debug_enabled = 1;
|
||||
reader.fs.files["/com_prefs"] = {image.begin(), image.begin() + size};
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.usb_debug_enabled == 0);
|
||||
if (size > logging_offset) assert(reader.prefs.usb_logging_enabled == 0);
|
||||
}
|
||||
for (uint8_t saved : {0, 1, 2, 255}) {
|
||||
image[debug_offset] = saved;
|
||||
cli.fs.files["/com_prefs"] = image;
|
||||
cli.prefs.usb_debug_enabled = 1;
|
||||
cli.loadPrefsInt(&cli.fs, "/com_prefs");
|
||||
assert(cli.prefs.usb_debug_enabled == (saved == 1 ? 1 : 0));
|
||||
assert(cli.prefs.usb_logging_enabled == 0);
|
||||
assert(cli.prefs.primary_radio_preamble == 48 && cli.prefs.espnow_bridge_enabled == 1);
|
||||
}
|
||||
cli.prefs.usb_debug_enabled = 1;
|
||||
cli.loadPrefsInt(&cli.fs, "/missing");
|
||||
assert(cli.prefs.usb_debug_enabled == 0);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class CommonUsbDebugTest(unittest.TestCase):
|
||||
def test_actual_common_commands_and_persistence(self):
|
||||
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text(encoding='utf-8')
|
||||
header = (ROOT / 'src/helpers/CommonCLI.h').read_text(encoding='utf-8')
|
||||
fields = header.split('class NodePrefs :', 1)[1].split('private:', 1)[0]
|
||||
fields = '\n'.join(re.findall(
|
||||
r'^\s*(?:float|double|char|u?int(?:8|16|32)_t)\s+[^;]+;', fields, re.M))
|
||||
constants = '\n'.join(re.findall(
|
||||
r'^\s*#define\s+\w+\s+(?:0x[\dA-Fa-f]+|\d+)\s*$', header, re.M))
|
||||
constants += '\n' + '\n'.join(re.findall(
|
||||
r'^static const size_t LEGACY_\w+\s*=[^;]+;', source, re.M))
|
||||
constants += '\n' + re.search(
|
||||
r'^static constexpr uint8_t DEFAULT_ADVERT_LOC_POLICY\s*=[^;]+;',
|
||||
header, re.M).group()
|
||||
helpers = '\n'.join(extract_braced(source, signature) for signature in (
|
||||
'static bool bwMatches(', 'static bool isValidLoRaBandwidth(',
|
||||
'static float defaultLoRaBandwidth(', 'static void markDirectRetryPrefsValid(',
|
||||
'static void applyFloodRetryPreset(', 'static void applyDirectRetryPreset(',
|
||||
'static void setDefaultDirectRetryPrefs(', 'static bool directRetryPrefsValid('))
|
||||
methods = '\n'.join(extract_braced(source, signature) for signature in (
|
||||
'static const char* commonPrefsSaveResultName(',
|
||||
'bool CommonCLI::recoverCommonPrefsFiles(',
|
||||
'bool CommonCLI::saveCommonPrefsImageAtomically(',
|
||||
'void CommonCLI::savePrefs(FILESYSTEM*',
|
||||
'void CommonCLI::savePrefs(PrefsSaveRouting::Scope scope)',
|
||||
'bool CommonCLI::trySavePrefs(', 'void CommonCLI::loadPrefsInt('))
|
||||
code = HARNESS
|
||||
for key, value in {
|
||||
'FIELDS': fields, 'CONSTANTS': constants, 'HELPERS': helpers,
|
||||
'LEGACY': extract_braced(header, 'struct LegacyObserverTail') + ';',
|
||||
'SERIALIZER': 'template<typename Writer>\n' + extract_braced(
|
||||
source, 'static bool writeCommonPrefsImage('),
|
||||
'STORE': extract_braced(source, 'class CommonPrefsFileStore'),
|
||||
'METHODS': methods,
|
||||
'DYNAMIC': extract_braced(header, 'class BridgePrefs :'),
|
||||
'SET_DEBUG': extract_braced(source, 'if (strncmp(config, "usb.debug", 9)'),
|
||||
'SET_MASTER': extract_braced(source, 'if (strncmp(config, "usb.logging", 11)'),
|
||||
'GET_DEBUG': extract_braced(source, 'if (strcmp(config, "usb.debug") == 0)'),
|
||||
'GET_MASTER': extract_braced(source, 'if (strcmp(config, "usb.logging") == 0)'),
|
||||
}.items():
|
||||
code = code.replace('@' + key + '@', value)
|
||||
sanitizer_flags = [] if os.name == 'nt' else [
|
||||
'-fsanitize=address,undefined', '-fno-sanitize-recover=all',
|
||||
'-fno-pie', '-no-pie']
|
||||
with tempfile.TemporaryDirectory(prefix='common-usb-debug-') as directory:
|
||||
work = Path(directory)
|
||||
transaction = (ROOT / 'src/helpers/ContactFileTransaction.h').read_text()
|
||||
(work / 'ContactFileTransaction.h').write_text(transaction.replace(
|
||||
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
|
||||
(work / 'test.cpp').write_text(code, encoding='utf-8')
|
||||
variants = [('ENABLE_OTA', platform) for platform in (
|
||||
'NRF52_PLATFORM', 'STM32_PLATFORM', 'ESP32_PLATFORM', 'RP2040_PLATFORM')]
|
||||
variants.append(('ENABLE_OTA', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE'))
|
||||
variants.extend((('NRF52_PLATFORM',), ('ESP32_PLATFORM', 'WITH_MQTT_BRIDGE')))
|
||||
for macros in variants:
|
||||
with self.subTest(macros=macros):
|
||||
exe = work / ('test.exe' if os.name == 'nt' else 'test')
|
||||
built = subprocess.run([
|
||||
os.environ.get('CXX', 'g++'), '-std=c++17',
|
||||
*['-D' + macro + '=1' for macro in macros], *sanitizer_flags,
|
||||
'-I' + str(work), '-I' + str(ROOT / 'test/fixtures/radio_profiles/mocks'),
|
||||
'-I' + str(ROOT / 'src'), '-I' + str(ROOT / 'src/helpers'),
|
||||
str(work / 'test.cpp'), '-o', str(exe)],
|
||||
capture_output=True, text=True, timeout=60)
|
||||
self.assertEqual(built.returncode, 0, built.stderr)
|
||||
tested = subprocess.run([str(exe)], capture_output=True, text=True, timeout=10)
|
||||
self.assertEqual(tested.returncode, 0, tested.stderr)
|
||||
|
||||
def test_outer_load_applies_both_gates(self):
|
||||
source = (ROOT / 'src/helpers/CommonCLI.cpp').read_text(encoding='utf-8')
|
||||
load = extract_braced(source, 'void CommonCLI::loadPrefs(FILESYSTEM*')
|
||||
self.assertIn('_prefs->usb_debug_enabled = 0;', load)
|
||||
self.assertIn('mesh::setUsbLoggingEnabled(_prefs->usb_logging_enabled != 0);', load)
|
||||
self.assertIn('mesh::setUsbDebugEnabled(_prefs->usb_debug_enabled != 0);', load)
|
||||
self.assertLess(load.index('mesh::setUsbDebugEnabled('),
|
||||
load.index('mesh::setUsbLoggingEnabled('))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user