sema: New program for hacking with semaphores.
authorMark Wooding <mdw@distorted.org.uk>
Sun, 6 Mar 2016 22:25:44 +0000 (22:25 +0000)
committerMark Wooding <mdw@distorted.org.uk>
Tue, 3 May 2016 08:49:54 +0000 (09:49 +0100)
This one needs some surprising support features.

  * C++ can find out what the maximum `time_t' value is, even though C
    can't.

  * Introduce a magic `compiler_fence' function, to force things into
    memory while we synchronize hackily with a signal handler.

Makefile.am
configure.ac
debian/control
debian/inst
fence.c [new file with mode: 0644]
fence.h [new file with mode: 0644]
sema.1 [new file with mode: 0644]
sema.c [new file with mode: 0644]
timemax.cc [new file with mode: 0644]
timemax.h [new file with mode: 0644]

index 79da5b4..2521b3d 100644 (file)
@@ -110,6 +110,17 @@ gorp_LDADD          = $(catacomb_LIBS)
 dist_man_MANS          += gorp.1
 endif
 
+## sema
+if HAVE_MLIB
+bin_PROGRAMS           += sema
+sema_SOURCES            = sema.c
+sema_SOURCES           += fence.c fence.h
+sema_SOURCES           += timemax.cc timemax.h
+sema_LDADD              = $(mLib_LIBS)
+sema_LINK               = $(LINK) # don't need C++ libraries here
+dist_man_MANS          += sema.1
+endif
+
 ## cdb tools
 if HAVE_LIBCDB
 bin_PROGRAMS           += cdb-probe cdb-check-domain
index bf1b725..4724381 100644 (file)
@@ -49,6 +49,8 @@ dnl C programming environment.
 ## Compiler.
 AC_PROG_CC
 AX_CFLAGS_WARN_ALL
+AC_PROG_CXX
+AX_CXXFLAGS_WARN_ALL
 
 ## Libraries.
 OLIBS=$LIBS
index 3e234a7..428958e 100644 (file)
@@ -180,6 +180,16 @@ Description: Identify and fix problematic whitespace in text files.
  It can safely update files in place, and could therefore be used as part
  of a commit hook.
 
+Package: sema
+Architecture: any
+Depends: ${shlibs:Depends}
+Section: utils
+Description: Perform simple operations on SysV semaphores.
+ The sema program performs simple operations on (System V-style) semaphores.
+ It's not intended to be a utility for general-purpose hacking on existing
+ semaphores, but rather a tool for doing synchronization between shell
+ scripts or other simple programs.
+
 Package: x86-model
 Architecture: any-i386 any-amd64
 Depends: ${shlibs:Depends}
index f1c7b02..0169cd9 100644 (file)
@@ -44,6 +44,7 @@ stamp.1 stamp /usr/share/man/man1
 unfwd distorted-mail /usr/bin
 unfwd.1 distorted-mail /usr/share/man/man1
 xtitle xtitle /usr/bin
+xtitle.1 xtitle /usr/share/man/man1
 z zz /usr/bin
 z.1 zz /usr/share/man/man1
 x86-model x86-model /usr/bin
@@ -54,3 +55,5 @@ with-umask with-umask /usr/bin
 with-umask.1 with-umask /usr/share/man/man1
 sshsvc-mkauthkeys sshsvc-mkauthkeys /usr/bin
 sshsvc-mkauthkeys.1 sshsvc-mkauthkeys /usr/share/man/man1
