mirror of
https://github.com/fish-shell/fish-shell
synced 2025-01-20 00:44:43 +00:00
d9f901f36d
commit 50f414a45d58fcab664ff662dd27befcfa0fdd95 Author: Mahmoud Al-Qudsi <mqudsi@neosmart.net> Date: Sat Aug 19 13:43:35 2017 -0500 Converted file_id_t set to unordered_set with custom hash commit 83ef2dd7cc1bc3e4fdf0b2d3546d6811326cc3c9 Author: Mahmoud Al-Qudsi <mqudsi@neosmart.net> Date: Sat Aug 19 13:43:14 2017 -0500 Converted remaining set<wcstring> to unordered_set<wcstring> commit 053da88f933f27505b3cf4810402e2a2be070203 Author: Mahmoud Al-Qudsi <mqudsi@neosmart.net> Date: Sat Aug 19 13:29:21 2017 -0500 Switched function sets to unordered_set commit d469742a14ac99599022a9258cda8255178826b5 Author: Mahmoud Al-Qudsi <mqudsi@neosmart.net> Date: Sat Aug 19 13:21:32 2017 -0500 Converted list of modified variables to an unordered set commit 5c06f866beeafb23878b1a932c7cd2558412c283 Author: Mahmoud Al-Qudsi <mqudsi@neosmart.net> Date: Sat Aug 19 13:15:20 2017 -0500 Convert const_string_set_t to std::unordered_set As it is a readonly-list of raw character pointer strings (not wcstring), this necessitated the addition of a hashing function since the C++ standard library does not come with a char pointer hash function. To that end, a zlib-licensed [0] port of the excellent, lightweight XXHash family of 32- and 64-bit hashing algorithms in the form of a C++ header-only include library has been included. XXHash32/64 is pretty much universally the fastest hashing library for general purpose applications, and has been thoroughly vetted and is used in countless open source projects. The single-header version of this library makes it a lot simpler to include in the fish project, and the license compatibility with fish' GPLv2 and the zero-lib nature should make it an easy decision. std::unordered_set brings a massive speedup as compared to the default std::set, and the further use of the fast XXHash library to provide the string hashing should make all forms of string lookups in fish significantly faster (to a user-noticeable extent). 0: http://create.stephan-brumme.com/about.html commit 30d7710be8f0c23a4d42f7e713fcb7850f99036e Author: Mahmoud Al-Qudsi <mqudsi@neosmart.net> Date: Sat Aug 19 12:29:39 2017 -0500 Using std::unordered_set for completions backing store While the completions shown to the user are sorted, their storage in memory does not need to be since they are re-sorted before they are shown in completions.cpp. commit 695e83331d7a60ba188e57f6ea0d9b6da54860c6 Author: Mahmoud Al-Qudsi <mqudsi@neosmart.net> Date: Sat Aug 19 12:06:53 2017 -0500 Updated is_loading to use unordered_set
269 lines
9.5 KiB
C++
269 lines
9.5 KiB
C++
// The classes responsible for autoloading functions and completions.
|
|
#include "config.h" // IWYU pragma: keep
|
|
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
#include <stddef.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <memory>
|
|
#include <set>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "autoload.h"
|
|
#include "common.h"
|
|
#include "env.h"
|
|
#include "exec.h"
|
|
#include "wutil.h" // IWYU pragma: keep
|
|
|
|
/// The time before we'll recheck an autoloaded file.
|
|
static const int kAutoloadStalenessInterval = 15;
|
|
|
|
file_access_attempt_t access_file(const wcstring &path, int mode) {
|
|
// fwprintf(stderr, L"Touch %ls\n", path.c_str());
|
|
file_access_attempt_t result = {};
|
|
struct stat statbuf;
|
|
if (wstat(path, &statbuf)) {
|
|
result.error = errno;
|
|
} else {
|
|
result.mod_time = statbuf.st_mtime;
|
|
if (waccess(path, mode)) {
|
|
result.error = errno;
|
|
} else {
|
|
result.accessible = true;
|
|
}
|
|
}
|
|
|
|
// Note that we record the last checked time after the call, on the assumption that in a slow
|
|
// filesystem, the lag comes before the kernel check, not after.
|
|
result.stale = false;
|
|
result.last_checked = time(NULL);
|
|
return result;
|
|
}
|
|
|
|
autoload_t::autoload_t(const wcstring &env_var_name_var,
|
|
command_removed_function_t cmd_removed_callback)
|
|
: lock(), env_var_name(env_var_name_var), command_removed(cmd_removed_callback) {}
|
|
|
|
void autoload_t::entry_was_evicted(wcstring key, autoload_function_t node) {
|
|
// This should only ever happen on the main thread.
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
// Tell ourselves that the command was removed if it was loaded.
|
|
if (node.is_loaded) this->command_removed(std::move(key));
|
|
}
|
|
|
|
int autoload_t::unload(const wcstring &cmd) { return this->evict_node(cmd); }
|
|
|
|
int autoload_t::load(const wcstring &cmd, bool reload) {
|
|
int res;
|
|
CHECK_BLOCK(0);
|
|
ASSERT_IS_MAIN_THREAD();
|
|
|
|
env_var_t path_var = env_get(env_var_name);
|
|
|
|
// Do we know where to look?
|
|
if (path_var.empty()) return 0;
|
|
|
|
// Check if the lookup path has changed. If so, drop all loaded files. path_var may only be
|
|
// inspected on the main thread.
|
|
if (path_var != this->last_path) {
|
|
this->last_path = path_var;
|
|
this->last_path_tokenized.clear();
|
|
this->last_path.to_list(this->last_path_tokenized);
|
|
|
|
scoped_lock locker(lock);
|
|
this->evict_all_nodes();
|
|
}
|
|
|
|
// Mark that we're loading this. Hang onto the iterator for fast erasing later. Note that
|
|
// std::set has guarantees about not invalidating iterators, so this is safe to do across the
|
|
// callouts below.
|
|
auto insert_result = is_loading_set.insert(cmd);
|
|
auto where = insert_result.first;
|
|
bool inserted = insert_result.second;
|
|
|
|
// Warn and fail on infinite recursion. It's OK to do this because this function is only called
|
|
// on the main thread.
|
|
if (!inserted) {
|
|
// We failed to insert.
|
|
const wchar_t *fmt =
|
|
_(L"Could not autoload item '%ls', it is already being autoloaded. "
|
|
L"This is a circular dependency in the autoloading scripts, please remove it.");
|
|
debug(0, fmt, cmd.c_str());
|
|
return 1;
|
|
}
|
|
// Try loading it.
|
|
res = this->locate_file_and_maybe_load_it(cmd, true, reload, this->last_path_tokenized);
|
|
// Clean up.
|
|
is_loading_set.erase(where);
|
|
return res;
|
|
}
|
|
|
|
bool autoload_t::can_load(const wcstring &cmd, const env_vars_snapshot_t &vars) {
|
|
const env_var_t path_var = vars.get(env_var_name);
|
|
if (path_var.missing_or_empty()) return false;
|
|
|
|
std::vector<wcstring> path_list;
|
|
path_var.to_list(path_list);
|
|
return this->locate_file_and_maybe_load_it(cmd, false, false, path_list);
|
|
}
|
|
|
|
/// Check whether the given command is loaded.
|
|
bool autoload_t::has_tried_loading(const wcstring &cmd) {
|
|
scoped_lock locker(lock);
|
|
autoload_function_t *func = this->get(cmd);
|
|
return func != NULL;
|
|
}
|
|
|
|
/// @return Whether this function is stale.
|
|
static bool is_stale(const autoload_function_t *func) {
|
|
return time(NULL) - func->access.last_checked > kAutoloadStalenessInterval;
|
|
}
|
|
|
|
autoload_function_t *autoload_t::get_autoloaded_function_with_creation(const wcstring &cmd,
|
|
bool allow_eviction) {
|
|
ASSERT_IS_LOCKED(lock);
|
|
autoload_function_t *func = this->get(cmd);
|
|
if (!func) {
|
|
bool added;
|
|
if (allow_eviction) {
|
|
added = this->insert(cmd, autoload_function_t(false));
|
|
} else {
|
|
added = this->insert_no_eviction(cmd, autoload_function_t(false));
|
|
}
|
|
func = this->get(cmd);
|
|
assert(func);
|
|
}
|
|
return func;
|
|
}
|
|
|
|
static bool use_cached(autoload_function_t *func, bool really_load, bool allow_stale_functions) {
|
|
if (!func) {
|
|
return false; // can't use a function that doesn't exist
|
|
}
|
|
if (really_load && !func->is_placeholder && !func->is_loaded) {
|
|
return false; // can't use an unloaded function
|
|
}
|
|
if (!allow_stale_functions && is_stale(func)) {
|
|
return false; // can't use a stale function
|
|
}
|
|
return true; // I guess we can use it
|
|
}
|
|
|
|
/// This internal helper function does all the real work. By using two functions, the internal
|
|
/// function can return on various places in the code, and the caller can take care of various
|
|
/// cleanup work.
|
|
/// @param cmd the command name ('grep')
|
|
/// @param really_load Whether to actually parse it as a function, or just check it it exists
|
|
/// @param reload Whether to reload it if it's already loaded
|
|
/// @param path_list The set of paths to check
|
|
/// @return If really_load is true, returns whether the function was loaded. Otherwise returns
|
|
/// whether the function existed.
|
|
bool autoload_t::locate_file_and_maybe_load_it(const wcstring &cmd, bool really_load, bool reload,
|
|
const wcstring_list_t &path_list) {
|
|
// Note that we are NOT locked in this function!
|
|
bool reloaded = false;
|
|
|
|
// Try using a cached function. If we really want the function to be loaded, require that it be
|
|
// really loaded. If we're not reloading, allow stale functions.
|
|
{
|
|
bool allow_stale_functions = !reload;
|
|
scoped_lock locker(lock);
|
|
autoload_function_t *func = this->get(cmd); // get the function
|
|
|
|
// If we can use this function, return whether we were able to access it.
|
|
if (use_cached(func, really_load, allow_stale_functions)) {
|
|
return func->access.accessible;
|
|
}
|
|
}
|
|
|
|
// The source of the script will end up here.
|
|
wcstring script_source;
|
|
|
|
// Whether we found an accessible file.
|
|
bool found_file = false;
|
|
|
|
// Iterate over path searching for suitable completion files.
|
|
for (size_t i = 0; i < path_list.size() && !found_file; i++) {
|
|
wcstring next = path_list.at(i);
|
|
wcstring path = next + L"/" + cmd + L".fish";
|
|
|
|
const file_access_attempt_t access = access_file(path, R_OK);
|
|
if (!access.accessible) {
|
|
continue;
|
|
}
|
|
|
|
// Now we're actually going to take the lock.
|
|
scoped_lock locker(lock);
|
|
autoload_function_t *func = this->get(cmd);
|
|
|
|
// Generate the source if we need to load it.
|
|
bool need_to_load_function =
|
|
really_load &&
|
|
(func == NULL || func->access.mod_time != access.mod_time || !func->is_loaded);
|
|
if (need_to_load_function) {
|
|
// Generate the script source.
|
|
script_source = L"source " + escape_string(path, ESCAPE_ALL);
|
|
|
|
// Remove any loaded command because we are going to reload it. Note that this
|
|
// will deadlock if command_removed calls back into us.
|
|
if (func && func->is_loaded) {
|
|
command_removed(cmd);
|
|
func->is_placeholder = false;
|
|
}
|
|
|
|
// Mark that we're reloading it.
|
|
reloaded = true;
|
|
}
|
|
|
|
// Create the function if we haven't yet. This does not load it. Do not trigger
|
|
// eviction unless we are actually loading, because we don't want to evict off of
|
|
// the main thread.
|
|
if (!func) func = get_autoloaded_function_with_creation(cmd, really_load);
|
|
|
|
// It's a fiction to say the script is loaded at this point, but we're definitely
|
|
// going to load it down below.
|
|
if (need_to_load_function) func->is_loaded = true;
|
|
|
|
// Unconditionally record our access time.
|
|
func->access = access;
|
|
found_file = true;
|
|
}
|
|
|
|
// If no file or builtin script was found we insert a placeholder function. Later we only
|
|
// research if the current time is at least five seconds later. This way, the files won't be
|
|
// searched over and over again.
|
|
if (!found_file && script_source.empty()) {
|
|
scoped_lock locker(lock);
|
|
// Generate a placeholder.
|
|
autoload_function_t *func = this->get(cmd);
|
|
if (!func) {
|
|
if (really_load) {
|
|
this->insert(cmd, autoload_function_t(true));
|
|
} else {
|
|
this->insert_no_eviction(cmd, autoload_function_t(true));
|
|
}
|
|
func = this->get(cmd);
|
|
assert(func);
|
|
}
|
|
func->access.last_checked = time(NULL);
|
|
}
|
|
|
|
// If we have a script, either built-in or a file source, then run it.
|
|
if (really_load && !script_source.empty()) {
|
|
// Do nothing on failure.
|
|
exec_subshell(script_source, false /* do not apply exit status */);
|
|
}
|
|
|
|
if (really_load) {
|
|
return reloaded;
|
|
}
|
|
|
|
return found_file || !script_source.empty();
|
|
}
|