list the sources with a stored session

known_sources() reads the session files the layout store already owns, keeping
only those that name a signal: a development machine accumulates one file per
probe run on a throwaway port, and all 140 of them here hold empty slots.
This commit is contained in:
Martino Ferrari committed 2026-10-04 00:08:15 +02:00
1 parent c604d62917
commit eabdde34f9
2 files changed
+78

No files matched your search

+56
View File
@@ -3,8 +3,11 @@
#include "layout_store.hpp" #include "layout_store.hpp"
#include <algorithm>
#include <cstdint> #include <cstdint>
#include <cstdlib>
#include <cstdio> #include <cstdio>
#include <dirent.h>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <sys/stat.h> #include <sys/stat.h>
@@ -117,6 +120,59 @@ std::string layout_path(const std::string& host, std::uint16_t port) {
return dir + "/" + sanitize(host) + "_" + std::to_string(port) + ".toml"; return dir + "/" + sanitize(host) + "_" + std::to_string(port) + ".toml";
} }
std::vector<KnownSource> known_sources() {
std::vector<KnownSource> out;
const std::string dir = config_dir();
DIR* d = ::opendir(dir.c_str());
// No directory yet is an empty list, not an error: a first run has never saved anything.
if (d == nullptr) return out;
while (const dirent* e = ::readdir(d)) {
const std::string name = e->d_name;
if (name.size() <= 5 || name.compare(name.size() - 5, 5, ".toml") != 0) continue;
const std::string stem = name.substr(0, name.size() - 5);
// The *last* '_': `sanitize()` replaces anything unsafe in a host with one, so the host part
// may hold them too and a first-match split would return a host cut in half.
const std::size_t cut = stem.rfind('_');
if (cut == std::string::npos || cut + 1 == stem.size()) continue;
const long port = std::strtol(stem.c_str() + cut + 1, nullptr, 10);
if (port <= 0 || port > 65535) continue;
LayoutState st;
const std::string path = dir + "/" + name;
if (!load_layout(path, st)) continue;
KnownSource s;
s.host = stem.substr(0, cut);
s.port = static_cast<std::uint16_t>(port);
s.multicast = st.multicast;
s.interface_addr = st.interface_addr;
s.rows = static_cast<int>(st.rows.size());
for (const LayoutState::Row& row : st.rows) {
for (const std::string& sig : row.signals) {
if (!sig.empty()) ++s.signals;
}
}
if (s.signals == 0) continue; // see the header: a session nobody configured
struct stat sb {};
s.mtime = (::stat(path.c_str(), &sb) == 0) ? static_cast<std::int64_t>(sb.st_mtime) : 0;
out.push_back(std::move(s));
}
::closedir(d);
// Stable, so two files sharing an mtime keep the order the directory gave them rather than
// swapping between runs.
std::stable_sort(out.begin(), out.end(),
[](const KnownSource& a, const KnownSource& b) { return a.mtime > b.mtime; });
return out;
}
bool forget_source(const KnownSource& src) {
// Through `layout_path()` rather than by rebuilding the name here: one expression of the
// scheme. `src.host` came back out of a filename, so it is already sanitized, and `sanitize()`
// keeps alphanumerics, '.', '-' and '_' -- it is idempotent, so the round trip is exact.
return std::remove(layout_path(src.host, src.port).c_str()) == 0;
}
std::string math_node_text(const MathNode& node) { std::string math_node_text(const MathNode& node) {
// The op is written as its index through `math_op_at()`, not as the raw enum value: that accessor // The op is written as its index through `math_op_at()`, not as the raw enum value: that accessor
// is the contract the dialog, the engine and this file share, so the format is tied to it rather // is the contract the dialog, the engine and this file share, so the format is tied to it rather
+22
View File
@@ -53,6 +53,28 @@ struct LayoutState {
std::vector<Measure> measures; std::vector<Measure> measures;
}; };
/// A data source udpscope has a stored session for: one row of the startup chooser.
struct KnownSource {
std::string host; ///< the CONFIG server, even for a multicast session
std::uint16_t port = 0;
std::string multicast; ///< empty = unicast
std::string interface_addr; ///< empty = the default route
int rows = 0; ///< plots the session had
int signals = 0; ///< named slots across those plots
std::int64_t mtime = 0; ///< orders the list; never shown
};
/// Every session file that names at least one signal, newest first.
///
/// The filter is what makes the list usable: a development machine accumulates one session file
/// per probe run on a throwaway port -- 140 of them when this was written -- and every one holds
/// `signals = ["", "", "", ""]`. A listed source is one somebody actually configured.
std::vector<KnownSource> known_sources();
/// Deletes one source's session file; the chooser's `x`. False means the file was already gone or
/// could not be removed, which costs the caller nothing -- there is no state to roll back.
bool forget_source(const KnownSource& src);
/// One `MathNode` as `kind|source|in_a|in_b|gain|offset|fc|q|lo|hi|x|y`, and back. The kind is the /// One `MathNode` as `kind|source|in_a|in_b|gain|offset|fc|q|lo|hi|x|y`, and back. The kind is the
/// op's *index* into `math_op_at()`, which is why the enum's order is a stored format and not just /// op's *index* into `math_op_at()`, which is why the enum's order is a stored format and not just
/// an implementation detail: reordering it renames every saved graph's operations. /// an implementation detail: reordering it renames every saved graph's operations.