mirror of
https://github.com/fish-shell/fish-shell
synced 2024-12-24 11:53:09 +00:00
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:
parent
422ff0f173
commit
eaa3741336
5 changed files with 133 additions and 142 deletions
19
src/env.cpp
19
src/env.cpp
|
@ -334,7 +334,7 @@ static bool variable_is_colon_delimited_array(const wcstring &str) {
|
|||
}
|
||||
|
||||
/// 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) {
|
||||
const struct passwd *pw = getpwuid(getuid());
|
||||
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
|
||||
setup_path();
|
||||
setup_user();
|
||||
setup_user(false);
|
||||
|
||||
// Set up the version variable.
|
||||
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");
|
||||
char *unam_narrow = wcs2str(unam.c_str());
|
||||
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);
|
||||
const env_var_t unam = env_get_string(L"USER");
|
||||
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().
|
||||
if (is_electric(key)) {
|
||||
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
|
||||
// ask for an environment variable, so don't take the lock here (we don't need it).
|
||||
if (key == L"history" && is_main_thread()) {
|
||||
if (key == L"history") {
|
||||
// Big hack. We only allow getting the history on the main thread. Note that history_t
|
||||
// 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;
|
||||
|
||||
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") {
|
||||
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) {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <pwd.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
|
@ -34,6 +33,7 @@
|
|||
#include "env.h"
|
||||
#include "env_universal_common.h"
|
||||
#include "fallback.h" // IWYU pragma: keep
|
||||
#include "path.h"
|
||||
#include "utf8.h"
|
||||
#include "util.h"
|
||||
#include "wutil.h"
|
||||
|
@ -70,7 +70,6 @@
|
|||
"# 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);
|
||||
|
||||
|
@ -83,12 +82,10 @@ static wcstring vars_filename_in_directory(const wcstring &wdir) {
|
|||
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;
|
||||
static const wcstring default_vars_path() {
|
||||
wcstring path;
|
||||
path_get_config(path);
|
||||
return vars_filename_in_directory(path);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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.
|
||||
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());
|
||||
const char *error = strerror(errno);
|
||||
debug(0, _(L"Unable to write to universal variables file '%ls': %s"), path.c_str(),
|
||||
error);
|
||||
success = false;
|
||||
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) {
|
||||
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());
|
||||
const char *error = strerror(errno);
|
||||
debug(0, _(L"Unable to rename file from '%ls' to '%ls': %s"), src.c_str(), dst.c_str(),
|
||||
error);
|
||||
}
|
||||
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;
|
||||
|
@ -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
|
||||
// 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));
|
||||
assert(!string_suffixes_string(L"/", directory)); //!OCLINT(multiple unary operator)
|
||||
|
||||
bool success = false;
|
||||
int saved_errno = 0;
|
||||
int saved_errno;
|
||||
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());
|
||||
|
@ -592,7 +544,8 @@ bool env_universal_t::open_temporary_file(const wcstring &directory, wcstring *o
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -614,13 +567,12 @@ bool env_universal_t::open_and_acquire_lock(const wcstring &path, int *out_fd) {
|
|||
for (;;) {
|
||||
int fd = wopen_cloexec(path, flags, 0644);
|
||||
if (fd < 0) {
|
||||
int err = errno;
|
||||
if (err == EINTR) {
|
||||
if (errno == EINTR) {
|
||||
/* Signal; try again */
|
||||
continue;
|
||||
}
|
||||
#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
|
||||
// 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.
|
||||
|
@ -630,7 +582,9 @@ bool env_universal_t::open_and_acquire_lock(const wcstring &path, int *out_fd) {
|
|||
}
|
||||
#endif
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1013,8 +967,8 @@ class universal_notifier_shmem_poller_t : public universal_notifier_t {
|
|||
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);
|
||||
const char *error = strerror(errno);
|
||||
debug(0, _(L"Unable to open shared memory with path '%s': %s"), path, error);
|
||||
errored = true;
|
||||
}
|
||||
|
||||
|
@ -1023,8 +977,9 @@ class universal_notifier_shmem_poller_t : public universal_notifier_t {
|
|||
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);
|
||||
const char *error = strerror(errno);
|
||||
debug(0, _(L"Unable to fstat shared memory object with path '%s': %s"), path,
|
||||
error);
|
||||
errored = true;
|
||||
}
|
||||
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.
|
||||
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);
|
||||
const char *error = strerror(errno);
|
||||
debug(0, _(L"Unable to truncate shared memory object with path '%s': %s"), path, error);
|
||||
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,
|
||||
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);
|
||||
const char *error = strerror(errno);
|
||||
debug(0, _(L"Unable to memory map shared memory object with path '%s': %s"), path,
|
||||
error);
|
||||
this->region = NULL;
|
||||
} else {
|
||||
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) {
|
||||
// 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());
|
||||
if (errno != ENOENT && errno != EPERM) {
|
||||
const char *error = strerror(errno);
|
||||
debug(
|
||||
0,
|
||||
_(L"Unable to make or open a FIFO for universal variables with path '%ls': %s"),
|
||||
vars_path.c_str(), error);
|
||||
}
|
||||
pipe_fd = -1;
|
||||
} else {
|
||||
|
@ -1424,8 +1380,8 @@ static universal_notifier_t::notifier_strategy_t fetch_default_strategy_from_env
|
|||
}
|
||||
}
|
||||
if (i >= opt_count) {
|
||||
fprintf(stderr, "Warning: unrecognized value for %s: '%s'\n",
|
||||
UNIVERSAL_NOTIFIER_ENV_NAME, var);
|
||||
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);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#ifndef FISH_ENV_UNIVERSAL_COMMON_H
|
||||
#define FISH_ENV_UNIVERSAL_COMMON_H
|
||||
#include "config.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
#include "config.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <wchar.h>
|
||||
#include <wctype.h>
|
||||
#if HAVE_NCURSES_H
|
||||
#include <ncurses.h>
|
||||
#elif HAVE_NCURSES_CURSES_H
|
||||
|
@ -16,8 +19,7 @@
|
|||
#elif HAVE_NCURSES_TERM_H
|
||||
#include <ncurses/term.h>
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <wctype.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
@ -372,6 +374,7 @@ int input_init() {
|
|||
} else {
|
||||
debug(0, _(L"Using fallback terminal type '%ls'"), DEFAULT_TERM);
|
||||
}
|
||||
putc('\n', stderr);
|
||||
}
|
||||
|
||||
input_terminfo_init();
|
||||
|
|
128
src/path.cpp
128
src/path.cpp
|
@ -5,9 +5,12 @@
|
|||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <wchar.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
@ -213,78 +216,101 @@ wcstring path_apply_working_directory(const wcstring &path, const wcstring &work
|
|||
return new_path;
|
||||
}
|
||||
|
||||
static wcstring path_create_config() {
|
||||
bool done = false;
|
||||
wcstring res;
|
||||
|
||||
const env_var_t xdg_dir = env_get_string(L"XDG_CONFIG_HOME");
|
||||
if (!xdg_dir.missing()) {
|
||||
res = xdg_dir + L"/fish";
|
||||
if (!create_directory(res)) {
|
||||
done = true;
|
||||
/// We separate this from path_create() for two reasons. First it's only caused if there is a
|
||||
/// problem, and thus is not central to the behavior of that function. Second, we only want to issue
|
||||
/// 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);
|
||||
|
||||
debug(0, custom_error_msg.c_str());
|
||||
if (path.empty()) {
|
||||
debug(0, _(L"Unable to locate the %ls directory."), which_dir.c_str());
|
||||
debug(0, _(L"Please set the %ls or HOME environment variable "
|
||||
L"before starting fish."),
|
||||
xdg_var.c_str());
|
||||
} else {
|
||||
const env_var_t home = env_get_string(L"HOME");
|
||||
if (!home.missing()) {
|
||||
res = home + L"/.config/fish";
|
||||
if (!create_directory(res)) {
|
||||
done = true;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
putc('\n', stderr);
|
||||
}
|
||||
|
||||
static wcstring path_create_data() {
|
||||
bool done = false;
|
||||
wcstring res;
|
||||
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;
|
||||
|
||||
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;
|
||||
// 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 {
|
||||
const env_var_t home = env_get_string(L"HOME");
|
||||
if (!home.missing()) {
|
||||
res = home + L"/.local/share/fish";
|
||||
if (!create_directory(res)) {
|
||||
done = true;
|
||||
const env_var_t home = env_get_string(L"HOME", ENV_GLOBAL | ENV_EXPORT);
|
||||
if (!home.missing_or_empty()) {
|
||||
path = home + L"/.config/fish";
|
||||
if (create_directory(path) != -1) {
|
||||
path_done = true;
|
||||
} else {
|
||||
saved_errno = errno;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!done) {
|
||||
res.clear();
|
||||
|
||||
debug(0, _(L"Unable to create a data directory for fish. Your history will not be saved. "
|
||||
L"Please set the $XDG_DATA_HOME variable to a directory where the current user "
|
||||
L"has write access."));
|
||||
if (!path_done) {
|
||||
maybe_issue_path_warning(which_dir, custom_error_msg, using_xdg, xdg_var, path,
|
||||
saved_errno);
|
||||
path.clear();
|
||||
}
|
||||
return res;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/// Cache the config path.
|
||||
bool path_get_config(wcstring &path) {
|
||||
static const wcstring result = path_create_config();
|
||||
path = result;
|
||||
return !result.empty();
|
||||
static bool config_path_done = false;
|
||||
static wcstring config_path(L"");
|
||||
|
||||
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.
|
||||
bool path_get_data(wcstring &path) {
|
||||
static const wcstring result = path_create_data();
|
||||
path = result;
|
||||
return !result.empty();
|
||||
static bool data_path_done = false;
|
||||
static wcstring data_path(L"");
|
||||
|
||||
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) {
|
||||
|
|
Loading…
Reference in a new issue