#include #include #include #include #include #include #include #include #include #include #include #include #include #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 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(); d.propagate = yaml["propagate"].as(); d.freeze = yaml["freeze"].as(); *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 Clone(nf7::Env& env) const noexcept override { return std::make_unique(env, Data {mem_.data()}); } void PostHandle(const nf7::File::Event& ev) noexcept override { switch (ev.type) { case Event::kAdd: store_ = std::make_shared(*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(t). Select(this, store_.get()); } private: class ItemStore; std::shared_ptr store_; std::deque rows_; nf7::GenericMemento 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 { 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 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 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(param_.max_rows - items_.size()); } const auto keep = static_cast(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 self(nf7::Logger*) noexcept override { return shared_from_this(); } void param(const Data& d) noexcept { std::unique_lock k(mtx_); param_ = d; } private: std::mutex mtx_; std::deque 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(*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