mirror of
https://github.com/fish-shell/fish-shell.git
synced 2024-12-12 08:16:28 +08:00
1498 lines
53 KiB
C++
1498 lines
53 KiB
C++
// The utility library for universal variables. Used both by the client library and by the daemon.
|
|
#include "config.h"
|
|
|
|
#include <arpa/inet.h> // IWYU pragma: keep
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <limits.h>
|
|
#include <pwd.h>
|
|
#include <stdarg.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
#include <wchar.h>
|
|
#include <wctype.h>
|
|
#include <string>
|
|
#ifdef HAVE_SYS_SELECT_H
|
|
#include <sys/select.h>
|
|
#endif
|
|
#include <netinet/in.h>
|
|
#include <map>
|
|
#include <utility>
|
|
// We need the ioctl.h header so we can check if SIOCGIFHWADDR is defined by it so we know if we're
|
|
// on a Linux system.
|
|
#include <sys/ioctl.h> // IWYU pragma: keep
|
|
// We need the sys/file.h for the flock() declaration on Linux but not OS X.
|
|
#include <sys/file.h> // IWYU pragma: keep
|
|
|
|
#include "common.h"
|
|
#include "env.h"
|
|
#include "env_universal_common.h"
|
|
#include "fallback.h" // IWYU pragma: keep
|
|
#include "utf8.h"
|
|
#include "util.h"
|
|
#include "wutil.h"
|
|
|
|
#if __APPLE__
|
|
#define FISH_NOTIFYD_AVAILABLE 1
|
|
#include <notify.h>
|
|
#endif
|
|
|
|
// NAME_MAX is not defined on Solaris and suggests the use of pathconf()
|
|
// There is no obvious sensible pathconf() for shared memory and _XPG_NAME_MAX
|
|
// seems a reasonable choice.
|
|
#if !defined(NAME_MAX) && defined(_XOPEN_NAME_MAX)
|
|
#define NAME_MAX _XOPEN_NAME_MAX
|
|
#endif
|
|
|
|
/// The set command.
|
|
#define SET_STR L"SET"
|
|
|
|
/// The set_export command.
|
|
#define SET_EXPORT_STR L"SET_EXPORT"
|
|
|
|
/// Non-wide version of the set command.
|
|
#define SET_MBS "SET"
|
|
|
|
/// Non-wide version of the set_export command.
|
|
#define SET_EXPORT_MBS "SET_EXPORT"
|
|
|
|
/// Error message.
|
|
#define PARSE_ERR L"Unable to parse universal variable message: '%ls'"
|
|
|
|
/// Small note about not editing ~/.fishd manually. Inserted at the top of all .fishd files.
|
|
#define SAVE_MSG \
|
|
"# This file is automatically generated by the fish.\n# Do NOT edit it directly, your " \
|
|
"changes will be overwritten.\n"
|
|
|
|
static wcstring fishd_get_config();
|
|
static wcstring get_machine_identifier();
|
|
static bool get_hostname_identifier(wcstring *result);
|
|
|
|
static wcstring vars_filename_in_directory(const wcstring &wdir) {
|
|
if (wdir.empty()) return L"";
|
|
|
|
wcstring result = wdir;
|
|
result.append(L"/fishd.");
|
|
result.append(get_machine_identifier());
|
|
return result;
|
|
}
|
|
|
|
static const wcstring &default_vars_path() {
|
|
// Oclint complains about this being a "redundant local variable"; however it isn't because the
|
|
// assignment to a static var is needed to keep the object from being deleted when this function
|
|
// returns.
|
|
static wcstring cached_result = vars_filename_in_directory(fishd_get_config()); //!OCLINT
|
|
return cached_result;
|
|
}
|
|
|
|
/// Check, and create if necessary, a secure runtime path Derived from tmux.c in tmux
|
|
/// (http://tmux.sourceforge.net/).
|
|
static int check_runtime_path(const char *path) {
|
|
// Copyright (c) 2007 Nicholas Marriott <nicm@users.sourceforge.net>
|
|
//
|
|
// Permission to use, copy, modify, and distribute this software for any
|
|
// purpose with or without fee is hereby granted, provided that the above
|
|
// copyright notice and this permission notice appear in all copies.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
// WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
|
|
// IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
|
// OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
struct stat statpath;
|
|
uid_t uid = geteuid();
|
|
|
|
if (mkdir(path, S_IRWXU) != 0 && errno != EEXIST) return errno;
|
|
if (lstat(path, &statpath) != 0) return errno;
|
|
if (!S_ISDIR(statpath.st_mode) || statpath.st_uid != uid ||
|
|
(statpath.st_mode & (S_IRWXG | S_IRWXO)) != 0)
|
|
return EACCES;
|
|
return 0;
|
|
}
|
|
|
|
/// Return the path of an appropriate runtime data directory.
|
|
static wcstring get_runtime_path() {
|
|
wcstring result;
|
|
const char *dir = getenv("XDG_RUNTIME_DIR");
|
|
|
|
// Check that the path is actually usable. Technically this is guaranteed by the fdo spec but in
|
|
// practice it is not always the case: see #1828 and #2222.
|
|
int mode = R_OK | W_OK | X_OK;
|
|
if (dir != NULL && access(dir, mode) == 0 && check_runtime_path(dir) == 0) {
|
|
result = str2wcstring(dir);
|
|
} else {
|
|
const char *uname = getenv("USER");
|
|
if (uname == NULL) {
|
|
const struct passwd *pw = getpwuid(getuid());
|
|
uname = pw->pw_name;
|
|
}
|
|
|
|
// /tmp/fish.user
|
|
std::string tmpdir = "/tmp/fish.";
|
|
tmpdir.append(uname);
|
|
if (check_runtime_path(tmpdir.c_str()) != 0) {
|
|
debug(0,
|
|
L"Runtime path not available. Try deleting the directory %s and restarting fish.",
|
|
tmpdir.c_str());
|
|
} else {
|
|
result = str2wcstring(tmpdir);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Returns a "variables" file in the appropriate runtime directory. This is called infrequently and
|
|
/// so does not need to be cached.
|
|
static wcstring default_named_pipe_path() {
|
|
// Note that vars_filename_in_directory returns empty string when passed the empty string.
|
|
return vars_filename_in_directory(get_runtime_path());
|
|
}
|
|
|
|
/// Test if the message msg contains the command cmd.
|
|
static bool match(const wchar_t *msg, const wchar_t *cmd) {
|
|
size_t len = wcslen(cmd);
|
|
if (wcsncasecmp(msg, cmd, len) != 0) return false;
|
|
|
|
if (msg[len] && msg[len] != L' ' && msg[len] != L'\t') return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static void report_error(int err_code, const wchar_t *err_format, ...) {
|
|
va_list va;
|
|
va_start(va, err_format);
|
|
const wcstring err_text = vformat_string(err_format, va);
|
|
va_end(va);
|
|
|
|
if (!err_text.empty()) {
|
|
fwprintf(stderr, L"%ls: ", err_text.c_str());
|
|
}
|
|
fwprintf(stderr, L"%s\n", strerror(err_code));
|
|
}
|
|
|
|
/// The universal variable format has some funny escaping requirements; here we try to be safe.
|
|
static bool is_universal_safe_to_encode_directly(wchar_t c) {
|
|
if (c < 32 || c > 128) return false;
|
|
|
|
return iswalnum(c) || wcschr(L"/_", c);
|
|
}
|
|
|
|
/// Escape specified string.
|
|
static wcstring full_escape(const wchar_t *in) {
|
|
wcstring out;
|
|
for (; *in; in++) {
|
|
wchar_t c = *in;
|
|
if (is_universal_safe_to_encode_directly(c)) {
|
|
out.push_back(c);
|
|
} else if (c <= (wchar_t)ASCII_MAX) {
|
|
// See #1225 for discussion of use of ASCII_MAX here.
|
|
append_format(out, L"\\x%.2x", c);
|
|
} else if (c < 65536) {
|
|
append_format(out, L"\\u%.4x", c);
|
|
} else {
|
|
append_format(out, L"\\U%.8x", c);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// Converts input to UTF-8 and appends it to receiver, using storage as temp storage.
|
|
static bool append_utf8(const wcstring &input, std::string *receiver, std::string *storage) {
|
|
bool result = false;
|
|
if (wchar_to_utf8_string(input, storage)) {
|
|
receiver->append(*storage);
|
|
result = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Creates a file entry like "SET fish_color_cwd:FF0". Appends the result to *result (as UTF8).
|
|
/// Returns true on success. storage may be used for temporary storage, to avoid allocations.
|
|
static bool append_file_entry(fish_message_type_t type, const wcstring &key_in,
|
|
const wcstring &val_in, std::string *result, std::string *storage) {
|
|
assert(storage != NULL);
|
|
assert(result != NULL);
|
|
|
|
// Record the length on entry, in case we need to back up.
|
|
bool success = true;
|
|
const size_t result_length_on_entry = result->size();
|
|
|
|
// Append header like "SET "
|
|
result->append(type == SET ? SET_MBS : SET_EXPORT_MBS);
|
|
result->push_back(' ');
|
|
|
|
// Append variable name like "fish_color_cwd".
|
|
if (wcsvarname(key_in.c_str())) {
|
|
debug(0, L"Illegal variable name: '%ls'", key_in.c_str());
|
|
success = false;
|
|
}
|
|
if (success && !append_utf8(key_in, result, storage)) {
|
|
debug(0, L"Could not convert %ls to narrow character string", key_in.c_str());
|
|
success = false;
|
|
}
|
|
|
|
// Append ":".
|
|
if (success) {
|
|
result->push_back(':');
|
|
}
|
|
|
|
// Append value.
|
|
if (success && !append_utf8(full_escape(val_in.c_str()), result, storage)) {
|
|
debug(0, L"Could not convert %ls to narrow character string", val_in.c_str());
|
|
success = false;
|
|
}
|
|
|
|
// Append newline.
|
|
if (success) {
|
|
result->push_back('\n');
|
|
}
|
|
|
|
// Don't modify result on failure. It's sufficient to simply resize it since all we ever did was
|
|
// append to it.
|
|
if (!success) {
|
|
result->resize(result_length_on_entry);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
env_universal_t::env_universal_t(const wcstring &path)
|
|
: explicit_vars_path(path), tried_renaming(false), last_read_file(kInvalidFileID) {
|
|
VOMIT_ON_FAILURE(pthread_mutex_init(&lock, NULL));
|
|
}
|
|
|
|
env_universal_t::~env_universal_t() { pthread_mutex_destroy(&lock); }
|
|
|
|
env_var_t env_universal_t::get(const wcstring &name) const {
|
|
env_var_t result = env_var_t::missing_var();
|
|
var_table_t::const_iterator where = vars.find(name);
|
|
if (where != vars.end()) {
|
|
result = env_var_t(where->second.val);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool env_universal_t::get_export(const wcstring &name) const {
|
|
bool result = false;
|
|
var_table_t::const_iterator where = vars.find(name);
|
|
if (where != vars.end()) {
|
|
result = where->second.exportv;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void env_universal_t::set_internal(const wcstring &key, const wcstring &val, bool exportv,
|
|
bool overwrite) {
|
|
ASSERT_IS_LOCKED(lock);
|
|
if (!overwrite && this->modified.find(key) != this->modified.end()) {
|
|
// This value has been modified and we're not overwriting it. Skip it.
|
|
return;
|
|
}
|
|
|
|
var_entry_t *entry = &vars[key];
|
|
if (entry->exportv != exportv || entry->val != val) {
|
|
entry->val = val;
|
|
entry->exportv = exportv;
|
|
|
|
// If we are overwriting, then this is now modified.
|
|
if (overwrite) {
|
|
this->modified.insert(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
void env_universal_t::set(const wcstring &key, const wcstring &val, bool exportv) {
|
|
scoped_lock locker(lock);
|
|
this->set_internal(key, val, exportv, true /* overwrite */);
|
|
}
|
|
|
|
bool env_universal_t::remove_internal(const wcstring &key) {
|
|
ASSERT_IS_LOCKED(lock);
|
|
size_t erased = this->vars.erase(key);
|
|
if (erased > 0) {
|
|
this->modified.insert(key);
|
|
}
|
|
return erased > 0;
|
|
}
|
|
|
|
bool env_universal_t::remove(const wcstring &key) {
|
|
scoped_lock locker(lock);
|
|
return this->remove_internal(key);
|
|
}
|
|
|
|
wcstring_list_t env_universal_t::get_names(bool show_exported, bool show_unexported) const {
|
|
wcstring_list_t result;
|
|
scoped_lock locker(lock);
|
|
var_table_t::const_iterator iter;
|
|
for (iter = vars.begin(); iter != vars.end(); ++iter) {
|
|
const wcstring &key = iter->first;
|
|
const var_entry_t &e = iter->second;
|
|
if ((e.exportv && show_exported) || (!e.exportv && show_unexported)) {
|
|
result.push_back(key);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// Given a variable table, generate callbacks representing the difference between our vars and the
|
|
// new vars.
|
|
void env_universal_t::generate_callbacks(const var_table_t &new_vars,
|
|
callback_data_list_t *callbacks) const {
|
|
assert(callbacks != NULL);
|
|
|
|
// Construct callbacks for erased values.
|
|
for (var_table_t::const_iterator iter = this->vars.begin(); iter != this->vars.end(); ++iter) {
|
|
const wcstring &key = iter->first;
|
|
|
|
// Skip modified values.
|
|
if (this->modified.find(key) != this->modified.end()) {
|
|
continue;
|
|
}
|
|
|
|
// If the value is not present in new_vars, it has been erased.
|
|
if (new_vars.find(key) == new_vars.end()) {
|
|
callbacks->push_back(callback_data_t(ERASE, key, L""));
|
|
}
|
|
}
|
|
|
|
// Construct callbacks for newly inserted or changed values.
|
|
for (var_table_t::const_iterator iter = new_vars.begin(); iter != new_vars.end(); ++iter) {
|
|
const wcstring &key = iter->first;
|
|
|
|
// Skip modified values.
|
|
if (this->modified.find(key) != this->modified.end()) {
|
|
continue;
|
|
}
|
|
|
|
// See if the value has changed.
|
|
const var_entry_t &new_entry = iter->second;
|
|
var_table_t::const_iterator existing = this->vars.find(key);
|
|
if (existing == this->vars.end() || existing->second.exportv != new_entry.exportv ||
|
|
existing->second.val != new_entry.val) {
|
|
// Value has changed.
|
|
callbacks->push_back(
|
|
callback_data_t(new_entry.exportv ? SET_EXPORT : SET, key, new_entry.val));
|
|
}
|
|
}
|
|
}
|
|
|
|
void env_universal_t::acquire_variables(var_table_t *vars_to_acquire) {
|
|
// Copy modified values from existing vars to vars_to_acquire.
|
|
for (std::set<wcstring>::iterator iter = this->modified.begin(); iter != this->modified.end();
|
|
++iter) {
|
|
const wcstring &key = *iter;
|
|
var_table_t::iterator src_iter = this->vars.find(key);
|
|
if (src_iter == this->vars.end()) {
|
|
/* The value has been deleted. */
|
|
vars_to_acquire->erase(key);
|
|
} else {
|
|
// The value has been modified. Copy it over. Note we can destructively modify the
|
|
// source entry in vars since we are about to get rid of this->vars entirely.
|
|
var_entry_t &src = src_iter->second;
|
|
var_entry_t &dst = (*vars_to_acquire)[key];
|
|
dst.val.swap(src.val);
|
|
dst.exportv = src.exportv;
|
|
}
|
|
}
|
|
|
|
// We have constructed all the callbacks and updated vars_to_acquire. Acquire it!
|
|
this->vars.swap(*vars_to_acquire);
|
|
}
|
|
|
|
void env_universal_t::load_from_fd(int fd, callback_data_list_t *callbacks) {
|
|
ASSERT_IS_LOCKED(lock);
|
|
assert(fd >= 0);
|
|
// Get the dev / inode.
|
|
const file_id_t current_file = file_id_for_fd(fd);
|
|
if (current_file == last_read_file) {
|
|
debug(5, L"universal log sync elided based on fstat()");
|
|
} else {
|
|
// Read a variables table from the file.
|
|
var_table_t new_vars = this->read_message_internal(fd);
|
|
|
|
// Announce changes.
|
|
if (callbacks != NULL) {
|
|
this->generate_callbacks(new_vars, callbacks);
|
|
}
|
|
|
|
// Acquire the new variables.
|
|
this->acquire_variables(&new_vars);
|
|
last_read_file = current_file;
|
|
}
|
|
}
|
|
|
|
bool env_universal_t::load_from_path(const wcstring &path, callback_data_list_t *callbacks) {
|
|
ASSERT_IS_LOCKED(lock);
|
|
|
|
// Check to see if the file is unchanged. We do this again in load_from_fd, but this avoids
|
|
// opening the file unnecessarily.
|
|
if (last_read_file != kInvalidFileID && file_id_for_path(path) == last_read_file) {
|
|
debug(5, L"universal log sync elided based on fast stat()");
|
|
return true;
|
|
}
|
|
|
|
bool result = false;
|
|
int fd = wopen_cloexec(path, O_RDONLY);
|
|
if (fd >= 0) {
|
|
debug(5, L"universal log reading from file");
|
|
this->load_from_fd(fd, callbacks);
|
|
close(fd);
|
|
result = true;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Writes our state to the fd. path is provided only for error reporting.
|
|
bool env_universal_t::write_to_fd(int fd, const wcstring &path) {
|
|
ASSERT_IS_LOCKED(lock);
|
|
assert(fd >= 0);
|
|
bool success = true;
|
|
|
|
// Stuff we output to fd.
|
|
std::string contents;
|
|
|
|
// Temporary storage.
|
|
std::string storage;
|
|
|
|
// Write the save message. If this fails, we don't bother complaining.
|
|
write_loop(fd, SAVE_MSG, strlen(SAVE_MSG));
|
|
|
|
var_table_t::const_iterator iter = vars.begin();
|
|
while (iter != vars.end()) {
|
|
// Append the entry. Note that append_file_entry may fail, but that only affects one
|
|
// variable; soldier on.
|
|
const wcstring &key = iter->first;
|
|
const var_entry_t &entry = iter->second;
|
|
append_file_entry(entry.exportv ? SET_EXPORT : SET, key, entry.val, &contents, &storage);
|
|
|
|
// Go to next.
|
|
++iter;
|
|
|
|
// Flush if this is the last iteration or we exceed a page.
|
|
if (iter == vars.end() || contents.size() >= 4096) {
|
|
if (write_loop(fd, contents.data(), contents.size()) < 0) {
|
|
int err = errno;
|
|
report_error(err, L"Unable to write to universal variables file '%ls'",
|
|
path.c_str());
|
|
success = false;
|
|
break;
|
|
}
|
|
contents.clear();
|
|
}
|
|
}
|
|
|
|
// Since we just wrote out this file, it matches our internal state; pretend we read from it.
|
|
this->last_read_file = file_id_for_fd(fd);
|
|
|
|
// We don't close the file.
|
|
return success;
|
|
}
|
|
|
|
bool env_universal_t::move_new_vars_file_into_place(const wcstring &src, const wcstring &dst) {
|
|
int ret = wrename(src, dst);
|
|
if (ret != 0) {
|
|
int err = errno;
|
|
report_error(err, L"Unable to rename file from '%ls' to '%ls'", src.c_str(), dst.c_str());
|
|
}
|
|
return ret == 0;
|
|
}
|
|
|
|
static wcstring fishd_get_config() {
|
|
bool done = false;
|
|
wcstring result;
|
|
|
|
env_var_t xdg_dir = env_get_string(L"XDG_CONFIG_HOME", ENV_GLOBAL | ENV_EXPORT);
|
|
if (!xdg_dir.missing_or_empty()) {
|
|
result = xdg_dir;
|
|
append_path_component(result, L"/fish");
|
|
if (!create_directory(result)) {
|
|
done = true;
|
|
}
|
|
} else {
|
|
env_var_t home = env_get_string(L"HOME", ENV_GLOBAL | ENV_EXPORT);
|
|
if (!home.missing_or_empty()) {
|
|
result = home;
|
|
append_path_component(result, L"/.config/fish");
|
|
if (!create_directory(result)) {
|
|
done = 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!done) {
|
|
// Bad juju.
|
|
debug(0, _(L"Unable to create a configuration directory for fish. Your personal settings "
|
|
L"will not be saved. Please set the $XDG_CONFIG_HOME variable to a directory "
|
|
L"where the current user has write access."));
|
|
result.clear();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool env_universal_t::load() {
|
|
scoped_lock locker(lock);
|
|
callback_data_list_t callbacks;
|
|
const wcstring vars_path =
|
|
explicit_vars_path.empty() ? default_vars_path() : explicit_vars_path;
|
|
bool success = load_from_path(vars_path, &callbacks);
|
|
if (!success && !tried_renaming && errno == ENOENT) {
|
|
// We failed to load, because the file was not found. Older fish used the hostname only. Try
|
|
// moving the filename based on the hostname into place; if that succeeds try again.
|
|
// Silently "upgraded."
|
|
tried_renaming = true;
|
|
wcstring hostname_id;
|
|
if (get_hostname_identifier(&hostname_id)) {
|
|
const wcstring hostname_path = wdirname(vars_path) + L'/' + hostname_id;
|
|
if (0 == wrename(hostname_path, vars_path)) {
|
|
// We renamed - try again.
|
|
success = this->load();
|
|
}
|
|
}
|
|
}
|
|
return success;
|
|
}
|
|
|
|
bool env_universal_t::open_temporary_file(const wcstring &directory, wcstring *out_path,
|
|
int *out_fd) {
|
|
// Create and open a temporary file for writing within the given directory. Try to create a
|
|
// temporary file, up to 10 times. We don't use mkstemps because we want to open it CLO_EXEC.
|
|
// This should almost always succeed on the first try.
|
|
assert(!string_suffixes_string(L"/", directory));
|
|
|
|
bool success = false;
|
|
int saved_errno = 0;
|
|
const wcstring tmp_name_template = directory + L"/fishd.tmp.XXXXXX";
|
|
wcstring tmp_name;
|
|
|
|
for (size_t attempt = 0; attempt < 10 && !success; attempt++) {
|
|
char *narrow_str = wcs2str(tmp_name_template.c_str());
|
|
#if HAVE_MKOSTEMP
|
|
int result_fd = mkostemp(narrow_str, O_CLOEXEC);
|
|
#else
|
|
int result_fd = mkstemp(narrow_str);
|
|
if (result_fd != -1) {
|
|
fcntl(result_fd, F_SETFD, FD_CLOEXEC);
|
|
}
|
|
#endif
|
|
|
|
saved_errno = errno;
|
|
success = result_fd != -1;
|
|
*out_fd = result_fd;
|
|
*out_path = str2wcstring(narrow_str);
|
|
free(narrow_str);
|
|
}
|
|
|
|
if (!success) {
|
|
report_error(saved_errno, L"Unable to open temporary file '%ls'", out_path->c_str());
|
|
}
|
|
return success;
|
|
}
|
|
|
|
bool env_universal_t::open_and_acquire_lock(const wcstring &path, int *out_fd) {
|
|
// Attempt to open the file for reading at the given path, atomically acquiring a lock. On BSD,
|
|
// we can use O_EXLOCK. On Linux, we open the file, take a lock, and then compare fstat() to
|
|
// stat(); if they match, it means that the file was not replaced before we acquired the lock.
|
|
//
|
|
// We pass O_RDONLY with O_CREAT; this creates a potentially empty file. We do this so that we
|
|
// have something to lock on.
|
|
int result_fd = -1;
|
|
bool needs_lock = true;
|
|
int flags = O_RDWR | O_CREAT;
|
|
#ifdef O_EXLOCK
|
|
flags |= O_EXLOCK;
|
|
needs_lock = false;
|
|
#endif
|
|
for (;;) {
|
|
int fd = wopen_cloexec(path, flags, 0644);
|
|
if (fd < 0) {
|
|
int err = errno;
|
|
if (err == EINTR) {
|
|
/* Signal; try again */
|
|
continue;
|
|
}
|
|
#ifdef O_EXLOCK
|
|
else if (err == ENOTSUP || err == EOPNOTSUPP) {
|
|
// Filesystem probably does not support locking. Clear the flag and try again. Note
|
|
// that we try taking the lock via flock anyways. Note that on Linux the two errno
|
|
// symbols have the same value but on BSD they're different.
|
|
flags &= ~O_EXLOCK;
|
|
needs_lock = true;
|
|
continue;
|
|
}
|
|
#endif
|
|
else {
|
|
report_error(err, L"Unable to open universal variable file '%ls'", path.c_str());
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we get here, we must have a valid fd.
|
|
assert(fd >= 0);
|
|
|
|
// Try taking the lock, if necessary. If we failed, we may be on lockless NFS, etc.; in that
|
|
// case we pretend we succeeded. See the comment in save_to_path for the rationale.
|
|
if (needs_lock) {
|
|
while (flock(fd, LOCK_EX) < 0) {
|
|
/* error */
|
|
if (errno != EINTR) {
|
|
/* Do nothing per #2149 */
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Hopefully we got the lock. However, it's possible the file changed out from under us
|
|
// while we were waiting for the lock. Make sure that didn't happen.
|
|
if (file_id_for_fd(fd) != file_id_for_path(path)) {
|
|
// Oops, it changed! Try again.
|
|
close(fd);
|
|
continue;
|
|
}
|
|
|
|
// Finally, we have an fd that's valid and hopefully locked. We're done.
|
|
assert(fd >= 0);
|
|
result_fd = fd;
|
|
break;
|
|
}
|
|
|
|
*out_fd = result_fd;
|
|
|
|
return result_fd >= 0;
|
|
}
|
|
|
|
// Returns true if modified variables were written, false if not. (There may still be variable
|
|
// changes due to other processes on a false return).
|
|
bool env_universal_t::sync(callback_data_list_t *callbacks) {
|
|
debug(5, L"universal log sync");
|
|
scoped_lock locker(lock);
|
|
// Our saving strategy:
|
|
//
|
|
// 1. Open the file, producing an fd.
|
|
// 2. Lock the file (may be combined with step 1 on systems with O_EXLOCK)
|
|
// 3. After taking the lock, check if the file at the given path is different from what we
|
|
// opened. If so, start over.
|
|
// 4. Read from the file. This can be elided if its dev/inode is unchanged since the last read
|
|
// 5. Open an adjacent temporary file
|
|
// 6. Write our changes to an adjacent file
|
|
// 7. Move the adjacent file into place via rename. This is assumed to be atomic.
|
|
// 8. Release the lock and close the file
|
|
//
|
|
// Consider what happens if Process 1 and 2 both do this simultaneously. Can there be data loss?
|
|
// Process 1 opens the file and then attempts to take the lock. Now, either process 1 will see
|
|
// the original file, or process 2's new file. If it sees the new file, we're OK: it's going to
|
|
// read from the new file, and so there's no data loss. If it sees the old file, then process 2
|
|
// must have locked it (if process 1 locks it, switch their roles). The lock will block until
|
|
// process 2 reaches step 7; at that point process 1 will reach step 2, notice that the file has
|
|
// changed, and then start over.
|
|
//
|
|
// It's possible that the underlying filesystem does not support locks (lockless NFS). In this
|
|
// case, we risk data loss if two shells try to write their universal variables simultaneously.
|
|
// In practice this is unlikely, since uvars are usually written interactively.
|
|
//
|
|
// Prior versions of fish used a hard link scheme to support file locking on lockless NFS. The
|
|
// risk here is that if the process crashes or is killed while holding the lock, future
|
|
// instances of fish will not be able to obtain it. This seems to be a greater risk than that of
|
|
// data loss on lockless NFS. Users who put their home directory on lockless NFS are playing
|
|
// with fire anyways.
|
|
const wcstring &vars_path =
|
|
explicit_vars_path.empty() ? default_vars_path() : explicit_vars_path;
|
|
|
|
if (vars_path.empty()) {
|
|
debug(2, L"No universal variable path available");
|
|
return false;
|
|
}
|
|
|
|
// If we have no changes, just load.
|
|
if (modified.empty()) {
|
|
this->load_from_path(vars_path, callbacks);
|
|
debug(5, L"universal log no modifications");
|
|
return false;
|
|
}
|
|
|
|
const wcstring directory = wdirname(vars_path);
|
|
bool success = true;
|
|
int vars_fd = -1;
|
|
int private_fd = -1;
|
|
wcstring private_file_path;
|
|
|
|
debug(5, L"universal log performing full sync");
|
|
|
|
// Open the file.
|
|
if (success) {
|
|
success = this->open_and_acquire_lock(vars_path, &vars_fd);
|
|
if (!success) debug(5, L"universal log open_and_acquire_lock() failed");
|
|
}
|
|
|
|
// Read from it.
|
|
if (success) {
|
|
assert(vars_fd >= 0);
|
|
this->load_from_fd(vars_fd, callbacks);
|
|
}
|
|
|
|
// Open adjacent temporary file.
|
|
if (success) {
|
|
success = this->open_temporary_file(directory, &private_file_path, &private_fd);
|
|
if (!success) debug(5, L"universal log open_temporary_file() failed");
|
|
}
|
|
|
|
// Write to it.
|
|
if (success) {
|
|
assert(private_fd >= 0);
|
|
success = this->write_to_fd(private_fd, private_file_path);
|
|
if (!success) debug(5, L"universal log write_to_fd() failed");
|
|
}
|
|
|
|
if (success) {
|
|
// Ensure we maintain ownership and permissions (#2176).
|
|
struct stat sbuf;
|
|
if (wstat(vars_path, &sbuf) >= 0) {
|
|
if (fchown(private_fd, sbuf.st_uid, sbuf.st_gid) == -1)
|
|
debug(5, L"universal log fchown() failed");
|
|
if (fchmod(private_fd, sbuf.st_mode) == -1) debug(5, L"universal log fchmod() failed");
|
|
}
|
|
|
|
// Linux by default stores the mtime with low precision, low enough that updates that occur in quick
|
|
// succession may result in the same mtime (even the nanoseconds field). So manually set the mtime
|
|
// of the new file to a high-precision clock. Note that this is only necessary because Linux
|
|
// aggressively reuses inodes, causing the ABA problem; on other platforms we tend to notice the
|
|
// file has changed due to a different inode (or file size!)
|
|
//
|
|
// It's probably worth finding a simpler solution to this. The tests ran into this, but it's
|
|
// unlikely to affect users.
|
|
#if HAVE_CLOCK_GETTIME && HAVE_FUTIMENS
|
|
struct timespec times[2] = {};
|
|
times[0].tv_nsec = UTIME_OMIT; // don't change ctime
|
|
if (0 == clock_gettime(CLOCK_REALTIME, ×[1])) {
|
|
futimens(private_fd, times);
|
|
}
|
|
#endif
|
|
|
|
// Apply new file.
|
|
success = this->move_new_vars_file_into_place(private_file_path, vars_path);
|
|
if (!success) debug(5, L"universal log move_new_vars_file_into_place() failed");
|
|
}
|
|
|
|
if (success) {
|
|
// Since we moved the new file into place, clear the path so we don't try to unlink it.
|
|
private_file_path.clear();
|
|
}
|
|
|
|
// Clean up.
|
|
if (vars_fd >= 0) {
|
|
close(vars_fd);
|
|
}
|
|
if (private_fd >= 0) {
|
|
close(private_fd);
|
|
}
|
|
if (!private_file_path.empty()) {
|
|
wunlink(private_file_path);
|
|
}
|
|
|
|
if (success) {
|
|
// All of our modified variables have now been written out.
|
|
modified.clear();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
var_table_t env_universal_t::read_message_internal(int fd) {
|
|
var_table_t result;
|
|
|
|
// Temp value used to avoid repeated allocations.
|
|
wcstring storage;
|
|
|
|
// The line we construct (and then parse).
|
|
std::string line;
|
|
wcstring wide_line;
|
|
for (;;) {
|
|
// Read into a buffer. Note this is NOT null-terminated!
|
|
char buffer[1024];
|
|
ssize_t amt = read_loop(fd, buffer, sizeof buffer);
|
|
if (amt <= 0) {
|
|
break;
|
|
}
|
|
const size_t bufflen = (size_t)amt;
|
|
|
|
// Walk over it by lines. The contents of an unterminated line will be left in 'line' for
|
|
// the next iteration.
|
|
ssize_t line_start = 0;
|
|
while (line_start < amt) {
|
|
// Run until we hit a newline.
|
|
size_t cursor = line_start;
|
|
while (cursor < bufflen && buffer[cursor] != '\n') {
|
|
cursor++;
|
|
}
|
|
|
|
// Copy over what we read.
|
|
line.append(buffer + line_start, cursor - line_start);
|
|
|
|
// Process it if it's a newline (which is true if we are before the end of the buffer).
|
|
if (cursor < bufflen && !line.empty()) {
|
|
if (utf8_to_wchar(line.data(), line.size(), &wide_line, 0)) {
|
|
env_universal_t::parse_message_internal(wide_line, &result, &storage);
|
|
}
|
|
line.clear();
|
|
}
|
|
|
|
// Skip over the newline (or skip past the end).
|
|
line_start = cursor + 1;
|
|
}
|
|
}
|
|
|
|
// We make no effort to handle an unterminated last line.
|
|
return result;
|
|
}
|
|
|
|
/// Parse message msg/
|
|
void env_universal_t::parse_message_internal(const wcstring &msgstr, var_table_t *vars,
|
|
wcstring *storage) {
|
|
const wchar_t *msg = msgstr.c_str();
|
|
|
|
// debug(3, L"parse_message( %ls );", msg);
|
|
if (msg[0] == L'#') return;
|
|
|
|
bool is_set_export = match(msg, SET_EXPORT_STR);
|
|
bool is_set = !is_set_export && match(msg, SET_STR);
|
|
if (is_set || is_set_export) {
|
|
const wchar_t *name, *tmp;
|
|
const bool exportv = is_set_export;
|
|
|
|
name = msg + (exportv ? wcslen(SET_EXPORT_STR) : wcslen(SET_STR));
|
|
while (name[0] == L'\t' || name[0] == L' ') name++;
|
|
|
|
tmp = wcschr(name, L':');
|
|
if (tmp) {
|
|
// Use 'storage' to hold our key to avoid allocations.
|
|
storage->assign(name, tmp - name);
|
|
const wcstring &key = *storage;
|
|
|
|
wcstring val;
|
|
if (unescape_string(tmp + 1, &val, 0)) {
|
|
var_entry_t &entry = (*vars)[key];
|
|
entry.exportv = exportv;
|
|
entry.val.swap(val); // acquire the value
|
|
}
|
|
} else {
|
|
debug(1, PARSE_ERR, msg);
|
|
}
|
|
} else {
|
|
debug(1, PARSE_ERR, msg);
|
|
}
|
|
}
|
|
|
|
/// Maximum length of hostname. Longer hostnames are truncated.
|
|
#define HOSTNAME_LEN 32
|
|
|
|
/// Length of a MAC address.
|
|
#define MAC_ADDRESS_MAX_LEN 6
|
|
|
|
// Thanks to Jan Brittenson, http://lists.apple.com/archives/xcode-users/2009/May/msg00062.html
|
|
#ifdef SIOCGIFHWADDR
|
|
|
|
// Linux
|
|
#include <net/if.h>
|
|
static bool get_mac_address(unsigned char macaddr[MAC_ADDRESS_MAX_LEN],
|
|
const char *interface = "eth0") {
|
|
bool result = false;
|
|
const int dummy = socket(AF_INET, SOCK_STREAM, 0);
|
|
if (dummy >= 0) {
|
|
struct ifreq r;
|
|
strncpy((char *)r.ifr_name, interface, sizeof r.ifr_name - 1);
|
|
r.ifr_name[sizeof r.ifr_name - 1] = 0;
|
|
if (ioctl(dummy, SIOCGIFHWADDR, &r) >= 0) {
|
|
memcpy(macaddr, r.ifr_hwaddr.sa_data, MAC_ADDRESS_MAX_LEN);
|
|
result = true;
|
|
}
|
|
close(dummy);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#elif defined(HAVE_GETIFADDRS)
|
|
|
|
// OS X and BSD
|
|
#include <ifaddrs.h>
|
|
#include <net/if_dl.h>
|
|
static bool get_mac_address(unsigned char macaddr[MAC_ADDRESS_MAX_LEN],
|
|
const char *interface = "en0") {
|
|
// BSD, Mac OS X
|
|
struct ifaddrs *ifap;
|
|
bool ok = false;
|
|
|
|
if (getifaddrs(&ifap) != 0) {
|
|
return ok;
|
|
}
|
|
|
|
for (const ifaddrs *p = ifap; p; p = p->ifa_next) {
|
|
bool is_af_link = p->ifa_addr && p->ifa_addr->sa_family == AF_LINK;
|
|
if (is_af_link && p->ifa_name && p->ifa_name[0] &&
|
|
!strcmp((const char *)p->ifa_name, interface)) {
|
|
const sockaddr_dl &sdl = *reinterpret_cast<sockaddr_dl *>(p->ifa_addr);
|
|
|
|
size_t alen = sdl.sdl_alen;
|
|
if (alen > MAC_ADDRESS_MAX_LEN) alen = MAC_ADDRESS_MAX_LEN;
|
|
memcpy(macaddr, sdl.sdl_data + sdl.sdl_nlen, alen);
|
|
ok = true;
|
|
break;
|
|
}
|
|
}
|
|
freeifaddrs(ifap);
|
|
return ok;
|
|
}
|
|
|
|
#else
|
|
|
|
// Unsupported
|
|
static bool get_mac_address(unsigned char macaddr[MAC_ADDRESS_MAX_LEN]) { return false; }
|
|
|
|
#endif
|
|
|
|
/// Function to get an identifier based on the hostname.
|
|
static bool get_hostname_identifier(wcstring *result) {
|
|
bool success = false;
|
|
char hostname[HOSTNAME_LEN + 1] = {};
|
|
if (gethostname(hostname, HOSTNAME_LEN) == 0) {
|
|
result->assign(str2wcstring(hostname));
|
|
success = true;
|
|
}
|
|
return success;
|
|
}
|
|
|
|
/// Get a sort of unique machine identifier. Prefer the MAC address; if that fails, fall back to the
|
|
/// hostname; if that fails, pick something.
|
|
wcstring get_machine_identifier() {
|
|
wcstring result;
|
|
unsigned char mac_addr[MAC_ADDRESS_MAX_LEN] = {};
|
|
if (get_mac_address(mac_addr)) {
|
|
result.reserve(2 * MAC_ADDRESS_MAX_LEN);
|
|
for (size_t i = 0; i < MAC_ADDRESS_MAX_LEN; i++) {
|
|
append_format(result, L"%02x", mac_addr[i]);
|
|
}
|
|
} else if (!get_hostname_identifier(&result)) {
|
|
result.assign(L"nohost"); // fallback to a dummy value
|
|
}
|
|
return result;
|
|
}
|
|
|
|
class universal_notifier_shmem_poller_t : public universal_notifier_t {
|
|
// This is what our shared memory looks like. Everything here is stored in network byte order
|
|
// (big-endian).
|
|
struct universal_notifier_shmem_t {
|
|
uint32_t magic;
|
|
uint32_t version;
|
|
uint32_t universal_variable_seed;
|
|
};
|
|
|
|
#define SHMEM_MAGIC_NUMBER 0xF154
|
|
#define SHMEM_VERSION_CURRENT 1000
|
|
|
|
private:
|
|
long long last_change_time;
|
|
uint32_t last_seed;
|
|
volatile universal_notifier_shmem_t *region;
|
|
|
|
void open_shmem() {
|
|
assert(region == NULL);
|
|
|
|
// Use a path based on our uid to avoid collisions.
|
|
char path[NAME_MAX];
|
|
snprintf(path, sizeof path, "/%ls_shmem_%d", program_name ? program_name : L"fish",
|
|
getuid());
|
|
|
|
bool errored = false;
|
|
int fd = shm_open(path, O_RDWR | O_CREAT, 0600);
|
|
if (fd < 0) {
|
|
int err = errno;
|
|
report_error(err, L"Unable to open shared memory with path '%s'", path);
|
|
errored = true;
|
|
}
|
|
|
|
// Get the size.
|
|
off_t size = 0;
|
|
if (!errored) {
|
|
struct stat buf = {};
|
|
if (fstat(fd, &buf) < 0) {
|
|
int err = errno;
|
|
report_error(err, L"Unable to fstat shared memory object with path '%s'", path);
|
|
errored = true;
|
|
}
|
|
size = buf.st_size;
|
|
}
|
|
|
|
// Set the size, if it's too small.
|
|
bool set_size = !errored && size < (off_t)sizeof(universal_notifier_shmem_t);
|
|
if (set_size && ftruncate(fd, sizeof(universal_notifier_shmem_t)) < 0) {
|
|
int err = errno;
|
|
report_error(err, L"Unable to truncate shared memory object with path '%s'", path);
|
|
errored = true;
|
|
}
|
|
|
|
// Memory map the region.
|
|
if (!errored) {
|
|
void *addr = mmap(NULL, sizeof(universal_notifier_shmem_t), PROT_READ | PROT_WRITE,
|
|
MAP_SHARED, fd, 0);
|
|
if (addr == MAP_FAILED) {
|
|
int err = errno;
|
|
report_error(err, L"Unable to memory map shared memory object with path '%s'",
|
|
path);
|
|
this->region = NULL;
|
|
} else {
|
|
this->region = static_cast<universal_notifier_shmem_t *>(addr);
|
|
}
|
|
}
|
|
|
|
// Close the fd, even if the mapping succeeded.
|
|
if (fd >= 0) {
|
|
close(fd);
|
|
}
|
|
|
|
// Read the current seed.
|
|
this->poll();
|
|
// cppcheck-suppress memleak // addr not really leaked
|
|
}
|
|
|
|
public:
|
|
// Our notification involves changing the value in our shared memory. In practice, all clients
|
|
// will be in separate processes, so it suffices to set the value to a pid. For testing
|
|
// purposes, however, it's useful to keep them in the same process, so we increment the value.
|
|
// This isn't "safe" in the sense that multiple simultaneous increments may result in one being
|
|
// lost, but it should always result in the value being changed, which is sufficient.
|
|
void post_notification() {
|
|
if (region != NULL) {
|
|
/* Read off the seed */
|
|
uint32_t seed = ntohl(region->universal_variable_seed); //!OCLINT(constant cond op)
|
|
|
|
// Increment it. Don't let it wrap to zero.
|
|
do {
|
|
seed++;
|
|
} while (seed == 0);
|
|
last_seed = seed;
|
|
|
|
// Write out our data.
|
|
region->magic = htonl(SHMEM_MAGIC_NUMBER); //!OCLINT(constant cond op)
|
|
region->version = htonl(SHMEM_VERSION_CURRENT); //!OCLINT(constant cond op)
|
|
region->universal_variable_seed = htonl(seed); //!OCLINT(constant cond op)
|
|
}
|
|
}
|
|
|
|
universal_notifier_shmem_poller_t() : last_change_time(0), last_seed(0), region(NULL) {
|
|
open_shmem();
|
|
}
|
|
|
|
~universal_notifier_shmem_poller_t() {
|
|
if (region != NULL) {
|
|
// Behold: C++ in all its glory!
|
|
void *address = const_cast<void *>(static_cast<volatile void *>(region));
|
|
if (munmap(address, sizeof(universal_notifier_shmem_t)) < 0) {
|
|
wperror(L"munmap");
|
|
}
|
|
}
|
|
}
|
|
|
|
bool poll() {
|
|
bool result = false;
|
|
if (region != NULL) {
|
|
uint32_t seed = ntohl(region->universal_variable_seed); //!OCLINT(constant cond op)
|
|
if (seed != last_seed) {
|
|
result = true;
|
|
last_seed = seed;
|
|
last_change_time = get_time();
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
unsigned long usec_delay_between_polls() const {
|
|
// If it's been less than five seconds since the last change, we poll quickly Otherwise we
|
|
// poll more slowly. Note that a poll is a very cheap shmem read. The bad part about making
|
|
// this high is the process scheduling/wakeups it produces.
|
|
long long usec_per_sec = 1000000;
|
|
if (get_time() - last_change_time < 5LL * usec_per_sec) {
|
|
return usec_per_sec / 10; // 10 times a second
|
|
}
|
|
return usec_per_sec / 3; // 3 times a second
|
|
}
|
|
};
|
|
|
|
/// A notifyd-based notifier. Very straightforward.
|
|
class universal_notifier_notifyd_t : public universal_notifier_t {
|
|
int notify_fd;
|
|
int token;
|
|
std::string name;
|
|
|
|
void setup_notifyd() {
|
|
#if FISH_NOTIFYD_AVAILABLE
|
|
// Per notify(3), the user.uid.%d style is only accessible to processes with that uid.
|
|
char local_name[256];
|
|
snprintf(local_name, sizeof local_name, "user.uid.%d.%ls.uvars", getuid(),
|
|
program_name ? program_name : L"fish");
|
|
name.assign(local_name);
|
|
|
|
uint32_t status =
|
|
notify_register_file_descriptor(name.c_str(), &this->notify_fd, 0, &this->token);
|
|
if (status != NOTIFY_STATUS_OK) {
|
|
fprintf(stderr,
|
|
"Warning: notify_register_file_descriptor() failed with status %u. Universal "
|
|
"variable notifications may not be received.",
|
|
status);
|
|
}
|
|
if (this->notify_fd >= 0) {
|
|
// Mark us for non-blocking reads, and CLO_EXEC.
|
|
int flags = fcntl(this->notify_fd, F_GETFL, 0);
|
|
if (flags >= 0 && !(flags & O_NONBLOCK)) {
|
|
fcntl(this->notify_fd, F_SETFL, flags | O_NONBLOCK);
|
|
}
|
|
|
|
set_cloexec(this->notify_fd);
|
|
// Serious hack: notify_fd is likely the read end of a pipe. The other end is owned by
|
|
// libnotify, which does not mark it as CLO_EXEC (it should!). The next fd is probably
|
|
// notify_fd + 1. Do it ourselves. If the implementation changes and some other FD gets
|
|
// marked as CLO_EXEC, that's probably a good thing.
|
|
set_cloexec(this->notify_fd + 1);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public:
|
|
universal_notifier_notifyd_t() : notify_fd(-1), token(-1 /* NOTIFY_TOKEN_INVALID */) {
|
|
setup_notifyd();
|
|
}
|
|
|
|
~universal_notifier_notifyd_t() {
|
|
if (token != -1 /* NOTIFY_TOKEN_INVALID */) {
|
|
#if FISH_NOTIFYD_AVAILABLE
|
|
notify_cancel(token);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
int notification_fd() { return notify_fd; }
|
|
|
|
bool notification_fd_became_readable(int fd) {
|
|
// notifyd notifications come in as 32 bit values. We don't care about the value. We set
|
|
// ourselves as non-blocking, so just read until we can't read any more.
|
|
assert(fd == notify_fd);
|
|
bool read_something = false;
|
|
unsigned char buff[64];
|
|
ssize_t amt_read;
|
|
do {
|
|
amt_read = read(notify_fd, buff, sizeof buff);
|
|
read_something = (read_something || amt_read > 0);
|
|
} while (amt_read == sizeof buff);
|
|
return read_something;
|
|
}
|
|
|
|
void post_notification() {
|
|
#if FISH_NOTIFYD_AVAILABLE
|
|
uint32_t status = notify_post(name.c_str());
|
|
if (status != NOTIFY_STATUS_OK) {
|
|
fprintf(stderr,
|
|
"Warning: notify_post() failed with status %u. Universal variable "
|
|
"notifications may not be sent.",
|
|
status);
|
|
}
|
|
#endif
|
|
}
|
|
};
|
|
|
|
#define NAMED_PIPE_FLASH_DURATION_USEC (1000000 / 10)
|
|
#define SUSTAINED_READABILITY_CLEANUP_DURATION_USEC (1000000 * 5)
|
|
|
|
// Named-pipe based notifier. All clients open the same named pipe for reading and writing. The
|
|
// pipe's readability status is a trigger to enter polling mode.
|
|
//
|
|
// To post a notification, write some data to the pipe, wait a little while, and then read it back.
|
|
//
|
|
// To receive a notification, watch for the pipe to become readable. When it does, enter a polling
|
|
// mode until the pipe is no longer readable. To guard against the possibility of a shell exiting
|
|
// when there is data remaining in the pipe, if the pipe is kept readable too long, clients will
|
|
// attempt to read data out of it (to render it no longer readable).
|
|
class universal_notifier_named_pipe_t : public universal_notifier_t {
|
|
int pipe_fd;
|
|
long long readback_time_usec;
|
|
size_t readback_amount;
|
|
|
|
bool polling_due_to_readable_fd;
|
|
long long drain_if_still_readable_time_usec;
|
|
|
|
void make_pipe(const wchar_t *test_path) {
|
|
wcstring vars_path = test_path ? wcstring(test_path) : default_named_pipe_path();
|
|
vars_path.append(L".notifier");
|
|
const std::string narrow_path = wcs2string(vars_path);
|
|
|
|
int fd = wopen_cloexec(vars_path, O_RDWR | O_NONBLOCK, 0600);
|
|
if (fd < 0 && errno == ENOENT) {
|
|
// File doesn't exist, try creating it.
|
|
int mkfifo_status = mkfifo(narrow_path.c_str(), 0600);
|
|
if (mkfifo_status != -1) {
|
|
fd = wopen_cloexec(vars_path, O_RDWR | O_NONBLOCK, 0600);
|
|
}
|
|
}
|
|
|
|
if (fd < 0) {
|
|
// Maybe open failed, maybe mkfifo failed.
|
|
int err = errno;
|
|
// We explicitly do NOT report an error for ENOENT or EACCESS. This works around #1955,
|
|
// where $XDG_RUNTIME_DIR may get a bogus value under success.
|
|
if (err != ENOENT && err != EPERM) {
|
|
report_error(
|
|
err, L"Unable to make or open a FIFO for universal variables with path '%ls'",
|
|
vars_path.c_str());
|
|
}
|
|
pipe_fd = -1;
|
|
} else {
|
|
pipe_fd = fd;
|
|
}
|
|
}
|
|
|
|
void drain_excessive_data() {
|
|
// The pipe seems to have data on it, that won't go away. Read a big chunk out of it. We
|
|
// don't read until it's exhausted, because if someone were to pipe say /dev/null, that
|
|
// would cause us to hang!
|
|
size_t read_amt = 64 * 1024;
|
|
void *buff = malloc(read_amt);
|
|
read_ignore(this->pipe_fd, buff, read_amt);
|
|
free(buff);
|
|
}
|
|
|
|
public:
|
|
explicit universal_notifier_named_pipe_t(const wchar_t *test_path)
|
|
: pipe_fd(-1),
|
|
readback_time_usec(0),
|
|
readback_amount(0),
|
|
polling_due_to_readable_fd(false),
|
|
drain_if_still_readable_time_usec(0) {
|
|
make_pipe(test_path);
|
|
}
|
|
|
|
~universal_notifier_named_pipe_t() {
|
|
if (pipe_fd >= 0) {
|
|
close(pipe_fd);
|
|
}
|
|
}
|
|
|
|
int notification_fd() {
|
|
if (polling_due_to_readable_fd) {
|
|
// We are in polling mode because we think our fd is readable. This means that, if we
|
|
// return it to be select()'d on, we'll be called back immediately. So don't return it.
|
|
return -1;
|
|
}
|
|
// We are not in polling mode. Return the fd so it can be watched.
|
|
return pipe_fd;
|
|
}
|
|
|
|
bool notification_fd_became_readable(int fd) {
|
|
// Our fd is readable. We deliberately do not read anything out of it: if we did, other
|
|
// sessions may miss the notification. Instead, we go into "polling mode:" we do not
|
|
// select() on our fd for a while, and sync periodically until the fd is no longer readable.
|
|
// However, if we are the one who posted the notification, we don't sync (until we clean
|
|
// up!)
|
|
UNUSED(fd);
|
|
bool should_sync = false;
|
|
if (readback_time_usec == 0) {
|
|
polling_due_to_readable_fd = true;
|
|
drain_if_still_readable_time_usec =
|
|
get_time() + SUSTAINED_READABILITY_CLEANUP_DURATION_USEC;
|
|
should_sync = true;
|
|
}
|
|
return should_sync;
|
|
}
|
|
|
|
void post_notification() {
|
|
if (pipe_fd >= 0) {
|
|
// We need to write some data (any data) to the pipe, then wait for a while, then read
|
|
// it back. Nobody is expected to read it except us.
|
|
int pid_nbo = htonl(getpid()); //!OCLINT(constant cond op)
|
|
ssize_t amt_written = write(this->pipe_fd, &pid_nbo, sizeof pid_nbo);
|
|
if (amt_written < 0 && (errno == EWOULDBLOCK || errno == EAGAIN)) {
|
|
// Very unsual: the pipe is full!
|
|
drain_excessive_data();
|
|
}
|
|
|
|
// Now schedule a read for some time in the future.
|
|
this->readback_time_usec = get_time() + NAMED_PIPE_FLASH_DURATION_USEC;
|
|
this->readback_amount += sizeof pid_nbo;
|
|
}
|
|
}
|
|
|
|
unsigned long usec_delay_between_polls() const {
|
|
unsigned long readback_delay = ULONG_MAX;
|
|
if (this->readback_time_usec > 0) {
|
|
// How long until the readback?
|
|
long long now = get_time();
|
|
if (now >= this->readback_time_usec) {
|
|
// Oops, it already passed! Return something tiny.
|
|
readback_delay = 1000;
|
|
} else {
|
|
readback_delay = (unsigned long)(this->readback_time_usec - now);
|
|
}
|
|
}
|
|
|
|
unsigned long polling_delay = ULONG_MAX;
|
|
if (polling_due_to_readable_fd) {
|
|
// We're in polling mode. Don't return a value less than our polling interval.
|
|
polling_delay = NAMED_PIPE_FLASH_DURATION_USEC;
|
|
}
|
|
|
|
// Now return the smaller of the two values. If we get ULONG_MAX, it means there's no more
|
|
// need to poll; in that case return 0.
|
|
unsigned long result = mini(readback_delay, polling_delay);
|
|
if (result == ULONG_MAX) {
|
|
result = 0;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool poll() {
|
|
// Check if we are past the readback time.
|
|
if (this->readback_time_usec > 0 && get_time() >= this->readback_time_usec) {
|
|
// Read back what we wrote. We do nothing with the value.
|
|
while (this->readback_amount > 0) {
|
|
char buff[64];
|
|
size_t amt_to_read = mini(this->readback_amount, sizeof buff);
|
|
read_ignore(this->pipe_fd, buff, amt_to_read);
|
|
this->readback_amount -= amt_to_read;
|
|
}
|
|
assert(this->readback_amount == 0);
|
|
this->readback_time_usec = 0;
|
|
}
|
|
|
|
// Check to see if we are doing readability polling.
|
|
if (!polling_due_to_readable_fd || pipe_fd < 0) {
|
|
return false;
|
|
}
|
|
|
|
// We are polling, so we are definitely going to sync.
|
|
// See if this is still readable.
|
|
fd_set fds;
|
|
FD_ZERO(&fds);
|
|
FD_SET(this->pipe_fd, &fds);
|
|
struct timeval timeout = {};
|
|
select(this->pipe_fd + 1, &fds, NULL, NULL, &timeout);
|
|
if (!FD_ISSET(this->pipe_fd, &fds)) {
|
|
// No longer readable, no longer polling.
|
|
polling_due_to_readable_fd = false;
|
|
drain_if_still_readable_time_usec = 0;
|
|
} else {
|
|
// Still readable. If it's been readable for a long time, there is probably
|
|
// lingering data on the pipe.
|
|
if (get_time() >= drain_if_still_readable_time_usec) {
|
|
drain_excessive_data();
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
|
|
class universal_notifier_null_t : public universal_notifier_t {}; // does nothing
|
|
|
|
static universal_notifier_t::notifier_strategy_t fetch_default_strategy_from_environment() {
|
|
universal_notifier_t::notifier_strategy_t result = universal_notifier_t::strategy_default;
|
|
|
|
const struct {
|
|
const char *name;
|
|
universal_notifier_t::notifier_strategy_t strat;
|
|
} options[] = {{"default", universal_notifier_t::strategy_default},
|
|
{"shmem", universal_notifier_t::strategy_shmem_polling},
|
|
{"pipe", universal_notifier_t::strategy_named_pipe},
|
|
{"notifyd", universal_notifier_t::strategy_notifyd}};
|
|
const size_t opt_count = sizeof options / sizeof *options;
|
|
|
|
const char *var = getenv(UNIVERSAL_NOTIFIER_ENV_NAME);
|
|
if (var == NULL || var[0] == '\0') {
|
|
return result;
|
|
}
|
|
|
|
size_t i;
|
|
for (i = 0; i < opt_count; i++) {
|
|
if (!strcmp(var, options[i].name)) {
|
|
result = options[i].strat;
|
|
break;
|
|
}
|
|
}
|
|
if (i >= opt_count) {
|
|
fprintf(stderr, "Warning: unrecognized value for %s: '%s'\n",
|
|
UNIVERSAL_NOTIFIER_ENV_NAME, var);
|
|
fprintf(stderr, "Warning: valid values are ");
|
|
for (size_t j = 0; j < opt_count; j++) {
|
|
fprintf(stderr, "%s%s", j > 0 ? ", " : "", options[j].name);
|
|
}
|
|
fputc('\n', stderr);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
universal_notifier_t::notifier_strategy_t universal_notifier_t::resolve_default_strategy() {
|
|
static universal_notifier_t::notifier_strategy_t s_explicit_strategy =
|
|
fetch_default_strategy_from_environment();
|
|
if (s_explicit_strategy != strategy_default) {
|
|
return s_explicit_strategy;
|
|
}
|
|
#if FISH_NOTIFYD_AVAILABLE
|
|
return strategy_notifyd;
|
|
#elif defined(__CYGWIN__)
|
|
return strategy_shmem_polling;
|
|
#else
|
|
return strategy_named_pipe;
|
|
#endif
|
|
}
|
|
|
|
universal_notifier_t &universal_notifier_t::default_notifier() {
|
|
static universal_notifier_t *result = new_notifier_for_strategy(strategy_default);
|
|
return *result;
|
|
}
|
|
|
|
universal_notifier_t *universal_notifier_t::new_notifier_for_strategy(
|
|
universal_notifier_t::notifier_strategy_t strat, const wchar_t *test_path) {
|
|
if (strat == strategy_default) {
|
|
strat = resolve_default_strategy(); //!OCLINT(parameter reassignment)
|
|
}
|
|
switch (strat) {
|
|
case strategy_shmem_polling: {
|
|
return new universal_notifier_shmem_poller_t();
|
|
}
|
|
case strategy_notifyd: {
|
|
return new universal_notifier_notifyd_t();
|
|
}
|
|
case strategy_named_pipe: {
|
|
return new universal_notifier_named_pipe_t(test_path);
|
|
}
|
|
case strategy_null: {
|
|
return new universal_notifier_null_t();
|
|
}
|
|
default: {
|
|
fprintf(stderr, "Unsupported strategy %d\n", strat);
|
|
return NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Default implementations.
|
|
universal_notifier_t::universal_notifier_t() {}
|
|
|
|
universal_notifier_t::~universal_notifier_t() {}
|
|
|
|
int universal_notifier_t::notification_fd() { return -1; }
|
|
|
|
void universal_notifier_t::post_notification() {}
|
|
|
|
bool universal_notifier_t::poll() { return false; }
|
|
|
|
unsigned long universal_notifier_t::usec_delay_between_polls() const { return 0; }
|
|
|
|
bool universal_notifier_t::notification_fd_became_readable(int fd) {
|
|
UNUSED(fd);
|
|
return false;
|
|
}
|