improve error msg about invalid HOME/XDG_* var

This change increases the amount of useful information when fish is
unable to create or use its config or data directory. We now make it
clear when neither var is set or one is set to an unusable location.

Fixes #3545
This commit is contained in:
Kurtis Rader 2016-11-26 16:35:48 -08:00
parent 422ff0f173
commit eaa3741336
5 changed files with 133 additions and 142 deletions

View file

@ -334,7 +334,7 @@ static bool variable_is_colon_delimited_array(const wcstring &str) {
} }
/// Set up the USER variable. /// Set up the USER variable.
static void setup_user(bool force=false) { static void setup_user(bool force) {
if (env_get_string(L"USER").missing_or_empty() || force) { if (env_get_string(L"USER").missing_or_empty() || force) {
const struct passwd *pw = getpwuid(getuid()); const struct passwd *pw = getpwuid(getuid());
if (pw && pw->pw_name) { if (pw && pw->pw_name) {
@ -404,7 +404,7 @@ void env_init(const struct config_paths_t *paths /* or NULL */) {
// Set up the USER and PATH variables // Set up the USER and PATH variables
setup_path(); setup_path();
setup_user(); setup_user(false);
// Set up the version variable. // Set up the version variable.
wcstring version = str2wcstring(get_fish_version()); wcstring version = str2wcstring(get_fish_version());
@ -429,7 +429,8 @@ void env_init(const struct config_paths_t *paths /* or NULL */) {
const env_var_t unam = env_get_string(L"USER"); const env_var_t unam = env_get_string(L"USER");
char *unam_narrow = wcs2str(unam.c_str()); char *unam_narrow = wcs2str(unam.c_str());
struct passwd *pw = getpwnam(unam_narrow); struct passwd *pw = getpwnam(unam_narrow);
if (pw == NULL) { // Maybe USER is set but it's bogus. Reset USER from the db and try again. if (pw == NULL) {
// Maybe USER is set but it's bogus. Reset USER from the db and try again.
setup_user(true); setup_user(true);
const env_var_t unam = env_get_string(L"USER"); const env_var_t unam = env_get_string(L"USER");
unam_narrow = wcs2str(unam.c_str()); unam_narrow = wcs2str(unam.c_str());
@ -747,9 +748,12 @@ env_var_t env_get_string(const wcstring &key, env_mode_flags_t mode) {
// that in env_set(). // that in env_set().
if (is_electric(key)) { if (is_electric(key)) {
if (!search_global) return env_var_t::missing_var(); if (!search_global) return env_var_t::missing_var();
// Big hack. We only allow getting the history on the main thread. Note that history_t may if (key == L"history") {
// ask for an environment variable, so don't take the lock here (we don't need it). // Big hack. We only allow getting the history on the main thread. Note that history_t
if (key == L"history" && is_main_thread()) { // may ask for an environment variable, so don't take the lock here (we don't need it).
if (!is_main_thread()) {
return env_var_t::missing_var();
}
env_var_t result; env_var_t result;
history_t *history = reader_get_history(); history_t *history = reader_get_history();
@ -767,7 +771,8 @@ env_var_t env_get_string(const wcstring &key, env_mode_flags_t mode) {
} else if (key == L"umask") { } else if (key == L"umask") {
return format_string(L"0%0.3o", get_umask()); return format_string(L"0%0.3o", get_umask());
} }
// We should never get here unless the electric var list is out of sync. // We should never get here unless the electric var list is out of sync with the above code.
DIE("unerecognized electric var name");
} }
if (search_local || search_global) { if (search_local || search_global) {

View file

@ -7,7 +7,6 @@
#include <fcntl.h> #include <fcntl.h>
#include <limits.h> #include <limits.h>
#include <pwd.h> #include <pwd.h>
#include <stdarg.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <sys/mman.h> #include <sys/mman.h>
@ -34,6 +33,7 @@
#include "env.h" #include "env.h"
#include "env_universal_common.h" #include "env_universal_common.h"
#include "fallback.h" // IWYU pragma: keep #include "fallback.h" // IWYU pragma: keep
#include "path.h"
#include "utf8.h" #include "utf8.h"
#include "util.h" #include "util.h"
#include "wutil.h" #include "wutil.h"
@ -70,7 +70,6 @@
"# This file is automatically generated by the fish.\n# Do NOT edit it directly, your " \ "# This file is automatically generated by the fish.\n# Do NOT edit it directly, your " \
"changes will be overwritten.\n" "changes will be overwritten.\n"
static wcstring fishd_get_config();
static wcstring get_machine_identifier(); static wcstring get_machine_identifier();
static bool get_hostname_identifier(wcstring *result); static bool get_hostname_identifier(wcstring *result);
@ -83,12 +82,10 @@ static wcstring vars_filename_in_directory(const wcstring &wdir) {
return result; return result;
} }
static const wcstring &default_vars_path() { static const wcstring default_vars_path() {
// Oclint complains about this being a "redundant local variable"; however it isn't because the wcstring path;
// assignment to a static var is needed to keep the object from being deleted when this function path_get_config(path);
// returns. return vars_filename_in_directory(path);
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 /// Check, and create if necessary, a secure runtime path Derived from tmux.c in tmux
@ -166,18 +163,6 @@ static bool match(const wchar_t *msg, const wchar_t *cmd) {
return true; 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. /// 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) { static bool is_universal_safe_to_encode_directly(wchar_t c) {
if (c < 32 || c > 128) return false; if (c < 32 || c > 128) return false;
@ -479,9 +464,9 @@ bool env_universal_t::write_to_fd(int fd, const wcstring &path) {
// Flush if this is the last iteration or we exceed a page. // Flush if this is the last iteration or we exceed a page.
if (iter == vars.end() || contents.size() >= 4096) { if (iter == vars.end() || contents.size() >= 4096) {
if (write_loop(fd, contents.data(), contents.size()) < 0) { if (write_loop(fd, contents.data(), contents.size()) < 0) {
int err = errno; const char *error = strerror(errno);
report_error(err, L"Unable to write to universal variables file '%ls'", debug(0, _(L"Unable to write to universal variables file '%ls': %s"), path.c_str(),
path.c_str()); error);
success = false; success = false;
break; break;
} }
@ -499,45 +484,13 @@ bool env_universal_t::write_to_fd(int fd, const wcstring &path) {
bool env_universal_t::move_new_vars_file_into_place(const wcstring &src, const wcstring &dst) { bool env_universal_t::move_new_vars_file_into_place(const wcstring &src, const wcstring &dst) {
int ret = wrename(src, dst); int ret = wrename(src, dst);
if (ret != 0) { if (ret != 0) {
int err = errno; const char *error = strerror(errno);
report_error(err, L"Unable to rename file from '%ls' to '%ls'", src.c_str(), dst.c_str()); debug(0, _(L"Unable to rename file from '%ls' to '%ls': %s"), src.c_str(), dst.c_str(),
error);
} }
return ret == 0; 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() { bool env_universal_t::load() {
scoped_lock locker(lock); scoped_lock locker(lock);
callback_data_list_t callbacks; callback_data_list_t callbacks;
@ -566,12 +519,11 @@ bool env_universal_t::open_temporary_file(const wcstring &directory, wcstring *o
// Create and open a temporary file for writing within the given directory. Try to create a // 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. // 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. // This should almost always succeed on the first try.
assert(!string_suffixes_string(L"/", directory)); assert(!string_suffixes_string(L"/", directory)); //!OCLINT(multiple unary operator)
bool success = false; bool success = false;
int saved_errno = 0; int saved_errno;
const wcstring tmp_name_template = directory + L"/fishd.tmp.XXXXXX"; const wcstring tmp_name_template = directory + L"/fishd.tmp.XXXXXX";
wcstring tmp_name;
for (size_t attempt = 0; attempt < 10 && !success; attempt++) { for (size_t attempt = 0; attempt < 10 && !success; attempt++) {
char *narrow_str = wcs2str(tmp_name_template.c_str()); char *narrow_str = wcs2str(tmp_name_template.c_str());
@ -592,7 +544,8 @@ bool env_universal_t::open_temporary_file(const wcstring &directory, wcstring *o
} }
if (!success) { if (!success) {
report_error(saved_errno, L"Unable to open temporary file '%ls'", out_path->c_str()); const char *error = strerror(saved_errno);
debug(0, _(L"Unable to open temporary file '%ls': %s"), out_path->c_str(), error);
} }
return success; return success;
} }
@ -614,13 +567,12 @@ bool env_universal_t::open_and_acquire_lock(const wcstring &path, int *out_fd) {
for (;;) { for (;;) {
int fd = wopen_cloexec(path, flags, 0644); int fd = wopen_cloexec(path, flags, 0644);
if (fd < 0) { if (fd < 0) {
int err = errno; if (errno == EINTR) {
if (err == EINTR) {
/* Signal; try again */ /* Signal; try again */
continue; continue;
} }
#ifdef O_EXLOCK #ifdef O_EXLOCK
else if (err == ENOTSUP || err == EOPNOTSUPP) { else if (errno == ENOTSUP || errno == EOPNOTSUPP) {
// Filesystem probably does not support locking. Clear the flag and try again. Note // 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 // 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. // symbols have the same value but on BSD they're different.
@ -630,7 +582,9 @@ bool env_universal_t::open_and_acquire_lock(const wcstring &path, int *out_fd) {
} }
#endif #endif
else { else {
report_error(err, L"Unable to open universal variable file '%ls'", path.c_str()); const char *error = strerror(errno);
debug(0, _(L"Unable to open universal variable file '%ls': %s"), path.c_str(),
error);
break; break;
} }
} }
@ -1013,8 +967,8 @@ class universal_notifier_shmem_poller_t : public universal_notifier_t {
bool errored = false; bool errored = false;
int fd = shm_open(path, O_RDWR | O_CREAT, 0600); int fd = shm_open(path, O_RDWR | O_CREAT, 0600);
if (fd < 0) { if (fd < 0) {
int err = errno; const char *error = strerror(errno);
report_error(err, L"Unable to open shared memory with path '%s'", path); debug(0, _(L"Unable to open shared memory with path '%s': %s"), path, error);
errored = true; errored = true;
} }
@ -1023,8 +977,9 @@ class universal_notifier_shmem_poller_t : public universal_notifier_t {
if (!errored) { if (!errored) {
struct stat buf = {}; struct stat buf = {};
if (fstat(fd, &buf) < 0) { if (fstat(fd, &buf) < 0) {
int err = errno; const char *error = strerror(errno);
report_error(err, L"Unable to fstat shared memory object with path '%s'", path); debug(0, _(L"Unable to fstat shared memory object with path '%s': %s"), path,
error);
errored = true; errored = true;
} }
size = buf.st_size; size = buf.st_size;
@ -1033,8 +988,8 @@ class universal_notifier_shmem_poller_t : public universal_notifier_t {
// Set the size, if it's too small. // Set the size, if it's too small.
bool set_size = !errored && size < (off_t)sizeof(universal_notifier_shmem_t); bool set_size = !errored && size < (off_t)sizeof(universal_notifier_shmem_t);
if (set_size && ftruncate(fd, sizeof(universal_notifier_shmem_t)) < 0) { if (set_size && ftruncate(fd, sizeof(universal_notifier_shmem_t)) < 0) {
int err = errno; const char *error = strerror(errno);
report_error(err, L"Unable to truncate shared memory object with path '%s'", path); debug(0, _(L"Unable to truncate shared memory object with path '%s': %s"), path, error);
errored = true; errored = true;
} }
@ -1043,9 +998,9 @@ class universal_notifier_shmem_poller_t : public universal_notifier_t {
void *addr = mmap(NULL, sizeof(universal_notifier_shmem_t), PROT_READ | PROT_WRITE, void *addr = mmap(NULL, sizeof(universal_notifier_shmem_t), PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0); MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) { if (addr == MAP_FAILED) {
int err = errno; const char *error = strerror(errno);
report_error(err, L"Unable to memory map shared memory object with path '%s'", debug(0, _(L"Unable to memory map shared memory object with path '%s': %s"), path,
path); error);
this->region = NULL; this->region = NULL;
} else { } else {
this->region = static_cast<universal_notifier_shmem_t *>(addr); this->region = static_cast<universal_notifier_shmem_t *>(addr);
@ -1242,13 +1197,14 @@ class universal_notifier_named_pipe_t : public universal_notifier_t {
if (fd < 0) { if (fd < 0) {
// Maybe open failed, maybe mkfifo failed. // Maybe open failed, maybe mkfifo failed.
int err = errno;
// We explicitly do NOT report an error for ENOENT or EACCESS. This works around #1955, // 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. // where $XDG_RUNTIME_DIR may get a bogus value under success.
if (err != ENOENT && err != EPERM) { if (errno != ENOENT && errno != EPERM) {
report_error( const char *error = strerror(errno);
err, L"Unable to make or open a FIFO for universal variables with path '%ls'", debug(
vars_path.c_str()); 0,
_(L"Unable to make or open a FIFO for universal variables with path '%ls': %s"),
vars_path.c_str(), error);
} }
pipe_fd = -1; pipe_fd = -1;
} else { } else {
@ -1424,8 +1380,8 @@ static universal_notifier_t::notifier_strategy_t fetch_default_strategy_from_env
} }
} }
if (i >= opt_count) { if (i >= opt_count) {
fprintf(stderr, "Warning: unrecognized value for %s: '%s'\n", fprintf(stderr, "Warning: unrecognized value for %s: '%s'\n", UNIVERSAL_NOTIFIER_ENV_NAME,
UNIVERSAL_NOTIFIER_ENV_NAME, var); var);
fprintf(stderr, "Warning: valid values are "); fprintf(stderr, "Warning: valid values are ");
for (size_t j = 0; j < opt_count; j++) { for (size_t j = 0; j < opt_count; j++) {
fprintf(stderr, "%s%s", j > 0 ? ", " : "", options[j].name); fprintf(stderr, "%s%s", j > 0 ? ", " : "", options[j].name);

View file

@ -1,5 +1,6 @@
#ifndef FISH_ENV_UNIVERSAL_COMMON_H #ifndef FISH_ENV_UNIVERSAL_COMMON_H
#define FISH_ENV_UNIVERSAL_COMMON_H #define FISH_ENV_UNIVERSAL_COMMON_H
#include "config.h"
#include <pthread.h> #include <pthread.h>
#include <stdio.h> #include <stdio.h>

View file

@ -2,8 +2,11 @@
#include "config.h" #include "config.h"
#include <errno.h> #include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> #include <unistd.h>
#include <wchar.h> #include <wchar.h>
#include <wctype.h>
#if HAVE_NCURSES_H #if HAVE_NCURSES_H
#include <ncurses.h> #include <ncurses.h>
#elif HAVE_NCURSES_CURSES_H #elif HAVE_NCURSES_CURSES_H
@ -16,8 +19,7 @@
#elif HAVE_NCURSES_TERM_H #elif HAVE_NCURSES_TERM_H
#include <ncurses/term.h> #include <ncurses/term.h>
#endif #endif
#include <stdlib.h>
#include <wctype.h>
#include <algorithm> #include <algorithm>
#include <memory> #include <memory>
#include <string> #include <string>
@ -372,6 +374,7 @@ int input_init() {
} else { } else {
debug(0, _(L"Using fallback terminal type '%ls'"), DEFAULT_TERM); debug(0, _(L"Using fallback terminal type '%ls'"), DEFAULT_TERM);
} }
putc('\n', stderr);
} }
input_terminfo_init(); input_terminfo_init();

View file

@ -5,9 +5,12 @@
#include <assert.h> #include <assert.h>
#include <errno.h> #include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <wchar.h> #include <wchar.h>
#include <string> #include <string>
#include <vector> #include <vector>
@ -213,78 +216,101 @@ wcstring path_apply_working_directory(const wcstring &path, const wcstring &work
return new_path; return new_path;
} }
static wcstring path_create_config() { /// We separate this from path_create() for two reasons. First it's only caused if there is a
bool done = false; /// problem, and thus is not central to the behavior of that function. Second, we only want to issue
wcstring res; /// the message once. If the current shell starts a new fish shell (e.g., by running `fish -c` from
/// a function) we don't want that subshell to issue the same warnings.
static void maybe_issue_path_warning(const wcstring &which_dir, const wcstring &custom_error_msg,
bool using_xdg, const wcstring &xdg_var, const wcstring &path,
int saved_errno) {
wcstring warning_var_name = L"_FISH_WARNED_" + which_dir;
if (env_exist(warning_var_name.c_str(), ENV_GLOBAL | ENV_EXPORT)) {
return;
}
env_set(warning_var_name, L"1", ENV_GLOBAL | ENV_EXPORT);
const env_var_t xdg_dir = env_get_string(L"XDG_CONFIG_HOME"); debug(0, custom_error_msg.c_str());
if (!xdg_dir.missing()) { if (path.empty()) {
res = xdg_dir + L"/fish"; debug(0, _(L"Unable to locate the %ls directory."), which_dir.c_str());
if (!create_directory(res)) { debug(0, _(L"Please set the %ls or HOME environment variable "
done = true; L"before starting fish."),
xdg_var.c_str());
} else {
const wchar_t *env_var = using_xdg ? xdg_var.c_str() : L"HOME";
debug(0, _(L"Unable to locate %ls directory derived from $%ls: '%ls'."), which_dir.c_str(),
env_var, path.c_str());
debug(0, _(L"The error was '%s'."), strerror(saved_errno));
debug(0, _(L"Please set $%ls to a directory where you have write access."), env_var);
}
putc('\n', stderr);
}
static void path_create(wcstring &path, const wcstring &xdg_var, const wcstring &which_dir,
const wcstring &custom_error_msg) {
bool path_done = false;
bool using_xdg = false;
int saved_errno = 0;
// The vars we fetch must be exported. Allowing them to be universal doesn't make sense and
// allowing that creates a lock inversion that deadlocks the shell since we're called before
// uvars are available.
const env_var_t xdg_dir = env_get_string(xdg_var, ENV_GLOBAL | ENV_EXPORT);
if (!xdg_dir.missing_or_empty()) {
using_xdg = true;
path = xdg_dir + L"/fish";
if (create_directory(path) != -1) {
path_done = true;
} else {
saved_errno = errno;
} }
} else { } else {
const env_var_t home = env_get_string(L"HOME"); const env_var_t home = env_get_string(L"HOME", ENV_GLOBAL | ENV_EXPORT);
if (!home.missing()) { if (!home.missing_or_empty()) {
res = home + L"/.config/fish"; path = home + L"/.config/fish";
if (!create_directory(res)) { if (create_directory(path) != -1) {
done = true; path_done = true;
}
}
}
if (!done) {
res.clear();
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."));
}
return res;
}
static wcstring path_create_data() {
bool done = false;
wcstring res;
const env_var_t xdg_dir = env_get_string(L"XDG_DATA_HOME");
if (!xdg_dir.missing()) {
res = xdg_dir + L"/fish";
if (!create_directory(res)) {
done = true;
}
} else { } else {
const env_var_t home = env_get_string(L"HOME"); saved_errno = errno;
if (!home.missing()) {
res = home + L"/.local/share/fish";
if (!create_directory(res)) {
done = true;
} }
} }
} }
if (!done) { if (!path_done) {
res.clear(); maybe_issue_path_warning(which_dir, custom_error_msg, using_xdg, xdg_var, path,
saved_errno);
debug(0, _(L"Unable to create a data directory for fish. Your history will not be saved. " path.clear();
L"Please set the $XDG_DATA_HOME variable to a directory where the current user "
L"has write access."));
} }
return res;
return;
} }
/// Cache the config path. /// Cache the config path.
bool path_get_config(wcstring &path) { bool path_get_config(wcstring &path) {
static const wcstring result = path_create_config(); static bool config_path_done = false;
path = result; static wcstring config_path(L"");
return !result.empty();
if (!config_path_done) {
path_create(config_path, L"XDG_CONFIG_HOME", L"config",
_(L"Your personal settings will not be saved."));
config_path_done = true;
}
path = config_path;
return !config_path.empty();
} }
/// Cache the data path. /// Cache the data path.
bool path_get_data(wcstring &path) { bool path_get_data(wcstring &path) {
static const wcstring result = path_create_data(); static bool data_path_done = false;
path = result; static wcstring data_path(L"");
return !result.empty();
if (!data_path_done) {
data_path_done = true;
path_create(data_path, L"XDG_DATA_HOME", L"data", _(L"Your history will not be saved."));
}
path = data_path;
return !data_path.empty();
} }
void path_make_canonical(wcstring &path) { void path_make_canonical(wcstring &path) {