+sema sema /usr/bin
+sema.1 sema /usr/share/man/man1
diff --git a/fence.c b/fence.c
new file mode 100644 (file)
index 0000000..c57abf6
--- /dev/null
+++ b/fence.c
@@ -0,0 +1,45 @@
+/* -*-c-*-
+ *
+ * Bodge to control memory access ordering
+ *
+ * (c) 2016 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "fence.h"
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* --- @compiler_fence@ --- *
+ *
+ * Arguments:  some pointers to things
+ *
+ * Returns:    Nothing.
+ *
+ * Use:                Actually doesn't do anything at all, but in a separate
+ *             translation unit so that the compiler can't tell that.  So it
+ *             won't be able to move memory accesses to the addressed
+ *             objects across the call.
+ */
+
+void compiler_fence(void *p, ...) { }
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/fence.h b/fence.h
new file mode 100644 (file)
index 0000000..a78b9f7
--- /dev/null
+++ b/fence.h
@@ -0,0 +1,59 @@
+/* -*-c-*-
+ *
+ * Bodge to control memory access ordering
+ *
+ * (c) 2016 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef FENCE_H
+#define FENCE_H
+
+#ifdef __cplusplus
+  extern "C" {
+#endif
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <mLib/macros.h>
+
+/*----- Functions provided ------------------------------------------------*/
+
+/* --- @compiler_fence@ --- *
+ *
+ * Arguments:  some pointers to things
+ *
+ * Returns:    Nothing.
+ *
+ * Use:                Actually doesn't do anything at all, but in a separate
+ *             translation unit so that the compiler can't tell that.  So it
+ *             won't be able to move memory accesses to the addressed
+ *             objects across the call.
+ */
+
+#define FENCE_END ((void *)0)
+EXECL_LIKE(0) void compiler_fence(void */*p*/, ...);
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#ifdef __cplusplus
+  }
+#endif
+
+#endif
diff --git a/sema.1 b/sema.1
new file mode 100644 (file)
index 0000000..3c81b23
--- /dev/null
+++ b/sema.1
@@ -0,0 +1,370 @@
+.TH "sema" 1 "6 March 2016" "Mark Wooding" "Toys"
+.SH NAME
+sema \- operations on (SysV-style) semaphores
+.
+.SH SYNOPSIS
+.B sema
+.RB [ \-w
+.IR when ]
+.I subcmd
+.RI [ args ...]
+.PP
+Subcommands:
+'RS
+.br
+.B mkfile
+.RB [ \-x ]
+.RB [ \-m
+.IR mode ]
+.I name
+.br
+.B new
+.RB [ \-x ]
+.RB [ \-m
+.IR mode ]
+.I name
+.I value
+.br
+.B rm
+.RB [ \-f ]
+.I name
+.br
+.B get
+.I name
+.br
+.B set
+.I name
+.I value
+.br
+.B post
+.RB [ \-n
+.IR count ]
+.I name
+.br
+.B wait
+.RB [ \-n
+.IR count ]
+.I name
+.RI [ cmd
+.IR args ...]
+.RE
+.
+.SH DESCRIPTION
+The
+.B sema
+program performs simple operations
+on (System V-style) semaphores.
+It's not intended to be a utility for
+general-purpose hacking on
+existing semaphores,
+but rather a tool for doing synchronization
+between shell scripts or other simple programs.
+The two biggest limitations of
+.B sema
+are that it only ever acts on the first semaphore of a set,
+and it has no way to change the project-id
+(see
+.BR ftok (3)).
+.SS Common features
+Semaphores are identified by a
+.IR name .
+Currently, this is always a pathname,
+though additional ways of designating semaphore sets
+may be added in later versions,
+so an open-ended syntax is used:
+a
+.I name
+has the form
+.IP
+.RI [ type \c
+.BR : ] \c
+.I value
+.PP
+Currently defined
+.IR type s
+and the required
+.IR value s
+are as follows.
+.TP
+.B file
+The
+.I value
+is a pathname to a file in the filesystem.
+The file's inode number and other information
+are converted to an IPC key using
+.BR ftok (3)
+which is used to fetch a semaphore id using
+.BR semget (2).
+.PP
+The
+.I type
+may be omitted if it is
+.B file
+and the
+.I value
+does not contain a
+.RB ` : '
+before its first
+.RB ` / '
+(if any);
+as a result,
+most plain pathnames can be used directly.
+.PP
+The command-line options accepted before the subcommand name
+are as follows.
+.TP
+.B "\-h, \-\-help"
+Write a help message to standard output
+and exit successfully.
+.TP
+.B "\-v, \-\-version"
+Write a version number to standard output
+and exit successfully.
+.TP
+.BI "\-w, \-\-wait " duration
+Nearly all operations on SysV semaphores may need to block
+for an extended period of time.
+This is obvious for waiting, but, alas,
+semaphore initialization is poorly designed,
+and
+.I every
+operation which expects a properly initialized semaphore
+has little choice but to wait until the semaphore has been set up.
+.IP
+If
+.I duration
+is
+.B forever
+(the default)
+then
+.B sema
+will wait indefinitely until the thing it's waiting for happens.
+If
+.I duration
+is
+.B none
+or
+.B never
+then
+.B sema
+will exit immediately,
+with status 251,
+instead of waiting.
+Otherwise,
+.I
+duration
+should be a
+(possibly fractional \(en i.e., with a decimal point)
+number followed by an optional unit suffix:
+.RB ` s '
+for seconds (the default),
+.RB ` m '
+for minutes,
+.RB ` h '
+for hours, and
+.RB ` d '
+for days.
+.SS Exit status
+.IP 0
+Success.
+.IP 251
+The semaphore did not become available
+within the period
+.B sema
+was told to wait.
+.IP 252
+The semaphore was not properly initialized
+within the period
+.B sema
+was told to wait.
+.IP 253
+An error was detected in
+.BR sema 's
+command-line arguments.
+.IP 254
+A system error occurred.
+.IP Other
+Exit status from the program executed by the
+.B wait
+subcommand.
+.PP
+If
+.B sema
+detects an error,
+it writes a human-readable description of the problem
+to standard error and exits with one of the above status codes.
+The codes have been chosen so as not to conflict
+with those commonly used by other programs,
+so that errors from
+.B sema
+itself can be distinguished from problems encountered
+by the program executed by the
+.B wait
+subcommand,
+but the available space for error codes is small
+and conflicts are inevitable.
+.
+.SH SUBCOMMANDS
+.SS mkfile
+Create the file designated by the semaphore
+.I name
+argument.
+(An error is reported if the
+.I name
+does not designate a pathname.)
+.PP
+Options are as follows.
+.TP
+.BI "\-m, \-\-mode " mode
+Set the permissions for the new file.
+The
+.I mode
+should be a numeric file permission specification, in octal,
+as for
+.BR chmod (1).
+The default is to allow read and write according to the process umask.
+.TP
+.B "\-x, \-\-exclusive"
+Fail if the file already exists.
+.SS new
+Create a new semaphore set containing a single semaphore
+with the given
+.I name
+and initial
+.IR value .
+Options are as follows.
+.TP
+.BI "\-m, \-\-mode " mode
+Set the permissions for the new file.
+The
+.I mode
+should be a numeric file permission specification, in octal;
+the read and write bits determine whether the owner, group and others
+can read and change the semaphore;
+the execute bits are ignored.
+The default is to allow read and write according to the process umask.
+.TP
+.B "\-x, \-\-exclusive"
+Fail if a semaphore set with the given
+.I name
+already exists.
+.SS rm
+Delete the semaphore set with the given
+.IR name .
+.PP
+Options are as follows.
+.TP
+.B "\-f, \-\-force"
+Don't report an error if no semaphore set exists with the given name.
+.SS get
+Fetch the current value of the semaphore with the given
+.I name
+and
+write it, in decimal, to standard outout, followed by a newline.
+.PP
+Because this captures a snapshot of the state
+of an asynchronously changing value,
+this command is only really useful for diagnostic purposes
+or when the system is known to be quiescent.
+.PP
+There are no options.
+.SS set
+Set the value of the semaphore with the given
+.I name
+to
+.IR value .
+.PP
+Because this modifies a the state
+of an asynchronously changing value,
+this command is only really useful
+when the system is known to be quiescent.
+.PP
+There are no options.
+.SS post
+Atomically increment the value of the semaphore with the given
+.IR name .
+This operation can't block
+(though it may still be necessary to wait
+until the semaphore set is initialized).
+.PP
+Options are as follows.
+.TP
+.BI "\-n, \-\-count " count
+Adjust the semaphore value by
+.IR count ,
+which must be a positive integer,
+rather than 1.
+.SS wait
+Wait until the value of the semaphore with the given
+.I name
+is positive and then atomically decrement it.
+.PP
+If a
+.I command
+is provided
+(possibly with some
+.IR arguments )
+then execute it and then increment the semaphore value again
+when it finishes.
+The program is executed directly by
+.BR execvp (3).
+Restoring the semaphore value is reliable:
+it is done by the kernel,
+so there's no risk of some program crashing and
+leaving the semaphore in an inconsistent state;
+though obviously if the program gets stuck
+it will continue to hold the semaphore until it's killed.
+The semaphore is released as soon as the
+.I command
+exits;
+if it forked child processed,
+the semaphore will be released
+and the children will continue to run.
+.PP
+Options are as follows.
+.TP
+.BI "\-n, \-\-count " count
+Adjust the semaphore value by
+.IR count ,
+which must be a positive integer,
+rather than 1.
+.B sema
+will wait until semaphore value is at least
+.IR count ,
+and atomically decrease it by
+.IR count .
+If there is a
+.I command
+then
+.B sema
+arranges for the semaphore value to be increased by
+.I count
+when it exits.
+.
+.SH BUGS
+System V semaphores are remarkably awful.
+POSIX semaphores are superficially much better,
+but actually deficient in a number of ways.
+Most significantly for our purposes,
+there's no analogue of the
+.B SEM_UNDO
+feature,
+so to implement the feature of
+.B wait
+which holds the semaphore during the execution of a command
+.B sema
+would have to wait for it to finish;
+and if
+.B sema
+is killed in the meantime then nobody will fix the semaphore.
+Another important deficiency is that
+POSIX semaphores can only be adjusted a single step at a time,
+so the
+.B \-n
+feature of the
+.B wait
+and
+.B post
+commands can't be implemented satisfactorily.
+.SH AUTHOR
+Mark Wooding, <mdw@distorted.org.uk>
diff --git a/sema.c b/sema.c
new file mode 100644 (file)
index 0000000..e150bcd
--- /dev/null
+++ b/sema.c
@@ -0,0 +1,878 @@
+/* -*-c-*-
+ *
+ * Operations on SysV semaphores
+ *
+ * (c) 2016 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <float.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <sys/types.h>
+#include <sys/ipc.h>
+#include <sys/sem.h>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <mLib/mdwopt.h>
+#include <mLib/quis.h>
+#include <mLib/report.h>
+#include <mLib/tv.h>
+
+#include "fence.h"
+#include "timemax.h"
+
+/* Oh, for pity's sake why did nobody do this for us? */
+union semun {
+  int val;
+  struct semid_ds *buf;
+  unsigned short *array;
+};
+
+/*----- Random utilities --------------------------------------------------*/
+
+static int parse_int(const char *what, const char *p,
+                    int radix, int min, int max)
+{
+  char *q;
+  int oerr;
+  long i = -1;
+
+  oerr = errno;
+  if (isspace((unsigned char)*p)) errno = EINVAL;
+  else i = strtol(p, &q, radix);
+  if (*q || errno || !(min <= i && i <= max))
+    die(253, "invalid %s `%s'", what, p);
+  errno = oerr;
+  return ((int)i);
+}
+
+/*----- Timeout handling --------------------------------------------------*/
+
+enum { NEVER, UNTIL, FOREVER };
+
+struct timeout {
+  int wait;
+  struct timeval tv;
+};
+
+static struct timeval now;
+
+static void update_now(void) { int rc = gettimeofday(&now, 0); assert(!rc); }
+
+static void f2tv(double t, struct timeval *tv)
+  { tv->tv_sec = t; tv->tv_usec = 1e6*(t - tv->tv_sec); }
+
+static void parse_timeout(const char *p, struct timeout *t)
+{
+  double x = 0;
+  char *q;
+  int oerr;
+  time_t t_max = timemax();
+  struct timeval tv;
+
+  if (strcmp(p, "forever") == 0)
+    t->wait = FOREVER;
+  else if (strcmp(p, "none") == 0 || strcmp(p, "never") == 0)
+    t->wait = NEVER;
+  else {
+    oerr = errno; errno = 0;
+    if (isspace((unsigned char)*p)) errno = EINVAL;
+    else x = strtod(p, &q);
+    if (x < 0) errno = ERANGE;
+    if (!errno) {
+      switch (tolower((unsigned char )*q)) {
+       case 'd': x *= 24;
+       case 'h': x *= 60;
+       case 'm': x *= 60;
+       case 's': q++; break;
+      }
+      if (*q) errno = EINVAL;
+    }
+    if (errno) die(253, "invalid timeout specification `%s'", p);
+    if (x >= t_max - now.tv_sec) t->wait = FOREVER;
+    else if (!x) t->wait = NEVER;
+    else { t->wait = UNTIL; f2tv(x, &tv); TV_ADD(&t->tv, &now, &tv); }
+    errno = oerr;
+  }
+}
+
+/*----- Semaphore operations ----------------------------------------------*/
+
+#define SEMA_PROJ 0xd1 /* fair die roll */
+
+enum { NTY_PATH };
+struct nameinfo {
+  unsigned ty;
+  union {
+    const char *path;
+  } u;
+};
+
+static void parse_name(const char *name, struct nameinfo *ni)
+{
+  size_t plen = strcspn(name, ":/");
+  switch (name[plen]) {
+    case 0: case '/': ni->ty = NTY_PATH; ni->u.path = name; break;
+    case ':':
+      if (strncmp(name, "file:", 5) == 0)
+       { ni->ty = NTY_PATH; ni->u.path = name + plen + 1; }
+      else
+       die(253, "unknown name type `%.*s'", (int)plen, name);
+      break;
+  }
+}
+
+static int sema_initialized_p(int semid)
+{
+  union semun u;
+  struct semid_ds sem;
+
+  u.buf = &sem;
+  if (semctl(semid, 0, IPC_STAT, u) < 0) {
+    die(254, "failed to check that semaphore set is initialized: %s",
+       strerror(errno));
+  }
+  return (!!sem.sem_otime);
+}
+
+static void make_sema_file(const char *name, int of, mode_t mode)
+{
+  int fd;
+  struct nameinfo ni;
+
+  parse_name(name, &ni);
+  if (ni.ty != NTY_PATH)
+    die(253, "semaphore name `%s' doesn't designate a path", name);
+
+  if ((fd = open(ni.u.path, O_CREAT | of, mode)) < 0) {
+    die(254, "failed to create semaphore file `%s': %s",
+       ni.u.path, strerror(errno));
+  }
+  close(fd);
+}
+
+#define OF_PROBE 1u
+#define OF_UNINIT 2u
+
+static int open_sema(const char *name, unsigned f, int of,
+                    mode_t mode, int ival,
+                    const struct timeout *t)
+{
+  int ff = mode & 0777, rc, nrace = 5;
+  int semid;
+  union semun u;
+  struct sembuf buf[1];
+  double w, ww;
+  struct nameinfo ni;
+  struct timeval tv, ttv;
+  key_t k;
+
+  /* Turn the name into an IPC key. */
+  parse_name(name, &ni);
+  assert(ni.ty == NTY_PATH);
+  if ((k = ftok(ni.u.path, SEMA_PROJ)) == (key_t)-1) {
+    die(254, "failed to get key from semaphore file `%s': %s",
+       ni.u.path, strerror(errno));
+  }
+
+  for (;;) {
+    /* Oh, horrors.  A newly created semaphore set is uninitialized.  But if
+     * we set IPC_CREAT without IPC_EXCL then we don't have any idea whether
+     * the semaphore set was created or not.  So we have this little dance to
+     * do.
+     */
+    if ((of & (O_CREAT | O_EXCL)) != (O_CREAT | O_EXCL)) {
+      semid = semget(k, 1, ff);
+      if (semid >= 0) goto await;
+      else if (errno != ENOENT) goto fail;
+      else if (ff & O_CREAT) /* try to create -- below */;
+      else if (f & OF_PROBE) return (-1);
+      else goto fail;
+    }
+
+    /* So, here, we have O_CREAT set, and either the semaphore set doesn't
+     * seem to exist yet, or O_EXCL is set and we didn't bother checking yet.
+     * So now we try to create the set.  If that fails with something other
+     * than EEXIST, or we were trying with O_EXCL, then we're done.
+     * Otherwise it's just appeared out of nowhere, so let's briefly try
+     * racing with whoever else it is, but give up if it doesn't look like
+     * we're likely to win.
+     */
+    if ((semid = semget(k, 1, ff | IPC_CREAT | IPC_EXCL)) >= 0) break;
+    else if ((ff & O_EXCL) || errno != EEXIST || nrace--) goto fail;
+  }
+
+  /* Right, we just created the semaphore set.  Now we have to initialize it,
+   * because nobody did that for us.  Set the initial value through semop to
+   * set the sem_otime stamp as an indicator that the set is properly
+   * initialized.
+   */
+  u.val = ival ? 0 : 1;
+  buf[0].sem_num = 0;
+  buf[0].sem_op = ival ? ival : -1;
+  buf[0].sem_flg = IPC_NOWAIT;
+  if (semctl(semid, 0, SETVAL, u) < 0 ||
+      semop(semid, buf, 1) < 0)
+    die(254, "failed to initialize semaphore set: %s", strerror(errno));
+
+  /* Whew. */
+  return (semid);
+
+  /* Make sure that the semaphore set is actually initialized before we
+   * continue.
+   */
+await:
+  if ((f & OF_UNINIT) || sema_initialized_p(semid)) return (semid);
+  if (t->wait == NEVER) goto notready;
+  w = 1e-4;
+  update_now();
+  for (;;) {
+    ww = w/2 + w*rand()/RAND_MAX; f2tv(ww, &tv); TV_ADD(&tv, &tv, &now);
+    if (t->wait == UNTIL) {
+      if (TV_CMP(&now, >=, &t->tv)) goto notready;
+      if (TV_CMP(&tv, >, &t->tv)) tv = t->tv;
+    }
+    for (;;) {
+      TV_SUB(&ttv, &tv, &now);
+      rc = select(0, 0, 0, 0, &ttv);
+      update_now();
+      if (!rc) break;
+      else if (errno != EINTR)
+       die(254, "unexpected error from select: %s", strerror(errno));
+    }
+    if (sema_initialized_p(semid)) return (0);
+    w *= 2;
+    if (w >= 10) w = 10;
+  }
+
+  /* Done. */
+  return (semid);
+
+  /* Report a get failure. */
+fail:
+  die(254, "failed to open semaphore set: %s", strerror(errno));
+
+notready:
+  die(252, "semaphore set not ready");
+}
+
+static void rm_sema(int semid)
+{
+  if (semctl(semid, 0, IPC_RMID))
+    die(254, "failed to remove semaphore set: %s", strerror(errno));
+}
+
+static unsigned read_sema(int semid)
+{
+  int val;
+
+  if ((val = semctl(semid, 0, GETVAL)) < 0)
+    die(254, "failed to read semaphore value: %s", strerror(errno));
+  return (val);
+}
+
+static void set_sema(int semid, unsigned val)
+{
+  union semun u;
+
+  u.val = val;
+  if (semctl(semid, 0, SETVAL, u) < 0)
+    die(254, "failed to set semaphore value: %s", strerror(errno));
+}
+
+static void post_sema(int semid, int by)
+{
+  struct sembuf buf[1];
+
+  buf[0].sem_num = 0;
+  buf[0].sem_op = by;
+  buf[0].sem_flg = 0;
+  if (semop(semid, buf, 1))
+    die(254, "failed to post semaphore: %s", strerror(errno));
+}
+
+static struct sembuf *sigsembuf;
+static int sigflag;
+
+static void wait_alarm(int hunoz)
+  { sigflag = 1; if (sigsembuf) sigsembuf->sem_flg |= IPC_NOWAIT; }
+
+static int wait_sema(int semid, int by, int flg, const struct timeout *t)
+{
+  struct sembuf buf[1];
+  struct sigaction sa, osa;
+  sigset_t ss, oss, ps;
+  struct timeval tv;
+  struct itimerval it, oit;
+  int rc, sig;
+  unsigned f = 0;
+#define f_alarm 1u
+
+  /* Set up the command buffer. */
+  buf[0].sem_num = 0;
+  buf[0].sem_op = -by;
+  buf[0].sem_flg = flg;
+
+  /* Modify it according to the timeout settings. */
+  switch (t->wait) {
+
+    case NEVER:
+      /* Don't wait. */
+      buf[0].sem_flg |= IPC_NOWAIT;
+      break;
+
+    case FOREVER:
+      /* Wait until it's ready. */
+      break;
+
+    case UNTIL:
+      /* Wait until the specified time.  This is fiddly, and will likely
+       * require setting an alarm.
+       */
+
+      /* If the timeout is already in the past, then don't bother with the
+       * alarm, and simply don't wait.
+       */
+      if (TV_CMP(&t->tv, <=, &now)) {
+       buf[0].sem_flg |= IPC_NOWAIT;
+       break;
+      }
+
+      /* Find the current alarm setting. */
+      if (getitimer(ITIMER_REAL, &oit)) {
+       die(254, "failed to read current alarm setting: %s",
+           strerror(errno));
+      }
+
+      /* If that's not set, or it's going to go off after our timeout, then
+       * we should override it temporarily.  Otherwise, just let it do its
+       * (likely lethal) thing.
+       */
+      if (!oit.it_value.tv_sec && !oit.it_value.tv_usec)
+       f |= f_alarm;
+      else {
+       TV_ADD(&oit.it_value, &oit.it_value, &now);
+       if (TV_CMP(&t->tv, <, &oit.it_value)) f |= f_alarm;
+      }
+
+      /* If we are setting an alarm, then now is the time to mess about with
+       * signals and timers.
+       */
+      if (f & f_alarm) {
+
+       /* Mask out the signal while we fiddle with the settings. */
+       sigemptyset(&ss);
+       sigaddset(&ss, SIGALRM);
+       if (sigprocmask(SIG_BLOCK, &ss, &oss))
+         die(254, "failed to block alarm signal: %s", strerror(errno));
+
+       /* Establish our new signal handler. */
+       sigsembuf = &buf[0];
+       sigflag = 0;
+       sa.sa_handler = wait_alarm;
+       sa.sa_mask = ss;
+       sa.sa_flags = 0;
+       if (sigaction(SIGALRM, &sa, &osa))
+         die(254, "failed to set alarm handler: %s", strerror(errno));
+
+       /* Set up the timer. */
+       TV_SUB(&it.it_value, &t->tv, &now);
+       it.it_interval.tv_sec = 0; it.it_interval.tv_usec = 0;
+       if (setitimer(ITIMER_REAL, &it, 0))
+         die(254, "failed to set alarm: %s", strerror(errno));
+
+       /* This bit's quite scummy.  There isn't a POSIX-standard way to wait
+        * for a finite time before giving up, so we use alarms.  But there
+        * also isn't a standard way to avoid a race between the alarm going
+        * off and us waiting for the semaphore, because semop doesn't have a
+        * signal set argument.  So instead we have the signal handler frob
+        * the semop parameter block as well as setting a flag.  Assuming
+        * that semop is actually a system call, then one of the following
+        * will happen: the signal will happen before the call, in which case
+        * it will set IPC_NOWAIT and semop will either succeed or fail
+        * immediately; the signal will happen during the call, while we're
+        * waiting for the semaphore, so semop will return with EINTR; or the
+        * signal will happen afterwards, by which point it's too late for us
+        * to care.
+        */
+       compiler_fence(buf, &sigflag, FENCE_END);
+       if (sigprocmask(SIG_UNBLOCK, &ss, 0))
+         die(254, "failed to unblock alarm signal: %s", strerror(errno));
+      }
+      break;
+  }
+
+  /* Wait for the semaphore. */
+  if ((rc = semop(semid, buf, 1)) < 0 &&
+      errno != EAGAIN &&
+      !(errno == EINTR && sigflag))
+    die(254, "failed to post semaphore: %s", strerror(errno));
+
+  /* Now we clean up again afterwards. */
+  update_now();
+
+  /* If we set an alarm, we must dismantle it and restore the previous
+   * situation.
+   */
+  if (f & f_alarm) {
+
+    /* We're messing with the alarm again, so mask it out temporarily. */
+    if (sigprocmask(SIG_BLOCK, &ss, &oss))
+      die(254, "failed to block alarm signal: %s", strerror(errno));
+
+    /* Turn off the alarm timer.  We don't need it any more. */
+    it.it_value.tv_sec = 0; it.it_value.tv_usec = 0;
+    if (setitimer(ITIMER_REAL, &it, 0))
+      die(254, "failed to disarm alarm timer: %s", strerror(errno));
+
+    /* At this point, if there's an alarm signal pending, it must be from the
+     * timer we set, and it's too late to do any good.  So just clear it and
+     * move on.  After this point, alarms will be from the previously
+     * established timer, and we should let them happen.
+     */
+    if (sigpending(&ps))
+      die(254, "failed to read pending signals: %s", strerror(errno));
+    if (sigismember(&ps, SIGALRM) && sigwait(&ss, &sig))
+      die(254, "failed to clear pending alarm: %s", strerror(errno));
+
+    /* Figure out how to restore the old timer. */
+    if (oit.it_value.tv_sec || oit.it_value.tv_usec) {
+      if (TV_CMP(&oit.it_value, >, &now))
+       TV_SUB(&oit.it_value, &oit.it_value, &now);
+      else {
+       /* We should have had an alarm by now.  Schedule one for when we
+        * unblock the signal again.
+        */
+       raise(SIGALRM);
+
+       /* Sort out the timer again. */
+       if (!oit.it_interval.tv_sec && !oit.it_interval.tv_usec)
+         oit.it_value.tv_sec = 0, oit.it_value.tv_usec = 0;
+       else {
+         TV_SUB(&tv, &now, &oit.it_value);
+         if (TV_CMP(&tv, <, &oit.it_interval))
+           TV_SUB(&oit.it_value, &oit.it_interval, &tv);
+         else {
+           /* We've overshot the previous deadline, and missed at least one
+            * repetition.  If this is bad, then we've already failed quite
+            * miserably.
+            */
+           oit.it_value = oit.it_interval;
+         }
+       }
+      }
+    }
+
+    /* We've now recovered the right settings, even if that's just to turn
+     * the alarm off, so go do that.
+     */
+    if ((oit.it_value.tv_sec || oit.it_value.tv_usec) &&
+       setitimer(ITIMER_REAL, &oit, 0))
+      die(254, "failed to restore old alarm settings: %s", strerror(errno));
+
+    /* Restore the old signal handler. */
+    if (sigaction(SIGALRM, &osa, 0))
+      die(254, "failed to restore old alarm handler: %s", strerror(errno));
+
+    /* And finally we can restore the previous signal mask. */
+    if (sigprocmask(SIG_SETMASK, &oss, 00))
+      die(254, "failed to restore old signal mask: %s", strerror(errno));
+  }
+
+  return (rc ? -1 : 0);
+}
+
+/*----- Main code ---------------------------------------------------------*/
+
+static struct timeout timeout = { FOREVER };
+
+static int cmd_mkfile(int argc, char *argv[])
+{
+  mode_t mode = 0666, mask = 0;
+  const char *name = 0;
+  int of = 0;
+  unsigned f = 0;
+#define f_bogus 1u
+#define f_mode 2u
+
+  for (;;) {
+    static const struct option opts[] = {
+      { "mode",                        OPTF_ARGREQ,    0,      'm' },
+      { "exclusive",           0,              0,      'x' },
+      { 0,                     0,              0,      0 }
+    };
+    int o = mdwopt(argc, argv, "m:x", opts, 0, 0, OPTF_NOPROGNAME);
+
+    if (o < 0) break;
+    switch (o) {
+      case 'm':
+       mode = parse_int("mode", optarg, 8, 0, 07777);
+       f |= f_mode;
+       break;
+      case 'x': of |= O_EXCL; break;
+      default: f |= f_bogus; break;
+    }
+  }
+  argc -= optind; argv += optind;
+  if (argc < 1) f |= f_bogus;
+  else { name = argv[0]; argc--; argv++; }
+  if (argc) f |= f_bogus;
+  if (f & f_bogus) return (-1);
+
+  if (f & f_mode) mask = umask(0);
+  make_sema_file(name, of, mode);
+  if (f & f_mode) umask(mask);
+  return (0);
+
+#undef f_bogus
+#undef f_mode
+}
+
+static int cmd_new(int argc, char *argv[])
+{
+  mode_t mode = 0, mask;
+  const char *name = 0;
+  unsigned init = 0;
+  int of = O_CREAT;
+  unsigned f = 0;
+#define f_bogus 1u
+#define f_mode 2u
+
+  for (;;) {
+    static const struct option opts[] = {
+      { "mode",                        OPTF_ARGREQ,    0,      'm' },
+      { "exclusive",           0,              0,      'x' },
+      { 0,                     0,              0,      0 }
+    };
+    int o = mdwopt(argc, argv, "m:x", opts, 0, 0, OPTF_NOPROGNAME);
+
+    if (o < 0) break;
+    switch (o) {
+      case 'm':
+       mode = parse_int("mode", optarg, 8, 0, 0777);
+       f |= f_mode;
+       break;
+      case 'x': of |= O_EXCL; break;
+      default: f |= f_bogus; break;
+    }
+  }
+  argc -= optind; argv += optind;
+  if (argc < 2) f |= f_bogus;
+  else {
+    name = argv[0];
+    init = parse_int("initial semaphore value", argv[1],
+                    0, 0, SEM_VALUE_MAX);
+    argc -= 2; argv += 2;
+  }
+  if (argc) f |= f_bogus;
+  if (f & f_bogus) return (-1);
+
+  if (!(f & f_mode)) {
+    mask = umask(0); umask(mask);
+    mode = ~mask & 0777;
+  }
+  open_sema(name, 0, of, mode, init, &timeout);
+  return (0);
+
+#undef f_bogus
+#undef f_mode
+}
+
+static int cmd_rm(int argc, char *argv[])
+{
+  const char *name = 0;
+  unsigned f = 0, ff = 0;
+  int semid;
+#define f_bogus 1u
+
+  for (;;) {
+    static const struct option opts[] = {
+      { "force",               0,              0,      'f' },
+      { 0,                     0,              0,      0 }
+    };
+    int o = mdwopt(argc, argv, "f", opts, 0, 0, OPTF_NOPROGNAME);
+
+    if (o < 0) break;
+    switch (o) {
+      case 'f': ff |= OF_PROBE; break;
+      default: f |= f_bogus; break;
+    }
+  }
+  argc -= optind; argv += optind;
+  if (!argc) f |= f_bogus;
+  else { name = argv[0]; argc--; argv++; }
+  if (argc) f |= f_bogus;
+  if (f & f_bogus) return (-1);
+
+  semid = open_sema(name, ff | OF_UNINIT, 0, 0, 0, &timeout);
+  if (semid >= 0) rm_sema(semid);
+
+  return (0);
+
+#undef f_bogus
+}
+
+static int cmd_set(int argc, char *argv[])
+{
+  const char *name = 0;
+  int semid;
+  unsigned n = 0;
+  unsigned f = 0;
+#define f_bogus 1u
+
+  for (;;) {
+    static const struct option opts[] = {
+      { 0,                     0,              0,      0 }
+    };
+    int o = mdwopt(argc, argv, "", opts, 0, 0, OPTF_NOPROGNAME);
+
+    if (o < 0) break;
+    switch (o) {
+      default: f |= f_bogus; break;
+    }
+  }
+  argc -= optind; argv += optind;
+  if (argc < 2) f |= f_bogus;
+  else {
+    name = argv[0];
+    n = parse_int("count", argv[1], 0, 0, SEM_VALUE_MAX);
+    argc -= 2; argv += 2;
+  }
+  if (argc) f |= f_bogus;
+  if (f & f_bogus) return (-1);
+
+  semid = open_sema(name, 0, 0, 0, 0, &timeout);
+  set_sema(semid, n);
+  return (0);
+
+#undef f_bogus
+}
+
+static int cmd_get(int argc, char *argv[])
+{
+  const char *name = 0;
+  int semid;
+  unsigned f = 0;
+#define f_bogus 1u
+
+  for (;;) {
+    static const struct option opts[] = {
+      { 0,                     0,              0,      0 }
+    };
+    int o = mdwopt(argc, argv, "", opts, 0, 0, OPTF_NOPROGNAME);
+
+    if (o < 0) break;
+    switch (o) {
+      default: f |= f_bogus; break;
+    }
+  }
+  argc -= optind; argv += optind;
+  if (!argc) f |= f_bogus;
+  else { name = argv[0]; argc--; argv++; }
+  if (argc) f |= f_bogus;
+  if (f & f_bogus) return (-1);
+
+  semid = open_sema(name, 0, 0, 0, 0, &timeout);
+  printf("%u\n", read_sema(semid));
+  return (0);
+
+#undef f_bogus
+}
+
+static int cmd_post(int argc, char *argv[])
+{
+  const char *name = 0;
+  int semid, n = 1;
+  unsigned f = 0;
+#define f_bogus 1u
+
+  for (;;) {
+    static const struct option opts[] = {
+      { "count",               OPTF_ARGREQ,    0,      'n' },
+      { 0,                     0,              0,      0 }
+    };
+    int o = mdwopt(argc, argv, "n:", opts, 0, 0, OPTF_NOPROGNAME);
+
+    if (o < 0) break;
+    switch (o) {
+      case 'n': n = parse_int("count", optarg, 0, 1, SEM_VALUE_MAX); break;
+      default: f |= f_bogus; break;
+    }
+  }
+  argc -= optind; argv += optind;
+  if (!argc) f |= f_bogus;
+  else { name = argv[0]; argc--; argv++; }
+  if (argc) f |= f_bogus;
+  if (f & f_bogus) return (-1);
+
+  semid = open_sema(name, 0, 0, 0, 0, &timeout);
+  post_sema(semid, n);
+  return (0);
+
+#undef f_bogus
+}
+
+static int cmd_wait(int argc, char *argv[])
+{
+  const char *name = 0;
+  int semid, n = 1;
+  int flg = 0;
+  unsigned f = 0;
+#define f_bogus 1u
+
+  for (;;) {
+    static const struct option opts[] = {
+      { "count",               OPTF_ARGREQ,    0,      'n' },
+      { 0,                     0,              0,      0 }
+    };
+    int o = mdwopt(argc, argv, "-n:", opts, 0, 0, OPTF_NOPROGNAME);
+
+    if (o < 0) break;
+    switch (o) {
+      case 'n': n = parse_int("count", optarg, 0, 1, SEM_VALUE_MAX); break;
+      case 0:
+       if (!name) name = optarg;
+       else { optind--; goto done_opts; }
+       break;
+      default: f |= f_bogus; break;
+    }
+  }
+done_opts:
+  argc -= optind; argv += optind;
+  if (!name) {
+    if (!argc) f |= f_bogus;
+    else { name = argv[0]; argc--; argv++; }
+  }
+  if (argc) flg |= SEM_UNDO;
+  if (f & f_bogus) return (-1);
+
+  semid = open_sema(name, 0, 0, 0, 0, &timeout);
+  if (wait_sema(semid, n, flg, &timeout))
+    die(251, "semaphore busy");
+
+  if (argc) {
+    execvp(argv[0], argv);
+    die(254, "failed to exec `%s': %s", argv[0], strerror(errno));
+  }
+  return (0);
+
+#undef f_bogus
+}
+
+static const struct subcmd {
+  const char *name;
+  int (*func)(int, char *[]);
+  const char *usage;
+} subcmds[] = {
+  { "mkfile", cmd_mkfile,      "[-x] [-m MODE] NAME" },
+  { "new", cmd_new,            "[-x] [-m MODE] NAME VALUE" },
+  { "rm", cmd_rm,              "[-f] NAME" },
+  { "get", cmd_get,            "NAME" },
+  { "set", cmd_set,            "NAME VALUE" },
+  { "post", cmd_post,          "[-n COUNT] NAME" },
+  { "wait", cmd_wait,          "[-n COUNT] NAME [COMMAND ARGUMENTS ...]" },
+  { 0, 0,              0 }
+};
+
+static void version(FILE *fp)
+  { pquis(fp, "$, version " VERSION "\n"); }
+
+static void usage(FILE *fp)
+{
+  const struct subcmd *c;
+
+  fprintf(fp, "usage:\n");
+  for (c = subcmds; c->name; c++)
+    fprintf(fp, "\t%s [-OPTIONS] %s %s\n", QUIS, c->name, c->usage);
+}
+
+static void help(FILE *fp)
+{
+  version(fp); putc('\n', fp);
+  usage(fp);
+  fprintf(fp, "\n\
+Top-level options:\n\
+\n\
+  -w, --wait=WHEN              How long to wait (`forever', `never', or\n\
+                                 nonnegative number followed by `s', `m',\n\
+                                 `h', `d')\n");
+}
+
+int main(int argc, char *argv[])
+{
+  const struct subcmd *c;
+  int rc;
+  unsigned f = 0;
+#define f_bogus 1u
+
+  ego(argv[0]);
+  update_now();
+  for (;;) {
+    static const struct option opts[] = {
+      { "help",                        0,              0,      'h' },
+      { "version",             0,              0,      'v' },
+      { "wait",                        OPTF_ARGREQ,    0,      'w' },
+      { 0,                     0,              0,      0 }
+    };
+    int o = mdwopt(argc, argv, "+hvw:", opts, 0, 0, 0);
+
+    if (o < 0) break;
+    switch (o) {
+      case 'h': help(stdout); exit(0); break;
+      case 'v': version(stdout); exit(0); break;
+      case 'w': parse_timeout(optarg, &timeout); break;
+      default: f |= f_bogus; break;
+    }
+  }
+  argc -= optind; argv += optind;
+  if (!argc) f |= f_bogus;
+  if (f & f_bogus) { usage(stderr); exit(253); }
+
+  optind = 0;
+  for (c = subcmds; c->name; c++)
+    if (strcmp(argv[0], c->name) == 0) goto found;
+  die(253, "unknown command `%s'", argv[0]);
+found:
+  argc--; argv++;
+  rc = c->func(argc, argv);
+  if (rc < 0) {
+    fprintf(stderr, "usage: %s %s %s\n", QUIS, c->name, c->usage);
+    exit(253);
+  }
+  return (rc);
+
+#undef f_bogus
+}
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/timemax.cc b/timemax.cc
new file mode 100644 (file)
index 0000000..f02cba5
--- /dev/null
@@ -0,0 +1,49 @@
+/* -*-c++-*-
+ *
+ * Return the largest allowable time_t value
+ *
+ * (c) 2016 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <ctime>
+#include <limits>
+
+#include "timemax.h"
+
+using namespace std;
+
+/*----- Main code ---------------------------------------------------------*/
+
+/* --- @timemax@ --- *
+ *
+ * Arguments:  ---
+ *
+ * Returns:    The largest value repesentable in an object of type @time_t@.
+ *
+ * Use:                For some stupid reason there isn't a @TIME_MAX@ anywhere, and
+ *             it's really hard to discover the right value in C.  But it's
+ *             quite easy in C++, so that's what we do.
+ */
+
+time_t timemax() { return numeric_limits<time_t>::max(); }
+
+/*----- That's all, folks -------------------------------------------------*/
diff --git a/timemax.h b/timemax.h
new file mode 100644 (file)
index 0000000..a20bb9e
--- /dev/null
+++ b/timemax.h
@@ -0,0 +1,57 @@
+/* -*-c-*-
+ *
+ * Return the largest allowable @time_t@ value
+ *
+ * (c) 2016 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef TIMEMAX_H
+#define TIMEMAX_H
+
+#ifdef __cplusplus
+  extern "C" {
+#endif
+
+/*----- Header files ------------------------------------------------------*/
+
+#include <time.h>
+
+/*----- Functions provided ------------------------------------------------*/
+
+/* --- @timemax@ --- *
+ *
+ * Arguments:  ---
+ *
+ * Returns:    The largest value repesentable in an object of type @time_t@.
+ *
+ * Use:                For some stupid reason there isn't a @TIME_MAX@ anywhere, and
+ *             it's really hard to discover the right value in C.  But it's
+ *             quite easy in C++, so that's what we do.
+ */
+
+time_t timemax(void);
+
+/*----- That's all, folks -------------------------------------------------*/
+
+#ifdef __cplusplus
+  }
+#endif
+
+#endif