1 // The classes responsible for autoloading functions and completions.
2 #include "config.h"  // IWYU pragma: keep
3 
4 #include "autoload.h"
5 
6 #include <chrono>
7 
8 #include "common.h"
9 #include "env.h"
10 #include "exec.h"
11 #include "lru.h"
12 #include "parser.h"
13 #include "wutil.h"  // IWYU pragma: keep
14 
15 /// The time before we'll recheck an autoloaded file.
16 static const int kAutoloadStalenessInterval = 15;
17 
18 /// Represents a file that we might want to autoload.
19 struct autoloadable_file_t {
20     /// The path to the file.
21     wcstring path;
22 
23     /// The metadata for the file.
24     file_id_t file_id;
25 };
26 
27 /// Class representing a cache of files that may be autoloaded.
28 /// This is responsible for performing cached accesses to a set of paths.
29 class autoload_file_cache_t {
30     /// A timestamp is a monotonic point in time.
31     using timestamp_t = std::chrono::time_point<std::chrono::steady_clock>;
32 
33     /// The directories from which to load.
34     const wcstring_list_t dirs_{};
35 
36     /// Our LRU cache of checks that were misses.
37     /// The key is the command, the  value is the time of the check.
38     struct misses_lru_cache_t : public lru_cache_t<misses_lru_cache_t, timestamp_t> {};
39     misses_lru_cache_t misses_cache_;
40 
41     /// The set of files that we have returned to the caller, along with the time of the check.
42     /// The key is the command (not the path).
43     struct known_file_t {
44         autoloadable_file_t file;
45         timestamp_t last_checked;
46     };
47     std::unordered_map<wcstring, known_file_t> known_files_;
48 
49     /// \return the current timestamp.
current_timestamp()50     static timestamp_t current_timestamp() { return std::chrono::steady_clock::now(); }
51 
52     /// \return whether a timestamp is fresh enough to use.
53     static bool is_fresh(timestamp_t then, timestamp_t now);
54 
55     /// Attempt to find an autoloadable file by searching our path list for a given comand.
56     /// \return the file, or none() if none.
57     maybe_t<autoloadable_file_t> locate_file(const wcstring &cmd) const;
58 
59    public:
60     /// Initialize with a set of directories.
autoload_file_cache_t(wcstring_list_t dirs)61     explicit autoload_file_cache_t(wcstring_list_t dirs) : dirs_(std::move(dirs)) {}
62 
63     /// Initialize with empty directories.
64     autoload_file_cache_t() = default;
65 
66     /// \return the directories.
dirs() const67     const wcstring_list_t &dirs() const { return dirs_; }
68 
69     /// Check if a command \p cmd can be loaded.
70     /// If \p allow_stale is true, allow stale entries; otherwise discard them.
71     /// This returns an autoloadable file, or none() if there is no such file.
72     maybe_t<autoloadable_file_t> check(const wcstring &cmd, bool allow_stale = false);
73 };
74 
locate_file(const wcstring & cmd) const75 maybe_t<autoloadable_file_t> autoload_file_cache_t::locate_file(const wcstring &cmd) const {
76     // Re-use the storage for path.
77     wcstring path;
78     for (const wcstring &dir : dirs()) {
79         // Construct the path as dir/cmd.fish
80         path = dir;
81         path += L"/";
82         path += cmd;
83         path += L".fish";
84 
85         file_id_t file_id = file_id_for_path(path);
86         if (file_id != kInvalidFileID) {
87             // Found it.
88             autoloadable_file_t result;
89             result.path = std::move(path);
90             result.file_id = file_id;
91             return result;
92         }
93     }
94     return none();
95 }
96 
is_fresh(timestamp_t then,timestamp_t now)97 bool autoload_file_cache_t::is_fresh(timestamp_t then, timestamp_t now) {
98     auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now - then);
99     return seconds.count() < kAutoloadStalenessInterval;
100 }
101 
check(const wcstring & cmd,bool allow_stale)102 maybe_t<autoloadable_file_t> autoload_file_cache_t::check(const wcstring &cmd, bool allow_stale) {
103     // Check hits.
104     auto iter = known_files_.find(cmd);
105     if (iter != known_files_.end()) {
106         if (allow_stale || is_fresh(iter->second.last_checked, current_timestamp())) {
107             // Re-use this cached hit.
108             return iter->second.file;
109         }
110         // The file is stale, remove it.
111         known_files_.erase(iter);
112     }
113 
114     // Check misses.
115     if (timestamp_t *miss = misses_cache_.get(cmd)) {
116         if (allow_stale || is_fresh(*miss, current_timestamp())) {
117             // Re-use this cached miss.
118             return none();
119         }
120         // The miss is stale, remove it.
121         misses_cache_.evict_node(cmd);
122     }
123 
124     // We couldn't satisfy this request from the cache. Hit the disk.
125     maybe_t<autoloadable_file_t> file = locate_file(cmd);
126     if (file.has_value()) {
127         auto ins = known_files_.emplace(cmd, known_file_t{*file, current_timestamp()});
128         assert(ins.second && "Known files cache should not have contained this cmd");
129         (void)ins;
130     } else {
131         bool ins = misses_cache_.insert(cmd, current_timestamp());
132         assert(ins && "Misses cache should not have contained this cmd");
133         (void)ins;
134     }
135     return file;
136 }
137 
autoload_t(wcstring env_var_name)138 autoload_t::autoload_t(wcstring env_var_name)
139     : env_var_name_(std::move(env_var_name)), cache_(make_unique<autoload_file_cache_t>()) {}
140 
141 autoload_t::autoload_t(autoload_t &&) noexcept = default;
142 autoload_t::~autoload_t() = default;
143 
invalidate_cache()144 void autoload_t::invalidate_cache() {
145     auto cache = make_unique<autoload_file_cache_t>(cache_->dirs());
146     cache_ = std::move(cache);
147 }
148 
can_autoload(const wcstring & cmd)149 bool autoload_t::can_autoload(const wcstring &cmd) {
150     return cache_->check(cmd, true /* allow stale */).has_value();
151 }
152 
get_autoloaded_commands() const153 wcstring_list_t autoload_t::get_autoloaded_commands() const {
154     wcstring_list_t result;
155     result.reserve(autoloaded_files_.size());
156     for (const auto &kv : autoloaded_files_) {
157         result.push_back(kv.first);
158     }
159     // Sort the output to make it easier to test.
160     std::sort(result.begin(), result.end());
161     return result;
162 }
163 
resolve_command(const wcstring & cmd,const environment_t & env)164 maybe_t<wcstring> autoload_t::resolve_command(const wcstring &cmd, const environment_t &env) {
165     if (maybe_t<env_var_t> mvar = env.get(env_var_name_)) {
166         return resolve_command(cmd, mvar->as_list());
167     } else {
168         return resolve_command(cmd, wcstring_list_t{});
169     }
170 }
171 
resolve_command(const wcstring & cmd,const wcstring_list_t & paths)172 maybe_t<wcstring> autoload_t::resolve_command(const wcstring &cmd, const wcstring_list_t &paths) {
173     // Are we currently in the process of autoloading this?
174     if (current_autoloading_.count(cmd) > 0) return none();
175 
176     // Check to see if our paths have changed. If so, replace our cache.
177     // Note we don't have to modify autoloadable_files_. We'll naturally detect if those have
178     // changed when we query the cache.
179     if (paths != cache_->dirs()) {
180         cache_ = make_unique<autoload_file_cache_t>(paths);
181     }
182 
183     // Do we have an entry to load?
184     auto mfile = cache_->check(cmd);
185     if (!mfile) return none();
186 
187     // Is this file the same as what we previously autoloaded?
188     auto iter = autoloaded_files_.find(cmd);
189     if (iter != autoloaded_files_.end() && iter->second == mfile->file_id) {
190         // The file has been autoloaded and is unchanged.
191         return none();
192     }
193 
194     // We're going to (tell our caller to) autoload this command.
195     current_autoloading_.insert(cmd);
196     autoloaded_files_[cmd] = mfile->file_id;
197     return std::move(mfile->path);
198 }
199 
perform_autoload(const wcstring & path,parser_t & parser)200 void autoload_t::perform_autoload(const wcstring &path, parser_t &parser) {
201     wcstring script_source = L"source " + escape_string(path, ESCAPE_ALL);
202     exec_subshell(script_source, parser, false /* do not apply exit status */);
203 }
204