aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.dir-locals.el25
-rw-r--r--.gitignore3
-rw-r--r--.travis.yml21
-rw-r--r--ChangeLog.rst (renamed from ChangeLog)18
-rw-r--r--Makefile.am22
-rw-r--r--README.md72
-rw-r--r--README.rst132
-rw-r--r--configure.ac27
-rw-r--r--meson.build64
-rw-r--r--sshfs.1.in4
-rw-r--r--sshfs.c170
-rw-r--r--sshnodelay.c59
-rw-r--r--test/.gitignore1
-rw-r--r--test/conftest.py89
-rw-r--r--test/lsan_suppress.txt11
-rw-r--r--test/meson.build11
-rw-r--r--test/pytest.ini2
-rwxr-xr-xtest/test_sshfs.py378
-rwxr-xr-xtest/travis-build.sh48
-rwxr-xr-xtest/travis-install.sh21
-rw-r--r--test/util.py100
-rw-r--r--test/wrong_command.c9
22 files changed, 974 insertions, 313 deletions
diff --git a/.dir-locals.el b/.dir-locals.el
new file mode 100644
index 0000000..a9d100f
--- /dev/null
+++ b/.dir-locals.el
@@ -0,0 +1,25 @@
+((python-mode . ((indent-tabs-mode . nil)))
+ (autoconf-mode . ((indent-tabs-mode . t)))
+ (c-mode . ((c-file-style . "stroustrup")
+ (indent-tabs-mode . t)
+ (tab-width . 8)
+ (c-basic-offset . 8)
+ (c-file-offsets .
+ ((block-close . 0)
+ (brace-list-close . 0)
+ (brace-list-entry . 0)
+ (brace-list-intro . +)
+ (case-label . 0)
+ (class-close . 0)
+ (defun-block-intro . +)
+ (defun-close . 0)
+ (defun-open . 0)
+ (else-clause . 0)
+ (inclass . +)
+ (label . 0)
+ (statement . 0)
+ (statement-block-intro . +)
+ (statement-case-intro . +)
+ (statement-cont . +)
+ (substatement . +)
+ (topmost-intro . 0))))))
diff --git a/.gitignore b/.gitignore
index f39c637..086e986 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,8 +6,6 @@
# NOTE! Please use 'git ls-files -i --exclude-standard'
# command after changing this file, to see if there are
# any tracked files which get ignored after the change.
-.*
-!.gitignore
*.o
*.lo
*.la
@@ -35,3 +33,4 @@ sshfs.1
/.pc
/patches
/m4
+.deps/
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e5b0f76
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,21 @@
+sudo: required
+dist: trusty
+group: deprecated-2017Q2
+
+language:
+ - c
+addons:
+ apt:
+ sources:
+ - ubuntu-toolchain-r-test
+ packages:
+ - valgrind
+ - clang
+ - gcc
+ - gcc-6
+ - fuse
+ - libfuse2
+ - libfuse-dev
+install: test/travis-install.sh
+script: test/travis-build.sh
+
diff --git a/ChangeLog b/ChangeLog.rst
index 4644ae8..36ea132 100644
--- a/ChangeLog
+++ b/ChangeLog.rst
@@ -1,3 +1,21 @@
+Unreleased Changes
+------------------
+
+* Fixed a crash due to a race condition when listing
+ directory contents.
+* Added unit tests
+* Documented limited hardlink support.
+* Added support for building with Meson.
+* Added support for more SSH options.
+* Dropped support for the *nodelay* workaround - the last OpenSSH
+ version for which this was useful was released in 2006.
+* Dropped support for the *nodelaysrv* workaround. The same effect
+ (enabling NODELAY on the server side *and* enabling X11 forwarding)
+ can be achieved by explicitly passing `-o ForwardX11`
+* Removed support for `-o workaround=all`. Workarounds should always
+ enabled explicitly and only when needed. There is no point in always
+ enabling a potentially changing set of workarounds.
+
Release 2.9 (2017-04-17)
------------------------
diff --git a/Makefile.am b/Makefile.am
index 3d8f9cb..e34fbb3 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -15,28 +15,14 @@ sshfs_CFLAGS = $(SSHFS_CFLAGS)
sshfs_CPPFLAGS = -D_REENTRANT -DFUSE_USE_VERSION=26 -DLIBDIR=\"$(libdir)\" \
-DIDMAP_DEFAULT="\"$(IDMAP_DEFAULT)\""
-EXTRA_DIST = sshnodelay.c sshfs.1.in
-CLEANFILES = sshnodelay.so sshfs.1 sshfs.1.tmp
+EXTRA_DIST = sshfs.1.in meson.build
+CLEANFILES = sshfs.1 sshfs.1.tmp
dist_man_MANS = sshfs.1
sshfs.1: sshfs.1.in
$(AM_V_GEN)sed \
- -e 's,__IDMAP_DEFAULT__,$(IDMAP_DEFAULT),g' \
- -e 's,__UNMOUNT_COMMAND__,$(UNMOUNT_COMMAND),g' \
+ -e 's/[@]IDMAP_DEFAULT@/$(IDMAP_DEFAULT)/g' \
+ -e 's/[@]UNMOUNT_COMMAND@/$(UNMOUNT_COMMAND)/g' \
<$(srcdir)/sshfs.1.in >sshfs.1.tmp || exit 1; \
mv sshfs.1.tmp sshfs.1
-
-if SSH_NODELAY_SO
-all-local: sshnodelay.so
-
-install-exec-local: sshnodelay.so
- test -z "$(libdir)" || $(mkdir_p) "$(DESTDIR)$(libdir)"
- $(INSTALL) -m 755 sshnodelay.so "$(DESTDIR)$(libdir)/sshnodelay.so"
-
-uninstall-local:
- rm -f "$(DESTDIR)$(libdir)/sshnodelay.so"
-
-sshnodelay.so:
- $(CC) -Wall -W -s --shared -fPIC $(sshnodelay_libs) sshnodelay.c -o sshnodelay.so
-endif
diff --git a/README.md b/README.md
deleted file mode 100644
index 8913bd6..0000000
--- a/README.md
+++ /dev/null
@@ -1,72 +0,0 @@
-Abstract
-========
-
-This is a filesystem client based on the SSH File Transfer Protocol.
-Since most SSH servers already support this protocol it is very easy
-to set up: i.e. on the server side there's nothing to do. On the
-client side mounting the filesystem is as easy as logging into the
-server with ssh.
-
-The idea of sshfs was taken from the SSHFS filesystem distributed with
-LUFS, which I found very useful. There were some limitations of that
-codebase, so I rewrote it. Features of this implementation are:
-
- - Based on FUSE (the best userspace filesystem framework for Linux ;)
-
- - Multithreading: more than one request can be on it's way to the
- server
-
- - Allowing large reads (max 64k)
-
- - Caching directory contents
-
- - Reconnect on failure
-
-Latest version
-==============
-
-The latest version and more information can be found on
-http://github.com/libfuse/sshfs
-
-
-How to mount a filesystem
-=========================
-
-Once sshfs is installed (see next section) running it is very simple:
-
- sshfs hostname: mountpoint
-
-Note, that it's recommended to run it as user, not as root. For this
-to work the mountpoint must be owned by the user. If the username is
-different on the host you are connecting to, then use the
-"username@host:" form. If you need to enter a password sshfs will ask
-for it (actually it just runs ssh which ask for the password if
-needed). You can also specify a directory after the ":". The default
-is the home directory.
-
-Also many ssh options can be specified (see the manual pages for
-sftp(1) and ssh_config(5)), including the remote port number
-(`-oport=PORT`)
-
-To unmount the filesystem:
-
- fusermount -u mountpoint
-
-
-Installing
-==========
-
-First you need to download FUSE 2.2 or later from
-http://github.com/libfuse/libfuse.
-
-You also need to install the devel package for glib2.0. After
-installing FUSE, compile sshfs the usual way:
-
- ./configure
- make
- make install (as root)
-
-And you are ready to go.
-
-If checking out from git for the first time also do `autoreconf -i`
-before doing `./configure`.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..bacada2
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,132 @@
+SSHFS
+=====
+
+
+About
+-----
+
+SSHFS allows you to mount a remote filesystem using SFTP. Most SSH
+servers support and enable this SFTP access by default, so SSHFS is
+very simple to use - there's nothing to do on the server-side.
+
+
+How to use
+----------
+
+Once sshfs is installed (see next section) running it is very simple::
+
+ sshfs [user@]hostname:[directory] mountpoint
+
+It is recommended to run SSHFS as regular user (not as root). For
+this to work the mountpoint must be owned by the user. If username is
+omitted SSHFS will use the local username. If the directory is
+omitted, SSHFS will mount the (remote) home directory. If you need to
+enter a password sshfs will ask for it (actually it just runs ssh
+which ask for the password if needed).
+
+Also many ssh options can be specified (see the manual pages for
+*sftp(1)* and *ssh_config(5)*), including the remote port number
+(``-oport=PORT``)
+
+To unmount the filesystem::
+
+ fusermount -u mountpoint
+
+On BSD and OS-X, to unmount the filesystem::
+
+ umount mountpoint
+
+
+Installation
+------------
+
+First, download the latest SSHFS release from
+https://github.com/libfuse/sshfs/releases. On Linux and BSD, you will
+also need to have libfuse_ installed. On OS-X, you need OSXFUSE_
+instead. Finally, you need the Glib_ development package (which should
+be available from your operating system's package manager).
+
+To build and install, we recommend to use Meson_ (version 0.38 or
+newer) and Ninja_. After extracting the sshfs tarball, create a
+(temporary) build directory and run Meson::
+
+ $ md build; cd build
+ $ meson ..
+
+Normally, the default build options will work fine. If you
+nevertheless want to adjust them, you can do so with the *mesonconf*
+command::
+
+ $ mesonconf # list options
+ $ mesonconf -D strip=true # set an option
+
+To build, test and install SSHFS, you then use Ninja (running the
+tests requires the `py.test`_ Python module)::
+
+ $ ninja
+ $ python3 -m pytest test/ # optional, but recommended
+ $ sudo ninja install
+
+.. _libfuse: http://github.com/libfuse/libfuse
+.. _OSXFUSE: https://osxfuse.github.io/
+.. _Glib: https://developer.gnome.org/glib/stable/
+.. _Meson: http://mesonbuild.com/
+.. _Ninja: https://ninja-build.org/
+.. _`py.test`: http://www.pytest.org/
+
+Alternate Installation
+----------------------
+
+If you are not able to use Meson and Ninja, please report this to the
+sshfs mailing list. Until the problem is resolved, you may fall back
+to an in-source build using autotools::
+
+ $ ./configure
+ $ make
+ $ sudo make install
+
+Note that support for building with autotools may disappear at some
+point, so if you depend on using autotools for some reason please let
+the sshfs developers know!
+
+
+Caveats
+-------
+
+Rename
+~~~~~~
+
+Some SSH servers do not support atomically overwriting the destination
+when renaming a file. In this case you will get an error when you
+attempt to rename a file and the destination already exists. A
+workaround is to first remove the destination file, and then do the
+rename. SSHFS can do this automatically if you call it with `-o
+workaround=rename`. However, in this case it is still possible that
+someone (or something) recreates the destination file after SSHFS has
+removed it, but before SSHFS had the time to rename the old file. In
+this case, the rename will still fail.
+
+Hardlinks
+~~~~~~~~~
+
+If the SSH server supports the *hardlinks* extension, SSHFS will allow
+you to create hardlinks. However, hardlinks will always appear as
+individual files when seen through an SSHFS mount, i.e. they will
+appear to have different inodes and an *st_nlink* value of 1.
+
+
+Getting Help
+------------
+
+If you need help, please ask on the <fuse-sshfs@lists.sourceforge.net>
+mailing list (subscribe at
+https://lists.sourceforge.net/lists/listinfo/fuse-sshfs).
+
+Please report any bugs on the GitHub issue tracker at
+https://github.com/libfuse/libfuse/issues.
+
+Professional Support
+--------------------
+
+Professional support is available. Please contact Nikolaus Rath
+<Nikolaus@rath.org> for details.
diff --git a/configure.ac b/configure.ac
index f33bb1b..93af51b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -8,8 +8,6 @@ AM_PROG_CC_C_O
CFLAGS="$CFLAGS -Wall -W"
LIBS=
AC_SEARCH_LIBS(dlsym, [dl])
-sshnodelay_libs=$LIBS
-AC_SUBST(sshnodelay_libs)
LIBS=
case "$target_os" in
@@ -18,31 +16,6 @@ case "$target_os" in
*) osname=unknown;;
esac
-AC_ARG_ENABLE(sshnodelay,
- [ --disable-sshnodelay Don't compile NODELAY workaround for ssh])
-
-if test -z "$enable_sshnodelay"; then
- AC_MSG_CHECKING([OpenSSH version])
- [eval `ssh -V 2>&1 | sed -n 's/^OpenSSH_\([1-9][0-9]*\)\.\([0-9][0-9]*\).*/ssh_major=\1 ssh_minor=\2/p'`]
- if test "x$ssh_major" != x -a "x$ssh_minor" != x; then
- if test $ssh_major -gt 4 -o \( $ssh_major = 4 -a $ssh_minor -ge 4 \); then
- AC_MSG_RESULT([$ssh_major.$ssh_minor >= 4.4, disabling NODELAY workaround])
- enable_sshnodelay=no
- else
- AC_MSG_RESULT([$ssh_major.$ssh_minor < 4.4, enabling NODELAY workaround])
- enable_sshnodelay=yes
- fi
- else
- AC_MSG_RESULT([not found])
- fi
-fi
-
-if test "$enable_sshnodelay" = "yes"; then
- AC_DEFINE(SSH_NODELAY_WORKAROUND, 1, [Compile ssh NODELAY workaround])
-fi
-
-AM_CONDITIONAL(SSH_NODELAY_SO, test "$enable_sshnodelay" = "yes")
-
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH
PKG_CHECK_MODULES([SSHFS], [fuse >= 2.3 glib-2.0 gthread-2.0])
have_fuse_opt_parse=no
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..659c37d
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,64 @@
+project('sshfs', 'c', version: '3.0.0',
+ meson_version: '>= 0.38',
+ default_options: [ 'buildtype=plain' ])
+
+add_global_arguments('-D_REENTRANT', '-DHAVE_CONFIG_H', '-O2', '-g',
+ '-Wall', '-Wextra', '-Wno-sign-compare',
+ '-Wmissing-declarations', '-Wwrite-strings',
+ language: 'c')
+
+# Some (stupid) GCC versions warn about unused return values even when they are
+# casted to void. This makes -Wunused-result pretty useless, since there is no
+# way to suppress the warning when we really *want* to ignore the value.
+cc = meson.get_compiler('c')
+code = '''
+__attribute__((warn_unused_result)) int get_4() {
+ return 4;
+}
+int main(void) {
+ (void) get_4();
+ return 0;
+}'''
+if not cc.compiles(code, args: [ '-O0', '-Werror=unused-result' ])
+ message('Compiler warns about unused result even when casting to void')
+ add_global_arguments('-Wno-unused-result', language: 'c')
+endif
+
+
+cfg = configuration_data()
+
+cfg.set_quoted('PACKAGE_VERSION', meson.project_version())
+
+include_dirs = [ include_directories('.') ]
+sshfs_sources = ['sshfs.c', 'cache.c']
+if target_machine.system() == 'darwin'
+ cfg.set_quoted('IDMAP_DEFAULT', 'user')
+ sshfs_sources += [ 'compat/fuse_opt.c', 'compat/darwin_compat.c' ]
+ include_dirs += [ include_directories('compat') ]
+else
+ cfg.set_quoted('IDMAP_DEFAULT', 'none')
+endif
+
+configure_file(input: 'sshfs.1.in',
+ output: 'sshfs.1',
+ configuration : cfg)
+configure_file(output: 'config.h',
+ configuration : cfg)
+
+sshfs_deps = [ dependency('fuse', version: '>= 2.3'),
+ dependency('glib-2.0'),
+ dependency('gthread-2.0') ]
+
+executable('sshfs', sshfs_sources,
+ include_directories: include_dirs,
+ dependencies: sshfs_deps,
+ c_args: ['-DFUSE_USE_VERSION=26'],
+ install: true,
+ install_dir: get_option('bindir'))
+
+# This is a little ugly. Is there a better way to tell Meson that the
+# manpage is in the build directory?
+install_man(join_paths(meson.current_build_dir(), 'sshfs.1'))
+
+subdir('test')
+
diff --git a/sshfs.1.in b/sshfs.1.in
index 4ed4fa4..b96e7dc 100644
--- a/sshfs.1.in
+++ b/sshfs.1.in
@@ -7,7 +7,7 @@ SSHFS \- filesystem client based on ssh
\fBsshfs\fP [\fIuser\fP@]\fBhost\fP:[\fIdir\fP] \fBmountpoint\fP [\fIoptions\fP]
.SS unmounting
.TP
-\fB__UNMOUNT_COMMAND__ mountpoint\fP
+\fB@UNMOUNT_COMMAND@ mountpoint\fP
.SH DESCRIPTION
SSHFS (Secure SHell FileSystem) is a file system for Linux (and other
operating systems with a FUSE implementation, such as Mac OS X or FreeBSD)
@@ -97,7 +97,7 @@ fix buffer fillup bug in server (default: on)
.RE
.TP
\fB\-o\fR idmap=TYPE
-user/group ID mapping (default: __IDMAP_DEFAULT__)
+user/group ID mapping (default: @IDMAP_DEFAULT@)
.RS 8
.TP
none
diff --git a/sshfs.c b/sshfs.c
index f116633..97eaf06 100644
--- a/sshfs.c
+++ b/sshfs.c
@@ -132,8 +132,6 @@
#define SFTP_SERVER_PATH "/usr/lib/sftp-server"
-#define SSHNODELAY_SO "sshnodelay.so"
-
/* Asynchronous readdir parameters */
#define READDIR_START 2
#define READDIR_MAX 32
@@ -216,8 +214,6 @@ struct sshfs {
struct fuse_args ssh_args;
char *workarounds;
int rename_workaround;
- int nodelay_workaround;
- int nodelaysrv_workaround;
int truncate_workaround;
int buflimit_workaround;
int fstat_workaround;
@@ -297,6 +293,7 @@ static const char *ssh_opts[] = {
"AddressFamily",
"BatchMode",
"BindAddress",
+ "CertificateFile",
"ChallengeResponseAuthentication",
"CheckHostIP",
"Cipher",
@@ -307,28 +304,40 @@ static const char *ssh_opts[] = {
"ConnectTimeout",
"ControlMaster",
"ControlPath",
+ "ControlPersist",
+ "FingerprintHash",
"GlobalKnownHostsFile",
"GSSAPIAuthentication",
"GSSAPIDelegateCredentials",
"HostbasedAuthentication",
+ "HostbasedKeyTypes",
"HostKeyAlgorithms",
"HostKeyAlias",
"HostName",
"IdentitiesOnly",
"IdentityFile",
+ "IdentityAgent",
+ "IPQoS",
"KbdInteractiveAuthentication",
"KbdInteractiveDevices",
+ "KexAlgorithms",
"LocalCommand",
"LogLevel",
"MACs",
"NoHostAuthenticationForLocalhost",
"NumberOfPasswordPrompts",
"PasswordAuthentication",
+ "PermitLocalCommand",
+ "PKCS11Provider",
"Port",
"PreferredAuthentications",
"ProxyCommand",
+ "ProxyJump",
+ "ProxyUseFdpass",
+ "PubkeyAcceptedKeyTypes"
"PubkeyAuthentication",
"RekeyLimit",
+ "RevokedHostKeys",
"RhostsRSAAuthentication",
"RSAAuthentication",
"ServerAliveCountMax",
@@ -336,9 +345,11 @@ static const char *ssh_opts[] = {
"SmartcardDevice",
"StrictHostKeyChecking",
"TCPKeepAlive",
+ "UpdateHostKeys",
"UsePrivilegedPort",
"UserKnownHostsFile",
"VerifyHostKeyDNS",
+ "VisualHostKey",
NULL,
};
@@ -408,23 +419,11 @@ static struct fuse_opt sshfs_opts[] = {
static struct fuse_opt workaround_opts[] = {
SSHFS_OPT("none", rename_workaround, 0),
- SSHFS_OPT("none", nodelay_workaround, 0),
- SSHFS_OPT("none", nodelaysrv_workaround, 0),
SSHFS_OPT("none", truncate_workaround, 0),
SSHFS_OPT("none", buflimit_workaround, 0),
SSHFS_OPT("none", fstat_workaround, 0),
- SSHFS_OPT("all", rename_workaround, 1),
- SSHFS_OPT("all", nodelay_workaround, 1),
- SSHFS_OPT("all", nodelaysrv_workaround, 1),
- SSHFS_OPT("all", truncate_workaround, 1),
- SSHFS_OPT("all", buflimit_workaround, 1),
- SSHFS_OPT("all", fstat_workaround, 1),
SSHFS_OPT("rename", rename_workaround, 1),
SSHFS_OPT("norename", rename_workaround, 0),
- SSHFS_OPT("nodelay", nodelay_workaround, 1),
- SSHFS_OPT("nonodelay", nodelay_workaround, 0),
- SSHFS_OPT("nodelaysrv", nodelaysrv_workaround, 1),
- SSHFS_OPT("nonodelaysrv", nodelaysrv_workaround, 0),
SSHFS_OPT("truncate", truncate_workaround, 1),
SSHFS_OPT("notruncate", truncate_workaround, 0),
SSHFS_OPT("buflimit", buflimit_workaround, 1),
@@ -877,85 +876,6 @@ static void ssh_add_arg(const char *arg)
_exit(1);
}
-#ifdef SSH_NODELAY_WORKAROUND
-static int do_ssh_nodelay_workaround(void)
-{
-#ifdef __APPLE__
- char *oldpreload = getenv("DYLD_INSERT_LIBRARIES");
-#else
- char *oldpreload = getenv("LD_PRELOAD");
-#endif
- char *newpreload;
- char sopath[PATH_MAX];
- int res;
-
-#ifdef __APPLE__
- char *sshfs_program_path_base = NULL;
- if (!sshfs_program_path[0]) {
- goto nobundle;
- }
- sshfs_program_path_base = dirname(sshfs_program_path);
- if (!sshfs_program_path_base) {
- goto nobundle;
- }
- snprintf(sopath, sizeof(sopath), "%s/%s", sshfs_program_path_base,
- SSHNODELAY_SO);
- res = access(sopath, R_OK);
- if (res == -1) {
- goto nobundle;
- }
- goto pathok;
-
-nobundle:
-#endif /* __APPLE__ */
-
- snprintf(sopath, sizeof(sopath), "%s/%s", LIBDIR, SSHNODELAY_SO);
- res = access(sopath, R_OK);
- if (res == -1) {
- char *s;
- if (!realpath(sshfs.progname, sopath))
- return -1;
-
- s = strrchr(sopath, '/');
- if (!s)
- s = sopath;
- else
- s++;
-
- if (s + strlen(SSHNODELAY_SO) >= sopath + sizeof(sopath))
- return -1;
-
- strcpy(s, SSHNODELAY_SO);
- res = access(sopath, R_OK);
- if (res == -1) {
- fprintf(stderr, "sshfs: cannot find %s\n",
- SSHNODELAY_SO);
- return -1;
- }
- }
-
-#ifdef __APPLE__
-pathok:
-#endif
-
- newpreload = g_strdup_printf("%s%s%s",
- oldpreload ? oldpreload : "",
- oldpreload ? " " : "",
- sopath);
-
-#ifdef __APPLE__
- if (!newpreload || setenv("DYLD_INSERT_LIBRARIES", newpreload, 1) == -1)
- fprintf(stderr, "warning: failed set DYLD_INSERT_LIBRARIES for ssh nodelay workaround\n");
-#else /* !__APPLE__ */
- if (!newpreload || setenv("LD_PRELOAD", newpreload, 1) == -1) {
- fprintf(stderr, "warning: failed set LD_PRELOAD "
- "for ssh nodelay workaround\n");
- }
-#endif /* __APPLE__ */
- g_free(newpreload);
- return 0;
-}
-#endif
static int pty_expect_loop(void)
{
@@ -1089,29 +1009,6 @@ static int start_ssh(void)
} else if (pid == 0) {
int devnull;
-#ifdef SSH_NODELAY_WORKAROUND
- if (sshfs.nodelay_workaround &&
- do_ssh_nodelay_workaround() == -1) {
- fprintf(stderr,
- "warning: ssh nodelay workaround disabled\n");
- }
-#endif
-
- if (sshfs.nodelaysrv_workaround) {
- int i;
- /*
- * Hack to work around missing TCP_NODELAY
- * setting in sshd
- */
- for (i = 1; i < sshfs.ssh_args.argc; i++) {
- if (strcmp(sshfs.ssh_args.argv[i], "-x") == 0) {
- replace_arg(&sshfs.ssh_args.argv[i],
- "-X");
- break;
- }
- }
- }
-
devnull = open("/dev/null", O_WRONLY);
if (dup2(sockpair[1], 0) == -1 || dup2(sockpair[1], 1) == -1) {
@@ -2178,11 +2075,16 @@ static int sftp_readdir_async(struct buffer *handle, fuse_cache_dirh_t h,
outstanding--;
if (done) {
+ /* We need to cache want_reply, since processing
+ thread may free req right after unlock() if
+ want_reply == 0 */
+ int want_reply;
pthread_mutex_lock(&sshfs.lock);
if (sshfs_req_pending(req))
req->want_reply = 0;
+ want_reply = req->want_reply;
pthread_mutex_unlock(&sshfs.lock);
- if (!req->want_reply)
+ if (!want_reply)
continue;
}
@@ -2645,7 +2547,8 @@ static int sshfs_fsync(const char *path, int isdatasync,
int err;
(void) isdatasync;
- if (err = sshfs_flush(path, fi))
+ err = sshfs_flush(path, fi);
+ if (err)
return err;
if (!sshfs.ext_fsync)
@@ -3422,14 +3325,10 @@ static void usage(const char *progname)
" cache if full (default: 5)\n"
" -o workaround=LIST colon separated list of workarounds\n"
" none no workarounds enabled\n"
-" all all workarounds enabled\n"
" [no]rename fix renaming to existing file (default: off)\n"
-#ifdef SSH_NODELAY_WORKAROUND
-" [no]nodelay set nodelay tcp flag in ssh (default: on)\n"
-#endif
-" [no]nodelaysrv set nodelay tcp flag in sshd (default: off)\n"
" [no]truncate fix truncate for old servers (default: off)\n"
" [no]buflimit fix buffer fillup bug in server (default: on)\n"
+" [no]fstat fix fstat for old servers (default: off)\n"
" -o idmap=TYPE user/group ID mapping (default: " IDMAP_DEFAULT ")\n"
" none no translation of the ID space\n"
" user only translate UID/GID of connecting user\n"
@@ -3549,10 +3448,14 @@ static int workaround_opt_proc(void *data, const char *arg, int key,
return -1;
}
-int parse_workarounds(void)
+static int parse_workarounds(void)
{
int res;
- char *argv[] = { "", "-o", sshfs.workarounds, NULL };
+ /* Need separate variables because literals are const
+ char */
+ char argv0[] = "";
+ char argv1[] = "-o";
+ char *argv[] = { argv0, argv1, sshfs.workarounds, NULL };
struct fuse_args args = FUSE_ARGS_INIT(3, argv);
char *s = sshfs.workarounds;
if (!s)
@@ -3958,14 +3861,11 @@ int main(int argc, char *argv[])
memset(sshfs_program_path, 0, PATH_MAX);
}
#endif /* __APPLE__ */
- g_thread_init(NULL);
sshfs.blksize = 4096;
/* SFTP spec says all servers should allow at least 32k I/O */
sshfs.max_read = 32768;
sshfs.max_write = 32768;
- sshfs.nodelay_workaround = 1;
- sshfs.nodelaysrv_workaround = 0;
#ifdef __APPLE__
sshfs.rename_workaround = 1;
#else
@@ -4164,6 +4064,13 @@ int main(int argc, char *argv[])
exit(1);
}
+ res = fuse_set_signal_handlers(fuse_get_session(fuse));
+ if (res == -1) {
+ fuse_unmount(mountpoint, ch);
+ fuse_destroy(fuse);
+ exit(1);
+ }
+
/*
* FIXME: trim $PATH so it doesn't contain anything inside the
* mountpoint, which would deadlock.
@@ -4176,9 +4083,6 @@ int main(int argc, char *argv[])
}
res = fuse_daemonize(foreground);
- if (res != -1)
- res = fuse_set_signal_handlers(fuse_get_session(fuse));
-
if (res == -1) {
fuse_unmount(mountpoint, ch);
fuse_destroy(fuse);
diff --git a/sshnodelay.c b/sshnodelay.c
deleted file mode 100644
index a9f307c..0000000
--- a/sshnodelay.c
+++ /dev/null
@@ -1,59 +0,0 @@
-#define _GNU_SOURCE
-#include <dlfcn.h>
-#include <sys/types.h>
-#include <sys/socket.h>
-#include <netinet/in.h>
-#include <netinet/tcp.h>
-
-/* Wrapper around connect(2) to explicitly set TCP_NODELAY. */
-static int nodelay_connect(
- int (*real_connect)(int, const struct sockaddr *, socklen_t),
- int sock, const struct sockaddr *addr, socklen_t addrlen)
-{
- int res = real_connect(sock, addr, addrlen);
- if (!res && addr->sa_family == AF_INET) {
- int opt = 1;
- setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
- }
- return res;
-}
-
-#if __APPLE__
-
-/* OS X does not have LD_PRELOAD but has DYLD_INSERT_LIBRARIES. The right
- * environment variable is set by sshfs.c when attempting to load the
- * sshnodelay workaround.
- *
- * However, things are not that simple: DYLD_INSERT_LIBRARIES does not
- * behave exactly like LD_PRELOAD. Instead, the dyld dynamic linker will
- * look for __DATA __interpose sections on the libraries given via the
- * DYLD_INSERT_LIBRARIES variable. The contents of this section are pairs
- * of replacement functions and functions to be replaced, respectively.
- * Prepare such section here. */
-
-int custom_connect(int sock, const struct sockaddr *addr, socklen_t addrlen);
-
-typedef struct interpose_s {
- void *new_func;
- void *orig_func;
-} interpose_t;
-
-static const interpose_t interposers[] \
- __attribute__ ((section("__DATA, __interpose"))) = {
- { (void *)custom_connect, (void *)connect },
-};
-
-int custom_connect(int sock, const struct sockaddr *addr, socklen_t addrlen)
-{
- return nodelay_connect(connect, sock, addr, addrlen);
-}
-
-#else /* !__APPLE__ */
-
-int connect(int sock, const struct sockaddr *addr, socklen_t addrlen)
-{
- return nodelay_connect(dlsym(RTLD_NEXT, "connect"),
- sock, addr, addrlen);
-}
-
-#endif /* !__APPLE__ */
diff --git a/test/.gitignore b/test/.gitignore
new file mode 100644
index 0000000..c18dd8d
--- /dev/null
+++ b/test/.gitignore
@@ -0,0 +1 @@
+__pycache__/
diff --git a/test/conftest.py b/test/conftest.py
new file mode 100644
index 0000000..70cd0c6
--- /dev/null
+++ b/test/conftest.py
@@ -0,0 +1,89 @@
+import sys
+import pytest
+import time
+import re
+
+# If a test fails, wait a moment before retrieving the captured
+# stdout/stderr. When using a server process, this makes sure that we capture
+# any potential output of the server that comes *after* a test has failed. For
+# example, if a request handler raises an exception, the server first signals an
+# error to FUSE (causing the test to fail), and then logs the exception. Without
+# the extra delay, the exception will go into nowhere.
+@pytest.mark.hookwrapper
+def pytest_pyfunc_call(pyfuncitem):
+ outcome = yield
+ failed = outcome.excinfo is not None
+ if failed:
+ time.sleep(1)
+
+@pytest.fixture()
+def pass_capfd(request, capfd):
+ '''Provide capfd object to UnitTest instances'''
+ request.instance.capfd = capfd
+
+def check_test_output(capfd):
+ (stdout, stderr) = capfd.readouterr()
+
+ # Write back what we've read (so that it will still be printed.
+ sys.stdout.write(stdout)
+ sys.stderr.write(stderr)
+
+ # Strip out false positives
+ for (pattern, flags, count) in capfd.false_positives:
+ cp = re.compile(pattern, flags)
+ (stdout, cnt) = cp.subn('', stdout, count=count)
+ if count == 0 or count - cnt > 0:
+ stderr = cp.sub('', stderr, count=count - cnt)
+
+ patterns = [ r'\b{}\b'.format(x) for x in
+ ('exception', 'error', 'warning', 'fatal', 'traceback',
+ 'fault', 'crash(?:ed)?', 'abort(?:ed)',
+ 'uninitiali[zs]ed') ]
+ patterns += ['^==[0-9]+== ']
+ for pattern in patterns:
+ cp = re.compile(pattern, re.IGNORECASE | re.MULTILINE)
+ hit = cp.search(stderr)
+ if hit:
+ raise AssertionError('Suspicious output to stderr (matched "%s")' % hit.group(0))
+ hit = cp.search(stdout)
+ if hit:
+ raise AssertionError('Suspicious output to stdout (matched "%s")' % hit.group(0))
+
+def register_output(self, pattern, count=1, flags=re.MULTILINE):
+ '''Register *pattern* as false positive for output checking
+
+ This prevents the test from failing because the output otherwise
+ appears suspicious.
+ '''
+
+ self.false_positives.append((pattern, flags, count))
+
+# This is a terrible hack that allows us to access the fixtures from the
+# pytest_runtest_call hook. Among a lot of other hidden assumptions, it probably
+# relies on tests running sequential (i.e., don't dare to use e.g. the xdist
+# plugin)
+current_capfd = None
+@pytest.yield_fixture(autouse=True)
+def save_cap_fixtures(request, capfd):
+ global current_capfd
+ capfd.false_positives = []
+
+ # Monkeypatch in a function to register false positives
+ type(capfd).register_output = register_output
+
+ if request.config.getoption('capture') == 'no':
+ capfd = None
+ current_capfd = capfd
+ bak = current_capfd
+ yield
+
+ # Try to catch problems with this hack (e.g. when running tests
+ # simultaneously)
+ assert bak is current_capfd
+ current_capfd = None
+
+@pytest.hookimpl(trylast=True)
+def pytest_runtest_call(item):
+ capfd = current_capfd
+ if capfd is not None:
+ check_test_output(capfd)
diff --git a/test/lsan_suppress.txt b/test/lsan_suppress.txt
new file mode 100644
index 0000000..4352b3a
--- /dev/null
+++ b/test/lsan_suppress.txt
@@ -0,0 +1,11 @@
+# Suppression file for address sanitizer.
+
+# There are some leaks in command line option parsing. They should be
+# fixed at some point, but are harmless since the consume just a small,
+# constant amount of memory and do not grow.
+leak:fuse_opt_parse
+
+
+# Leaks in fusermount3 are harmless as well (it's a short-lived
+# process) - but patches are welcome!
+leak:fusermount.c
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..90eb94e
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,11 @@
+test_scripts = [ 'conftest.py', 'pytest.ini', 'test_sshfs.py',
+ 'util.py' ]
+custom_target('test_scripts', input: test_scripts,
+ output: test_scripts, build_by_default: true,
+ command: ['cp', '-fPu', '--preserve=mode',
+ '@INPUT@', meson.current_build_dir() ])
+
+# Provide something helpful when running 'ninja test'
+wrong_cmd = executable('wrong_command', 'wrong_command.c',
+ install: false)
+test('wrong_cmd', wrong_cmd)
diff --git a/test/pytest.ini b/test/pytest.ini
new file mode 100644
index 0000000..9516154
--- /dev/null
+++ b/test/pytest.ini
@@ -0,0 +1,2 @@
+[pytest]
+addopts = --verbose --assert=rewrite --tb=native -x -r a
diff --git a/test/test_sshfs.py b/test/test_sshfs.py
new file mode 100755
index 0000000..3da10df
--- /dev/null
+++ b/test/test_sshfs.py
@@ -0,0 +1,378 @@
+#!/usr/bin/env python3
+
+if __name__ == '__main__':
+ import pytest
+ import sys
+ sys.exit(pytest.main([__file__] + sys.argv[1:]))
+
+import subprocess
+import os
+import sys
+import pytest
+import stat
+import shutil
+import filecmp
+import errno
+from tempfile import NamedTemporaryFile
+from util import (wait_for_mount, umount, cleanup, base_cmdline,
+ basename, fuse_test_marker, safe_sleep)
+from os.path import join as pjoin
+
+TEST_FILE = __file__
+
+pytestmark = fuse_test_marker()
+
+with open(TEST_FILE, 'rb') as fh:
+ TEST_DATA = fh.read()
+
+def name_generator(__ctr=[0]):
+ __ctr[0] += 1
+ return 'testfile_%d' % __ctr[0]
+
+@pytest.mark.parametrize("debug", (False, True))
+@pytest.mark.parametrize("cache_timeout", (0, 1))
+def test_sshfs(tmpdir, debug, cache_timeout, capfd):
+
+ # Avoid false positives from debug messages
+ #if debug:
+ # capfd.register_output(r'^ unique: [0-9]+, error: -[0-9]+ .+$',
+ # count=0)
+
+ # Test if we can ssh into localhost without password
+ try:
+ res = subprocess.call(['ssh', '-o', 'KbdInteractiveAuthentication=no',
+ '-o', 'ChallengeResponseAuthentication=no',
+ '-o', 'PasswordAuthentication=no',
+ 'localhost', '--', 'true'], stdin=subprocess.DEVNULL,
+ timeout=10)
+ except subprocess.TimeoutExpired:
+ res = 1
+ if res != 0:
+ pytest.fail('Unable to ssh into localhost without password prompt.')
+
+ mnt_dir = str(tmpdir.mkdir('mnt'))
+ src_dir = str(tmpdir.mkdir('src'))
+
+ cmdline = base_cmdline + [ pjoin(basename, 'sshfs'),
+ '-f', 'localhost:' + src_dir, mnt_dir ]
+ if debug:
+ cmdline += [ '-o', 'sshfs_debug' ]
+
+ # SSHFS Cache
+ if cache_timeout == 0:
+ cmdline += [ '-o', 'cache=no' ]
+ else:
+ cmdline += [ '-o', 'cache_timeout=%d' % cache_timeout ]
+
+ # FUSE Cache
+ cmdline += [ '-o', 'entry_timeout=0',
+ '-o', 'attr_timeout=0' ]
+
+
+ new_env = dict(os.environ) # copy, don't modify
+
+ # Abort on warnings from glib
+ new_env['G_DEBUG'] = 'fatal-warnings'
+
+ mount_process = subprocess.Popen(cmdline, env=new_env)
+ try:
+ wait_for_mount(mount_process, mnt_dir)
+
+ tst_statvfs(mnt_dir)
+ tst_readdir(src_dir, mnt_dir)
+ tst_open_read(src_dir, mnt_dir)
+ tst_open_write(src_dir, mnt_dir)
+ tst_create(mnt_dir)
+ tst_passthrough(src_dir, mnt_dir, cache_timeout)
+ tst_mkdir(mnt_dir)
+ tst_rmdir(src_dir, mnt_dir, cache_timeout)
+ tst_unlink(src_dir, mnt_dir, cache_timeout)
+ tst_symlink(mnt_dir)
+ if os.getuid() == 0:
+ tst_chown(mnt_dir)
+
+ # SSHFS only supports one second resolution when setting
+ # file timestamps.
+ tst_utimens(mnt_dir, tol=1)
+
+ tst_link(mnt_dir)
+ tst_truncate_path(mnt_dir)
+ tst_truncate_fd(mnt_dir)
+ tst_open_unlink(mnt_dir)
+ except:
+ cleanup(mnt_dir)
+ raise
+ else:
+ umount(mount_process, mnt_dir)
+
+def tst_unlink(src_dir, mnt_dir, cache_timeout):
+ name = name_generator()
+ fullname = mnt_dir + "/" + name
+ with open(pjoin(src_dir, name), 'wb') as fh:
+ fh.write(b'hello')
+ if cache_timeout:
+ safe_sleep(cache_timeout+1)
+ assert name in os.listdir(mnt_dir)
+ os.unlink(fullname)
+ with pytest.raises(OSError) as exc_info:
+ os.stat(fullname)
+ assert exc_info.value.errno == errno.ENOENT
+ assert name not in os.listdir(mnt_dir)
+ assert name not in os.listdir(src_dir)
+
+def tst_mkdir(mnt_dir):
+ dirname = name_generator()
+ fullname = mnt_dir + "/" + dirname
+ os.mkdir(fullname)
+ fstat = os.stat(fullname)
+ assert stat.S_ISDIR(fstat.st_mode)
+ assert os.listdir(fullname) == []
+ assert fstat.st_nlink in (1,2)
+ assert dirname in os.listdir(mnt_dir)
+
+def tst_rmdir(src_dir, mnt_dir, cache_timeout):
+ name = name_generator()
+ fullname = mnt_dir + "/" + name
+ os.mkdir(pjoin(src_dir, name))
+ if cache_timeout:
+ safe_sleep(cache_timeout+1)
+ assert name in os.listdir(mnt_dir)
+ os.rmdir(fullname)
+ with pytest.raises(OSError) as exc_info:
+ os.stat(fullname)
+ assert exc_info.value.errno == errno.ENOENT
+ assert name not in os.listdir(mnt_dir)
+ assert name not in os.listdir(src_dir)
+
+def tst_symlink(mnt_dir):
+ linkname = name_generator()
+ fullname = mnt_dir + "/" + linkname
+ os.symlink("/imaginary/dest", fullname)
+ fstat = os.lstat(fullname)
+ assert stat.S_ISLNK(fstat.st_mode)
+ assert os.readlink(fullname) == "/imaginary/dest"
+ assert fstat.st_nlink == 1
+ assert linkname in os.listdir(mnt_dir)
+
+def tst_create(mnt_dir):
+ name = name_generator()
+ fullname = pjoin(mnt_dir, name)
+ with pytest.raises(OSError) as exc_info:
+ os.stat(fullname)
+ assert exc_info.value.errno == errno.ENOENT
+ assert name not in os.listdir(mnt_dir)
+
+ fd = os.open(fullname, os.O_CREAT | os.O_RDWR)
+ os.close(fd)
+
+ assert name in os.listdir(mnt_dir)
+ fstat = os.lstat(fullname)
+ assert stat.S_ISREG(fstat.st_mode)
+ assert fstat.st_nlink == 1
+ assert fstat.st_size == 0
+
+def tst_chown(mnt_dir):
+ filename = pjoin(mnt_dir, name_generator())
+ os.mkdir(filename)
+ fstat = os.lstat(filename)
+ uid = fstat.st_uid
+ gid = fstat.st_gid
+
+ uid_new = uid + 1
+ os.chown(filename, uid_new, -1)
+ fstat = os.lstat(filename)
+ assert fstat.st_uid == uid_new
+ assert fstat.st_gid == gid
+
+ gid_new = gid + 1
+ os.chown(filename, -1, gid_new)
+ fstat = os.lstat(filename)
+ assert fstat.st_uid == uid_new
+ assert fstat.st_gid == gid_new
+
+def tst_open_read(src_dir, mnt_dir):
+ name = name_generator()
+ with open(pjoin(src_dir, name), 'wb') as fh_out, \
+ open(TEST_FILE, 'rb') as fh_in:
+ shutil.copyfileobj(fh_in, fh_out)
+
+ assert filecmp.cmp(pjoin(mnt_dir, name), TEST_FILE, False)
+
+def tst_open_write(src_dir, mnt_dir):
+ name = name_generator()
+ fd = os.open(pjoin(src_dir, name),
+ os.O_CREAT | os.O_RDWR)
+ os.close(fd)
+ fullname = pjoin(mnt_dir, name)
+ with open(fullname, 'wb') as fh_out, \
+ open(TEST_FILE, 'rb') as fh_in:
+ shutil.copyfileobj(fh_in, fh_out)
+
+ assert filecmp.cmp(fullname, TEST_FILE, False)
+
+def tst_open_unlink(mnt_dir):
+ name = pjoin(mnt_dir, name_generator())
+ data1 = b'foo'
+ data2 = b'bar'
+ fullname = pjoin(mnt_dir, name)
+ with open(fullname, 'wb+', buffering=0) as fh:
+ fh.write(data1)
+ os.unlink(fullname)
+ with pytest.raises(OSError) as exc_info:
+ os.stat(fullname)
+ assert exc_info.value.errno == errno.ENOENT
+ assert name not in os.listdir(mnt_dir)
+ fh.write(data2)
+ fh.seek(0)
+ assert fh.read() == data1+data2
+
+def tst_statvfs(mnt_dir):
+ os.statvfs(mnt_dir)
+
+def tst_link(mnt_dir):
+ name1 = pjoin(mnt_dir, name_generator())
+ name2 = pjoin(mnt_dir, name_generator())
+ shutil.copyfile(TEST_FILE, name1)
+ assert filecmp.cmp(name1, TEST_FILE, False)
+
+ fstat1 = os.lstat(name1)
+ assert fstat1.st_nlink == 1
+
+ os.link(name1, name2)
+
+ fstat1 = os.lstat(name1)
+ fstat2 = os.lstat(name2)
+ for attr in ('st_mode', 'st_dev', 'st_uid', 'st_gid',
+ 'st_size', 'st_atime', 'st_mtime', 'st_ctime'):
+ assert getattr(fstat1, attr) == getattr(fstat2, attr)
+ assert os.path.basename(name2) in os.listdir(mnt_dir)
+ assert filecmp.cmp(name1, name2, False)
+
+ os.unlink(name2)
+
+ assert os.path.basename(name2) not in os.listdir(mnt_dir)
+ with pytest.raises(FileNotFoundError):
+ os.lstat(name2)
+
+ os.unlink(name1)
+
+def tst_readdir(src_dir, mnt_dir):
+ newdir = name_generator()
+ src_newdir = pjoin(src_dir, newdir)
+ mnt_newdir = pjoin(mnt_dir, newdir)
+ file_ = src_newdir + "/" + name_generator()
+ subdir = src_newdir + "/" + name_generator()
+ subfile = subdir + "/" + name_generator()
+
+ os.mkdir(src_newdir)
+ shutil.copyfile(TEST_FILE, file_)
+ os.mkdir(subdir)
+ shutil.copyfile(TEST_FILE, subfile)
+
+ listdir_is = os.listdir(mnt_newdir)
+ listdir_is.sort()
+ listdir_should = [ os.path.basename(file_), os.path.basename(subdir) ]
+ listdir_should.sort()
+ assert listdir_is == listdir_should
+
+ os.unlink(file_)
+ os.unlink(subfile)
+ os.rmdir(subdir)
+ os.rmdir(src_newdir)
+
+def tst_truncate_path(mnt_dir):
+ assert len(TEST_DATA) > 1024
+
+ filename = pjoin(mnt_dir, name_generator())
+ with open(filename, 'wb') as fh:
+ fh.write(TEST_DATA)
+
+ fstat = os.stat(filename)
+ size = fstat.st_size
+ assert size == len(TEST_DATA)
+
+ # Add zeros at the end
+ os.truncate(filename, size + 1024)
+ assert os.stat(filename).st_size == size + 1024
+ with open(filename, 'rb') as fh:
+ assert fh.read(size) == TEST_DATA
+ assert fh.read(1025) == b'\0' * 1024
+
+ # Truncate data
+ os.truncate(filename, size - 1024)
+ assert os.stat(filename).st_size == size - 1024
+ with open(filename, 'rb') as fh:
+ assert fh.read(size) == TEST_DATA[:size-1024]
+
+ os.unlink(filename)
+
+def tst_truncate_fd(mnt_dir):
+ assert len(TEST_DATA) > 1024
+ with NamedTemporaryFile('w+b', 0, dir=mnt_dir) as fh:
+ fd = fh.fileno()
+ fh.write(TEST_DATA)
+ fstat = os.fstat(fd)
+ size = fstat.st_size
+ assert size == len(TEST_DATA)
+
+ # Add zeros at the end
+ os.ftruncate(fd, size + 1024)
+ assert os.fstat(fd).st_size == size + 1024
+ fh.seek(0)
+ assert fh.read(size) == TEST_DATA
+ assert fh.read(1025) == b'\0' * 1024
+
+ # Truncate data
+ os.ftruncate(fd, size - 1024)
+ assert os.fstat(fd).st_size == size - 1024
+ fh.seek(0)
+ assert fh.read(size) == TEST_DATA[:size-1024]
+
+def tst_utimens(mnt_dir, tol=0):
+ filename = pjoin(mnt_dir, name_generator())
+ os.mkdir(filename)
+ fstat = os.lstat(filename)
+
+ atime = fstat.st_atime + 42.28
+ mtime = fstat.st_mtime - 42.23
+ if sys.version_info < (3,3):
+ os.utime(filename, (atime, mtime))
+ else:
+ atime_ns = fstat.st_atime_ns + int(42.28*1e9)
+ mtime_ns = fstat.st_mtime_ns - int(42.23*1e9)
+ os.utime(filename, None, ns=(atime_ns, mtime_ns))
+
+ fstat = os.lstat(filename)
+
+ assert abs(fstat.st_atime - atime) < tol
+ assert abs(fstat.st_mtime - mtime) < tol
+ if sys.version_info >= (3,3):
+ assert abs(fstat.st_atime_ns - atime_ns) < tol*1e9
+ assert abs(fstat.st_mtime_ns - mtime_ns) < tol*1e9
+
+def tst_passthrough(src_dir, mnt_dir, cache_timeout):
+ name = name_generator()
+ src_name = pjoin(src_dir, name)
+ mnt_name = pjoin(src_dir, name)
+ assert name not in os.listdir(src_dir)
+ assert name not in os.listdir(mnt_dir)
+ with open(src_name, 'w') as fh:
+ fh.write('Hello, world')
+ assert name in os.listdir(src_dir)
+ if cache_timeout:
+ safe_sleep(cache_timeout+1)
+ assert name in os.listdir(mnt_dir)
+ assert os.stat(src_name) == os.stat(mnt_name)
+
+ name = name_generator()
+ src_name = pjoin(src_dir, name)
+ mnt_name = pjoin(src_dir, name)
+ assert name not in os.listdir(src_dir)
+ assert name not in os.listdir(mnt_dir)
+ with open(mnt_name, 'w') as fh:
+ fh.write('Hello, world')
+ assert name in os.listdir(src_dir)
+ if cache_timeout:
+ safe_sleep(cache_timeout+1)
+ assert name in os.listdir(mnt_dir)
+ assert os.stat(src_name) == os.stat(mnt_name)
diff --git a/test/travis-build.sh b/test/travis-build.sh
new file mode 100755
index 0000000..ba04295
--- /dev/null
+++ b/test/travis-build.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+
+set -e
+
+# Disable leak checking for now, there are some issues (or false positives)
+# that we still need to fix
+export ASAN_OPTIONS="detect_leaks=0"
+
+export LSAN_OPTIONS="suppressions=$(pwd)/test/lsan_suppress.txt"
+export CC
+
+TEST_CMD="python3 -m pytest --maxfail=99 test/"
+
+# Standard build with Valgrind
+for CC in gcc gcc-6 clang; do
+ mkdir build-${CC}; cd build-${CC}
+ if [ ${CC} == 'gcc-6' ]; then
+ build_opts='-D b_lundef=false'
+ else
+ build_opts=''
+ fi
+ meson -D werror=true ${build_opts} ../
+ ninja
+
+ TEST_WITH_VALGRIND=true ${TEST_CMD}
+ cd ..
+done
+(cd build-$CC; sudo ninja install)
+
+# Sanitized build
+CC=clang
+for san in undefined address; do
+ mkdir build-${san}; cd build-${san}
+ # b_lundef=false is required to work around clang
+ # bug, cf. https://groups.google.com/forum/#!topic/mesonbuild/tgEdAXIIdC4
+ meson -D b_sanitize=${san} -D b_lundef=false -D werror=true ..
+ ninja
+ ${TEST_CMD}
+ cd ..
+done
+
+# Autotools build
+CC=gcc
+autoreconf -i
+./configure
+make
+${TEST_CMD}
+sudo make install
diff --git a/test/travis-install.sh b/test/travis-install.sh
new file mode 100755
index 0000000..d7d1f05
--- /dev/null
+++ b/test/travis-install.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -e
+
+sudo ln -svf $(which python3) /usr/bin/python3
+sudo python3 -m pip install pytest meson
+wget https://github.com/ninja-build/ninja/releases/download/v1.7.2/ninja-linux.zip
+unzip ninja-linux.zip
+chmod 755 ninja
+sudo chown root:root ninja
+sudo mv -fv ninja /usr/local/bin
+valgrind --version
+ninja --version
+meson --version
+
+# Setup ssh
+ssh-keygen -b 768 -t rsa -f ~/.ssh/id_rsa -P ''
+cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
+chmod 600 ~/.ssh/authorized_keys
+ssh -o "StrictHostKeyChecking=no" localhost echo "SSH connection succeeded"
+
diff --git a/test/util.py b/test/util.py
new file mode 100644
index 0000000..f2a485c
--- /dev/null
+++ b/test/util.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+import subprocess
+import pytest
+import os
+import stat
+import time
+from os.path import join as pjoin
+
+basename = pjoin(os.path.dirname(__file__), '..')
+
+def wait_for_mount(mount_process, mnt_dir,
+ test_fn=os.path.ismount):
+ elapsed = 0
+ while elapsed < 30:
+ if test_fn(mnt_dir):
+ return True
+ if mount_process.poll() is not None:
+ pytest.fail('file system process terminated prematurely')
+ time.sleep(0.1)
+ elapsed += 0.1
+ pytest.fail("mountpoint failed to come up")
+
+def cleanup(mnt_dir):
+ subprocess.call(['fusermount', '-z', '-u', mnt_dir],
+ stdout=subprocess.DEVNULL,
+ stderr=subprocess.STDOUT)
+
+def umount(mount_process, mnt_dir):
+ subprocess.check_call(['fusermount', '-z', '-u', mnt_dir ])
+ assert not os.path.ismount(mnt_dir)
+
+ # Give mount process a little while to terminate. Popen.wait(timeout)
+ # was only added in 3.3...
+ elapsed = 0
+ while elapsed < 30:
+ code = mount_process.poll()
+ if code is not None:
+ if code == 0:
+ return
+ pytest.fail('file system process terminated with code %s' % (code,))
+ time.sleep(0.1)
+ elapsed += 0.1
+ pytest.fail('mount process did not terminate')
+
+def safe_sleep(secs):
+ '''Like time.sleep(), but sleep for at least *secs*
+
+ `time.sleep` may sleep less than the given period if a signal is
+ received. This function ensures that we sleep for at least the
+ desired time.
+ '''
+
+ now = time.time()
+ end = now + secs
+ while now < end:
+ time.sleep(end - now)
+ now = time.time()
+
+def fuse_test_marker():
+ '''Return a pytest.marker that indicates FUSE availability
+
+ If system/user/environment does not support FUSE, return
+ a `pytest.mark.skip` object with more details. If FUSE is
+ supported, return `pytest.mark.uses_fuse()`.
+ '''
+
+ skip = lambda x: pytest.mark.skip(reason=x)
+
+ with subprocess.Popen(['which', 'fusermount'], stdout=subprocess.PIPE,
+ universal_newlines=True) as which:
+ fusermount_path = which.communicate()[0].strip()
+
+ if not fusermount_path or which.returncode != 0:
+ return skip("Can't find fusermount executable")
+
+ if not os.path.exists('/dev/fuse'):
+ return skip("FUSE kernel module does not seem to be loaded")
+
+ if os.getuid() == 0:
+ return pytest.mark.uses_fuse()
+
+ mode = os.stat(fusermount_path).st_mode
+ if mode & stat.S_ISUID == 0:
+ return skip('fusermount executable not setuid, and we are not root.')
+
+ try:
+ fd = os.open('/dev/fuse', os.O_RDWR)
+ except OSError as exc:
+ return skip('Unable to open /dev/fuse: %s' % exc.strerror)
+ else:
+ os.close(fd)
+
+ return pytest.mark.uses_fuse()
+
+# Use valgrind if requested
+if os.environ.get('TEST_WITH_VALGRIND', 'no').lower().strip() \
+ not in ('no', 'false', '0'):
+ base_cmdline = [ 'valgrind', '-q', '--' ]
+else:
+ base_cmdline = []
diff --git a/test/wrong_command.c b/test/wrong_command.c
new file mode 100644
index 0000000..8366a98
--- /dev/null
+++ b/test/wrong_command.c
@@ -0,0 +1,9 @@
+#include <stdio.h>
+
+int main(void) {
+ fprintf(stderr, "\x1B[31m\e[1m"
+ "This is not the command you are looking for.\n"
+ "You probably want to run 'python3 -m pytest test/' instead"
+ "\e[0m\n");
+ return 1;
+}