Files
nf7/file/system_logger.cc
2022-08-27 17:13:01 +09:00

446 lines
13 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 "nf7.hh"
#include "common/dir_item.hh"
#include "common/file_base.hh"
#include "common/generic_context.hh"
#include "common/generic_type_info.hh"
#include "common/gui_window.hh"
#include "common/life.hh"
#include "common/logger.hh"
#include "common/logger_ref.hh"
#include "common/node.hh"
#include "common/ptr_selector.hh"
#include "common/yas_std_atomic.hh"
using namespace std::literals;
namespace nf7 {
namespace {
class Logger final : public nf7::File,
public nf7::DirItem {
public:
static inline const nf7::GenericTypeInfo<Logger> kType = {
"System/Logger", {"nf7::DirItem"}};
static void UpdateTypeTooltip() noexcept {
ImGui::TextUnformatted("Records log output from other files.");
ImGui::Bullet(); ImGui::TextUnformatted("implements nf7::Logger");
ImGui::Bullet(); ImGui::TextUnformatted(
"logged are children and grandchildren of a dir that has this with name '_logger'");
ImGui::Bullet(); ImGui::TextUnformatted(
"recorded logs won't be permanentized");
}
class Node;
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 Param final {
public:
Param(uint32_t mr, bool p, bool f) : max_rows(mr), propagate(p), freeze(f) {
}
std::atomic<uint32_t> max_rows;
std::atomic<bool> propagate;
std::atomic<bool> freeze;
};
class ItemStore;
Logger(nf7::Env& env, uint32_t max_rows = 1024, bool propagate = false, bool freeze = false) noexcept :
File(kType, env), DirItem(DirItem::kMenu),
param_(std::make_shared<Param>(max_rows, propagate, freeze)),
win_(*this, "LogView") {
win_.shown() = true;
}
Logger(nf7::Deserializer& ar) : Logger(ar.env()) {
ar(win_, param_->max_rows, param_->propagate, param_->freeze);
if (param_->max_rows == 0) {
throw DeserializeException("max_rows must be 1 or more");
}
}
void Serialize(nf7::Serializer& ar) const noexcept override {
ar(win_, param_->max_rows, param_->propagate, param_->freeze);
}
std::unique_ptr<nf7::File> Clone(nf7::Env& env) const noexcept override {
return std::make_unique<Logger>(
env, param_->max_rows, param_->propagate, param_->freeze);
}
void Handle(const nf7::File::Event& ev) noexcept override;
void Update() noexcept override;
void UpdateMenu() noexcept override;
void UpdateRowMenu(const Row&) noexcept;
nf7::File::Interface* interface(const std::type_info& t) noexcept override {
return InterfaceSelector<nf7::DirItem, nf7::Logger>(t).
Select(this, store_.get());
}
private:
std::shared_ptr<Param> param_;
std::shared_ptr<ItemStore> store_;
std::deque<Row> rows_;
const char* popup_ = nullptr;
nf7::gui::Window win_;
void DropExceededRows() noexcept {
if (rows_.size() <= param_->max_rows) return;
rows_.erase(rows_.begin(), rows_.end()-param_->max_rows);
}
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());
}
};
class Logger::ItemStore final : public nf7::Context,
public nf7::Logger,
public std::enable_shared_from_this<ItemStore> {
public:
ItemStore() = delete;
ItemStore(File& owner, const std::shared_ptr<Param>& param) noexcept :
Context(owner.env(), owner.id()), param_(param) {
}
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) {
// max_rows may be changed
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();
}
private:
std::mutex mtx_;
std::deque<nf7::Logger::Item> items_;
std::shared_ptr<Param> param_;
};
class Logger::Node final : public nf7::FileBase, public nf7::Node {
public:
static inline const nf7::GenericTypeInfo<Logger::Node> kType = {
"System/Logger/Node", {"nf7::Node"}};
static void UpdateTypeTooltip() noexcept {
ImGui::TextUnformatted("Sends message to logger.");
ImGui::Bullet(); ImGui::TextUnformatted("implements nf7::Node");
}
Node(nf7::Env& env) noexcept :
nf7::FileBase(kType, env, {&logger_}),
nf7::Node(nf7::Node::kNone),
life_(*this), logger_(*this) {
}
Node(nf7::Deserializer& ar) : Node(ar.env()) {
}
void Serialize(nf7::Serializer&) const noexcept override {
}
std::unique_ptr<nf7::File> Clone(nf7::Env& env) const noexcept override {
return std::make_unique<Logger::Node>(env);
}
std::shared_ptr<nf7::Node::Lambda> CreateLambda(
const std::shared_ptr<nf7::Node::Lambda>& parent) noexcept override {
return std::make_shared<Logger::Node::Lambda>(*this, parent);
}
std::span<const std::string> GetInputs() const noexcept override {
static const std::vector<std::string> kInputs = {"msg"};
return kInputs;
}
std::span<const std::string> GetOutputs() const noexcept override {
return {};
}
nf7::File::Interface* interface(const std::type_info& t) noexcept override {
return InterfaceSelector<nf7::Node>(t).Select(this);
}
private:
nf7::Life<Logger::Node> life_;
nf7::LoggerRef logger_;
class Lambda final : public nf7::Node::Lambda {
public:
Lambda(Logger::Node& f, const std::shared_ptr<nf7::Node::Lambda>& parent) noexcept :
nf7::Node::Lambda(f, parent), f_(f.life_) {
}
void Handle(std::string_view, const nf7::Value& v,
const std::shared_ptr<nf7::Node::Lambda>&) noexcept override
try {
f_.EnforceAlive();
if (v.isString()) {
f_->logger_.Info(v.string());
} else {
f_->logger_.Info("["s+v.typeName()+"]");
}
} catch (nf7::Exception&) {
}
private:
nf7::Life<Logger::Node>::Ref f_;
};
};
void Logger::Handle(const Event& ev) noexcept {
switch (ev.type) {
case Event::kAdd:
store_ = std::make_shared<ItemStore>(*this, param_);
return;
case Event::kRemove:
store_ = nullptr;
return;
default:
return;
}
}
void Logger::Update() noexcept {
if (const auto name = std::exchange(popup_, nullptr)) {
ImGui::OpenPopup(name);
}
const auto em = ImGui::GetFontSize();
// config popup
if (ImGui::BeginPopup("ConfigPopup")) {
ImGui::TextUnformatted("System/Logger Config");
ImGui::Spacing();
static const uint32_t kMinRows = 1, kMaxRows = 1024*1024;
uint32_t max_rows = param_->max_rows;
if (ImGui::DragScalar("max rows", ImGuiDataType_U32, &max_rows, 1, &kMinRows, &kMaxRows)) {
param_->max_rows = max_rows;
DropExceededRows();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("the oldest row is dropped when exceed");
}
bool propagate = param_->propagate;
if (ImGui::Checkbox("propagate", &propagate)) {
param_->propagate = propagate;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("after handling, passes the msg to outer logger if exists");
}
bool freeze = param_->freeze;
if (ImGui::Checkbox("freeze", &freeze)) {
param_->freeze = freeze;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("stop handling except propagation");
}
ImGui::EndPopup();
}
// LogView
if (win_.shownInCurrentFrame()) {
ImGui::SetNextWindowSize({48*em, 16*em}, ImGuiCond_FirstUseEver);
}
if (win_.Begin()) {
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()) {
ImGui::TextUnformatted(row.msg.c_str());
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(row.msg.c_str());
}
}
// 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();
}
}
win_.End();
}
void Logger::UpdateMenu() noexcept {
ImGui::MenuItem("shown", nullptr, &win_.shown());
if (ImGui::MenuItem("config")) {
popup_ = "ConfigPopup";
}
}
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(); });
}
}
}
} // namespace nf7