fish-shell/src/history.cpp
2017-01-29 18:00:48 -08:00

1767 lines
63 KiB
C++

// History functions, part of the user interface.
#include "config.h" // IWYU pragma: keep
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// We need the sys/file.h for the flock() declaration on Linux but not OS X.
#include <sys/file.h> // IWYU pragma: keep
#include <sys/mman.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include <wchar.h>
#include <wctype.h>
#include <algorithm>
#include <cwchar>
#include <iterator>
#include <map>
#include "common.h"
#include "env.h"
#include "fallback.h" // IWYU pragma: keep
#include "history.h"
#include "io.h"
#include "iothread.h"
#include "lru.h"
#include "parse_constants.h"
#include "parse_tree.h"
#include "path.h"
#include "reader.h"
#include "signal.h"
#include "wutil.h" // IWYU pragma: keep
// Our history format is intended to be valid YAML. Here it is:
//
// - cmd: ssh blah blah blah
// when: 2348237
// paths:
// - /path/to/something
// - /path/to/something_else
//
// Newlines are replaced by \n. Backslashes are replaced by \\.
// When we rewrite the history, the number of items we keep.
#define HISTORY_SAVE_MAX (1024 * 256)
// Default buffer size for flushing to the history file.
#define HISTORY_OUTPUT_BUFFER_SIZE (16 * 1024)
namespace {
/// Helper class for certain output. This is basically a string that allows us to ensure we only
/// flush at record boundaries, and avoids the copying of ostringstream. Have you ever tried to
/// implement your own streambuf? Total insanity.
static size_t safe_strlen(const char *s) { return s ? strlen(s) : 0; }
class history_output_buffer_t {
// A null-terminated C string.
std::vector<char> buffer;
// Offset is the offset of the null terminator.
size_t offset;
public:
/// Add a bit more to HISTORY_OUTPUT_BUFFER_SIZE because we flush once we've exceeded that size.
history_output_buffer_t() : buffer(HISTORY_OUTPUT_BUFFER_SIZE + 128, '\0'), offset(0) {}
/// Append one or more strings.
void append(const char *s1, const char *s2 = NULL, const char *s3 = NULL) {
const char *ptrs[4] = {s1, s2, s3, NULL};
const size_t lengths[4] = {safe_strlen(s1), safe_strlen(s2), safe_strlen(s3), 0};
// Determine the additional size we'll need.
size_t additional_length = 0;
for (size_t i = 0; i < sizeof lengths / sizeof *lengths; i++) {
additional_length += lengths[i];
}
// Allocate that much, plus a null terminator.
size_t required_size = offset + additional_length + 1;
if (required_size > buffer.size()) {
buffer.resize(required_size, '\0');
}
// Copy.
for (size_t i = 0; ptrs[i] != NULL; i++) {
memmove(&buffer.at(offset), ptrs[i], lengths[i]);
offset += lengths[i];
}
// Null terminator was appended by virtue of the resize() above (or in a previous
// invocation).
assert(buffer.at(buffer.size() - 1) == '\0');
}
/// Output to a given fd, resetting our buffer. Returns true on success, false on error.
bool flush_to_fd(int fd) {
bool result = write_loop(fd, &buffer.at(0), offset) >= 0;
offset = 0;
return result;
}
/// Return how much data we've accumulated.
size_t output_size() const { return offset; }
};
class time_profiler_t {
const char *what;
double start;
public:
explicit time_profiler_t(const char *w) {
what = w;
start = timef();
}
~time_profiler_t() {
double end = timef();
debug(2, "%s: %.0f ms", what, (end - start) * 1000);
}
};
/// Lock the history file.
/// Returns true on success, false on failure.
static bool history_file_lock(int fd, int lock_type) {
static bool do_locking = true;
if (!do_locking) return false;
double start_time = timef();
int retval = flock(fd, lock_type);
double duration = timef() - start_time;
if (duration > 0.25) {
debug(1, _(L"Locking the history file took too long (%.3f seconds)."), duration);
do_locking = false;
return false;
}
return retval != -1;
}
/// Our LRU cache is used for restricting the amount of history we have, and limiting how long we
/// order it.
class history_lru_item_t {
public:
wcstring text;
time_t timestamp;
path_list_t required_paths;
explicit history_lru_item_t(const history_item_t &item)
: text(item.str()),
timestamp(item.timestamp()),
required_paths(item.get_required_paths()) {}
};
class history_lru_cache_t : public lru_cache_t<history_lru_cache_t, history_lru_item_t> {
typedef lru_cache_t<history_lru_cache_t, history_lru_item_t> super;
public:
using super::super;
/// Function to add a history item.
void add_item(const history_item_t &item) {
// Skip empty items.
if (item.empty()) return;
// See if it's in the cache. If it is, update the timestamp. If not, we create a new node
// and add it. Note that calling get_node promotes the node to the front.
wcstring key = item.str();
history_lru_item_t *node = this->get(key);
if (node == NULL) {
this->insert(std::move(key), history_lru_item_t(item));
} else {
node->timestamp = std::max(node->timestamp, item.timestamp());
// What to do about paths here? Let's just ignore them.
}
}
};
class history_collection_t {
pthread_mutex_t m_lock;
std::map<wcstring, history_t *> m_histories;
public:
history_collection_t() { VOMIT_ON_FAILURE_NO_ERRNO(pthread_mutex_init(&m_lock, NULL)); }
~history_collection_t() {
for (std::map<wcstring, history_t *>::const_iterator i = m_histories.begin();
i != m_histories.end(); ++i) {
delete i->second;
}
pthread_mutex_destroy(&m_lock);
}
history_t &alloc(const wcstring &name);
void save();
};
} // anonymous namespace
static history_collection_t histories;
static wcstring history_filename(const wcstring &name, const wcstring &suffix);
/// Replaces newlines with a literal backslash followed by an n, and replaces backslashes with two
/// backslashes.
static void escape_yaml(std::string *str);
/// Inverse of escape_yaml.
static void unescape_yaml(std::string *str);
/// Read one line, stripping off any newline, and updating cursor. Note that our input string is NOT
/// null terminated; it's just a memory mapped file.
static size_t read_line(const char *base, size_t cursor, size_t len, std::string &result) {
// Locate the newline.
assert(cursor <= len);
const char *start = base + cursor;
const char *a_newline = (char *)memchr(start, '\n', len - cursor);
if (a_newline != NULL) { // we found a newline
result.assign(start, a_newline - start);
// Return the amount to advance the cursor; skip over the newline.
return a_newline - start + 1;
}
// We ran off the end.
result.clear();
return len - cursor;
}
/// Trims leading spaces in the given string, returning how many there were.
static size_t trim_leading_spaces(std::string &str) {
size_t i = 0, max = str.size();
while (i < max && str[i] == ' ') i++;
str.erase(0, i);
return i;
}
static bool extract_prefix_and_unescape_yaml(std::string *key, std::string *value,
const std::string &line) {
size_t where = line.find(":");
if (where != std::string::npos) {
key->assign(line, 0, where);
// Skip a space after the : if necessary.
size_t val_start = where + 1;
if (val_start < line.size() && line.at(val_start) == ' ') val_start++;
value->assign(line, val_start, line.size() - val_start);
unescape_yaml(key);
unescape_yaml(value);
}
return where != std::string::npos;
}
/// Remove backslashes from all newlines. This makes a string from the history file better formated
/// for on screen display.
static wcstring history_unescape_newlines_fish_1_x(const wcstring &in_str) {
wcstring out;
for (const wchar_t *in = in_str.c_str(); *in; in++) {
if (*in == L'\\') {
if (*(in + 1) != L'\n') {
out.push_back(*in);
}
} else {
out.push_back(*in);
}
}
return out;
}
/// Try to infer the history file type based on inspecting the data.
static history_file_type_t infer_file_type(const char *data, size_t len) {
history_file_type_t result = history_type_unknown;
if (len > 0) { // old fish started with a #
if (data[0] == '#') {
result = history_type_fish_1_x;
} else { // assume new fish
result = history_type_fish_2_0;
}
}
return result;
}
/// Decode an item via the fish 1.x format. Adapted from fish 1.x's item_get().
static history_item_t decode_item_fish_1_x(const char *begin, size_t length) {
const char *end = begin + length;
const char *pos = begin;
wcstring out;
bool was_backslash = false;
bool first_char = true;
bool timestamp_mode = false;
time_t timestamp = 0;
while (1) {
wchar_t c;
size_t res;
mbstate_t state = {};
if (MB_CUR_MAX == 1) { // single-byte locale
c = (unsigned char)*pos;
res = 1;
} else {
res = mbrtowc(&c, pos, end - pos, &state);
}
if (res == (size_t)-1) {
pos++;
continue;
} else if (res == (size_t)-2) {
break;
} else if (res == (size_t)0) {
pos++;
continue;
}
pos += res;
if (c == L'\n') {
if (timestamp_mode) {
const wchar_t *time_string = out.c_str();
while (*time_string && !iswdigit(*time_string)) time_string++;
if (*time_string) {
time_t tm = (time_t)fish_wcstol(time_string);
if (!errno && tm >= 0) {
timestamp = tm;
}
}
out.clear();
timestamp_mode = false;
continue;
}
if (!was_backslash) break;
}
if (first_char) {
first_char = false;
if (c == L'#') timestamp_mode = true;
}
out.push_back(c);
was_backslash = (c == L'\\') && !was_backslash;
}
out = history_unescape_newlines_fish_1_x(out);
return history_item_t(out, timestamp);
}
/// Decode an item via the fish 2.0 format.
static history_item_t decode_item_fish_2_0(const char *base, size_t len) {
wcstring cmd;
time_t when = 0;
path_list_t paths;
size_t indent = 0, cursor = 0;
std::string key, value, line;
// Read the "- cmd:" line.
size_t advance = read_line(base, cursor, len, line);
trim_leading_spaces(line);
if (!extract_prefix_and_unescape_yaml(&key, &value, line) || key != "- cmd") {
goto done; //!OCLINT(goto is the cleanest way to handle bad input)
}
cursor += advance;
cmd = str2wcstring(value);
// Read the remaining lines.
for (;;) {
size_t advance = read_line(base, cursor, len, line);
size_t this_indent = trim_leading_spaces(line);
if (indent == 0) indent = this_indent;
if (this_indent == 0 || indent != this_indent) break;
if (!extract_prefix_and_unescape_yaml(&key, &value, line)) break;
// We are definitely going to consume this line.
cursor += advance;
if (key == "when") {
// Parse an int from the timestamp. Should this fail, strtol returns 0; that's
// acceptable.
char *end = NULL;
long tmp = strtol(value.c_str(), &end, 0);
when = tmp;
} else if (key == "paths") {
// Read lines starting with " - " until we can't read any more.
for (;;) {
size_t advance = read_line(base, cursor, len, line);
if (trim_leading_spaces(line) <= indent) break;
if (strncmp(line.c_str(), "- ", 2)) break;
// We're going to consume this line.
cursor += advance;
// Skip the leading dash-space and then store this path it.
line.erase(0, 2);
unescape_yaml(&line);
paths.push_back(str2wcstring(line));
}
}
}
done:
history_item_t result(cmd, when);
result.set_required_paths(paths);
return result;
}
static history_item_t decode_item(const char *base, size_t len, history_file_type_t type) {
if (type == history_type_fish_2_0) return decode_item_fish_2_0(base, len);
if (type == history_type_fish_1_x) return decode_item_fish_1_x(base, len);
return history_item_t(L"");
}
/// We can merge two items if they are the same command. We use the more recent timestamp, more
/// recent identifier, and the longer list of required paths.
bool history_item_t::merge(const history_item_t &item) {
bool result = false;
if (this->contents == item.contents) {
this->creation_timestamp = std::max(this->creation_timestamp, item.creation_timestamp);
if (this->required_paths.size() < item.required_paths.size()) {
this->required_paths = item.required_paths;
}
if (this->identifier < item.identifier) {
this->identifier = item.identifier;
}
result = true;
}
return result;
}
#if 0
history_item_t::history_item_t(const wcstring &str)
: contents(str), contents_lower(L""), creation_timestamp(time(NULL)), identifier(0) {
for (wcstring::const_iterator it = str.begin(); it != str.end(); ++it) {
contents_lower.push_back(towlower(*it));
}
}
#endif
history_item_t::history_item_t(const wcstring &str, time_t when, history_identifier_t ident)
: contents(str), contents_lower(L""), creation_timestamp(when), identifier(ident) {
for (wcstring::const_iterator it = str.begin(); it != str.end(); ++it) {
contents_lower.push_back(towlower(*it));
}
}
bool history_item_t::matches_search(const wcstring &term, enum history_search_type_t type,
bool case_sensitive) const {
// We don't use a switch below because there are only three cases and if the strings are the
// same length we can use the faster HISTORY_SEARCH_TYPE_EXACT for the other two cases.
//
// Too, we consider equal strings to match a prefix search, so that autosuggest will allow
// suggesting what you've typed.
if (case_sensitive) {
if (type == HISTORY_SEARCH_TYPE_EXACT || term.size() == contents.size()) {
return term == contents;
} else if (type == HISTORY_SEARCH_TYPE_CONTAINS) {
return contents.find(term) != wcstring::npos;
} else if (type == HISTORY_SEARCH_TYPE_PREFIX) {
return string_prefixes_string(term, contents);
}
} else {
wcstring lterm(L"");
for (wcstring::const_iterator it = term.begin(); it != term.end(); ++it) {
lterm.push_back(towlower(*it));
}
if (type == HISTORY_SEARCH_TYPE_EXACT || lterm.size() == contents.size()) {
return lterm == contents_lower;
} else if (type == HISTORY_SEARCH_TYPE_CONTAINS) {
return contents_lower.find(lterm) != wcstring::npos;
} else if (type == HISTORY_SEARCH_TYPE_PREFIX) {
return string_prefixes_string(lterm, contents_lower);
}
}
DIE("unexpected history_search_type_t value");
}
/// Append our YAML history format to the provided vector at the given offset, updating the offset.
static void append_yaml_to_buffer(const wcstring &wcmd, time_t timestamp,
const path_list_t &required_paths,
history_output_buffer_t *buffer) {
std::string cmd = wcs2string(wcmd);
escape_yaml(&cmd);
buffer->append("- cmd: ", cmd.c_str(), "\n");
char timestamp_str[96];
snprintf(timestamp_str, sizeof timestamp_str, "%ld", (long)timestamp);
buffer->append(" when: ", timestamp_str, "\n");
if (!required_paths.empty()) {
buffer->append(" paths:\n");
for (path_list_t::const_iterator iter = required_paths.begin();
iter != required_paths.end(); ++iter) {
std::string path = wcs2string(*iter);
escape_yaml(&path);
buffer->append(" - ", path.c_str(), "\n");
}
}
}
/// Parse a timestamp line that looks like this: spaces, "when:", spaces, timestamp, newline
/// The string is NOT null terminated; however we do know it contains a newline, so stop when we
/// reach it.
static bool parse_timestamp(const char *str, time_t *out_when) {
const char *cursor = str;
// Advance past spaces.
while (*cursor == ' ') cursor++;
// Look for "when:".
size_t when_len = 5;
if (strncmp(cursor, "when:", when_len) != 0) return false;
cursor += when_len;
// Advance past spaces.
while (*cursor == ' ') cursor++;
// Try to parse a timestamp.
long timestamp = 0;
if (isdigit(*cursor) && (timestamp = strtol(cursor, NULL, 0)) > 0) {
*out_when = (time_t)timestamp;
return true;
}
return false;
}
/// Returns a pointer to the start of the next line, or NULL. The next line must itself end with a
/// newline. Note that the string is not null terminated.
static const char *next_line(const char *start, size_t length) {
// Handle the hopeless case.
if (length < 1) return NULL;
// Get a pointer to the end, that we must not pass.
const char *const end = start + length;
// Skip past the next newline.
const char *nextline = (const char *)memchr(start, '\n', length);
if (!nextline || nextline >= end) {
return NULL;
}
// Skip past the newline character itself.
if (++nextline >= end) {
return NULL;
}
// Make sure this new line is itself "newline terminated". If it's not, return NULL.
const char *next_newline = (const char *)memchr(nextline, '\n', end - nextline);
if (!next_newline) {
return NULL;
}
return nextline;
}
/// Support for iteratively locating the offsets of history items.
/// Pass the address and length of a mapped region.
/// Pass a pointer to a cursor size_t, initially 0.
/// If custoff_timestamp is nonzero, skip items created at or after that timestamp.
/// Returns (size_t)-1 when done.
static size_t offset_of_next_item_fish_2_0(const char *begin, size_t mmap_length,
size_t *inout_cursor, time_t cutoff_timestamp) {
size_t cursor = *inout_cursor;
size_t result = (size_t)-1;
while (cursor < mmap_length) {
const char *line_start = begin + cursor;
// Advance the cursor to the next line.
const char *a_newline = (const char *)memchr(line_start, '\n', mmap_length - cursor);
if (a_newline == NULL) break;
// Advance the cursor past this line. +1 is for the newline.
cursor = a_newline - begin + 1;
// Skip lines with a leading space, since these are in the interior of one of our items.
if (line_start[0] == ' ') continue;
// Skip very short lines to make one of the checks below easier.
if (a_newline - line_start < 3) continue;
// Try to be a little YAML compatible. Skip lines with leading %, ---, or ...
if (!memcmp(line_start, "%", 1) || !memcmp(line_start, "---", 3) ||
!memcmp(line_start, "...", 3))
continue;
// Hackish: fish 1.x rewriting a fish 2.0 history file can produce lines with lots of
// leading "- cmd: - cmd: - cmd:". Trim all but one leading "- cmd:".
const char *double_cmd = "- cmd: - cmd: ";
const size_t double_cmd_len = strlen(double_cmd);
while ((size_t)(a_newline - line_start) > double_cmd_len &&
!memcmp(line_start, double_cmd, double_cmd_len)) {
// Skip over just one of the - cmd. In the end there will be just one left.
line_start += strlen("- cmd: ");
}
// Hackish: fish 1.x rewriting a fish 2.0 history file can produce commands like "when:
// 123456". Ignore those.
const char *cmd_when = "- cmd: when:";
const size_t cmd_when_len = strlen(cmd_when);
if ((size_t)(a_newline - line_start) >= cmd_when_len &&
!memcmp(line_start, cmd_when, cmd_when_len)) {
continue;
}
// At this point, we know line_start is at the beginning of an item. But maybe we want to
// skip this item because of timestamps. A 0 cutoff means we don't care; if we do care, then
// try parsing out a timestamp.
if (cutoff_timestamp != 0) {
// Hackish fast way to skip items created after our timestamp. This is the mechanism by
// which we avoid "seeing" commands from other sessions that started after we started.
// We try hard to ensure that our items are sorted by their timestamps, so in theory we
// could just break, but I don't think that works well if (for example) the clock
// changes. So we'll read all subsequent items.
const char *const end = begin + mmap_length;
// Walk over lines that we think are interior. These lines are not null terminated, but
// are guaranteed to contain a newline.
bool has_timestamp = false;
time_t timestamp = 0;
const char *interior_line;
for (interior_line = next_line(line_start, end - line_start);
interior_line != NULL && !has_timestamp;
interior_line = next_line(interior_line, end - interior_line)) {
// If the first character is not a space, it's not an interior line, so we're done.
if (interior_line[0] != ' ') break;
// Hackish optimization: since we just stepped over some interior line, update the
// cursor so we don't have to look at these lines next time.
cursor = interior_line - begin;
// Try parsing a timestamp from this line. If we succeed, the loop will break.
has_timestamp = parse_timestamp(interior_line, &timestamp);
}
// Skip this item if the timestamp is past our cutoff.
if (has_timestamp && timestamp > cutoff_timestamp) {
continue;
}
}
// We made it through the gauntlet.
result = line_start - begin;
break; //!OCLINT(avoid branching statement as last in loop)
}
*inout_cursor = cursor;
return result;
}
/// Same as offset_of_next_item_fish_2_0, but for fish 1.x (pre fishfish).
/// Adapted from history_populate_from_mmap in history.c
static size_t offset_of_next_item_fish_1_x(const char *begin, size_t mmap_length,
size_t *inout_cursor) {
if (mmap_length == 0 || *inout_cursor >= mmap_length) return (size_t)-1;
const char *end = begin + mmap_length;
const char *pos;
bool ignore_newline = false;
bool do_push = true;
bool all_done = false;
size_t result = *inout_cursor;
for (pos = begin + *inout_cursor; pos < end && !all_done; pos++) {
if (do_push) {
ignore_newline = (*pos == '#');
do_push = false;
}
if (*pos == '\\') {
pos++;
} else if (*pos == '\n') {
if (!ignore_newline) {
// pos will be left pointing just after this newline, because of the ++ in the loop.
all_done = true;
}
ignore_newline = false;
}
}
*inout_cursor = (pos - begin);
return result;
}
/// Returns the offset of the next item based on the given history type, or -1.
static size_t offset_of_next_item(const char *begin, size_t mmap_length,
history_file_type_t mmap_type, size_t *inout_cursor,
time_t cutoff_timestamp) {
size_t result = (size_t)-1;
if (mmap_type == history_type_fish_2_0) {
result = offset_of_next_item_fish_2_0(begin, mmap_length, inout_cursor, cutoff_timestamp);
} else if (mmap_type == history_type_fish_1_x) {
result = offset_of_next_item_fish_1_x(begin, mmap_length, inout_cursor);
}
return result;
}
history_t &history_collection_t::alloc(const wcstring &name) {
// Note that histories are currently never deleted, so we can return a reference to them without
// using something like shared_ptr.
scoped_lock locker(m_lock);
history_t *&current = m_histories[name];
if (current == NULL) current = new history_t(name);
return *current;
}
history_t &history_t::history_with_name(const wcstring &name) { return histories.alloc(name); }
history_t::history_t(const wcstring &pname)
: name(pname),
first_unwritten_new_item_index(0),
has_pending_item(false),
disable_automatic_save_counter(0),
mmap_start(NULL),
mmap_length(0),
mmap_type(history_file_type_t(-1)),
mmap_file_id(kInvalidFileID),
boundary_timestamp(time(NULL)),
countdown_to_vacuum(-1),
loaded_old(false),
chaos_mode(false) {
pthread_mutex_init(&lock, NULL);
}
history_t::~history_t() { pthread_mutex_destroy(&lock); }
void history_t::add(const history_item_t &item, bool pending) {
scoped_lock locker(lock);
// Try merging with the last item.
if (!new_items.empty() && new_items.back().merge(item)) {
// We merged, so we don't have to add anything. Maybe this item was pending, but it just got
// merged with an item that is not pending, so pending just becomes false.
this->has_pending_item = false;
} else {
// We have to add a new item.
new_items.push_back(item);
this->has_pending_item = pending;
save_internal_unless_disabled();
}
}
void history_t::save_internal_unless_disabled() {
// This must be called while locked.
ASSERT_IS_LOCKED(lock);
// Respect disable_automatic_save_counter.
if (disable_automatic_save_counter > 0) {
return;
}
// We may or may not vacuum. We try to vacuum every kVacuumFrequency items, but start the
// countdown at a random number so that even if the user never runs more than 25 commands, we'll
// eventually vacuum. If countdown_to_vacuum is -1, it means we haven't yet picked a value for
// the counter.
const int kVacuumFrequency = 25;
if (countdown_to_vacuum < 0) {
static unsigned int seed = (unsigned int)time(NULL);
// Generate a number in the range [0, kVacuumFrequency).
countdown_to_vacuum = rand_r(&seed) / (RAND_MAX / kVacuumFrequency + 1);
}
// Determine if we're going to vacuum.
bool vacuum = false;
if (countdown_to_vacuum == 0) {
countdown_to_vacuum = kVacuumFrequency;
vacuum = true;
}
// This might be a good candidate for moving to a background thread.
time_profiler_t profiler(vacuum ? "save_internal vacuum" //!OCLINT(unused var)
: "save_internal no vacuum"); //!OCLINT(side-effect)
this->save_internal(vacuum);
// Update our countdown.
assert(countdown_to_vacuum > 0);
countdown_to_vacuum--;
}
void history_t::add(const wcstring &str, history_identifier_t ident, bool pending) {
time_t when = time(NULL);
// Big hack: do not allow timestamps equal to our boundary date. This is because we include
// items whose timestamps are equal to our boundary when reading old history, so we can catch
// "just closed" items. But this means that we may interpret our own items, that we just wrote,
// as old items, if we wrote them in the same second as our birthdate.
if (when == this->boundary_timestamp) {
when++;
}
this->add(history_item_t(str, when, ident), pending);
}
// Remove matching history entries from our list of new items. This only supports literal,
// case-sensitive, matches.
void history_t::remove(const wcstring &str_to_remove) {
// Add to our list of deleted items.
deleted_items.insert(str_to_remove);
size_t idx = new_items.size();
while (idx--) {
bool matched = new_items.at(idx).str() == str_to_remove;
if (matched) {
new_items.erase(new_items.begin() + idx);
// If this index is before our first_unwritten_new_item_index, then subtract one from
// that index so it stays pointing at the same item. If it is equal to or larger, then
// we have not yet writen this item, so we don't have to adjust the index.
if (idx < first_unwritten_new_item_index) {
first_unwritten_new_item_index--;
}
}
}
assert(first_unwritten_new_item_index <= new_items.size());
}
void history_t::set_valid_file_paths(const wcstring_list_t &valid_file_paths,
history_identifier_t ident) {
// 0 identifier is used to mean "not necessary".
if (ident == 0) {
return;
}
scoped_lock locker(lock);
// Look for an item with the given identifier. It is likely to be at the end of new_items.
for (history_item_list_t::reverse_iterator iter = new_items.rbegin(); iter != new_items.rend();
++iter) {
if (iter->identifier == ident) { // found it
iter->required_paths = valid_file_paths;
break;
}
}
}
void history_t::get_string_representation(wcstring *result, const wcstring &separator) {
scoped_lock locker(lock);
bool first = true;
std::set<wcstring> seen;
// If we have a pending item, we skip the first encountered (i.e. last) new item.
bool next_is_pending = this->has_pending_item;
// Append new items. Note that in principle we could use const_reverse_iterator, but we do not
// because reverse_iterator is not convertible to const_reverse_iterator. See
// http://github.com/fish-shell/fish-shell/issues/431.
for (history_item_list_t::reverse_iterator iter = new_items.rbegin(); iter < new_items.rend();
++iter) {
// Skip a pending item if we have one.
if (next_is_pending) {
next_is_pending = false;
continue;
}
// Skip duplicates.
if (!seen.insert(iter->str()).second) continue;
if (!first) result->append(separator);
result->append(iter->str());
first = false;
}
// Append old items.
load_old_if_needed();
for (std::deque<size_t>::reverse_iterator iter = old_item_offsets.rbegin();
iter != old_item_offsets.rend(); ++iter) {
size_t offset = *iter;
const history_item_t item =
decode_item(mmap_start + offset, mmap_length - offset, mmap_type);
// Skip duplicates.
if (!seen.insert(item.str()).second) continue;
if (!first) result->append(separator);
result->append(item.str());
first = false;
}
}
history_item_t history_t::item_at_index(size_t idx) {
scoped_lock locker(lock);
// 0 is considered an invalid index.
assert(idx > 0);
idx--;
// Determine how many "resolved" (non-pending) items we have. We can have at most one pending
// item, and it's always the last one.
size_t resolved_new_item_count = new_items.size();
if (this->has_pending_item && resolved_new_item_count > 0) {
resolved_new_item_count -= 1;
}
// idx == 0 corresponds to the last resolved item.
if (idx < resolved_new_item_count) {
return new_items.at(resolved_new_item_count - idx - 1);
}
// Now look in our old items.
idx -= resolved_new_item_count;
load_old_if_needed();
size_t old_item_count = old_item_offsets.size();
if (idx < old_item_count) {
// idx == 0 corresponds to last item in old_item_offsets.
size_t offset = old_item_offsets.at(old_item_count - idx - 1);
return decode_item(mmap_start + offset, mmap_length - offset, mmap_type);
}
// Index past the valid range, so return an empty history item.
return history_item_t(wcstring(), 0);
}
void history_t::populate_from_mmap(void) {
mmap_type = infer_file_type(mmap_start, mmap_length);
size_t cursor = 0;
for (;;) {
size_t offset =
offset_of_next_item(mmap_start, mmap_length, mmap_type, &cursor, boundary_timestamp);
// If we get back -1, we're done.
if (offset == (size_t)-1) break;
// Remember this item.
old_item_offsets.push_back(offset);
}
}
/// Do a private, read-only map of the entirety of a history file with the given name. Returns true
/// if successful. Returns the mapped memory region by reference.
bool history_t::map_file(const wcstring &name, const char **out_map_start, size_t *out_map_len,
file_id_t *file_id) {
wcstring filename = history_filename(name, L"");
if (filename.empty()) {
return false;
}
int fd = wopen_cloexec(filename, O_RDONLY);
if (fd == -1) {
return false;
}
bool result = false;
// Get the file ID if requested.
if (file_id != NULL) *file_id = file_id_for_fd(fd);
// Take a read lock to guard against someone else appending. This is released when the file
// is closed (below). We will read the file after releasing the lock, but that's not a
// problem, because we never modify already written data. In short, the purpose of this lock
// is to ensure we don't see the file size change mid-update.
//
// We may fail to lock (e.g. on lockless NFS - see issue #685. In that case, we proceed as
// if it did not fail. The risk is that we may get an incomplete history item; this is
// unlikely because we only treat an item as valid if it has a terminating newline.
//
// Simulate a failing lock in chaos_mode.
if (!chaos_mode) history_file_lock(fd, LOCK_SH);
off_t len = lseek(fd, 0, SEEK_END);
if (len != (off_t)-1) {
size_t mmap_length = (size_t)len;
if (lseek(fd, 0, SEEK_SET) == 0) {
char *mmap_start;
if ((mmap_start = (char *)mmap(0, mmap_length, PROT_READ, MAP_PRIVATE, fd, 0)) !=
MAP_FAILED) {
result = true;
*out_map_start = mmap_start;
*out_map_len = mmap_length;
}
}
}
if (!chaos_mode) history_file_lock(fd, LOCK_UN);
close(fd);
return result;
}
bool history_t::load_old_if_needed(void) {
if (loaded_old) return true;
loaded_old = true;
// PCA not sure why signals were blocked here
// signal_block();
bool ok = false;
if (map_file(name, &mmap_start, &mmap_length, &mmap_file_id)) {
// Here we've mapped the file.
ok = true;
time_profiler_t profiler("populate_from_mmap"); //!OCLINT(side-effect)
this->populate_from_mmap();
}
// signal_unblock();
return ok;
}
void history_search_t::skip_matches(const wcstring_list_t &skips) {
external_skips = skips;
std::sort(external_skips.begin(), external_skips.end());
}
bool history_search_t::should_skip_match(const wcstring &str) const {
return std::binary_search(external_skips.begin(), external_skips.end(), str);
}
bool history_search_t::go_forwards() {
// Pop the top index (if more than one) and return if we have any left.
if (prev_matches.size() > 1) {
prev_matches.pop_back();
return true;
}
return false;
}
bool history_search_t::go_backwards() {
// Backwards means increasing our index.
const size_t max_idx = (size_t)-1;
size_t idx = 0;
if (!prev_matches.empty()) idx = prev_matches.back().first;
if (idx == max_idx) return false;
const bool main_thread = is_main_thread();
while (++idx < max_idx) {
if (main_thread ? reader_interrupted() : reader_thread_job_is_stale()) {
return false;
}
const history_item_t item = history->item_at_index(idx);
// We're done if it's empty or we cancelled.
if (item.empty()) {
return false;
}
// Look for a term that matches and that we haven't seen before.
const wcstring &str = item.str();
if (item.matches_search(term, search_type, case_sensitive) && !match_already_made(str) &&
!should_skip_match(str)) {
prev_matches.push_back(prev_match_t(idx, item));
return true;
}
}
return false;
}
/// Goes to the end (forwards).
void history_search_t::go_to_end(void) { prev_matches.clear(); }
/// Returns if we are at the end, which is where we start.
bool history_search_t::is_at_end(void) const { return prev_matches.empty(); }
/// Goes to the beginning (backwards).
void history_search_t::go_to_beginning(void) {
// Go backwards as far as we can.
while (go_backwards()) { //!OCLINT(empty while statement)
// Do nothing.
}
}
history_item_t history_search_t::current_item() const {
assert(!prev_matches.empty()); //!OCLINT(double negative)
return prev_matches.back().second;
}
wcstring history_search_t::current_string() const {
history_item_t item = this->current_item();
return item.str();
}
bool history_search_t::match_already_made(const wcstring &match) const {
for (std::vector<prev_match_t>::const_iterator iter = prev_matches.begin();
iter != prev_matches.end(); ++iter) {
if (iter->second.str() == match) return true;
}
return false;
}
static void replace_all(std::string *str, const char *needle, const char *replacement) {
size_t needle_len = strlen(needle), replacement_len = strlen(replacement);
size_t offset = 0;
while ((offset = str->find(needle, offset)) != std::string::npos) {
str->replace(offset, needle_len, replacement);
offset += replacement_len;
}
}
static void escape_yaml(std::string *str) {
replace_all(str, "\\", "\\\\"); // replace one backslash with two
replace_all(str, "\n", "\\n"); // replace newline with backslash + literal n
}
/// This function is called frequently, so it ought to be fast.
static void unescape_yaml(std::string *str) {
size_t cursor = 0, size = str->size();
while (cursor < size) {
// Operate on a const version of str, to avoid needless COWs that at() does.
const std::string &const_str = *str;
// Look for a backslash.
size_t backslash = const_str.find('\\', cursor);
if (backslash == std::string::npos || backslash + 1 >= size) {
// Either not found, or found as the last character.
break;
} else {
// Backslash found. Maybe we'll do something about it. Be sure to invoke the const
// version of at().
char escaped_char = const_str.at(backslash + 1);
if (escaped_char == '\\') {
// Two backslashes in a row. Delete the second one.
str->erase(backslash + 1, 1);
size--;
} else if (escaped_char == 'n') {
// Backslash + n. Replace with a newline.
str->replace(backslash, 2, "\n");
size--;
}
// The character at index backslash has now been made whole; start at the next
// character.
cursor = backslash + 1;
}
}
}
static wcstring history_filename(const wcstring &name, const wcstring &suffix) {
wcstring path;
if (!path_get_data(path)) return L"";
wcstring result = path;
result.append(L"/");
result.append(name);
result.append(L"_history");
result.append(suffix);
return result;
}
void history_t::clear_file_state() {
ASSERT_IS_LOCKED(lock);
// Erase everything we know about our file.
if (mmap_start != NULL && mmap_start != MAP_FAILED) {
munmap((void *)mmap_start, mmap_length);
}
mmap_start = NULL;
mmap_length = 0;
loaded_old = false;
old_item_offsets.clear();
}
void history_t::compact_new_items() {
// Keep only the most recent items with the given contents. This algorithm could be made more
// efficient, but likely would consume more memory too.
std::set<wcstring> seen;
size_t idx = new_items.size();
while (idx--) {
const history_item_t &item = new_items[idx];
if (!seen.insert(item.contents).second) {
// This item was not inserted because it was already in the set, so delete the item at
// this index.
new_items.erase(new_items.begin() + idx);
if (idx < first_unwritten_new_item_index) {
// Decrement first_unwritten_new_item_index if we are deleting a previously written
// item.
first_unwritten_new_item_index--;
}
}
}
}
bool history_t::save_internal_via_rewrite() {
// This must be called while locked.
ASSERT_IS_LOCKED(lock);
bool ok = false;
wcstring tmp_name_template = history_filename(name, L".XXXXXX");
if (!tmp_name_template.empty()) {
// Make an LRU cache to save only the last N elements.
history_lru_cache_t lru(HISTORY_SAVE_MAX);
// Insert old items in, from old to new. Merge them with our new items, inserting items with
// earlier timestamps first.
history_item_list_t::const_iterator new_item_iter = new_items.begin();
// Map in existing items (which may have changed out from underneath us, so don't trust our
// old mmap'd data).
const char *local_mmap_start = NULL;
size_t local_mmap_size = 0;
if (map_file(name, &local_mmap_start, &local_mmap_size, NULL)) {
const history_file_type_t local_mmap_type =
infer_file_type(local_mmap_start, local_mmap_size);
size_t cursor = 0;
for (;;) {
size_t offset = offset_of_next_item(local_mmap_start, local_mmap_size,
local_mmap_type, &cursor, 0);
// If we get back -1, we're done.
if (offset == (size_t)-1) break;
// Try decoding an old item.
const history_item_t old_item = decode_item(
local_mmap_start + offset, local_mmap_size - offset, local_mmap_type);
if (old_item.empty() || deleted_items.count(old_item.str()) > 0) {
// debug(0, L"Item is deleted : %s\n",
// old_item.str().c_str());
continue;
}
// The old item may actually be more recent than our new item, if it came from
// another session. Insert all new items at the given index with an earlier
// timestamp.
for (; new_item_iter != new_items.end(); ++new_item_iter) {
if (new_item_iter->timestamp() < old_item.timestamp()) {
// This "new item" is in fact older.
lru.add_item(*new_item_iter);
} else {
// The new item is not older.
break;
}
}
// Now add this old item.
lru.add_item(old_item);
}
munmap((void *)local_mmap_start, local_mmap_size);
}
// Insert any remaining new items.
for (; new_item_iter != new_items.end(); ++new_item_iter) {
lru.add_item(*new_item_iter);
}
signal_block();
// Try to create a CLO_EXEC temporary file, up to 10 times.
// This should almost always succeed on the first try.
int out_fd = -1;
wcstring tmp_name;
for (size_t attempt = 0; attempt < 10 && out_fd == -1; attempt++) {
char *narrow_str = wcs2str(tmp_name_template.c_str());
out_fd = fish_mkstemp_cloexec(narrow_str);
if (out_fd >= 0) {
tmp_name = str2wcstring(narrow_str);
}
free(narrow_str);
}
if (out_fd >= 0) {
// Write them out.
bool errored = false;
history_output_buffer_t buffer;
for (history_lru_cache_t::iterator iter = lru.begin(); iter != lru.end(); ++iter) {
const wcstring &text = (*iter).first;
const history_lru_item_t &item = (*iter).second;
append_yaml_to_buffer(text, item.timestamp, item.required_paths, &buffer);
if (buffer.output_size() >= HISTORY_OUTPUT_BUFFER_SIZE &&
!buffer.flush_to_fd(out_fd)) {
errored = true;
break;
}
}
if (!errored && buffer.flush_to_fd(out_fd)) {
ok = true;
}
if (!ok) {
// This message does not have high enough priority to be shown by default.
debug(2, L"Error when writing history file");
} else {
wcstring new_name = history_filename(name, wcstring());
// Ensure we maintain the ownership and permissions of the original (#2355). If the
// stat fails, we assume (hope) our default permissions are correct. This
// corresponds to e.g. someone running sudo -E as the very first command. If they
// did, it would be tricky to set the permissions correctly. (bash doesn't get this
// case right either).
struct stat sbuf;
if (wstat(new_name, &sbuf) >= 0) { // success
if (fchown(out_fd, sbuf.st_uid, sbuf.st_gid) == -1) {
debug(2, L"Error %d when changing ownership of history file", errno);
}
if (fchmod(out_fd, sbuf.st_mode) == -1) {
debug(2, L"Error %d when changing mode of history file", errno);
}
}
if (wrename(tmp_name, new_name) == -1) {
debug(2, L"Error %d when renaming history file", errno);
}
}
close(out_fd);
}
signal_unblock();
// Make sure we clear all nodes, since this doesn't happen automatically.
lru.evict_all_nodes();
}
if (ok) {
// We've saved everything, so we have no more unsaved items.
this->first_unwritten_new_item_index = new_items.size();
// We deleted our deleted items.
this->deleted_items.clear();
// Our history has been written to the file, so clear our state so we can re-reference the
// file.
this->clear_file_state();
}
return ok;
}
bool history_t::save_internal_via_appending() {
// This must be called while locked.
ASSERT_IS_LOCKED(lock);
// No deleting allowed.
assert(deleted_items.empty());
bool ok = false;
// If the file is different (someone vacuumed it) then we need to update our mmap.
bool file_changed = false;
// Get the path to the real history file.
wcstring history_path = history_filename(name, wcstring());
signal_block();
// Open the file.
int out_fd = wopen_cloexec(history_path, O_WRONLY | O_APPEND);
if (out_fd >= 0) {
// Check to see if the file changed.
if (file_id_for_fd(out_fd) != mmap_file_id) file_changed = true;
// Exclusive lock on the entire file. This is released when we close the file (below). This
// may fail on (e.g.) lockless NFS. If so, proceed as if it did not fail; the risk is that
// we may get interleaved history items, which is considered better than no history, or
// forcing everything through the slow copy-move mode. We try to minimize this possibility
// by writing with O_APPEND.
//
// Simulate a failing lock in chaos_mode
if (!chaos_mode) history_file_lock(out_fd, LOCK_EX);
// We (hopefully successfully) took the exclusive lock. Append to the file.
// Note that this is sketchy for a few reasons:
// - Another shell may have appended its own items with a later timestamp, so our file may
// no longer be sorted by timestamp.
// - Another shell may have appended the same items, so our file may now contain
// duplicates.
//
// We cannot modify any previous parts of our file, because other instances may be reading
// those portions. We can only append.
//
// Originally we always rewrote the file on saving, which avoided both of these problems.
// However, appending allows us to save history after every command, which is nice!
//
// Periodically we "clean up" the file by rewriting it, so that most of the time it doesn't
// have duplicates, although we don't yet sort by timestamp (the timestamp isn't really used
// for much anyways).
// So far so good. Write all items at or after first_unwritten_new_item_index. Note that we
// write even a pending item - pending items are ignored by history within the command
// itself, but should still be written to the file.
bool errored = false;
history_output_buffer_t buffer;
while (first_unwritten_new_item_index < new_items.size()) {
const history_item_t &item = new_items.at(first_unwritten_new_item_index);
append_yaml_to_buffer(item.str(), item.timestamp(), item.get_required_paths(), &buffer);
if (buffer.output_size() >= HISTORY_OUTPUT_BUFFER_SIZE) {
errored = !buffer.flush_to_fd(out_fd);
if (errored) break;
}
// We wrote this item, hooray.
first_unwritten_new_item_index++;
}
if (!errored && buffer.flush_to_fd(out_fd)) {
ok = true;
}
if (!chaos_mode) history_file_lock(out_fd, LOCK_UN);
close(out_fd);
}
signal_unblock();
// If someone has replaced the file, forget our file state.
if (file_changed) {
this->clear_file_state();
}
return ok;
}
/// Save the specified mode to file; optionally also vacuums.
void history_t::save_internal(bool vacuum) {
ASSERT_IS_LOCKED(lock);
// Nothing to do if there's no new items.
if (first_unwritten_new_item_index >= new_items.size() && deleted_items.empty()) return;
// Compact our new items so we don't have duplicates.
this->compact_new_items();
// Try saving. If we have items to delete, we have to rewrite the file. If we do not, we can
// append to it.
bool ok = false;
if (!vacuum && deleted_items.empty()) {
// Try doing a fast append.
ok = save_internal_via_appending();
}
if (!ok) {
// We did not or could not append; rewrite the file ("vacuum" it).
this->save_internal_via_rewrite();
}
}
void history_t::save(void) {
scoped_lock locker(lock);
this->save_internal(false);
}
// Formats a single history record, including a trailing newline. Returns true
// if bytes were written to the output stream and false otherwise.
static bool format_history_record(const history_item_t &item, const wchar_t *show_time_format,
bool null_terminate, io_streams_t &streams) {
if (show_time_format) {
const time_t seconds = item.timestamp();
struct tm timestamp;
if (!localtime_r(&seconds, &timestamp)) return false;
const int max_tstamp_length = 100;
wchar_t timestamp_string[max_tstamp_length + 1];
if (std::wcsftime(timestamp_string, max_tstamp_length, show_time_format, &timestamp) == 0) {
return false;
}
streams.out.append(timestamp_string);
}
streams.out.append(item.str());
if (null_terminate) {
streams.out.append(L'\0');
} else {
streams.out.append(L'\n');
}
return true;
}
bool history_t::search(history_search_type_t search_type, wcstring_list_t search_args,
const wchar_t *show_time_format, long max_items, bool case_sensitive,
bool null_terminate, io_streams_t &streams) {
// scoped_lock locker(lock);
if (search_args.empty()) {
// Start at one because zero is the current command.
for (int i = 1; !this->item_at_index(i).empty() && max_items; ++i, --max_items) {
if (!format_history_record(this->item_at_index(i), show_time_format, null_terminate,
streams)) {
return false;
}
}
return true;
}
for (wcstring_list_t::const_iterator iter = search_args.begin(); iter != search_args.end();
++iter) {
const wcstring &search_string = *iter;
if (search_string.empty()) {
streams.err.append_format(L"Searching for the empty string isn't allowed");
return false;
}
history_search_t searcher =
history_search_t(*this, search_string, search_type, case_sensitive);
while (searcher.go_backwards()) {
if (!format_history_record(searcher.current_item(), show_time_format, null_terminate,
streams)) {
return false;
}
if (--max_items == 0) return true;
}
}
return true;
}
void history_t::disable_automatic_saving() {
scoped_lock locker(lock);
disable_automatic_save_counter++;
assert(disable_automatic_save_counter != 0); // overflow!
}
void history_t::enable_automatic_saving() {
scoped_lock locker(lock);
assert(disable_automatic_save_counter > 0); // underflow
disable_automatic_save_counter--;
save_internal_unless_disabled();
}
void history_t::clear(void) {
scoped_lock locker(lock);
new_items.clear();
deleted_items.clear();
first_unwritten_new_item_index = 0;
old_item_offsets.clear();
wcstring filename = history_filename(name, L"");
if (!filename.empty()) wunlink(filename);
this->clear_file_state();
}
bool history_t::is_empty(void) {
scoped_lock locker(lock);
// If we have new items, we're not empty.
if (!new_items.empty()) return false;
bool empty = false;
if (loaded_old) {
// If we've loaded old items, see if we have any offsets.
empty = old_item_offsets.empty();
} else {
// If we have not loaded old items, don't actually load them (which may be expensive); just
// stat the file and see if it exists and is nonempty.
const wcstring where = history_filename(name, L"");
struct stat buf = {};
if (wstat(where, &buf) != 0) {
// Access failed, assume missing.
empty = true;
} else {
// We're empty if the file is empty.
empty = (buf.st_size == 0);
}
}
return empty;
}
/// Populates from older location (in config path, rather than data path) This is accomplished by
/// clearing ourselves, and copying the contents of the old history file to the new history file.
/// The new contents will automatically be re-mapped later.
void history_t::populate_from_config_path() {
wcstring old_file;
if (path_get_config(old_file)) {
old_file.append(L"/");
old_file.append(name);
old_file.append(L"_history");
int src_fd = wopen_cloexec(old_file, O_RDONLY, 0);
if (src_fd != -1) {
wcstring new_file = history_filename(name, wcstring());
// Clear must come after we've retrieved the new_file name, and before we open
// destination file descriptor, since it destroys the name and the file.
this->clear();
int dst_fd = wopen_cloexec(new_file, O_WRONLY | O_CREAT, 0644);
char buf[BUFSIZ];
ssize_t size;
while ((size = read(src_fd, buf, BUFSIZ)) > 0) {
ssize_t written = write(dst_fd, buf, static_cast<size_t>(size));
if (written < 0) {
// This message does not have high enough priority to be shown by default.
debug(2, L"Error when writing history file");
break;
}
}
close(src_fd);
close(dst_fd);
}
}
}
/// Indicate whether we ought to import the bash history file into fish.
static bool should_import_bash_history_line(const std::string &line) {
if (line.empty()) return false;
// Very naive tests! Skip export; probably should skip others.
const char *const ignore_prefixes[] = {"export ", "#"};
for (size_t i = 0; i < sizeof ignore_prefixes / sizeof *ignore_prefixes; i++) {
const char *prefix = ignore_prefixes[i];
if (!line.compare(0, strlen(prefix), prefix)) {
return false;
}
}
// Skip lines with backticks.
if (line.find('`') != std::string::npos) return false;
return true;
}
void history_t::populate_from_bash(FILE *stream) {
// Bash's format is very simple: just lines with #s for comments. Ignore a few commands that are
// bash-specific. This list ought to be expanded.
std::string line;
for (;;) {
line.clear();
bool success = false;
bool has_newline = false;
// Loop until we've read a line.
do {
char buff[128];
success = (bool)fgets(buff, sizeof buff, stream);
if (success) {
// Skip the newline.
char *a_newline = strchr(buff, '\n');
if (a_newline) *a_newline = '\0';
has_newline = (a_newline != NULL);
// Append what we've got.
line.append(buff);
}
} while (success && !has_newline);
// Maybe add this line.
if (should_import_bash_history_line(line)) {
this->add(str2wcstring(line));
}
if (line.empty()) break;
}
}
void history_t::incorporate_external_changes() {
// To incorporate new items, we simply update our timestamp to now, so that items from previous
// instances get added. We then clear the file state so that we remap the file. Note that this
// is somehwhat expensive because we will be going back over old items. An optimization would be
// to preserve old_item_offsets so that they don't have to be recomputed. (However, then items
// *deleted* in other instances would not show up here).
time_t new_timestamp = time(NULL);
scoped_lock locker(lock);
// If for some reason the clock went backwards, we don't want to start dropping items; therefore
// we only do work if time has progressed. This also makes multiple calls cheap.
if (new_timestamp > this->boundary_timestamp) {
this->boundary_timestamp = new_timestamp;
this->clear_file_state();
// We also need to erase new_items, since we go through those first, and that means we
// will not properly interleave them with items from other instances.
// We'll pick them up from the file (#2312)
this->save_internal(false);
this->new_items.clear();
this->first_unwritten_new_item_index = 0;
}
}
void history_init() {}
void history_collection_t::save() {
// Save all histories.
for (std::map<wcstring, history_t *>::iterator iter = m_histories.begin();
iter != m_histories.end(); ++iter) {
history_t *hist = iter->second;
hist->save();
}
}
void history_destroy() { histories.save(); }
void history_sanity_check() {
// No sanity checking implemented yet...
}
path_list_t valid_paths(const path_list_t &paths, const wcstring &working_directory) {
ASSERT_IS_BACKGROUND_THREAD();
wcstring_list_t result;
for (const wcstring &path : paths) {
if (path_is_valid(path, working_directory)) {
result.push_back(path);
}
}
return result;
}
bool all_paths_are_valid(const path_list_t &paths, const wcstring &working_directory) {
ASSERT_IS_BACKGROUND_THREAD();
for (const wcstring &path : paths) {
if (!path_is_valid(path, working_directory)) {
return false;
}
}
return true;
}
static bool string_could_be_path(const wcstring &potential_path) {
// Assume that things with leading dashes aren't paths.
if (potential_path.empty() || potential_path.at(0) == L'-') {
return false;
}
return true;
}
void history_t::add_pending_with_file_detection(const wcstring &str) {
ASSERT_IS_MAIN_THREAD();
// Find all arguments that look like they could be file paths.
bool impending_exit = false;
parse_node_tree_t tree;
parse_tree_from_string(str, parse_flag_none, &tree, NULL);
size_t count = tree.size();
path_list_t potential_paths;
for (size_t i = 0; i < count; i++) {
const parse_node_t &node = tree.at(i);
if (!node.has_source()) {
continue;
}
if (node.type == symbol_argument) {
wcstring potential_path = node.get_source(str);
bool unescaped = unescape_string_in_place(&potential_path, UNESCAPE_DEFAULT);
if (unescaped && string_could_be_path(potential_path)) {
potential_paths.push_back(potential_path);
}
} else if (node.type == symbol_plain_statement) {
// Hack hack hack - if the command is likely to trigger an exit, then don't do
// background file detection, because we won't be able to write it to our history file
// before we exit.
if (tree.decoration_for_plain_statement(node) == parse_statement_decoration_exec) {
impending_exit = true;
}
wcstring command;
tree.command_for_plain_statement(node, str, &command);
unescape_string_in_place(&command, UNESCAPE_DEFAULT);
if (contains(command, L"exit", L"reboot")) {
impending_exit = true;
}
}
}
// If we got a path, we'll perform file detection for autosuggestion hinting.
history_identifier_t identifier = 0;
if (!potential_paths.empty() && !impending_exit) {
// Grab the next identifier.
static history_identifier_t sLastIdentifier = 0;
identifier = ++sLastIdentifier;
// Prevent saving until we're done, so we have time to get the paths.
this->disable_automatic_saving();
// Check for which paths are valid on a background thread,
// then on the main thread update our history item
const wcstring wd = env_get_pwd_slash();
iothread_perform([=]() { return valid_paths(potential_paths, wd); },
[=](path_list_t validated_paths) {
this->set_valid_file_paths(validated_paths, identifier);
this->enable_automatic_saving();
});
}
// Actually add the item to the history.
this->add(str, identifier, true /* pending */);
// If we think we're about to exit, save immediately, regardless of any disabling. This may
// cause us to lose file hinting for some commands, but it beats losing history items.
if (impending_exit) {
this->save();
}
}
/// Very simple, just mark that we have no more pending items.
void history_t::resolve_pending() {
scoped_lock locker(lock);
this->has_pending_item = false;
}