390 lines
10 KiB
C++
390 lines
10 KiB
C++
#include <atomic>
|
|
#include <cinttypes>
|
|
#include <deque>
|
|
#include <exception>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <typeinfo>
|
|
#include <utility>
|
|
#include <iostream>
|
|
|
|
#include <imgui.h>
|
|
|
|
#include <yaml-cpp/yaml.h>
|
|
|
|
#include "nf7.hh"
|
|
|
|
#include "common/dir_item.hh"
|
|
#include "common/file_base.hh"
|
|
#include "common/generic_config.hh"
|
|
#include "common/generic_context.hh"
|
|
#include "common/generic_memento.hh"
|
|
#include "common/generic_type_info.hh"
|
|
#include "common/gui.hh"
|
|
#include "common/gui_window.hh"
|
|
#include "common/life.hh"
|
|
#include "common/logger.hh"
|
|
#include "common/logger_ref.hh"
|
|
#include "common/ptr_selector.hh"
|
|
#include "common/yas_std_atomic.hh"
|
|
|
|
|
|
using namespace std::literals;
|
|
|
|
namespace nf7 {
|
|
namespace {
|
|
|
|
class Logger final : public nf7::FileBase,
|
|
public nf7::GenericConfig, public nf7::DirItem {
|
|
public:
|
|
static inline const nf7::GenericTypeInfo<Logger> kType = {
|
|
"System/Logger", {"nf7::DirItem"}, "records log output from other files",
|
|
};
|
|
|
|
struct Row final {
|
|
public:
|
|
File::Id file;
|
|
std::source_location srcloc;
|
|
|
|
const char* level;
|
|
std::string msg;
|
|
std::string path;
|
|
std::string location;
|
|
std::exception_ptr ex;
|
|
|
|
std::string Stringify() const noexcept {
|
|
std::stringstream st;
|
|
st << level << "\n";
|
|
st << " " << msg << "\n";
|
|
st << " from " << path << "\n";
|
|
st << " at " << location;
|
|
return st.str();
|
|
}
|
|
};
|
|
|
|
struct Data {
|
|
uint32_t max_rows = 1024;
|
|
bool propagate = false;
|
|
bool freeze = false;
|
|
|
|
Data() noexcept { }
|
|
void serialize(auto& ar) {
|
|
ar(max_rows, propagate, freeze);
|
|
if (max_rows == 0) {
|
|
throw DeserializeException("max_rows must be 1 or more");
|
|
}
|
|
}
|
|
|
|
std::string Stringify() const noexcept {
|
|
YAML::Emitter st;
|
|
st << YAML::BeginMap;
|
|
st << YAML::Key << "max_rows";
|
|
st << YAML::Value << max_rows;
|
|
st << YAML::Key << "propagate";
|
|
st << YAML::Value << propagate;
|
|
st << YAML::Key << "freeze";
|
|
st << YAML::Value << freeze;
|
|
st << YAML::EndMap;
|
|
return {st.c_str(), st.size()};
|
|
}
|
|
void Parse(const std::string& str)
|
|
try {
|
|
const auto yaml = YAML::Load(str);
|
|
|
|
Data d;
|
|
d.max_rows = yaml["max_rows"].as<uint32_t>();
|
|
d.propagate = yaml["propagate"].as<bool>();
|
|
d.freeze = yaml["freeze"].as<bool>();
|
|
|
|
*this = std::move(d);
|
|
} catch (YAML::Exception& e) {
|
|
throw nf7::Exception {e.what()};
|
|
}
|
|
};
|
|
|
|
Logger(nf7::Env& env, Data&& d = {}) noexcept :
|
|
nf7::FileBase(kType, env),
|
|
nf7::GenericConfig(mem_),
|
|
nf7::DirItem(DirItem::kMenu),
|
|
mem_(*this, std::move(d)),
|
|
win_(*this, "Log View") {
|
|
mem_.onCommit = mem_.onRestore = [this]() {
|
|
store_->param(mem_.data());
|
|
};
|
|
|
|
win_.onConfig = []() {
|
|
const auto em = ImGui::GetFontSize();
|
|
ImGui::SetNextWindowSize({48*em, 16*em}, ImGuiCond_FirstUseEver);
|
|
};
|
|
win_.onUpdate = [this]() { LogView(); };
|
|
}
|
|
|
|
Logger(nf7::Deserializer& ar) : Logger(ar.env()) {
|
|
ar(win_, mem_.data());
|
|
}
|
|
void Serialize(nf7::Serializer& ar) const noexcept override {
|
|
ar(win_, mem_.data());
|
|
}
|
|
std::unique_ptr<nf7::File> Clone(nf7::Env& env) const noexcept override {
|
|
return std::make_unique<Logger>(env, Data {mem_.data()});
|
|
}
|
|
|
|
void PostHandle(const nf7::File::Event& ev) noexcept override {
|
|
switch (ev.type) {
|
|
case Event::kAdd:
|
|
store_ = std::make_shared<ItemStore>(*this);
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
void UpdateMenu() noexcept override;
|
|
void UpdateRowMenu(const Row&) noexcept;
|
|
|
|
nf7::File::Interface* interface(const std::type_info& t) noexcept override {
|
|
return nf7::InterfaceSelector<nf7::Config, nf7::DirItem, nf7::Logger>(t).
|
|
Select(this, store_.get());
|
|
}
|
|
|
|
private:
|
|
class ItemStore;
|
|
std::shared_ptr<ItemStore> store_;
|
|
std::deque<Row> rows_;
|
|
|
|
nf7::GenericMemento<Data> mem_;
|
|
|
|
nf7::gui::Window win_;
|
|
|
|
|
|
// log record management
|
|
void DropExceededRows() noexcept {
|
|
if (rows_.size() <= mem_->max_rows) return;
|
|
rows_.erase(rows_.begin(), rows_.end()-mem_->max_rows);
|
|
}
|
|
|
|
// stringify
|
|
std::string GetPathString(File::Id id) const noexcept
|
|
try {
|
|
return env().GetFileOrThrow(id).abspath().Stringify();
|
|
} catch (ExpiredException&) {
|
|
return "[EXPIRED]";
|
|
}
|
|
static const char* GetLevelString(nf7::Logger::Level lv) noexcept {
|
|
switch (lv) {
|
|
case nf7::Logger::kTrace:
|
|
return "TRAC";
|
|
case nf7::Logger::kInfo:
|
|
return "INFO";
|
|
case nf7::Logger::kWarn:
|
|
return "WARN";
|
|
case nf7::Logger::kError:
|
|
return "ERRR";
|
|
default:
|
|
assert(false);
|
|
return "X(";
|
|
}
|
|
}
|
|
static std::string GetLocationString(const std::source_location loc) noexcept {
|
|
return loc.file_name()+":"s+std::to_string(loc.line());
|
|
}
|
|
|
|
// gui
|
|
void LogView() noexcept;
|
|
|
|
|
|
class ItemStore final : public nf7::Context,
|
|
public nf7::Logger,
|
|
public std::enable_shared_from_this<ItemStore> {
|
|
public:
|
|
ItemStore() = delete;
|
|
ItemStore(File& f) noexcept : nf7::Context(f) {
|
|
}
|
|
ItemStore(const ItemStore&) = delete;
|
|
ItemStore(ItemStore&&) = delete;
|
|
ItemStore& operator=(const ItemStore&) = delete;
|
|
ItemStore& operator=(ItemStore&&) = delete;
|
|
|
|
void Write(nf7::Logger::Item&& item) noexcept override {
|
|
if (param_.freeze) return;
|
|
if (param_.propagate) {
|
|
// TODO propagation
|
|
}
|
|
|
|
std::unique_lock<std::mutex> k(mtx_);
|
|
if (items_.size() >= param_.max_rows) items_.pop_front();
|
|
items_.push_back(std::move(item));
|
|
}
|
|
|
|
bool MoveItemsTo(auto& owner) noexcept {
|
|
std::unique_lock<std::mutex> k(mtx_);
|
|
if (items_.empty()) return false;
|
|
auto& rows = owner.rows_;
|
|
|
|
auto itr = items_.begin();
|
|
if (rows.size()+items_.size() > param_.max_rows) {
|
|
if (items_.size() > param_.max_rows) {
|
|
itr += static_cast<intmax_t>(param_.max_rows - items_.size());
|
|
}
|
|
const auto keep =
|
|
static_cast<intmax_t>(param_.max_rows) - std::distance(itr, items_.end());
|
|
rows.erase(rows.begin(), rows.end()-keep);
|
|
}
|
|
for (; itr < items_.end(); ++itr) {
|
|
Row row = {
|
|
.file = itr->file,
|
|
.srcloc = itr->srcloc,
|
|
.level = GetLevelString(itr->level),
|
|
.msg = std::move(itr->msg),
|
|
.path = owner.GetPathString(itr->file),
|
|
.location = GetLocationString(itr->srcloc),
|
|
.ex = itr->ex,
|
|
};
|
|
rows.push_back(std::move(row));
|
|
}
|
|
items_.clear();
|
|
return true;
|
|
}
|
|
|
|
std::string GetDescription() const noexcept override {
|
|
return "System/Logger shared instance";
|
|
}
|
|
std::shared_ptr<nf7::Logger> self(nf7::Logger*) noexcept override {
|
|
return shared_from_this();
|
|
}
|
|
|
|
void param(const Data& d) noexcept {
|
|
std::unique_lock<std::mutex> k(mtx_);
|
|
param_ = d;
|
|
}
|
|
|
|
private:
|
|
std::mutex mtx_;
|
|
std::deque<nf7::Logger::Item> items_;
|
|
|
|
Data param_;
|
|
};
|
|
};
|
|
|
|
|
|
void Logger::UpdateMenu() noexcept {
|
|
win_.MenuItem();
|
|
}
|
|
void Logger::UpdateRowMenu(const Row& row) noexcept {
|
|
if (ImGui::MenuItem("copy as text")) {
|
|
ImGui::SetClipboardText(row.Stringify().c_str());
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("clear")) {
|
|
env().ExecMain(
|
|
std::make_shared<nf7::GenericContext>(*this), [this]() { rows_.clear(); });
|
|
}
|
|
}
|
|
|
|
void Logger::LogView() noexcept {
|
|
constexpr auto kTableFlags =
|
|
ImGuiTableFlags_Resizable |
|
|
ImGuiTableFlags_Hideable |
|
|
ImGuiTableFlags_RowBg |
|
|
ImGuiTableFlags_Borders |
|
|
ImGuiTableFlags_ContextMenuInBody |
|
|
ImGuiTableFlags_SizingStretchProp |
|
|
ImGuiTableFlags_ScrollY;
|
|
if (ImGui::BeginTable("logs", 4, kTableFlags, ImGui::GetContentRegionAvail(), 0)) {
|
|
const bool updated = store_->MoveItemsTo(*this);
|
|
const bool autoscroll = updated && ImGui::GetScrollY() == ImGui::GetScrollMaxY();
|
|
|
|
ImGui::TableSetupColumn("level");
|
|
ImGui::TableSetupColumn("msg");
|
|
ImGui::TableSetupColumn("path");
|
|
ImGui::TableSetupColumn("location");
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
ImGui::TableHeadersRow();
|
|
|
|
for (const auto& row : rows_) {
|
|
ImGui::TableNextRow();
|
|
ImGui::PushID(&row);
|
|
|
|
if (autoscroll && &row == &rows_.back()) {
|
|
ImGui::SetScrollHereY();
|
|
}
|
|
|
|
// level column
|
|
if (ImGui::TableSetColumnIndex(0)) {
|
|
constexpr auto kFlags =
|
|
ImGuiSelectableFlags_SpanAllColumns |
|
|
ImGuiSelectableFlags_AllowItemOverlap;
|
|
ImGui::Selectable(row.level, false, kFlags);
|
|
if (ImGui::BeginPopupContextItem()) {
|
|
UpdateRowMenu(row);
|
|
ImGui::EndPopup();
|
|
}
|
|
}
|
|
// msg column
|
|
if (ImGui::TableNextColumn()) {
|
|
auto len = row.msg.find('\n');
|
|
if (len == std::string::npos) {
|
|
len = row.msg.size();
|
|
}
|
|
const char* str = row.msg.c_str();
|
|
ImGui::TextUnformatted(str, str+len);
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::TextUnformatted(row.msg.c_str());
|
|
if (row.ex) {
|
|
ImGui::Spacing();
|
|
ImGui::TextUnformatted("exception stack:");
|
|
for (auto ptr = row.ex; ptr;)
|
|
try {
|
|
ImGui::Bullet();
|
|
std::rethrow_exception(ptr);
|
|
} catch (Exception& e) {
|
|
ImGui::TextUnformatted(e.msg().c_str());
|
|
ptr = e.reason();
|
|
} catch (std::exception& e) {
|
|
ImGui::TextUnformatted(e.what());
|
|
ptr = nullptr;
|
|
}
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
// path column
|
|
if (ImGui::TableNextColumn()) {
|
|
ImGui::TextUnformatted(row.path.c_str());
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip(row.path.c_str());
|
|
}
|
|
}
|
|
// location column
|
|
if (ImGui::TableNextColumn()) {
|
|
ImGui::TextUnformatted(row.location.c_str());
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
ImGui::Text(row.location.c_str());
|
|
for (auto ptr = row.ex; ptr;)
|
|
try {
|
|
ImGui::Bullet();
|
|
std::rethrow_exception(ptr);
|
|
} catch (Exception& e) {
|
|
e.UpdatePanic();
|
|
ImGui::Spacing();
|
|
ptr = e.reason();
|
|
} catch (std::exception& e) {
|
|
ImGui::Text("std::exception (%s)", e.what());
|
|
ptr = nullptr;
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::EndTable();
|
|
}
|
|
}
|
|
|
|
}
|
|
} // namespace nf7
|