summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--NEWS2
-rw-r--r--doc/index.html6
-rw-r--r--doc/s6-usertree-maker.html240
-rw-r--r--doc/upgrade.html2
-rw-r--r--package/deps.mak3
-rw-r--r--package/modes1
-rw-r--r--package/targets.mak4
-rw-r--r--src/usertree/deps-exe/s6-usertree-maker1
-rw-r--r--src/usertree/s6-usertree-maker.c302
9 files changed, 561 insertions, 0 deletions
diff --git a/NEWS b/NEWS
index 4998b5b..0d85471 100644
--- a/NEWS
+++ b/NEWS
@@ -14,6 +14,8 @@ SIGQUIT semantics changed to immediately bail. SIGINT is now
 trapped and forwarded to the service's process group.
  - New binary: s6-svperms, implementing a split permissions
 model. (By default, everything is the same as before.)
+ - New binary: s6-usertree-maker, creating service directories
+for supervision trees managed by users.
 
 
 In 2.9.2.0
diff --git a/doc/index.html b/doc/index.html
index fdeb902..28883e1 100644
--- a/doc/index.html
+++ b/doc/index.html
@@ -261,6 +261,12 @@ synchronization</a>.
 <li><a href="ucspilogd.html">The <tt>ucspilogd</tt> program</a></li>
 </ul>
 
+<h4> Management of user supervision trees </h4>
+
+<ul>
+<li><a href="s6-usertree-maker.html">The <tt>s6-usertree-maker</tt> program</a></li>
+</ul>
+
 <h4> Timed lock acquisition </h4>
 
 <ul>
