diff --git a/README.md b/README.md index e05e9b2..3d098e5 100644 --- a/README.md +++ b/README.md @@ -51,3 +51,4 @@ You can check this [repository](https://github.com/Tiaansu/greet-component) for ## Thanks to - [Amir's omp-node](https://github.com/AmyrAhmady/omp-node) (I copied it's `include` style so other components can use this component) - [maddinatOr's samp-log-core](https://github.com/maddinat0r/samp-log-core) (most of the code were from this project) +- [Toiletduck / Eksqtr](https://github.com/eksqtr) (big thanks to this person. Many ideas were from him) \ No newline at end of file diff --git a/include/omp-logger.hpp b/include/omp-logger.hpp index 958c170..4386b18 100644 --- a/include/omp-logger.hpp +++ b/include/omp-logger.hpp @@ -35,6 +35,7 @@ namespace OmpLogger } struct IOmpLog; +struct PaginatedResult; static const UID OmpLoggerComponent_UID = UID(0xAB3BC9C4E583C8B4); class IOmpLoggerComponent : public IComponent @@ -68,6 +69,8 @@ struct IOmpLog virtual uint32_t getColor() const = 0; + virtual PaginatedResult fetchLogs(int linesPerPage, int pageStart, const std::string& searchTerm, bool caseSensitive) const = 0; + virtual bool log(AMX* amx, OmpLogger::ELogLevel level, StringView message) const = 0; virtual bool log(OmpLogger::ELogLevel level, StringView message) const = 0; @@ -103,4 +106,11 @@ class OmpLoggerManager private: IOmpLoggerComponent* ompLogger_ = nullptr; +}; + +struct PaginatedResult +{ + std::vector lines; + int currentPage; + int totalPages; }; \ No newline at end of file diff --git a/omp-logger.inc b/omp-logger.inc index cd281ef..f41256d 100644 --- a/omp-logger.inc +++ b/omp-logger.inc @@ -31,3 +31,17 @@ native bool:Logger_Info(Logger:id, const format[], OPEN_MP_TAGS:...); native bool:Logger_Warning(Logger:id, const format[], OPEN_MP_TAGS:...); native bool:Logger_Error(Logger:id, const format[], OPEN_MP_TAGS:...); native bool:Logger_Fatal(Logger:id, const format[], OPEN_MP_TAGS:...); + +// Logs result +native Logger_FetchLogs(playerid, Logger:logger, amount, pageStart, const callback[], const search[] = "", bool:caseSensitive = true); +native Logger_GetResult(LogsResult:result, row, logs[], size = sizeof logs); +native Logger_FreeResult(LogsResult:result); + +// Cleanup results once they go out of scope +stock operator~(const LogsResult:result[], len) +{ + for (new i = 0; i < len; ++ i) + { + Logger_FreeResult(result[i]); + } +} diff --git a/src/component.cpp b/src/component.cpp index d692656..02970f8 100644 --- a/src/component.cpp +++ b/src/component.cpp @@ -3,6 +3,7 @@ #include #include "component.hpp" #include "helpers/utils.hpp" +#include "threaded-queue.hpp" // API IOmpLog* OmpLoggerComponent::createLogger(StringView name, int32_t color, OmpLogger::ELogLevel level, bool isPlugin) @@ -26,7 +27,7 @@ IOmpLog* OmpLoggerComponent::createLogger(StringView name, int32_t color, OmpLog color = 0; } - std::FILE* file = ::fopen(filepath.c_str(), "a"); + std::FILE* file = ::fopen(filepath.c_str(), "a+"); if (file == nullptr) { core_->logLn(LogLevel::Error, "Failed to open log file \"%s\".", filepath.c_str()); @@ -48,6 +49,24 @@ IOmpLog* OmpLoggerComponent::getLogger(int id) return pool_.get(id); } +// Fetch logs +ILogsResult* OmpLoggerComponent::initLogsResult(std::vector logs) +{ + return logsResults_.emplace(logs); +} + +bool OmpLoggerComponent::deleteLogsResult(ILogsResult* result) +{ + int id = static_cast(result)->getID(); + logsResults_.release(id, false); + return true; +} + +ILogsResult* OmpLoggerComponent::getLogsResult(int id) +{ + return logsResults_.get(id); +} + // Callbacks void OmpLoggerComponent::onLoad(ICore* c) { @@ -68,6 +87,7 @@ void OmpLoggerComponent::onInit(IComponentList* components) setAmxFunctions(pawn_->getAmxFunctions()); setAmxLookups(components); pawn_->getEventDispatcher().addEventHandler(this); + core_->getEventDispatcher().addEventHandler(this); IConfig& config = core_->getConfig(); isLogLevelNameCapitalized_ = (config.getBool("logger.log_level_capitalized")) ? (*config.getBool("logger.log_level_capitalized")) : false; @@ -193,6 +213,8 @@ void OmpLoggerComponent::free() serverLoggerFile_ = nullptr; } + ThreadedQueue::Get()->Destroy(); + delete this; core_->printLn("[omp-logger] Logger released."); } @@ -210,7 +232,13 @@ void OmpLoggerComponent::onAmxLoad(IPawnScript& script) void OmpLoggerComponent::onAmxUnload(IPawnScript& script) { - debugEraseAMX(script.GetAMX()); + AMX* amx = script.GetAMX(); + debugEraseAMX(amx); +} + +void OmpLoggerComponent::onTick(Microseconds elapsed, TimePoint now) +{ + ThreadedQueue::Get()->Process(); } OmpLoggerComponent::~OmpLoggerComponent() @@ -219,6 +247,10 @@ OmpLoggerComponent::~OmpLoggerComponent() { pawn_->getEventDispatcher().removeEventHandler(this); } + if (core_) + { + core_->getEventDispatcher().removeEventHandler(this); + } } // API - Config diff --git a/src/component.hpp b/src/component.hpp index cd9525b..c2d223a 100644 --- a/src/component.hpp +++ b/src/component.hpp @@ -11,10 +11,12 @@ #include "omp-logger.hpp" #include "omp-log.hpp" #include "debug-manager.hpp" +#include "logs-result.hpp" class OmpLoggerComponent final : public IOmpLoggerComponent , public PawnEventHandler + , public CoreEventHandler { private: ICore* core_ = nullptr; @@ -23,6 +25,8 @@ class OmpLoggerComponent final MarkedPoolStorage pool_; + MarkedPoolStorage logsResults_; + inline static OmpLoggerComponent* instance_ = nullptr; // configs @@ -44,6 +48,13 @@ class OmpLoggerComponent final IOmpLog* getLogger(int id) override; + // Fetch logs + ILogsResult* initLogsResult(std::vector logs); + + bool deleteLogsResult(ILogsResult* result); + + ILogsResult* getLogsResult(int id); + // Component StringView componentName() const override { @@ -54,6 +65,8 @@ class OmpLoggerComponent final { return SemanticVersion(0, 0, 1, 0); } + + void onTick(Microseconds elapsed, TimePoint now) override; void onLoad(ICore* c) override; diff --git a/src/logs-result.cpp b/src/logs-result.cpp new file mode 100644 index 0000000..e0c25c1 --- /dev/null +++ b/src/logs-result.cpp @@ -0,0 +1,19 @@ +#include "logs-result.hpp" +#include "component.hpp" + +using namespace Impl; + +int LogsResult::getID() const +{ + return poolID; +} + +std::string LogsResult::getLog(int row) const +{ + return logs_.at(row); +} + +LogsResult::LogsResult(std::vector logs) + : logs_(std::move(logs)) +{ +} \ No newline at end of file diff --git a/src/logs-result.hpp b/src/logs-result.hpp new file mode 100644 index 0000000..7c91763 --- /dev/null +++ b/src/logs-result.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +using namespace Impl; + +struct ILogsResult +{ + virtual int getID() const = 0; + + virtual std::string getLog(int row) const = 0; +}; + +class LogsResult final + : public ILogsResult + , public PoolIDProvider + , public NoCopy +{ +private: + std::vector logs_; + +public: + int getID() const override; + + std::string getLog(int row) const override; + + LogsResult(std::vector logs); +}; \ No newline at end of file diff --git a/src/natives.cpp b/src/natives.cpp index 12977ba..edfeec4 100644 --- a/src/natives.cpp +++ b/src/natives.cpp @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include @@ -7,6 +9,7 @@ #include "component.hpp" #include "natives.hpp" #include "omp-logger.hpp" +#include "threaded-queue.hpp" #include "helpers/format.hpp" SCRIPT_API(Logger_Create, int(std::string const& name, int32_t color, OmpLogger::ELogLevel level)) @@ -56,4 +59,39 @@ SCRIPT_API(Logger_Fatal, bool(IOmpLog& logger, cell const* format)) { AMX* amx = GetAMX(); return logger.log(amx, OmpLogger::ELogLevel::Fatal, AmxStringFormatter(format, amx, GetParams(), 2)); +} + +SCRIPT_API(Logger_FetchLogs, bool(IPlayer& player, IOmpLog& logger, int linesPerPage, int pageStart, std::string const& callback, std::string const& searchTerm, bool caseSensitive)) +{ + AMX* amx = GetAMX(); + + auto func = [&player, &logger, amx, callback, searchTerm, linesPerPage, pageStart, caseSensitive]() { + + int funcIDX = 0; + if (!amx_FindPublic(amx, callback.c_str(), &funcIDX)) + { + PaginatedResult result = logger.fetchLogs(linesPerPage, pageStart, searchTerm, caseSensitive); + ILogsResult* logsResult = OmpLoggerComponent::Get()->initLogsResult(result.lines); + amx_Push(amx, result.totalPages); + amx_Push(amx, result.currentPage); + amx_Push(amx, (int)result.lines.size()); + amx_Push(amx, logsResult->getID()); + amx_Push(amx, logger.getID()); + amx_Push(amx, player.getID()); + amx_Exec(amx, NULL, funcIDX); + } + }; + ThreadedQueue::Get()->Dispatch(func); + return 1; +} + +SCRIPT_API(Logger_GetResult, int(ILogsResult& result, int row, OutputOnlyString& logs)) +{ + logs = result.getLog(row); + return std::get(logs).length(); +} + +SCRIPT_API(Logger_FreeResult, bool(ILogsResult& result)) +{ + return OmpLoggerComponent::Get()->deleteLogsResult(&result); } \ No newline at end of file diff --git a/src/natives.hpp b/src/natives.hpp index 5184ae5..7031553 100644 --- a/src/natives.hpp +++ b/src/natives.hpp @@ -104,4 +104,104 @@ namespace pawn_natives ParamCast(AMX*, cell*, int) = delete; ParamCast() = delete; }; + + // Logs result + template <> + struct ParamLookup + { + static ILogsResult* Val(cell ref) noexcept + { + if (auto pool = OmpLoggerComponent::Get()) + { + return pool->getLogsResult(ref); + } + return nullptr; + } + }; + + template <> + class ParamCast + { + public: + ParamCast(AMX* amx, cell* params, int idx) noexcept + { + value_ = ParamLookup::Val(params[idx]); + } + + ~ParamCast() + { + } + + ParamCast(ParamCast const&) = delete; + ParamCast(ParamCast&&) = delete; + + operator ILogsResult*() + { + return value_; + } + + bool Error() const + { + return false; + } + + static constexpr int Size = 1; + + private: + ILogsResult* value_; + }; + + template <> + class ParamCast + { + public: + ParamCast(AMX* amx, cell* params, int idx) + { + value_ = ParamLookup::Val(params[idx]); + if (value_ == nullptr) + { + AmxFuncCallInfo dest; + if (!DebugManager::Get()->GetFunctionCall(amx, amx->cip, dest)) + { + OmpLoggerComponent::Get()->getCore()->logLn(LogLevel::Warning, "Invalid logs result id %i", static_cast(params[idx])); + } + else + { + OmpLoggerComponent::Get()->getCore()->logLn(LogLevel::Warning, "Invalid logs result id %i (%s:%i)", static_cast(params[idx]), dest.file, static_cast(dest.line)); + } + error_ = true; + } + } + + ~ParamCast() + { + } + + ParamCast(ParamCast const&) = delete; + ParamCast(ParamCast&&) = delete; + + operator ILogsResult&() + { + return *value_; + } + + bool Error() const + { + return error_; + } + + static constexpr int Size = 1; + + private: + ILogsResult* value_; + bool error_ = false; + }; + + template <> + class ParamCast + { + public: + ParamCast(AMX*, cell*, int) = delete; + ParamCast() = delete; + }; } \ No newline at end of file diff --git a/src/omp-log.cpp b/src/omp-log.cpp index 5fa60c5..5694f00 100644 --- a/src/omp-log.cpp +++ b/src/omp-log.cpp @@ -34,6 +34,152 @@ std::FILE* OmpLog::getFile() const return file_; } +bool caseInsensitiveCompare(const std::string& str1, const std::string& str2) +{ + std::string lowerStr1 = str1; + std::string lowerStr2 = str2; + std::transform(lowerStr1.begin(), lowerStr1.end(), lowerStr1.begin(), ::tolower); + std::transform(lowerStr2.begin(), lowerStr2.end(), lowerStr2.begin(), ::tolower); + return lowerStr1.find(lowerStr2) != std::string::npos; +} + +long long countTotalLines(std::FILE* file) +{ + if (file == nullptr) + { + return 0; + } + + ::fseek(file, 0, SEEK_SET); + + long long lineCount = 0; + char buffer[4096]; + + while (::fgets(buffer, sizeof(buffer), file) != nullptr) + { + lineCount ++; + } + + ::fseek(file, 0, SEEK_SET); + return lineCount; +} + +long long countMatchingLines(std::FILE* file, const std::string& searchTerm, bool caseSensitive) +{ + if (file == nullptr) + { + return 0; + } + + ::fseek(file, 0, SEEK_SET); + + long long lineCount = 0; + char buffer[4096]; + while (std::fgets(buffer, sizeof(buffer), file) != nullptr) + { + std::string line(buffer); + if (caseSensitive) + { + if (line.find(searchTerm) != std::string::npos) + { + lineCount ++; + } + } + else + { + if (caseInsensitiveCompare(line, searchTerm)) + { + lineCount ++; + } + } + } + + std::fseek(file, 0, SEEK_SET); + return lineCount; +} + +PaginatedResult OmpLog::fetchLogs(int linesPerPage, int pageStart, const std::string& searchTerm, bool caseSensitive) const +{ + PaginatedResult result; + + if (file_ == nullptr) + { + return result; + } + + if (!searchTerm.empty()) + { + result.totalPages = (countMatchingLines(file_, searchTerm, caseSensitive) + linesPerPage - 1) / linesPerPage; + } + else + { + result.totalPages = (countTotalLines(file_) + linesPerPage - 1) / linesPerPage; + } + + if (pageStart == -1 || pageStart > result.totalPages) + { + pageStart = 1; + } + + result.currentPage = pageStart; + + long long startLine = (pageStart - 1) * linesPerPage; + + if (!searchTerm.empty()) + { + ::fseek(file_, 0, SEEK_SET); + long long currentLine = 0; + char buffer[4096]; + while (currentLine < startLine && ::fgets(buffer, sizeof(buffer), file_) != nullptr) + { + std::string line(buffer); + + if (caseSensitive) + { + if (line.find(searchTerm) != std::string::npos) + { + currentLine ++; + } + } + else + { + if (caseInsensitiveCompare(line, searchTerm)) + { + currentLine ++; + } + } + } + } + else + { + ::fseek(file_, 0, SEEK_SET); + long long currentLine = 0; + char buffer[4096]; + while (currentLine < startLine && ::fgets(buffer, sizeof(buffer), file_) != nullptr) + { + currentLine ++; + } + } + + char buffer[4096]; + long long linesRead = 0; + while (linesRead < linesPerPage && ::fgets(buffer, sizeof(buffer), file_) != nullptr) + { + std::string line(buffer); + line.erase(std::find_if(line.rbegin(), line.rend(), [](unsigned char ch) { + return !std::isspace(ch); + }).base(), line.end()); + + bool caseSensitiveResult = caseSensitive ? line.find(searchTerm) != std::string::npos : caseInsensitiveCompare(line, searchTerm); + if (searchTerm.empty() || caseSensitiveResult) + { + result.lines.push_back(line); + linesRead ++; + } + } + return result; +} + bool OmpLog::log(AMX* amx, OmpLogger::ELogLevel level, StringView message) const { return log_INTERNAL(amx, level, message); diff --git a/src/omp-log.hpp b/src/omp-log.hpp index 52dd94b..68468bc 100644 --- a/src/omp-log.hpp +++ b/src/omp-log.hpp @@ -38,6 +38,8 @@ class OmpLog final bool log(OmpLogger::ELogLevel level, StringView message) const override; + PaginatedResult fetchLogs(int linesPerPage, int pageStart, const std::string& searchTerm, bool caseSensitive) const override; + OmpLog(StringView name, uint32_t color, OmpLogger::ELogLevel level, std::FILE* file); private: diff --git a/src/singleton.hpp b/src/singleton.hpp new file mode 100644 index 0000000..a66ac79 --- /dev/null +++ b/src/singleton.hpp @@ -0,0 +1,34 @@ +#pragma once + + +template +class Singleton +{ +protected: + static T *m_Instance; + +public: + Singleton() + { } + virtual ~Singleton() + { } + + inline static T *Get() + { + if (m_Instance == nullptr) + m_Instance = new T; + return m_Instance; + } + + inline static void Destroy() + { + if (m_Instance != nullptr) + { + delete m_Instance; + m_Instance = nullptr; + } + } +}; + +template +T* Singleton::m_Instance = nullptr; \ No newline at end of file diff --git a/src/threaded-queue.cpp b/src/threaded-queue.cpp new file mode 100644 index 0000000..e3fc139 --- /dev/null +++ b/src/threaded-queue.cpp @@ -0,0 +1,20 @@ +#include "threaded-queue.hpp" + + +void ThreadedQueue::Dispatch(ThreadedQueue::Callback callback) +{ + std::lock_guard lock_guard(m_Mutex); + m_Queue.push(callback); +} + +void ThreadedQueue::Process() +{ + std::lock_guard lock_guard(m_Mutex); + while (!m_Queue.empty()) + { + auto action = std::move(m_Queue.front()); + m_Queue.pop(); + + action(); + } +} \ No newline at end of file diff --git a/src/threaded-queue.hpp b/src/threaded-queue.hpp new file mode 100644 index 0000000..b1f49fa --- /dev/null +++ b/src/threaded-queue.hpp @@ -0,0 +1,28 @@ +// Taken from pawn-websockets & samp mysql + +#pragma once + +#include "singleton.hpp" + +#include +#include +#include + +class ThreadedQueue : public Singleton +{ + typedef std::function Callback; + typedef std::queue CallbackQueue; + + friend class Singleton; + +public: + void Dispatch(Callback callback); + void Process(); + +private: + CallbackQueue m_Queue; + std::mutex m_Mutex; + + ThreadedQueue() = default; + ~ThreadedQueue() = default; +}; \ No newline at end of file