diff --git a/src/hictk/cli/cli.cpp b/src/hictk/cli/cli.cpp index 709549a2..dc88ff56 100644 --- a/src/hictk/cli/cli.cpp +++ b/src/hictk/cli/cli.cpp @@ -5,6 +5,7 @@ #include "hictk/tools/cli.hpp" #include +#include #include #include @@ -25,6 +26,13 @@ std::string_view Cli::get_printable_subcommand() const noexcept { return Cli::subcommand_to_str(get_subcommand()); } +void Cli::log_warnings() const noexcept { + for (const auto& w : _warnings) { + SPDLOG_WARN(FMT_STRING("{}"), w); + } + _warnings.clear(); +} + auto Cli::parse_arguments() -> Config { try { _cli.name(_exec_name); diff --git a/src/hictk/cli/cli_dump.cpp b/src/hictk/cli/cli_dump.cpp index f897db77..2988ded6 100644 --- a/src/hictk/cli/cli_dump.cpp +++ b/src/hictk/cli/cli_dump.cpp @@ -145,7 +145,6 @@ void Cli::make_dump_subcommand() { void Cli::validate_dump_subcommand() const { assert(_cli.get_subcommand("dump")->parsed()); - std::vector warnings; std::vector errors; const auto& c = std::get(_config); @@ -164,26 +163,26 @@ void Cli::validate_dump_subcommand() const { const auto resolution_parsed = !subcmd.get_option("--resolution")->empty(); if ((is_cooler || is_scool) && resolution_parsed) { - warnings.emplace_back("--resolution is ignored when file is in .[s]cool format."); + _warnings.emplace_back("--resolution is ignored when file is in .[s]cool format."); } const auto range_parsed = !subcmd.get_option("--range")->empty(); if (range_parsed && c.table != "chroms" && c.table != "bins" && c.table != "pixels" && c.table != "weights") { - warnings.emplace_back( + _warnings.emplace_back( "--range and --range2 are ignored when --table is not bins, chroms, pixels, or weights"); } const auto query_file_parsed = !subcmd.get_option("--query-file")->empty(); if (query_file_parsed && c.table != "bins" && c.table != "pixels") { - warnings.emplace_back("--query-file is ignored when --table is not bins or pixels"); + _warnings.emplace_back("--query-file is ignored when --table is not bins or pixels"); } const auto matrix_type_parsed = !subcmd.get_option("--matrix-type")->empty(); const auto matrix_unit_parsed = !subcmd.get_option("--matrix-unit")->empty(); if (!is_hic && (matrix_type_parsed || matrix_unit_parsed)) { - warnings.emplace_back( + _warnings.emplace_back( "--matrix-type and --matrix-unit are ignored when input file is not in .hic format."); } @@ -205,10 +204,6 @@ void Cli::validate_dump_subcommand() const { errors.emplace_back("--balance requires --table=pixels."); } - for (const auto& w : warnings) { - SPDLOG_WARN(FMT_STRING("{}"), w); - } - if (!errors.empty()) { throw std::runtime_error( fmt::format(FMT_STRING("the following error(s) where encountered while validating CLI " diff --git a/src/hictk/cli/cli_fix_mcool.cpp b/src/hictk/cli/cli_fix_mcool.cpp index be972ad9..80621eb3 100644 --- a/src/hictk/cli/cli_fix_mcool.cpp +++ b/src/hictk/cli/cli_fix_mcool.cpp @@ -103,7 +103,6 @@ void Cli::make_fix_mcool_subcommand() { void Cli::validate_fix_mcool_subcommand() const { const auto& c = std::get(_config); std::vector errors; - std::vector warnings{}; if (!c.force && std::filesystem::exists(c.path_to_output)) { errors.emplace_back(fmt::format( @@ -113,20 +112,20 @@ void Cli::validate_fix_mcool_subcommand() const { if (c.skip_balancing) { const auto* sc = _cli.get_subcommand("fix-mcool"); if (!sc->get_option("--tmpdir")->empty()) { - warnings.emplace_back("option --tmpdir is ignored when --skip-balancing is provided."); + _warnings.emplace_back("option --tmpdir is ignored when --skip-balancing is provided."); } if (!sc->get_option("--in-memory")->empty()) { - warnings.emplace_back("option --in-memory is ignored when --skip-balancing is provided."); + _warnings.emplace_back("option --in-memory is ignored when --skip-balancing is provided."); } if (!sc->get_option("--compression-lvl")->empty()) { - warnings.emplace_back( + _warnings.emplace_back( "option --compression-lvl is ignored when --skip-balancing is provided."); } if (!sc->get_option("--chunk-size")->empty()) { - warnings.emplace_back("option --chunk-size is ignored when --skip-balancing is provided."); + _warnings.emplace_back("option --chunk-size is ignored when --skip-balancing is provided."); } if (!sc->get_option("--threads")->empty()) { - warnings.emplace_back("option --threads is ignored when --skip-balancing is provided."); + _warnings.emplace_back("option --threads is ignored when --skip-balancing is provided."); } } @@ -137,10 +136,6 @@ void Cli::validate_fix_mcool_subcommand() const { FMT_STRING("fixing .mcool with storage-mode=\"{}\" is not supported"), *storage_mode)); } - for (const auto& w : warnings) { - SPDLOG_WARN(FMT_STRING("{}"), w); - } - if (!errors.empty()) { throw std::runtime_error(fmt::format( FMT_STRING( diff --git a/src/hictk/cli/cli_load.cpp b/src/hictk/cli/cli_load.cpp index ff120aa6..f413c859 100644 --- a/src/hictk/cli/cli_load.cpp +++ b/src/hictk/cli/cli_load.cpp @@ -197,7 +197,6 @@ void Cli::make_load_subcommand() { void Cli::validate_load_subcommand() const { assert(_cli.get_subcommand("load")->parsed()); - std::vector warnings; std::vector errors; const auto& c = std::get(_config); const auto& sc = *_cli.get_subcommand("load"); @@ -230,14 +229,10 @@ void Cli::validate_load_subcommand() const { } if (c.format == "4dn" && c.format == "validpairs" && c.assume_sorted) { - warnings.emplace_back( + _warnings.emplace_back( "--assume-sorted has no effect when ingesting interactions in 4dn or validpairs format."); } - for (const auto& w : warnings) { - SPDLOG_WARN(FMT_STRING("{}"), w); - } - if (!errors.empty()) { throw std::runtime_error( fmt::format(FMT_STRING("the following error(s) where encountered while validating CLI " diff --git a/src/hictk/cli/cli_zoomify.cpp b/src/hictk/cli/cli_zoomify.cpp index 5f8dd85f..b4e77a16 100644 --- a/src/hictk/cli/cli_zoomify.cpp +++ b/src/hictk/cli/cli_zoomify.cpp @@ -175,7 +175,6 @@ static std::vector detect_invalid_resolutions( void Cli::validate_zoomify_subcommand() const { assert(_cli.get_subcommand("zoomify")->parsed()); - std::vector warnings; std::vector errors; const auto& c = std::get(_config); @@ -206,7 +205,7 @@ void Cli::validate_zoomify_subcommand() const { if (base_resolution == 0) { // Variable bin size errors.clear(); - warnings.clear(); + _warnings.clear(); errors.emplace_back("Zoomifying files with variable bin size is currently not supported."); } else { if (const auto dupl = detect_duplicate_resolutions(c.resolutions); !dupl.empty()) { @@ -226,16 +225,12 @@ void Cli::validate_zoomify_subcommand() const { const auto nice_or_pow2_steps_parsed = !sc->get_option("--nice-steps")->empty() || !sc->get_option("--pow2-steps")->empty(); if (!c.resolutions.empty() && nice_or_pow2_steps_parsed) { - warnings.emplace_back( + _warnings.emplace_back( "--nice-steps and --pow2-steps are ignored when resolutions are explicitly set with " "--resolutions."); } } - for (const auto& w : warnings) { - SPDLOG_WARN(FMT_STRING("{}"), w); - } - if (!errors.empty()) { throw std::runtime_error( fmt::format(FMT_STRING("the following error(s) where encountered while validating CLI " diff --git a/src/hictk/include/hictk/tools/cli.hpp b/src/hictk/include/hictk/tools/cli.hpp index f4505c03..245559cb 100644 --- a/src/hictk/include/hictk/tools/cli.hpp +++ b/src/hictk/include/hictk/tools/cli.hpp @@ -220,6 +220,7 @@ class Cli { [[nodiscard]] int exit(const CLI::ParseError& e) const; [[nodiscard]] int exit() const noexcept; [[nodiscard]] static std::string_view subcommand_to_str(subcommand s) noexcept; + void log_warnings() const noexcept; private: int _argc; @@ -229,6 +230,7 @@ class Cli { Config _config{}; CLI::App _cli{}; subcommand _subcommand{subcommand::help}; + mutable std::vector _warnings{}; void make_balance_subcommand(); void make_ice_balance_subcommand(CLI::App& app); diff --git a/src/hictk/main.cpp b/src/hictk/main.cpp index 14873a0d..c1b8f8a7 100644 --- a/src/hictk/main.cpp +++ b/src/hictk/main.cpp @@ -3,19 +3,20 @@ // SPDX-License-Identifier: MIT #include -#include -#include +#include +#include #include #include -#include +#include #include +#include #include -#include -#include +#include #include -#include +#include #include +#include #include #include "hictk/tools/cli.hpp" @@ -25,51 +26,178 @@ using namespace hictk::tools; -static void setup_logger_console() { - auto stderr_sink = std::make_shared(); - // [2021-08-12 17:49:34.581] [info]: my log msg - stderr_sink->set_pattern("[%Y-%m-%d %T.%e] %^[%l]%$: %v"); +template +class GlobalLogger { + // [2021-08-12 17:49:34.581] [info]: my log msg + static constexpr auto *_msg_pattern{"[%Y-%m-%d %T.%e] %^[%l]%$: %v"}; + using HictkLogMsg = std::pair; + std::deque _msg_buffer{}; - auto main_logger = std::make_shared("main_logger", stderr_sink); + std::mutex _mtx; + std::atomic _num_msg_enqueued{}; + std::atomic _ok{false}; - spdlog::set_default_logger(main_logger); -} + [[nodiscard]] static std::shared_ptr init_stderr_sink() { + auto stderr_sink = std::make_shared(); + stderr_sink->set_pattern(_msg_pattern); + stderr_sink->set_level(spdlog::level::debug); -static void setup_logger_console(int verbosity_lvl, bool print_version) { - spdlog::set_level(spdlog::level::level_enum(verbosity_lvl)); - for (auto& sink : spdlog::default_logger()->sinks()) { - sink->set_level(spdlog::level::level_enum(verbosity_lvl)); + return stderr_sink; } - if (print_version) { - SPDLOG_INFO(FMT_STRING("Running hictk v{}"), hictk::config::version::str()); + [[nodiscard]] std::shared_ptr init_callback_sink() { + if constexpr (CAPACITY != 0 && SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_WARN) { + auto callback_sink = std::make_shared( + [this](const spdlog::details::log_msg &msg) noexcept { enqueue_msg(msg); }); + callback_sink->set_pattern(_msg_pattern); + callback_sink->set_level(spdlog::level::warn); + + return callback_sink; + } + + return {}; + } + + template + void print_noexcept(fmt::format_string fmt, T &&...args) noexcept { + try { + fmt::print(stderr, fmt, std::forward(args)...); + } catch (...) { // NOLINT + _ok = false; + } + } + + void enqueue_msg(const spdlog::details::log_msg &msg) noexcept { + if (msg.level < spdlog::level::warn) [[likely]] { + return; + } + + ++_num_msg_enqueued; + + try { + [[maybe_unused]] const std::scoped_lock lck(_mtx); + if (_msg_buffer.size() == CAPACITY) [[unlikely]] { + _msg_buffer.pop_front(); + } + _msg_buffer.emplace_back(msg.level, std::string{msg.payload.begin(), msg.payload.end()}); + } catch (...) { // NOLINT + } + } + + void replay_warnings() { + [[maybe_unused]] const std::scoped_lock lck(_mtx); + if (_msg_buffer.empty()) { + return; + } + auto stderr_sink = init_stderr_sink(); + stderr_sink->set_level(spdlog::level::warn); + + auto logger = std::make_shared("tmp_logger", std::move(stderr_sink)); + logger->set_level(spdlog::level::warn); + + if (_num_msg_enqueued <= _msg_buffer.size()) { + logger->warn(FMT_STRING("replaying the last {} warning message(s)"), + _num_msg_enqueued.load()); + } else { + logger->warn(FMT_STRING("replaying the last {}/{} warning messages"), _msg_buffer.size(), + _num_msg_enqueued.load()); + } + for (const auto &msg : _msg_buffer) { + logger->log(msg.first, msg.second); + } + _msg_buffer.clear(); } -} -static std::tuple parse_cli_and_setup_logger(Cli& cli) { + public: + GlobalLogger() noexcept { + try { + spdlog::set_default_logger(std::make_shared( + "main_logger", spdlog::sinks_init_list{init_stderr_sink(), init_callback_sink()})); + _ok = true; + } catch (const std::exception &e) { + print_noexcept(FMT_STRING("FAILURE! Failed to setup hictk's logger: {}"), e.what()); + } catch (...) { + print_noexcept(FMT_STRING("FAILURE! Failed to setup hictk's logger: unknown error")); + } + } + + GlobalLogger(const GlobalLogger &other) = delete; + GlobalLogger(GlobalLogger &&other) noexcept = default; + + GlobalLogger &operator=(const GlobalLogger &other) = delete; + GlobalLogger &operator=(GlobalLogger &&other) noexcept = default; + + ~GlobalLogger() noexcept { + if (!_ok) { + return; + } + + try { + replay_warnings(); + } catch (const std::exception &e) { + print_noexcept(FMT_STRING("FAILURE! Failed to replay hictk warnings: {}"), e.what()); + } catch (...) { + print_noexcept(FMT_STRING("FAILURE! Failed to replay hictk warnings: unknown error")); + } + } + + static void set_level(int lvl) { + if (auto logger = spdlog::default_logger(); logger) { + for (auto &sink : logger->sinks()) { + sink->set_level(std::max(sink->level(), spdlog::level::level_enum{lvl})); + } + logger->set_level(spdlog::level::level_enum{lvl}); + } + } + + void print_welcome_msg() { + if (_ok) { + SPDLOG_INFO(FMT_STRING("Running hictk v{}"), hictk::config::version::str()); + } + } + + [[nodiscard]] constexpr bool ok() const noexcept { return _ok; } + + void clear() noexcept { + if (_ok) { + _msg_buffer.clear(); + _num_msg_enqueued = 0; + } + } +}; + +// NOLINTNEXTLINE(*-err58-cpp, *-avoid-non-const-global-variables) +static auto global_logger = std::make_unique>(); + +static auto acquire_global_logger() noexcept { return std::move(global_logger); } + +static std::tuple parse_cli_and_setup_logger(Cli &cli) { try { auto config = cli.parse_arguments(); const auto subcmd = cli.get_subcommand(); std::visit( - [&](const auto& config_) { + [&](const auto &config_) { using T = hictk::remove_cvref_t; - constexpr auto is_monostate = std::is_same_v; - if constexpr (!is_monostate) { - setup_logger_console(config_.verbosity, subcmd != Cli::subcommand::help && - subcmd != Cli::subcommand::dump); + if constexpr (!std::is_same_v) { + if (global_logger && global_logger->ok()) { + global_logger->set_level(config_.verbosity); // NOLINT + if (subcmd != Cli::subcommand::help && subcmd != Cli::subcommand::dump) { + global_logger->print_welcome_msg(); + } + } } }, config); return std::make_tuple(cli.exit(), subcmd, config); - } catch (const CLI::ParseError& e) { + } catch (const CLI::ParseError &e) { // This takes care of formatting and printing error messages (if any) return std::make_tuple(cli.exit(e), Cli::subcommand::help, Config()); - } catch (const std::filesystem::filesystem_error& e) { + } catch (const std::filesystem::filesystem_error &e) { SPDLOG_ERROR(FMT_STRING("FAILURE! {}"), e.what()); return std::make_tuple(1, Cli::subcommand::help, Config()); - } catch (const spdlog::spdlog_ex& e) { + } catch (const spdlog::spdlog_ex &e) { fmt::print( stderr, FMT_STRING( @@ -80,18 +208,23 @@ static std::tuple parse_cli_and_setup_logger(Cli& } // NOLINTNEXTLINE(bugprone-exception-escape) -int main(int argc, char** argv) noexcept { +int main(int argc, char **argv) noexcept { std::unique_ptr cli{nullptr}; - std::ios::sync_with_stdio(false); - std::cin.tie(nullptr); try { - setup_logger_console(); + std::ios::sync_with_stdio(false); + std::cin.tie(nullptr); + + auto local_logger = acquire_global_logger(); cli = std::make_unique(argc, argv); const auto [ec, subcmd, config] = parse_cli_and_setup_logger(*cli); if (ec != 0 || subcmd == Cli::subcommand::help) { + local_logger->clear(); return ec; } + + cli->log_warnings(); + using sc = Cli::subcommand; switch (subcmd) { case sc::balance: @@ -128,13 +261,13 @@ int main(int argc, char** argv) noexcept { "Default branch in switch statement in hictk::main() should be unreachable! " "If you see this message, please file an issue on GitHub"); } - } catch (const CLI::ParseError& e) { + } catch (const CLI::ParseError &e) { assert(cli); return cli->exit(e); // This takes care of formatting and printing error messages (if any) - } catch (const std::bad_alloc& err) { + } catch (const std::bad_alloc &err) { fmt::print(stderr, FMT_STRING("FAILURE! Unable to allocate enough memory: {}\n"), err.what()); return 1; - } catch (const std::exception& e) { + } catch (const std::exception &e) { if (cli) { fmt::print(stderr, FMT_STRING("FAILURE! hictk {} encountered the following error: {}\n"), cli->get_printable_subcommand(), e.what());