diff --git a/doc/s6-usertree-maker.html b/doc/s6-usertree-maker.html
new file mode 100644
index 0000000..4756006
--- /dev/null
+++ b/doc/s6-usertree-maker.html
@@ -0,0 +1,240 @@
+<html>
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta http-equiv="Content-Language" content="en" />
+    <title>s6: the s6-usertree-maker program</title>
+    <meta name="Description" content="s6: the s6-usertree-maker program" />
+    <meta name="Keywords" content="s6 command s6-usertree-maker user supervision tree s6-svscan" />
+    <!-- <link rel="stylesheet" type="text/css" href="//skarnet.org/default.css" /> -->
+  </head>
+<body>
+
+<p>
+<a href="index.html">s6</a><br />
+<a href="//skarnet.org/software/">Software</a><br />
+<a href="//skarnet.org/">skarnet.org</a>
+</p>
+
+<h1> The s6-usertree-maker program </h1>
+
+<p>
+s6-usertree-maker creates a <a href="servicedir.html">service directory</a>
+implementing a service that runs a <a href="s6-svscan.html">s6-svscan</a>
+instance owned by a given user, on a <a href="scandir.html">scan directory</a>
+belonging to that user. It is meant to help admins deploy systems where
+each user has their own supervision subtree, rooted in the main supervision
+tree owned by root.
+</p>
+
+<p>
+ Alternatively, s6-usertree-maker can create source definition directories
+for the <a href="//skarnet.org/software/s6-rc/">s6-rc</a> service manager.
+</p>
+
+<h2> Interface </h2>
+
+<pre>
+     s6-usertree-maker \
+       [ -d <em>userscandir</em> ] \
+       [ -p <em>path</em> ] \
+       [ -E <em>envdir</em> [ -e <em>var</em> -e <em>var</em> ... ] ] \
+       [ -r <em>service</em>/<em>logger</em>/<em>pipeline</em> ] \
+       [ -l <em>loguser</em> ] \
+       [ -t <em>stamptype</em> ] \
+       [ -n <em>nfiles</em> ] \
+       [ -s <em>filesize</em> ] \
+       [ -S <em>maxsize</em> ] \
+       user logdir dir
+</pre>
+
+<p>
+s6-usertree-maker creates a service directory in <em>dir</em>, that launches
+a supervision tree as user <em>user</em> on scan directory <em>userscandir</em>,
+with a catch-all logger logging the tree's output via
+<a href="s6-log.html">s6-log</a> to the <em>logdir</em> directory.
+</p>
+
+<h2> Exit codes </h2>
+
+<ul>
+ <li> 0: success </li>
+ <li> 100: wrong usage </li>
+ <li> 111: system call failed </li>
+</ul>
+
+<h2> Options </h2>
+
+<ul>
+ <li> <tt>-d</tt>&nbsp;<em>userscandir</em>&nbsp;: the supervision tree will be run
+on the <em>userscandir</em> directory. <em>userscandir</em> is subject to variable
+substitution (see below). Default is <strong><tt>${HOME}/service</tt></strong>. </li> <br />
+
+ <li> <tt>-p</tt>&nbsp;<em>path</em>&nbsp;: the supervision tree will be run with a
+PATH environment variable set to <em>path</em>. <em>path</em> is subject to variable
+substitution. Default is <strong><tt>/usr/bin:/bin</tt></strong>, or whatever has been
+given to the <tt>--with-default-path</tt> option to skalibs' configure script. </li> <br />
+
+ <li> <tt>-E</tt>&nbsp;<em>envdir</em>&nbsp;: the supervision tree will be run with
+the environment variables defined in the directory <em>envdir</em>, which will be
+read via <a href="s6-envdir.html">s6-envdir</a> without options. By default, no
+envdir is defined and the supervision tree will only be run with the basic
+environment variables listed below. </li> <br />
+
+ <li> <tt>-e</tt>&nbsp;<em>var</em>&nbsp;: Perform variable substitution on <em>var</em>.
+This option is repeatable, and only makes sense when the <tt>-E</tt> option is also
+given. For every <em>var</em> listed via a <tt>-e</tt> option, the contents of
+<em>var</em> will be subjected to variable substitution before the supervision tree
+is run. This is only useful if <em>var</em> is defined in <em>envdir</em>, as a
+template, that is then instanced for <em>user</em> when the service is run. By
+default, only the PATH environment variable, customizable via <tt>-p</tt>, is
+subjected to variable substitution. </li> <br />
+
+ <li> <tt>-r</tt>&nbsp;<em>service</em>/<em>logger</em>/<em>pipeline</em>&nbsp;:
+create <a href="//skarnet.org/software/s6-rc">s6-rc</a> source definition directories.
+When this option is given, <em>dir</em> is not created as a service directory, but
+as a directory containing two services: <em>dir</em>/<em>service</em> and
+<em>dir</em>/<em>logger</em>, and <em>dir</em> is suitable as a source argument to
+<a href="//skarnet.org/software/s6-rc/s6-rc-compile.html">s6-rc-compile</a>. The
+<tt>/</tt><em>pipeline</em> part can be omitted, but if it is present, <em>pipeline</em>
+is used as a name for a bundle containing both <em>service</em> and <em>logger</em>.
+When this option is not given, <em>dir</em> is a regular service directory for direct
+inclusion (or linking) in the parent scan directory (and the catch-all logger for
+the user subtree is declared in <em>dir</em><tt>/log</tt>). </li> <br />
+
+ <li> <tt>-l</tt>&nbsp;<em>loguser</em>&nbsp;: run the catch-all logger of the user
+subdirectory as user <em>loguser</em>. Default is <strong><tt>root</tt></strong>. </li> <br />
+
+ <li> <tt>-t</tt>&nbsp;<em>stamptype</em>&nbsp;: how
+logs are timestamped by the catch-all logger. 0 means no
+timestamp, 1 means
+<a href="http://cr.yp.to/libtai/tai64.html">external TAI64N format</a>,
+2 means
+<a href="http://www.iso.org/iso/home/standards/iso8601.htm">ISO 8601 format</a>,
+and 3 means both. Default is <strong><tt>1</tt></strong>. </li> <br />
+
+  <li> <tt>-n</tt>&nbsp;<em>nfiles</em>&nbsp;: maximum number of archive files
+in <em>logdir</em>. Default is <strong><tt>10</tt></strong>. </li> <br />
+
+  <li> <tt>-s</tt>&nbsp;<em>filesize</em>&nbsp;: maximum size of the <tt>current</tt>
+file (and archive files) in <em>logdir</em>. Default is <strong><tt>1000000</tt></strong>. </li> <br />
+
+  <li> <tt>-S</tt>&nbsp;<em>maxsize</em>&nbsp;: maximum total size of the
+archives in the <em>logdir</em>. Default is <strong><tt>0</tt></strong>,
+meaning no limits apart from those enforced by the <tt>-n</tt> and
+<tt>-s</tt> options. </li> <br />
+</ul>
+
+<h2> Operation of the service </h2>
+
+<p>
+ When the service is started, its run script will execute the following
+operations:
+</p>
+
+<ul>
+ <li> Clear all its environment variables, except PATH. This prevents
+any data leak from the parent supervision tree into the user subtree. </li>
+ <li> Fill its environment with data related to <em>user</em>:
+  <ul>
+   <li> USER is set to <em>user</em> </li>
+   <li> HOME is set to <em>user</em>'s home directory </li>
+   <li> UID is set to <em>user</em>'s uid </li>
+   <li> GID is set to <em>user</em>'s primary gid </li>
+   <li> GIDLIST is set to <em>user</em>'s supplementary groups list </li>
+  </ul> </li>
+ <li> If the service has been created with the <tt>-E</tt> option to s6-usertree-maker:
+  <ul>
+   <li> Add all the variables defined in <em>envdir</em> to its environment </li>
+   <li> For every variable <em>var</em> given via a <tt>-e</tt> option, subject
+<em>var</em> to substitution with the USER, HOME, UID, GID and GIDLIST variables
+(see below). </li>
+  </ul> </li>
+ <li> Set the PATH environment variable to <em>path</em>, subjected to
+variable substitution. </li>
+ <li> Execute into <a href="s6-svscan.html">s6-svscan</a>, running in
+<em>userscandir</em> (which is first subjected to variable substitution). </li>
+</ul>
+
+<p>
+ The service is logged: its stderr and stdout are piped to a
+<a href="s6-log.html">s6-log</a> process running as <em>loguser</em> and
+writing to the <em>logdir</em> directory. This logger is the catch-all logger
+for the supervision tree owned by <em>user</em>; it is recommended to make
+<em>loguser</em> distinct from <em>user</em>, and to have <em>logdir</em>
+in a place that is <strong>not</strong> under the control of <em>user</em>.
+If <em>user</em> wants to keep control of their logs, they can declare a
+logger for each of their services.
+</p>
+
+<h2> Variable substitution </h2>
+
+<p>
+ When the service starts, the USER, HOME, UID, GID and GIDLIST
+environment variables are deduced from <em>user</em>'s identity.
+The value of those variables may be used in a few configuration
+knobs:
+</p>
+
+<ul>
+ <li> The value of <em>userscandir</em>: it is likely that the
+scan directory belonging to <em>user</em> resides under <em>user</em>'s
+home directory. Or under <tt>/run/user/${UID}</tt>, or some similar
+scheme. </li>
+ <li> The PATH environment variable, declared in <em>path</em>: it is
+often useful to prepend the default system PATH with a user-specific
+directory that hosts that user's binaries. For instance, you may want
+the PATH to be set as something like <tt>${HOME}/bin:/usr/bin:/bin</tt>. </li>
+ <li> Any variable declared in <em>envdir</em> and given as an argument
+to a <tt>-e</tt> option to s6-usertree-maker. If <em>envdir</em> is a
+template valid for all users, it may contain variables that depends on
+user-specific data: for instance, the XDG_CONFIG_HOME variable may be
+set to <tt>${HOME}/.config</tt>. </li>
+</ul>
+
+<p>
+ When the strings <tt>${USER}</tt>, <tt>${HOME}</tt>, <tt>${UID}</tt>,
+<tt>${GID}</tt>, or <tt>${GIDLIST}</tt> appear in the value for
+<em>userscandir</em>, <em>path</em>, or any of the <em>var</em>
+variables, they are substituted with the corresponding value of the USER,
+HOME, UID, GID, or GIDLIST environment variable instead.
+</p>
+
+<p>
+ For instance, if no <tt>-d</tt> option is provided, the default value
+for <em>userscandir</em> is <tt>${HOME}/service</tt>. If the provided
+<em>user</em> is <tt>ska</tt> and ska's home directory is <tt>/home/ska</tt>,
+then <a href="s6-svscan.html">s6-svscan</a> will be run on
+<tt>/home/ska/service</tt>.
+</p>
+
+<h2> Example </h2>
+
+<pre>
+     s6-usertree-maker -d '/run/user/${UID}/service' -p '${HOME}/bin:/usr/bin:/bin' -E /etc/user-env -e XDG_CONFIG_HOME -l catchlog ska /var/log/usertree/ska usertree-ska
+</pre>
+
+<p>
+ creates a service directory in <tt>usertree-ska</tt> declaring a service that
+starts a supervision tree on <tt>/run/user/1000/service</tt> if ska has uid 1000,
+with <tt>/home/ska/bin:/usr/bin/bin</tt> as its PATH if ska's home directory is
+<tt>/home/ska</tt>, and with all the environment variables declared in
+<tt>/etc/user-env</tt>, among which the XDG_CONFIG_HOME variable is processed
+for variable substitution. The supervision tree has a catch-all logger running
+as user catchlog, and storing its data in the <tt>/var/log/usertree/ska</tt>
+directory.
+</p>
+
+<h2> Notes </h2>
+
+<ul>
+ <li> s6-usertree-maker makes use of the fact that
+<a href="//skarnet.org/software/execline/">execline</a> scripts are much
+easier to generate programmatically and to harden than shell scripts, so it is only
+built if s6 is built with <a href="//skarnet.org/software/execline/">execline</a>
+support - i.e. the <tt>--disable-execline</tt> switch has <em>not</em> been given
+to configure. </li>
+</ul>
+
+</body>
+</html>
diff --git a/doc/upgrade.html b/doc/upgrade.html
index 5619304..fd70083 100644
--- a/doc/upgrade.html
+++ b/doc/upgrade.html
@@ -41,6 +41,8 @@ directories. <a href="s6-supervise.html">s6-supervise</a> now always starts
 it child as a session leader. </li>
  <li> Split permissions on service control are now officially supported.
 New binary: <a href="s6-svperms.html">s6-svperms</a>. </li>
