From 89f2bae00c1b87580f432f9a719ba8998493e6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awek=20Rudnicki?= Date: Mon, 7 Aug 2017 12:41:33 +0200 Subject: [PATCH] Allow inode cache invalidation in high-level API We re-introduce the functionality of invalidating the caches for an inode specified by path by adding a new routine fuse_invalidate_path. This is useful for network-based file systems which use the high-level API, enabling them to notify the kernel about external changes. This is a revival of Miklos Szeredi's original code for the fuse_invalidate routine. --- ChangeLog.rst | 3 + example/.gitignore | 1 + example/Makefile.am | 4 +- example/meson.build | 1 + example/notify_inval_inode_fh.c | 292 ++++++++++++++++++++++++++++++++ include/fuse.h | 13 ++ lib/fuse.c | 40 +++++ lib/fuse_versionscript | 1 + test/test_ctests.py | 28 ++- 9 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 example/notify_inval_inode_fh.c diff --git a/ChangeLog.rst b/ChangeLog.rst index c798918..1ca0980 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,6 +1,9 @@ Unreleased Changes ================== +* Added new `fuse_invalidate_path()` routine for cache invalidation + from the high-level FUSE API, along with an example and tests. + * There's a new `printcap` example that can be used to determine the capabilities of the running kernel. diff --git a/example/.gitignore b/example/.gitignore index c5d2cdb..d59f16e 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -11,5 +11,6 @@ /cuse_client /passthrough_ll /notify_inval_inode +/notify_inval_inode_fh /notify_store_retrieve /notify_inval_entry diff --git a/example/Makefile.am b/example/Makefile.am index db87ade..e9a315e 100644 --- a/example/Makefile.am +++ b/example/Makefile.am @@ -3,8 +3,8 @@ AM_CPPFLAGS = -I$(top_srcdir)/include -D_REENTRANT noinst_HEADERS = ioctl.h noinst_PROGRAMS = passthrough passthrough_fh null hello hello_ll \ - ioctl ioctl_client poll poll_client \ - passthrough_ll notify_inval_inode \ + ioctl ioctl_client poll poll_client passthrough_ll \ + notify_inval_inode notify_inval_inode_fh \ notify_store_retrieve notify_inval_entry \ cuse cuse_client printcap diff --git a/example/meson.build b/example/meson.build index 13dae10..406c4c4 100644 --- a/example/meson.build +++ b/example/meson.build @@ -11,6 +11,7 @@ if not platform.endswith('bsd') endif threaded_examples = [ 'notify_inval_inode', + 'notify_inval_inode_fh', 'notify_store_retrieve', 'notify_inval_entry', 'poll' ] diff --git a/example/notify_inval_inode_fh.c b/example/notify_inval_inode_fh.c new file mode 100644 index 0000000..f37fcef --- /dev/null +++ b/example/notify_inval_inode_fh.c @@ -0,0 +1,292 @@ +/* + FUSE: Filesystem in Userspace + Copyright (C) 2016 Nikolaus Rath + (C) 2017 EditShare LLC + + This program can be distributed under the terms of the GNU GPL. + See the file COPYING. + */ + +/** @file + * + * This example implements a file system with two files: + * * 'current-time', whose contents change dynamically: + * it always contains the current time (same as in + * notify_inval_inode.c). + * * 'growing', whose size changes dynamically, growing + * by 1 byte after each update. This aims to check + * if cached file metadata is also invalidated. + * + * ## Compilation ## + * + * gcc -Wall notify_inval_inode_fh.c `pkg-config fuse3 --cflags --libs` -o notify_inval_inode_fh + * + * ## Source code ## + * \include notify_inval_inode_fh.c + */ + +#define FUSE_USE_VERSION 31 + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include /* for fuse_cmdline_opts */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* We can't actually tell the kernel that there is no + timeout, so we just send a big value */ +#define NO_TIMEOUT 500000 + +#define MAX_STR_LEN 128 +#define TIME_FILE_NAME "current_time" +#define TIME_FILE_INO 2 +#define GROW_FILE_NAME "growing" +#define GROW_FILE_INO 3 + +static char time_file_contents[MAX_STR_LEN]; +static size_t grow_file_size; + +/* Command line parsing */ +struct options { + int no_notify; + int update_interval; +}; +static struct options options = { + .no_notify = 0, + .update_interval = 1, +}; + +#define OPTION(t, p) { t, offsetof(struct options, p), 1 } +static const struct fuse_opt option_spec[] = { + OPTION("--no-notify", no_notify), + OPTION("--update-interval=%d", update_interval), + FUSE_OPT_END +}; + +static void *xmp_init(struct fuse_conn_info *conn, struct fuse_config *cfg) +{ + (void) conn; + cfg->entry_timeout = NO_TIMEOUT; + cfg->attr_timeout = NO_TIMEOUT; + cfg->negative_timeout = 0; + + return NULL; +} + +static int xmp_getattr(const char *path, + struct stat *stbuf, struct fuse_file_info* fi) { + (void) fi; + if (strcmp(path, "/") == 0) { + stbuf->st_ino = 1; + stbuf->st_mode = S_IFDIR | 0755; + stbuf->st_nlink = 1; + } else if (strcmp(path, "/" TIME_FILE_NAME) == 0) { + stbuf->st_ino = TIME_FILE_INO; + stbuf->st_mode = S_IFREG | 0444; + stbuf->st_nlink = 1; + stbuf->st_size = strlen(time_file_contents); + } else if (strcmp(path, "/" GROW_FILE_NAME) == 0) { + stbuf->st_ino = GROW_FILE_INO; + stbuf->st_mode = S_IFREG | 0444; + stbuf->st_nlink = 1; + stbuf->st_size = grow_file_size; + } else { + return -ENOENT; + } + + return 0; +} + +static int xmp_readdir(const char *path, void *buf, fuse_fill_dir_t filler, + off_t offset, struct fuse_file_info *fi, + enum fuse_readdir_flags flags) { + (void) fi; + (void) offset; + (void) flags; + if (strcmp(path, "/") != 0) { + return -ENOTDIR; + } else { + (void) filler; + (void) buf; + struct stat file_stat; + xmp_getattr("/" TIME_FILE_NAME, &file_stat, NULL); + filler(buf, TIME_FILE_NAME, &file_stat, 0, 0); + xmp_getattr("/" GROW_FILE_NAME, &file_stat, NULL); + filler(buf, GROW_FILE_NAME, &file_stat, 0, 0); + return 0; + } +} + +static int xmp_open(const char *path, struct fuse_file_info *fi) { + (void) path; + /* Make cache persistent even if file is closed, + this makes it easier to see the effects */ + fi->keep_cache = 1; + return 0; +} + +static int xmp_read(const char *path, char *buf, size_t size, off_t offset, + struct fuse_file_info *fi) { + (void) fi; + (void) offset; + if (strcmp(path, "/" TIME_FILE_NAME) == 0) { + int file_length = strlen(time_file_contents); + int to_copy = offset + size <= file_length + ? size + : file_length - offset; + memcpy(buf, time_file_contents, to_copy); + return to_copy; + } else { + assert(strcmp(path, "/" GROW_FILE_NAME) == 0); + int to_copy = offset + size <= grow_file_size + ? size + : grow_file_size - offset; + memset(buf, 'x', to_copy); + return to_copy; + } +} + +static struct fuse_operations xmp_oper = { + .init = xmp_init, + .getattr = xmp_getattr, + .readdir = xmp_readdir, + .open = xmp_open, + .read = xmp_read, +}; + +static void update_fs(void) { + static int count = 0; + struct tm *now; + time_t t; + t = time(NULL); + now = localtime(&t); + assert(now != NULL); + + int time_file_size = strftime(time_file_contents, MAX_STR_LEN, + "The current time is %H:%M:%S\n", now); + assert(time_file_size != 0); + + grow_file_size = count++; +} + +static int invalidate(struct fuse *fuse, const char *path) { + int status = fuse_invalidate_path(fuse, path); + if (status == -ENOENT) { + return 0; + } else { + return status; + } +} + +static void* update_fs_loop(void *data) { + struct fuse *fuse = (struct fuse*) data; + + while (1) { + update_fs(); + if (!options.no_notify) { + assert(invalidate(fuse, "/" TIME_FILE_NAME) == 0); + assert(invalidate(fuse, "/" GROW_FILE_NAME) == 0); + } + sleep(options.update_interval); + } + return NULL; +} + +static void show_help(const char *progname) +{ + printf("usage: %s [options] \n\n", progname); + printf("File-system specific options:\n" + " --update-interval= Update-rate of file system contents\n" + " --no-notify Disable kernel notifications\n" + "\n"); +} + +int main(int argc, char *argv[]) { + struct fuse_args args = FUSE_ARGS_INIT(argc, argv); + struct fuse *fuse; + struct fuse_cmdline_opts opts; + int res; + + /* Initialize the files */ + update_fs(); + + if (fuse_opt_parse(&args, &options, option_spec, NULL) == -1) + return 1; + + if (fuse_parse_cmdline(&args, &opts) != 0) + return 1; + + if (opts.show_version) { + printf("FUSE library version %s\n", fuse_pkgversion()); + fuse_lowlevel_version(); + res = 0; + goto out1; + } else if (opts.show_help) { + show_help(argv[0]); + fuse_cmdline_help(); + fuse_lib_help(&args); + res = 0; + goto out1; + } else if (!opts.mountpoint) { + fprintf(stderr, "error: no mountpoint specified\n"); + res = 1; + goto out1; + } + + fuse = fuse_new(&args, &xmp_oper, sizeof(xmp_oper), NULL); + if (fuse == NULL) { + res = 1; + goto out1; + } + + if (fuse_mount(fuse,opts.mountpoint) != 0) { + res = 1; + goto out2; + } + + if (fuse_daemonize(opts.foreground) != 0) { + res = 1; + goto out3; + } + + pthread_t updater; /* Start thread to update file contents */ + int ret = pthread_create(&updater, NULL, update_fs_loop, (void *) fuse); + if (ret != 0) { + fprintf(stderr, "pthread_create failed with %s\n", strerror(ret)); + return 1; + }; + + struct fuse_session *se = fuse_get_session(fuse); + if (fuse_set_signal_handlers(se) != 0) { + res = 1; + goto out3; + } + + if (opts.singlethread) + res = fuse_loop(fuse); + else + res = fuse_loop_mt(fuse, opts.clone_fd); + if (res) + res = 1; + + fuse_remove_signal_handlers(se); +out3: + fuse_unmount(fuse); +out2: + fuse_destroy(fuse); +out1: + free(opts.mountpoint); + fuse_opt_free_args(&args); + return res; +} diff --git a/include/fuse.h b/include/fuse.h index 5eb257c..4898029 100644 --- a/include/fuse.h +++ b/include/fuse.h @@ -1009,6 +1009,19 @@ int fuse_getgroups(int size, gid_t list[]); */ int fuse_interrupted(void); +/** + * Invalidates cache for the given path. + * + * This calls fuse_lowlevel_notify_inval_inode internally. + * + * @return 0 on successful invalidation, negative error value otherwise. + * This routine may return -ENOENT to indicate that there was + * no entry to be invalidated, e.g., because the path has not + * been seen before or has been forgotten; this should not be + * considered to be an error. + */ +int fuse_invalidate_path(struct fuse *f, const char *path); + /** * The real main function * diff --git a/lib/fuse.c b/lib/fuse.c index a5df0b8..0f2a6d6 100644 --- a/lib/fuse.c +++ b/lib/fuse.c @@ -893,6 +893,36 @@ out_err: return node; } +static int lookup_path_in_cache(struct fuse *f, + const char *path, fuse_ino_t *inop) +{ + char *tmp = strdup(path); + if (!tmp) + return -ENOMEM; + + pthread_mutex_lock(&f->lock); + fuse_ino_t ino = FUSE_ROOT_ID; + + int err = 0; + char *save_ptr; + char *path_element = strtok_r(tmp, "/", &save_ptr); + while (path_element != NULL) { + struct node *node = lookup_node(f, ino, path_element); + if (node == NULL) { + err = -ENOENT; + break; + } + ino = node->nodeid; + path_element = strtok_r(NULL, "/", &save_ptr); + } + pthread_mutex_unlock(&f->lock); + free(tmp); + + if (!err) + *inop = ino; + return err; +} + static char *add_name(char **buf, unsigned *bufsize, char *s, const char *name) { size_t len = strlen(name); @@ -4400,6 +4430,16 @@ int fuse_interrupted(void) return 0; } +int fuse_invalidate_path(struct fuse *f, const char *path) { + fuse_ino_t ino; + int err = lookup_path_in_cache(f, path, &ino); + if (err) { + return err; + } + + return fuse_lowlevel_notify_inval_inode(f->se, ino, 0, 0); +} + #define FUSE_LIB_OPT(t, p, v) { t, offsetof(struct fuse_config, p), v } static const struct fuse_opt fuse_lib_opts[] = { diff --git a/lib/fuse_versionscript b/lib/fuse_versionscript index 5fa3264..e1eba6b 100644 --- a/lib/fuse_versionscript +++ b/lib/fuse_versionscript @@ -136,6 +136,7 @@ FUSE_3.1 { global: fuse_lib_help; fuse_new_30; + fuse_invalidate_path; } FUSE_3.0; # Local Variables: diff --git a/test/test_ctests.py b/test/test_ctests.py index 4ad03fb..2da55e5 100644 --- a/test/test_ctests.py +++ b/test/test_ctests.py @@ -13,6 +13,7 @@ from distutils.version import LooseVersion from util import (wait_for_mount, umount, cleanup, base_cmdline, safe_sleep, basename, fuse_test_marker) from os.path import join as pjoin +import os.path pytestmark = fuse_test_marker() @@ -34,7 +35,7 @@ def test_write_cache(tmpdir, writeback): subprocess.check_call(cmdline) -names = [ 'notify_inval_inode' ] +names = [ 'notify_inval_inode', 'notify_inval_inode_fh' ] if sys.platform == 'linux': names.append('notify_store_retrieve') @pytest.mark.parametrize("name", names) @@ -65,4 +66,27 @@ def test_notify1(tmpdir, name, notify): else: umount(mount_process, mnt_dir) - +@pytest.mark.parametrize("notify", (True, False)) +def test_notify_file_size(tmpdir, notify): + mnt_dir = str(tmpdir) + cmdline = base_cmdline + \ + [ pjoin(basename, 'example', 'notify_inval_inode_fh'), + '-f', '--update-interval=1', mnt_dir ] + if not notify: + cmdline.append('--no-notify') + mount_process = subprocess.Popen(cmdline) + try: + wait_for_mount(mount_process, mnt_dir) + filename = pjoin(mnt_dir, 'growing') + size = os.path.getsize(filename) + safe_sleep(2) + new_size = os.path.getsize(filename) + if notify: + assert new_size > size + else: + assert new_size == size + except: + cleanup(mnt_dir) + raise + else: + umount(mount_process, mnt_dir)