Note: Be sure to read the Gotchas list before starting your code, and refer back to it frequently when you run into troubles.
passthrough.c or
passthrough_fh.c
(the latter implements file handles, so it's
a better choice if you're developing a complex filesystem, but the
file handles definitely make the code harder to understand). To
compile either of those you'll also need passthrough_helpers.h. The
existing clients provide a scaffolding for you to work from, but
you'll still need to understand what all
the functions are supposed to do, and also how to compile and run your
client. That's what this Web page is for.
Many of the FUSE functions are closely related to Unix system calls.
Rather than repeating the full specification (especially error
conditions) here, it's better for you to refer to the Unix man page.
You can do this on any Unix machine with the "man" command. By
convention, if I refer you to the "stat(2) system call",
that means
you should type "man 2 stat" to get the necessary information.
Many FUSE functions offer two ways to identify the file being operated
upon. The first, which is always available, is the
"path" argument, which is the full pathname (relative to
the filesystem root) of the file in question. If you choose to do so,
all your functions can use that argument to locate a file.
However, pathname lookup is often a very expensive operation, so FUSE
sometimes provides you another option: a "file handle" in the
"fuse_file_info" structure. The file handle is stored in
that structure's "fh" element, which is an unsigned
64-bit integer (uint64_t) uninterpreted by FUSE. If you
choose to use it, you should set that field in your
open and
opendir functions; other
functions can then use it. In many FUSE implementations, the file
handle is actually a pointer to a useful data structure, which is
typecast to an integer only to keep the compiler happy. But you can make
it an index into an array, a hash key, or pretty much anything else
you choose.
For many operations, it is useful to have a relevant "context" in
which to execute them. For historical reasons, the context isn't
passed as an argument; instead you must call
fuse_get_context with no argument, which returns a
pointer to a struct fuse_context with the following
usable elements:
uid
gid
pid
private_data
void*) to the private data
returned by the init function.
umask
umask of the process invoking the
operation. See umask(2).
The following is a brief description of all the API functions you can
create in a FUSE filesystem. Note that many are not required,
especially if you are implementing a partial filesystem like the ones
in these assignments; especially important functions are highlighted
in yellow. However, I have tried to provide
documentation for all the functions here. Unless otherwise specified,
all functions return
an nonnegative integer for
success, or a negative value selected from
/usr/include/asm-generic/errno*.h
if
there is an error.
All of your functions should be named with a prefix that is closely
related to your filesystem name. For example, in an SSH filesystem
you should use ssh_getattr, ssh_read, etc.
void* init(struct
fuse_conn_info *conn, struct fuse_config *cfg)
fuse_conn_info structure gives information
about what features are supported by FUSE, and can also be used
to request certain capabilities.
The fuse_config
structure describes further options; the reason for having
two different option arguments is historical. For full
documentation, refer to the commentary in
/usr/include/fuse3/fuse_common.h (for
fuse_conn_info) and
/usr/include/fuse3/fuse.h (for
fuse_config).
The return value from init will be made
available to all file
operations in the private_data field of
fuse_context. It
is also passed as a parameter to the destroy() method.
(Note: see the warning under Other
Options below, regarding relative pathnames.)
void destroy(void*
private_data)
private_data comes from the return value of
init. This function should free any data
that init allocated with malloc,
and should make sure everything related to the filesystem
has been flushed to persistent
storage.
getattr(const char *path,
struct stat *stbuf, struct fuse_file_info *fi)
stat(2) manual
page. For the
given pathname, this should fill in the elements of the
"stat" structure. If a field is meaningless or
semi-meaningless (e.g., st_rdev) then it
should be set to 0 or given a "reasonable" value. This
call is pretty much required for a usable filesystem.
NOTE: st_dev and
st_ino are often used by application programs
to decide whether two file names are aliases for the same
physical storage, so setting them to 0 isn't ideal.
fgetattr(const char *path, struct
stat *stbuf, struct fuse_file_info *fi)
getattr, but called when
fgetattr(2) is
invoked by the user program. Usually it can just call
getattr.
readlink(const char *path, char
*buf, size_t size)
path is a symbolic link, fill
buf with its target, up to
size. See readlink(2) for how to handle a
too-small buffer and for error codes. Not required if you
don't support symbolic links.
NOTE: Symbolic-link support requires only
readlink and symlink. FUSE
itself will take care of tracking symbolic links in paths,
so your path-evaluation code doesn't need to worry about it.
opendir(const char *path, struct
fuse_file_info *fi)
default_permissions option is enabled. If
opendir sets a file handle in
fi->fh, it will be passed to
readdir, releasedir, and
fsyncdir.
readdir(const char *path,
void *buf, fuse_fill_dir_t filler, off_t offset, struct
fuse_file_info *fi, enum fuse_readdir_flags flags)
struct
dirent) to the caller. This is one of the most
complex FUSE functions. It is related to, but not
identical to, the readdir(2) and
getdents(2) system calls,
and the readdir(3) library function. Because
of its
complexity, it is described separately in "Readdir Function" below.
Required for essentially any
filesystem, since it's what makes
ls and a whole bunch of other things work.
mknod(const char *path, mode_t
mode, dev_t rdev)
mknod(2) for most details.
This function is rarely needed,
since it's uncommon to make these objects inside
special-purpose filesystems.
create(const char *path, mode_t
mode, struct fuse_file_info *info)
mknod. I implemented it in
two lines by passing the call off to mknod and
adding S_IFREG to the mode, and
then calling open. But if you don't
implement it, Fuse will do the same.
mkdir(const char *path,
mode_t mode)
mode. See
mkdir(2) for details. This function is needed for any
reasonable read/write filesystem. Note: the
mode argument might or might not contain type
bits; use mode | S_IFDIR if you need those
bits to be correct.
unlink(const
char *path)
unlink only deletes the data when the
last hard link is removed. See unlink(2) for
details. This function is needed for almost any
read/write filesystem.
rmdir(const
char *path)
rmdir(2) for details.
symlink(const char *to, const
char *from)
from" which,
when evaluated, will lead to "to". Not
required if you don't support symbolic links.
NOTE: Symbolic-link support requires only
readlink and symlink. FUSE
itself will take care of tracking symbolic links in paths,
so your path-evaluation code doesn't need to worry about it.
rename(const char *from, const
char *to, unsigned int flags)
from" to the target "to". Note
that the source and target don't have to be in the same
directory, so it may be necessary to move the source to an
entirely new directory. If the target already exists and
flags is RENAME_NOREPLACE, the
rename should return an error (presumably
EEXIST). If flags is
RENAME_EXCHANGE, both files must exist and
they must be atomically swapped. Otherwise, if the target
already exists it must be atomically replaced; many
application programs depend on this behavior.
See rename(2) for full details.
link(const char *from, const char
*to)
from" and
"to". Hard links aren't required for a
working filesystem, and many successful filesystems don't
support them. If you do implement hard links, be
aware that they have an effect on how unlink works. See
link(2) for details.
chmod(const char *path, mode_t
mode, struct fuse_file_info *fi)
mode should be examined. See chmod(2) for
details. If the file is not currently open,
fi will be NULL; if it
is open fi may still be
NULL.
chown(const char *path, uid_t uid,
gid_t gid, struct fuse_file_info *fi)
chown(2) for details. As with
chmod (q.v.) fi might be
NULL. If the capability
FUSE_CAP_HANDLE_KILLPRIV is not
enabled, this function must clear the setuid and setgid
bits.
NOTE: if the FUSE program is passing calls through to an underlying filesystem (as opposed to a backing device), FUSE doesn't deal particularly well with file ownership, since it usually runs as an unprivileged user and this call is restricted to the superuser. It's often easier to pretend that all files are owned by the user who mounted the filesystem, and to skip implementing this function.
truncate(const
char *path, off_t size, struct fuse_file_info *fi)
size bytes long. See truncate(2) for
details. This call is required for read/write
filesystems, because recreating a file will first truncate
it. If the file is not currently open, fi
will be NULL; if it is open fi
might still be NULL. If the capability
FUSE_CAP_HANDLE_KILLPRIV is not
enabled, this function must clear the setuid and setgid
bits.
utimens(const char *path, const
struct timespec ts[2], struct fuse_file_info *fi)
utimensat(2) for full details. Note that the time
specifications are allowed to have certain special values;
however, I don't know if FUSE functions have to support
them. This function isn't necessary but is nice to have
in a fully functional filesystem.
open(const char *path,
struct fuse_file_info *fi)
fi->flags.
If you aren't using file handles, this
function should just check for existence and permissions,
perform truncation (see below),
and return either success or an error code. If you use
file handles, you should also allocate any necessary
structures and set fi->fh. In addition,
fi has some other fields that an advanced
filesystem might find useful; see the structure definition in
fuse_common.h for very brief commentary.
Some important rules about open:
O_TRUNC flag is set in
fi->flags, the file should be
truncated to a length of zero as part of opening
it. Strictly speaking, O_TRUNC
should only be honored when the open mode is
either O_RDWR or
O_WRONLY, but the kernel will never
generate those combinations.
O_CREAT,
O_EXCL, and O_NOCTTY)
flags will be filtered out because they are
handled by the kernel.
O_RDONLY, O_WRONLY,
O_RDWR, O_EXEC,
O_SEARCH) to check permissions. If
the filesystem was run with the
-o default_permissions option, the
kernel will do the check for you and you can
safely skip it.
O_WRONLY. The
filesystem should be prepared to handle this.
O_APPEND flag and ensure
that each write is appending to the end of the file.
O_APPEND. However, unless all
changes to the file come through the kernel this
will not work reliably. The filesystem should
thus either ignore the O_APPEND flag
(and let the kernel handle it), or return an error
indicating that reliable O_APPEND is
not available).
fi->fh and use it in all other
file operations (read, write, flush, release,
fsync). Alternatively, the filesystem may
implement stateless file I/O and not store
anything in fi->fh.
fi:
direct_io indicates that
direct I/O (see open(2))
should be used on this file.
keep_cache should be set to
1 if it's OK for the kernel to keep cached
data even after the file is closed. If
it's 0, then the kernel will invalidate its
internal cache upon close.
nonseekable can be to 1 to
indicate that lseek is not
supported on this file.
no_flush indicates that it
is not necessary to flush dirty cache
contents when the file is closed.
If you do not wish to give any special handling to
open (e.g., if you are writing a stateless
filesystem) then you should return ENOSYS and
also make sure that init sets
FUSE_CAP_NO_OPEN_SUPPORT
in fuse_conn_info. In this case the "error"
of ENOSYS will be treated as success.
See the comments in
/usr/include/fuse3/fuse_common.h for more information.
read(const char *path,
char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)
size bytes from the given file into the
buffer buf, beginning offset
bytes into the file. See read(2) for full details.
Returns the number of bytes transferred, or 0 if
offset was at or beyond the end of the file.
Except when EOF or an error is encountered, or when the
direct_io option is enabled, read
must transfer
exactly the number of bytes requested.
Required for nearly any sensible filesystem.
write(const char *path,
const char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)
read above,
except that it can't return 0.
statfs(const char *path, struct
statvfs *stbuf
statvfs(2)
for a description of the structure contents. Usually, you
can ignore the path. Not required, but handy
for read/write filesystems since this is how programs like
df determine the free space. FUSE ignores the
f_favail, f_fsid, and
f_flag fields.
release(const char *path, struct
fuse_file_info *fi)
close(2)
is related. Release is called when FUSE is
completely done with a file, i.e. when the last
open of the file is closed. At that point,
you can free
up any temporarily allocated data structures.
There is exactly one
release per open.
releasedir(const char *path,
struct fuse_file_info *fi)
release, except for directories.
fsync(const char *path, int
isdatasync, struct fuse_file_info *fi)
isdatasync is nonzero, only data, not
metadata, needs to be flushed. When this call returns,
all file data should be on stable storage. Many
filesystems leave this call unimplemented, although
technically that's a Bad Thing since it risks losing data.
If you store your filesystem inside a plain file on
another filesystem, you can implement fsync by calling
fdatasync(2) on that file, which will flush
too much data
(slowing performance) but achieve the desired guarantee.
A higher-performance option would be to open the backing
file with O_DSYNC and manage a private cache,
but this approach is too complex for most Fuse filesystems.
fsyncdir(const char *path, int
isdatasync, struct fuse_file_info *fi)
fsync, but for directories.
flush(const char *path, struct
fuse_file_info *fi)
close so that the filesystem
has a chance to flush dirty data. Notes:
flush are not
guaranteed to be propagated back to the user.
flush
calls for each open. It is not possible to
associate flushes with
opens. That's what
release is for.
flush call for
each open. Note: There is
no guarantee that flush will ever be called
at all!
lock(const char *path, struct
fuse_file_info *fi, int cmd, struct flock *locks)
bmap(const char *path, size_t blocksize,
uint64_t *blockno)
bmap(9). If the
filesystem
is backed by a block device, it converts
blockno from a file-relative block number to
a device-relative block number. It isn't entirely clear how the
blocksize parameter is intended to be used.
setxattr(const char *path, const
char *name, const char *value, size_t size, int flags)
setxattr(2).
getxattr(const char *path, const
char *name, char *value, size_t size)
getxattr(2).
listxattr(const char *path,
const char *list, size_t size)
listxattr(2).
removexattr(const char *path,
const char *name)
removexattr(2).
ioctl(const char *path, unsigned int cmd,
void *arg, struct fuse_file_info *fi, unsigned int flags,
void *data)
ioctl(2) system call. As such, almost
everything is up to the filesystem. On a 64-bit machine,
FUSE_IOCTL_COMPAT will be set in
flagsfor 32-bit ioctls.
The size and direction of data is determined by
_IOC_*() decoding of cmd. For
_IOC_NONE, data will be
NULL; for _IOC_WRITE
data is being written by the user; for
_IOC_READ it is being read, and if both are
set the data is bidirectional. In all
non-NULL cases, the data
area is _IOC_SIZE(cmd) bytes in size.
If FUSE_IOCTL_DIR is set in
flags, then the fuse_file_info
or path refers to a directory.
Note: the application's
request parameter to ioctl(2)
will be truncated to 32 bits when passing cmd
to the filesystem.
poll(const char *path, struct
fuse_file_info *fi, struct fuse_pollhandle *ph, unsigned
*reventsp);
select(2) system call). If ph is
non-NULL,
when the filesystem is ready for I/O it should call
fuse_notify_poll (possibly asynchronously)
with the specified ph; this will clear all
pending polls. The callee is responsible for
destroying ph with
fuse_pollhandle_destroy() when
ph is no longer needed.
write_buf(const char *path,
struct fuse_bufvec *buf, off_t off, struct fuse_file_info
*fi)
write, except that
buf is a structure that describes multiple
areas of memory. If implemented, this function allows the
client to write from a number of different areas of memory
(called a "gather" write) in a single operation.
read_buf(const char *path,
struct fuse_bufvec *buf, off_t off, struct fuse_file_info
*fi)
read, except that
buf is a structure that describes multiple
areas of memory. If implemented, this function allows the
client to read to a number of different areas of memory
(called a "scatter" read) in a single operation.
fallocate(const char *path, int
mode, off_t offset, off_t len, struct fuse_file_info *fi)
fallocate(2) manual page for descriptions of
the arguments.
copy_file_range(const
char *path_in, struct fuse_file_info *fi_in, off_t
offset_in, const char *path_out, struct fuse_file_info
*fi_out, off_t offset_out, size_t size, int flags)
copy_file_range(2) for details.
lseek(const char *path, off_t
off, int whence, struct fuse_file_info *fi)
lseek(2)
for details. Note that this function only needs to
support SEEK_DATA and SEEK_HOLE;
the kernel handles the other values of whence
internally.
The readdir function is somewhat like read,
in that it starts at a given offset and returns results in a
caller-supplied buffer. However, the offset is not a byte offset, and
the results are a series of struct dirents rather than
being uninterpreted bytes. To make life easier, FUSE provides a
"filler" function that will help you put things into the buffer. If
the flags argument to readdir contains the
FUSE_READDIR_PLUS bit, it is best to pass
FUSE_FILL_DIR_PLUS when calling the filler function and
set all fields in stbuf, below. (But if you don't do so,
your filesystem will still work; it'll just be a bit slower on very
large directories.)
readdir can operate in one of two modes. In mode 1, the
entire directory is returned by a single readdir call.
In that case, you should call the filler function in a loop, always
passing an offset argument of 0, until you have processed
all directory entries. The filler will return 1 only if an error
happens.
In mode 2, the directory is read piecewise through multiple
readdir calls. Each call to the filler should supply a
nonzero offset. The filler will return 1 when it can't
accept any more data, and a later call to readdir with
the same offset will pick up where the previous one left
off.
In both modes, directory entries are returned to the user by calling the filler function. Its prototype is:
int fuse_fill_dir_t (void *buf, const char *name,
const struct stat *stbuf, off_t off,
enum fuse_fill_dir_flags flags);
The general plan for a complete and correct mode-2 readdir is:
offset
(see below).
struct stat that describes
the file as for getattr (but unless the
flags passed to the filler function contain
FUSE_FILL_DIR_PLUS, FUSE only looks at
st_ino and the file-type bits of
st_mode).
filler function with arguments of
buf, the null-terminated filename, the address of
your struct stat (or NULL if you
have none),
the offset of the next directory entry, and the flags.
The meaning of "offset" is up to you (see below).
filler returns nonzero, or if there are no
more files, return 0.
From FUSE's point of view, the offset is an uninterpreted
off_t (i.e., an unsigned integer). You provide an offset
when you call filler, and it's possible that such an
offset might come back to you as an argument later. Typically, it's
simply the byte offset (within your directory layout) of the directory
entry, but it's really up to you; if you want it to be an index into
an array, that's fine.
An important note: in mode 2, you must support the offset
argument. The way Fuse detects the end of the directory is by calling
readdir with a nonzero offset and getting no
results back. So if you don't pay attention to the
offset that is given to you, you'll never terminate.
Also, you should never provide a zero offset, since Fuse
will give that back to you and you'll then start over at the front of
the directory!
It's also important to note that readdir can return
errors in a number of instances; in particular it can return -EBADF if
the file handle is invalid, or -ENOENT if you use the
path argument and the path doesn't exist.
Here's some sample pseudocode:
for (i = offset; i < max_dir_entries; i++)
if (entry[i].valid)
if (filler(buf, entry[i].name, NULL, i + 1))
return 0;
return 0;
The above code makes several assumptions and (in general) won't work
well with a multi-block directory that is being accessed by several
processes, but it should give you an outline
of how things should work.
The lock function is somewhat
complex. The cmd will be one of F_GETLK,
F_SETLK, or F_SETLKW. The fields in
locks are defined in the fcntl(2) manual page; the
l_whence field in that structure will always be
SEEK_SET.
For checking lock ownership, the fi->owner argument must
be used.
Contrary to what some other documentation states, the FUSE library does not appear to do anything special to help you out with locking. If you want locking to work, you will need to implement the lock function. (Persons who have more knowledge of how FUSE locking works are encouraged to contact me on this topic, since the existing documentation appears to be inaccurate.)
Once you've written your operations, you need some boilerplate.
As mentioned above, all of your functions should be named with a
sensible prefix; here I use "prefix" to represent that.
Create a fuse_operations struct that lists the functions
you implemented (for any unimplemented ones, you could simply delete the
relevant lines—but see Gotchas below for
why this is a bad idea):
static struct fuse_operations prefix_oper = {
.init = prefix_init,
.destroy = prefix_destroy,
.getattr = prefix_getattr,
.fgetattr = prefix_fgetattr,
.access = prefix_access,
.readlink = prefix_readlink,
.readdir = prefix_readdir,
.mknod = prefix_mknod,
.mkdir = prefix_mkdir,
.symlink = prefix_symlink,
.unlink = prefix_unlink,
.rmdir = prefix_rmdir,
.rename = prefix_rename,
.link = prefix_link,
.chmod = prefix_chmod,
.chown = prefix_chown,
.truncate = prefix_truncate,
.ftruncate = prefix_ftruncate,
.utimens = prefix_utimens,
.create = prefix_create,
.open = prefix_open,
.read = prefix_read,
.write = prefix_write,
.statfs = prefix_statfs,
.release = prefix_release,
.opendir = prefix_opendir,
.releasedir = prefix_releasedir,
.fsync = prefix_fsync,
.flush = prefix_flush,
.fsyncdir = prefix_fsyncdir,
.lock = prefix_lock,
.bmap = prefix_bmap,
.ioctl = prefix_ioctl,
.poll = prefix_poll,
#ifdef HAVE_SETXATTR
.setxattr = prefix_setxattr,
.getxattr = prefix_getxattr,
.listxattr = prefix_listxattr,
.removexattr = prefix_removexattr,
#endif
.flag_nullpath_ok = 0, /* See below */
};
Set flag_nullpath_ok nonzero if your code can accept a
NULL path argument (because it gets file information from
fi->fh) for the following operations:
fgetattr,
flush,
fsync,
fsyncdir,
ftruncate,
lock,
read,
readdir,
release,
releasedir, and
write.
This will allow FUSE to run more efficiently.
Finally, since your client is actually an executable program, you need
a main:
int main(int argc, char *argv[])
{
umask(0);
return fuse_main(argc, argv, &prefix_oper, NULL);
}
You can do your development on any machine you choose that supports FUSE. Mac users can try OSXFUSE; Linux users should be able to find FUSE as part of their distribution.
Compiling a FUSE program requires a slightly complicated command:
gcc -g -Og `pkg-config fuse --cflags --libs` my_hello.c -o my_helloA better approach, of course, is to use
make. This truly
minimal Makefile will let you type
"make foo" for any foo.c. You are
encouraged to use it and extend it to be more sensible.
To run a FUSE program, you'll need two windows and an empty scratch directory. You'll run your filesystem under a debugger in window #1; window #2 will be used for testing. The scratch directory is needed because you must have an empty directory on which to mount your shiny new filesystem.
The simplest (and incorrect, for our purposes) way to run a FUSE
program is to make a scratch directory and then pass that as an
argument to the program. For example, if you're running the "hello,
world" filesystem (hello.c):
$ mkdir testdir $ ./hello testdir $ ls testdir hello $ cat testdir/hello hello, world $ fusermount3 -u testdir # Or on Mac OS, umount testdir $ rmdir testdir
When you run your program this way, it automatically goes into the
background and starts serving up your filesystem. After you finish
testing, the fusermount3 command unmounts your filesystem
and kills the background program.
As a practical matter, it's easier to leave testdir
lying around rather than making it and removing it every time. (Most
production systems have a number of empty directories hanging around
just in case
you want to mount on top of them—often, either /mnt or
something inside /mnt.)
Of course, it's unlikely that your program will work perfectly the first time, so it's better to run it under the debugger. To do that, you'll need two windows. In window #1, do:
$ mkdir testdir # if necessary $ gdb hello [gdb noise] (gdb) [set breakpoints, etc.] (gdb) run -s -f -d testdir
The -s and -f switches mean
"single-threaded" and "run in foreground", respectively, which makes gdb
behave in a much friendlier fashion. The -d switch means
"debug", which causes FUSE to report the details of every operation
it's doing. (The
-d switch actually implies -f; I included it
above because sometimes the debugging output clutters things, in which
case you need -s -f.)
Now, in window #2 you can do:
$ ls testdir ... # Other trial commands $ fusermount3 -u testdir # Or on Mac OS, umount testdir
IMPORTANT: You need to do the fusermount3
even if your program crashes or you abort it. Otherwise you'll get a
confusing "Transport endpoint not connected" message the next time you
try to mount the test system.
If you have used gdb to set breakpoints, then when you do "ls testdir", your window may seem to hang. That's OK; just go over to the gdb window and step through your code. When it returns a result, your test window will come alive again.
Your new FUSE client has a lot of options. The simplest invocation
just specifies a mount point. For example, if your client is named
fuse_client and you're mounting on "~/foo", use:
./fuse_client ~/foo
There are tons of switches available; use
./fuse_client -s -h . to see them all.
The important ones are:
There are several common problems that plague programmers new to Fuse. This is a partial list:
-s switch to avoid this problem.
getattr
getattr like crazy. Implement it
first, or nothing will work.
truncate system call to make
writes work correctly.
chdir
is suppressed when you run with the -f switch, so your
code might appear to work fine under the debugger.
So if your code creates a scratch file named simply
"scratch", with -f you'll wind up with "./scratch" but
without -f it'll try to make "/scratch", which you don't
have permission to do.
To
avoid the problem, either (a) use absolute pathnames for
every file name hardwired into your code (e.g.,
"/home/me/fuse/scratch"), or
(b) record your current working directory by calling
get_current_dir_name before you invoke
fuse_main, and then convert relative
pathnames into corresponding absolute ones. Obviously,
(b) is the preferred approach.
printf/fprintf debugging
code will only work if you run with the -f
switch. Otherwise, Fuse disconnects stdout
and stderr, and all your output will just
disappear. So if you use printf for debugging,
be sure to run with -f or -d.
(Or open a log file with an absolute pathname and use
fprintf to print to it.)
fuse_operations struct,
Fuse will silently generate a failure when it is called,
and you'll never find out that you need to write it.
Instead, write every unimplemented function as a stub that
prints a message to stderr and returns an
error code (ENOSYS is preferred). When you see the
message, you'll know what extra functions you need to
write.
© 2022, Geoff Kuenning
This page is maintained by Geoff Kuenning.