Subject: [PATCH] [FEAT KRN1015] hmcdrvfs: SE/HMC file access
From: Michael Holzheu <holzheu@linux.vnet.ibm.com>

Summary:     hmcdrvfs: SE/HMC file access
Description: Use the hmcdrvfs command to provide (read-only) access to
             files stored on a (assigned) DVD inserted in a HMC drive.
             This command creates a FUSE.HMCDRVFS filesystem at the
             specified mount point. The feature works with either the
             LPAR or the z/VM hypervisor. But note especially for z/VM
             that the DVD must be assigned to the associated system
             image (LPAR).

             Distributor info: We suggest a subpackage "s390-tools-hmcdrvfs"
             that is installed by default.
Upstream-ID: -
Problem-ID:  KRN1015

Signed-off-by: Michael Holzheu <holzheu@linux.vnet.ibm.com>
---
 Makefile            |    2 
 common.mak          |   22 
 hmcdrvfs/Makefile   |   54 +
 hmcdrvfs/hmcdrvfs.1 |  415 +++++++++++++
 hmcdrvfs/hmcdrvfs.c | 1572 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 hmcdrvfs/lshmc      |   90 ++
 hmcdrvfs/lshmc.8    |  104 +++
 7 files changed, 2258 insertions(+), 1 deletion(-)

--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,7 @@ SUB_DIRS = $(LIB_DIRS) zipl zdump fdasd
 	   tape390 osasnmpd qetharp ip_watcher qethconf scripts zconf \
 	   vmconvert vmcp man mon_tools dasdinfo vmur cpuplugd ipl_tools \
 	   ziomon iucvterm hyptop cmsfs-fuse qethqoat zfcpdump zdsfs systemd \
-	   cpumf
+	   cpumf hmcdrvfs
 
 all: subdirs_make
 
--- a/common.mak
+++ b/common.mak
@@ -48,6 +48,28 @@ ifeq ("${W}","1")
 else
 	WARNFLAGS = -W -Wall
 endif
+
+#
+# Check for build dependency
+#
+# $1: Name of tool or feature that requires dependency
+# $2: Name of include file to check
+# $3: Name of required devel package
+# $4: Option to skip build (e.g. HAVE_FUSE=0)
+#
+check_dep=\
+printf "\#include <%s>" $2 | $(CROSS_COMPILE)gcc $(filter-out --coverage, $(CFLAGS)) $(CPPFLAGS) -c -o /dev/null -xc - 2> /dev/null; \
+if [ $$? != 0 ]; \
+then \
+	printf "  REQCHK  %s (%s)\n" $1 $2; \
+	printf "********************************************************************************\n" >&2; \
+	printf "* Missing build requirement for: %-45s *\n" $1 >&2; \
+	printf "* Install package..............: %-45s *\n" $3 >&2; \
+	printf "* You can skip build with......: make %-40s *\n" $4 >&2; \
+	printf "********************************************************************************\n" >&2; \
+	exit 1; \
+fi
+
 # Support alternate install root
 INSTROOT        =
 USRSBINDIR      = $(INSTROOT)/usr/sbin
