301 lines
12 KiB
C++
301 lines
12 KiB
C++
|
|
// SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
module Crafter.Debug:Registry_impl;
|
||
|
|
import std;
|
||
|
|
import Crafter.Debug.Core;
|
||
|
|
import :Registry;
|
||
|
|
|
||
|
|
using namespace Crafter::Debug;
|
||
|
|
using namespace Crafter::Debug::Detail;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
std::atomic<std::uint64_t> frame{ 0 };
|
||
|
|
|
||
|
|
// Points kept per sample path, enough for `trace` over a few seconds of
|
||
|
|
// frames without an app logging every frame growing without bound.
|
||
|
|
constexpr std::size_t sampleHistory = 512;
|
||
|
|
|
||
|
|
bool ValidName(std::string_view name) {
|
||
|
|
return !name.empty() && std::ranges::none_of(name, [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; });
|
||
|
|
}
|
||
|
|
|
||
|
|
// The registered name that `path` starts with, longest first, ending at a
|
||
|
|
// step boundary: "bodies" for "bodies[3].pos", not "bod".
|
||
|
|
template<class Map>
|
||
|
|
auto LongestPrefix(const Map& map, std::string_view path) -> decltype(map.end()) {
|
||
|
|
auto best = map.end();
|
||
|
|
std::size_t bestLength = 0;
|
||
|
|
for (auto it = map.begin(); it != map.end(); ++it) {
|
||
|
|
const std::string_view name = it->first;
|
||
|
|
if (!path.starts_with(name)) continue;
|
||
|
|
if (path.size() > name.size() && path[name.size()] != '.' && path[name.size()] != '[') continue;
|
||
|
|
if (name.size() >= bestLength) {
|
||
|
|
best = it;
|
||
|
|
bestLength = name.size();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return best;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::string_view> MemberNames(const Value& object) {
|
||
|
|
std::vector<std::string_view> names;
|
||
|
|
for (const auto& [key, member] : object.Members()) names.push_back(key);
|
||
|
|
return names;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string JoinNames(std::span<const std::string_view> names) {
|
||
|
|
std::string joined;
|
||
|
|
for (const std::string_view name : names) {
|
||
|
|
if (!joined.empty()) joined += ", ";
|
||
|
|
joined += name;
|
||
|
|
}
|
||
|
|
return joined;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Follows `rest` (".name" and "[index]" steps) from `root`. `where` names
|
||
|
|
// the root in errors. Returns nullptr and sets `error` when a step fails.
|
||
|
|
template<class V>
|
||
|
|
V* Walk(V& root, std::string_view rest, std::string where, std::string& error) {
|
||
|
|
V* current = &root;
|
||
|
|
while (!rest.empty()) {
|
||
|
|
if (rest.front() == '.') {
|
||
|
|
rest.remove_prefix(1);
|
||
|
|
const std::size_t end = std::min(rest.find('.'), rest.find('['));
|
||
|
|
const std::string_view name = rest.substr(0, end);
|
||
|
|
rest.remove_prefix(name.size());
|
||
|
|
if (!current->IsObject()) {
|
||
|
|
error = std::format("'{}' is {}, so it has no member '{}'", where, KindName(current->Kind()), name);
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
V* next = current->Find(name);
|
||
|
|
if (next == nullptr) {
|
||
|
|
const std::vector<std::string_view> names = MemberNames(*current);
|
||
|
|
const auto suggestion = Suggest(name, names);
|
||
|
|
error = suggestion ? std::format("'{}' has no member '{}'; did you mean '{}'?", where, name, *suggestion)
|
||
|
|
: std::format("'{}' has no member '{}' (members: {})", where, name, JoinNames(names));
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
where += '.';
|
||
|
|
where += name;
|
||
|
|
current = next;
|
||
|
|
} else if (rest.front() == '[') {
|
||
|
|
const std::size_t close = rest.find(']');
|
||
|
|
std::size_t index = 0;
|
||
|
|
const std::string_view digits = rest.substr(1, close == std::string_view::npos ? std::string_view::npos : close - 1);
|
||
|
|
const auto [end, failure] = std::from_chars(digits.data(), digits.data() + digits.size(), index);
|
||
|
|
if (close == std::string_view::npos || failure != std::errc() || end != digits.data() + digits.size()) {
|
||
|
|
error = std::format("'{}' is followed by a malformed index: '{}'", where, rest);
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
rest.remove_prefix(close + 1);
|
||
|
|
if (!current->IsArray()) {
|
||
|
|
error = std::format("'{}' is {}, not an array", where, KindName(current->Kind()));
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
if (index >= current->Size()) {
|
||
|
|
error = std::format("'{}' has {} items, so [{}] is out of range", where, current->Size(), index);
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
where += std::format("[{}]", index);
|
||
|
|
current = ¤t->Items()[index];
|
||
|
|
} else {
|
||
|
|
error = std::format("cannot read '{}' after '{}'", rest, where);
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return current;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
std::uint64_t Crafter::Debug::Detail::CurrentFrame() {
|
||
|
|
return frame.load(std::memory_order_relaxed);
|
||
|
|
}
|
||
|
|
|
||
|
|
void Crafter::Debug::Detail::SetFrame(std::uint64_t value) {
|
||
|
|
frame.store(value, std::memory_order_relaxed);
|
||
|
|
}
|
||
|
|
|
||
|
|
bool Crafter::Debug::Detail::GlobMatch(std::string_view glob, std::string_view text) {
|
||
|
|
std::size_t g = 0;
|
||
|
|
std::size_t t = 0;
|
||
|
|
std::size_t star = std::string_view::npos;
|
||
|
|
std::size_t mark = 0;
|
||
|
|
while (t < text.size()) {
|
||
|
|
if (g < glob.size() && (glob[g] == '?' || glob[g] == text[t])) {
|
||
|
|
++g;
|
||
|
|
++t;
|
||
|
|
} else if (g < glob.size() && glob[g] == '*') {
|
||
|
|
star = g++;
|
||
|
|
mark = t;
|
||
|
|
} else if (star != std::string_view::npos) {
|
||
|
|
g = star + 1;
|
||
|
|
t = ++mark;
|
||
|
|
} else {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
while (g < glob.size() && glob[g] == '*') ++g;
|
||
|
|
return g == glob.size();
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Crafter::Debug::Detail::Joined(std::string_view prefix, std::string_view name) {
|
||
|
|
if (prefix.empty()) return std::string(name);
|
||
|
|
if (name.empty()) return std::string(prefix);
|
||
|
|
return std::format("{}.{}", prefix, name);
|
||
|
|
}
|
||
|
|
|
||
|
|
Registry& Registry::Instance() {
|
||
|
|
static Registry registry;
|
||
|
|
return registry;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::uint64_t Registry::NewOwner() {
|
||
|
|
return nextOwner.fetch_add(1, std::memory_order_relaxed);
|
||
|
|
}
|
||
|
|
|
||
|
|
void Registry::AddCommand(CommandEntry entry) {
|
||
|
|
if (!ValidName(entry.name)) throw std::invalid_argument(std::format("Crafter.Debug: '{}' is not a command name: it must be non-empty, without whitespace", entry.name));
|
||
|
|
std::string name = entry.name;
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
commands.insert_or_assign(std::move(name), std::make_shared<const CommandEntry>(std::move(entry)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void Registry::AddWatch(WatchEntry entry) {
|
||
|
|
if (!ValidName(entry.path)) throw std::invalid_argument(std::format("Crafter.Debug: '{}' is not a watch path: it must be non-empty, without whitespace", entry.path));
|
||
|
|
std::string path = entry.path;
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
watches.insert_or_assign(std::move(path), std::make_shared<const WatchEntry>(std::move(entry)));
|
||
|
|
}
|
||
|
|
|
||
|
|
void Registry::AddSample(std::string_view path, Value value, std::uint64_t sampleFrame, std::uint64_t owner) {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
auto found = samples.find(path);
|
||
|
|
if (found == samples.end()) found = samples.emplace(std::string(path), Series{}).first;
|
||
|
|
Series& series = found->second;
|
||
|
|
series.owner = owner;
|
||
|
|
series.points.push_back({ sampleFrame, std::move(value) });
|
||
|
|
if (series.points.size() > sampleHistory) series.points.pop_front();
|
||
|
|
}
|
||
|
|
|
||
|
|
void Registry::RemoveOwner(std::uint64_t owner) {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
std::erase_if(commands, [&](const auto& item) { return item.second->owner == owner; });
|
||
|
|
std::erase_if(watches, [&](const auto& item) { return item.second->owner == owner; });
|
||
|
|
std::erase_if(samples, [&](const auto& item) { return item.second.owner == owner; });
|
||
|
|
}
|
||
|
|
|
||
|
|
std::shared_ptr<const CommandEntry> Registry::FindCommand(std::string_view name) const {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
const auto found = commands.find(name);
|
||
|
|
return found == commands.end() ? nullptr : found->second;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::shared_ptr<const CommandEntry>> Registry::Commands(std::string_view glob) const {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
std::vector<std::shared_ptr<const CommandEntry>> matched;
|
||
|
|
for (const auto& [name, entry] : commands) {
|
||
|
|
if (GlobMatch(glob, name)) matched.push_back(entry);
|
||
|
|
}
|
||
|
|
return matched;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::shared_ptr<const WatchEntry>> Registry::Watches(std::string_view glob) const {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
std::vector<std::shared_ptr<const WatchEntry>> matched;
|
||
|
|
for (const auto& [path, entry] : watches) {
|
||
|
|
if (GlobMatch(glob, path)) matched.push_back(entry);
|
||
|
|
}
|
||
|
|
return matched;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<std::string> Registry::SamplePaths(std::string_view glob) const {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
std::vector<std::string> matched;
|
||
|
|
for (const auto& [path, series] : samples) {
|
||
|
|
if (GlobMatch(glob, path)) matched.push_back(path);
|
||
|
|
}
|
||
|
|
return matched;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<SamplePoint> Registry::SampleHistory(std::string_view path) const {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
const auto found = samples.find(path);
|
||
|
|
if (found == samples.end()) return {};
|
||
|
|
return std::vector<SamplePoint>(found->second.points.begin(), found->second.points.end());
|
||
|
|
}
|
||
|
|
|
||
|
|
Resolved Registry::Resolve(std::string_view path) const {
|
||
|
|
std::shared_ptr<const WatchEntry> watch;
|
||
|
|
std::optional<SamplePoint> latest;
|
||
|
|
std::string base;
|
||
|
|
{
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
const auto foundWatch = LongestPrefix(watches, path);
|
||
|
|
const auto foundSample = LongestPrefix(samples, path);
|
||
|
|
const std::size_t watchLength = foundWatch == watches.end() ? 0 : foundWatch->first.size();
|
||
|
|
const std::size_t sampleLength = foundSample == samples.end() ? 0 : foundSample->first.size();
|
||
|
|
if (foundWatch != watches.end() && watchLength >= sampleLength) {
|
||
|
|
watch = foundWatch->second;
|
||
|
|
base = foundWatch->first;
|
||
|
|
} else if (foundSample != samples.end() && !foundSample->second.points.empty()) {
|
||
|
|
latest = foundSample->second.points.back();
|
||
|
|
base = foundSample->first;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!watch && !latest) {
|
||
|
|
const auto suggestion = SuggestPath(path);
|
||
|
|
return { Value(), suggestion ? std::format("no watch or sample '{}'; did you mean '{}'?", path, *suggestion)
|
||
|
|
: std::format("no watch or sample '{}'; list them with: crafter-debug get '*'", path) };
|
||
|
|
}
|
||
|
|
Value root = watch ? watch->get() : std::move(latest->value);
|
||
|
|
std::string error;
|
||
|
|
const Value* reached = Walk(std::as_const(root), path.substr(base.size()), base, error);
|
||
|
|
if (reached == nullptr) return { Value(), error };
|
||
|
|
return { *reached, {} };
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<std::string> Registry::Set(std::string_view path, const Value& value) const {
|
||
|
|
std::shared_ptr<const WatchEntry> watch;
|
||
|
|
{
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
const auto found = LongestPrefix(watches, path);
|
||
|
|
if (found != watches.end()) watch = found->second;
|
||
|
|
}
|
||
|
|
if (!watch) {
|
||
|
|
const auto suggestion = SuggestPath(path);
|
||
|
|
return suggestion ? std::format("no watch '{}'; did you mean '{}'?", path, *suggestion) : std::format("no watch '{}'", path);
|
||
|
|
}
|
||
|
|
if (!watch->set) return std::format("'{}' is read-only", watch->path);
|
||
|
|
const std::string_view rest = path.substr(watch->path.size());
|
||
|
|
if (rest.empty()) return watch->set(value);
|
||
|
|
Value whole = watch->get();
|
||
|
|
std::string error;
|
||
|
|
Value* part = Walk(whole, rest, watch->path, error);
|
||
|
|
if (part == nullptr) return error;
|
||
|
|
*part = value;
|
||
|
|
return watch->set(whole);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<std::string> Registry::SuggestCommand(std::string_view name) const {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
std::vector<std::string_view> names;
|
||
|
|
for (const auto& [command, entry] : commands) names.push_back(command);
|
||
|
|
if (const auto suggestion = Suggest(name, names)) return std::string(*suggestion);
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<std::string> Registry::SuggestPath(std::string_view path) const {
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
std::vector<std::string_view> names;
|
||
|
|
for (const auto& [watch, entry] : watches) names.push_back(watch);
|
||
|
|
for (const auto& [sample, series] : samples) names.push_back(sample);
|
||
|
|
if (const auto suggestion = Suggest(path, names)) return std::string(*suggestion);
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
|
||
|
|
Group::Group(std::string prefix) : prefix(std::move(prefix)), owner(Registry::Instance().NewOwner()) {}
|
||
|
|
|
||
|
|
Group::~Group() {
|
||
|
|
Registry::Instance().RemoveOwner(owner);
|
||
|
|
}
|