diff options
-rw-r--r-- | .dir-locals.el | 25 | ||||
-rw-r--r-- | .gitignore | 3 | ||||
-rw-r--r-- | .travis.yml | 21 | ||||
-rw-r--r-- | ChangeLog.rst (renamed from ChangeLog) | 18 | ||||
-rw-r--r-- | Makefile.am | 22 | ||||
-rw-r--r-- | README.md | 72 | ||||
-rw-r--r-- | README.rst | 132 | ||||
-rw-r--r-- | configure.ac | 27 | ||||
-rw-r--r-- | meson.build | 64 | ||||
-rw-r--r-- | sshfs.1.in | 4 | ||||
-rw-r--r-- | sshfs.c | 170 | ||||
-rw-r--r-- | sshnodelay.c | 59 | ||||
-rw-r--r-- | test/.gitignore | 1 | ||||
-rw-r--r-- | test/conftest.py | 89 | ||||
-rw-r--r-- | test/lsan_suppress.txt | 11 | ||||
-rw-r--r-- | test/meson.build | 11 | ||||
-rw-r--r-- | test/pytest.ini | 2 | ||||
-rwxr-xr-x | test/test_sshfs.py | 378 | ||||
-rwxr-xr-x | test/travis-build.sh | 48 | ||||
-rwxr-xr-x | test/travis-install.sh | 21 | ||||
-rw-r--r-- | test/util.py | 100 | ||||
-rw-r--r-- | test/wrong_command.c | 9 |
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)))))) @@ -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') + @@ -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 @@ -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; +} |