diff --git a/CMakeLists.txt b/CMakeLists.txt index c39d2f4..317177d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -221,6 +221,7 @@ target_sources( src/unwind/unwind_with_unwind.cpp src/unwind/unwind_with_winapi.cpp src/unwind/unwind_with_dbghelp.cpp + src/snippets/snippet.cpp ) target_include_directories( diff --git a/README.md b/README.md index 04d462d..1a04932 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,9 @@ namespace cpptrace { `cpptrace::demangle` provides a helper function for name demangling, since it has to implement that helper internally anyways. +`cpptrace::get_snippet` gets a text snippet, if possible, from for the given source file for +/- `context_size` lines +around `line`. + `cpptrace::isatty` and the fileno definitions are useful for deciding whether to use color when printing stack traces. `cpptrace::register_terminate_handler()` is a helper function to set a custom `std::terminate` handler that prints a @@ -259,6 +262,12 @@ stack trace from a cpptrace exception (more info below) and otherwise behaves li ```cpp namespace cpptrace { std::string demangle(const std::string& name); + std::string get_snippet( + const std::string& path, + std::size_t line, + std::size_t context_size, + bool color = false + ); bool isatty(int fd); extern const int stdin_fileno; diff --git a/include/cpptrace/cpptrace.hpp b/include/cpptrace/cpptrace.hpp index b41ad65..19d4c38 100644 --- a/include/cpptrace/cpptrace.hpp +++ b/include/cpptrace/cpptrace.hpp @@ -179,6 +179,9 @@ namespace cpptrace { void print() const; void print(std::ostream& stream) const; void print(std::ostream& stream, bool color) const; + void print_with_snippets() const; + void print_with_snippets(std::ostream& stream) const; + void print_with_snippets(std::ostream& stream, bool color) const; void clear(); bool empty() const noexcept; std::string to_string(bool color = false) const; @@ -194,6 +197,7 @@ namespace cpptrace { inline const_iterator cend() const noexcept { return frames.cend(); } private: void print(std::ostream& stream, bool color, bool newline_at_end, const char* header) const; + void print_with_snippets(std::ostream& stream, bool color, bool newline_at_end, const char* header) const; friend void print_terminate_trace(); }; @@ -236,6 +240,12 @@ namespace cpptrace { // utilities: CPPTRACE_EXPORT std::string demangle(const std::string& name); + CPPTRACE_EXPORT std::string get_snippet( + const std::string& path, + std::size_t line, + std::size_t context_size, + bool color = false + ); CPPTRACE_EXPORT bool isatty(int fd); CPPTRACE_EXPORT extern const int stdin_fileno; diff --git a/src/cpptrace.cpp b/src/cpptrace.cpp index 0bba379..72f59dd 100644 --- a/src/cpptrace.cpp +++ b/src/cpptrace.cpp @@ -20,15 +20,7 @@ #include "utils/utils.hpp" #include "binary/object.hpp" #include "binary/safe_dl.hpp" - -#define ESC "\033[" -#define RESET ESC "0m" -#define RED ESC "31m" -#define GREEN ESC "32m" -#define YELLOW ESC "33m" -#define BLUE ESC "34m" -#define MAGENTA ESC "35m" -#define CYAN ESC "36m" +#include "snippets/snippet.hpp" namespace cpptrace { CPPTRACE_FORCE_NO_INLINE @@ -173,10 +165,10 @@ namespace cpptrace { std::size_t counter, const stacktrace_frame& frame ) { - const auto reset = color ? ESC "0m" : ""; - const auto green = color ? ESC "32m" : ""; - const auto yellow = color ? ESC "33m" : ""; - const auto blue = color ? ESC "34m" : ""; + const auto reset = color ? RESET : ""; + const auto green = color ? GREEN : ""; + const auto yellow = color ? YELLOW : ""; + const auto blue = color ? BLUE : ""; stream << '#' << std::setw(static_cast(frame_number_width)) @@ -253,6 +245,45 @@ namespace cpptrace { } } + void stacktrace::print_with_snippets() const { + print_with_snippets(std::cerr, true); + } + + void stacktrace::print_with_snippets(std::ostream& stream) const { + print_with_snippets(stream, true); + } + + void stacktrace::print_with_snippets(std::ostream& stream, bool color) const { + print_with_snippets(stream, color, true, nullptr); + } + + void stacktrace::print_with_snippets(std::ostream& stream, bool color, bool newline_at_end, const char* header) const { + if( + color && ( + (&stream == &std::cout && isatty(stdout_fileno)) || (&stream == &std::cerr && isatty(stderr_fileno)) + ) + ) { + detail::enable_virtual_terminal_processing_if_needed(); + } + stream<<(header ? header : "Stack trace (most recent call first):") << '\n'; + std::size_t counter = 0; + if(frames.empty()) { + stream<<"" << '\n'; + return; + } + const auto frame_number_width = detail::n_digits(static_cast(frames.size()) - 1); + for(const auto& frame : frames) { + print_frame(stream, color, frame_number_width, counter, frame); + if(newline_at_end || &frame != &frames.back()) { + stream << '\n'; + } + if(frame.line.has_value() && !frame.filename.empty()) { + stream << detail::get_snippet(frame.filename, frame.line.value(), 2, color); + } + counter++; + } + } + void stacktrace::clear() { frames.clear(); } @@ -368,6 +399,10 @@ namespace cpptrace { return detail::demangle(name); } + std::string get_snippet(const std::string& path, std::size_t line, std::size_t context_size, bool color) { + return detail::get_snippet(path, line, context_size, color); + } + bool isatty(int fd) { return detail::isatty(fd); } diff --git a/src/snippets/snippet.cpp b/src/snippets/snippet.cpp new file mode 100644 index 0000000..f34c308 --- /dev/null +++ b/src/snippets/snippet.cpp @@ -0,0 +1,129 @@ +#include "snippet.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../utils/common.hpp" + +namespace cpptrace { +namespace detail { + constexpr std::int64_t max_size = 1024 * 1024 * 10; // 10 MiB + + class snippet_manager { + bool loaded_contents; + std::string contents; + // for index i, gives the index in `contents` of one past the end of the line (i.e. the \n or contents.end()) + std::vector line_table; + public: + snippet_manager(const std::string& path) : loaded_contents(false) { + std::ifstream file; + try { + file.open(path, std::ios::ate); + if(file.is_open()) { + std::ifstream::pos_type size = file.tellg(); + if(size == std::ifstream::pos_type(-1) || size > max_size) { + return; + } + // else load file + file.seekg(0, std::ios::beg); + contents.resize(size); + if(!file.read(&contents[0], size)) { + // error ... + } + build_line_table(); + loaded_contents = true; + } + } catch(const std::ifstream::failure&) { + // ... + } + } + + std::string get_line(std::size_t line) const { // 0-indexed line TODO: reconsider + if(!loaded_contents || line >= line_table.size()) { + return ""; + } else if(line == 0) { + return contents.substr(0, line_table[line]); + } else { + return contents.substr(line_table[line - 1] + 1, line_table[line] - line_table[line - 1] - 1); + } + } + + std::size_t num_lines() const { + return line_table.size(); + } + + bool ok() const { + return loaded_contents; + } + private: + void build_line_table() { + std::size_t pos = 0; + while(true) { + std::size_t new_pos = contents.find('\n', pos); + if(new_pos == std::string::npos) { + line_table.push_back(contents.size()); + break; + } else { + line_table.push_back(new_pos); + pos = new_pos + 1; + } + } + } + }; + + std::mutex snippet_manager_mutex; + std::unordered_map snippet_managers; + + const snippet_manager& get_manager(const std::string& path) { + std::unique_lock lock(snippet_manager_mutex); + auto it = snippet_managers.find(path); + if(it == snippet_managers.end()) { + return snippet_managers.insert({path, snippet_manager(path)}).first->second; + } else { + return it->second; + } + } + + std::string get_snippet(const std::string& path, std::size_t target_line, std::size_t context_size, bool color) { + target_line--; + const auto& manager = get_manager(path); + if(!manager.ok()) { + return ""; + } + auto begin = target_line <= context_size ? 0 : target_line - context_size; + auto original_begin = begin; + auto end = std::min(target_line + context_size, manager.num_lines() - 1); + std::vector lines; + for(auto line = begin; line <= end; line++) { + lines.push_back(manager.get_line(line)); + } + // trim blank lines + while(begin < target_line && lines[begin - original_begin].empty()) { + begin++; + } + while(end > target_line && lines[end - original_begin].empty()) { + end--; + } + // make the snippet + std::string snippet; + constexpr std::size_t margin_width = 8; + for(auto line = begin; line <= end; line++) { + if(color && line == target_line) { + snippet += YELLOW; + } + auto line_str = std::to_string(line); + snippet += std::string(margin_width - line_str.size(), ' ') + line_str + ": "; + if(color && line == target_line) { + snippet += RESET; + } + snippet += lines[line - original_begin] + "\n"; + } + return snippet; + } +} +} diff --git a/src/snippets/snippet.hpp b/src/snippets/snippet.hpp new file mode 100644 index 0000000..cad7cc5 --- /dev/null +++ b/src/snippets/snippet.hpp @@ -0,0 +1,14 @@ +#ifndef SNIPPET_HPP +#define SNIPPET_HPP + +#include +#include + +namespace cpptrace { +namespace detail { + // 1-indexed line + std::string get_snippet(const std::string& path, std::size_t line, std::size_t context_size, bool color); +} +} + +#endif diff --git a/src/utils/common.hpp b/src/utils/common.hpp index c7cad62..d091afa 100644 --- a/src/utils/common.hpp +++ b/src/utils/common.hpp @@ -41,6 +41,15 @@ #include +#define ESC "\033[" +#define RESET ESC "0m" +#define RED ESC "31m" +#define GREEN ESC "32m" +#define YELLOW ESC "33m" +#define BLUE ESC "34m" +#define MAGENTA ESC "35m" +#define CYAN ESC "36m" + namespace cpptrace { namespace detail { // Placed here instead of utils because it's used by error.hpp and utils.hpp diff --git a/test/demo.cpp b/test/demo.cpp index 228bc1e..88c54a9 100644 --- a/test/demo.cpp +++ b/test/demo.cpp @@ -7,6 +7,7 @@ void trace() { cpptrace::generate_trace().print(); + cpptrace::generate_trace().print_with_snippets(); throw cpptrace::logic_error("foobar"); }