+ <li> New program that creates service directories to run a supervision tree
+managed by a user: <a href="s6-usertree-maker.html">s6-usertree-maker</a>. </li>
 </ul>
 
 <h2> in 2.9.2.0 </h2>
diff --git a/package/deps.mak b/package/deps.mak
index e5394cd..53cf864 100644
--- a/package/deps.mak
+++ b/package/deps.mak
@@ -133,6 +133,7 @@ src/supervision/s6-svstat.o src/supervision/s6-svstat.lo: src/supervision/s6-svs
 src/supervision/s6-svwait.o src/supervision/s6-svwait.lo: src/supervision/s6-svwait.c src/supervision/s6-svlisten.h
 src/supervision/s6_svlisten_loop.o src/supervision/s6_svlisten_loop.lo: src/supervision/s6_svlisten_loop.c src/supervision/s6-svlisten.h src/include/s6/ftrigr.h src/include/s6/s6-supervise.h
 src/supervision/s6_svlisten_signal_handler.o src/supervision/s6_svlisten_signal_handler.lo: src/supervision/s6_svlisten_signal_handler.c src/supervision/s6-svlisten.h
+src/usertree/s6-usertree-maker.o src/usertree/s6-usertree-maker.lo: src/usertree/s6-usertree-maker.c src/include/s6/config.h
 
 s6-accessrules-cdb-from-fs: EXTRA_LIBS := -lskarnet ${SOCKET_LIB} ${SYSCLOCK_LIB}
 s6-accessrules-cdb-from-fs: src/conn-tools/s6-accessrules-cdb-from-fs.o
