svc - Initial commit, preliminary service manager
authorMatthew Dillon <dillon@apollo.backplane.com>
Tue, 11 Nov 2014 06:54:42 +0000 (22:54 -0800)
committerMatthew Dillon <dillon@apollo.backplane.com>
Tue, 11 Nov 2014 06:54:42 +0000 (22:54 -0800)
* This is a preliminary service manager frontend and demon.
  It is not yet ready for prime-time (lots of support bits
  are missing).

* The main premise of this program is to be able to create
  and manage services in a more robust fashion, obtain status,
  log recent output or all output, and so forth.

  Among the many features, this program uses the new REAPER support
  to ensure that all processes related to the service can be accounted
  for.

* Includes a comprehensive manual page that lists all target features
  (many of which are not yet implemented as of this commit).

sbin/Makefile
sbin/svc/Makefile [new file with mode: 0644]
sbin/svc/execute.c [new file with mode: 0644]
sbin/svc/remote.c [new file with mode: 0644]
sbin/svc/subs.c [new file with mode: 0644]
sbin/svc/svc.8 [new file with mode: 0644]
sbin/svc/svc.c [new file with mode: 0644]
sbin/svc/svc.h [new file with mode: 0644]

index 655b4fa..c7d7569 100644 (file)
@@ -91,6 +91,7 @@ SUBDIR=       adjkerntz \
        startslip \
        swapon \
        sysctl \
+       svc \
        tcplay \
        tunefs \
        udevd \