--- /dev/null
+++ b/hmcdrvfs/Makefile
@@ -0,0 +1,54 @@
+#!/usr/bin/make -f
+
+include ../common.mak
+
+ifeq (${HAVE_FUSE},0)
+
+all:
+	$(SKIP) HAVE_FUSE=0
+
+install:
+	$(SKIP) HAVE_FUSE=0
+
+else
+
+check_dep:
+	$(call check_dep, \
+		"hmcdrvfs", \
+		"fuse.h", \
+		"fuse-devel", \
+		"HAVE_FUSE=0")
+
+CPPFLAGS += -I../include
+CFLAGS += -DFUSE_USE_VERSION=26 -D_GNU_SOURCE -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -I/usr/include/fuse
+LDLIBS += -lfuse -lpthread -lrt -ldl -lm
+
+OBJECTS = hmcdrvfs.o
+
+
+all: check_dep hmcdrvfs
+
+$(OBJECTS): Makefile
+
+hmcdrvfs: $(OBJECTS)
+
+install: all install-scripts
+	$(INSTALL) -g $(GROUP) -o $(OWNER) -m 755 hmcdrvfs $(USRBINDIR)
+	$(INSTALL) -g $(GROUP) -o $(OWNER) -m 644 hmcdrvfs.1 $(MANDIR)/man1
+	$(INSTALL) -g $(GROUP) -o $(OWNER) -m 644 lshmc.8 $(MANDIR)/man8
+
+install-scripts: lshmc
+	@for i in $^; do \
+		cat $$i | \
+		sed -e 's+%S390_TOOLS_VERSION%+$(S390_TOOLS_RELEASE)+' \
+		>$(USRSBINDIR)/$$i; \
+		chown $(OWNER).$(GROUP) $(USRSBINDIR)/$$i; \
+		chmod 755 $(USRSBINDIR)/$$i; \
+	done
+
+endif
+
+clean:
+	rm -f hmcdrvfs *.o
+
+.PHONY: all install install-scripts clean check_dep
--- /dev/null
+++ b/hmcdrvfs/hmcdrvfs.1
@@ -0,0 +1,415 @@
+.\" Copyright IBM Corp. 2015
+.\"
+.TH HMCDRVFS 1 "Mar 2015" "s390-tools"
+.\" disable hyphenation for words below
+.hw hmcdrv hmcdrvfs fuse DIAGNOSTICS
+
+.SH NAME
+hmcdrvfs \- mount a
+.SM FUSE
+file system for remote access to a
+.SM DVD
+in a
+.SM HMC DVD
+drive
+
+.SH SYNOPSIS
+mounting:
+.nf
+.RS
+\fBhmcdrvfs\fP MOUNTPOINT [OPTIONS]
+.RE
+.fi
+.PP
+unmounting:
+.nf
+.RS
+\fBfusermount\fP -u MOUNTPOINT
+.RE
+.fi
+
+.SH DESCRIPTION
+Use the \fBhmcdrvfs\fP command for read-only access to files
+on a
+.SM DVD
+in the
+.SM DVD
+drive of an
+.SM HMC\c
+\&.
+On the
+.SM HMC\c
+, the
+.SM DVD
+must be assigned to the
+.SM LPAR
+within which your Linux instance runs.  For
+.SM z/VM
+guests, the
+.SM DVD
+must be
+assigned to the
+.SM LPAR
+where the
+.SM z/VM
+hypervisor runs.
+With the
+.SM DVD
+assigned to your Linux instance, this command creates a
+.SM FUSE.HMCDRVFS
+file system with the content of the
+.SM DVD
+at the specified mountpoint.
+
+.SH OPTIONS
+.SS "General mount options"
+.TP
+\fB-o\fP opt[,opt...]
+.SM FUSE
+or mount command options;
+for the
+.SM FUSE
+options see below, for mount options
+see \fBmount(8)\fP
+.TP
+\fB-h\fP, \fB--help\fP
+print usage information, then exit (see also DIAGNOSTICS)
+.TP
+\fB-v\fP, \fB--version\fP
+print version information, then exit
+
+.SS "Specific FUSE.HMCDRVFS options"
+.TP
+.BI "-o hmclang=" LANG
+specify the language setting on the
+.SM HMC\c
+; for valid values, see \fBlocale(1)\fP;
+for more information, see DIAGNOSTICS and EXAMPLES
+.TP
+.BI "-o hmctz=" TZ
+specify the time zone setting on the
+.SM HMC\c
+; for valid values, see \fBtzset(3)\fP;
+for more information, see DIAGNOSTICS and EXAMPLES
+
+.SS "Applicable FUSE options (version 2.6)"
+.TP
+\fB-d\fP, \fB-o debug\fP
+enable debug output (implies \fB-f\fP)
+.TP
+.B -f
+foreground operation
+.TP
+.B -s
+disable multi-threaded operation
+.TP
+.B -o allow_other
+allow access by other users
+.TP
+.B -o allow_root
+allow access by root
+.TP
+.B -o nonempty
+allow mounts over non-empty file/dir
+.TP
+.B -o default_permissions
+enable permission checking by kernel
+.TP
+.BI "-o fsname=" NAME
+set file system name
+.TP
+.BI "-o subtype=" TYPE
+set file system type
+.TP
+.BI "-o max_read=" N
+set maximum size of read requests
+.TP
+.B -o direct_io
+use direct I/O
+.TP
+.B -o kernel_cache
+cache files in kernel
+.TP
+\fB-o \fP[\fBno\fP]\fBauto_cache\fP
+enable caching based on modification times
+.TP
+.BI "-o umask=" M
+set file permissions (octal)
+.TP
+.BI "-o uid=" N
+set file owner
+.TP
+.BI "-o gid=" N
+set file group
+.TP
+.BI "-o entry_timeout=" T
+cache timeout for names (default: 1.0 second)
+.TP
+.BI "-o attr_timeout=" T
+cache timeout for attributes (default: 1.0 second)
+.TP
+.BI "-o ac_attr_timeout=" T
+auto cache timeout for attributes (default: \fBattr_timeout\fP)
+.TP
+.BI "-o max_readahead=" N
+set maximum readahead
+.TP
+.B -o async_read
+perform reads asynchronously (default)
+.TP
+.B -o sync_read
+perform reads synchronously
+.TP
+.B -o no_remote_lock
+disable remote file locking
+.TP
+.B -o intr
+allow requests to be interrupted
+.TP
+.BI "-o intr_signal=" NUM
+signal to send on interrupt
+
+
+.SH EXAMPLES
+To mount the
+.SM HMC
+drive
+.SM DVD
+at \fI/mnt/hmc\fP without any special options use:
+.PP
+.nf
+.RS
+.B $ hmcdrvfs /mnt/hmc
+.RE
+.fi
+.PP
+In case the kernel module \fIhmcdrv\fP was not loaded in advance use:
+.PP
+.nf
+.RS
+.B # modprobe hmcdrv
+.B # hmcdrvfs /mnt/hmc
+.RE
+.fi
+.PP
+To translate the UID and GID of files on the
+.SM HMC
+drive
+.SM DVD
+to your system users and groups along with overriding the permissions use for
+example:
+.PP
+.nf
+.RS
+.B $ hmcdrvfs /mnt/hmc -o uid=500 -o gid=1000 -o umask=0337
+.RE
+.fi
+.PP
+To speed up transfer rates to frequently accessed directories use
+the cache timeout option:
+.PP
+.nf
+.RS
+.B $ hmcdrvfs /mnt/hmc -o entry_timeout=60
+.RE
+.PP
+.fi
+If the
+.SM HMC
+is in a different timezone and is configured for a
+different language use, for example:
+.PP
+.nf
+.RS
+.B $ hmcdrvfs /mnt/hmc -o hmclang=de_DE -o hmctz=Europe/Berlin
+.RE
+.fi
+.PP
+or disregarding any daylight saving time, specifying hours west of
+Prime Meridian (UTC):
+.PP
+.nf
+.RS
+.B $ hmcdrvfs /mnt/hmc -o hmclang=de_DE -o hmctz="GMT-1"
+.RE
+.fi
+.PP
+To unmount the
+.SM HMC
+drive
+.SM DVD
+mounted on \fI/mnt/hmc\fP use:
+.PP
+.nf
+.RS
+.B $ fusermount -u /mnt/hmc
+.RE
+.fi
+.PP
+It is also possible to mount by using \fBfstab(5)\fP.  Specify
+the mount point and associated mount options in \fI/etc/fstab\fP,
+for example:
+.PP
+.nf
+.RS
+hmcdrvfs /mnt/hmc fuse ro,noatime,allow_other,uid=500,gid=1000
+.RE
+.fi
+.PP
+You can then mount the file system with this command:
+.PP
+.nf
+.RS
+.B # mount /mnt/hmc
+.RE
+.fi
+
+.SH FILES
+Some general options about mount policy can be set in the
+\fI/etc/\:fuse.conf\fP file.  These options are:
+.TP
+.BI "mount_max=" NNN
+Set the maximum number of
+.SM FUSE
+mounts allowed to non-root users.
+The default is 1000.
+.TP
+.B user_allow_other
+Allow non-root users to specify the \fBallow_other\fP or \fBallow_root\fP
+mount options.
+
+.SH DIAGNOSTICS
+.IP 1. 3
+The
+.SM FUSE.HMCDRVFS
+file system needs access to device node \fI/dev/\:hmcdrv\fP.  This node is
+created automatically when the \fIhmcdrv\fP kernel module is loaded (see Linux
+kernel configuration option
+.SM CONFIG_HMC_DRV\c
+).  The user process that runs the \fBhmcdrvfs\fP command must have
+sufficient privileges to read from and write to node \fI/dev/\:hmcdrv\fP.
+Use the commands \fBchown(1)\fP, \fBchgrp(1)\fP and/or \fBchmod(1)\fP on
+node \fI/dev/\:hmcdrv\fP to ensure this condition.
+
+.IP 2. 3
+In addition to the required permissions, there are some environmental
+requirements:
+.RS
+.IP - 2
+In a
+.SM z/VM
+environment, the
+.SM z/VM
+guest virtual machine must have at least privilege class B.
+.IP - 2
+For Linux in
+.SM LPAR
+mode, the
+.SM LPAR
+activation profile must allow issuing
+.SM SCLP
+requests.
+.IP - 2
+On the
+.SM HMC\c
+, the
+.SM DVD
+must be assigned to the associated system image (use menu \fIAccess Removable
+Media\fP).
+.RE
+
+.IP 3. 3
+The
+.SM FUSE.HMCDRVFS
+file system maintains a file attributes cache, with an aging timeout.  This
+timeout is related to the \fBentry_timeout\fP and \fBattr_timeout\fP
+.SM FUSE
+options.  Its value exceeds the greater of the two, \fBentry_timeout\fP and
+\fBattr_timeout\fP, by 30 - 60 seconds.  This timeout affects the performance
+of the
+.SM FUSE.HMCDRVFS
+file system.
+.RE
+
+.IP 4. 3
+Different language and time zone settings on the
+.SM HMC
+and your Linux instance can result in incorrect file modification information.
+.RS
+.IP - 2
+Use the \fBhmclang\fP specific
+.SM FUSE.HMCDRVFS
+option if the language settings
+of the
+.SM HMC
+and your Linux instance do not match.  Correctly setting this option prevents
+incorrect file modification dates in the file details.  Omitting the
+\fBhmclang\fP option can result in incorrect dates of the form 01-01-YYYY,
+with misleading values for the day and month.
+.IP - 2
+Use the \fBhmctz\fP specific
+.SM FUSE.HMCDRVFS
+option if the time zone settings
+of the
+.SM HMC
+and your Linux instance do not match.  Correctly setting this option prevents
+incorrect file modification times in the file details.  Omitting \fBhmctz\fP,
+an incorrect specification, or a missing time zone description file can result
+in modification times that differ up to 25 hours from the correct times.
+Specifications that cannot be interpreted result in GMT/UTC being set.  If the
+.SM HMC
+is set to GMT/UTC, specify \fBhmctz=""\fP.
+.RE
+
+.IP 5. 3
+The following generic mount options from \fBmount(8)\fP are ignored:
+.RS 3
+.TP
+\fB-w\fP, \fB--rw\fP, \fB-o rw\fP
+mount the file system read/write
+.TP
+.B -o atime
+update inode access times on this file system
+.RE
+
+.IP 6. 3
+The following
+.SM FUSE
+mount options from \fBmount.fuse(8)\fP are
+ignored:
+.RS 3
+.TP
+.B -o hard_remove
+immediate removal
+.TP
+.B -o negative_timeout
+cache timeout for deleted names
+.TP
+.BI "-o max_write=" N
+set maximum size of write requests
+.TP
+.B -o atomic_o_trunc
+enable atomic open\+truncate support
+.TP
+.B -o big_writes
+enable larger than 4kB writes
+.TP
+.BI "-o subdir=" DIR
+prepend this directory to all paths
+.TP
+.B -o use_ino
+let file system set inode numbers
+.TP
+.B -o readdir_ino
+try to fill in d_ino in readdir
+.RE
+
+.SH SEE ALSO
+.BR tzset(3),
+.BR locale(1),
+.BR fusermount(1),
+.BR fstab(5),
+.BR mount(8),
+.B mount.fuse(8)
+.BR lshmc(8),
+and Linux on System z: Device Drivers, Features and Commands
--- /dev/null
+++ b/hmcdrvfs/hmcdrvfs.c
@@ -0,0 +1,1572 @@
+/*
+ *    HMC Drive FUSE Filesystem (FUSE.HMCDRVFS)
+ *
+ *    Copyright IBM Corp. 2015
+ *    Author(s): Ralf Hoppe (rhoppe@de.ibm.com)
+ *
+ */
+
+#include <fuse.h>
+
+#include <limits.h>
+#include <inttypes.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include <strings.h>
+#include <locale.h>
+#include <langinfo.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <ctype.h>
+#include <time.h>
+#include <libgen.h>
+#include <syslog.h>
+#include <pthread.h>
+#include "zt_common.h"
+
+#define HMCDRV_FUSE_LOGNAME	"hmcdrvfs" /* log prefix */
+#define HMCDRV_FUSE_LOGHEAD	HMCDRV_FUSE_LOGNAME ": " /* log header */
+#define HMCDRV_FUSE_FTPDEV	"/dev/hmcdrv" /* DIAG/SCLP FTP device */
+#define HMCDRV_FUSE_MAXPATH	192 /* max. path length (including EOS) */
+#define HMCDRV_FUSE_MAXFTPLEN	3 /* max. length of FTP cmd (dir/nls/get) */
+#define HMCDRV_FUSE_OFSPATH	(HMCDRV_FUSE_MAXFTPLEN + 1) /* path in cmd */
+#define HMCDRV_FUSE_MAXCMDLEN	(HMCDRV_FUSE_MAXPATH + HMCDRV_FUSE_OFSPATH)
+
+/* internal increase (margin/offset) of cache timeout with respect to FUSE
+ * options 'attr_timeout' and/or 'entry_timeout' (having default 1s)
+ */
+#define HMCDRV_FUSE_CACHE_TMOFS 30
+
+/* size of file name/attributes cache (should be a prime number)
+ */
+#define HMCDRV_FUSE_CACHE_SIZE	2053
+
+/* number of hash table lines inspected in a single garbage loop
+ */
+#define HMCDRV_FUSE_GARBAGE_MAX 128
+
+/* max. size of FTP 'dir <path>' output chunk size
+ */
+#define HMCDRV_FUSE_DIRBUF_SIZE (64 * 1024)
+
+/* max. string length in FTP 'dir <path>' output buffer
+ */
+#define HMCDRV_FUSE_DIRBUF_LEN	(HMCDRV_FUSE_DIRBUF_SIZE - 1)
+
+
+/* pointer to path (token) in FTP command string associated with file 'fp'
+ */
+#define HMCDRV_FUSE_PATH(fp)	((fp)->ftpcmd + HMCDRV_FUSE_OFSPATH)
+
+
+
+/*
+ * DEBUG log macro (requires -DDEBUG, else empty)
+ */
+#ifdef DEBUG
+#define HMCDRV_FUSE_DBGLOG(fmt, ...) syslog(LOG_ERR, (fmt), ##__VA_ARGS__)
+#else
+#define HMCDRV_FUSE_DBGLOG(fmt, ...) do {} while (0)
+#endif /* DEBUG */
+
+
+/*
+ * log output to 'stderr' and optionally (if from a fork'ed process/thread)
+ * to syslog
+ */
+#define HMCDRV_FUSE_LOG(sev, fmt, ...)				\
+	hmcdrv_fuse_log((sev), HMCDRV_FUSE_LOGHEAD "%s: " fmt "\n",	\
+			hmcdrv_fuse_logsevname[sev], ##__VA_ARGS__)
+
+
+#define HMCDRV_FUSE_STR(x)	#x
+#define HMCDRV_FUSE_STRINGER(x)	HMCDRV_FUSE_STR(x)
+#define HMCDRV_FUSE_RELEASE	HMCDRV_FUSE_STRINGER(S390_TOOLS_RELEASE)
+
+#define HMCDRV_FUSE_OPT(t, p, v) { t, offsetof(struct hmcdrv_fuse_opt, p), v }
+
+
+/*
+ * option key IDs (used on command line parsing)
+ */
+enum {
+	HMCDRV_FUSE_OPTKEY_VERSION, /* "-v", "--version" */
+	HMCDRV_FUSE_OPTKEY_HELP, /* "-h", "--help" */
+	HMCDRV_FUSE_OPTKEY_RO, /* read-only, e.g. "-o ro" or "-r" */
+	HMCDRV_FUSE_OPTKEY_RW, /* option associated with -wr (ignored) */
+	HMCDRV_FUSE_OPTKEY_NATM, /* option "noatime" */
+	HMCDRV_FUSE_OPTKEY_CTMO, /* cache timeout (entry/attr_timeout) */
+	HMCDRV_FUSE_OPTKEY_NSUP, /* unsupported options, e.g. "subdir" */
+};
+
+
+/*
+ * internal used FTP command IDs
+ */
+enum hmcdrv_fuse_cmdid {
+	HMCDRV_FUSE_CMDID_NLS = 0,
+	HMCDRV_FUSE_CMDID_DIR = 1,
+	HMCDRV_FUSE_CMDID_GET = 2,
+	HMCDRV_FUSE_CMDID_SIZE
+};
+
+
+/*
+ * options/parameters to lookup at start
+ */
+struct hmcdrv_fuse_opt {
+	unsigned int ro:1; /* option(s) "ro", "-r", etc. */
+	unsigned int noatime:1; /* option "noatime" */
+
+	char *hmctz; /* HMC timezone option "-o hmctz=TZ" */
+	char *hmclang; /* HMC locale option "-o hmclang=LANG" */
+};
+
+
+/*
+ * HMC drive FUSE context
+ */
+struct hmcdrv_fuse_context {
+	struct hmcdrv_fuse_opt opt; /* some saved startup options */
+	int fd; /* file descriptor of FTP device HMCDRV_FUSE_FTPDEV */
+	char *path; /* mount point path (root) */
+	struct stat st; /* mount point stat attributes */
+	time_t ctmo; /* cache timeout (derived from entry/attr_timeout) */
+	pthread_t tid; /* cache aging thread ID */
+	pthread_mutex_t mutex; /* cache access mutex */
+	pid_t pid; /* PID of main() */
+	char *abmon[12]; /* abbreviated month name of HMC locale */
+	int ablen[12]; /* length of each abbreviated month name */
+};
+
+
+/*
+ * SE/HMC drive FUSE file private data
+ */
+struct hmcdrv_fuse_file {
+	struct hmcdrv_fuse_file *next; /* collision list (equal hash) */
+	struct stat st; /* stat structure of this file */
+	char *symlnk; /* pointer to path name of symlink target (S_IFLNK) */
+	time_t timeout; /* cache timeout for this file */
+	size_t cmdlen; /* length of FTP command + path */
+	char ftpcmd[0]; /* FTP command + path (max HMCDRV_FUSE_MAXCMDLEN) */
+};
+
+
+/*
+ * all file attributes accumulated from interpreting tokens/fields of 'dir'
+ * command listing
+ */
+struct hmcdrv_fuse_attr {
+	struct stat *st; /* pointer to stat structure */
+	struct tm ftm; /* local time */
+	char *fname; /* (base-) name */
+};
+
+
+/*
+ * prototype (forward) declaration for hmcdrv_cache_dir()
+ */
+static struct hmcdrv_fuse_file *hmcdrv_file_get(const char *path);
+
+
+/*
+ * log severity strings
+ */
+static const char *const hmcdrv_fuse_logsevname[] = {
+
+	"Emergency", /* (0) LOG_EMERG */
+	"Alert", /* (1) LOG_ALERT */
+	"Critical", /* (2) LOG_CRIT */
+	"Error", /* (3) LOG_ERR */
+	"Warning", /* (4) LOG_WARNING */
+	"Notice", /* (5) LOG_NOTICE */
+	"Info", /* (6) LOG_INFO */
+	"Debug" /* (7) DEBUG */
+};
+
+
+/*
+ * file attributes cache
+ */
+static struct hmcdrv_fuse_file *hmcdrv_fuse_cache[HMCDRV_FUSE_CACHE_SIZE];
+
+
+/*
+ * context
+ */
+static struct hmcdrv_fuse_context hmcdrv_ctx = {
+	.fd = -1,
+	.ctmo = 1 + HMCDRV_FUSE_CACHE_TMOFS,
+	.mutex = PTHREAD_MUTEX_INITIALIZER,
+};
+
+
+/*
+ * log output from hmcdrvfs daemon
+ */
+static void hmcdrv_fuse_log(int sev, const char *fmt, ...)
+{
+	va_list argp;
+
+	va_start(argp, fmt);
+	vfprintf(stderr, fmt, argp); /* always to stderr */
+	va_end(argp);
+
+	if (hmcdrv_ctx.pid != getpid()) {
+		struct fuse_context *fctx = fuse_get_context();
+
+		if ((fctx != NULL) && (hmcdrv_ctx.pid != fctx->pid)) {
+			va_start(argp, fmt);
+			vsyslog(sev,
+				fmt + (sizeof(HMCDRV_FUSE_LOGHEAD) - 1),
+				argp);
+			va_end(argp);
+		}
+	}
+}
+
+
+/*
+ * calculate a hash value from a file path
+ */
+static unsigned int hmcdrv_hash_path(const char *path)
+{
+	unsigned int hash = ~0;
+	int len = strlen(path);
+
+	while (len > 0) {
+		hash += (unsigned) *path;
+
+		++path;
+		--len;
+	}
+
+	hash %= HMCDRV_FUSE_CACHE_SIZE;
+	return hash;
+}
+
+
+/*
+ * restart of cache entry aging time
+ */
+static void hmcdrv_cache_trestart(struct hmcdrv_fuse_file *fp)
+{
+	fp->timeout = time(NULL) + hmcdrv_ctx.ctmo;
+}
+
+
+/*
+ * update or free symlink information
+ */
+static void hmcdrv_cache_symlink(struct hmcdrv_fuse_file *fp,
+				const char *symlink)
+{
+	if (fp->symlnk != NULL) {
+		free(fp->symlnk);
+		fp->symlnk = NULL;
+	}
+
+	if ((symlink != NULL) && (symlink[0] != '\0')) {
+		fp->symlnk = malloc(HMCDRV_FUSE_MAXPATH);
+
+		if (fp->symlnk != NULL) {
+			strncpy(fp->symlnk, symlink, HMCDRV_FUSE_MAXPATH);
+			fp->symlnk[HMCDRV_FUSE_MAXPATH - 1] = '\0';
+		}
+	}
+}
+
+
+
+/*
+ * refresh the attributes of file/directory 'path'
+ * note: if there is no cache entry for this file, then create one
+ */
+static void hmcdrv_cache_refresh(const char *path, const struct stat *st,
+				const char *symlink)
+{
+	struct hmcdrv_fuse_file **pbase; /* storage location of fp */
+	struct hmcdrv_fuse_file *fp;
+	unsigned int index;
+
+	int pathlen = strlen(path);
+
+	if ((pathlen == 0) || (pathlen >= HMCDRV_FUSE_MAXPATH)) {
+		HMCDRV_FUSE_LOG(LOG_WARNING,
+				"Invalid path length for '%s'", path);
+		return;
+	}
+
+	index = hmcdrv_hash_path(path);
+	pbase = &hmcdrv_fuse_cache[index];
+	fp = *pbase;
+
+	while (fp != NULL) {
+		if (strcmp(HMCDRV_FUSE_PATH(fp), path) == 0) {
+			fp->st = *st;		   /* update file info */
+			hmcdrv_cache_symlink(fp, symlink);
+			hmcdrv_cache_trestart(fp); /* restart aging */
+			return;
+		}
+
+		pbase = &fp->next;
+		fp = fp->next;
+	}
+
+	/* entry does not exist so far - create new one
+	 */
+	fp = malloc((offsetof(struct hmcdrv_fuse_file, ftpcmd) +
+		     HMCDRV_FUSE_OFSPATH + 1) + pathlen);
+
+	if (fp == NULL) {
+		HMCDRV_FUSE_LOG(LOG_ERR, "Out of memory, %ld bytes",
+				(offsetof(struct hmcdrv_fuse_file, ftpcmd) +
+				 HMCDRV_FUSE_OFSPATH + 1) + pathlen);
+	} else {
+		*pbase = fp;
+		fp->cmdlen = pathlen + HMCDRV_FUSE_OFSPATH;
+		fp->st = *st;
+		memcpy(HMCDRV_FUSE_PATH(fp), path, pathlen + 1);
+		fp->symlnk = NULL;
+		hmcdrv_cache_symlink(fp, symlink);
+		hmcdrv_cache_trestart(fp);
+		fp->next = NULL;
+	}
+}
+
+
+/*
+ * search for a file/directory path in cache
+ */
+static struct hmcdrv_fuse_file *hmcdrv_cache_find(const char *path)
+{
+	unsigned index = hmcdrv_hash_path(path);
+	struct hmcdrv_fuse_file *fp = hmcdrv_fuse_cache[index];
+
+	while (fp != NULL) {
+		if (strcmp(HMCDRV_FUSE_PATH(fp), path) == 0) {
+			hmcdrv_cache_trestart(fp);
+			return fp;
+		}
+
+		fp = fp->next;
+	}
+
+	return NULL;
+}
+
+
+/*
+ * check for aging timeout of cache entry at index
+ */
+static void hmcdrv_cache_expire(unsigned index, time_t now)
+{
+	struct hmcdrv_fuse_file **pbase; /* storage location of fp */
+	struct hmcdrv_fuse_file *fp, *next;
+
+	pbase = &hmcdrv_fuse_cache[index];
+	fp = *pbase;
+
+	/* iterate the collision list */
+	while (fp != NULL) {
+		next = fp->next;
+
+		if ((fp->timeout <= now) &&
+		    (strcmp(HMCDRV_FUSE_PATH(fp), "/") != 0)) {
+			hmcdrv_cache_symlink(fp, NULL);
+			*pbase = next;
+			free(fp);
+		} else {
+			pbase = &fp->next;
+		}
+
+		fp = next;
+	}
+}
+
+
+/*
+ * cache expiry handler thread
+ */
+static void *hmcdrv_cache_aging(void *UNUSED(arg))
+{
+	unsigned int cnt;
+	time_t now;
+
+	unsigned index = 0;
+
+	while (1) {
+		sleep(1);
+
+		pthread_mutex_lock(&hmcdrv_ctx.mutex);
+		now = time(NULL);
+
+		/* each second scan a number of cache entries
+		 */
+		for (cnt = 0; cnt < HMCDRV_FUSE_GARBAGE_MAX; ++cnt) {
+
+			hmcdrv_cache_expire(index, now);
+
+			if (++index == HMCDRV_FUSE_CACHE_SIZE)
+				index = 0;
+		}
+
+		pthread_mutex_unlock(&hmcdrv_ctx.mutex);
+	}
+
+	return NULL;
+}
+
+
+/*
+ * convert a FTP command ID into a string
+ *
+ * Note: If a command string is shorter than HMCDRV_FUSE_MAXFTPLEN then
+ *       define it right-aligned (prefixed with spaces) in table 'cmdstr'.
+ */
+static void hmcdrv_ftp_str(enum hmcdrv_fuse_cmdid cmd, char *ftp)
+{
+	static const char *cmdstr[HMCDRV_FUSE_CMDID_SIZE] = {
+
+#if (HMCDRV_FUSE_MAXFTPLEN != 3)
+#error The length of strings in cmdstr[] must match HMCDRV_FUSE_MAXFTPLEN
+#endif
+		"nls", /* HMCDRV_FUSE_CMDID_NLS */
+		"dir", /* HMCDRV_FUSE_CMDID_DIR */
+		"get"  /* HMCDRV_FUSE_CMDID_GET */
+	};
+
+	strcpy(ftp, cmdstr[cmd]);
+	ftp[HMCDRV_FUSE_MAXFTPLEN] = ' '; /* overwrite '\0' from strcpy() */
+}
+
+
+/*
+ * FTP command execution via kernel device
+ */
+static ssize_t hmcdrv_ftp_transfer(struct hmcdrv_fuse_file *fp, char *buf,
+				   size_t len, off_t offset)
+{
+	static off_t current_offset;
+	static char last_ftpcmd[HMCDRV_FUSE_MAXCMDLEN];
+
+	ssize_t retlen;
+
+	/*
+	 * First check if this is a sequential read from the same file.	 If
+	 * so skip repositioning the files seek pointer and emitting a new
+	 * command.
+	 */
+	if ((offset != current_offset) ||
+	    (strncmp(fp->ftpcmd, last_ftpcmd, HMCDRV_FUSE_MAXCMDLEN) != 0)) {
+
+		if ((lseek(hmcdrv_ctx.fd, offset, SEEK_END) < 0) ||
+		    (write(hmcdrv_ctx.fd, fp->ftpcmd, fp->cmdlen) < 0)) {
+			last_ftpcmd[0] = '\0';
+			return -errno;
+		}
+
+		current_offset = offset;
+	}
+
+	retlen = read(hmcdrv_ctx.fd, buf, len);
+
+	if (retlen < 0) {
+		last_ftpcmd[0] = '\0';
+		return -errno;
+	}
+
+	current_offset += retlen;
+	strncpy(last_ftpcmd, fp->ftpcmd, HMCDRV_FUSE_MAXCMDLEN);
+	last_ftpcmd[HMCDRV_FUSE_MAXCMDLEN - 1] = '\0';
+	return retlen;
+}
+
+
+/*
+ * FTP command assembly and execution
+ */
+static ssize_t hmcdrv_ftp_cmd(struct hmcdrv_fuse_file *fp,
+			      enum hmcdrv_fuse_cmdid cmd,
+			      char *buf, size_t len, off_t offset)
+{
+	if (fp == NULL)
+		return -EBADF;
+
+	hmcdrv_ftp_str(cmd, fp->ftpcmd);
+	return hmcdrv_ftp_transfer(fp, buf, len, offset);
+}
+
+
+
+/*
+ * returns a file path (from internal file structure) to a buffer,
+ * with appending a slash (in case it is missing)
+ */
+static int hmcdrv_path_copy(struct hmcdrv_fuse_file *fp, char *dest)
+{
+	char *src = HMCDRV_FUSE_PATH(fp);
+	int len = 0;
+
+	while ((len < (HMCDRV_FUSE_MAXPATH - 1)) &&
+	       (*src != '\0')) {
+
+		*dest = *src;
+
+		++len;
+		++dest;
+		++src;
+	}
+
+	if ((len > 0) && (*(dest - 1) != '/')) {
+		*dest++ = '/';
+		++len;
+	}
+
+	*dest = '\0';
+	return len;
+}
+
+
+/*
+ * convert a string into into a unsigned number,
+ * returning 0 on success or -errno on error
+ */
+static int hmcdrv_parse_uint(const char *s, unsigned int *pval)
+{
+	errno = 0;
+	*pval = strtoul(s, NULL, 10);
+	return -errno;
+}
+
+
+/*
+ * convert a string into into a signed number (with range check),
+ * returning 0 on success or -errno on error
+ */
+static int hmcdrv_parse_int(const char *s, int vmin, int vmax, int *pval)
+{
+	errno = 0;
+	*pval = strtol(s, NULL, 10);
+
+	if (errno == 0) {
+		if ((*pval >= vmin) && (*pval <= vmax))
+			return 0;
+
+		errno = ERANGE;
+	}
+
+	return -errno;
+}
+
+
+/*
+ * convert an abbreviated month name (%b) into a number
+ */
+static int hmcdrv_parse_month(const char *s)
+{
+	int i;
+
+	for (i = 0; i < 12; ++i) {
+		if (strncasecmp(hmcdrv_ctx.abmon[i], s,
+				hmcdrv_ctx.ablen[i]) == 0)
+			return i;
+	}
+
+	return -1;
+}
+
+
+/*
+ * return the file mode from a 'ls -l' like mode string
+ */
+static mode_t hmcdrv_parse_mode(const char *s)
+{
+	int i;
+	mode_t type;
+	mode_t mode = 0;
+
+	switch (*s) {
+	case 'd': /* directory */
+		type = S_IFDIR;
+		break;
+
+	case 'b': /* block device */
+		type = S_IFBLK;
+		break;
+
+	case 'c': /* character device */
+		type = S_IFCHR;
+		break;
+
+	case 'l': /* symbolic link */
+		type = S_IFLNK;
+		break;
+
+	case 'p': /* FIFO */
+		type = S_IFIFO;
+		break;
+
+	case 's': /* socket */
+		type = S_IFSOCK;
+		break;
+
+	case '-':
+		type = S_IFREG;
+		break;
+
+	default:
+		return 0;
+	}
+
+	for (i = 0; i < 9; ++i) {
+		mode <<= 1;
+
+		switch (*++s) {
+		case 'r':
+		case 'w':
+		case 'x':
+			mode |= 1;
+			break;
+
+		case '-':
+			break;
+
+		default:
+			return 0;
+		}
+
+	}
+
+	mode &= ~(S_IWUSR | S_IWGRP | S_IWOTH);
+	return mode | type;
+}
+
+
+/*
+ * convert a time string from format "%H:%M" into a 'struct tm' (without any
+ * local time conversion)
+ */
+static int hmcdrv_parse_time(const char *s, struct tm *t)
+{
+	char *endptr;
+	int hour, minute;
+
+	errno = 0;
+	hour = strtol(s, &endptr, 10);
+
+	if ((errno == 0) && (endptr != s) &&
+	    (hour >= 0) && (hour < 24)) {
+		if (*endptr == ':') {
+			s = ++endptr;
+			errno = 0;
+			minute = strtol(s, &endptr, 10);
+
+			if ((errno == 0) && (endptr != s) &&
+			    (minute >= 0) && (minute < 60)) {
+
+				t->tm_hour = hour;
+				t->tm_min = minute;
+
+				return 0; /* success */
+			}
+		}
+	}
+
+	return -1;
+}
+
+
+/*
+ * interpret token depending on current field number and
+ * fill the attributes structure
+ *
+ * Return: 'field' incremented by 1 or INT_MAX on parse errors
+ */
+static int hmcdrv_parse_ntoken(int field, char *token,
+			       struct hmcdrv_fuse_attr *attr)
+{
+	switch (field) {
+	case 0: /* mode */
+		attr->st->st_mode = hmcdrv_parse_mode(token);
+
+		if (attr->st->st_mode == 0) /* error ? */
+			field = INT_MAX - 1; /* stop */
+		break;
+
+	case 1: /* number of hard links to this file */
+		errno = 0;
+		attr->st->st_nlink = strtoul(token, NULL, 10);
+
+		if (errno != 0)
+			field = INT_MAX - 1;
+		break;
+
+	case 2: /* user ID */
+		if (hmcdrv_parse_uint(token, &attr->st->st_uid) != 0) {
+			attr->st->st_uid = hmcdrv_ctx.st.st_uid;
+			HMCDRV_FUSE_LOG(LOG_WARNING,
+					"Could not parse UID (will use %u)",
+					attr->st->st_uid);
+		}
+
+		break;
+
+	case 3: /* group ID */
+		if (hmcdrv_parse_uint(token, &attr->st->st_gid) != 0) {
+			attr->st->st_gid = hmcdrv_ctx.st.st_gid;
+			HMCDRV_FUSE_LOG(LOG_WARNING,
+					"Could not parse GID (will use %u)",
+					attr->st->st_gid);
+		}
+
+		break;
+
+	case 4: /* file size */
+		errno = 0;
+		attr->st->st_size = strtol(token, NULL, 10);
+
+		if (errno != 0)
+			field = INT_MAX - 1;
+		break;
+
+	case 5: /* month of year [0,11] */
+		attr->ftm.tm_mon = hmcdrv_parse_month(token);
+		break;
+
+	case 6: /* day of month [1,31] */
+		if (hmcdrv_parse_int(token, 1, 31, &attr->ftm.tm_mday) != 0) {
+			field = INT_MAX - 1;
+		} else {
+			/*
+			 * in case month is not in English language we've
+			 * got an error in attr->ftm.tm_mon
+			 */
+			if (attr->ftm.tm_mon < 0) {
+				attr->ftm.tm_mday = 1; /* YYYY-01-01 */
+				attr->ftm.tm_mon = 0;
+			}
+		}
+
+		break;
+
+	case 7: /* %H:%M or %Y */
+		if (hmcdrv_parse_time(token, &attr->ftm) != 0) {
+			if (hmcdrv_parse_int(token, 1900, INT_MAX,
+					     &attr->ftm.tm_year) != 0)
+				field = INT_MAX - 1;
+			else
+				attr->ftm.tm_year -= 1900;
+		}
+
+		break;
+
+	case 8: /* file name (rest of line) */
+		attr->fname = token;
+		break;
+
+
+	default:
+		break;
+	}
+
+	return ++field;
+}
+
+
+/*
+ * parse filename and attributes from 'dir' output (a single line)
+ *
+ * Note: Because HMC command 'dir' always uses the HMC server local time the
+ *       current timezone should be set correctly, based on startup option
+ *       "-o hmctz=TZ".
+ *
+ * Return: pointer to start of next line (skipping any whitespace)
+ *         or pointer to '\0'
+ */
+static char *hmcdrv_parse_line(char *line, char *namebuf,
+			       int bufsize, struct stat *st,
+			       char *symlink)
+{
+	int field = 0;
+	struct hmcdrv_fuse_attr attr = {.fname = NULL, .st = st};
+
+	memset(st, 0, sizeof(*st));
+	namebuf[0] = '\0';
+
+	/* set default values
+	 */
+	st->st_mtime = time(NULL);  /* see next line: gmtime_r() */
+	gmtime_r(&st->st_mtime, &attr.ftm);   /* tm_year default */
+	attr.ftm.tm_isdst = -1; /* mktime() should use "TZ" info */
+	attr.ftm.tm_hour = attr.ftm.tm_min = attr.ftm.tm_sec = 0;
+
+	while ((*line != '\0') && isspace(*line))
+		++line; /* skip leading whitespace (incl. newlines) */
+
+
+	while ((*line != '\0') && (*line != '\n') && (field < 9)) {
+
+		while ((*line != '\0') &&
+		       (*line != '\n') &&
+		       isspace(*line))
+			++line; /* skip spaces, but not newlines */
+
+		if ((*line != '\0') && (*line != '\n')) {
+			field = hmcdrv_parse_ntoken(field, line, &attr);
+
+			while ((*line != '\0') &&
+			       (*line != '\n') &&
+			       !isspace(*line))
+				++line; /* search end of field */
+		}
+	} /* while */
+
+	/* search end of line now (skips some fields in case of error)
+	 */
+	while ((*line != '\0') && (*line != '\n'))
+		++line;
+
+	/*
+	 * If start of file name was found by parser, then indicate success
+	 * returning the file attributes.  But do this only if the end of
+	 * line was really found, else this seems to be an incomplete line
+	 * in current chunk (fragment).
+	 */
+	if ((attr.fname != NULL) && (*line == '\n')) {
+		char *arrow = NULL; /* " -> " pointer, when symlink */
+		*line = '\0'; /* temporary set EOS for strncpy() */
+
+		if (S_ISLNK(st->st_mode)) {
+			arrow = strstr(attr.fname, " -> ");
+
+			if (arrow != NULL)
+				*arrow = '\0';
+		}
+
+		strncpy(namebuf, attr.fname, bufsize);
+		namebuf[bufsize - 1] = '\0'; /* safety */
+
+		if (arrow == NULL) {
+			symlink[0] = '\0';
+		} else {
+			strncpy(symlink, arrow + 4, HMCDRV_FUSE_MAXPATH);
+			*arrow = ' '; /* restore */
+		}
+
+		*line = '\n'; /* restore */
+		st->st_mtime = mktime(&attr.ftm); /* UTC from local time */
+
+		if (st->st_mtime == (time_t) -1)
+			st->st_mtime = 0;
+
+		st->st_atime = st->st_ctime = st->st_mtime;
+
+		/* In contrast to FUSE documentation the members below must
+		 * be set in stat structure.  For example 'du' uses them,
+		 * because it shows the disk usage from used blocks and not
+		 * the file size (unless option '--apparent-size' is given).
+		 */
+		st->st_blksize = 2048U; /* nearly all CD/DVDs have this */
+		st->st_blocks = (st->st_size + 511U) / 512U;
+	}
+
+
+	while ((*line != '\0') && isspace(*line))
+		++line; /* skip whitespace after EOL, incl. newlines */
+
+	return line;
+}
+
+
+/*
+ * performs a FTP 'dir', then scans/parses the output, fills the cache and
+ * (optional) calls the FUSE filler function for every file in 'dir' listing
+ */
+static int hmcdrv_cache_dir(const char *dir, fuse_fill_dir_t filler, void *buf)
+{
+	char path[HMCDRV_FUSE_MAXPATH]; /* constructed path of a file */
+	char symlink[HMCDRV_FUSE_MAXPATH]; /* target of symlink (S_IFLNK) */
+	struct hmcdrv_fuse_file *fpdir; /* directory file pointer */
+	struct stat st; /* attributes of file */
+	char *dirbuf; /* 'dir' listing (chunk) buffer */
+	char *next; /* 'next line' pointer in 'dir' buffer */
+	char *line; /* current line start position in 'dir' buffer */
+	int dirlen; /* number of characters in 'dir' buffer */
+	int fraglen; /* fragment length in 'dir' buffer */
+	char *fname; /* buffer pointer for basename of file */
+	int len; /* maximum length of file basename (buffer space) */
+	off_t offset; /* device 'dir' position */
+
+	char *tzenv = NULL; /* "TZ" environment variable */
+
+	fpdir = hmcdrv_file_get(dir);
+
+	if (fpdir == NULL) {
+		HMCDRV_FUSE_LOG(LOG_ERR,
+				"Could not find directory '%s' in cache",
+				dir);
+		return -ENOENT;
+	}
+
+	dirbuf = malloc(HMCDRV_FUSE_DIRBUF_SIZE);
+
+	if (dirbuf == NULL) {
+		HMCDRV_FUSE_LOG(LOG_ERR,
+				"Out of memory, %d bytes",
+				HMCDRV_FUSE_DIRBUF_SIZE);
+		return -ENOMEM;
+	}
+
+	/* set HMC timezone (optional), else host timezone remains active -
+	 * possibly different from HMC timezone and so resulting in wrong
+	 * file modification times (cf. mktime() in hmcdrv_parse_line())
+	 */
+	if (hmcdrv_ctx.opt.hmctz != NULL) { /* "-o hmctz=TZ" present ? */
+		tzenv = getenv("TZ");
+
+		if (tzenv != NULL)
+			tzenv = strdup(tzenv);
+
+		if (setenv("TZ", hmcdrv_ctx.opt.hmctz, 1) != 0) {
+			HMCDRV_FUSE_LOG(LOG_ERR,
+					"Could not set HMC timezone '%s': %s",
+					hmcdrv_ctx.opt.hmctz,
+					strerror(errno));
+
+			if (tzenv != NULL) {
+				setenv("TZ", tzenv, 1);
+				free(tzenv);
+			}
+
+			free(dirbuf);
+			return -ENOMEM;
+		}
+
+		tzset();
+	}
+
+	len = hmcdrv_path_copy(fpdir, path); /* get directory path */
+	fname = path + len; /* start of basename in path buffer */
+	len = HMCDRV_FUSE_MAXPATH - len; /* space for basename */
+	HMCDRV_FUSE_DBGLOG("scanning '%s'...", path);
+
+	/* do the 'dir' chunks and parse all
+	 */
+	dirlen = hmcdrv_ftp_cmd(fpdir, HMCDRV_FUSE_CMDID_DIR,
+				dirbuf, HMCDRV_FUSE_DIRBUF_LEN, 0);
+	next = dirbuf; /* first line at start of buffer */
+	offset = dirlen;
+
+	while (dirlen > 0) {
+
+		next[dirlen] = '\0'; /* force end of string */
+
+		/* parse all files in this 'dir' listing chunk
+		 */
+		do {
+			line = next;
+
+			/* parse next line and retrieve the file name
+			 */
+			next = hmcdrv_parse_line(line, fname, len,
+						 &st, symlink);
+
+			/* If parsing of 'dir' line was successful, then
+			 * store file attributes and name in cache. Else
+			 * skip this line because of incorrect syntax -
+			 * possibly it is a fragment (at end of buffer) or
+			 * this line does not hold any valid file info.
+			 */
+			if (*fname != '\0') {
+				hmcdrv_cache_refresh(path, &st, symlink);
+
+				if ((filler != NULL) &&
+				    (filler(buf, fname, &st, 0) != 0))
+					filler = NULL; /* stop filling */
+#ifdef DEBUG
+				strftime(symlink, sizeof(symlink),
+					 "%c", gmtime(&st.st_mtime));
+				HMCDRV_FUSE_DBGLOG(" * %s (size=%zu, mode=%o, uid=%u, gid=%u, time=%s)",
+						   fname,
+						   st.st_size, st.st_mode,
+						   st.st_uid, st.st_gid,
+						   symlink);
+#endif
+			}
+
+		} while (*next != '\0');
+
+		/* Try to read more bytes from the FTP device. But regard
+		 * remaining chars from the last line, in case it could not
+		 * completely parsed (in other words the line was a
+		 * fragment, and has to be moved to start of buffer).
+		 */
+		if (*fname == '\0') { /* no valid filename? */
+			fraglen = next - line;
+
+			if (fraglen > 0)
+				memcpy(dirbuf, line, fraglen);
+		} else {
+			fraglen = 0;
+		}
+
+		dirlen = hmcdrv_ftp_cmd(fpdir, HMCDRV_FUSE_CMDID_DIR,
+					dirbuf + fraglen,
+					HMCDRV_FUSE_DIRBUF_LEN - fraglen,
+					offset);
+
+		if (dirlen > 0) { /* on success continue */
+			next = dirbuf;
+			offset += dirlen;
+			dirlen += fraglen;
+		}
+	}
+
+
+	free(dirbuf);
+
+	/* restore old host timezone
+	 */
+	if (hmcdrv_ctx.opt.hmctz != NULL) {
+		if (tzenv != NULL) {
+			putenv(tzenv);
+			free(tzenv);
+		} else {
+			unsetenv("TZ");
+		}
+
+		tzset();
+	}
+
+	return dirlen;
+}
+
+
+/*
+ * cache all files located in parent directory of 'dir',
+ * so also caching the attributes of 'dir' again
+ *
+ * Return: 0 on success, else a negative error code (-ENOENT if 'dir' is "/")
+ */
+static int hmcdrv_cache_parent(const char *dir)
+{
+	char *tmp, *parent;
+
+	if (strcmp(dir, "/") == 0) /* root has no parent */
+		return -ENOENT;
+
+	tmp = strdup(dir); /* need a copy because of dirname() */
+
+	if (tmp == NULL) {
+		HMCDRV_FUSE_LOG(LOG_ERR, "Out of memory, %zd bytes",
+				strlen(dir) + 1);
+		return -ENOMEM;
+	}
+
+	parent = dirname(tmp);
+	HMCDRV_FUSE_DBGLOG("re-scanning parent '%s'...", parent);
+	hmcdrv_cache_dir(parent, NULL, NULL);
+	free(tmp);
+	return 0;
+}
+
+
+/*
+ * return the file attributes data pointer (or NULL on error)
+ */
+static struct hmcdrv_fuse_file *hmcdrv_file_get(const char *path)
+{
+	struct hmcdrv_fuse_file *fp = hmcdrv_cache_find(path);
+
+	if (fp != NULL) /* path found in cache ? */
+		return fp;
+
+	/* in very, very rare cases we must scan the parent again,
+	 * recursive up to the root directory
+	 */
+	if (hmcdrv_cache_parent(path) != 0)
+		return NULL;
+
+	/* because the mutex is taken by the caller, we MUST find this file
+	 * now in cache (if not so then the HMC drive DVD may have changed
+	 * without unmounting FUSE.HMCDRVFS)
+	 */
+	return hmcdrv_cache_find(path);
+}
+
+
+/*
+ * obtain file status information (attributes) on FUSE.HMCDRVFS filesystem
+ *
+ * Note: The most important function which FUSE calls (very often).
+ */
+static int hmcdrv_fuse_getattr(const char *path, struct stat *stbuf)
+{
+	struct hmcdrv_fuse_file *fp;
+	int rc = 0;
+
+	pthread_mutex_lock(&hmcdrv_ctx.mutex);
+	fp = hmcdrv_file_get(path);
+
+	if (fp == NULL)
+		rc = -ENOENT;
+	else
+		*stbuf = fp->st;
+
+	pthread_mutex_unlock(&hmcdrv_ctx.mutex);
+	return rc;
+}
+
+
+/*
+ * get symlink target path
+ */
+static int hmcdrv_fuse_readlink(const char *path, char *buf, size_t size)
+{
+	struct hmcdrv_fuse_file *fp;
+	int rc = 0;
+
+	pthread_mutex_lock(&hmcdrv_ctx.mutex);
+	fp = hmcdrv_file_get(path);
+
+	if (fp == NULL) {
+		rc = -ENOENT;
+	} else {
+		if (fp->symlnk == NULL) {
+			rc = -ENOMEM;
+		} else {
+			if (!S_ISLNK(fp->st.st_mode)) {
+				rc = -EINVAL;
+			} else {
+				strncpy(buf, fp->symlnk, size);
+				buf[size - 1] = '\0';
+			}
+		}
+	}
+
+	pthread_mutex_unlock(&hmcdrv_ctx.mutex);
+	return rc;
+}
+
+
+/*
+ * open a directory on FUSE.HMCDRVFS filesystem
+ */
+static int hmcdrv_fuse_opendir(const char *UNUSED(path),
+			       struct fuse_file_info *fi)
+{
+	if ((fi->flags & O_ACCMODE) != O_RDONLY)
+		return -EACCES; /* all files are read-only */
+
+	return 0;
+}
+
+
+/*
+ * read a directory on FUSE.HMCDRVFS filesystem
+ */
+static int hmcdrv_fuse_readdir(const char *path, void *buf,
+			       fuse_fill_dir_t filler, off_t UNUSED(offset),
+			       struct fuse_file_info *UNUSED(fi))
+{
+	int ret;
+
+	filler(buf, ".", NULL, 0);
+	filler(buf, "..", NULL, 0);
+
+	pthread_mutex_lock(&hmcdrv_ctx.mutex);
+	ret = hmcdrv_cache_dir(path, filler, buf);
+	pthread_mutex_unlock(&hmcdrv_ctx.mutex);
+	return ret;
+}
+
+
+/*
+ * open a file on FUSE.HMCDRVFS filesystem
+ */
+static int hmcdrv_fuse_open(const char *UNUSED(path), struct fuse_file_info *fi)
+{
+	if ((fi->flags & O_ACCMODE) != O_RDONLY)
+		return -EACCES; /* all files are read-only */
+
+	return 0;
+}
+
+
+/*
+ * read a file on FUSE.HMCDRVFS filesystem
+ */
+static int hmcdrv_fuse_read(const char *path, char *buf, size_t size,
+			    off_t offset, struct fuse_file_info *UNUSED(fi))
+{
+	struct hmcdrv_fuse_file *fp;
+	int rc;
+
+	pthread_mutex_lock(&hmcdrv_ctx.mutex);
+	fp = hmcdrv_file_get(path);
+
+	if (fp == NULL)
+		rc = -ENOENT;
+	else
+		rc = hmcdrv_ftp_cmd(fp, HMCDRV_FUSE_CMDID_GET,
+				    buf, size, offset);
+
+	pthread_mutex_unlock(&hmcdrv_ctx.mutex);
+	return rc;
+}
+
+
+/*
+ * initialize FUSE.HMCDRVFS filesystem
+ *
+ * Note: calls fuse_exit() on errors
+ *
+ * Return: value to be passed in the private_data field of fuse_context to
+ *         all file operations and as a parameter to the destroy() method
+ */
+static void *hmcdrv_fuse_init(struct fuse_conn_info *UNUSED(conn))
+{
+	pthread_mutexattr_t attr;
+
+	memset(hmcdrv_fuse_cache, 0, sizeof(hmcdrv_fuse_cache));
+	openlog(HMCDRV_FUSE_LOGNAME, LOG_PID, LOG_DAEMON);
+
+	if (pthread_mutexattr_init(&attr) != 0)
+		goto err_out;
+
+	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
+	hmcdrv_ctx.fd = open(HMCDRV_FUSE_FTPDEV, O_RDWR);
+
+	if (hmcdrv_ctx.fd < 0)
+		goto err_dev;
+
+	if (pthread_mutex_init(&hmcdrv_ctx.mutex, &attr) != 0)
+		goto err_mutex;
+
+	hmcdrv_cache_refresh("/", &hmcdrv_ctx.st, NULL); /* never expires */
+
+	if (pthread_create(&hmcdrv_ctx.tid, NULL,
+			   hmcdrv_cache_aging, NULL) == 0) {
+
+		pthread_mutexattr_destroy(&attr);
+		return &hmcdrv_ctx.tid;
+	}
+
+err_mutex:
+	close(hmcdrv_ctx.fd);
+err_dev:
+	pthread_mutexattr_destroy(&attr);
+err_out:
+	HMCDRV_FUSE_LOG(LOG_ERR, "Initialization failed: %s",
+			strerror(errno));
+	closelog();
+	fuse_exit(fuse_get_context()->fuse);
+	return NULL;
+}
+
+
+/*
+ * clean up FUSE.HMCDRVFS filesystem
+ */
+static void hmcdrv_fuse_exit(void *arg)
+{
+	struct hmcdrv_fuse_file *fp, *next;
+	int i;
+
+	if (arg != NULL)
+		pthread_cancel(*(pthread_t *) arg);
+
+	pthread_mutex_lock(&hmcdrv_ctx.mutex);
+
+	for (i = 0; i < HMCDRV_FUSE_CACHE_SIZE; ++i) {
+		fp = hmcdrv_fuse_cache[i];
+
+		while (fp != NULL) {
+			next = fp->next;
+			hmcdrv_cache_symlink(fp, NULL);
+			free(fp);
+			fp = next;
+		}
+	}
+
+	memset(hmcdrv_fuse_cache, 0, sizeof(hmcdrv_fuse_cache));
+	pthread_mutex_destroy(&hmcdrv_ctx.mutex);
+	closelog();
+
+	if (hmcdrv_ctx.fd >= 0)
+		close(hmcdrv_ctx.fd);
+}
+
+
+/*
+ * FUSE.HMCDRVFS internal main function
+ */
+static int hmcdrv_fuse_main(struct fuse_args *args)
+{
+	static struct fuse_operations hmcdrv_fuse_op = {
+		.init = hmcdrv_fuse_init,
+		.destroy = hmcdrv_fuse_exit,
+		.getattr = hmcdrv_fuse_getattr,
+		.opendir = hmcdrv_fuse_opendir,
+		.readdir = hmcdrv_fuse_readdir,
+		.open = hmcdrv_fuse_open,
+		.read = hmcdrv_fuse_read,
+		.readlink = hmcdrv_fuse_readlink
+	};
+
+
+#if FUSE_VERSION >= 26
+	return fuse_main(args->argc, args->argv, &hmcdrv_fuse_op, NULL);
+#else
+	return fuse_main(args->argc, args->argv, &hmcdrv_fuse_op);
+#endif
+}
+
+
+/*
+ * usage output
+ */
+static void hmcdrv_fuse_usage(const char *progname)
+{
+	fprintf(stderr,
+		"Usage: %s MOUNTPOINT [OPTIONS]\n\n"
+		"Use the %s command to read files from a HMC drive DVD.\n"
+		"\n"
+		"General options:\n"
+		"    -o opt,[opt...]        Mount options\n"
+		"    -h   --help            Print help, then exit\n"
+		"    -v   --version         Print version, then exit\n"
+		"\n"
+		"Specific options:\n"
+		"    -o hmclang=LANG        HMC speaks language LANG (see locale(1))\n"
+		"    -o hmctz=TZ            HMC is in timezone TZ (see tzset(3))\n"
+		"\n"
+		"Attention:\n"
+		"    The following general and FUSE specific mount options will\n"
+		"    be ignored (most do not make sense on a read-only media):\n"
+		"    --rw, -w, -o rw, -o atime, -o max_write=N, -o big_writes,\n"
+		"    -o atomic_o_trunc, -o hard_remove, -o negative_timeout=T,\n"
+		"    -o use_ino, -o readdir_ino, -o subdir=DIR\n"
+		"\n",
+		progname, progname);
+}
+
+
+/*
+ * process the "-o hmclang=LANG" command line option (if there is one) and
+ * prepare abbreviated month names (%b) in context structure
+ *
+ * Return: 0 on success, -1 on error
+ */
+static int hmcdrv_optproc_lang(void)
+{
+	static const nl_item nl_abmon[12] = {
+		ABMON_1, ABMON_2, ABMON_3, ABMON_4, ABMON_5, ABMON_6,
+		ABMON_7, ABMON_8, ABMON_9, ABMON_10, ABMON_11, ABMON_12};
+
+	unsigned int i;
+	char *lc = NULL; /* current LC_TIME locale */
+
+	if (hmcdrv_ctx.opt.hmclang != NULL) {
+		HMCDRV_FUSE_DBGLOG("option \"-o hmclang=%s\" detected",
+				   hmcdrv_ctx.opt.hmclang);
+
+		lc = setlocale(LC_TIME, NULL);
+
+		if (setlocale(LC_TIME, hmcdrv_ctx.opt.hmclang) == NULL) {
+			setlocale(LC_TIME, lc);
+			return -1;
+		}
+	}
+
+	for (i = 0; i < (sizeof(nl_abmon) / sizeof(nl_abmon[0])); ++i) {
+		hmcdrv_ctx.abmon[i] = nl_langinfo(nl_abmon[i]);
+		hmcdrv_ctx.ablen[i] = strlen(hmcdrv_ctx.abmon[i]);
+	}
+
+	if (lc)
+		setlocale(LC_TIME, lc); /* restore LC_TIME locale */
+
+	return 0;
+}
+
+
+/*
+ * option parsing function
+ */
+static int hmcdrv_fuse_optproc(void *data, const char *arg,
+			       int key, struct fuse_args *outargs)
+{
+	double tmo; /* timeout T in "-o entry/attr_timeout=T" */
+	mode_t mask; /* umask() of caller */
+
+	(void)outargs;
+
+	switch (key) {
+	case HMCDRV_FUSE_OPTKEY_NSUP:
+		HMCDRV_FUSE_LOG(LOG_WARNING,
+				"FUSE option \"%s\" ignored (unsupported)",
+				arg);
+		return 0; /* remove this option(s) */
+
+	case HMCDRV_FUSE_OPTKEY_RW: /* write options (unsupported) */
+		HMCDRV_FUSE_LOG(LOG_WARNING,
+				"FUSE option \"%s\" ignored (r/o filesystem)",
+				arg);
+		return 0; /* remove this option(s) */
+
+	case HMCDRV_FUSE_OPTKEY_RO: /* "-o ro" or "-r" */
+		((struct hmcdrv_fuse_opt *)data)->ro = 1;
+		return 1;
+
+	case HMCDRV_FUSE_OPTKEY_NATM: /* "-o noatime" */
+		((struct hmcdrv_fuse_opt *)data)->noatime = 1;
+		return 1;
+
+	case HMCDRV_FUSE_OPTKEY_CTMO: /* "-o entry/attr_timeout=T" */
+		if (sscanf(arg, "%*[^=]=%lf", &tmo) == 1) {
+			if (tmo < 1.0)
+				tmo = 1.0;
+
+			if (hmcdrv_ctx.ctmo <
+			    (HMCDRV_FUSE_CACHE_TMOFS + (time_t)tmo))
+				hmcdrv_ctx.ctmo =
+					HMCDRV_FUSE_CACHE_TMOFS + (time_t)tmo;
+
+			HMCDRV_FUSE_DBGLOG("option \"%s\" detected (cache timeout now is %" PRIdMAX " sec.)",
+					   arg, (intmax_t) hmcdrv_ctx.ctmo);
+		}
+
+		return 1;
+
+	case HMCDRV_FUSE_OPTKEY_VERSION:
+		fprintf(stderr, HMCDRV_FUSE_LOGHEAD
+			"HMC drive DVD file system, version %s\n"
+			"Copyright IBM Corp. 2015\n",
+			HMCDRV_FUSE_RELEASE);
+		exit(EXIT_SUCCESS);
+
+	case HMCDRV_FUSE_OPTKEY_HELP:
+		hmcdrv_fuse_usage(outargs->argv[0]);
+		fuse_opt_add_arg(outargs, "-ho");
+		hmcdrv_fuse_main(outargs);
+		exit(EXIT_SUCCESS);
+
+	case FUSE_OPT_KEY_NONOPT: /* normally the mount point */
+		mask = umask(0);
+		umask(mask);
+		hmcdrv_ctx.st.st_uid = getgid();
+		hmcdrv_ctx.st.st_gid = getuid();
+		hmcdrv_ctx.st.st_ino = 0;
+		hmcdrv_ctx.st.st_size = 0; /* unknown */
+		hmcdrv_ctx.st.st_blksize = 2048U; /* DVD sector size */
+		hmcdrv_ctx.st.st_nlink = 2; /* minimum */
+		hmcdrv_ctx.st.st_mtime = time(NULL);
+		hmcdrv_ctx.st.st_atime = hmcdrv_ctx.st.st_mtime;
+		hmcdrv_ctx.st.st_ctime = hmcdrv_ctx.st.st_mtime;
+
+		hmcdrv_ctx.st.st_mode =
+			(S_IFDIR |
+			 S_IXUSR | S_IRUSR |
+			 S_IXGRP | S_IRGRP |
+			 S_IXOTH | S_IROTH) &
+			~(mask | (S_IWUSR | S_IWGRP | S_IWOTH));
+
+		HMCDRV_FUSE_DBGLOG("mount point is %s (uid = %u, gid = %u)",
+				   arg,
+				   hmcdrv_ctx.st.st_uid,
+				   hmcdrv_ctx.st.st_gid);
+		return 1;
+
+
+	default:
+		HMCDRV_FUSE_DBGLOG("option \"%s\" passed to FUSE", arg);
+		return 1; /* pass all other options to fuse_main() */
+	}
+}
+
+
+/*
+ * FUSE.HMCDRVFS entry function
+ */
+int main(int argc, char *argv[])
+{
+	static struct fuse_opt lookup_opt[] = {
+
+		HMCDRV_FUSE_OPT("hmctz=%s", hmctz, 0),
+		HMCDRV_FUSE_OPT("hmclang=%s", hmclang, 0),
+
+		FUSE_OPT_KEY("ro", HMCDRV_FUSE_OPTKEY_RO),
+		FUSE_OPT_KEY("-r", HMCDRV_FUSE_OPTKEY_RO),
+		FUSE_OPT_KEY("--read-only", HMCDRV_FUSE_OPTKEY_RO),
+
+		FUSE_OPT_KEY("noatime", HMCDRV_FUSE_OPTKEY_NATM),
+		FUSE_OPT_KEY("entry_timeout=%lf", HMCDRV_FUSE_OPTKEY_CTMO),
+		FUSE_OPT_KEY("attr_timeout=%lf", HMCDRV_FUSE_OPTKEY_CTMO),
+
+		/* unsupported (ignored) */
+		FUSE_OPT_KEY("subdir=%s", HMCDRV_FUSE_OPTKEY_NSUP),
+		FUSE_OPT_KEY("use_ino", HMCDRV_FUSE_OPTKEY_NSUP),
+		FUSE_OPT_KEY("readdir_ino", HMCDRV_FUSE_OPTKEY_NSUP),
+
+		/* to be ignored on read-only filesystem */
+		FUSE_OPT_KEY("rw", HMCDRV_FUSE_OPTKEY_RW),
+		FUSE_OPT_KEY("-w", HMCDRV_FUSE_OPTKEY_RW),
+		FUSE_OPT_KEY("--rw", HMCDRV_FUSE_OPTKEY_RW),
+		FUSE_OPT_KEY("atime", HMCDRV_FUSE_OPTKEY_RW),
+		FUSE_OPT_KEY("max_write=%u", HMCDRV_FUSE_OPTKEY_RW),
+		FUSE_OPT_KEY("big_writes", HMCDRV_FUSE_OPTKEY_RW),
+		FUSE_OPT_KEY("hard_remove", HMCDRV_FUSE_OPTKEY_RW),
+		FUSE_OPT_KEY("atomic_o_trunc", HMCDRV_FUSE_OPTKEY_RW),
+		FUSE_OPT_KEY("negative_timeout=%lf", HMCDRV_FUSE_OPTKEY_RW),
+
+		/* options, that exit immediately */
+		FUSE_OPT_KEY("-v", HMCDRV_FUSE_OPTKEY_VERSION),
+		FUSE_OPT_KEY("--version", HMCDRV_FUSE_OPTKEY_VERSION),
+		FUSE_OPT_KEY("-h", HMCDRV_FUSE_OPTKEY_HELP),
+		FUSE_OPT_KEY("--help", HMCDRV_FUSE_OPTKEY_HELP),
+
+		FUSE_OPT_END
+	};
+
+	int ret;
+	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);
+
+	memset(&hmcdrv_ctx.opt, 0, sizeof(hmcdrv_ctx.opt));
+	hmcdrv_ctx.pid = getpid();
+
+	fuse_opt_parse(&args, &hmcdrv_ctx.opt, lookup_opt,
+		       hmcdrv_fuse_optproc);
+
+	/* the following options are required on a read-only media */
+	if (!hmcdrv_ctx.opt.ro)
+		fuse_opt_add_arg(&args, "-oro");
+
+	if (!hmcdrv_ctx.opt.noatime)
+		fuse_opt_add_arg(&args, "-onoatime");
+
+	if (hmcdrv_optproc_lang() != 0) {
+		HMCDRV_FUSE_LOG(LOG_ERR,
+				"Unknown HMC language in '-o hmclang=%s'",
+				hmcdrv_ctx.opt.hmclang);
+		exit(EXIT_FAILURE);
+	}
+
+	/* notice that there is no way to check the timezone parameter for
+	 * correct syntax, not in glibc as well as POSIX 1003.1
+	 */
+	if (hmcdrv_ctx.opt.hmctz != NULL)
+		HMCDRV_FUSE_DBGLOG("option \"-o hmctz=%s\" detected",
+				   hmcdrv_ctx.opt.hmctz);
+
+	ret = hmcdrv_fuse_main(&args);
+	fuse_opt_free_args(&args);
+
+	return ret;
+}
--- /dev/null
+++ b/hmcdrvfs/lshmc
@@ -0,0 +1,90 @@
+#!/bin/bash
+
+###############################################################################
+# lshmc - print files from a HMC drive DVD
+#
+# Copyright IBM Corp. 2015
+# Author(s): Ralf Hoppe <rhoppe@de.ibm.com>
+###############################################################################
+
+TOOL=$(basename $0)
+FTPDEV="/dev/hmcdrv"
+FTPCMD="dir"
+
+#------------------------------------------------------------------------------
+# Print usage
+#------------------------------------------------------------------------------
+function PrintUsage() {
+	cat <<-EOD
+		Usage: $(basename $0) [OPTIONS] [FILE]
+
+		List information about the FILE(s) residing on a HMC drive DVD.
+		Use OPTIONS described below or present simple wildcards on
+		behalf of FILE.
+
+		-h, --help	Print this help, then exit.
+		-v, --version	Print version information, then exit.
+		-s, --short	Print only files, in a short listing format.
+	EOD
+}
+
+#------------------------------------------------------------------------------
+# Print version
+#------------------------------------------------------------------------------
+function PrintVersion()
+{
+	cat <<-EOD
+	$TOOL: version %S390_TOOLS_VERSION%
+	Copyright IBM Corp. 2015
+	EOD
+}
+
+
+FILES=""
+
+while [ $# -gt 0 ]; do
+	case $1 in
+	--help|-h)
+		PrintUsage
+		exit 0
+		;;
+	--version|-v)
+		PrintVersion
+		exit 0
+		;;
+	--short|-s)
+		FTPCMD="nls"
+		;;
+	-*)
+		echo "$TOOL: Invalid option $1"
+		echo "Try '$TOOL --help' for more information."
+		exit 1
+		;;
+	*)
+		FILES="$FILES $1"
+		;;
+	esac
+
+	shift
+done
+
+
+if [ ! -c "$FTPDEV" ]; then
+	echo "$TOOL: Device \"$FTPDEV\" does not exist (modprobe hmcdrv ?)"
+	exit 1
+fi
+
+if [ ${#FILES} -eq 0 ]; then
+	FILES="/"
+fi
+
+# open device $FTPDEV and assign the file descriptor to variable fd
+exec {fd}<>${FTPDEV}
+# echo the FTP command into device file
+echo "$FTPCMD $FILES" >&${fd}
+# and output the response
+cat <&${fd}
+STATUS="$?"
+# close file descriptor $fd
+exec {fd}>&-
+exit $STATUS
--- /dev/null
+++ b/hmcdrvfs/lshmc.8
@@ -0,0 +1,104 @@
+.\" Copyright IBM Corp. 2015
+.\"
+.TH LSHMC 8 "Mar 2015" "s390-tools"
+.\" disable hyphenation for words below
+.hw hmcdrv lshmc
+
+.\" save horizontal spacing to registers
+.nr default-word-spacing \n[.ss]
+.nr default-sentence-spacing \n[.sss]
+
+.\" define a macro for default horizontal spacing
+.de ssd
+.ss \n[default-word-spacing] \n[default-sentence-spacing]
+..
+
+.SH NAME
+lshmc \- list
+.SM HMC
+drive
+.SM DVD
+contents
+
+.SH SYNOPSIS
+.TP
+\fBlshmc\fP [OPTIONS] [FILE]
+
+.SH DESCRIPTION
+List files on a
+.SM DVD
+in the
+.SM DVD
+drive of the Hardware Management Console
+.ss 0
+(
+.SM HMC
+)
+.ssd
+\&.
+By default, the command lists all files in the root directory of the
+.SM DVD\c
+\&.
+Use FILE to list a different set of files.  FILE can specify the path, relative
+to the
+.SM DVD
+root directory, for a directory and file and can contain the * and ? wildcard
+characters.
+
+.SH OPTIONS
+.TP
+.B "-h, --help"
+Print help text, then exit.
+.TP
+.B "-v, --version"
+Print version information, then exit.
+.TP
+.B "-s, --short"
+Print only regular files (no directories, symbolic links and special files),
+in a short listing format.
+
+.SH EXAMPLES
+
+To list the files in a
+.SM HMC
+drive
+.SM DVD
+root directory use:
+.PP
+.nf
+.RS
+.B # lshmc
+.RE
+.fi
+.PP
+In case the kernel module \fIhmcdrv\fP was not loaded in advance use:
+.PP
+.nf
+.RS
+.B # modprobe hmcdrv
+.B # lshmc
+.RE
+.fi
+.PP
+To list all HTML files in subdirectory \fIwww\fP use:
+.PP
+.nf
+.RS
+.B # lshmc /www/*.html
+.RE
+.fi
+.PP
+
+.SH DIAGNOSTICS
+The \fBlshmc\fP command needs access to device node \fI/dev/\:hmcdrv\fP.  This
+node is created automatically when the \fIhmcdrv\fP kernel module is loaded
+(see Linux kernel configuration option
+.SM CONFIG_HMC_DRV\c
+).  The user process that runs the \fBlshmc\fP command must have sufficient
+privileges to read from and write to node \fI/dev/\:hmcdrv\fP.  Use the commands
+\fBchown(1)\fP, \fBchgrp(1)\fP and/or \fBchmod(1)\fP on node
+\fI/dev/\:hmcdrv\fP to ensure this condition.
+
+.SH SEE ALSO
+.B ls(1), chown(1), chgrp(1), chmod(1)
+and Linux on System z: Device Drivers, Features and Commands
