Merge pull request #5097 from ridiculousfish/history_read

Don't mmap history files on remote file systems

This merges some changes to history that may help to mitigate the crashes seen in #5088 . These SIGBUS crashes occur when reading a memory mapping whose underlying file was truncated. It's not clear why this should occur more often on NFS (or ever). However memory mapping over NFS is sketchy anyways so this is desirable regardless.
This commit is contained in:
ridiculousfish 2018-07-21 13:51:49 -07:00 committed by GitHub
commit a11e955c84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 300 additions and 221 deletions

View File

@ -2,6 +2,7 @@
#include "config.h" // IWYU pragma: keep
#include <ctype.h>
#include <cstdint>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
@ -155,6 +156,22 @@ static bool history_file_lock(int fd, int lock_type) {
return retval != -1;
}
// History file types.
enum history_file_type_t { history_type_fish_2_0, history_type_fish_1_x };
/// Try to infer the history file type based on inspecting the data.
static maybe_t<history_file_type_t> infer_file_type(const void *data, size_t len) {
maybe_t<history_file_type_t> result{};
if (len > 0) { // old fish started with a #
if (static_cast<const char *>(data)[0] == '#') {
result = history_type_fish_1_x;
} else { // assume new fish
result = history_type_fish_2_0;
}
}
return result;
}
/// 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 {
@ -204,6 +221,112 @@ class history_collection_t {
} // anonymous namespace
// history_file_contents_t holds the read-only contents of a file.
class history_file_contents_t {
// The memory mapped pointer.
void *start_;
// The mapped length.
size_t length_;
// The type of the mapped file.
history_file_type_t type_;
// Private constructor; use the static create() function.
history_file_contents_t(void *mmap_start, size_t mmap_length, history_file_type_t type)
: start_(mmap_start), length_(mmap_length), type_(type) {
assert(mmap_start != MAP_FAILED && "Invalid mmap address");
}
history_file_contents_t(history_file_contents_t &&) = delete;
void operator=(history_file_contents_t &&) = delete;
// Check if we should mmap the fd.
// Don't try mmap() on non-local filesystems.
static bool should_mmap(int fd) {
if (history_t::never_mmap) return false;
// mmap only if we are known not-remote (return is 0).
int ret = fd_check_is_remote(fd);
return ret == 0;
}
// Read up to len bytes from fd into address, zeroing the rest.
// Return true on success, false on failure.
static bool read_from_fd(int fd, void *address, size_t len) {
size_t remaining = len;
char *ptr = static_cast<char *>(address);
while (remaining > 0) {
ssize_t amt = read(fd, ptr, remaining);
if (amt < 0) {
if (errno != EINTR) {
return false;
}
} else if (amt == 0) {
break;
} else {
remaining -= amt;
ptr += amt;
}
}
bzero(ptr, remaining);
return true;
}
public:
// Access the address at a given offset.
const char *address_at(size_t offset) const {
assert(offset <= length_ && "Invalid offset");
auto base = static_cast<const char *>(start_);
return base + offset;
}
// Return a pointer to the beginning.
const char *begin() const { return address_at(0); }
// Return a pointer to one-past-the-end.
const char *end() const { return address_at(length_); }
// Get the size of the contents.
size_t length() const { return length_; }
// Get the file type.
history_file_type_t type() const { return type_; }
~history_file_contents_t() { munmap(start_, length_); }
// Construct a history file contents from a file descriptor. The file descriptor is not closed.
static std::unique_ptr<history_file_contents_t> create(int fd) {
// Check that the file is seekable, and its size.
off_t len = lseek(fd, 0, SEEK_END);
if (len <= 0 || len >= SIZE_MAX) return nullptr;
if (lseek(fd, 0, SEEK_SET) != 0) return nullptr;
// Read the file, possibly ussing mmap.
void *mmap_start = nullptr;
if (should_mmap(fd)) {
// We feel confident to map the file directly. Note this is still risky: if another
// process truncates the file we risk SIGBUS.
mmap_start = mmap(0, size_t(len), PROT_READ, MAP_PRIVATE, fd, 0);
if (mmap_start == MAP_FAILED) return nullptr;
} else {
// We don't want to map the file. mmap some private memory and then read into it. We use
// mmap instead of malloc so that the destructor can always munmap().
mmap_start =
mmap(0, size_t(len), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mmap_start == MAP_FAILED) return nullptr;
if (!read_from_fd(fd, mmap_start, len)) return nullptr;
}
// Check the file type.
auto mtype = infer_file_type(mmap_start, len);
if (!mtype) return nullptr;
return std::unique_ptr<history_file_contents_t>(
new history_file_contents_t(mmap_start, len, *mtype));
}
};
static history_collection_t histories;
static wcstring history_filename(const wcstring &name, const wcstring &suffix);
@ -274,19 +397,6 @@ static wcstring history_unescape_newlines_fish_1_x(const wcstring &in_str) {
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;
@ -416,9 +526,15 @@ done:
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);
static history_item_t decode_item(const history_file_contents_t &contents, size_t offset) {
const char *base = contents.address_at(offset);
size_t len = contents.length() - offset;
switch (contents.type()) {
case history_type_fish_2_0:
return decode_item_fish_2_0(base, len);
case history_type_fish_1_x:
return decode_item_fish_1_x(base, len);
}
return history_item_t(L"");
}
@ -564,19 +680,21 @@ static const char *next_line(const char *start, size_t length) {
}
/// 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.
/// Pass the file contents and 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,
static size_t offset_of_next_item_fish_2_0(const history_file_contents_t &contents,
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;
size_t result = size_t(-1);
const size_t length = contents.length();
const char *const begin = contents.begin();
const char *const end = contents.end();
while (cursor < length) {
const char *line_start = contents.address_at(cursor);
// Advance the cursor to the next line.
const char *a_newline = (const char *)memchr(line_start, '\n', mmap_length - cursor);
const char *a_newline = (const char *)memchr(line_start, '\n', length - cursor);
if (a_newline == NULL) break;
// Advance the cursor past this line. +1 is for the newline.
@ -621,8 +739,6 @@ static size_t offset_of_next_item_fish_2_0(const char *begin, size_t mmap_length
// 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;
@ -693,16 +809,16 @@ static size_t offset_of_next_item_fish_1_x(const char *begin, size_t mmap_length
}
/// 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,
static size_t offset_of_next_item(const history_file_contents_t &contents, 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);
switch (contents.type()) {
case history_type_fish_2_0:
return offset_of_next_item_fish_2_0(contents, inout_cursor, cutoff_timestamp);
;
case history_type_fish_1_x:
return offset_of_next_item_fish_1_x(contents.begin(), contents.length(), inout_cursor);
}
return result;
return size_t(-1);
}
history_t &history_collection_t::get_creating(const wcstring &name) {
@ -722,18 +838,12 @@ history_t &history_t::history_with_name(const wcstring &name) {
}
history_t::history_t(wcstring pname)
: name(std::move(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) {}
: name(std::move(pname)), boundary_timestamp(time(NULL)), history_file_id(kInvalidFileID) {}
history_t::~history_t() = default;
bool history_t::chaos_mode = false;
bool history_t::never_mmap = false;
void history_t::add(const history_item_t &item, bool pending) {
scoped_lock locker(lock);
@ -849,11 +959,8 @@ void history_t::get_history(wcstring_list_t &result) {
bool next_is_pending = this->has_pending_item;
std::unordered_set<wcstring> seen;
// 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
// https://github.com/fish-shell/fish-shell/issues/431.
for (history_item_list_t::reverse_iterator iter = new_items.rbegin(); iter < new_items.rend();
++iter) {
// Append new items.
for (auto iter = new_items.crbegin(); iter < new_items.crend(); ++iter) {
// Skip a pending item if we have one.
if (next_is_pending) {
next_is_pending = false;
@ -865,12 +972,9 @@ void history_t::get_history(wcstring_list_t &result) {
// 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) {
for (auto iter = old_item_offsets.crbegin(); iter != old_item_offsets.crend(); ++iter) {
size_t offset = *iter;
const history_item_t item =
decode_item(mmap_start + offset, mmap_length - offset, mmap_type);
const history_item_t item = decode_item(*file_contents, offset);
if (seen.insert(item.str()).second) result.push_back(item.str());
}
}
@ -910,7 +1014,7 @@ history_item_t history_t::item_at_index_assume_locked(size_t idx) {
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);
return decode_item(*file_contents, offset);
}
// Index past the valid range, so return an empty history item.
@ -942,88 +1046,50 @@ std::unordered_map<long, wcstring> history_t::items_at_indexes(const std::vector
return result;
}
void history_t::populate_from_mmap() {
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;
void history_t::populate_from_file_contents() {
old_item_offsets.clear();
if (file_contents) {
size_t cursor = 0;
for (;;) {
size_t offset = offset_of_next_item(*file_contents, &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);
}
}
bool history_t::map_fd(int fd, const char **out_map_start, size_t *out_map_len) const {
if (fd < 0) {
return false;
}
// 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.
bool result = false;
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;
}
// Remember this item.
old_item_offsets.push_back(offset);
}
}
if (!chaos_mode) history_file_lock(fd, LOCK_UN);
return result;
}
/// 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) const {
wcstring filename = history_filename(name, L"");
if (filename.empty()) {
return false;
}
int fd = wopen_cloexec(filename, O_RDONLY);
if (fd < 0) {
return false;
}
// Get the file ID if requested.
if (file_id != NULL) *file_id = file_id_for_fd(fd);
bool result = this->map_fd(fd, out_map_start, out_map_len);
close(fd);
return result;
}
bool history_t::load_old_if_needed() {
if (loaded_old) return true;
void history_t::load_old_if_needed() {
if (loaded_old) return;
loaded_old = true;
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();
}
time_profiler_t profiler("load_old"); //!OCLINT(side-effect)
wcstring filename = history_filename(name, L"");
if (!filename.empty()) {
int fd = wopen_cloexec(filename, O_RDONLY);
if (fd >= 0) {
// 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);
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);
close(fd);
return ok;
time_profiler_t profiler("populate_from_file_contents"); //!OCLINT(side-effect)
this->populate_from_file_contents();
}
}
}
void history_search_t::skip_matches(const wcstring_list_t &skips) {
@ -1171,11 +1237,7 @@ static wcstring history_filename(const wcstring &session_id, const wcstring &suf
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;
file_contents.reset();
loaded_old = false;
old_item_offsets.clear();
}
@ -1215,23 +1277,17 @@ bool history_t::rewrite_to_temporary_file(int existing_fd, int dst_fd) const {
// Make an LRU cache to save only the last N elements.
history_lru_cache_t lru(HISTORY_SAVE_MAX);
// 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 (existing_fd >= 0 && map_fd(existing_fd, &local_mmap_start, &local_mmap_size)) {
const history_file_type_t local_mmap_type =
infer_file_type(local_mmap_start, local_mmap_size);
// Read in existing items (which may have changed out from underneath us, so don't trust our
// old file contents).
if (auto local_file = history_file_contents_t::create(existing_fd)) {
size_t cursor = 0;
for (;;) {
size_t offset =
offset_of_next_item(local_mmap_start, local_mmap_size, local_mmap_type, &cursor, 0);
size_t offset = offset_of_next_item(*local_file, &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);
const history_item_t old_item = decode_item(*local_file, offset);
if (old_item.empty() || deleted_items.count(old_item.str()) > 0) {
// debug(0, L"Item is deleted : %s\n", old_item.str().c_str());
@ -1240,7 +1296,6 @@ bool history_t::rewrite_to_temporary_file(int existing_fd, int dst_fd) const {
// Add this old item.
lru.add_item(old_item);
}
munmap((void *)local_mmap_start, local_mmap_size);
}
// Insert any unwritten new items
@ -1424,9 +1479,8 @@ bool history_t::save_internal_via_appending() {
// We are going to open the file, lock it, append to it, and then close it
// After locking it, we need to stat the file at the path; if there is a new file there, it
// means
// the file was replaced and we have to try again
// Limit our max tries so we don't do this forever
// means the file was replaced and we have to try again.
// Limit our max tries so we don't do this forever.
int history_fd = -1;
for (int i = 0; i < max_save_tries; i++) {
int fd = wopen_cloexec(history_path, O_WRONLY | O_APPEND);
@ -1449,7 +1503,7 @@ bool history_t::save_internal_via_appending() {
} else {
// File IDs match, so the file we opened is still at that path
// We're going to use this fd
if (file_id != this->mmap_file_id) {
if (file_id != this->history_file_id) {
file_changed = true;
}
history_fd = fd;
@ -1500,11 +1554,10 @@ bool history_t::save_internal_via_appending() {
// Since we just modified the file, update our mmap_file_id to match its current state
// Otherwise we'll think the file has been changed by someone else the next time we go to
// write
// write.
// We don't update the mapping since we only appended to the file, and everything we
// appended
// remains in our new_items
this->mmap_file_id = file_id_for_fd(history_fd);
// appended remains in our new_items
this->history_file_id = file_id_for_fd(history_fd);
close(history_fd);
}

View File

@ -104,16 +104,18 @@ class history_item_t {
typedef std::deque<history_item_t> history_item_list_t;
// The type of file that we mmap'd.
enum history_file_type_t { history_type_unknown, history_type_fish_2_0, history_type_fish_1_x };
class history_file_contents_t;
class history_t {
friend class history_tests_t;
private:
// No copying.
history_t(const history_t &);
history_t &operator=(const history_t &);
// No copying or moving.
history_t() = delete;
history_t(const history_t &) = delete;
history_t(history_t &&) = delete;
history_t &operator=(const history_t &) = delete;
history_t &operator=(history_t &&) = delete;
// Privately add an item. If pending, the item will not be returned by history searches until a
// call to resolve_pending.
@ -134,29 +136,23 @@ class history_t {
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;
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;
bool has_pending_item{false};
// Whether we should disable saving to the file for a time.
uint32_t disable_automatic_save_counter;
uint32_t disable_automatic_save_counter{0};
// Deleted item contents.
std::unordered_set<wcstring> deleted_items;
// The mmaped region for the history file.
const char *mmap_start;
// The buffer containing the history file contents.
std::unique_ptr<history_file_contents_t> file_contents;
// The size of the mmap'd region.
size_t mmap_length;
// The type of file we mmap'd.
history_file_type_t mmap_type;
// The file ID of the file we mmap'd.
file_id_t mmap_file_id;
// 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
@ -165,22 +161,22 @@ class history_t {
time_t boundary_timestamp;
// How many items we add until the next vacuum. Initially a random value.
int countdown_to_vacuum;
int countdown_to_vacuum{-1};
// Figure out the offsets of our mmap data.
void populate_from_mmap(void);
// 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;
// Whether we've loaded old items.
bool loaded_old;
// Figure out the offsets of our file contents.
void populate_from_file_contents();
// Loads old if necessary.
bool load_old_if_needed(void);
// Loads old items if necessary.
void load_old_if_needed();
// Memory maps the history file if necessary.
bool mmap_if_needed(void);
// Reads the history file if necessary.
bool mmap_if_needed();
// Deletes duplicates in new_items.
void compact_new_items();
@ -201,29 +197,26 @@ class history_t {
// Saves history unless doing so is disabled.
void save_internal_unless_disabled();
// 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 map_file(const wcstring &name, const char **out_map_start, size_t *out_map_len,
file_id_t *file_id) const;
// Like map_file but takes a file descriptor
bool map_fd(int fd, const char **out_map_start, size_t *out_map_len) const;
// Whether we're in maximum chaos mode, useful for testing.
bool chaos_mode;
// 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 ); // constructor
explicit history_t(wcstring name);
~history_t();
// Whether we're in maximum chaos mode, useful for testing.
// This causes things like locks to fail.
static bool chaos_mode;
// Whether to force the read path instead of mmap. This is useful for testing.
static bool never_mmap;
// Returns history with the given name, creating it if necessary.
static history_t &history_with_name(const wcstring &name);
// Determines whether the history is empty. Unfortunately this cannot be const, since it may
// require populating the history.
bool is_empty(void);
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
@ -320,25 +313,25 @@ class history_search_t {
void skip_matches(const wcstring_list_t &skips);
// Finds the next search term (forwards in time). Returns true if one was found.
bool go_forwards(void);
bool go_forwards();
// Finds the previous search result (backwards in time). Returns true if one was found.
bool go_backwards(void);
bool go_backwards();
// Goes to the end (forwards).
void go_to_end(void);
void go_to_end();
// Returns if we are at the end. We start out at the end.
bool is_at_end(void) const;
bool is_at_end() const;
// Goes to the beginning (backwards).
void go_to_beginning(void);
void go_to_beginning();
// Returns the current search result item. asserts if there is no current item.
history_item_t current_item(void) const;
history_item_t current_item() const;
// Returns the current search result item contents. asserts if there is no current item.
wcstring current_string(void) const;
wcstring current_string() const;
// Constructor.
history_search_t(history_t &hist, const wcstring &str,

View File

@ -101,7 +101,7 @@ void reader_run_command(const wcstring &buff);
const wchar_t *reader_get_buffer();
/// Returns the current reader's history.
history_t *reader_get_history(void);
history_t *reader_get_history();
/// Set the string of characters in the command buffer, as well as the cursor position.
///

View File

@ -689,7 +689,7 @@ void wildcard_expander_t::expand_intermediate_segment(const wcstring &base_dir,
continue;
}
const file_id_t file_id = file_id_t::file_id_from_stat(&buf);
const file_id_t file_id = file_id_t::from_stat(buf);
if (!this->visited_files.insert(file_id).second) {
// Symlink loop! This directory was already visited, so skip it.
continue;

View File

@ -12,6 +12,11 @@
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#if defined(__linux__)
#include <sys/statfs.h>
#endif
#include <sys/mount.h>
#include <sys/statvfs.h>
#include <sys/types.h>
#include <unistd.h>
#include <wchar.h>
@ -278,6 +283,32 @@ int make_fd_blocking(int fd) {
return err == -1 ? errno : 0;
}
int fd_check_is_remote(int fd) {
#if defined(__linux__)
struct statfs buf{0};
if (fstatfs(fd, &buf) < 0) {
return -1;
}
// Linux has constants for these like NFS_SUPER_MAGIC, SMB_SUPER_MAGIC, CIFS_MAGIC_NUMBER but
// these are in varying headers. Simply hard code them.
switch (buf.f_type) {
case 0x6969: // NFS_SUPER_MAGIC
case 0x517B: // SMB_SUPER_MAGIC
case 0xFF534D42: // CIFS_MAGIC_NUMBER
return 1;
default:
// Other FSes are assumed local.
return 0;
}
#elif defined(MNT_LOCAL)
struct statfs buf {};
if (fstatfs(fd, &buf) < 0) return -1;
return (buf.f_flags & MNT_LOCAL) ? 0 : 1;
#else
return -1;
#endif
}
static inline void safe_append(char *buffer, const char *s, size_t buffsize) {
strncat(buffer, s, buffsize - strlen(buffer) - 1);
}
@ -620,25 +651,23 @@ unsigned long long fish_wcstoull(const wchar_t *str, const wchar_t **endptr, int
return result;
}
file_id_t file_id_t::file_id_from_stat(const struct stat *buf) {
assert(buf != NULL);
file_id_t file_id_t::from_stat(const struct stat &buf) {
file_id_t result = {};
result.device = buf->st_dev;
result.inode = buf->st_ino;
result.size = buf->st_size;
result.change_seconds = buf->st_ctime;
result.mod_seconds = buf->st_mtime;
result.device = buf.st_dev;
result.inode = buf.st_ino;
result.size = buf.st_size;
result.change_seconds = buf.st_ctime;
result.mod_seconds = buf.st_mtime;
#ifdef HAVE_STRUCT_STAT_ST_CTIME_NSEC
result.change_nanoseconds = buf->st_ctime_nsec;
result.mod_nanoseconds = buf->st_mtime_nsec;
result.change_nanoseconds = buf.st_ctime_nsec;
result.mod_nanoseconds = buf.st_mtime_nsec;
#elif defined(__APPLE__)
result.change_nanoseconds = buf->st_ctimespec.tv_nsec;
result.mod_nanoseconds = buf->st_mtimespec.tv_nsec;
result.change_nanoseconds = buf.st_ctimespec.tv_nsec;
result.mod_nanoseconds = buf.st_mtimespec.tv_nsec;
#elif defined(_BSD_SOURCE) || defined(_SVID_SOURCE) || defined(_XOPEN_SOURCE)
result.change_nanoseconds = buf->st_ctim.tv_nsec;
result.mod_nanoseconds = buf->st_mtim.tv_nsec;
result.change_nanoseconds = buf.st_ctim.tv_nsec;
result.mod_nanoseconds = buf.st_mtim.tv_nsec;
#else
result.change_nanoseconds = 0;
result.mod_nanoseconds = 0;
@ -651,7 +680,7 @@ file_id_t file_id_for_fd(int fd) {
file_id_t result = kInvalidFileID;
struct stat buf = {};
if (fd >= 0 && 0 == fstat(fd, &buf)) {
result = file_id_t::file_id_from_stat(&buf);
result = file_id_t::from_stat(buf);
}
return result;
}
@ -660,7 +689,7 @@ file_id_t file_id_for_path(const wcstring &path) {
file_id_t result = kInvalidFileID;
struct stat buf = {};
if (0 == wstat(path, &buf)) {
result = file_id_t::file_id_from_stat(&buf);
result = file_id_t::from_stat(buf);
}
return result;
}

View File

@ -28,6 +28,10 @@ int make_fd_nonblocking(int fd);
/// Mark an fd as blocking; returns errno or 0 on success.
int make_fd_blocking(int fd);
/// Check if an fd is on a remote filesystem (NFS, SMB, CFS)
/// Return 1 if remote, 0 if local, -1 on error or if not implemented on this platform.
int fd_check_is_remote(int fd);
/// Wide character version of opendir(). Note that opendir() is guaranteed to set close-on-exec by
/// POSIX (hooray).
DIR *wopendir(const wcstring &name);
@ -137,7 +141,7 @@ struct file_id_t {
// Used to permit these as keys in std::map.
bool operator<(const file_id_t &rhs) const;
static file_id_t file_id_from_stat(const struct stat *buf);
static file_id_t from_stat(const struct stat &buf);
private:
int compare_file_id(const file_id_t &rhs) const;