diff --git a/sbin/svc/Makefile b/sbin/svc/Makefile
new file mode 100644 (file)
index 0000000..f9ddd9c
--- /dev/null
@@ -0,0 +1,11 @@
+# service manager
+#
+
+PROG=  svc
+MAN=   svc.8
+SRCS=  svc.c subs.c remote.c execute.c
+#WARNS?=       2
+CFLAGS+=       -pthread
+LDADD=         -lpthread
+
+.include <bsd.prog.mk>
diff --git a/sbin/svc/execute.c b/sbin/svc/execute.c
new file mode 100644 (file)
index 0000000..b40a371
--- /dev/null
@@ -0,0 +1,545 @@
+/*
+ * Copyright (c) 2014 The DragonFly Project.  All rights reserved.
+ *
+ * This code is derived from software contributed to The DragonFly Project
+ * by Matthew Dillon <dillon@backplane.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ * 3. Neither the name of The DragonFly Project nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific, prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+/*
+ * Handle remote listen/connect and parsing operations.
+ */
+
+#include "svc.h"
+
+pthread_mutex_t serial_mtx;
+time_t LastStart;      /* uptime */
+time_t LastStop;       /* uptime */
+pid_t DirectPid = -1;
+runstate_t RunState = RS_STOPPED;
+command_t *InitCmd;
+int RestartCounter;
+
+int
+execute_init(command_t *cmd)
+{
+        char buf[32];
+        pid_t pid;
+       pid_t stoppingpid = -1;
+       time_t nextstop = 0;
+       int lfd;        /* unix domain listen socket */
+       int pfd;        /* pid file */
+       int xfd;
+       int rc;
+       int fds[2];
+       char c;
+
+       if (cmd->label == NULL || cmd->ext_ac == 0) {
+               fprintf(cmd->fp, "init requires a label and command\n");
+               return 1;
+       }
+       fprintf(cmd->fp, "initializing new service: %s\n", cmd->label);
+
+       if (pipe(fds) < 0) {
+               fprintf(cmd->fp, "Unable to create pipe: %s\n",
+                       strerror(errno));
+               return 1;
+       }
+       if ((xfd = open("/dev/null", O_RDWR)) < 0) {
+               fprintf(cmd->fp, "Unable to open /dev/null: %s\n",
+                       strerror(errno));
+               return 1;
+       }
+
+       /*
+        * Setup pidfile and unix domain listen socket and lock the
+        * pidfile.
+        */
+       rc = setup_pid_and_socket(cmd, &lfd, &pfd);
+       if (rc) {
+               close(fds[0]);
+               close(fds[1]);
+               return rc;
+       }
+
+       /*
+        * Detach the service
+        */
+        if ((pid = fork()) != 0) {
+                /*
+                 * Parent
+                 */
+               close(fds[1]);
+                if (pid < 0) {
+                        fprintf(cmd->fp, "fork failed: %s\n", strerror(errno));
+                } else {
+                       /*
+                        * Fill-in pfd before returning.
+                        */
+                       snprintf(buf, sizeof(buf), "%d\n", (int)pid);
+                       write(pfd, buf, strlen(buf));
+               }
+               close(lfd);
+               close(pfd);
+
+               /*
+                * Wait for child to completely detach from the tty
+                * before returning.
+                */
+               read(fds[0], &c, 1);
+               close(fds[0]);
+
+               return 0;
+        }
+
+       /*
+        * Service demon is now running.
+        *
+        * Detach from terminal, setup logfile.
+        */
+       close(fds[0]);
+       fclose(cmd->fp);
+       if (cmd->logfile)
+               cmd->fp = fopen(cmd->logfile, "a");
+       else
+               cmd->fp = fdopen(dup(xfd), "a");
+       if (xfd != 0) {
+               dup2(xfd, 0);
+               close(xfd);
+       }
+       dup2(fileno(cmd->fp), 1);
+       dup2(fileno(cmd->fp), 2);
+       if ((xfd = open("/dev/tty", O_RDWR)) >= 0) {
+               ioctl(xfd, TIOCNOTTY, 0);       /* no controlling tty */
+               close(xfd);
+       }
+       setsid();                               /* new session */
+
+       /*
+        * Signal parent that we are completely detached now.
+        */
+       c = 1;
+       write(fds[1], &c, 1);
+       close(fds[1]);
+       InitCmd = cmd;
+
+       /*
+        * Start accept thread for unix domain listen socket.
+        * Start service
+        */
+       remote_listener(cmd, lfd);
+       pthread_mutex_lock(&serial_mtx);
+       execute_start(cmd);
+
+       /*
+        * Become the reaper for all children recursively.
+        */
+       if (procctl(P_PID, getpid(), PROC_REAP_ACQUIRE, NULL) < 0) {
+               fprintf(cmd->fp, "svc is unable to become the "
+                                "reaper for its children\n");
+               fflush(cmd->fp);
+       }
+
+       /*
+        * Main loop is the reaper
+        */
+       for (;;) {
+               union reaper_info info;
+               struct timespec ts;
+               int status;
+               int dt;
+               pid_t usepid;
+
+               /*
+                * If we are running just block doing normal reaping,
+                * if we are stopping we have to poll for reaping while
+                * we handle stopping.
+                */
+               if (RunState == RS_STARTED) {
+                       pthread_mutex_unlock(&serial_mtx);
+                       pid = wait3(&status, 0, NULL);
+                       pthread_mutex_lock(&serial_mtx);
+               } else {
+                       pid = wait3(&status, WNOHANG, NULL);
+               }
+               clock_gettime(CLOCK_MONOTONIC_FAST, &ts);
+
+               if (pid > 0) {
+                       if (pid == DirectPid) {
+                               fprintf(cmd->fp,
+                                       "svc %s: lost direct child %d\n",
+                                       cmd->label, pid);
+                               fflush(cmd->fp);
+                               DirectPid = -1;
+                               if (cmd->restart_some) {
+                                       RunState = RS_STOPPED;
+                                       LastStop = ts.tv_sec;
+                               } /* else still considered normal run state */
+                       } else {
+                               /* reap random disconnected child */
+                               fprintf(cmd->fp,
+                                       "svc %s: reap indirect child %d\n",
+                                       cmd->label,
+                                       (int)pid);
+                       }
+               }
+
+               /*
+                * Calculate the pid to potentially act on and/or
+                * determine if any children still exist.
+                */
+               if (DirectPid >= 0) {
+                       usepid = DirectPid;
+               } else if (procctl(P_PID, getpid(),
+                                  PROC_REAP_STATUS, &info) == 0) {
+                       usepid = info.status.pid_head;
+               } else {
+                       usepid = -1;
+               }
+
+               /*
+                * If stoppingpid changes we have to reset the TERM->KILL
+                * timer.
+                */
+               if (usepid < 0) {
+                       if (RunState != RS_STOPPED) {
+                               RunState = RS_STOPPED;
+                               LastStop = ts.tv_sec;
+                       }
+               } else if (stoppingpid != usepid &&
+                          (RunState == RS_STOPPING2 ||
+                           RunState == RS_STOPPING3)) {
+                       RunState = RS_STOPPING1;
+               }
+               stoppingpid = usepid;
+
+               /*
+                * State machine
+                */
+               switch(RunState) {
+               case RS_STARTED:
+                       if (usepid < 0) {
+                               RunState = RS_STOPPED;
+                               LastStop = ts.tv_sec;
+                       }
+                       break;
+               case RS_STOPPED:
+                       dt = (int)(ts.tv_sec - LastStop);
+
+                       if (cmd->exit_mode) {
+                               /*
+                                * Service demon was told to exit on service
+                                * stop (-x passed to init).
+                                */
+                               fprintf(cmd->fp,
+                                       "svc %s: service demon exiting\n",
+                                       cmd->label);
+                               exit(0);
+                       } else if (cmd->manual_stop) {
+                               /*
+                                * Service demon was told to stop via
+                                * commanded (not automatic) action.  We
+                                * do not auto-restart the service in
+                                * this situation.
+                                */
+                               pthread_mutex_unlock(&serial_mtx);
+                               if (dt < 0 || dt > 60)
+                                       sleep(60);
+                               else
+                                       sleep(1);
+                               pthread_mutex_lock(&serial_mtx);
+                       } else if (cmd->restart_some || cmd->restart_all) {
+                               /*
+                                * Handle automatic restarts
+                                */
+                               if (dt > cmd->restart_timo) {
+                                       execute_start(cmd);
+                               } else {
+                                       pthread_mutex_unlock(&serial_mtx);
+                                       sleep(1);
+                                       pthread_mutex_lock(&serial_mtx);
+                               }
+                       } else {
+                               /*
+                                * No automatic restart was configured,
+                                * wait for commanded action.
+                                */
+                               pthread_mutex_unlock(&serial_mtx);
+                               if (dt < 0 || dt > 60)
+                                       sleep(60);
+                               else
+                                       sleep(1);
+                               pthread_mutex_lock(&serial_mtx);
+                       }
+                       break;
+               case RS_STOPPING1:
+                       /*
+                        * Reset TERM->KILL timer
+                        */
+                       nextstop = ts.tv_sec;
+                       RunState = RS_STOPPING2;
+                       /* fall through */
+               case RS_STOPPING2:
+                       if (cmd->termkill_timo == 0) {
+                               nextstop = ts.tv_sec - 1;
+                       } else {
+                               kill(stoppingpid, SIGTERM);
+                               fprintf(cmd->fp, "svc %s: sigterm %d\n",
+                                       cmd->label, stoppingpid);
+                               sleep(1);
+                       }
+                       RunState = RS_STOPPING3;
+                       /* fall through */
+               case RS_STOPPING3:
+                       dt = (int)(ts.tv_sec - nextstop);
+                       if (dt > cmd->termkill_timo) {
+                               fprintf(cmd->fp, "svc %s: sigkill %d\n",
+                                       cmd->label, stoppingpid);
+                               kill(stoppingpid, SIGKILL);
+                       }
+                       sleep(1);
+                       break;
+               }
+       }
+       pthread_mutex_unlock(&serial_mtx);
+       exit(0);
+       /* does not return */
+}
+
+int
+execute_start(command_t *cmd)
+{
+       struct timespec ts;
+       int maxwait = 60;
+
+       while (RunState == RS_STOPPING1 ||
+              RunState == RS_STOPPING2 ||
+              RunState == RS_STOPPING3) {
+               fprintf(cmd->fp,
+                       "svc %s: Waiting for previous action to complete\n",
+                       cmd->label);
+               fflush(cmd->fp);
+               pthread_mutex_unlock(&serial_mtx);
+               sleep(1);
+               pthread_mutex_lock(&serial_mtx);
+               if (--maxwait == 0) {
+                       fprintf(cmd->fp,
+                               "svc %s: Giving up waiting for action\n",
+                               cmd->label);
+                       fflush(cmd->fp);
+                       break;
+               }
+       }
+       if (RunState == RS_STARTED) {
+               fprintf(cmd->fp, "svc %s: Already started pid %d\n",
+                       cmd->label, DirectPid);
+               fflush(cmd->fp);
+               return 0;
+       }
+
+       clock_gettime(CLOCK_MONOTONIC_FAST, &ts);
+       if ((DirectPid = fork()) == 0) {
+               closefrom(3);
+               execvp(InitCmd->ext_av[0], InitCmd->ext_av);
+               exit(99);
+       }
+       if (DirectPid >= 0) {
+               RunState = RS_STARTED;
+               LastStart = ts.tv_sec;
+       } else {
+               RunState = RS_STOPPED;
+               LastStop = ts.tv_sec;
+       }
+       InitCmd->manual_stop = 0;
+       fprintf(cmd->fp, "svc %s: Starting pid %d\n", cmd->label, DirectPid);
+       fflush(cmd->fp);
+
+       return 0;
+}
+
+int
+execute_restart(command_t *cmd)
+{
+       int rc;
+
+       rc = execute_stop(cmd) + execute_start(cmd);
+       return rc;
+}
+
+int
+execute_stop(command_t *cmd)
+{
+       struct timespec ts;
+       int save_restart_some;
+       int save_restart_all;
+       int maxwait = 60;
+
+       save_restart_some = InitCmd->restart_some;
+       save_restart_all = InitCmd->restart_all;
+       if (cmd->commanded)
+               InitCmd->manual_stop = 1;
+       if (cmd->commanded && (cmd->restart_some || cmd->restart_all)) {
+               InitCmd->restart_some = cmd->restart_some;
+               InitCmd->restart_all = cmd->restart_all;
+       }
+       fprintf(cmd->fp, "svc %s: Stopping\n", cmd->label);
+       fflush(cmd->fp);
+
+       if (DirectPid >= 0) {
+               kill(DirectPid, SIGTERM);
+       }
+
+       clock_gettime(CLOCK_MONOTONIC_FAST, &ts);
+       LastStop = ts.tv_sec;
+       RunState = RS_STOPPING1;
+
+       /*
+        * If commanded (verses automatic), we are running remote in our
+        * own thread and we need to wait for the action to complete.
+        */
+       if (cmd->commanded) {
+               while (RunState == RS_STOPPING1 ||
+                      RunState == RS_STOPPING2 ||
+                      RunState == RS_STOPPING3) {
+                       fprintf(cmd->fp,
+                               "svc %s: Waiting for service to stop\n",
+                               cmd->label);
+                       fflush(cmd->fp);
+                       pthread_mutex_unlock(&serial_mtx);
+                       sleep(1);
+                       pthread_mutex_lock(&serial_mtx);
+                       if (--maxwait == 0) {
+                               fprintf(cmd->fp,
+                                       "svc %s: Giving up waiting for stop\n",
+                                       cmd->label);
+                               fflush(cmd->fp);
+                               break;
+                       }
+               }
+               if (cmd->restart_some || cmd->restart_all) {
+                       InitCmd->restart_some = save_restart_some;
+                       InitCmd->restart_all = save_restart_all;
+               }
+       }
+
+       return 0;
+}
+
+int
+execute_exit(command_t *cmd)
+{
+       execute_stop(cmd);
+       fprintf(cmd->fp, "svc %s: Exiting\n", cmd->label);
+
+       exit(0);
+}
+
+int
+execute_list(command_t *cmd)
+{
+       fprintf(cmd->fp, "%-16s\n", cmd->label);
+
+       return 0;
+}
+
+int
+execute_status(command_t *cmd)
+{
+       const char *state;
+
+       switch(RunState) {
+       case RS_STOPPED:
+               if (InitCmd && InitCmd->manual_stop)
+                       state = "stopped (manual)";
+               else
+                       state = "stopped";
+               break;
+       case RS_STARTED:
+               state = "running";
+               break;
+       case RS_STOPPING1:
+       case RS_STOPPING2:
+       case RS_STOPPING3:
+               state = "killing";
+               break;
+       default:
+               state = "unknown";
+               break;
+       }
+
+       fprintf(cmd->fp, "%-16s %s\n", cmd->label, state);
+
+       return 0;
+}
+
+int
+execute_log(command_t *cmd __unused)
+{
+       return 0;
+}
+
+int
+execute_logfile(command_t *cmd)
+{
+       char *logfile;
+       int fd;
+       int rc;
+
+       logfile = cmd->logfile;
+       if (cmd->ext_av && cmd->ext_av[0])
+               logfile = cmd->ext_av[0];
+       if (logfile == NULL && InitCmd)
+               logfile = InitCmd->logfile;
+
+       rc = 0;
+       if (InitCmd && logfile) {
+               if (InitCmd->logfile &&
+                   strcmp(InitCmd->logfile, logfile) == 0) {
+                       fprintf(cmd->fp, "svc %s: Reopen logfile %s\n",
+                               cmd->label, logfile);
+               } else {
+                       fprintf(cmd->fp, "svc %s: Change logfile to %s\n",
+                               cmd->label, logfile);
+               }
+               fd = open(logfile, O_WRONLY|O_CREAT|O_APPEND, 0640);
+               if (fd >= 0) {
+                       fflush(stdout);
+                       fflush(stderr);
+                       dup2(fd, fileno(stdout));
+                       dup2(fd, fileno(stderr));
+                       sreplace(&InitCmd->logfile, logfile);
+                       close(fd);
+               } else {
+                       fprintf(cmd->fp,
+                               "svc %s: Unable to open/create \"%s\": %s\n",
+                               cmd->label, logfile, strerror(errno));
+                       rc = 1;
+               }
+       }
+       return rc;
+}
diff --git a/sbin/svc/remote.c b/sbin/svc/remote.c
new file mode 100644 (file)
index 0000000..f39e44e
--- /dev/null
@@ -0,0 +1,369 @@
+/*
+ * Copyright (c) 2014 The DragonFly Project.  All rights reserved.
+ *
+ * This code is derived from software contributed to The DragonFly Project
+ * by Matthew Dillon <dillon@backplane.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ * 3. Neither the name of The DragonFly Project nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific, prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+/*
+ * Handle remote listen/connect and parsing operations.
+ */
+
+#include "svc.h"
+
+typedef struct SvcConnect {
+       struct SvcConnect *next;
+       command_t *cmd;
+       char      *label;
+       pthread_t td;
+       int     active;
+       int     fd;
+       int     rc;
+       FILE    *fpr;
+       FILE    *fpw;
+} connect_t;
+
+static void *remote_connect_thread(void *arg);
+static void *remote_listener_thread(void *arg);
+static void *remote_accepted_thread(void *arg);
+static void remote_issue(connect_t *conn, command_t *cmd);
+static int decode_args(connect_t *conn, char ***avp,
+                       const char *ptr, size_t len);
+
+connect_t *CHead;
+connect_t **CNextP = &CHead;
+
+
+/*
+ * Execute cmd on the running service by connecting to the service, passing-in
+ * the command, and processing results.
+ *
+ * Called only by master process
+ */
+void
+remote_execute(command_t *cmd, const char *label)
+{
+       connect_t *conn = calloc(sizeof(*conn), 1);
+
+       conn->fd = -1;
+       conn->cmd = cmd;
+       conn->label = strdup(label);
+       conn->active = 1;
+       conn->next = *CNextP;
+       *CNextP = conn;
+
+       pthread_create(&conn->td, NULL, remote_connect_thread, conn);
+}
+
+/*
+ * Threaded connect/execute
+ */
+static
+void *
+remote_connect_thread(void *arg)
+{
+       connect_t *conn;
+       command_t *cmd;
+       struct sockaddr_un sou;
+       size_t len;
+       char *ptr;
+
+       conn = arg;
+       cmd = conn->cmd;
+
+       bzero(&sou, sizeof(sou));
+       if ((conn->fd = socket(AF_UNIX, SOCK_STREAM, 0)) >= 0) {
+               sou.sun_family = AF_UNIX;
+               snprintf(sou.sun_path, sizeof(sou.sun_path),
+                        "%s/service.%s.sk", cmd->piddir, conn->label);
+               len = strlen(sou.sun_path);
+               len = offsetof(struct sockaddr_un, sun_path[len+1]);
+
+               if (connect(conn->fd, (void *)&sou, len) < 0) {
+                       close(conn->fd);
+                       conn->fd = -1;
+               }
+       }
+       if (conn->fd >= 0) {
+               /*
+                * Issue command
+                */
+               conn->fpr = fdopen(conn->fd, "r");
+               conn->fpw = fdopen(dup(conn->fd), "w");
+               conn->fd = -1;
+               setvbuf(conn->fpr, NULL, _IOFBF, 0);
+               setvbuf(conn->fpw, NULL, _IOFBF, 0);
+               remote_issue(conn, cmd);
+               while ((ptr = fgetln(conn->fpr, &len)) != NULL) {
+                       if (len == 2 && ptr[0] == '.' && ptr[1] == '\n')
+                               break;
+                       fwrite(ptr, 1, len, cmd->fp);
+                       fflush(cmd->fp);
+               }
+               conn->rc = 0;
+               conn->active = 0;
+               fclose(conn->fpr);
+               fclose(conn->fpw);
+               conn->fpr = NULL;
+               conn->fpw = NULL;
+       } else {
+               /*
+                * Connection failed
+                */
+               fprintf(cmd->fp,
+                       "Unable to connect to service %s\n",
+                       conn->label);
+               conn->rc = 1;
+               conn->active = 0;
+       }
+       return NULL;
+}
+
+/*
+ * Called only by master process
+ *
+ * Collect status from all remote commands.
+ */
+int
+remote_wait(void)
+{
+       connect_t *scan;
+       int rc = 0;
+
+       while ((scan = CHead) != NULL) {
+               if (pthread_join(scan->td, NULL) < 0)
+                       continue;
+               assert(scan->active == 0);
+               rc += scan->rc;
+               CHead = scan->next;
+
+               if (scan->fpr) {
+                       fclose(scan->fpr);
+                       scan->fpr = NULL;
+               }
+               if (scan->fpw) {
+                       fclose(scan->fpw);
+                       scan->fpw = NULL;
+               }
+               if (scan->fd >= 0) {
+                       close(scan->fd);
+                       scan->fd = -1;
+               }
+               if (scan->label) {
+                       free(scan->label);
+                       scan->label = NULL;
+               }
+               scan->cmd = NULL;
+               free(scan);
+       }
+       return rc;
+}
+
+/*
+ * Create the unix domain socket and pid file for the service
+ * and start a thread to accept and process connections.
+ *
+ * Return 0 on success, non-zero if the socket could not be created.
+ */
+void
+remote_listener(command_t *cmd, int lfd)
+{
+       connect_t *conn;
+
+       /*
+        * child, create our unix domain socket listener thread.
+        */
+       conn = calloc(sizeof(*conn), 1);
+       conn->fd = lfd;
+       conn->cmd = cmd;
+       conn->label = strdup(cmd->label);
+       conn->active = 1;
+
+       conn->next = *CNextP;
+       *CNextP = conn;
+       pthread_create(&conn->td, NULL, remote_listener_thread, conn);
+}
+
+static void *
+remote_listener_thread(void *arg)
+{
+       connect_t *lconn = arg;
+       connect_t *conn;
+       struct sockaddr_un sou;
+       socklen_t len;
+
+       conn = calloc(sizeof(*conn), 1);
+       for (;;) {
+               len = sizeof(sou);
+               conn->fd = accept(lconn->fd, (void *)&sou, &len);
+               conn->label = strdup(lconn->label);
+               if (conn->fd < 0) {
+                       if (errno == EINTR)
+                               continue;
+                       break;
+               }
+               pthread_create(&conn->td, NULL, remote_accepted_thread, conn);
+               conn = calloc(sizeof(*conn), 1);
+       }
+       free(conn);
+
+       return NULL;
+}
+
+static void *
+remote_accepted_thread(void *arg)
+{
+       connect_t *conn = arg;
+       command_t cmd;
+       char *ptr;
+       size_t len;
+       int rc;
+       int ac;
+       char **av;
+
+       pthread_detach(conn->td);
+       conn->fpr = fdopen(conn->fd, "r");
+       conn->fpw = fdopen(dup(conn->fd), "w");
+       conn->fd = -1;
+       setvbuf(conn->fpr, NULL, _IOFBF, 0);
+       setvbuf(conn->fpw, NULL, _IOFBF, 0);
+
+       while ((ptr = fgetln(conn->fpr, &len)) != NULL) {
+               ac = decode_args(conn, &av, ptr, len);
+               rc = process_cmd(&cmd, conn->fpw, ac, av);
+               cmd.cmdline = 0;        /* we are the remote */
+               cmd.commanded = 1;      /* commanded action (vs automatic) */
+               sreplace(&cmd.label, conn->label);
+               if (rc == 0) {
+                       pthread_mutex_lock(&serial_mtx);
+                       rc = execute_cmd(&cmd);
+                       pthread_mutex_unlock(&serial_mtx);
+               }
+               free_cmd(&cmd);
+               afree(&av);
+               fwrite(".\n", 2, 1, conn->fpw);
+               fflush(conn->fpw);
+       }
+       fclose(conn->fpr);
+       fclose(conn->fpw);
+       conn->fpr = NULL;
+       conn->fpw = NULL;
+       free(conn->label);
+       free(conn);
+
+       return NULL;
+}
+
+/*
+ * Issue the command to the remote, encode the arguments.
+ */
+static
+void
+remote_issue(connect_t *conn, command_t *cmd)
+{
+       int i;
+
+       for (i = 1; i < cmd->orig_ac; ++i) {
+               const char *str = cmd->orig_av[i];
+
+               if (i != 1)
+                       putc(' ', conn->fpw);
+               while (*str) {
+                       if (*str == ' ' || *str == '\\' || *str == '\n')
+                               putc('\\', conn->fpw);
+                       putc(*str, conn->fpw);
+                       ++str;
+               }
+       }
+       putc('\n', conn->fpw);
+       fflush(conn->fpw);
+}
+
+/*
+ * Decode arguments
+ */
+static int
+decode_args(connect_t *conn __unused, char ***avp, const char *ptr, size_t len)
+{
+       char **av;
+       char *arg;
+       size_t i;
+       size_t j;
+       int acmax;
+       int ac;
+
+       if (len && ptr[len-1] == '\n')
+               --len;
+
+       acmax = 3;      /* av[0], first arg, terminating NULL */
+       for (i = 0; i < len; ++i) {
+               if (ptr[i] == ' ')
+                       ++acmax;
+       }
+       av = calloc(sizeof(char *), acmax);
+       av[0] = NULL;
+       ac = 1;
+
+       i = 0;
+       while (i < len) {
+               for (j = i; j < len; ++j) {
+                       if (ptr[j] == ' ')
+                               break;
+               }
+               arg = malloc(j - i + 1);        /* worst case arg size */
+               j = 0;
+               while (i < len) {
+                       if (ptr[i] == ' ')
+                               break;
+                       if (ptr[i] == '\\' && i + 1 < len) {
+                               arg[j++] = ptr[i+1];
+                               i += 2;
+                       } else {
+                               arg[j++] = ptr[i];
+                               i += 1;
+                       }
+               }
+               arg[j] = 0;
+               av[ac++] = arg;
+               if (i < len && ptr[i] == ' ')
+                       ++i;
+       }
+       av[ac] = NULL;
+
+#if 0
+       fprintf(conn->fpw, "DECODE ARGS: ");
+       for (i = 1; i < (size_t)ac; ++i)
+               fprintf(conn->fpw, " \"%s\"", av[i]);
+       fprintf(conn->fpw, "\n");
+       fflush(conn->fpw);
+#endif
+
+       *avp = av;
+       return ac;
+}
diff --git a/sbin/svc/subs.c b/sbin/svc/subs.c
new file mode 100644 (file)
index 0000000..ea62dcd
--- /dev/null
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2014 The DragonFly Project.  All rights reserved.
+ *
+ * This code is derived from software contributed to The DragonFly Project
+ * by Matthew Dillon <dillon@backplane.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ * 3. Neither the name of The DragonFly Project nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific, prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+/*
+ * Support routines
+ */
+
+#include "svc.h"
+
+void
+sfree(char **strp)
+{
+       if (*strp)
+               free(*strp);
+}
+
+void
+sreplace(char **strp, const char *orig)
+{
+       if (*strp) {
+               free(*strp);
+               *strp = NULL;
+       }
+       if (orig)
+               *strp = strdup(orig);
+}
+
+void
+sdup(char **strp)
+{
+       if (*strp)
+               *strp = strdup(*strp);
+}
+
+void
+afree(char ***aryp)
+{
+       char **ary = *aryp;
+       int i;
+
+       if (ary) {
+               for (i = 0; ary[i]; ++i)
+                       free(ary[i]);
+               free(ary);
+       }
+       *aryp = NULL;
+}
+
+void
+adup(char ***aryp)
+{
+       char **ary = *aryp;
+       char **nary;
+       int i;
+
+       if (ary) {
+               for (i = 0; ary[i]; ++i)
+                       ;
+               nary = calloc(sizeof(char *), i + 1);
+               bcopy(ary, nary, sizeof(char *) * (i + 1));
+               for (i = 0; nary[i]; ++i)
+                       nary[i] = strdup(nary[i]);
+               *aryp = nary;
+       }
+}
+
+/*
+ * Sets up the pidfile and unix domain socket.  We do not yet know the
+ * pid to store in the pidfile.
+ */
+int
+setup_pid_and_socket(command_t *cmd, int *lfdp, int *pfdp)
+{
+       struct sockaddr_un sou;
+       size_t len;
+       char *pidfile;
+
+       /*
+        * Create and test the pidfile.
+        */
+       asprintf(&pidfile, "%s/service.%s.pid", cmd->piddir, cmd->label);
+       *lfdp = -1;
+       *pfdp = open(pidfile, O_RDWR|O_CREAT|O_EXLOCK|O_NONBLOCK, 0644);
+       if (*pfdp < 0) {
+               if (errno == EWOULDBLOCK) {
+                       fprintf(cmd->fp, "Cannot init, %s is already active\n",
+                               cmd->label);
+               } else {
+                       fprintf(cmd->fp,
+                               "Cannot init, unable to create \"%s\": %s\n",
+                               cmd->label,
+                               strerror(errno));
+               }
+               free(pidfile);
+               return 1;
+       }
+       ftruncate(*pfdp, 0);
+
+       /*
+        * Create the unix-domain socket.
+        */
+       bzero(&sou, sizeof(sou));
+       if ((*lfdp = socket(AF_UNIX, SOCK_STREAM, 0)) >= 0) {
+               sou.sun_family = AF_UNIX;
+               snprintf(sou.sun_path, sizeof(sou.sun_path),
+                        "%s/service.%s.sk", cmd->piddir, cmd->label);
+               len = strlen(sou.sun_path);
+               len = offsetof(struct sockaddr_un, sun_path[len+1]);
+
+               /* remove stale file before trying to bind */
+               remove(sou.sun_path);
+
+               if (bind(*lfdp, (void *)&sou, len) < 0) {
+                       fprintf(cmd->fp, "Unable to bind \"%s\"\n",
+                               sou.sun_path);
+                       close(*lfdp);
+                       *lfdp = -1;
+               } else if (listen(*lfdp, 32) < 0) {
+                       fprintf(cmd->fp, "Unable to listen on \"%s\"\n",
+                               sou.sun_path);
+                       close(*lfdp);
+                       *lfdp = -1;
+               }
+       } else {
+               fprintf(cmd->fp, "Unable to create unix-domain socket\n");
+       }
+       if (*lfdp >= 0) {
+               return 0;
+       } else {
+               close(*pfdp);
+               *pfdp = -1;
+
+               return 1;
+       }
+}
diff --git a/sbin/svc/svc.8 b/sbin/svc/svc.8
new file mode 100644 (file)
index 0000000..b8e7448
--- /dev/null
@@ -0,0 +1,442 @@
+.\"
+.\" Copyright (c) 2014
+.\"    The DragonFly Project.  All rights reserved.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\"
+.\" 1. Redistributions of source code must retain the above copyright
+.\"    notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\"    notice, this list of conditions and the following disclaimer in
+.\"    the documentation and/or other materials provided with the
+.\"    distribution.
+.\" 3. Neither the name of The DragonFly Project nor the names of its
+.\"    contributors may be used to endorse or promote products derived
+.\"    from this software without specific, prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+.\" ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+.\" FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+.\" COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+.\" INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
+.\" BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+.\" LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+.\" AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+.\" OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+.\" OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.Dd November 10, 2014
+.Dt SERVICE 8
+.Os
+.Sh NAME
+.Nm service
+.Nd build an environment and monitor/maintain a service or command
+.Sh SYNOPSIS
+.Nm
+.Op Fl options
+.Ar directive
+.Ar label
+.Op arguments-to-directive
+.Sh DESCRIPTION
+.Nm
+is a program which can build, monitor, and manage a simple environment
+and execute a command within that environment.
+It uses the
+.Xr reapctl 2
+system call to round-up all processes and sub-processes created under the
+environment.
+It can detect when the specific command or the command and all processes
+exit and perform some action, and it can do a few relatively simple support
+functions to terminate, restart, or terminate/re-initiate.
+.Pp
+The following options are available:
+.Bl -tag -width indent
+.It Fl d
+Debug mode.  Additional debug output is printed.
+This will also force
+.Fl f .
+.It Fl f
+Foreground mode.  Instead of fork/detaching the service, the service itself
+runs in the foreground when the
+.Ar init
+directive is specified.  The pid will not be printed in this case.
+.It Fl p Ar directory
+Specify the directory to store pidfiles and sockets in, and to search
+for active labels.
+If not specified, the default is
+.Pa /var/run .
+If specified as
+.Ar none ,
+no pidfile will be created or maintained.  The
+.Ar none
+specification is not recommended except in combination with
+.Fl f
+.Fl x .
+.It Fl r Ar timo
+Specify the restart delay and enable automatic restarts if the original
+command under management exits, even if other processes are still present.
+This option also modifies the behavior of the
+.Ar stop
+service command, causing it to kill only the main process under management.
+Any forked children will be left intact.
+This option is exclusive with
+.Fl R .
+.It Fl R Ar timo
+Specify the restart delay and enable automatic restarts if all processes
+under management exit.
+This option also modifies the behavior of the
+.Ar stop
+service command, causing it to kill all processes under management.
+This option is exclusive with
+.Fl r .
+.It Fl x
+Causes the service demon itself to exit if the service being monitored
+exits or is stopped.
+Specify as an option to the left of the
+.Ar init
+directive.
+.Nm
+will still use
+.Fl r
+or
+.Fl R
+to indicate what is considered a dead service (some or all of the processes).
+If neither is specified,
+.Fl r
+is assumed.  If specified, the timeout is irrelevant as there will be no
+restart.
+.Pp
+This option also issues a
+.Ar stopall
+directive before exiting.  That is, it will still ensure that all processes
+running under the service, either direct or indirect, are dead before the
+service itself exits.
+.It Fl s
+Causes the service demon to issue a
+.Xr sync 2
+command after stopping or killing a service.
+.It Fl t Ar timo
+When stopping processes under management, specify the amount of time
+allowed to elapse after sending a SIGTERM before sending a SIGKILL.
+If 0 is specified, only SIGKILL will be sent.
+The default is 10 seconds.
+.It Fl u Ar user
+Set the uid of the command to execute.  The service demon itself is not
+affected.
+.Pp
+Specified when initializing a new service, has no effect for other directives.
+Cannot be overridden in
+.Ar start
+or
+.Ar restart .
+.It Fl g Ar group
+Set the gid of the command to execute.  The service demon itself is not
+affected.
+.Pp
+Specified when initializing a new service, has no effect for other directives.
+Cannot be overridden in
+.Ar start
+or
+.Ar restart .
+.It Fl G Ar group-list
+Set the group-list of the command to execute.  The service demon itself is not
+affected.
+.Pp
+Specified when initializing a new service, has no effect for other directives.
+Cannot be overridden in
+.Ar start
+or
+.Ar restart .
+.It Fl l Ar path
+Set the logfile path for the command.
+If not specified, no logfile will be created.
+However, the service monitor will still keep track of the last ~8KB or
+so of stdout/stderr output.
+.Pp
+Specified when initializing a new service, has no effect for other directives.
+.It Fl c Ar directory
+.It Fl C Ar directory
+Chroot into the specified directory when executing or re-executing the
+command.  The service itself stays outside the chroot.
+When
+.Fl c
+is used, the service will automatically mount /dev in the chroot if it
+does not already exist.
+This will be unmounted when the service exits or is killed, but remains
+in-place when the service is stopped.
+When
+.Fl C
+is used, the service will not automatically mount /dev in the chroot.
+.Pp
+Specified when initializing a new service, has no effect for other directives.
+Cannot be overridden in
+.Ar start
+or
+.Ar restart .
+.It Fl j Ar directory
+.It Fl J Ar directory
+Create a jail and operate in a manner similar to a chroot.
+.It Fl k Ar jail-spec
+Additional specification for the jail.  See below.
+.It Fl T Ar title
+Tell
+.Nm
+to use
+.Xr setproctitle 1
+to adjust what shows up in a ps command, to make process lists easier to
+diagnose.
+.It Fl F Ar restarts:pertimo
+Specify failure timing.
+If a service is automatically restarted more than the specified number
+within the specified period, the service is considered to be in a failed
+state when it next dies and will no longer be restarted.
+.Pp
+The situation will be syslogged and an email will be sent to
+.Ar service-errors
+with a description of the problem if the service is running as root.
+If the service is running as a user, the email is sent to the user.
+The system operator should generally setup a mail alias to point
+.Ar service-errors
+to the desired destination.
+.Pp
+This feature is disabled by default.
+If you only specify the restart count the rate will default to
+per 60 seconds.
+Specify as an option to the left of the
+.Ar init
+directive.
+.It Ar directive Op arguments-to-directive
+Specify a directive (see below).
+.It Ar label
+Specify a label to name or locate the service.
+Note that most directives allow a label prefix to be specified, affecting
+multiple services.
+If your label is postfixed by a number, you should use a fixed-width
+0-fill field for the number or risk some confusion.
+.El
+.Pp
+All timeouts and delays are specified in seconds.
+.Pp
+If neither
+.Fl r
+or
+.Fl R
+is specified in the
+.Ar init
+directive, the service will not automatically restart if the underlying
+processes exit.  The service demon will remain intact unless
+.Fl x
+has been specified.
+.Pp
+.Nm
+always creates a pid file in the pid directory named
+.Pa service.<label>.pid
+and maintains an open descriptor with an active exclusive
+.Xr flock 2
+on the file.
+Scripts can determine whether the service demon itself is running or not
+via the
+.Xr lockf 1
+utility, or may use the convenient
+.Ar status
+directive and check the exit code to get more detailed status.
+In addition, a service socket is created in the pid directory named
+.Pa service.<label>.sk
+which
+.Nm
+uses to communicate with a running service demon.
+.Pp
+Note that the service demon itself will not exit when the executed command
+exits unless you have used the
+.Fl x
+option, or the
+.Ar exit
+or
+.Ar kill
+directives.
+.Pp
+Some RC services, such as sendmail, may maintain multiple service processes
+and name each one with a postfix to the label.
+By specifying just the prefix, your directives will affect all matching
+labels.
+.Pp
+For build systems the
+.Fl x
+option is typically used, sometimes with the
+.Fl f
+option, and allowed to default to just waiting for the original command
+exec to exit.
+This will cause the service demon to then kill any remaining hanger-ons
+before exiting.
+.Sh DIRECTIVES
+.Bl -tag -width indent
+.It Ar init Ar exec-command Op arguments
+Start a new service with the specified label.
+This command will fail if the label is already in-use.
+This command will detach a new service demon, create a pidfile, and
+output the pid of the new service demon to stdout before returning.
+.Pp
+If the
+.Ar exec-command
+is a single word and not an absolute or relative path, the system
+command path will be searched for the command.
+.It Ar start
+Start a service that has been stopped.
+The label can be a wildcard prefix so, for example, if there are
+three sendmail services (sendmail01, sendmail02, sendmail03), then
+the label 'sendmail' will operate on all three.
+.Pp
+If the service is already running, this directive will be a NOP.
+.It Ar stop
+Stop a running service by sending a TERM signal and later a KILL signal
+if necessry, to some or all processes
+running under the service.  The processes signaled depend on the original
+.Fl r
+or
+.Fl R
+options specified when the service was initiated.
+These options, along with
+.Fl t
+may also be specified in this directive to override
+(but not permanently change) the original options.
+.Pp
+The service demon itself remains intact.
+.It Ar stopall
+This is a short-hand for
+.Fl R Ar 0
+.Ar stop .
+It will kill all sub-processes of the service regardless of whether
+.Fl r
+or
+.Fl R
+was used in the original
+.Ar init
+directive.
+.It Ar restart
+Execute the
+.Ar stop
+operation, sleep for a few seconds based on the original
+.Fl r
+or
+.Fl R
+options, and then execute the
+.Ar start
+operation.
+These options, along with
+.Fl t
+may also be specified in this directive to override
+(but not permanently change) the original options.
+.It Ar exit
+Execute the
+.Ar stop
+operation but override prior options and terminate ALL processes
+running under the service.
+The service demon itself then terminates and must be init'd again
+to restart.
+.It Ar kill
+Execute the
+.Ar stop
+operation but override prior options and terminate ALL processes
+running under the service.
+Also force the delay to 0, bypassing SIGTERM and causing SIGKILL to be
+sent.
+The service demon itself then terminates and must be init'd again
+to restart.
+.It Ar list
+List a subset of labels and their status.
+If no label is specified, all active labels are listed.
+.It Ar status
+Print the status of a particular label, exit with a 0 status if
+the service exists and is still considered to be running.
+Exit with 1 if the service exists but is considered to be stopped.
+Exit with 2 if the service does not exist.
+If multiple labels match, the worst condition found becomes the exit code.
+.Pp
+Scripts that use this feature can conveniently use the
+.Ar start
+directive to start any matching service that is considered stopped.
+The directive is a NOP for services that are considered to be running.
+.It Ar log
+The service demon monitors stdout/stderr output from programs it runs
+continuously and will remember the last ~8KB or so, which can be
+dumped via this directive.
+.It Ar logf
+This works the same as
+.Ar log
+but continues to monitor and dump the output until you ^C.
+In order to avoid potentially stalling the service under management,
+gaps may occur if the monitor is unable to keep up with the log
+output.
+.It Ar tailf
+This works similarly to
+.Ar logf
+but dumps fewer lines of log history before dovetailing into
+continuous monitoring.
+.It Ar logfile Op path
+Re-open, set, or change the logfile path for the monitor,
+creating a new logfile if necessary.
+The logfile is created by the parent monitor (the one not running in
+a chroot or jail or as a particular user or group).
+This way the service under management cannot modify or destroy it.
+.Pp
+It is highly recommended that you specify an absolute path when
+changing the logfile.
+If you wish to disable the logfile, set it to /dev/null.
+Disabling the logfile does not prevent you from viewing the
+last ~8KB and/or monitoring any logged data.
+.El
+.Pp
+Description of nominal operation
+.Xr reapctl 2
+system call.
+.Sh JAIL-SPECIFICATIONS
+A simple jail just chroots into a directory, possibly mounts /dev, and
+allows all current IP bindings to be used.
+The service demon itself does not run in the jail, but will keep the
+jail intact across
+.Ar stop
+and
+.Ar start/restart
+operations by leaving a forked process intact inside.
+If the jail is destroyed, the service demon will re-create it if necessary
+on a
+.Ar start/restart .
+.Fl k
+option may be used to specify additional parameters.
+Parameters are comma-delimited with no spaces.
+Values may be specified in the name=value format.
+For example:
+.Fl k Ar clean,ip=1.2.3.4,ip=5.6.7.8
+.Bl -tag -width indent
+.It Ar clean
+The jail is handed a clean environment, similar to what
+.Xr jail 8
+does.
+.It Ar ip=addr
+The jail is allowed to bind to the specified IP address.  This option may
+be specified multiple times.
+.El
+.Sh SIGNALS
+Generally speaking signals should not be sent to a service demon.
+Instead, the command should be run with an appropriate directive to
+adjust running behavior.
+However, the service demon will act on signals as follows:
+.Bl -tag -width indent
+.It Dv SIGTERM
+The service demon will execute the
+.Ar exit
+directive.
+.It Dv SIGHUP
+The service demon will execute the
+.Ar restart
+directive.
+.El
+.Sh HISTORY
+The
+.Nm
+utility first appeared in
+.Dx 4.0 .
diff --git a/sbin/svc/svc.c b/sbin/svc/svc.c
new file mode 100644 (file)
index 0000000..3b64072
--- /dev/null
@@ -0,0 +1,442 @@
+/*
+ * Copyright (c) 2014 The DragonFly Project.  All rights reserved.
+ *
+ * This code is derived from software contributed to The DragonFly Project
+ * by Matthew Dillon <dillon@backplane.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ * 3. Neither the name of The DragonFly Project nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific, prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+/*
+ * SERVICE MANAGER
+ *
+ * This program builds an environment to run a service in and provides
+ * numerous options for naming, tracking, and management.  It uses
+ * reapctl(2) to corral the processes under management.
+ */
+
+#include "svc.h"
+
+static int execute_remote(command_t *cmd, int (*func)(command_t *cmd));
+static int process_jailspec(command_t *cmd, const char *spec);
+
+int
+main(int ac, char **av)
+{
+       command_t cmd;
+       int rc;
+
+       signal(SIGPIPE, SIG_IGN);
+
+       rc = process_cmd(&cmd, stdout, ac, av);
+       cmd.cmdline = 1;        /* commanded from front-end */
+       cmd.commanded = 1;      /* commanded action (vs automatic) */
+       if (rc == 0)
+               rc = execute_cmd(&cmd);
+       free_cmd(&cmd);
+
+       return rc;
+}
+
+int
+process_cmd(command_t *cmd, FILE *fp, int ac, char **av)
+{
+       const char *optstr = "dfp:r:R:xst:u:g:G:l:c:C:j:J:k:T:F:";
+       struct group *grent;
+       struct passwd *pwent;
+       char *sub;
+       char *cpy;
+       int rc = 1;
+       int ch;
+       int i;
+
+       bzero(cmd, sizeof(*cmd));
+       cmd->fp = fp;                           /* error and output reporting */
+       sreplace(&cmd->piddir, "/var/run");     /* must not be NULL */
+       cmd->termkill_timo = -1;                /* will use default value */
+       cmd->orig_ac = ac;
+       cmd->orig_av = av;
+
+       optind = 1;
+       opterr = 1;
+       optreset = 1;
+
+       while ((ch = getopt(ac, av, optstr)) != -1) {
+               switch(ch) {
+               case 'd':
+                       cmd->debug = 1;
+                       cmd->foreground = 1;
+                       break;
+               case 'f':
+                       cmd->foreground = 1;
+                       break;
+               case 'p':
+                       sreplace(&cmd->piddir, optarg);
+                       break;
+               case 'r':
+                       cmd->restart_some = 1;
+                       cmd->restart_all = 0;
+                       cmd->restart_timo = strtol(optarg, NULL, 0);
+                       break;
+               case 'R':
+                       cmd->restart_some = 0;
+                       cmd->restart_all = 1;
+                       cmd->restart_timo = strtol(optarg, NULL, 0);
+                       break;
+               case 'x':
+                       cmd->exit_mode = 1;
+                       break;
+               case 's':
+                       cmd->sync_mode = 1;
+                       break;
+               case 't':
+                       cmd->termkill_timo = strtoul(optarg, NULL, 0);
+                       break;
+               case 'u':
+                       if (isdigit(optarg[0])) {
+                               pwent = getpwnam(optarg);
+                       } else {
+                               pwent = getpwuid(strtol(optarg, NULL, 0));
+                       }
+                       if (pwent == NULL) {
+                               fprintf(fp, "Cannot find user %s: %s\n",
+                                       optarg,
+                                       strerror(errno));
+                               goto failed;
+                       }
+                       sfree(&cmd->pwent.pw_name);
+                       sfree(&cmd->pwent.pw_passwd);
+                       sfree(&cmd->pwent.pw_class);
+                       sfree(&cmd->pwent.pw_gecos);
+                       sfree(&cmd->pwent.pw_dir);
+                       sfree(&cmd->pwent.pw_shell);
+                       cmd->pwent = *pwent;
+                       sdup(&cmd->pwent.pw_name);
+                       sdup(&cmd->pwent.pw_passwd);
+                       sdup(&cmd->pwent.pw_class);
+                       sdup(&cmd->pwent.pw_gecos);
+                       sdup(&cmd->pwent.pw_dir);
+                       sdup(&cmd->pwent.pw_shell);
+                       break;
+               case 'g':
+                       setgroupent(1);
+                       if (isdigit(optarg[0])) {
+                               grent = getgrnam(optarg);
+                       } else {
+                               grent = getgrgid(strtol(optarg, NULL, 0));
+                       }
+                       if (grent == NULL) {
+                               fprintf(fp, "Cannot find group %s: %s\n",
+                                       optarg,
+                                       strerror(errno));
+                               goto failed;
+                       }
+                       sfree(&cmd->grent.gr_name);
+                       sfree(&cmd->grent.gr_passwd);
+                       afree(&cmd->grent.gr_mem);
+                       cmd->grent = *grent;
+                       sdup(&cmd->grent.gr_name);
+                       sdup(&cmd->grent.gr_passwd);
+                       adup(&cmd->grent.gr_mem);
+                       break;
+               case 'G':
+                       setgroupent(1);
+                       cpy = strdup(optarg);
+                       sub = strtok(cpy, ",");
+                       i = 0;
+                       while (sub) {
+                               if (isdigit(optarg[0])) {
+                                       grent = getgrnam(optarg);
+                               } else {
+                                       grent = getgrgid(strtol(optarg,
+                                                               NULL, 0));
+                               }
+                               if (grent == NULL) {
+                                       fprintf(fp,
+                                               "Cannot find group %s: %s\n",
+                                               optarg,
+                                               strerror(errno));
+                                       i = -1;
+                               }
+                               if (i == NGROUPS) {
+                                       fprintf(fp,
+                                               "Too many groups specified, "
+                                               "max %d\n", NGROUPS);
+                                       i = -1;
+                               }
+                               if (i >= 0)
+                                       cmd->groups[i++] = grent->gr_gid;
+                               sub = strtok(NULL, ",");
+                       }
+                       free(cpy);
+                       if (i < 0)
+                               goto failed;
+                       cmd->ngroups = i;
+                       break;
+               case 'l':
+                       sreplace(&cmd->logfile, optarg);
+                       break;
+               case 'C':
+                       cmd->mountdev = 1;
+                       /* fall through */
+               case 'c':
+                       sreplace(&cmd->rootdir, optarg);
+                       break;
+               case 'J':
+                       cmd->mountdev = 1;
+                       /* fall through */
+               case 'j':
+                       sreplace(&cmd->jaildir, optarg);
+                       break;
+               case 'k':
+                       rc = process_jailspec(cmd, optarg);
+                       if (rc)
+                               goto failed;
+                       break;
+               case 'T':
+                       sreplace(&cmd->proctitle, optarg);
+                       break;
+               case 'F':
+                       cmd->restart_per = 60;
+                       if (sscanf(optarg, "%d:%d",
+                                  &cmd->restart_count,
+                                  &cmd->restart_per) < 1) {
+                               fprintf(fp, "bad restart specification: %s\n",
+                                       optarg);
+                               goto failed;
+                       }
+                       break;
+               default:
+                       fprintf(fp, "Unknown option %c\n", ch);
+                       goto failed;
+               }
+       }
+
+       /*
+        * directive [label] [...additional args]
+        */
+       i = optind;
+       if (av[i]) {
+               cmd->directive = strdup(av[i]);
+               ++i;
+               if (av[i]) {
+                       cmd->label = strdup(av[i]);
+                       ++i;
+                       cmd->ext_av = av + i;
+                       cmd->ext_ac = ac - i;
+                       adup(&cmd->ext_av);
+               }
+       } else {
+               fprintf(fp, "No directive specified\n");
+               goto failed;
+       }
+       rc = 0;
+failed:
+       endgrent();
+       endpwent();
+
+       return rc;
+}
+
+int
+execute_cmd(command_t *cmd)
+{
+       const char *directive;
+       int rc;
+
+       directive = cmd->directive;
+
+       if (strcmp(directive, "init") == 0) {
+               rc = execute_init(cmd);
+       } else if (strcmp(directive, "start") == 0) {
+               rc = execute_remote(cmd, execute_start);
+       } else if (strcmp(directive, "stop") == 0) {
+               rc = execute_remote(cmd, execute_stop);
+       } else if (strcmp(directive, "stopall") == 0) {
+               cmd->restart_some = 0;
+               cmd->restart_all = 1;
+               rc = execute_remote(cmd, execute_stop);
+       } else if (strcmp(directive, "restart") == 0) {
+               rc = execute_remote(cmd, execute_restart);
+       } else if (strcmp(directive, "exit") == 0) {
+               cmd->restart_some = 0;
+               cmd->restart_all = 1;   /* stop everything */
+               rc = execute_remote(cmd, execute_exit);
+       } else if (strcmp(directive, "kill") == 0) {
+               cmd->restart_some = 0;
+               cmd->restart_all = 1;   /* stop everything */
+               cmd->termkill_timo = 0; /* force immediate SIGKILL */
+               rc = execute_remote(cmd, execute_exit);
+       } else if (strcmp(directive, "list") == 0) {
+               rc = execute_remote(cmd, execute_list);
+       } else if (strcmp(directive, "status") == 0) {
+               rc = execute_remote(cmd, execute_status);
+       } else if (strcmp(directive, "log") == 0) {
+               rc = execute_remote(cmd, execute_log);
+       } else if (strcmp(directive, "logf") == 0) {
+               cmd->tail_mode = 1;
+               rc = execute_remote(cmd, execute_log);
+       } else if (strcmp(directive, "tailf") == 0) {
+               cmd->tail_mode = 2;
+               rc = execute_remote(cmd, execute_log);
+       } else if (strcmp(directive, "logfile") == 0) {
+               rc = execute_remote(cmd, execute_logfile);
+       } else {
+               fprintf(cmd->fp, "Uknown directive: %s\n", directive);
+               rc = 1;
+       }
+       return rc;
+}
+
+static
+int
+execute_remote(command_t *cmd, int (*func)(command_t *cmd))
+{
+       DIR *dir;
+       struct dirent *den;
+       const char *p1;
+       const char *p2;
+       char *plab;
+       size_t cmdlen;
+       size_t len;
+       int rc;
+
+       /*
+        * If already on the remote service just execute the operation
+        * as requested.
+        */
+       if (cmd->cmdline == 0) {
+               return (func(cmd));
+       }
+
+       /*
+        * Look for label(s).  If no exact match or label is NULL, scan
+        * piddir for matches.
+        */
+       if ((dir = opendir(cmd->piddir)) == NULL) {
+               fprintf(cmd->fp, "Unable to scan \"%s\"\n", cmd->piddir);
+               return 1;
+       }
+
+       rc = 0;
+       cmdlen = (cmd->label ? strlen(cmd->label) : 0);
+
+       while ((den = readdir(dir)) != NULL) {
+               /*
+                * service. prefix.
+                */
+               if (strncmp(den->d_name, "service.", 8) != 0)
+                       continue;
+
+               /*
+                * .sk suffix
+                */
+               p1 = den->d_name + 8;
+               p2 = strrchr(p1, '.');
+               if (p2 == NULL || p2 < p1 || strcmp(p2, ".sk") != 0)
+                       continue;
+
+               /*
+                * Extract the label from the service.<label>.sk name.
+                */
+               len = p2 - p1 - 4;
+               plab = strdup(p1);
+               *strrchr(plab, '.') = 0;
+
+               /*
+                * Start remote execution (in parallel) for all matching
+                * labels.  This will generally create some asynchronous
+                * threads.
+                */
+               if (cmdlen == 0 ||
+                   (cmdlen <= len && strncmp(cmd->label, plab, cmdlen) == 0)) {
+                       remote_execute(cmd, plab);
+               }
+               free(plab);
+       }
+       closedir(dir);
+
+       /*
+        * Wait for completion of remote commands and dump output.
+        */
+       rc = remote_wait();
+
+       return rc;
+}
+
+void
+free_cmd(command_t *cmd)
+{
+       sfree(&cmd->piddir);
+
+       sfree(&cmd->pwent.pw_name);
+       sfree(&cmd->pwent.pw_passwd);
+       sfree(&cmd->pwent.pw_class);
+       sfree(&cmd->pwent.pw_gecos);
+       sfree(&cmd->pwent.pw_dir);
+       sfree(&cmd->pwent.pw_shell);
+
+       sfree(&cmd->grent.gr_name);
+       sfree(&cmd->grent.gr_passwd);
+       afree(&cmd->grent.gr_mem);
+
+       sfree(&cmd->logfile);
+       sfree(&cmd->rootdir);
+       sfree(&cmd->jaildir);
+       sfree(&cmd->proctitle);
+       sfree(&cmd->directive);
+       sfree(&cmd->label);
+       afree(&cmd->ext_av);
+
+       bzero(cmd, sizeof(*cmd));
+}
+
+static
+int
+process_jailspec(command_t *cmd, const char *spec)
+{
+       char *cpy = strdup(spec);
+       char *ptr;
+       int rc = 0;
+
+       ptr = strtok(cpy, ",");
+       while (ptr) {
+               if (strcmp(ptr, "clean") == 0) {
+                       cmd->jail_clean = 1;
+               } else if (strncmp(ptr, "ip=", 3) == 0) {
+                       assert(0); /* XXX TODO */
+               } else {
+                       fprintf(cmd->fp, "jail-spec '%s' not understood\n",
+                               ptr);
+                       rc = 1;
+               }
+               ptr = strtok(NULL, ",");
+       }
+       free(cpy);
+
+       return rc;
+}
diff --git a/sbin/svc/svc.h b/sbin/svc/svc.h
new file mode 100644 (file)
index 0000000..3477f11
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2014 The DragonFly Project.  All rights reserved.
+ *
+ * This code is derived from software contributed to The DragonFly Project
+ * by Matthew Dillon <dillon@backplane.com>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ * 3. Neither the name of The DragonFly Project nor the names of its
+ *    contributors may be used to endorse or promote products derived
+ *    from this software without specific, prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/types.h>
+#include <sys/param.h>
+#include <sys/procctl.h>
+#include <sys/stat.h>
+#include <sys/file.h>
+#include <sys/time.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <sys/un.h>
+#include <sys/tty.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <unistd.h>
+#include <string.h>
+#include <signal.h>
+#include <dirent.h>
+#include <assert.h>
+#include <errno.h>
+#include <ctype.h>
+#include <pwd.h>
+#include <grp.h>
+#include <pthread.h>
+
+typedef struct SvcCommand {
+       int     mountdev : 1;
+       int     cmdline : 1;
+       int     foreground : 1;
+       int     restart_some : 1;
+       int     restart_all : 1;
+       int     exit_mode : 1;
+       int     sync_mode : 1;
+       int     tail_mode : 2;
+       int     jail_clean : 1;
+       int     manual_stop : 1;
+       int     commanded : 1;          /* command by system operator */
+       FILE    *fp;
+       char    *piddir;
+       char    *rootdir;
+       char    *jaildir;
+       char    *logfile;
+       char    *proctitle;
+       char    *directive;
+       char    *label;
+       char    **ext_av;
+       int     ext_ac;
+       char    **orig_av;
+       int     orig_ac;
+       int     restart_timo;
+       int     termkill_timo;
+       int     debug;
+       int     restart_per;
+       int     restart_count;
+       struct passwd pwent;
+       struct group grent;
+       gid_t   groups[NGROUPS];
+       int     ngroups;
+} command_t;
+
+typedef enum {
+       RS_STOPPED,     /* service died or stopped */
+       RS_STARTED,     /* service running */
+       RS_STOPPING1,   /* fresh pid to stop */
+       RS_STOPPING2,   /* TERM sent */
+       RS_STOPPING3,   /* KILL sent */
+} runstate_t;
+
+extern pthread_mutex_t serial_mtx;
+
+int process_cmd(command_t *cmd, FILE *fp, int ac, char **av);
+int execute_cmd(command_t *cmd);
+void free_cmd(command_t *cmd);
+
+void sfree(char **strp);
+void sreplace(char **strp, const char *orig);
+void sdup(char **strp);
+void afree(char ***aryp);
+void adup(char ***aryp);
+int setup_pid_and_socket(command_t *cmd, int *lfdp, int *pfdp);
+
+void remote_execute(command_t *cmd, const char *label);
+void remote_listener(command_t *cmd, int lfd);
+int remote_wait(void);
+
+int execute_init(command_t *cmd);
+int execute_start(command_t *cmd);
+int execute_stop(command_t *cmd);
+int execute_restart(command_t *cmd);
+int execute_exit(command_t *cmd);
+int execute_list(command_t *cmd);
+int execute_status(command_t *cmd);
+int execute_log(command_t *cmd);
+int execute_logfile(command_t *cmd);