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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/reference/build-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,19 @@ src/
main.cpp ← shared pipeline wiring (mm_main), platform-neutral
platform/
desktop/
main_desktop.cpp ← desktop entry point: int main() + SIGINT
main_desktop.cpp ← desktop entry point: int main() + SIGINT and SIGTERM
platform_config.h ← desktop platform constants
esp32/
platform_config.h ← ESP32 platform constants (reads sdkconfig)
esp32/
CMakeLists.txt ← ESP-IDF project root (thin wrapper)
main/
CMakeLists.txt ← idf_component_register() pointing at src/
main.cpp ← ESP32 entry point: app_main() + Ethernet init
main.cpp ← ESP32 entry point: app_main() + NVS init
sdkconfig.defaults ← board-specific defaults
```

The shared `src/main.cpp` defines `mm_main(keepRunning, gridW, gridH)`, the full pipeline wiring. Each platform provides a thin entry point that does platform-specific init (SIGINT on desktop, Ethernet on ESP32) then calls `mm_main()`.
The shared `src/main.cpp` defines `mm_main(keepRunning, httpPort)`, the full pipeline wiring. Each platform provides a thin entry point that does platform-specific init (SIGINT and SIGTERM on desktop, NVS on ESP32; the network starts in the platform layer) then calls `mm_main()`.

The project is a small set of CMake libraries. A core library is platform-independent, a platform library is selected at configure time, and the application target links both and provides the entry point. Further decomposition (effects, networking, drivers as separate libraries) happens when the codebase is large enough to justify it.

Expand Down
5 changes: 3 additions & 2 deletions esp32/main/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
#include "esp_heap_caps.h"
#include "nvs_flash.h"

#include <csignal> // sig_atomic_t: mm_main's stop flag
#include <cstdio>

extern void mm_main(volatile bool& keepRunning, uint16_t httpPort);
extern void mm_main(volatile std::sig_atomic_t& keepRunning, uint16_t httpPort);

static volatile bool running = true;
static volatile std::sig_atomic_t running = 1;

extern "C" void app_main() {
// Initialize NVS (required by WiFi and other drivers)
Expand Down
2 changes: 1 addition & 1 deletion moonwalk/02-install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ processes:
say: Check the service runs.
when: { run: { command: "systemctl status moonlight" } }
detail: |
To stop the board cleanly later: `sudo shutdown now`, since an SD card cut off mid-write can corrupt the filesystem. The service comes back at the next boot.
`sudo systemctl stop moonlight` stops MoonLight cleanly, its drivers releasing their outputs. To stop the board: `sudo shutdown now`, since an SD card cut off mid-write can corrupt the filesystem. The service comes back at the next boot.
- id: container-overlay-root
title: Start the container on an overlay root
actor: user
Expand Down
4 changes: 3 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@
///
#include "module_types.h"

#include <csignal> // sig_atomic_t: the stop flag a signal handler writes

#include "core/util/JsonFetch.h" // poll(): the JSON worker stops once nothing uses it
#include "core/module/Scheduler.h"
#include "core/util/ModuleFactory.h"
Expand Down Expand Up @@ -178,7 +180,7 @@ static void printModuleMetrics(mm::MoonModule* mod, int depth) {
}
}

void mm_main(volatile bool& keepRunning, uint16_t httpPort) {
void mm_main(volatile std::sig_atomic_t& keepRunning, uint16_t httpPort) {
mm::registerModuleTypes();
mm::Scheduler scheduler;

Expand Down
33 changes: 17 additions & 16 deletions src/platform/desktop/main_desktop.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@
#include <unistd.h>
#endif

extern void mm_main(volatile bool& keepRunning, uint16_t httpPort);
extern void mm_main(volatile std::sig_atomic_t& keepRunning, uint16_t httpPort);

static volatile bool running = true;
// sig_atomic_t: the one type a signal handler may write and the main thread read.
static volatile std::sig_atomic_t running = 1;
static bool cleanExit = false;

// Write a string literal to the error stream from a signal handler, its length taken from the array bound so it can never drift from the text.
Expand Down Expand Up @@ -56,9 +57,7 @@ static void crashHandler(int sig) {
raise(sig);
}

// Local time as an ISO-8601 stamp, thread-safely.
// `std::localtime` returns a pointer to a SHARED static tm, so two threads formatting a timestamp can each see the other's value, flagged independently by clang-tidy (concurrency-mt-unsafe) and CodeQL (critical).
// The reentrant form is spelled differently per platform, hence the branch.
// Local time as an ISO-8601 stamp, thread-safely: `std::localtime` shares one static tm across threads (clang-tidy concurrency-mt-unsafe, CodeQL critical), and the reentrant form is spelled per platform, hence the branch.
static void isoTimestamp(char* out, size_t n) {
const std::time_t t = std::time(nullptr);
std::tm tm{};
Expand All @@ -80,9 +79,7 @@ static void atExitHandler() {
}
}

// Show the interface the moment the server is up, since a user launching from a file manager has no terminal to read an address out of.
// And typing one is exactly the step that loses someone on a first run.
// Best effort, a failure being silent because the banner already printed the address and a missing browser must not stop the server; a flag opts out for a headless box.
// Open the interface once the server is up, since a user launched from a file manager has no terminal to read an address from; best effort, as the banner printed it, and a flag opts out for a headless box.
static void openLocalUi(uint16_t port) {
char url[64];
std::snprintf(url, sizeof(url), "http://localhost:%u/", static_cast<unsigned>(port));
Expand All @@ -94,7 +91,7 @@ static void openLocalUi(uint16_t port) {
#else
std::snprintf(cmd, sizeof(cmd), "xdg-open \"%s\" >/dev/null 2>&1 &", url);
#endif
// Degrade visibly: a box with no xdg-open (the headless case --no-browser exists for) would otherwise do nothing at all, leaving the user waiting for a window that is never coming. The URL is already on stdout above, so this only has to say the auto-open failed.
// Degrade visibly: a box without xdg-open, the headless case --no-browser is for, would leave the user waiting for a window; the URL is already on stdout, so this says only that the auto-open failed.
if (std::system(cmd) != 0) {
std::printf(" (could not open a browser automatically, open %s yourself)\n", url);
}
Expand Down Expand Up @@ -138,12 +135,12 @@ int main(int argc, char** argv) {
#ifdef _WIN32
// Winsock is initialized by a static RAII guard in platform_desktop.cpp, so it covers test binaries too (they link mm_platform but have their own main()). Nothing to do here.

// Windows has no sigaction, use signal() with one-shot semantics (the handler is restored to SIG_DFL on entry, so re-raise produces the OS crash dialog). No SIGPIPE on Windows; broken socket writes return an error from send(), which the code already checks.
// Windows has no sigaction: signal() is one-shot, restoring SIG_DFL on entry so a re-raise shows the OS crash dialog. No SIGPIPE there; a broken socket write returns an error from send(), which the code checks.
signal(SIGSEGV, crashHandler);
signal(SIGFPE, crashHandler);
signal(SIGABRT, crashHandler);
// signal() is one-shot on Windows, so the handler is already reset after the first Ctrl+C and a second one force-quits: the same escalation SA_RESETHAND gives the POSIX branch below.
signal(SIGINT, [](int) { running = false; });
signal(SIGINT, [](int) { running = 0; });
#else
struct sigaction sa{};
sa.sa_handler = crashHandler;
Expand All @@ -154,17 +151,23 @@ int main(int argc, char** argv) {
sigaction(SIGBUS, &sa, nullptr);
sigaction(SIGABRT, &sa, nullptr);

// The first interrupt asks for a clean stop and restores the default handler, so a second one kills the process outright. Without that a wedged shutdown leaves the key doing nothing however many times it is pressed, and the only way out is another terminal.
// The first interrupt asks for a clean stop and restores the default handler, so a second kills a wedged shutdown outright rather than leaving the key dead and another terminal the only way out.
struct sigaction saInt{};
saInt.sa_handler = [](int) {
running = false;
running = 0;
// Say so, because the first press otherwise looks ignored while teardown runs. safeWrite, not printf: only async-signal-safe calls are legal in a handler.
safeWrite("\nStopping. Press Ctrl+C again to force quit.\n");
};
sigemptyset(&saInt.sa_mask);
saInt.sa_flags = SA_RESETHAND;
sigaction(SIGINT, &saInt, nullptr);

// SIGTERM is how systemd, Docker and kill stop a service: the same clean stop, so a HUB75 panel goes dark rather than freezing lit. Not one-shot: a second one would cut the stop short, and a service manager escalates to SIGKILL itself.
struct sigaction saTerm{};
saTerm.sa_handler = [](int) { running = 0; };
sigemptyset(&saTerm.sa_mask);
sigaction(SIGTERM, &saTerm, nullptr);

// Ignore SIGPIPE, write() on a closed TCP connection delivers it by default, which kills the process silently (no crash report) when a browser tab closes mid-response. We handle broken writes via return-value checks instead.
signal(SIGPIPE, SIG_IGN);
#endif
Expand All @@ -173,9 +176,7 @@ int main(int argc, char** argv) {

char tbuf[32];
isoTimestamp(tbuf, sizeof(tbuf));
// The banner, then the URL, then how to stop.
// This window IS the app on a desktop: there is no tray icon and no GUI, so it has to say what it is and how to end it.
// The tick lines that follow print for the first 60 s and then go quiet unless the log level asks for them, so this stays readable once the machine settles.
// The banner, the URL, then how to stop: this window is the app on a desktop, with no tray icon or GUI, and the tick lines below print for the first 60 s, then go quiet unless the log level asks.
std::printf("MoonLight started at %s\n", tbuf);
std::printf("\n MoonLight is running: http://localhost:%u/\n", static_cast<unsigned>(httpPort));
std::printf(" Close this window (or press Ctrl-C) to stop.\n\n");
Expand Down
5 changes: 3 additions & 2 deletions teensy/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
#include <Arduino.h>
#include <CrashReport.h>

#include <csignal> // sig_atomic_t: mm_main's stop flag
#include <cstdint>

#include "platform/platform.h" // bootRecord: the reset cause, read before the crash report clears it

extern void mm_main(volatile bool& keepRunning, uint16_t httpPort);
extern void mm_main(volatile std::sig_atomic_t& keepRunning, uint16_t httpPort);

static volatile bool running = true;
static volatile std::sig_atomic_t running = 1;

void setup() {
Serial.begin(115200);
Expand Down
Loading