mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-11-22 10:57:30 +08:00
Switch history_t to use owning_lock internally
This removes a lot of tricky manual locking. It also removes the "main thread" dependency.
This commit is contained in:
parent
6bed255a52
commit
360116221a
18
src/common.h
18
src/common.h
|
@ -612,12 +612,16 @@ typedef std::lock_guard<std::recursive_mutex> scoped_rlock;
|
|||
//
|
||||
template <typename Data>
|
||||
class acquired_lock {
|
||||
std::unique_lock<std::mutex> lock;
|
||||
acquired_lock(std::mutex &lk, Data *v) : lock(lk), value(v) {}
|
||||
|
||||
template <typename T>
|
||||
friend class owning_lock;
|
||||
|
||||
template <typename T>
|
||||
friend class acquired_lock;
|
||||
|
||||
acquired_lock(std::mutex &lk, Data *v) : lock(lk), value(v) {}
|
||||
acquired_lock(std::unique_lock<std::mutex> &&lk, Data *v) : lock(std::move(lk)), value(v) {}
|
||||
|
||||
std::unique_lock<std::mutex> lock;
|
||||
Data *value;
|
||||
|
||||
public:
|
||||
|
@ -632,6 +636,14 @@ class acquired_lock {
|
|||
Data &operator*() { return *value; }
|
||||
const Data &operator*() const { return *value; }
|
||||
|
||||
/// Implicit conversion to const version.
|
||||
operator acquired_lock<const Data>() {
|
||||
// We're about to give up our lock, don't hold onto the data.
|
||||
const Data *cvalue = value;
|
||||
value = nullptr;
|
||||
return acquired_lock<const Data>(std::move(lock), cvalue);
|
||||
}
|
||||
|
||||
/// Create from a global lock.
|
||||
/// This is used in weird cases where a global lock protects more than one piece of data.
|
||||
static acquired_lock from_global(std::mutex &lk, Data *v) { return acquired_lock{lk, v}; }
|
||||
|
|
528
src/history.cpp
528
src/history.cpp
|
@ -324,9 +324,6 @@ class history_file_contents_t {
|
|||
}
|
||||
};
|
||||
|
||||
/// The set of all histories.
|
||||
static owning_lock<std::map<wcstring, std::unique_ptr<history_t>>> s_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
|
||||
|
@ -808,29 +805,148 @@ static size_t offset_of_next_item(const history_file_contents_t &contents, size_
|
|||
return size_t(-1);
|
||||
}
|
||||
|
||||
history_t &history_t::history_with_name(const wcstring &name) {
|
||||
// Return a history for the given name, creating it if necessary
|
||||
// Note that histories are currently never deleted, so we can return a reference to them without
|
||||
// using something like shared_ptr
|
||||
auto hs = s_histories.acquire();
|
||||
std::unique_ptr<history_t> &hist = (*hs)[name];
|
||||
if (!hist) {
|
||||
hist = make_unique<history_t>(name);
|
||||
}
|
||||
return *hist;
|
||||
}
|
||||
struct history_impl_t {
|
||||
// Privately add an item. If pending, the item will not be returned by history searches until a
|
||||
// call to resolve_pending.
|
||||
void add(const history_item_t &item, bool pending = false);
|
||||
|
||||
history_t::history_t(wcstring pname)
|
||||
: name(std::move(pname)), history_file_id(kInvalidFileID), boundary_timestamp(time(NULL)) {}
|
||||
// Internal function.
|
||||
void clear_file_state();
|
||||
|
||||
history_t::~history_t() = default;
|
||||
// The name of this list. Used for picking a suitable filename and for switching modes.
|
||||
const wcstring name;
|
||||
|
||||
bool history_t::chaos_mode = false;
|
||||
bool history_t::never_mmap = false;
|
||||
// New items. Note that these are NOT discarded on save. We need to keep these around so we can
|
||||
// distinguish between items in our history and items in the history of other shells that were
|
||||
// started after we were started.
|
||||
history_item_list_t new_items;
|
||||
|
||||
void history_t::add(const history_item_t &item, bool pending) {
|
||||
scoped_lock locker(lock);
|
||||
// The index of the first new item that we have not yet written.
|
||||
size_t first_unwritten_new_item_index{0};
|
||||
|
||||
// Whether we have a pending item. If so, the most recently added item is ignored by
|
||||
// item_at_index.
|
||||
bool has_pending_item{false};
|
||||
|
||||
// Whether we should disable saving to the file for a time.
|
||||
uint32_t disable_automatic_save_counter{0};
|
||||
|
||||
// Deleted item contents.
|
||||
std::unordered_set<wcstring> deleted_items{};
|
||||
|
||||
// The buffer containing the history file contents.
|
||||
std::unique_ptr<history_file_contents_t> file_contents{};
|
||||
|
||||
// The file ID of the history file.
|
||||
file_id_t history_file_id{kInvalidFileID};
|
||||
|
||||
// The boundary timestamp distinguishes old items from new items. Items whose timestamps are <=
|
||||
// the boundary are considered "old". Items whose timestemps are > the boundary are new, and are
|
||||
// ignored by this instance (unless they came from this instance). The timestamp may be adjusted
|
||||
// by incorporate_external_changes().
|
||||
time_t boundary_timestamp{time(NULL)};
|
||||
|
||||
// How many items we add until the next vacuum. Initially a random value.
|
||||
int countdown_to_vacuum{-1};
|
||||
|
||||
// Whether we've loaded old items.
|
||||
bool loaded_old{false};
|
||||
|
||||
// List of old items, as offsets into out mmap data.
|
||||
std::deque<size_t> old_item_offsets{};
|
||||
|
||||
// Figure out the offsets of our file contents.
|
||||
void populate_from_file_contents();
|
||||
|
||||
// Loads old items if necessary.
|
||||
void load_old_if_needed();
|
||||
|
||||
// Reads the history file if necessary.
|
||||
bool mmap_if_needed();
|
||||
|
||||
// Deletes duplicates in new_items.
|
||||
void compact_new_items();
|
||||
|
||||
// Attempts to rewrite the existing file to a target temporary file
|
||||
// Returns false on error, true on success
|
||||
bool rewrite_to_temporary_file(int existing_fd, int dst_fd) const;
|
||||
|
||||
// Saves history by rewriting the file.
|
||||
bool save_internal_via_rewrite();
|
||||
|
||||
// Saves history by appending to the file.
|
||||
bool save_internal_via_appending();
|
||||
|
||||
// Saves history.
|
||||
void save(bool vacuum = false);
|
||||
|
||||
// Saves history unless doing so is disabled.
|
||||
void save_unless_disabled();
|
||||
|
||||
explicit history_impl_t(wcstring name) : name(std::move(name)) {}
|
||||
history_impl_t(history_impl_t &&) = default;
|
||||
~history_impl_t() = default;
|
||||
|
||||
/// Returns whether this is using the default name.
|
||||
bool is_default() const;
|
||||
|
||||
// Determines whether the history is empty. Unfortunately this cannot be const, since it may
|
||||
// require populating the history.
|
||||
bool is_empty();
|
||||
|
||||
// Add a new history item to the end. If pending is set, the item will not be returned by
|
||||
// item_at_index until a call to resolve_pending(). Pending items are tracked with an offset
|
||||
// into the array of new items, so adding a non-pending item has the effect of resolving all
|
||||
// pending items.
|
||||
void add(const wcstring &str, history_identifier_t ident = 0, bool pending = false);
|
||||
|
||||
// Remove a history item.
|
||||
void remove(const wcstring &str);
|
||||
|
||||
// Add a new pending history item to the end, and then begin file detection on the items to
|
||||
// determine which arguments are paths
|
||||
void add_pending_with_file_detection(const wcstring &str, const wcstring &working_dir_slash);
|
||||
|
||||
// Resolves any pending history items, so that they may be returned in history searches.
|
||||
void resolve_pending();
|
||||
|
||||
// Enable / disable automatic saving. Main thread only!
|
||||
void disable_automatic_saving();
|
||||
void enable_automatic_saving();
|
||||
|
||||
// Irreversibly clears history.
|
||||
void clear();
|
||||
|
||||
// Populates from older location ()in config path, rather than data path).
|
||||
void populate_from_config_path();
|
||||
|
||||
// Populates from a bash history file.
|
||||
void populate_from_bash(FILE *f);
|
||||
|
||||
// Incorporates the history of other shells into this history.
|
||||
void incorporate_external_changes();
|
||||
|
||||
// Gets all the history into a list. This is intended for the $history environment variable.
|
||||
// This may be long!
|
||||
void get_history(wcstring_list_t &result);
|
||||
|
||||
// Let indexes be a list of one-based indexes into the history, matching the interpretation of
|
||||
// $history. That is, $history[1] is the most recently executed command. Values less than one
|
||||
// are skipped. Return a mapping from index to history item text.
|
||||
std::unordered_map<long, wcstring> items_at_indexes(const std::vector<long> &idxs);
|
||||
|
||||
// Sets the valid file paths for the history item with the given identifier.
|
||||
void set_valid_file_paths(const wcstring_list_t &valid_file_paths, history_identifier_t ident);
|
||||
|
||||
// Return the specified history at the specified index. 0 is the index of the current
|
||||
// commandline. (So the most recent item is at index 1.)
|
||||
history_item_t item_at_index(size_t idx);
|
||||
|
||||
// Return the number of history entries.
|
||||
size_t size();
|
||||
};
|
||||
|
||||
void history_impl_t::add(const history_item_t &item, bool pending) {
|
||||
// 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
|
||||
|
@ -840,14 +956,11 @@ void history_t::add(const history_item_t &item, bool pending) {
|
|||
// We have to add a new item.
|
||||
new_items.push_back(item);
|
||||
this->has_pending_item = pending;
|
||||
save_internal_unless_disabled();
|
||||
save_unless_disabled();
|
||||
}
|
||||
}
|
||||
|
||||
void history_t::save_internal_unless_disabled() {
|
||||
// This must be called while locked.
|
||||
ASSERT_IS_LOCKED(lock);
|
||||
|
||||
void history_impl_t::save_unless_disabled() {
|
||||
// Respect disable_automatic_save_counter.
|
||||
if (disable_automatic_save_counter > 0) {
|
||||
return;
|
||||
|
@ -872,16 +985,16 @@ void history_t::save_internal_unless_disabled() {
|
|||
}
|
||||
|
||||
// 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);
|
||||
time_profiler_t profiler(vacuum ? "save vacuum" //!OCLINT(unused var)
|
||||
: "save no vacuum"); //!OCLINT(side-effect)
|
||||
this->save(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) {
|
||||
void history_impl_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
|
||||
|
@ -896,7 +1009,7 @@ void history_t::add(const wcstring &str, history_identifier_t ident, bool pendin
|
|||
|
||||
// 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) {
|
||||
void history_impl_t::remove(const wcstring &str_to_remove) {
|
||||
// Add to our list of deleted items.
|
||||
deleted_items.insert(str_to_remove);
|
||||
|
||||
|
@ -916,15 +1029,13 @@ void history_t::remove(const wcstring &str_to_remove) {
|
|||
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) {
|
||||
void history_impl_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) {
|
||||
|
@ -935,9 +1046,7 @@ void history_t::set_valid_file_paths(const wcstring_list_t &valid_file_paths,
|
|||
}
|
||||
}
|
||||
|
||||
void history_t::get_history(wcstring_list_t &result) {
|
||||
scoped_lock locker(lock);
|
||||
|
||||
void history_impl_t::get_history(wcstring_list_t &result) {
|
||||
// If we have a pending item, we skip the first encountered (i.e. last) new item.
|
||||
bool next_is_pending = this->has_pending_item;
|
||||
std::unordered_set<wcstring> seen;
|
||||
|
@ -962,8 +1071,7 @@ void history_t::get_history(wcstring_list_t &result) {
|
|||
}
|
||||
}
|
||||
|
||||
size_t history_t::size() {
|
||||
scoped_lock locker(lock);
|
||||
size_t history_impl_t::size() {
|
||||
size_t new_item_count = new_items.size();
|
||||
if (this->has_pending_item && new_item_count > 0) new_item_count -= 1;
|
||||
load_old_if_needed();
|
||||
|
@ -971,9 +1079,7 @@ size_t history_t::size() {
|
|||
return new_item_count + old_item_count;
|
||||
}
|
||||
|
||||
history_item_t history_t::item_at_index_assume_locked(size_t idx) {
|
||||
ASSERT_IS_LOCKED(lock);
|
||||
|
||||
history_item_t history_impl_t::item_at_index(size_t idx) {
|
||||
// 0 is considered an invalid index.
|
||||
assert(idx > 0);
|
||||
idx--;
|
||||
|
@ -1004,13 +1110,7 @@ history_item_t history_t::item_at_index_assume_locked(size_t idx) {
|
|||
return history_item_t(wcstring(), 0);
|
||||
}
|
||||
|
||||
history_item_t history_t::item_at_index(size_t idx) {
|
||||
scoped_lock locker(lock);
|
||||
return item_at_index_assume_locked(idx);
|
||||
}
|
||||
|
||||
std::unordered_map<long, wcstring> history_t::items_at_indexes(const std::vector<long> &idxs) {
|
||||
scoped_lock locker(lock);
|
||||
std::unordered_map<long, wcstring> history_impl_t::items_at_indexes(const std::vector<long> &idxs) {
|
||||
std::unordered_map<long, wcstring> result;
|
||||
for (long idx : idxs) {
|
||||
if (idx <= 0) {
|
||||
|
@ -1022,14 +1122,14 @@ std::unordered_map<long, wcstring> history_t::items_at_indexes(const std::vector
|
|||
auto iter_inserted = result.emplace(idx, wcstring{});
|
||||
if (iter_inserted.second) {
|
||||
// New key.
|
||||
auto item = item_at_index_assume_locked(size_t(idx));
|
||||
auto item = item_at_index(size_t(idx));
|
||||
iter_inserted.first->second = std::move(item.contents);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void history_t::populate_from_file_contents() {
|
||||
void history_impl_t::populate_from_file_contents() {
|
||||
old_item_offsets.clear();
|
||||
if (file_contents) {
|
||||
size_t cursor = 0;
|
||||
|
@ -1044,7 +1144,7 @@ void history_t::populate_from_file_contents() {
|
|||
}
|
||||
}
|
||||
|
||||
void history_t::load_old_if_needed() {
|
||||
void history_impl_t::load_old_if_needed() {
|
||||
if (loaded_old) return;
|
||||
loaded_old = true;
|
||||
|
||||
|
@ -1063,10 +1163,10 @@ void history_t::load_old_if_needed() {
|
|||
// 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);
|
||||
if (!history_t::chaos_mode) history_file_lock(fd, LOCK_SH);
|
||||
file_contents = history_file_contents_t::create(fd);
|
||||
this->history_file_id = file_contents ? file_id_for_fd(fd) : kInvalidFileID;
|
||||
if (!chaos_mode) history_file_lock(fd, LOCK_UN);
|
||||
if (!history_t::chaos_mode) history_file_lock(fd, LOCK_UN);
|
||||
close(fd);
|
||||
|
||||
time_profiler_t profiler("populate_from_file_contents"); //!OCLINT(side-effect)
|
||||
|
@ -1182,15 +1282,14 @@ static wcstring history_filename(const wcstring &session_id, const wcstring &suf
|
|||
return result;
|
||||
}
|
||||
|
||||
void history_t::clear_file_state() {
|
||||
ASSERT_IS_LOCKED(lock);
|
||||
void history_impl_t::clear_file_state() {
|
||||
// Erase everything we know about our file.
|
||||
file_contents.reset();
|
||||
loaded_old = false;
|
||||
old_item_offsets.clear();
|
||||
}
|
||||
|
||||
void history_t::compact_new_items() {
|
||||
void history_impl_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::unordered_set<wcstring> seen;
|
||||
|
@ -1214,10 +1313,7 @@ void history_t::compact_new_items() {
|
|||
// Given the fd of an existing history file, or -1 if none, write
|
||||
// a new history file to temp_fd. Returns true on success, false
|
||||
// on error
|
||||
bool history_t::rewrite_to_temporary_file(int existing_fd, int dst_fd) const {
|
||||
// This must be called while locked.
|
||||
ASSERT_IS_LOCKED(lock);
|
||||
|
||||
bool history_impl_t::rewrite_to_temporary_file(int existing_fd, int dst_fd) const {
|
||||
// We are reading FROM existing_fd and writing TO dst_fd
|
||||
// dst_fd must be valid; existing_fd does not need to be
|
||||
assert(dst_fd >= 0);
|
||||
|
@ -1299,9 +1395,7 @@ static int create_temporary_file(const wcstring &name_template, wcstring *out_pa
|
|||
return out_fd;
|
||||
}
|
||||
|
||||
bool history_t::save_internal_via_rewrite() {
|
||||
// This must be called while locked.
|
||||
ASSERT_IS_LOCKED(lock);
|
||||
bool history_impl_t::save_internal_via_rewrite() {
|
||||
bool ok = false;
|
||||
|
||||
// We want to rewrite the file, while holding the lock for as briefly as possible
|
||||
|
@ -1409,10 +1503,7 @@ bool history_t::save_internal_via_rewrite() {
|
|||
|
||||
// Function called to save our unwritten history file by appending to the existing history file
|
||||
// Returns true on success, false on failure.
|
||||
bool history_t::save_internal_via_appending() {
|
||||
// This must be called while locked.
|
||||
ASSERT_IS_LOCKED(lock);
|
||||
|
||||
bool history_impl_t::save_internal_via_appending() {
|
||||
// No deleting allowed.
|
||||
assert(deleted_items.empty());
|
||||
|
||||
|
@ -1445,7 +1536,7 @@ bool history_t::save_internal_via_appending() {
|
|||
// by writing with O_APPEND.
|
||||
//
|
||||
// Simulate a failing lock in chaos_mode
|
||||
if (!chaos_mode) history_file_lock(fd, LOCK_EX);
|
||||
if (!history_t::chaos_mode) history_file_lock(fd, LOCK_EX);
|
||||
const file_id_t file_id = file_id_for_fd(fd);
|
||||
if (file_id_for_path(history_path) != file_id) {
|
||||
// The file has changed, we're going to retry
|
||||
|
@ -1521,9 +1612,7 @@ bool history_t::save_internal_via_appending() {
|
|||
}
|
||||
|
||||
/// Save the specified mode to file; optionally also vacuums.
|
||||
void history_t::save_internal(bool vacuum) {
|
||||
ASSERT_IS_LOCKED(lock);
|
||||
|
||||
void history_impl_t::save(bool vacuum) {
|
||||
// Nothing to do if there's no new items.
|
||||
if (first_unwritten_new_item_index >= new_items.size() && deleted_items.empty()) return;
|
||||
|
||||
|
@ -1550,11 +1639,6 @@ void history_t::save_internal(bool vacuum) {
|
|||
}
|
||||
}
|
||||
|
||||
void history_t::save() {
|
||||
scoped_lock locker(lock);
|
||||
this->save_internal(false);
|
||||
}
|
||||
|
||||
// Formats a single history record, including a trailing newline.
|
||||
//
|
||||
// Returns nothing. The only possible failure involves formatting the timestamp. If that happens we
|
||||
|
@ -1582,94 +1666,18 @@ static void format_history_record(const history_item_t &item, const wchar_t *sho
|
|||
}
|
||||
}
|
||||
|
||||
/// This handles the slightly unusual case of someone searching history for
|
||||
/// specific terms/patterns.
|
||||
bool history_t::search_with_args(history_search_type_t search_type, wcstring_list_t search_args,
|
||||
const wchar_t *show_time_format, size_t max_items,
|
||||
bool case_sensitive, bool null_terminate, bool reverse,
|
||||
io_streams_t &streams) {
|
||||
wcstring_list_t results;
|
||||
size_t hist_size = this->size();
|
||||
if (max_items > hist_size) max_items = hist_size;
|
||||
|
||||
for (const wcstring &search_string : search_args) {
|
||||
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 ? 0 : history_search_ignore_case);
|
||||
while (searcher.go_backwards()) {
|
||||
wcstring result;
|
||||
auto cur_item = searcher.current_item();
|
||||
format_history_record(cur_item, show_time_format, null_terminate, result);
|
||||
if (reverse) {
|
||||
results.push_back(result);
|
||||
} else {
|
||||
streams.out.append(result);
|
||||
}
|
||||
if (--max_items == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reverse) {
|
||||
for (auto it = results.rbegin(); it != results.rend(); it++) {
|
||||
streams.out.append(*it);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool history_t::search(history_search_type_t search_type, wcstring_list_t search_args,
|
||||
const wchar_t *show_time_format, size_t max_items, bool case_sensitive,
|
||||
bool null_terminate, bool reverse, io_streams_t &streams) {
|
||||
if (!search_args.empty()) {
|
||||
// User wants the results filtered. This is not the common case so we do it separate
|
||||
// from the code below for unfiltered output which is much cheaper.
|
||||
return search_with_args(search_type, search_args, show_time_format, max_items,
|
||||
case_sensitive, null_terminate, reverse, streams);
|
||||
}
|
||||
|
||||
// scoped_lock locker(lock);
|
||||
size_t hist_size = this->size();
|
||||
if (max_items > hist_size) max_items = hist_size;
|
||||
|
||||
if (reverse) {
|
||||
for (size_t i = max_items; i != 0; --i) {
|
||||
auto cur_item = this->item_at_index(i);
|
||||
wcstring result;
|
||||
format_history_record(cur_item, show_time_format, null_terminate, result);
|
||||
streams.out.append(result);
|
||||
}
|
||||
} else {
|
||||
// Start at one because zero is the current command.
|
||||
for (size_t i = 1; i < max_items + 1; ++i) {
|
||||
auto cur_item = this->item_at_index(i);
|
||||
wcstring result;
|
||||
format_history_record(cur_item, show_time_format, null_terminate, result);
|
||||
streams.out.append(result);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void history_t::disable_automatic_saving() {
|
||||
scoped_lock locker(lock);
|
||||
void history_impl_t::disable_automatic_saving() {
|
||||
disable_automatic_save_counter++;
|
||||
assert(disable_automatic_save_counter != 0); // overflow!
|
||||
}
|
||||
|
||||
void history_t::enable_automatic_saving() {
|
||||
scoped_lock locker(lock);
|
||||
void history_impl_t::enable_automatic_saving() {
|
||||
assert(disable_automatic_save_counter > 0); // underflow
|
||||
disable_automatic_save_counter--;
|
||||
save_internal_unless_disabled();
|
||||
save_unless_disabled();
|
||||
}
|
||||
|
||||
void history_t::clear() {
|
||||
scoped_lock locker(lock);
|
||||
void history_impl_t::clear() {
|
||||
new_items.clear();
|
||||
deleted_items.clear();
|
||||
first_unwritten_new_item_index = 0;
|
||||
|
@ -1679,11 +1687,9 @@ void history_t::clear() {
|
|||
this->clear_file_state();
|
||||
}
|
||||
|
||||
bool history_t::is_default() const { return name == DFLT_FISH_HISTORY_SESSION_ID; }
|
||||
|
||||
bool history_t::is_empty() {
|
||||
scoped_lock locker(lock);
|
||||
bool history_impl_t::is_default() const { return name == DFLT_FISH_HISTORY_SESSION_ID; }
|
||||
|
||||
bool history_impl_t::is_empty() {
|
||||
// If we have new items, we're not empty.
|
||||
if (!new_items.empty()) return false;
|
||||
|
||||
|
@ -1714,7 +1720,7 @@ bool history_t::is_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() {
|
||||
void history_impl_t::populate_from_config_path() {
|
||||
wcstring new_file = history_filename(name, wcstring());
|
||||
if (new_file.empty()) {
|
||||
return;
|
||||
|
@ -1789,7 +1795,7 @@ static bool should_import_bash_history_line(const wcstring &line) {
|
|||
/// comments. Ignore a few commands that are bash-specific. It makes no attempt to handle multiline
|
||||
/// commands. We can't actually parse bash syntax and the bash history file does not unambiguously
|
||||
/// encode multiline commands.
|
||||
void history_t::populate_from_bash(FILE *stream) {
|
||||
void history_impl_t::populate_from_bash(FILE *stream) {
|
||||
// Process the entire history file until EOF is observed.
|
||||
bool eof = false;
|
||||
while (!eof) {
|
||||
|
@ -1816,14 +1822,13 @@ void history_t::populate_from_bash(FILE *stream) {
|
|||
}
|
||||
}
|
||||
|
||||
void history_t::incorporate_external_changes() {
|
||||
void history_impl_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.
|
||||
|
@ -1834,19 +1839,12 @@ void history_t::incorporate_external_changes() {
|
|||
// 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->save(false);
|
||||
this->new_items.clear();
|
||||
this->first_unwritten_new_item_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void history_save_all() {
|
||||
auto histories = s_histories.acquire();
|
||||
for (auto &p : *histories) {
|
||||
p.second->save();
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the prefix for the files to be used for command and read history.
|
||||
wcstring history_session_id(const environment_t &vars) {
|
||||
wcstring result = DFLT_FISH_HISTORY_SESSION_ID;
|
||||
|
@ -1900,10 +1898,35 @@ static bool string_could_be_path(const wcstring &potential_path) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/// Very simple, just mark that we have no more pending items.
|
||||
void history_impl_t::resolve_pending() { this->has_pending_item = false; }
|
||||
|
||||
bool history_t::chaos_mode = false;
|
||||
bool history_t::never_mmap = false;
|
||||
|
||||
history_t::history_t(wcstring name)
|
||||
: impl_(make_unique<owning_lock<history_impl_t>>(history_impl_t(std::move(name)))) {}
|
||||
|
||||
history_t::~history_t() = default;
|
||||
|
||||
acquired_lock<history_impl_t> history_t::impl() { return impl_->acquire(); }
|
||||
|
||||
acquired_lock<const history_impl_t> history_t::impl() const { return impl_->acquire(); }
|
||||
|
||||
bool history_t::is_default() const { return impl()->is_default(); }
|
||||
|
||||
bool history_t::is_empty() { return impl()->is_empty(); }
|
||||
|
||||
void history_t::add(const history_item_t &item, bool pending) { impl()->add(item, pending); }
|
||||
|
||||
void history_t::add(const wcstring &str, history_identifier_t ident, bool pending) {
|
||||
impl()->add(str, ident, pending);
|
||||
}
|
||||
|
||||
void history_t::remove(const wcstring &str) { impl()->remove(str); }
|
||||
|
||||
void history_t::add_pending_with_file_detection(const wcstring &str,
|
||||
const wcstring &working_dir_slash) {
|
||||
ASSERT_IS_MAIN_THREAD();
|
||||
|
||||
// Find all arguments that look like they could be file paths.
|
||||
bool impending_exit = false;
|
||||
parse_node_tree_t tree;
|
||||
|
@ -1939,38 +1962,151 @@ void history_t::add_pending_with_file_detection(const wcstring &str,
|
|||
}
|
||||
|
||||
// If we got a path, we'll perform file detection for autosuggestion hinting.
|
||||
bool wants_file_detection = !potential_paths.empty() && !impending_exit;
|
||||
auto imp = this->impl();
|
||||
|
||||
history_identifier_t identifier = 0;
|
||||
if (!potential_paths.empty() && !impending_exit) {
|
||||
if (wants_file_detection) {
|
||||
// Grab the next identifier.
|
||||
static relaxed_atomic_t<history_identifier_t> s_last_identifier{0};
|
||||
identifier = ++s_last_identifier;
|
||||
imp->disable_automatic_saving();
|
||||
|
||||
// Prevent saving until we're done, so we have time to get the paths.
|
||||
this->disable_automatic_saving();
|
||||
// Add the item. Then check for which paths are valid on a background thread,
|
||||
// and unblock the item.
|
||||
// Don't hold the lock while we perform this file detection.
|
||||
imp->add(str, identifier, true /* pending */);
|
||||
iothread_perform([=]() {
|
||||
auto validated_paths = valid_paths(potential_paths, working_dir_slash);
|
||||
auto imp = this->impl();
|
||||
imp->set_valid_file_paths(validated_paths, identifier);
|
||||
imp->enable_automatic_saving();
|
||||
});
|
||||
} else {
|
||||
// Add the item.
|
||||
// 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.
|
||||
imp->add(str, identifier, true /* pending */);
|
||||
if (impending_exit) {
|
||||
imp->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
void history_t::resolve_pending() { impl()->resolve_pending(); }
|
||||
|
||||
// Check for which paths are valid on a background thread,
|
||||
// then on the main thread update our history item
|
||||
iothread_perform([=]() { return valid_paths(potential_paths, working_dir_slash); },
|
||||
[=](path_list_t validated_paths) {
|
||||
this->set_valid_file_paths(validated_paths, identifier);
|
||||
this->enable_automatic_saving();
|
||||
});
|
||||
void history_t::save() { impl()->save(); }
|
||||
|
||||
// Searches history.
|
||||
bool history_t::search(history_search_type_t search_type, const wcstring_list_t &search_args,
|
||||
const wchar_t *show_time_format, size_t max_items, bool case_sensitive,
|
||||
bool null_terminate, bool reverse, io_streams_t &streams) {
|
||||
if (!search_args.empty()) {
|
||||
// User wants the results filtered. This is not the common case so we do it separate
|
||||
// from the code below for unfiltered output which is much cheaper.
|
||||
return search_with_args(search_type, search_args, show_time_format, max_items,
|
||||
case_sensitive, null_terminate, reverse, streams);
|
||||
}
|
||||
|
||||
// Actually add the item to the history.
|
||||
this->add(str, identifier, true /* pending */);
|
||||
// scoped_lock locker(lock);
|
||||
size_t hist_size = this->size();
|
||||
if (max_items > hist_size) max_items = hist_size;
|
||||
|
||||
// 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();
|
||||
if (reverse) {
|
||||
for (size_t i = max_items; i != 0; --i) {
|
||||
auto cur_item = this->item_at_index(i);
|
||||
wcstring result;
|
||||
format_history_record(cur_item, show_time_format, null_terminate, result);
|
||||
streams.out.append(result);
|
||||
}
|
||||
} else {
|
||||
// Start at one because zero is the current command.
|
||||
for (size_t i = 1; i < max_items + 1; ++i) {
|
||||
auto cur_item = this->item_at_index(i);
|
||||
wcstring result;
|
||||
format_history_record(cur_item, show_time_format, null_terminate, result);
|
||||
streams.out.append(result);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool history_t::search_with_args(history_search_type_t search_type,
|
||||
const wcstring_list_t &search_args,
|
||||
const wchar_t *show_time_format, size_t max_items,
|
||||
bool case_sensitive, bool null_terminate, bool reverse,
|
||||
io_streams_t &streams) {
|
||||
wcstring_list_t results;
|
||||
size_t hist_size = this->size();
|
||||
if (max_items > hist_size) max_items = hist_size;
|
||||
|
||||
for (const wcstring &search_string : search_args) {
|
||||
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 ? 0 : history_search_ignore_case);
|
||||
while (searcher.go_backwards()) {
|
||||
wcstring result;
|
||||
auto cur_item = searcher.current_item();
|
||||
format_history_record(cur_item, show_time_format, null_terminate, result);
|
||||
if (reverse) {
|
||||
results.push_back(result);
|
||||
} else {
|
||||
streams.out.append(result);
|
||||
}
|
||||
if (--max_items == 0) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reverse) {
|
||||
for (auto it = results.rbegin(); it != results.rend(); it++) {
|
||||
streams.out.append(*it);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void history_t::clear() { impl()->clear(); }
|
||||
|
||||
void history_t::populate_from_config_path() { impl()->populate_from_config_path(); }
|
||||
|
||||
void history_t::populate_from_bash(FILE *f) { impl()->populate_from_bash(f); }
|
||||
|
||||
void history_t::incorporate_external_changes() { impl()->incorporate_external_changes(); }
|
||||
|
||||
void history_t::get_history(wcstring_list_t &result) { impl()->get_history(result); }
|
||||
|
||||
std::unordered_map<long, wcstring> history_t::items_at_indexes(const std::vector<long> &idxs) {
|
||||
return impl()->items_at_indexes(idxs);
|
||||
}
|
||||
|
||||
history_item_t history_t::item_at_index(size_t idx) { return impl()->item_at_index(idx); }
|
||||
|
||||
size_t history_t::size() { return impl()->size(); }
|
||||
|
||||
/// The set of all histories.
|
||||
static owning_lock<std::map<wcstring, std::unique_ptr<history_t>>> s_histories;
|
||||
|
||||
void history_save_all() {
|
||||
auto histories = s_histories.acquire();
|
||||
for (auto &p : *histories) {
|
||||
p.second->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;
|
||||
history_t &history_t::history_with_name(const wcstring &name) {
|
||||
// Return a history for the given name, creating it if necessary
|
||||
// Note that histories are currently never deleted, so we can return a reference to them without
|
||||
// using something like shared_ptr
|
||||
auto hs = s_histories.acquire();
|
||||
std::unique_ptr<history_t> &hist = (*hs)[name];
|
||||
if (!hist) {
|
||||
hist = make_unique<history_t>(name);
|
||||
}
|
||||
return *hist;
|
||||
}
|
||||
|
||||
static std::atomic<bool> private_mode{false};
|
||||
|
|
|
@ -61,6 +61,7 @@ typedef uint64_t history_identifier_t;
|
|||
|
||||
class history_item_t {
|
||||
friend class history_t;
|
||||
friend struct history_impl_t;
|
||||
friend class history_tests_t;
|
||||
|
||||
private:
|
||||
|
@ -106,11 +107,12 @@ class history_item_t {
|
|||
typedef std::deque<history_item_t> history_item_list_t;
|
||||
|
||||
class history_file_contents_t;
|
||||
struct history_impl_t;
|
||||
|
||||
class history_t {
|
||||
friend class history_tests_t;
|
||||
const std::unique_ptr<owning_lock<history_impl_t>> impl_;
|
||||
|
||||
private:
|
||||
// No copying or moving.
|
||||
history_t() = delete;
|
||||
history_t(const history_t &) = delete;
|
||||
|
@ -118,89 +120,13 @@ class history_t {
|
|||
history_t &operator=(const history_t &) = delete;
|
||||
history_t &operator=(history_t &&) = delete;
|
||||
|
||||
acquired_lock<history_impl_t> impl();
|
||||
acquired_lock<const history_impl_t> impl() const;
|
||||
|
||||
// Privately add an item. If pending, the item will not be returned by history searches until a
|
||||
// call to resolve_pending.
|
||||
void add(const history_item_t &item, bool pending = false);
|
||||
|
||||
// Lock for thread safety.
|
||||
std::mutex lock;
|
||||
|
||||
// Internal function.
|
||||
void clear_file_state();
|
||||
|
||||
// The name of this list. Used for picking a suitable filename and for switching modes.
|
||||
const wcstring name;
|
||||
|
||||
// New items. Note that these are NOT discarded on save. We need to keep these around so we can
|
||||
// distinguish between items in our history and items in the history of other shells that were
|
||||
// started after we were started.
|
||||
history_item_list_t new_items;
|
||||
|
||||
// The index of the first new item that we have not yet written.
|
||||
size_t first_unwritten_new_item_index{0};
|
||||
|
||||
// Whether we have a pending item. If so, the most recently added item is ignored by
|
||||
// item_at_index.
|
||||
bool has_pending_item{false};
|
||||
|
||||
// Whether we should disable saving to the file for a time.
|
||||
uint32_t disable_automatic_save_counter{0};
|
||||
|
||||
// Deleted item contents.
|
||||
std::unordered_set<wcstring> deleted_items;
|
||||
|
||||
// The buffer containing the history file contents.
|
||||
std::unique_ptr<history_file_contents_t> file_contents;
|
||||
|
||||
// The file ID of the history file.
|
||||
file_id_t history_file_id;
|
||||
|
||||
// The boundary timestamp distinguishes old items from new items. Items whose timestamps are <=
|
||||
// the boundary are considered "old". Items whose timestemps are > the boundary are new, and are
|
||||
// ignored by this instance (unless they came from this instance). The timestamp may be adjusted
|
||||
// by incorporate_external_changes().
|
||||
time_t boundary_timestamp;
|
||||
|
||||
// How many items we add until the next vacuum. Initially a random value.
|
||||
int countdown_to_vacuum{-1};
|
||||
|
||||
// Whether we've loaded old items.
|
||||
bool loaded_old{false};
|
||||
|
||||
// List of old items, as offsets into out mmap data.
|
||||
std::deque<size_t> old_item_offsets;
|
||||
|
||||
// Figure out the offsets of our file contents.
|
||||
void populate_from_file_contents();
|
||||
|
||||
// Loads old items if necessary.
|
||||
void load_old_if_needed();
|
||||
|
||||
// Reads the history file if necessary.
|
||||
bool mmap_if_needed();
|
||||
|
||||
// Deletes duplicates in new_items.
|
||||
void compact_new_items();
|
||||
|
||||
// Attempts to rewrite the existing file to a target temporary file
|
||||
// Returns false on error, true on success
|
||||
bool rewrite_to_temporary_file(int existing_fd, int dst_fd) const;
|
||||
|
||||
// Saves history by rewriting the file.
|
||||
bool save_internal_via_rewrite();
|
||||
|
||||
// Saves history by appending to the file.
|
||||
bool save_internal_via_appending();
|
||||
|
||||
// Saves history.
|
||||
void save_internal(bool vacuum);
|
||||
|
||||
// Saves history unless doing so is disabled.
|
||||
void save_internal_unless_disabled();
|
||||
|
||||
// Implementation of item_at_index and items_at_indexes
|
||||
history_item_t item_at_index_assume_locked(size_t idx);
|
||||
|
||||
public:
|
||||
explicit history_t(wcstring name);
|
||||
~history_t();
|
||||
|
@ -242,22 +168,18 @@ class history_t {
|
|||
void save();
|
||||
|
||||
// Searches history.
|
||||
bool search(history_search_type_t search_type, wcstring_list_t search_args,
|
||||
bool search(history_search_type_t search_type, const wcstring_list_t &search_args,
|
||||
const wchar_t *show_time_format, size_t max_items, bool case_sensitive,
|
||||
bool null_terminate, bool reverse, io_streams_t &streams);
|
||||
|
||||
bool search_with_args(history_search_type_t search_type, wcstring_list_t search_args,
|
||||
bool search_with_args(history_search_type_t search_type, const wcstring_list_t &search_args,
|
||||
const wchar_t *show_time_format, size_t max_items, bool case_sensitive,
|
||||
bool null_terminate, bool reverse, io_streams_t &streams);
|
||||
|
||||
// Enable / disable automatic saving. Main thread only!
|
||||
void disable_automatic_saving();
|
||||
void enable_automatic_saving();
|
||||
|
||||
// Irreversibly clears history.
|
||||
void clear();
|
||||
|
||||
// Populates from older location ()in config path, rather than data path).
|
||||
// Populates from older location (in config path, rather than data path).
|
||||
void populate_from_config_path();
|
||||
|
||||
// Populates from a bash history file.
|
||||
|
@ -275,9 +197,6 @@ class history_t {
|
|||
// are skipped. Return a mapping from index to history item text.
|
||||
std::unordered_map<long, wcstring> items_at_indexes(const std::vector<long> &idxs);
|
||||
|
||||
// Sets the valid file paths for the history item with the given identifier.
|
||||
void set_valid_file_paths(const wcstring_list_t &valid_file_paths, history_identifier_t ident);
|
||||
|
||||
// Return the specified history at the specified index. 0 is the index of the current
|
||||
// commandline. (So the most recent item is at index 1.)
|
||||
history_item_t item_at_index(size_t idx);
|
||||
|
|
Loading…
Reference in New Issue
Block a user