diff --git a/CMakeLists.txt b/CMakeLists.txt index dd9d8a1..e4517eb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ set(DEFINITIONS "") # imgui include because https://github.com/ocornut/imgui/issues/6184#issuecomment-1439570929 list(APPEND INCLUDES thirdparty thirdparty/imgui /usr/include/SDL2) list(APPEND SOURCES main.cpp event_loop.cpp logcat_thread.cpp logcat_entry.cpp log.cpp config.cpp filters.cpp misc.cpp pcre2_wrapper.cpp - group_panel.cpp fragments/filters.cpp fragments/export.cpp windows/logs.cpp windows/settings.cpp windows/main.cpp) + group_panel.cpp fragments/filters.cpp fragments/export.cpp fragments/import.cpp windows/logs.cpp windows/settings.cpp windows/main.cpp) list(APPEND IMGUI_SOURCES thirdparty/imgui/imgui.cpp thirdparty/imgui/imgui_draw.cpp thirdparty/imgui/imgui_widgets.cpp thirdparty/imgui/imgui_tables.cpp thirdparty/imgui/misc/cpp/imgui_stdlib.cpp thirdparty/imgui/misc/freetype/imgui_freetype.cpp thirdparty/imgui/backends/imgui_impl_sdl2.cpp thirdparty/imgui/backends/imgui_impl_opengl3.cpp) diff --git a/fragments/export.cpp b/fragments/export.cpp index e5b87fe..3ebcc75 100644 --- a/fragments/export.cpp +++ b/fragments/export.cpp @@ -13,7 +13,7 @@ enum class ExportAmount : int { static inline void export_logcat_entries(const LogcatEntries& logcat_entries, ExportAmount export_amount, bool export_buffers, File& file); static inline void export_logcat_entries_to_clipboard(const LogcatEntries& logcat_entries, ExportAmount export_amount, bool export_buffers); -static inline void export_logcat_entries(const LogcatEntries& logcat_entries, ExportAmount export_amount, bool export_buffers, auto cb); +static void export_logcat_entries(const LogcatEntries& logcat_entries, ExportAmount export_amount, bool export_buffers, auto cb); static inline std::optional save_file_picker(); void export_fragment(const LogcatEntries& logcat_entries) { @@ -64,7 +64,7 @@ static inline void export_logcat_entries_to_clipboard(const LogcatEntries& logca ImGui::SetClipboardText(text.c_str()); } -static inline void export_logcat_entries(const LogcatEntries& logcat_entries, ExportAmount export_amount, bool export_buffers, auto cb) { +static void export_logcat_entries(const LogcatEntries& logcat_entries, ExportAmount export_amount, bool export_buffers, auto cb) { size_t size; const LogcatEntry& (LogcatEntries::*get_fn)(size_t) const; unsigned int buffers_printed = 0; diff --git a/fragments/export.h b/fragments/export.h index 555c5df..a8c7044 100644 --- a/fragments/export.h +++ b/fragments/export.h @@ -1,7 +1,5 @@ #pragma once -#include - class LogcatEntries; // forward declaration from ../logcat_entry.h void export_fragment(const LogcatEntries& logcat_entries); diff --git a/fragments/import.cpp b/fragments/import.cpp new file mode 100644 index 0000000..2804252 --- /dev/null +++ b/fragments/import.cpp @@ -0,0 +1,154 @@ +#include +#include +#include + +#include "../file.h" +#include "../logcat_entry.h" + +static inline void import_logcat_entries(LogcatEntries& logcat_entries, bool ignore_buffer_lines, File& file); +static inline void import_logcat_entries_from_clipboard(LogcatEntries& logcat_entries, bool ignore_buffer_lines); +static void import_logcat_entries(LogcatEntries& logcat_entries, bool ignore_buffer_lines, auto cb); +static inline std::optional open_file_picker(); + +void import_fragment(LogcatEntries& logcat_entries) { + static bool ignore_buffer_lines = false; + + ImGui::Checkbox("Ignore buffer lines", &ignore_buffer_lines); + + if (ImGui::Button("Import from File")) { + std::optional file = open_file_picker(); + if (file) { + ImGui::CloseCurrentPopup(); + try { + import_logcat_entries(logcat_entries, ignore_buffer_lines, *file); + } catch (const std::exception& e) { + log(std::string("Failed to import logcat entries: ") + e.what()); + } + } + } + + ImGui::SameLine(); + if (ImGui::Button("Import from Clipboard")) { + ImGui::CloseCurrentPopup(); + try { + import_logcat_entries_from_clipboard(logcat_entries, ignore_buffer_lines); + } catch (const std::exception& e) { + log(std::string("Failed to import logcat entries: ") + e.what()); + } + } +} + +static inline void import_logcat_entries(LogcatEntries& logcat_entries, bool ignore_buffer_lines, File& file) { + auto handle_valid_char = [](char line[MAX_LOGCAT_LINE_SIZE], size_t* line_length, char c) -> std::optional { + if (c == '\n') { + return true; + } + if (*line_length == MAX_LOGCAT_LINE_SIZE) { + throw std::runtime_error("Received line longer than 512k"); + } + + line[*line_length] = c; + (*line_length)++; + + return std::nullopt; + }; + auto handle_char = [&](char line[MAX_LOGCAT_LINE_SIZE], size_t* line_length, int c) -> std::optional { + if (c != EOF) { + return handle_valid_char(line, line_length, static_cast(c)); + } + + if (feof(file.get())) { + return *line_length != 0; + } else if (ferror(file.get())) { + throw_system_error("fread()"); + } else { + throw std::runtime_error("fgetc() returned EOF without setting anything"); + } + + return std::nullopt; + }; + + import_logcat_entries(logcat_entries, ignore_buffer_lines, [&](char line[MAX_LOGCAT_LINE_SIZE], size_t* line_length) { + *line_length = 0; + + while (true) { + int c = fgetc(file.get()); + std::optional ret = handle_char(line, line_length, c); + if (ret) { + return *ret; + } + } + }); +} + +static inline void import_logcat_entries_from_clipboard(LogcatEntries& logcat_entries, bool ignore_buffer_lines) { + const char* text = ImGui::GetClipboardText(); + + import_logcat_entries(logcat_entries, ignore_buffer_lines, [&](char line[MAX_LOGCAT_LINE_SIZE], size_t* line_length) { + if (!text) { + return false; + } + + const char* newline_offset = strchr(text, '\n'); + *line_length = newline_offset ? static_cast(newline_offset - text) : strlen(text); + + if (*line_length > MAX_LOGCAT_LINE_SIZE) { + throw std::runtime_error("Received line longer than 512k"); + } + + memcpy(line, text, *line_length); + text = newline_offset ? &newline_offset[1] : nullptr; + return true; + }); +} + +static void import_logcat_entries(LogcatEntries& logcat_entries, bool ignore_buffer_lines, auto cb) { + char line[MAX_LOGCAT_LINE_SIZE]; + size_t line_length; + Buffer buffer = Buffer::Unknown; + + while (cb(line, &line_length)) { + if (line_length == 0) { + continue; + } + + std::optional logcat_entry = try_parse_logcat_entry(line, line_length, buffer); + if (logcat_entry) { + logcat_entries.push_back(std::move(*logcat_entry)); + continue; + } + + std::optional new_buffer = try_parse_buffer(line, line_length); + if (new_buffer) { + if (!ignore_buffer_lines) { + buffer = *new_buffer; + } + continue; + } + + throw std::invalid_argument(std::string("Cannot parse line: ") + std::string(line, line_length)); + } +} + +static inline std::optional open_file_picker() { + nfdchar_t* path; + nfdfilteritem_t filters[1] = {{"Log file", "log"}}; + nfdresult_t res = NFD_OpenDialog(&path, filters, 1, nullptr); + + if (res == NFD_OKAY) { + try { + File file(path, "r"); + NFD_FreePath(path); + return std::move(file); + } catch (const std::exception& e) { + NFD_FreePath(path); + log(std::string("Failed to open file from file picker: ") + e.what()); + } + } else if (res == NFD_CANCEL) { + // dialog was canceled, shrug + } else { + // ignore error when failing to open the file picker + } + + return std::nullopt; +} diff --git a/fragments/import.h b/fragments/import.h new file mode 100644 index 0000000..e2d6eec --- /dev/null +++ b/fragments/import.h @@ -0,0 +1,5 @@ +#pragma once + +class LogcatEntries; // forward declaration from ../logcat_entry.h + +void import_fragment(LogcatEntries& logcat_entries); diff --git a/logcat_entry.cpp b/logcat_entry.cpp index 5a844d0..e22f6f5 100644 --- a/logcat_entry.cpp +++ b/logcat_entry.cpp @@ -8,7 +8,7 @@ #include "pcre2_wrapper.h" static const Pcre2Regex LogcatEntryRegex( - "^ *(\\d+)(?:\\.\\d+)?" // time + "^ *(?:(\\d+)(?:\\.\\d+)?|(\\d{4,})-(\\d{2})-(\\d{2}) (\\d{2}):(\\d{2}):(\\d{2}))" // time and date "(?: +([\\w\\d._-]+))?" // optional user " +(\\d+)" // pid " +(\\d+)" // tid @@ -22,6 +22,7 @@ static inline char to_char(Priority priority); static inline std::string rightpad(const std::string& str, size_t characters); static inline Priority priority_from(char c); +static inline int to_int(const char* str, const char* expected_end); static inline long to_long(const char* str, const char* expected_end); static unsigned long long to_ull(const char* str, const char* expected_end); @@ -78,7 +79,7 @@ std::string to_string(const LogcatEntry& logcat_entry) { std::optional try_parse_logcat_entry(char* buf, size_t length, Buffer buffer) { - regmatch_t matches[8]; + regmatch_t matches[14]; matches[0].rm_so = 0; matches[0].rm_eo = static_cast(length); if (static_cast(matches[0].rm_eo) != length) { @@ -90,6 +91,7 @@ std::optional try_parse_logcat_entry(char* buf, size_t length, Buff } auto group_exists = [](const regmatch_t& match) { return match.rm_so > -1 && match.rm_eo > -1; }; + auto group_int = [&](const regmatch_t& match) { return to_int(&buf[match.rm_so], &buf[match.rm_eo]); }; auto group_long = [&](const regmatch_t& match) { return to_long(&buf[match.rm_so], &buf[match.rm_eo]); }; auto group_ull = [&](const regmatch_t& match) { return to_ull(&buf[match.rm_so], &buf[match.rm_eo]); }; auto group_string = [&](const regmatch_t& match) { return std::string(&buf[match.rm_so], static_cast(match.rm_eo - match.rm_so)); }; @@ -98,17 +100,33 @@ std::optional try_parse_logcat_entry(char* buf, size_t length, Buff // if pcre2 gives us negative offsets then i'll die LogcatEntry logcat_entry = { .buffer = buffer, - // time to be set at the end - .user = group_exists(matches[2]) ? std::optional(group_string(matches[2])) : std::nullopt, - .pid = group_ull(matches[3]), - .tid = group_ull(matches[4]), - .priority = group_priority(matches[5]), - .tag = group_string(matches[6]), - .message = group_string(matches[7]), + // time to be set later + .user = group_exists(matches[8]) ? std::optional(group_string(matches[8])) : std::nullopt, + .pid = group_ull(matches[9]), + .tid = group_ull(matches[10]), + .priority = group_priority(matches[11]), + .tag = group_string(matches[12]), + .message = group_string(matches[13]), }; - time_t time = group_long(matches[1]); - localtime_r(&time, &logcat_entry.time); + if (group_exists(matches[1])) { + time_t time = group_long(matches[1]); + localtime_r(&time, &logcat_entry.time); + } else { + struct tm tm = { + .tm_sec = group_int(matches[7]), + .tm_min = group_int(matches[6]), + .tm_hour = group_int(matches[5]), + .tm_mday = group_int(matches[4]), + .tm_mon = group_int(matches[3]), + .tm_year = group_int(matches[2]) - 1900, + .tm_isdst = -1, + .tm_gmtoff = 0, + }; + time_t time = mktime(&tm) + tm.tm_gmtoff; // for some reason you have to add the local time offset? + localtime_r(&time, &logcat_entry.time); + } + return std::move(logcat_entry); } @@ -237,6 +255,16 @@ static inline Priority priority_from(char c) { } } +static inline int to_int(const char* str, const char* expected_end) { + long res = to_long(str, expected_end); + if (res > INT_MAX) { + throw std::overflow_error(std::string(str) + " is too big"); + } else if (res < INT_MIN) { + throw std::underflow_error(std::string(str) + " is too small"); + } + return static_cast(res); +} + static inline long to_long(const char* str, const char* expected_end) { char* endptr; diff --git a/logcat_entry.h b/logcat_entry.h index ac31782..8877eef 100644 --- a/logcat_entry.h +++ b/logcat_entry.h @@ -7,6 +7,8 @@ struct Config; // forward declaration from config.h +#define MAX_LOGCAT_LINE_SIZE 512 * 1024 + enum class Buffer { Unknown = 0b1, Main = 0b10, diff --git a/logcat_thread.cpp b/logcat_thread.cpp index f4934cf..4f89ff2 100644 --- a/logcat_thread.cpp +++ b/logcat_thread.cpp @@ -26,7 +26,7 @@ static void mark_nonblock(int fd) { } } -static inline void look_for_newlines(char buf[NEWLINE_BUF_SIZE], size_t* used, size_t previous_used, +static inline void look_for_newlines(char buf[MAX_LOGCAT_LINE_SIZE], size_t* used, size_t previous_used, void(LogcatThread::*handle_line)(char*, size_t, bool), LogcatThread* logcat_thread, bool is_stdout) { size_t search_offset = previous_used; size_t real_offset = 0; @@ -57,7 +57,7 @@ static inline void look_for_newlines(char buf[NEWLINE_BUF_SIZE], size_t* used, s static inline void handle_fd(int fd, char* buf, size_t* used, void(LogcatThread::*handle_line)(char*, size_t, bool), LogcatThread* logcat_thread, bool is_stdout) { while (true) { - ssize_t read_size_u = read(fd, &buf[*used], (NEWLINE_BUF_SIZE - *used) * sizeof(char)); + ssize_t read_size_u = read(fd, &buf[*used], (MAX_LOGCAT_LINE_SIZE - *used) * sizeof(char)); if (read_size_u == 0) { return; } else if (read_size_u < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) { @@ -66,7 +66,7 @@ static inline void handle_fd(int fd, char* buf, size_t* used, throw_system_error("read()"); } size_t read_size = static_cast(read_size_u) / sizeof(char); - if (*used + read_size > NEWLINE_BUF_SIZE) { + if (*used + read_size > MAX_LOGCAT_LINE_SIZE) { throw std::runtime_error("Received line longer than 512k"); } size_t previous_used = *used; diff --git a/logcat_thread.h b/logcat_thread.h index ed94d58..08e3650 100644 --- a/logcat_thread.h +++ b/logcat_thread.h @@ -7,7 +7,6 @@ #include "logcat_entry.h" typedef std::variant LogcatThreadItem; -#define NEWLINE_BUF_SIZE 512 * 1024 enum class LogcatProcessRequest { None, @@ -54,9 +53,9 @@ private: int _stderr_read_fd = -1; int _stderr_write_fd = -1; - char _stdout_buf[NEWLINE_BUF_SIZE]; + char _stdout_buf[MAX_LOGCAT_LINE_SIZE]; size_t _stdout_buf_used = 0; - char _stderr_buf[NEWLINE_BUF_SIZE]; + char _stderr_buf[MAX_LOGCAT_LINE_SIZE]; size_t _stderr_buf_used = 0; pid_t _logcat_pid = -1; diff --git a/windows/main.cpp b/windows/main.cpp index 7c8ab57..60aeff6 100644 --- a/windows/main.cpp +++ b/windows/main.cpp @@ -9,6 +9,7 @@ #include "../logcat_entry.h" #include "../logcat_thread.h" #include "../fragments/export.h" +#include "../fragments/import.h" #include "main.h" static inline void render_table_item_context_menu(const LogcatEntry& logcat_entry) { @@ -164,10 +165,12 @@ void main_window(bool latest_log_entries_read, ImFont* monospace_font, LogcatThr ImGui::SameLine(); ImGui::SeparatorEx(ImGuiSeparatorFlags_Vertical); + ImGui::SameLine(); if (ImGui::Button("Clear")) { logcat_entries.clear(); } + ImGui::SameLine(); if (ImGui::Button("Export")) { ImGui::OpenPopup("export_logcat"); @@ -177,6 +180,15 @@ void main_window(bool latest_log_entries_read, ImFont* monospace_font, LogcatThr ImGui::EndPopup(); } + ImGui::SameLine(); + if (ImGui::Button("Import")) { + ImGui::OpenPopup("import_logcat"); + } + if (ImGui::BeginPopup("import_logcat")) { + import_fragment(logcat_entries); + ImGui::EndPopup(); + } + ImGui::Separator(); // copied from imgui/imgui_demo.cpp: [SECTION] Example App: Debug Console / ShowExampleAppConsole() // and [SECTION] Example App: Long Text / ShowExampleAppLongText()