Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
reload_controller.hpp File Reference

Watch a logic library on disk and hot-reload it when it changes (the standalone dev loop). More...

#include <pulp/format/reload/reload_transaction.hpp>
#include <pulp/format/reload/reload_trust_policy.hpp>
#include <pulp/runtime/crypto.hpp>
#include <array>
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <optional>
#include <string>
#include <system_error>
#include <utility>
#include <cerrno>
#include <csignal>
#include <unistd.h>

Go to the source code of this file.

Classes

class  pulp::format::reload::ReloadController
struct  pulp::format::reload::ReloadController::Baseline
 The version the controller compares against: the watched file's mtime and content hash it last acted on (or was seeded with). More...

Namespaces

namespace  pulp
namespace  pulp::format
 What the product grants a device: the quota table indexed by capability tier and live thermal state.
namespace  pulp::format::reload

Functions

bool pulp::format::reload::staged_pid_is_alive (long pid)
void pulp::format::reload::reap_stale_staged (const std::filesystem::path &dir, const std::string &stem)

Detailed Description

Watch a logic library on disk and hot-reload it when it changes (the standalone dev loop).

The shell drives poll() on a control-thread timer (the standalone app ticks it from its UI/event loop). When the watched logic library's modification time changes — i.e. the developer recompiled it — the controller runs a ReloadSession reload, which gates and (on success) swaps the new DSP into the live slot. Polling mtime is portable and dependency-free; a half-written file is harmless because the transaction's load/fingerprint/contract gates reject it (no swap), and the next completed write (new mtime) is retried.

Each distinct file version is attempted exactly once: the controller records the mtime it acted on whether the reload succeeded or failed, so a candidate that fails its gates is not retried on every tick — only the next edit (a new mtime) triggers another attempt. The first poll() establishes the baseline without reloading (the shell already loaded the initial logic).

CRITICAL — load each version from a UNIQUE path, for TWO independent reasons (do not "simplify" the staging away on the strength of only one):

  1. dlopen/LoadLibrary cache by path, so re-opening the watched path after the developer overwrites it returns the STALE in-memory image, not the new bytes.
  2. A live mapping must never sit on the watched path: a rebuild overwrites that file, often IN PLACE (truncate + rewrite of the same inode). On Linux an in-place overwrite of a still-mapped image bleeds the new bytes into the live mapping and corrupts the Processor running from it (its code/vtable) — a crash that surfaces when that processor is the fading-out side of the next crossfade. (macOS tolerates the overwrite, so this only bites on Linux.) ReloadableShell::load_initial stages the INITIAL image for the same reason. The controller therefore stages each detected version to a fresh, uniquely-named copy in stage_dir and reloads THAT, guaranteeing the loader reads the new file AND that the watched path never carries a live mapping. stage_dir must be a dlopen-safe location (NOT a world-writable temp dir on macOS, where dyld kills a process that loads an unsigned dylib from /tmp); it defaults to the watched file's own directory.