working on todo.md features
This commit is contained in:
1 parent
210d124814
commit
8f54841c2c
110 files changed
+15932
-6576
No files matched your search
+226
-41
@@ -114,56 +114,223 @@ std::string config_dir() {
|
||||
return std::string(home) + "/.config/udpscope";
|
||||
}
|
||||
|
||||
std::string cache_dir() {
|
||||
const char* xdg = std::getenv("XDG_CACHE_HOME");
|
||||
if (xdg != nullptr && xdg[0] != '\0') return std::string(xdg) + "/udpscope";
|
||||
const char* home = std::getenv("HOME");
|
||||
if (home == nullptr || home[0] == '\0') return "/tmp/udpscope-cache";
|
||||
return std::string(home) + "/.cache/udpscope";
|
||||
}
|
||||
|
||||
std::string layout_path(const std::string& host, std::uint16_t port) {
|
||||
const std::string dir = config_dir();
|
||||
mkdirs(dir);
|
||||
return dir + "/" + sanitize(host) + "_" + std::to_string(port) + ".toml";
|
||||
}
|
||||
|
||||
std::vector<KnownSource> known_sources() {
|
||||
std::vector<KnownSource> out;
|
||||
const std::string dir = config_dir();
|
||||
std::string config_dir_for(const std::string& host, std::uint16_t port) {
|
||||
return config_dir() + "/" + sanitize(host) + "_" + std::to_string(port);
|
||||
}
|
||||
|
||||
std::string configuration_path(const std::string& host, std::uint16_t port,
|
||||
const std::string& name) {
|
||||
const std::string dir = config_dir_for(host, port);
|
||||
mkdirs(dir);
|
||||
return dir + "/" + sanitize(name) + ".toml";
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool file_exists(const std::string& path) {
|
||||
struct stat sb {};
|
||||
return ::stat(path.c_str(), &sb) == 0;
|
||||
}
|
||||
|
||||
/// A session written before configurations existed is the configuration named `default`: copied
|
||||
/// into the per-source directory, remembered as active, and the old file renamed out of the way
|
||||
/// rather than deleted, so the import can be undone by hand. Runs before every read and write of
|
||||
/// the per-source directory, so a machine that upgrades keeps its session.
|
||||
void import_legacy(const std::string& host, std::uint16_t port) {
|
||||
const std::string dir = config_dir_for(host, port);
|
||||
if (file_exists(dir)) return;
|
||||
const std::string legacy = layout_path(host, port);
|
||||
if (!file_exists(legacy)) return;
|
||||
mkdirs(dir);
|
||||
FILE* in = std::fopen(legacy.c_str(), "rb");
|
||||
if (in == nullptr) return;
|
||||
const std::string target = dir + "/default.toml";
|
||||
FILE* out = std::fopen(target.c_str(), "wb");
|
||||
if (out == nullptr) {
|
||||
std::fclose(in);
|
||||
return;
|
||||
}
|
||||
char buf[4096];
|
||||
std::size_t n = 0;
|
||||
while ((n = std::fread(buf, 1, sizeof(buf), in)) > 0) std::fwrite(buf, 1, n, out);
|
||||
std::fclose(in);
|
||||
std::fclose(out);
|
||||
FILE* active = std::fopen((dir + "/active").c_str(), "wb");
|
||||
if (active != nullptr) {
|
||||
std::fputs("default\n", active);
|
||||
std::fclose(active);
|
||||
}
|
||||
::rename(legacy.c_str(), (legacy + ".imported").c_str());
|
||||
}
|
||||
|
||||
/// `a.toml` -> `a`; anything else -> "".
|
||||
std::string toml_stem(const std::string& file) {
|
||||
if (file.size() <= 5 || file.compare(file.size() - 5, 5, ".toml") != 0) return {};
|
||||
return file.substr(0, file.size() - 5);
|
||||
}
|
||||
|
||||
/// `host_port` -> {host, port}, or false. The *last* '_' is the split, because `sanitize()`
|
||||
/// replaces anything unsafe in a host with one.
|
||||
bool split_stem(const std::string& stem, std::string* host, std::uint16_t* port) {
|
||||
const std::size_t cut = stem.rfind('_');
|
||||
if (cut == std::string::npos || cut + 1 == stem.size()) return false;
|
||||
char* end = nullptr;
|
||||
const long p = std::strtol(stem.c_str() + cut + 1, &end, 10);
|
||||
if (end == nullptr || *end != '\0' || p <= 0 || p > 65535) return false;
|
||||
*host = stem.substr(0, cut);
|
||||
*port = static_cast<std::uint16_t>(p);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The configurations a source directory holds, newest first, by file name.
|
||||
std::vector<std::string> names_in_dir(const std::string& dir) {
|
||||
std::vector<std::pair<std::int64_t, std::string>> found;
|
||||
DIR* d = ::opendir(dir.c_str());
|
||||
if (d == nullptr) return {};
|
||||
while (const dirent* e = ::readdir(d)) {
|
||||
const std::string stem = toml_stem(e->d_name);
|
||||
if (stem.empty()) continue;
|
||||
struct stat sb {};
|
||||
const std::string path = dir + "/" + e->d_name;
|
||||
std::int64_t mtime = 0;
|
||||
if (::stat(path.c_str(), &sb) == 0) mtime = static_cast<std::int64_t>(sb.st_mtime);
|
||||
found.emplace_back(mtime, stem);
|
||||
}
|
||||
::closedir(d);
|
||||
std::stable_sort(found.begin(), found.end(),
|
||||
[](const auto& a, const auto& b) { return a.first > b.first; });
|
||||
std::vector<std::string> out;
|
||||
out.reserve(found.size());
|
||||
for (auto& f : found) out.push_back(std::move(f.second));
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::string> configuration_names(const std::string& host, std::uint16_t port) {
|
||||
import_legacy(host, port);
|
||||
return names_in_dir(config_dir_for(host, port));
|
||||
}
|
||||
|
||||
std::string active_configuration(const std::string& host, std::uint16_t port) {
|
||||
const std::vector<std::string> names = configuration_names(host, port);
|
||||
if (names.empty()) return "default";
|
||||
FILE* f = std::fopen((config_dir_for(host, port) + "/active").c_str(), "rb");
|
||||
if (f != nullptr) {
|
||||
char buf[512];
|
||||
const std::size_t n = std::fread(buf, 1, sizeof(buf) - 1, f);
|
||||
std::fclose(f);
|
||||
buf[n] = '\0';
|
||||
const std::string want = trim(std::string(buf));
|
||||
if (std::find(names.begin(), names.end(), want) != names.end()) return want;
|
||||
}
|
||||
// Newest, which is the one just saved when nothing wrote an `active`.
|
||||
return names.front();
|
||||
}
|
||||
|
||||
bool set_active_configuration(const std::string& host, std::uint16_t port,
|
||||
const std::string& name) {
|
||||
const std::string dir = config_dir_for(host, port);
|
||||
mkdirs(dir);
|
||||
FILE* f = std::fopen((dir + "/active").c_str(), "wb");
|
||||
if (f == nullptr) return false;
|
||||
std::fputs(sanitize(name).c_str(), f);
|
||||
std::fputs("\n", f);
|
||||
std::fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool forget_configuration(const std::string& host, std::uint16_t port, const std::string& name) {
|
||||
const std::string path = configuration_path(host, port, name);
|
||||
const bool gone = std::remove(path.c_str()) == 0;
|
||||
const std::string active = active_configuration(host, port);
|
||||
if (gone && sanitize(name) == sanitize(active)) {
|
||||
std::remove((config_dir_for(host, port) + "/active").c_str());
|
||||
}
|
||||
return gone;
|
||||
}
|
||||
|
||||
std::vector<KnownSource> known_sources() {
|
||||
const std::string root = config_dir();
|
||||
|
||||
// A legacy single-file session is imported first, so the listing below has one shape: the
|
||||
// per-source directory and its configuration files.
|
||||
if (DIR* d = ::opendir(root.c_str())) {
|
||||
while (const dirent* e = ::readdir(d)) {
|
||||
const std::string stem = toml_stem(e->d_name);
|
||||
if (stem.empty()) continue;
|
||||
std::string host;
|
||||
std::uint16_t port = 0;
|
||||
if (!split_stem(stem, &host, &port)) continue;
|
||||
// The scheme `layout_path()` writes, and nothing else: a stray `.toml` here is not ours.
|
||||
if (sanitize(host) + "_" + std::to_string(port) != stem) continue;
|
||||
import_legacy(host, port);
|
||||
}
|
||||
::closedir(d);
|
||||
}
|
||||
|
||||
std::vector<KnownSource> out;
|
||||
DIR* d = ::opendir(root.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;
|
||||
char* end = nullptr;
|
||||
const long port = std::strtol(stem.c_str() + cut + 1, &end, 10);
|
||||
if (end == nullptr || *end != '\0' || port <= 0 || port > 65535) continue;
|
||||
const std::string host = stem.substr(0, cut);
|
||||
// The name must be one `layout_path()` could have written, or the `×` -- which rebuilds it
|
||||
// through `layout_path()` -- would unlink a different session. `sanitize()` is idempotent on
|
||||
// any host that already came back out of a filename, so this is also true for hosts whose names
|
||||
// contain `_` from the sanitizer.
|
||||
if (sanitize(host) + "_" + std::to_string(port) != stem) continue;
|
||||
|
||||
LayoutState st;
|
||||
const std::string path = dir + "/" + name;
|
||||
if (!load_layout(path, st)) continue;
|
||||
|
||||
KnownSource s;
|
||||
s.host = host;
|
||||
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
|
||||
|
||||
const std::string dir = root + "/" + e->d_name;
|
||||
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));
|
||||
if (::stat(dir.c_str(), &sb) != 0 || !S_ISDIR(sb.st_mode)) continue;
|
||||
std::string host;
|
||||
std::uint16_t port = 0;
|
||||
if (!split_stem(e->d_name, &host, &port)) continue;
|
||||
// The name must be one `config_dir_for()` could have written, or `forget_source()` would walk
|
||||
// out of the directory the row came from.
|
||||
if (sanitize(host) + "_" + std::to_string(port) != e->d_name) continue;
|
||||
|
||||
// One row per *configuration*: the same source may hold several named sessions, which is the
|
||||
// point of the feature, and the chooser is where they are told apart.
|
||||
for (const std::string& name : names_in_dir(dir)) {
|
||||
const std::string path = dir + "/" + name + ".toml";
|
||||
LayoutState st;
|
||||
if (!load_layout(path, st)) continue;
|
||||
KnownSource s;
|
||||
s.host = host;
|
||||
s.port = port;
|
||||
s.config = name;
|
||||
s.comment = st.comment;
|
||||
s.buffer_mb = st.buffer_mb;
|
||||
s.relative_time = st.relative_time;
|
||||
s.capture_packets = st.capture_packets;
|
||||
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;
|
||||
}
|
||||
}
|
||||
// A configuration nobody configured is not worth a row: a development machine accumulates one
|
||||
// session file per probe run and every one holds `signals = ["", "", "", ""]`. One that
|
||||
// carries a *setting* is worth a row even before it has ever streamed -- that is how a host is
|
||||
// set up from the chooser's form, and the settings are the whole point of editing one.
|
||||
const bool configured = s.signals > 0 || s.buffer_mb > 0.0 || !st.comment.empty() ||
|
||||
st.relative_time || st.capture_packets > 0;
|
||||
if (!configured) continue;
|
||||
struct stat fb {};
|
||||
s.mtime = (::stat(path.c_str(), &fb) == 0) ? static_cast<std::int64_t>(fb.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
|
||||
@@ -174,10 +341,10 @@ std::vector<KnownSource> known_sources() {
|
||||
}
|
||||
|
||||
bool forget_source(const KnownSource& src) {
|
||||
// Through `layout_path()` rather than by rebuilding the name here: one expression of the
|
||||
// Through `configuration_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;
|
||||
return forget_configuration(src.host, src.port, src.config);
|
||||
}
|
||||
|
||||
std::string math_node_text(const MathNode& node) {
|
||||
@@ -315,6 +482,20 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
st.multicast = strip_quote(value);
|
||||
} else if (key == "interface") {
|
||||
st.interface_addr = strip_quote(value);
|
||||
} else if (key == "comment") {
|
||||
st.comment = strip_quote(value);
|
||||
} else if (key == "buffer_mb") {
|
||||
try {
|
||||
st.buffer_mb = std::stod(value);
|
||||
} catch (...) {
|
||||
}
|
||||
} else if (key == "relative_time") {
|
||||
st.relative_time = value == "true";
|
||||
} else if (key == "capture_packets") {
|
||||
try {
|
||||
st.capture_packets = static_cast<std::uint32_t>(std::max(0, std::stoi(value)));
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
} else if (section == Section::Plot && !st.rows.empty()) {
|
||||
if (key == "signals") {
|
||||
@@ -434,6 +615,10 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
o << "]\n";
|
||||
}
|
||||
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
|
||||
if (!state.comment.empty()) o << "comment = " << toml_escape(state.comment) << "\n";
|
||||
if (state.buffer_mb > 0.0) o << "buffer_mb = " << state.buffer_mb << "\n";
|
||||
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
||||
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
|
||||
for (const auto& row : state.rows) {
|
||||
o << "\n[[plot]]\nsignals = [";
|
||||
for (std::size_t i = 0; i < row.signals.size(); ++i) {
|
||||
|
||||
Reference in new issue
Block a user