@@ -253,3 +254,5 @@ s6-svstat: EXTRA_LIBS := -lskarnet ${SYSCLOCK_LIB}
 s6-svstat: src/supervision/s6-svstat.o ${LIBS6}
 s6-svwait: EXTRA_LIBS := -lskarnet ${SOCKET_LIB} ${SYSCLOCK_LIB} ${SPAWN_LIB}
 s6-svwait: src/supervision/s6-svwait.o src/supervision/s6_svlisten_loop.o ${LIBS6}
+s6-usertree-maker: EXTRA_LIBS := -lskarnet
+s6-usertree-maker: src/usertree/s6-usertree-maker.o
diff --git a/package/modes b/package/modes
index d8690ea..b457f7d 100644
--- a/package/modes
+++ b/package/modes
@@ -61,3 +61,4 @@ s6-fdholder-setdump		0755
 s6-fdholder-setdumpc		0755
 s6-fdholder-transferdump	0755
 s6-fdholder-transferdumpc	0755
+s6-usertree-maker		0755
diff --git a/package/targets.mak b/package/targets.mak
index ca447b8..32971b9 100644
--- a/package/targets.mak
+++ b/package/targets.mak
@@ -58,3 +58,7 @@ s6-setuidgid
 LIBEXEC_TARGETS := s6lockd-helper
 
 LIB_DEFS := S6=s6
