diff --git a/NEWS b/NEWS index 4737cc8c..f337bad5 100644 --- a/NEWS +++ b/NEWS @@ -29,6 +29,13 @@ lnav v0.8.0: * When typing in a command, the status bar will display a short summary of the currently entered command. * Added a "delete-filter" command. + * The 'goto' command now supports relative time values like + 'a minute ago', 'an hour later', and many more. + + Interface Changes: + * The 'r/R' hotkeys have been reassigned to navigate through the log + messages by the relative time value that was last used with the + 'goto' command. Fixes: * The pretty-print view should now work for text files. diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 02c29084..290c2daf 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -29,8 +29,9 @@ with the following commands: Navigation ---------- -* goto - Go to the given line number, N percent into the - file, or the given timestamp in the log view. +* goto - Go to the given line number, N + percent into the file, the given timestamp in the log view, or by the + relative time (e.g. 'a minute ago'). * relative-goto - Move the current view up or down by the given amount. * next-mark error|warning|search|user|file|partition - Move to the next diff --git a/docs/source/hotkeys.rst b/docs/source/hotkeys.rst index 5c892a9e..ff95f318 100644 --- a/docs/source/hotkeys.rst +++ b/docs/source/hotkeys.rst @@ -146,6 +146,9 @@ Chronological Navigation * - |ks| 0 |ke| - |ks| Shift |ke| + |ks| 0 |ke| - Next/previous day + * - |ks| r |ke| + - |ks| Shift |ke| + |ks| r |ke| + - Forward/backward by the relative time that was last used with the goto command. Bookmarks --------- diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a7fbaf58..9550c9c4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,6 +38,7 @@ set(diag_STAT_SRCS readline_curses.cc readline_highlighters.cc readline_possibilities.cc + relative_time.cc session_data.cc sequence_matcher.cc shared_buffer.cc @@ -108,6 +109,7 @@ set(diag_STAT_SRCS pthreadpp.hh readline_callbacks.hh readline_possibilities.hh + relative_time.hh sequence_sink.hh status_controllers.hh strong_int.hh diff --git a/src/Makefile.am b/src/Makefile.am index b11ce93f..0b657695 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -151,6 +151,7 @@ noinst_HEADERS = \ readline_curses.hh \ readline_highlighters.hh \ readline_possibilities.hh \ + relative_time.hh \ sequence_matcher.hh \ sequence_sink.hh \ session_data.hh \ @@ -235,6 +236,7 @@ libdiag_a_SOURCES = \ readline_curses.cc \ readline_highlighters.cc \ readline_possibilities.cc \ + relative_time.cc \ session_data.cc \ sequence_matcher.cc \ shared_buffer.cc \ diff --git a/src/help.txt b/src/help.txt index 5e4c84d2..48c0b1c0 100644 --- a/src/help.txt +++ b/src/help.txt @@ -207,6 +207,13 @@ through the file. 0/Shift 0 Move to the next/previous day boundary. + r/R Move forward/backward based on the relative time that + was last used with the 'goto' command. For example, + executing ':goto a minute later' will move the log view + forward a minute and then pressing 'r' will move it + forward a minute again. Pressing 'R' will then move the + view in the opposite direction, so backwards a minute. + m Mark/unmark the line at the top of the display. The line will be highlighted with reverse video to indicate that it is a user bookmark. You can use @@ -345,9 +352,6 @@ through the file. CTRL-W Toggle word-wrapping. - r/R Restore the next/previous session. The current session is - saved and then the new state is restored. - F2 Toggle mouse support. @@ -383,11 +387,15 @@ COMMANDS current-time Print the current time in human-readable form and as a unix-timestamp. - goto + goto Go to the given line number, N percent into the file, or the given timestamp in the log view. If the line number is negative, it is considered an offset - from the last line. + from the last line. Relative time values, like + 'a minute ago', 'an hour later', and many other formats + are supported. When using a relative time, the 'r/R' + hotkeys can be used to move the same amount again or in + the same amount in the opposite direction. relative-goto Move the current view up or down by the given amount. diff --git a/src/hotkeys.cc b/src/hotkeys.cc index b728dbc3..fdba526b 100644 --- a/src/hotkeys.cc +++ b/src/hotkeys.cc @@ -1030,35 +1030,44 @@ void handle_paging_key(int ch) break; case 'r': - if (!lnav_data.ld_session_file_names.empty()) { - lnav_data.ld_session_file_index = - (lnav_data.ld_session_file_index + 1) % - lnav_data.ld_session_file_names.size(); - reset_session(); - load_session(); - rebuild_indexes(true); - } - break; - case 'R': - if (lnav_data.ld_session_file_index == 0) { - lnav_data.ld_session_file_index = - lnav_data.ld_session_file_names.size() - 1; + if (lss) { + if (lnav_data.ld_last_relative_time.empty()) { + lnav_data.ld_rl_view->set_value( + "Use the 'goto' command to set the relative time to move by"); + } + else { + vis_line_t vl = tc->get_top(); + relative_time rt = lnav_data.ld_last_relative_time; + struct timeval tv; + content_line_t cl; + struct exttm tm; + + if (ch == 'R') { + rt.negate(); + } + + cl = lnav_data.ld_log_source.at(vl); + logline *ll = lnav_data.ld_log_source.find_line(cl); + ll->to_exttm(tm); + rt.add(tm); + tv.tv_sec = timegm(&tm.et_tm); + tv.tv_usec = tm.et_nsec / 1000; + vl = lnav_data.ld_log_source.find_from_time(tv); + if (rt.is_negative() && (vl > vis_line_t(0))) { + --vl; + if (vl == tc->get_top()) { + vl = vis_line_t(0); + } + } + tc->set_top(vl); + } } - else{ - lnav_data.ld_session_file_index -= 1; - } - reset_session(); - load_session(); - rebuild_indexes(true); break; case KEY_CTRL_R: reset_session(); rebuild_indexes(true); - lnav_data.ld_rl_view->set_alt_value(HELP_MSG_2( - r, R, - "to restore the next/previous session")); break; case KEY_CTRL_W: diff --git a/src/lnav.hh b/src/lnav.hh index 742c0f63..09535f01 100644 --- a/src/lnav.hh +++ b/src/lnav.hh @@ -61,6 +61,7 @@ #include "ansi_scrubber.hh" #include "curl_looper.hh" #include "papertrail_proc.hh" +#include "relative_time.hh" /** The command modes that are available while viewing a file. */ typedef enum { @@ -248,6 +249,8 @@ struct _lnav_data { input_state_tracker ld_input_state; curl_looper ld_curl_looper; + + relative_time ld_last_relative_time; }; extern struct _lnav_data lnav_data; diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index 5d40aa97..354585ab 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -49,6 +49,7 @@ #include "command_executor.hh" #include "url_loader.hh" #include "readline_curses.hh" +#include "relative_time.hh" #include "log_search_table.hh" using namespace std; @@ -249,20 +250,52 @@ static string com_current_time(string cmdline, vector &args) static string com_goto(string cmdline, vector &args) { - string retval = "error: expecting line number/percentage or timestamp"; + string retval = "error: expecting line number/percentage, timestamp, or relative time"; if (args.size() == 0) { args.push_back("line-time"); } else if (args.size() > 1) { + string all_args = cmdline.substr(cmdline.find(args[1], args[0].size())); textview_curses *tc = lnav_data.ld_view_stack.top(); int line_number, consumed; date_time_scanner dts; + struct relative_time::parse_error pe; + relative_time rt; struct timeval tv; struct exttm tm; float value; - if (dts.scan(args[1].c_str(), args[1].size(), NULL, &tm, tv) != NULL) { + if (rt.parse(all_args, pe)) { + if (tc == &lnav_data.ld_views[LNV_LOG]) { + content_line_t cl; + vis_line_t vl; + logline *ll; + + if (!rt.is_absolute()) { + lnav_data.ld_last_relative_time = rt; + } + + vl = tc->get_top(); + cl = lnav_data.ld_log_source.at(vl); + ll = lnav_data.ld_log_source.find_line(cl); + ll->to_exttm(tm); + rt.add(tm); + tv.tv_sec = timegm(&tm.et_tm); + tv.tv_usec = tm.et_nsec / 1000; + + vl = lnav_data.ld_log_source.find_from_time(tv); + tc->set_top(vl); + retval = ""; + if (!rt.is_absolute() && lnav_data.ld_rl_view != NULL) { + lnav_data.ld_rl_view->set_alt_value( + HELP_MSG_2(r, R, "to move forward/backward the same amount of time")); + } + } else { + retval = "error: relative time values only work in the log view"; + } + } + else if (dts.scan(args[1].c_str(), args[1].size(), NULL, &tm, tv) != NULL) { if (tc == &lnav_data.ld_views[LNV_LOG]) { vis_line_t vl; @@ -751,6 +784,7 @@ static string com_highlight(string cmdline, vector &args) } retval = "info: highlight pattern now active"; + tc->reload_data(); } } @@ -777,6 +811,7 @@ static string com_clear_highlight(string cmdline, vector &args) else { hm.erase(hm_iter); retval = "info: highlight pattern cleared"; + tc->reload_data(); } } @@ -1945,10 +1980,6 @@ static string com_redraw(string cmdline, vector &args) } else if (lnav_data.ld_window) { redrawwin(lnav_data.ld_window); - if (lnav_data.ld_rl_view != NULL) { - lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1( - CTRL-L, "to redraw the window")); - } } return ""; diff --git a/src/lnav_util.cc b/src/lnav_util.cc index cd38a43f..d4d85e38 100644 --- a/src/lnav_util.cc +++ b/src/lnav_util.cc @@ -430,6 +430,7 @@ const char *date_time_scanner::scan(const char *time_dest, } tv_out.tv_sec = gmt; tv_out.tv_usec = 0; + tm_out->et_flags = ETF_DAY_SET|ETF_MONTH_SET|ETF_YEAR_SET; this->dts_fmt_lock = curr_time_fmt; this->dts_fmt_len = off; diff --git a/src/log_format.cc b/src/log_format.cc index 40ed948f..4d2628e4 100644 --- a/src/log_format.cc +++ b/src/log_format.cc @@ -288,40 +288,46 @@ const char *log_format::log_scanf(const char *line, return retval; } -void log_format::check_for_new_year(std::vector &dst, - const struct timeval &log_tv) +void log_format::check_for_new_year(std::vector &dst, exttm etm, + struct timeval log_tv) { if (dst.empty()) { return; } time_t diff = dst.back().get_time() - log_tv.tv_sec; + int off_year = 0, off_month = 0, off_day = 0, off_hour = 0; + std::vector::iterator iter; + bool do_change = true; - if (diff > (5 * 60)) { - int off_year = 0, off_month = 0, off_day = 0, off_hour = 0; - std::vector::iterator iter; + if (diff <= 0) { + return; + } + if (diff > (60 * 24 * 60 * 60)) { + off_year = 1; + } else if (diff > (15 * 24 * 60 * 60)) { + off_month = 1; + } else if (diff > (12 * 60 * 60)) { + off_day = 1; + } else if (!(etm.et_flags & ETF_DAY_SET)) { + off_hour = 1; + } else { + do_change = false; + } - if (diff > (60 * 24 * 60 * 60)) { - off_year = 1; - } else if (diff > (15 * 24 * 60 * 60)) { - off_month = 1; - } else if (diff > (12 * 60 * 60)) { - off_day = 1; - } else { - off_hour = 1; - } + if (!do_change) { + return; + } + for (iter = dst.begin(); iter != dst.end(); iter++) { + time_t ot = iter->get_time(); + struct tm otm; - for (iter = dst.begin(); iter != dst.end(); iter++) { - time_t ot = iter->get_time(); - struct tm otm; - - gmtime_r(&ot, &otm); - otm.tm_year -= off_year; - otm.tm_mon -= off_month; - otm.tm_yday -= off_day; - otm.tm_hour -= off_hour; - iter->set_time(tm2sec(&otm)); - } + gmtime_r(&ot, &otm); + otm.tm_year -= off_year; + otm.tm_mon -= off_month; + otm.tm_yday -= off_day; + otm.tm_hour -= off_hour; + iter->set_time(tm2sec(&otm)); } } @@ -706,7 +712,7 @@ bool external_log_format::scan(std::vector &dst, if (!((log_time_tm.et_flags & ETF_DAY_SET) && (log_time_tm.et_flags & ETF_MONTH_SET) && (log_time_tm.et_flags & ETF_YEAR_SET))) { - this->check_for_new_year(dst, log_tv); + this->check_for_new_year(dst, log_time_tm, log_tv); } if (mod_cap != NULL) { diff --git a/src/log_format.hh b/src/log_format.hh index 33587c48..cc0e5057 100644 --- a/src/log_format.hh +++ b/src/log_format.hh @@ -150,6 +150,11 @@ public: /** @return The timestamp for the line. */ time_t get_time() const { return this->ll_time; }; + void to_exttm(struct exttm &tm_out) const { + tm_out.et_tm = *gmtime(&this->ll_time); + tm_out.et_nsec = this->ll_millis * 1000 * 1000; + }; + void set_time(time_t t) { this->ll_time = t; }; /** @return The millisecond timestamp for the line. */ @@ -255,7 +260,8 @@ public: bool operator<(const struct timeval &rhs) const { return ((this->ll_time < rhs.tv_sec) || - (this->ll_millis < (rhs.tv_usec / 1000))); + ((this->ll_time == rhs.tv_sec) && + (this->ll_millis < (rhs.tv_usec / 1000)))); }; private: @@ -654,8 +660,8 @@ public: return &this->lf_timestamp_format[0]; }; - void check_for_new_year(std::vector &dst, - const struct timeval &log_tv); + void check_for_new_year(std::vector &dst, exttm log_tv, + timeval timeval1); virtual std::string get_pattern_name() const { char name[32]; diff --git a/src/log_format_impls.cc b/src/log_format_impls.cc index 4aed114a..219f40ab 100644 --- a/src/log_format_impls.cc +++ b/src/log_format_impls.cc @@ -154,7 +154,7 @@ class generic_log_format : public log_format { logline::level_t level_val = logline::string2level( level_str, level.length()); - this->check_for_new_year(dst, log_tv); + this->check_for_new_year(dst, log_time, log_tv); dst.push_back(logline(offset, log_tv, level_val)); retval = true; diff --git a/src/ptimec.hh b/src/ptimec.hh index 3aae7856..602ef138 100644 --- a/src/ptimec.hh +++ b/src/ptimec.hh @@ -175,6 +175,7 @@ inline bool ptime_s(struct exttm *dst, const char *str, off_t &off_inout, ssize_ } secs2tm(&epoch, &dst->et_tm); + dst->et_flags = ETF_DAY_SET|ETF_MONTH_SET|ETF_YEAR_SET; return (epoch > 0); } @@ -241,6 +242,7 @@ inline bool ptime_i(struct exttm *dst, const char *str, off_t &off_inout, ssize_ dst->et_nsec = (epoch_ms % 1000ULL) * 1000000; epoch = (epoch_ms / 1000ULL); secs2tm(&epoch, &dst->et_tm); + dst->et_flags = ETF_DAY_SET|ETF_MONTH_SET|ETF_YEAR_SET; return (epoch_ms > 0); } diff --git a/src/relative_time.cc b/src/relative_time.cc new file mode 100644 index 00000000..d127a698 --- /dev/null +++ b/src/relative_time.cc @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2015, 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 "config.h" + +#include + +#include + +#include "pcrepp.hh" +#include "lnav_util.hh" +#include "relative_time.hh" + +using namespace std; + +static struct { + const char *name; + pcrepp pcre; +} MATCHERS[relative_time::RTT__MAX] = { + { "ws", pcrepp("\\A\\s+\\b") }, + { "am", pcrepp("\\Aam|a\\.m\\.\\b") }, + { "pm", pcrepp("\\Apm|p\\.m\\.\\b") }, + { "a", pcrepp("\\Aa\\b") }, + { "an", pcrepp("\\Aan\\b") }, + { "time", pcrepp("\\A(\\d{1,2}):(\\d{2})(?::(\\d{2}))?") }, + { "num", pcrepp("\\A((?:-|\\+)?\\d+)") }, + { "us", pcrepp("\\Amicros(?:econds?)?|us(?![a-zA-Z])") }, + { "ms", pcrepp("\\Amillis(?:econds?)?|ms(?![a-zA-Z])") }, + { "sec", pcrepp("\\As(?:ec(?:onds?)?)?(?![a-zA-Z])") }, + { "min", pcrepp("\\Am(?:in(?:utes?)?)?(?![a-zA-Z])") }, + { "h", pcrepp("\\Ah(?:ours?)?(?![a-zA-Z])") }, + { "day", pcrepp("\\Ad(?:ays?)?(?![a-zA-Z])") }, + { "week", pcrepp("\\Aw(?:eeks?)?(?![a-zA-Z])") }, + { "mon", pcrepp("\\Amon(?:ths?)?(?![a-zA-Z])") }, + { "year", pcrepp("\\Ay(?:ears?)?(?![a-zA-Z])") }, + { "today", pcrepp("\\Atoday\\b") }, + { "yest", pcrepp("\\Ayesterday\\b") }, + { "tomo", pcrepp("\\Atomorrow\\b") }, + { "noon", pcrepp("\\Anoon\\b") }, + { "and", pcrepp("\\Aand\\b") }, + { "ago", pcrepp("\\Aago\\b") }, + { "lter", pcrepp("\\Alater\\b") }, + { "bfor", pcrepp("\\Abefore\\b") }, +}; + +static int64_t TIME_SCALES[] = { + 1000 * 1000, + 60, + 60, + 24, +}; + +bool relative_time::parse(const char *str, size_t len, struct parse_error &pe_out) +{ + pcre_input pi(str, 0, len); + pcre_context_static<30> pc; + int64_t number = 0; + bool number_set = false; + + pe_out.pe_column = -1; + pe_out.pe_msg.clear(); + + while (true) { + if (pi.pi_next_offset >= pi.pi_length) { + if (number_set) { + pe_out.pe_msg = "Number given without a time unit"; + return false; + } + + this->rollover(); + return true; + } + + bool found = false; + for (int lpc = 0; lpc < RTT__MAX && !found; lpc++) { + token_t token = (token_t) lpc; + if (!MATCHERS[lpc].pcre.match(pc, pi, PCRE_ANCHORED)) { + continue; + } + + pe_out.pe_column = pc.all()->c_begin; + found = true; + if (RTT_MICROS <= token && token <= RTT_YEARS) { + if (!number_set) { + pe_out.pe_msg = "Expecting a number before time unit"; + return false; + } + number_set = false; + } + switch (token) { + case RTT_INVALID: + case RTT_WHITE: + case RTT_AND: + break; + case RTT_AM: + case RTT_PM: + if (number_set) { + this->rt_field[RTF_HOURS] = number; + this->rt_is_absolute[RTF_HOURS] = true; + this->rt_field[RTF_MINUTES] = 0; + this->rt_is_absolute[RTF_MINUTES] = true; + this->rt_field[RTF_SECONDS] = 0; + this->rt_is_absolute[RTF_SECONDS] = true; + this->rt_field[RTF_MICROSECONDS] = 0; + this->rt_is_absolute[RTF_MICROSECONDS] = true; + number_set = false; + } + if (!this->rt_is_absolute[RTF_HOURS]) { + pe_out.pe_msg = "Expecting absolute time with A.M. or P.M."; + return false; + } + if (token == RTT_AM) { + if (this->rt_field[RTF_HOURS] == 12) { + this->rt_field[RTF_HOURS] = 0; + } + } + else { + this->rt_field[RTF_HOURS] += 12; + } + break; + case RTT_A: + case RTT_AN: + number = 1; + number_set = true; + break; + case RTT_TIME: { + string hstr = pi.get_substr(pc[0]); + string mstr = pi.get_substr(pc[1]); + this->rt_field[RTF_HOURS] = atoi(hstr.c_str()); + this->rt_is_absolute[RTF_HOURS] = true; + this->rt_field[RTF_MINUTES] = atoi(mstr.c_str()); + this->rt_is_absolute[RTF_MINUTES] = true; + if (pc[2]->is_valid()) { + string sstr = pi.get_substr(pc[2]); + this->rt_field[RTF_SECONDS] = atoi(sstr.c_str()); + } + else { + this->rt_field[RTF_SECONDS] = 0; + } + this->rt_is_absolute[RTF_SECONDS] = true; + this->rt_field[RTF_MICROSECONDS] = 0; + this->rt_is_absolute[RTF_MICROSECONDS] = true; + break; + } + case RTT_NUMBER: { + if (number_set) { + pe_out.pe_msg = "No time unit given for the previous number"; + return false; + } + + string numstr = pi.get_substr(pc[0]); + + if (sscanf(numstr.c_str(), "%qd", &number) != 1) { + pe_out.pe_msg = "Invalid number: " + numstr; + return false; + } + number_set = true; + break; + } + case RTT_MICROS: + this->rt_field[RTF_MICROSECONDS] = number; + break; + case RTT_MILLIS: + this->rt_field[RTF_MICROSECONDS] = number * 1000; + break; + case RTT_SECONDS: + this->rt_field[RTF_SECONDS] = number; + break; + case RTT_MINUTES: + this->rt_field[RTF_MINUTES] = number; + break; + case RTT_HOURS: + this->rt_field[RTF_HOURS] = number; + break; + case RTT_DAYS: + this->rt_field[RTF_DAYS] = number; + break; + case RTT_WEEKS: + this->rt_field[RTF_DAYS] = number * 7; + break; + case RTT_MONTHS: + this->rt_field[RTF_MONTHS] = number; + break; + case RTT_YEARS: + this->rt_field[RTF_YEARS] = number; + break; + case RTT_BEFORE: + case RTT_AGO: + if (this->empty()) { + pe_out.pe_msg = "Expecting a time unit"; + return false; + } + for (int field = 0; field < RTF__MAX; field++) { + if (this->rt_field[field] > 0) { + this->rt_field[field] = -this->rt_field[field]; + } + } + break; + case RTT_LATER: + if (this->empty()) { + pe_out.pe_msg = "Expecting a time unit before 'later'"; + return false; + } + break; + case RTT_TODAY: + break; + case RTT_YESTERDAY: + this->rt_field[RTF_DAYS] = -1; + break; + case RTT_TOMORROW: + this->rt_field[RTF_DAYS] = 1; + break; + case RTT_NOON: + this->rt_field[RTF_HOURS] = 12; + this->rt_is_absolute[RTF_HOURS] = true; + break; + + case RTT__MAX: + assert(false); + break; + } + } + + if (!found) { + pe_out.pe_msg = "Unrecognized input"; + return false; + } + } +} + +void relative_time::rollover() +{ + for (int lpc = 0; lpc < RTF_DAYS; lpc++) { + int64_t val = this->rt_field[lpc]; + this->rt_field[lpc] = val % TIME_SCALES[lpc]; + this->rt_field[lpc + 1] += val / TIME_SCALES[lpc]; + } + if (std::abs(this->rt_field[RTF_DAYS]) > 31) { + int64_t val = this->rt_field[RTF_DAYS]; + this->rt_field[RTF_DAYS] = val % 31; + this->rt_field[RTF_MONTHS] += val / 31; + } + if (std::abs(this->rt_field[RTF_MONTHS]) > 12) { + int64_t val = this->rt_field[RTF_MONTHS]; + this->rt_field[RTF_MONTHS] = val % 12; + this->rt_field[RTF_YEARS] += val / 12; + } +} diff --git a/src/relative_time.hh b/src/relative_time.hh new file mode 100644 index 00000000..c10f4d4e --- /dev/null +++ b/src/relative_time.hh @@ -0,0 +1,214 @@ +/** + * Copyright (c) 2015, 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. + */ + +#ifndef LNAV_RELATIVE_TIME_HH +#define LNAV_RELATIVE_TIME_HH + +#include +#include +#include + +#include + +#include "ptimec.hh" + +class relative_time { +public: + enum token_t { + RTT_INVALID = -1, + + RTT_WHITE, + RTT_AM, + RTT_PM, + RTT_A, + RTT_AN, + RTT_TIME, + RTT_NUMBER, + RTT_MICROS, + RTT_MILLIS, + RTT_SECONDS, + RTT_MINUTES, + RTT_HOURS, + RTT_DAYS, + RTT_WEEKS, + RTT_MONTHS, + RTT_YEARS, + RTT_TODAY, + RTT_YESTERDAY, + RTT_TOMORROW, + RTT_NOON, + RTT_AND, + RTT_AGO, + RTT_LATER, + RTT_BEFORE, + + RTT__MAX + }; + + relative_time() { + this->clear(); + }; + + void clear() { + memset(this->rt_field, 0, sizeof(this->rt_field)); + memset(this->rt_is_absolute, 0, sizeof(this->rt_is_absolute)); + }; + + void negate() { + for (int lpc = 0; lpc < RTF__MAX; lpc++) { + if (!this->rt_is_absolute[lpc] && this->rt_field[lpc] != 0) { + this->rt_field[lpc] = -this->rt_field[lpc]; + } + } + }; + + bool is_negative() const { + for (int lpc = 0; lpc < RTF__MAX; lpc++) { + if (this->rt_field[lpc] < 0) { + return true; + } + } + return false; + }; + + bool is_absolute() const { + for (int lpc = 0; lpc < RTF__MAX; lpc++) { + if (this->rt_is_absolute[lpc]) { + return true; + } + } + return false; + }; + + bool empty() const { + for (int lpc = 0; lpc < RTF__MAX; lpc++) { + if (this->rt_field[lpc]) { + return false; + } + } + return true; + }; + + struct parse_error { + int pe_column; + std::string pe_msg; + }; + + bool parse(const char *str, size_t len, struct parse_error &pe_out); + + bool parse(const std::string &str, struct parse_error &pe_out) { + return this->parse(str.c_str(), str.length(), pe_out); + } + + void add(struct exttm &tm) { + if (this->rt_is_absolute[RTF_MICROSECONDS]) { + tm.et_nsec = this->rt_field[RTF_MICROSECONDS] * 1000; + } + else { + tm.et_nsec += this->rt_field[RTF_MICROSECONDS] * 1000; + } + if (this->rt_is_absolute[RTF_SECONDS]) { + tm.et_tm.tm_sec = this->rt_field[RTF_SECONDS]; + } + else { + tm.et_tm.tm_sec += this->rt_field[RTF_SECONDS]; + } + if (this->rt_is_absolute[RTF_MINUTES]) { + tm.et_tm.tm_min = this->rt_field[RTF_MINUTES]; + } + else { + tm.et_tm.tm_min += this->rt_field[RTF_MINUTES]; + } + if (this->rt_is_absolute[RTF_HOURS]) { + tm.et_tm.tm_hour = this->rt_field[RTF_HOURS]; + } + else { + tm.et_tm.tm_hour += this->rt_field[RTF_HOURS]; + } + if (this->rt_is_absolute[RTF_DAYS]) { + tm.et_tm.tm_mday = this->rt_field[RTF_DAYS]; + } + else { + tm.et_tm.tm_mday += this->rt_field[RTF_DAYS]; + } + if (this->rt_is_absolute[RTF_MONTHS]) { + tm.et_tm.tm_mon = this->rt_field[RTF_MONTHS]; + } + else { + tm.et_tm.tm_mon += this->rt_field[RTF_MONTHS]; + } + if (this->rt_is_absolute[RTF_YEARS]) { + tm.et_tm.tm_year = this->rt_field[RTF_YEARS]; + } + else { + tm.et_tm.tm_year += this->rt_field[RTF_YEARS]; + } + }; + + std::string to_string() { + char dst[128]; + + snprintf(dst, sizeof(dst), + "%qd%c%qd%c%qd%c%qd%c%qd%c%qd%c%qd%c", + this->rt_field[RTF_YEARS], + this->rt_is_absolute[RTF_YEARS] ? 'Y' : 'y', + this->rt_field[RTF_MONTHS], + this->rt_is_absolute[RTF_MONTHS] ? 'M' : 'm', + this->rt_field[RTF_DAYS], + this->rt_is_absolute[RTF_DAYS] ? 'D' : 'd', + this->rt_field[RTF_HOURS], + this->rt_is_absolute[RTF_HOURS] ? 'H' : 'h', + this->rt_field[RTF_MINUTES], + this->rt_is_absolute[RTF_MINUTES] ? 'M' : 'm', + this->rt_field[RTF_SECONDS], + this->rt_is_absolute[RTF_SECONDS] ? 'S' : 's', + this->rt_field[RTF_MICROSECONDS], + this->rt_is_absolute[RTF_MICROSECONDS] ? 'U' : 'u'); + return dst; + }; + + void rollover(); + + enum { + RTF_MICROSECONDS, + RTF_SECONDS, + RTF_MINUTES, + RTF_HOURS, + RTF_DAYS, + RTF_MONTHS, + RTF_YEARS, + + RTF__MAX + }; + + int64_t rt_field[RTF__MAX]; + bool rt_is_absolute[RTF__MAX]; +}; + +#endif //LNAV_RELATIVE_TIME_HH diff --git a/src/session_data.cc b/src/session_data.cc index e94dcf6a..a8e2c9ac 100644 --- a/src/session_data.cc +++ b/src/session_data.cc @@ -1300,7 +1300,6 @@ void reset_session(void) textview_curses::highlight_map_t &hmap = lnav_data.ld_views[LNV_LOG].get_highlights(); textview_curses::highlight_map_t::iterator hl_iter = hmap.begin(); - logfile_sub_source &lss = lnav_data.ld_log_source; log_info("reset session: time=%d", lnav_data.ld_session_time); @@ -1328,8 +1327,19 @@ void reset_session(void) lf->clear_time_offset(); } - lnav_data.ld_log_source.get_filters().clear_filters(); + for (int lpc = 0; lpc < LNV__MAX; lpc++) { + textview_curses &tc = lnav_data.ld_views[lpc]; + text_sub_source *tss = tc.get_sub_source(); - lss.get_user_bookmarks()[&textview_curses::BM_USER].clear(); - lss.get_user_bookmarks()[&textview_curses::BM_PARTITION].clear(); + if (tss == NULL) { + continue; + } + tss->get_filters().clear_filters(); + tss->text_filters_changed(); + tss->text_clear_marks(&textview_curses::BM_USER); + tc.get_bookmarks()[&textview_curses::BM_USER].clear(); + tss->text_clear_marks(&textview_curses::BM_PARTITION); + tc.get_bookmarks()[&textview_curses::BM_PARTITION].clear(); + tc.reload_data(); + } } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e8e8d2ec..f5a73a48 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -3,5 +3,10 @@ include_directories(../../lbuild/src ../src/ /opt/local/include) add_executable(test_chunky_index test_chunky_index.cc) add_executable(test_pcrepp test_pcrepp.cc ../src/lnav_log.cc ../src/pcrepp.cc) +add_executable(test_reltime test_reltime.cc + ../src/relative_time.cc + ../src/pcrepp.cc + ../src/lnav_log.cc) link_directories(/opt/local/lib) target_link_libraries(test_pcrepp /opt/local/lib/libpcre.a) +target_link_libraries(test_reltime /opt/local/lib/libpcre.a) diff --git a/test/Makefile.am b/test/Makefile.am index df88c3cc..e6b414f5 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -44,6 +44,7 @@ check_PROGRAMS = \ test_line_buffer2 \ test_log_accel \ test_pcrepp \ + test_reltime \ test_top_status \ test_yajlpp @@ -99,6 +100,9 @@ test_concise_LDADD = ../src/libdiag.a test_json_ptr_SOURCES = test_json_ptr.cc test_json_ptr_LDADD = ../src/libdiag.a +test_reltime_SOURCES = test_reltime.cc +test_reltime_LDADD = ../src/libdiag.a + drive_line_buffer_SOURCES = drive_line_buffer.cc drive_line_buffer_LDADD = ../src/libdiag.a $(CURSES_LIB) -lz @@ -244,6 +248,7 @@ dist_noinst_DATA = \ logfile_syslog.1 \ logfile_syslog.2 \ logfile_syslog_with_access_log.0 \ + logfile_syslog_with_mixed_times.0 \ logfile_tcf.0 \ logfile_tcf.1 \ logfile_tcsh_history.0 \ @@ -293,6 +298,7 @@ TESTS = \ test_log_accel \ test_logfile.sh \ test_pcrepp \ + test_reltime \ test_sessions.sh \ test_sql.sh \ test_sql_coll_func.sh \ diff --git a/test/drive_logfile.cc b/test/drive_logfile.cc index 6c6f41c7..3b53fbae 100644 --- a/test/drive_logfile.cc +++ b/test/drive_logfile.cc @@ -67,7 +67,9 @@ int main(int argc, char *argv[]) { std::vector paths, errors; - paths.push_back(getenv("test_dir")); + if (getenv("test_dir") != NULL) { + paths.push_back(getenv("test_dir")); + } load_formats(paths, errors); } diff --git a/test/logfile_syslog_with_mixed_times.0 b/test/logfile_syslog_with_mixed_times.0 new file mode 100644 index 00000000..f2314ae0 --- /dev/null +++ b/test/logfile_syslog_with_mixed_times.0 @@ -0,0 +1,13 @@ +Sep 13 00:58:45 Tim-Stacks-iMac kernel[0]: AirParrot device perform power state change 0 -> 1 +Sep 13 00:59:30 Tim-Stacks-iMac.local airportd[59]: _configureScanOffloadParameters: Unable to configure scan offloading on en1 (Device power is off) +Sep 13 01:23:54 Tim-Stacks-iMac kernel[0]: RTC: PowerByCalendarDate setting ignored +Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: vm_compressor_record_warmup (9478314 - 9492476) +Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer +Sep 13 01:25:39 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000 +Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff8428276000, 0xffffff8428336000 +Sep 13 03:12:58 Tim-Stacks-iMac kernel[0]: *** kernel exceeded 500 log message per second limit - remaining messages this second discarded *** +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: IOThunderboltSwitch<0xffffff803f4b3000>(0x0)::listenerCallback - Thunderbolt HPD packet for route = 0x0 port = 11 unplug = 0 +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: vm_compressor_flush - starting +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer +Sep 13 03:13:16 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000 +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff838f1fc000, 0xffffff838f2bc000 diff --git a/test/test_cmds.sh b/test/test_cmds.sh index d4542f8c..fa93b871 100644 --- a/test/test_cmds.sh +++ b/test/test_cmds.sh @@ -30,12 +30,45 @@ check_output "goto -1 is not working" <(0x0)::listenerCallback - Thunderbolt HPD packet for route = 0x0 port = 11 unplug = 0 +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: vm_compressor_flush - starting +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer +Sep 13 03:13:16 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000 +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff838f1fc000, 0xffffff838f2bc000 +EOF + + +run_test ${lnav_test} -n \ + -c ":goto 0" \ + -c ":goto 3:45" \ + ${test_dir}/logfile_syslog_with_mixed_times.0 + +check_output "goto 3:45 is not working?" <(0x0)::listenerCallback - Thunderbolt HPD packet for route = 0x0 port = 11 unplug = 0 +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: vm_compressor_flush - starting +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer +Sep 13 03:13:16 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000 +Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff838f1fc000, 0xffffff838f2bc000 +EOF + + run_test ${lnav_test} -n \ -c ":goto invalid" \ ${test_dir}/logfile_access_log.0 check_error_output "goto invalid is working" < + +#include "relative_time.hh" + +struct { + const char *reltime; + const char *expected; +} TEST_DATA[] = { + { "a minute ago", "0y0m0d0h-1m0s0u" }, + { "1m ago", "0y0m0d0h-1m0s0u" }, + { "a min ago", "0y0m0d0h-1m0s0u" }, + { "a m ago", "0y0m0d0h-1m0s0u" }, + { "+1 minute ago", "0y0m0d0h-1m0s0u" }, + { "-1 minute ago", "0y0m0d0h-1m0s0u" }, + { "-1 minute", "0y0m0d0h-1m0s0u" }, + { "1:40", "0y0m0d1H40M0S0U" }, + { "01:40", "0y0m0d1H40M0S0U" }, + { "1h40m", "0y0m0d1h40m0s0u" }, + { "1pm", "0y0m0d13H0M0S0U" }, + + { NULL, NULL } +}; + +struct { + const char *reltime; + const char *expected_error; +} BAD_TEST_DATA[] = { + { "ago", "" }, + { "minute", "" }, + { "1 2", "" }, + + { NULL, NULL } +}; + +int main(int argc, char *argv[]) +{ + time_t base_time = 1317913200; + struct exttm base_tm; + base_tm.et_tm = *gmtime(&base_time); + struct relative_time::parse_error pe; + struct exttm tm; + time_t new_time; + + relative_time rt; + + for (int lpc = 0; TEST_DATA[lpc].reltime; lpc++) { + rt.clear(); + rt.parse(TEST_DATA[lpc].reltime, pe); + printf("%s %s %s\n", TEST_DATA[lpc].reltime, TEST_DATA[lpc].expected, rt.to_string().c_str()); + assert(std::string(TEST_DATA[lpc].expected) == rt.to_string()); + } + + for (int lpc = 0; BAD_TEST_DATA[lpc].reltime; lpc++) { + bool rc; + rt.clear(); + rc = rt.parse(BAD_TEST_DATA[lpc].reltime, pe); + printf("%s -- %s\n", BAD_TEST_DATA[lpc].reltime, pe.pe_msg.c_str()); + assert(!rc); + } + + rt.parse("a minute ago", pe); + assert(rt.rt_field[relative_time::RTF_MINUTES] == -1); + + rt.parse("5 milliseconds", pe); + + assert(rt.rt_field[relative_time::RTF_MICROSECONDS] == 5 * 1000); + + rt.clear(); + rt.parse("5000 ms ago", pe); + assert(rt.rt_field[relative_time::RTF_SECONDS] == -5); + + rt.clear(); + rt.parse("5 hours 20 minutes ago", pe); + + assert(rt.rt_field[relative_time::RTF_HOURS] == -5); + assert(rt.rt_field[relative_time::RTF_MINUTES] == -20); + + rt.clear(); + rt.parse("5 hours and 20 minutes ago", pe); + + assert(rt.rt_field[relative_time::RTF_HOURS] == -5); + assert(rt.rt_field[relative_time::RTF_MINUTES] == -20); + + rt.clear(); + rt.parse("1:23", pe); + + assert(rt.rt_field[relative_time::RTF_HOURS] == 1); + assert(rt.rt_is_absolute[relative_time::RTF_HOURS]); + assert(rt.rt_field[relative_time::RTF_MINUTES] == 23); + assert(rt.rt_is_absolute[relative_time::RTF_MINUTES]); + + rt.clear(); + rt.parse("1:23:45", pe); + + assert(rt.rt_field[relative_time::RTF_HOURS] == 1); + assert(rt.rt_is_absolute[relative_time::RTF_HOURS]); + assert(rt.rt_field[relative_time::RTF_MINUTES] == 23); + assert(rt.rt_is_absolute[relative_time::RTF_MINUTES]); + assert(rt.rt_field[relative_time::RTF_SECONDS] == 45); + assert(rt.rt_is_absolute[relative_time::RTF_SECONDS]); + + tm = base_tm; + rt.add(tm); + + new_time = timegm(&tm.et_tm); + tm.et_tm = *gmtime(&new_time); + assert(tm.et_tm.tm_hour == 1); + assert(tm.et_tm.tm_min == 23); + + rt.clear(); + rt.parse("5 minutes ago", pe); + + tm = base_tm; + rt.add(tm); + + new_time = timegm(&tm.et_tm); + + assert(new_time == (base_time - (5 * 60))); + +}