diff options
Diffstat (limited to '')
-rw-r--r-- | src/session.export.cc | 484 |
1 files changed, 484 insertions, 0 deletions
diff --git a/src/session.export.cc b/src/session.export.cc new file mode 100644 index 0000000..109627b --- /dev/null +++ b/src/session.export.cc @@ -0,0 +1,484 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "session.export.hh" + +#include "base/injector.hh" +#include "bound_tags.hh" +#include "lnav.hh" +#include "sqlitepp.client.hh" +#include "sqlitepp.hh" +#include "textview_curses.hh" + +struct log_message_session_state { + int64_t lmss_time_msecs; + std::string lmss_format; + bool lmss_mark; + nonstd::optional<std::string> lmss_comment; + nonstd::optional<std::string> lmss_tags; + std::string lmss_hash; +}; + +template<> +struct from_sqlite<log_message_session_state> { + inline log_message_session_state operator()(int argc, + sqlite3_value** argv, + int argi) + { + return { + from_sqlite<int64_t>()(argc, argv, argi + 0), + from_sqlite<std::string>()(argc, argv, argi + 1), + from_sqlite<bool>()(argc, argv, argi + 2), + from_sqlite<nonstd::optional<std::string>>()(argc, argv, argi + 3), + from_sqlite<nonstd::optional<std::string>>()(argc, argv, argi + 4), + from_sqlite<std::string>()(argc, argv, argi + 5), + }; + } +}; + +struct log_filter_session_state { + std::string lfss_name; + bool lfss_enabled; + std::string lfss_type; + std::string lfss_language; + std::string lfss_pattern; +}; + +template<> +struct from_sqlite<log_filter_session_state> { + inline log_filter_session_state operator()(int argc, + sqlite3_value** argv, + int argi) + { + return { + from_sqlite<std::string>()(argc, argv, argi + 0), + from_sqlite<bool>()(argc, argv, argi + 1), + from_sqlite<std::string>()(argc, argv, argi + 2), + from_sqlite<std::string>()(argc, argv, argi + 3), + from_sqlite<std::string>()(argc, argv, argi + 4), + }; + } +}; + +struct log_file_session_state { + std::string lfss_content_id; + std::string lfss_format; + int64_t lfss_time_offset; +}; + +template<> +struct from_sqlite<log_file_session_state> { + inline log_file_session_state operator()(int argc, + sqlite3_value** argv, + int argi) + { + return { + from_sqlite<std::string>()(argc, argv, argi + 0), + from_sqlite<std::string>()(argc, argv, argi + 1), + from_sqlite<int64_t>()(argc, argv, argi + 2), + }; + } +}; + +namespace lnav { +namespace session { + +static nonstd::optional<ghc::filesystem::path> +find_container_dir(ghc::filesystem::path file_path) +{ + if (!ghc::filesystem::exists(file_path)) { + return nonstd::nullopt; + } + + nonstd::optional<ghc::filesystem::path> dir_with_last_readme; + + while (file_path.has_parent_path() + && file_path != file_path.root_directory()) + { + auto parent = file_path.parent_path(); + bool has_readme_entry = false; + std::error_code ec; + + for (const auto& entry : + ghc::filesystem::directory_iterator(parent, ec)) + { + if (!entry.is_regular_file()) { + continue; + } + + auto entry_filename = tolower(entry.path().filename().string()); + if (startswith(entry_filename, "readme")) { + has_readme_entry = true; + dir_with_last_readme = parent; + } + } + if (!has_readme_entry && dir_with_last_readme) { + return dir_with_last_readme; + } + + file_path = parent; + } + + return nonstd::nullopt; +} + +static std::string +replace_home_dir(std::string path) +{ + auto home_dir_opt = getenv_opt("HOME"); + + if (!home_dir_opt) { + return path; + } + + const auto* home_dir = home_dir_opt.value(); + + if (startswith(path, home_dir)) { + auto retval = path.substr(strlen(home_dir)); + + if (retval.front() != '/') { + retval.insert(0, "/"); + } + retval.insert(0, "$HOME"); + return retval; + } + + return path; +} + +Result<void, lnav::console::user_message> +export_to(FILE* file) +{ + static auto& lnav_db = injector::get<auto_sqlite3&>(); + + static const char* BOOKMARK_QUERY = R"( +SELECT log_time_msecs, log_format, log_mark, log_comment, log_tags, log_line_hash + FROM all_logs + WHERE log_mark = 1 OR log_comment IS NOT NULL OR log_tags IS NOT NULL +)"; + + static const char* FILTER_QUERY = R"( +SELECT view_name, enabled, type, language, pattern FROM lnav_view_filters +)"; + + static const char* FILE_QUERY = R"( +SELECT content_id, format, time_offset FROM lnav_file + WHERE format IS NOT NULL AND time_offset != 0 +)"; + + static constexpr const char HEADER[] = R"(#!lnav -Nf +# This file is an export of an lnav session. You can type +# '|/path/to/this/file' in lnav to execute this file and +# restore the state of the session. + +;SELECT raise_error('This session export was made with a newer version of lnav, please upgrade to ' || {0} || ' or later') + WHERE lnav_version() < {0} COLLATE naturalcase + +# The files loaded into the session were: + +)"; + + static constexpr const char LOG_DIR_INSERT[] = R"( +# Set this environment variable to override this value or edit this script. +;INSERT OR IGNORE INTO environ (name, value) VALUES ('LOG_DIR_{}', {}) +)"; + + static constexpr const char MARK_HEADER[] = R"( + +# The following SQL statements will restore the bookmarks, +# comments, and tags that were added in the session. + +;SELECT total_changes() AS before_mark_changes +)"; + + static constexpr const char MARK_FOOTER[] = R"( +;SELECT {} - (total_changes() - $before_mark_changes) AS failed_mark_changes +;SELECT echoln(printf('%sERROR%s: failed to restore %d bookmarks', + $ansi_red, $ansi_norm, $failed_mark_changes)) + WHERE $failed_mark_changes != 0 +)"; + + static const char* FILTER_HEADER = R"( + +# The following SQL statements will restore the filters that +# were added in the session. + +)"; + + static const char* FILE_HEADER = R"( + +# The following SQL statements will restore the state of the +# files in the session. + +;SELECT total_changes() AS before_file_changes +)"; + + static constexpr const char FILE_FOOTER[] = R"( +;SELECT {} - (total_changes() - $before_file_changes) AS failed_file_changes +;SELECT echoln(printf('%sERROR%s: failed to restore the state of %d files', + $ansi_red, $ansi_norm, $failed_file_changes)) + WHERE $failed_file_changes != 0 +)"; + + static constexpr const char VIEW_HEADER[] = R"( + +# The following commands will restore the state of the {} view. + +)"; + + auto prep_mark_res = prepare_stmt(lnav_db.in(), BOOKMARK_QUERY); + if (prep_mark_res.isErr()) { + return Err( + console::user_message::error("unable to export log bookmarks") + .with_reason(prep_mark_res.unwrapErr())); + } + + fmt::print(file, FMT_STRING(HEADER), sqlitepp::quote(PACKAGE_VERSION)); + + std::map<std::string, std::vector<std::string>> file_containers; + std::set<std::string> raw_files; + for (const auto& name_pair : lnav_data.ld_active_files.fc_file_names) { + const auto& open_opts = name_pair.second; + + if (!open_opts.loo_is_visible || !open_opts.loo_include_in_session + || open_opts.loo_temp_file + || open_opts.loo_source != logfile_name_source::USER) + { + continue; + } + + auto file_path_str = name_pair.first; + auto file_path = ghc::filesystem::path(file_path_str); + auto container_path_opt = find_container_dir(file_path); + if (container_path_opt) { + auto container_parent = container_path_opt.value().parent_path(); + auto file_container_path + = ghc::filesystem::relative(file_path, container_parent) + .string(); + file_containers[container_parent.string()].push_back( + file_container_path); + } else { + raw_files.insert(file_path_str); + } + } + for (const auto& file_path_str : raw_files) { + fmt::print( + file, FMT_STRING(":open {}\n"), replace_home_dir(file_path_str)); + } + size_t container_index = 0; + for (const auto& container_pair : file_containers) { + fmt::print(file, + FMT_STRING(LOG_DIR_INSERT), + container_index, + sqlitepp::quote(container_pair.first)); + for (const auto& file_path_str : container_pair.second) { + fmt::print(file, + FMT_STRING(":open $LOG_DIR_{}/{}\n"), + container_index, + file_path_str); + } + container_index += 1; + } + + fmt::print(file, FMT_STRING("\n:rebuild\n")); + + auto mark_count = 0; + auto each_mark_res + = prep_mark_res.unwrap().for_each_row<log_message_session_state>( + [file, &mark_count](const log_message_session_state& lmss) { + if (mark_count == 0) { + fmt::print(file, FMT_STRING(MARK_HEADER)); + } + mark_count += 1; + fmt::print(file, + FMT_STRING(";UPDATE all_logs " + "SET log_mark = {}, " + "log_comment = {}, " + "log_tags = {} " + "WHERE log_time_msecs = {} AND " + "log_format = {} AND " + "log_line_hash = {}\n"), + lmss.lmss_mark ? "1" : "0", + sqlitepp::quote(lmss.lmss_comment), + sqlitepp::quote(lmss.lmss_tags), + lmss.lmss_time_msecs, + sqlitepp::quote(lmss.lmss_format), + sqlitepp::quote(lmss.lmss_hash)); + return false; + }); + + if (each_mark_res.isErr()) { + return Err(console::user_message::error( + "failed to fetch bookmark metadata for log message") + .with_reason(each_mark_res.unwrapErr().fe_msg)); + } + + if (mark_count > 0) { + fmt::print(file, FMT_STRING(MARK_FOOTER), mark_count); + } + + auto prep_filter_res = prepare_stmt(lnav_db.in(), FILTER_QUERY); + if (prep_filter_res.isErr()) { + return Err(console::user_message::error("unable to export filter state") + .with_reason(prep_filter_res.unwrapErr())); + } + + auto added_filter_header = false; + auto each_filter_res + = prep_filter_res.unwrap().for_each_row<log_filter_session_state>( + [file, &added_filter_header](const log_filter_session_state& lfss) { + if (!added_filter_header) { + fmt::print(file, FMT_STRING("{}"), FILTER_HEADER); + added_filter_header = true; + } + fmt::print( + file, + FMT_STRING(";REPLACE INTO lnav_view_filters " + "(view_name, enabled, type, language, pattern) " + "VALUES ({}, {}, {}, {}, {})\n"), + sqlitepp::quote(lfss.lfss_name), + lfss.lfss_enabled ? 1 : 0, + sqlitepp::quote(lfss.lfss_type), + sqlitepp::quote(lfss.lfss_language), + sqlitepp::quote(lfss.lfss_pattern)); + return false; + }); + + if (each_filter_res.isErr()) { + return Err(console::user_message::error( + "failed to fetch filter state for views") + .with_reason(each_filter_res.unwrapErr().fe_msg)); + } + + auto prep_file_res = prepare_stmt(lnav_db.in(), FILE_QUERY); + if (prep_file_res.isErr()) { + return Err(console::user_message::error("unable to export file state") + .with_reason(prep_file_res.unwrapErr())); + } + + auto file_count = 0; + auto file_stmt = prep_file_res.unwrap(); + auto each_file_res = file_stmt.for_each_row<log_file_session_state>( + [file, &file_count](const log_file_session_state& lfss) { + if (file_count == 0) { + fmt::print(file, FMT_STRING("{}"), FILE_HEADER); + } + file_count += 1; + fmt::print(file, + FMT_STRING(";UPDATE lnav_file " + "SET time_offset = {} " + "WHERE content_id = {} AND format = {}\n"), + lfss.lfss_time_offset, + sqlitepp::quote(lfss.lfss_content_id), + sqlitepp::quote(lfss.lfss_format)); + return false; + }); + + if (each_file_res.isErr()) { + return Err(console::user_message::error("failed to fetch file state") + .with_reason(each_file_res.unwrapErr().fe_msg)); + } + + if (file_count > 0) { + fmt::print(file, FMT_STRING(FILE_FOOTER), file_count); + } + + for (auto view_index : {LNV_LOG, LNV_TEXT}) { + auto& tc = lnav_data.ld_views[view_index]; + if (tc.get_inner_height() == 0_vl) { + continue; + } + + fmt::print(file, FMT_STRING(VIEW_HEADER), lnav_view_titles[view_index]); + fmt::print(file, + FMT_STRING(":switch-to-view {}\n"), + lnav_view_strings[view_index]); + + auto* tss = tc.get_sub_source(); + auto* lss = dynamic_cast<logfile_sub_source*>(tss); + if (lss != nullptr) { + auto min_level = lss->get_min_log_level(); + + if (min_level != LEVEL_UNKNOWN) { + fmt::print(file, + FMT_STRING(":set-min-log-level {}\n"), + level_names[min_level]); + } + + struct timeval min_time, max_time; + char tsbuf[128]; + if (lss->get_min_log_time(min_time)) { + sql_strftime(tsbuf, sizeof(tsbuf), min_time, 'T'); + fmt::print(file, FMT_STRING(":hide-lines-before {}\n"), tsbuf); + } + if (lss->get_max_log_time(max_time)) { + sql_strftime(tsbuf, sizeof(tsbuf), max_time, 'T'); + fmt::print(file, FMT_STRING(":hide-lines-after {}\n"), tsbuf); + } + for (const auto& ld : *lss) { + if (ld->is_visible()) { + continue; + } + + if (ld->get_file_ptr()->get_open_options().loo_source + == logfile_name_source::ARCHIVE) + { + continue; + } + + auto container_path_opt + = find_container_dir(ld->get_file_ptr()->get_path()); + if (!container_path_opt) { + fmt::print(file, + FMT_STRING(":hide-file {}\n"), + ld->get_file_ptr()->get_path().string()); + continue; + } + auto container_parent + = container_path_opt.value().parent_path(); + auto file_container_path = ghc::filesystem::relative( + ld->get_file_ptr()->get_path(), container_parent); + fmt::print(file, + FMT_STRING(":hide-file */{}\n"), + file_container_path.string()); + } + } + + if (!tc.get_current_search().empty()) { + fmt::print(file, FMT_STRING("/{}\n"), tc.get_current_search()); + } + + fmt::print(file, FMT_STRING(":goto {}\n"), (int) tc.get_top()); + } + + return Ok(); +} + +} // namespace session +} // namespace lnav |