+
+ifneq ($(EXECLINE_LIB),)
+BIN_TARGETS += s6-usertree-maker
+endif
diff --git a/src/usertree/deps-exe/s6-usertree-maker b/src/usertree/deps-exe/s6-usertree-maker
new file mode 100644
index 0000000..e7187fe
--- /dev/null
+++ b/src/usertree/deps-exe/s6-usertree-maker
@@ -0,0 +1 @@
+-lskarnet
diff --git a/src/usertree/s6-usertree-maker.c b/src/usertree/s6-usertree-maker.c
new file mode 100644
index 0000000..9326e82
--- /dev/null
+++ b/src/usertree/s6-usertree-maker.c
@@ -0,0 +1,302 @@
+/* ISC license. */
+
+#include <stdint.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+
+#include <skalibs/config.h>
+#include <skalibs/uint64.h>
+#include <skalibs/types.h>
+#include <skalibs/bytestr.h>
+#include <skalibs/buffer.h>
+#include <skalibs/sgetopt.h>
+#include <skalibs/strerr2.h>
+#include <skalibs/stralloc.h>
+#include <skalibs/djbunix.h>
+#include <skalibs/skamisc.h>
+
+#include <execline/config.h>
+
+#include <s6/config.h>
+
+#define USAGE "s6-usertree-maker [ -d userscandir ] [ -p path ] [ -E envdir [ -e var ... ] ] [ -r service/logger[/pipeline] ] [ -l loguser ] [ -t stamptype ] [ -n nfiles ] [ -s filesize ] [ -S maxsize ] user logdir dir"
+#define dieusage() strerr_dieusage(100, USAGE)
+
+#define VARS_MAX 64
+
+static mode_t mask ;
+static stralloc sa = STRALLOC_ZERO ;
+
+static inline void write_run (char const *runfile, char const *user, char const *sc, char const *path, char const *userenvdir, char const *const *vars, size_t varlen)
+{
+  buffer b ;
+  char buf[2048] ;
+  int fd = open_trunc(runfile) ;
+  if (fd < 0) strerr_diefu3sys(111, "open ", runfile, " for writing") ;
+  buffer_init(&b, &buffer_write, fd, buf, 2048) ;
+  if (!string_quote(&sa, user, strlen(user))) goto errq ;
+  if (buffer_puts(&b, "#!" EXECLINE_SHEBANGPREFIX "execlineb -P\n"
+    EXECLINE_EXTBINPREFIX "fdmove -c 2 1\n"
+    EXECLINE_EXTBINPREFIX "emptyenv -p\n"
+    EXECLINE_EXTBINPREFIX "export USER ") < 0
+   || buffer_put(&b, sa.s, sa.len) < 0
+   || buffer_puts(&b, "\n"
+    S6_EXTBINPREFIX "s6-envuidgid -i -- ") < 0
+   || buffer_put(&b, sa.s, sa.len) < 0
+   || buffer_puts(&b, "\n"
+    EXECLINE_EXTBINPREFIX "backtick -in HOME { "
+    EXECLINE_EXTBINPREFIX "homeof ") < 0
+   || buffer_put(&b, sa.s, sa.len) < 0
+   || buffer_put(&b, " }\n", 3) < 0) goto err ;
+  sa.len = 0 ;
+  if (userenvdir)
+  {
+    if (!string_quote(&sa, userenvdir, strlen(userenvdir))) goto errq ;
+    if (buffer_puts(&b, S6_EXTBINPREFIX "s6-envdir -i -- ") < 0
+     || buffer_put(&b, sa.s, sa.len) < 0
+     || buffer_put(&b, "\n", 1) < 0) goto err ;
+    sa.len = 0 ;
+  }
+  if (buffer_puts(&b, EXECLINE_EXTBINPREFIX "multisubstitute\n{\n"
+   "  importas -i USER USER\n"
+   "  importas -i HOME HOME\n"
+   "  importas -i UID UID\n"
+   "  importas -i GID GID\n"
+   "  importas -i GIDLIST GIDLIST\n}\n") < 0) goto err ;
+  if (userenvdir && varlen)
+  {
+    if (buffer_puts(&b, EXECLINE_EXTBINPREFIX "multisubstitute\n{\n") < 0) goto err ;
+    for (size_t i = 0 ; i < varlen ; i++)
+    {
+      if (!string_quote(&sa, vars[i], strlen(vars[i]))) goto errq ;
+      if (buffer_puts(&b, "  importas -D \"\" -- ") < 0
+       || buffer_put(&b, sa.s, sa.len) < 0
+       || buffer_put(&b, " ", 1) < 0
+       || buffer_put(&b, sa.s, sa.len) < 0
+       || buffer_put(&b, "\n", 1) < 0) goto err ;
+      sa.len = 0 ;
+    }
+    if (buffer_put(&b, "}\n", 2) < 0) goto err ;
+    for (size_t i = 0 ; i < varlen ; i++)
+    {
+      if (!string_quote(&sa, vars[i], strlen(vars[i]))) goto errq ;
+      if (buffer_puts(&b, EXECLINE_EXTBINPREFIX "export ") < 0
+       || buffer_put(&b, sa.s, sa.len) < 0
+       || buffer_put(&b, " ${", 3) < 0
+       || buffer_put(&b, sa.s, sa.len) < 0
+       || buffer_put(&b, "}\n", 2) < 0) goto err ;
+      sa.len = 0 ;
+    }
+  }
+  if (!string_quote(&sa, path, strlen(path))) goto errq ;
+  if (buffer_puts(&b, EXECLINE_EXTBINPREFIX "export PATH ") < 0
+   || buffer_put(&b, sa.s, sa.len) < 0) goto err ;
+  sa.len = 0 ;
+  if (!string_quote(&sa, sc, strlen(sc))) goto errq ;
+  if (buffer_puts(&b, "\n"
+    S6_EXTBINPREFIX "s6-svscan -d3 -- ") < 0
+   || buffer_put(&b, sa.s, sa.len) < 0) goto err ;
+  sa.len = 0 ;
+  if (!buffer_putflush(&b, "\n", 1)) goto err ;
+  fd_close(fd) ;
+  return ;
+ err:
+  strerr_diefu2sys(111, "write to ", runfile) ;
+ errq:
+  strerr_diefu1sys(111, "quote string") ;
+}
+
+static inline void write_logrun (char const *runfile, char const *loguser, char const *logdir, unsigned int stamptype, unsigned int nfiles, uint64_t filesize, uint64_t maxsize)
+{
+  buffer b ;
+  char buf[1024] ;
+  char fmt[UINT64_FMT] ;
+  int fd = open_trunc(runfile) ;
+  if (fd < 0) strerr_diefu3sys(111, "open ", runfile, " for writing") ;
+  buffer_init(&b, &buffer_write, fd, buf, 1024) ;
+  if (buffer_puts(&b, "#!" EXECLINE_SHEBANGPREFIX "execlineb -P\n") < 0) goto err ;
+  if (loguser)
+  {
+    if (buffer_puts(&b, S6_EXTBINPREFIX "s6-setuidgid ") < 0) goto err ;
+    if (!string_quote(&sa, loguser, strlen(loguser))) strerr_diefu1sys(111, "quote string") ;
+    if (buffer_put(&b, sa.s, sa.len) < 0 || buffer_put(&b, "\n", 1) < 0) goto err ;
+    sa.len = 0 ;
+  }
+  if (buffer_puts(&b, S6_EXTBINPREFIX "s6-log -bd3 -- ") < 0) goto err ;
+  if (stamptype & 1 && buffer_put(&b, "t ", 2) < 0) goto err ;
+  if (stamptype & 2 && buffer_put(&b, "T ", 2) < 0) goto err ;
+  if (buffer_put(&b, "n", 1) < 0
+   || buffer_put(&b, fmt, uint_fmt(fmt, nfiles)) < 0
+   || buffer_put(&b, " s", 2) < 0
+   || buffer_put(&b, fmt, uint64_fmt(fmt, filesize)) < 0
+   || buffer_put(&b, " ", 1) < 0) goto err ;
+  if (maxsize)
+  {
+    if (buffer_put(&b, "S", 1) < 0
+     || buffer_put(&b, fmt, uint64_fmt(fmt, maxsize)) < 0
+     || buffer_put(&b, " ", 1) < 0) goto err ;
+  }
+  if (!string_quote(&sa, logdir, strlen(logdir))) strerr_diefu1sys(111, "quote string") ;
+  if (buffer_put(&b, sa.s, sa.len) < 0 || buffer_put(&b, "\n", 1) < 0) goto err ;
+  sa.len = 0 ;
+
+  if (!buffer_flush(&b)) goto err ;
+  fd_close(fd) ;
+  return ;
+ err:
+  strerr_diefu2sys(111, "write to ", runfile) ;
+}
+
+static void write_service (char const *dir, char const *user, char const *sc, char const *logger, char const *path, char const *userenvdir, char const *const *vars, size_t varlen)
+{
+  size_t dirlen = strlen(dir) ;
+  char fn[dirlen + 17] ;
+  memcpy(fn, dir, dirlen) ;
+  memcpy(fn + dirlen, "/notification-fd", 17) ;
+  if (!openwritenclose_unsafe(fn, "3\n", 2)) strerr_diefu2sys(111, "write to ", fn) ;
+  memcpy(fn + dirlen + 1, "run", 4) ;
+  write_run(fn, user, sc, path, userenvdir, vars, varlen) ;
+  if (logger)
+  {
+    struct iovec v[2] = { { .iov_base = (char *)logger, .iov_len = strlen(logger) }, { .iov_base = "\n", .iov_len = 1 } } ;
+    memcpy(fn + dirlen + 1, "type", 5) ;
+    if (!openwritenclose_unsafe(fn, "longrun\n", 8)) strerr_diefu2sys(111, "write to ", fn) ;
+    memcpy(fn + dirlen + 1, "producer-for", 13) ;
+    if (!openwritevnclose_unsafe(fn, v, 2)) strerr_diefu2sys(111, "write to ", fn) ;
+  }
+  else
+  {
+    if (chmod(fn, mask | ((mask >> 2) & 0111)) < 0)
+      strerr_diefu2sys(111, "chmod ", fn) ;
+  }
+}
+
+static void write_logger (char const *dir, char const *user, char const *logdir, unsigned int stamptype, unsigned int nfiles, uint64_t filesize, uint64_t maxsize, char const *service, char const *pipelinename)
+{
+  size_t dirlen = strlen(dir) ;
+  char fn[dirlen + 17] ;
+  if (mkdir(dir, 0755) < 0) strerr_diefu2sys(111, "mkdir ", dir) ;
+  memcpy(fn, dir, dirlen) ;
+  memcpy(fn + dirlen, "/notification-fd", 17) ;
+  if (!openwritenclose_unsafe(fn, "3\n", 2)) strerr_diefu2sys(111, "write to ", fn) ;
+  memcpy(fn + dirlen + 1, "run", 4) ;
+  write_logrun(fn, user, logdir, stamptype, nfiles, filesize, maxsize) ;
+  if (service)
+  {
+    struct iovec v[2] = { { .iov_base = (char *)service, .iov_len = strlen(service) }, { .iov_base = "\n", .iov_len = 1 } } ;
+    memcpy(fn + dirlen + 1, "type", 5) ;
+    if (!openwritenclose_unsafe(fn, "longrun\n", 8)) strerr_diefu2sys(111, "write to ", fn) ;
+    memcpy(fn + dirlen + 1, "consumer-for", 13) ;
+    if (!openwritevnclose_unsafe(fn, v, 2)) strerr_diefu2sys(111, "write to ", fn) ;
+    if (pipelinename)
+    {
+      v[0].iov_base = (char *)pipelinename ;
+      v[0].iov_len = strlen(pipelinename) ;
+      memcpy(fn + dirlen + 1, "pipeline-name", 14) ;
+      if (!openwritevnclose_unsafe(fn, v, 2)) strerr_diefu2sys(111, "write to ", fn) ;
+    }
+  }
+  else
+  {
+    if (chmod(fn, mask | ((mask >> 2) & 0111)) < 0)
+      strerr_diefu2sys(111, "chmod ", fn) ;
+  }
+}
+
+int main (int argc, char *const *argv)
+{
+  char const *userscandir = "${HOME}/service" ;
+  char const *path = SKALIBS_DEFAULTPATH ;
+  char const *userenvdir = 0 ;
+  char *rcinfo[3] = { 0, 0, 0 } ;
+  char const *loguser = 0 ;
+  unsigned int stamptype = 1 ;
+  unsigned int nfiles = 10 ;
+  uint64_t filesize = 1000000 ;
+  uint64_t maxsize = 0 ;
+  size_t dirlen ;
+  size_t varlen = 0 ;
+  char const *vars[VARS_MAX] ;
+  PROG = "s6-usertree-maker" ;
+  {
+    subgetopt_t l = SUBGETOPT_ZERO ;
+    for (;;)
+    {
+      int opt = subgetopt_r(argc, (char const *const *)argv, "d:p:E:e:r:l:t:n:s:S:", &l) ;
+      if (opt == -1) break ;
+      switch (opt)
+      {
+        case 'd' : userscandir = l.arg ; break ;
+        case 'p' : path = l.arg ; break ;
+        case 'E' : userenvdir = l.arg ; break ;
+        case 'e' :
+          if (varlen >= VARS_MAX) strerr_dief1x(100, "too many -v variables") ;
+          if (strchr(l.arg, '=')) strerr_dief2x(100, "invalid variable name: ", l.arg) ;
+          vars[varlen++] = l.arg ;
+          break ;
+        case 'r' : rcinfo[0] = (char *)l.arg ; break ;
+        case 'l' : loguser = l.arg ; break ;
+        case 't' : if (!uint0_scan(l.arg, &stamptype)) dieusage() ; break ;
+        case 'n' : if (!uint0_scan(l.arg, &nfiles)) dieusage() ; break ;
+        case 's' : if (!uint640_scan(l.arg, &filesize)) dieusage() ; break ;
+        case 'S' : if (!uint640_scan(l.arg, &maxsize)) dieusage() ; break ;
+        default : dieusage() ;
+      }
+    }
+    argc -= l.ind ; argv += l.ind ;
+  }
+  if (argc < 3) dieusage() ;
+  if (argv[1][0] != '/') strerr_dief1x(100, "logdir must be absolute") ;
+  if (userscandir[0] != '/' && !str_start(userscandir, "${HOME}/"))
+    strerr_dief1x(100, "userscandir must be absolute or start with ${HOME}/") ;
+  if (stamptype > 3) strerr_dief1x(100, "stamptype must be 0, 1, 2 or 3") ;
+  if (rcinfo[0])
+  {
+    if (strchr(rcinfo[0], '\n'))
+      strerr_dief2x(100, "newlines", " are forbidden in s6-rc names") ;
+    if (rcinfo[0][0] == '/')
+      strerr_dief2x(100, "service", " name cannot be empty") ;
+    rcinfo[1] = strchr(rcinfo[0], '/') ;
+    if (!rcinfo[1]) strerr_dief1x(100, "argument to -r must be: service/logger or service/logger/pipeline") ;
+    *rcinfo[1]++ = 0 ;
+    if (!rcinfo[1][0]) strerr_dief1x(100, "argument to -r must be: service/logger or service/logger/pipeline") ;
+    if (rcinfo[1][0] == '/')
+      strerr_dief2x(100, "logger", " name cannot be empty") ;
+    rcinfo[2] = strchr(rcinfo[1], '/') ;
+    if (rcinfo[2])
+    {
+      *rcinfo[2]++ = 0 ;
+      if (!rcinfo[2][0]) strerr_dief2x(100, "pipeline", " name cannot be empty") ;
+      if (strchr(rcinfo[2], '/')) strerr_dief2x(100, "slashes", " are forbidden in s6-rc names") ;
+    }
+  }
+  mask = umask(0) ;
+  umask(mask) ;
+  mask = ~mask & 0666 ;
+
+  if (mkdir(argv[2], 0755) < 0) strerr_diefu2sys(111, "mkdir ", argv[2]) ;
+  dirlen = strlen(argv[2]) ;
+  if (rcinfo[0])
+  {
+    size_t svclen = strlen(rcinfo[0]) ;
+    size_t loglen = strlen(rcinfo[1]) ;
+    char dir[dirlen + 2 + svclen > loglen ? svclen : loglen] ;
+    memcpy(dir, argv[2], dirlen) ;
+    dir[dirlen] = '/' ;
+    memcpy(dir + dirlen + 1, rcinfo[0], svclen + 1) ;
+    if (mkdir(dir, 0755) < 0) strerr_diefu2sys(111, "mkdir ", dir) ;
+    write_service(dir, argv[0], userscandir, rcinfo[1], path, userenvdir, vars, varlen) ;
+    memcpy(dir + dirlen + 1, rcinfo[1], loglen + 1) ;
+    write_logger(dir, loguser, argv[1], stamptype, nfiles, filesize, maxsize, rcinfo[0], rcinfo[2]) ;
+  }
+  else
+  {
+    char dir[dirlen + 5] ;
+    memcpy(dir, argv[2], dirlen) ;
+    memcpy(dir + dirlen, "/log", 5) ;
+    write_service(argv[2], argv[0], userscandir, 0, path, userenvdir, vars, varlen) ;
+    write_logger(dir, loguser, argv[1], stamptype, nfiles, filesize, maxsize, 0, 0) ;
+  }
+  return 0 ;
+}