chiark / gitweb /
systemd-activate: add a socket-activation test tool
authorZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Sat, 23 Feb 2013 16:52:04 +0000 (17:52 +0100)
committerZbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>
Thu, 28 Feb 2013 03:10:14 +0000 (22:10 -0500)
Makefile-man.am
Makefile.am
TODO
man/systemd-activate.xml [new file with mode: 0644]
src/activate/activate.c [new file with mode: 0644]
src/shared/socket-util.c
src/shared/socket-util.h

index 9a2f2f55046291defa24d765ecae90eda851184a..d02fff05e215990bd2f87c2164176baed0fd8134 100644 (file)
@@ -44,6 +44,7 @@ MANPAGES += \
        man/shutdown.8 \
        man/sysctl.d.5 \
        man/systemctl.1 \
+       man/systemd-activate.8 \
        man/systemd-analyze.1 \
        man/systemd-ask-password-console.service.8 \
        man/systemd-ask-password.1 \
index cc0d4da22fed32ecb1e7b34a8805ccbae3d99023..d00ca113e0b1fecb909c3a700e49efc208009550 100644 (file)
@@ -2313,6 +2313,18 @@ EXTRA_DIST += \
        src/libsystemd-id128/libsystemd-id128.pc.in \
        src/libsystemd-id128/libsystemd-id128.sym
 
+# ------------------------------------------------------------------------------
+
+rootlibexec_PROGRAMS += \
+       systemd-activate
+
+systemd_activate_SOURCES = \
+       src/activate/activate.c
+
+systemd_activate_LDADD = \
+       libsystemd-shared.la \
+       libsystemd-daemon.la
+
 # ------------------------------------------------------------------------------
 systemd_journald_SOURCES = \
        src/journal/journald.c \
@@ -2559,6 +2571,8 @@ libsystemd-journal-uninstall-hook:
 INSTALL_EXEC_HOOKS += libsystemd-journal-install-hook
 UNINSTALL_EXEC_HOOKS += libsystemd-journal-uninstall-hook
 
+# ------------------------------------------------------------------------------
+
 # Update catalog on installation. Do not bother if installing
 # in DESTDIR, since this is likely for packaging purposes.
 catalog-update-hook:
diff --git a/TODO b/TODO
index 5427d1b62fca257f7a33a27f85471ee19c069c16..f99f894115730d87fa7c4b60f76eb650718732d7 100644 (file)
--- a/TODO
+++ b/TODO
@@ -350,8 +350,6 @@ Features:
 
 * explore multiple service instances per listening socket idea
 
-* testing tool for socket activation: some binary that listens on a socket and passes it on using the usual socket activation protocol to some server.
-
 * shutdown: don't read-only mount anything when running in container
 
 * MountFlags=shared acts as MountFlags=slave right now.
diff --git a/man/systemd-activate.xml b/man/systemd-activate.xml
new file mode 100644 (file)
index 0000000..b2987d9
--- /dev/null
@@ -0,0 +1,171 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<!--
+This file is part of systemd.
+
+Copyright 2013 Zbigniew Jędrzejewski-Szmek
+
+systemd is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+systemd 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with systemd; If not, see <http://www.gnu.org/licenses/>.
+-->
+
+<refentry id="systemd-journal-gatewayd.service">
+
+  <refentryinfo>
+    <title>systemd-activate</title>
+    <productname>systemd</productname>
+
+    <authorgroup>
+      <author>
+        <contrib>Developer</contrib>
+        <firstname>Zbigniew</firstname>
+        <surname>Jędrzejewski-Szmek</surname>
+        <email>zbyszek@in.waw.pl</email>
+      </author>
+    </authorgroup>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>systemd-activate</refentrytitle>
+    <manvolnum>8</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>systemd-activate</refname>
+    <refpurpose>Test socket activation of daemons</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>/usr/lib/systemd/systemd-activate</command>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+      <arg choice="plain"><replaceable>daemon</replaceable></arg>
+      <arg choice="opt" rep="repeat">OPTIONS</arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><command>systemd-activate</command> can be used to
+    launch a socket activated daemon from the command-line for
+    testing purposes. It can also be used to launch single instances
+    of the daemon per connection (inetd-style).
+    </para>
+
+    <para>The daemon to launch and its options should be specifed
+    after options intended for <command>systemd-activate</command>.
+    </para>
+
+    <para>If the <option>-a</option> option is given, file descriptor
+    of the connection will be used as the standard input and output of
+    the launched process. Otherwise, standard input and output will be
+    inherited, and sockets will be passed through file descriptors 3
+    and higher. Sockets passed through <varname>$LISTEN_FDS</varname>
+    to <command>systemd-activate</command> will be passed through to
+    the dameon, in the original positions. Other sockets specified
+    with <option>--listen</option> will use consecutive descriptors.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>Options</title>
+    <variablelist>
+      <varlistentry>
+        <term><option>--help</option></term>
+        <term><option>-h</option></term>
+
+        <listitem><para>Prints a short help
+        text and exits.</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--version</option></term>
+
+        <listitem><para>Prints a short version
+        string and exits.</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>-l</option></term>
+        <term><option>--listen=<replaceable>address</replaceable></option></term>
+
+        <listitem><para>Listen on this <replaceable>address</replaceable>.
+        Takes a string like <literal>2000</literal> or
+        <literal>127.0.0.1:2001</literal>.</para>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>-a</option></term>
+        <term><option>--accept</option></term>
+
+        <listitem><para>Launch a separate instance of daemon per
+        connection and pass the connection socket as standard input
+        and standard output.</para></listitem>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Environment variables</title>
+    <variablelist class='environment-variables'>
+      <varlistentry>
+        <term><varname>$LISTEN_FDS</varname></term>
+        <term><varname>$LISTEN_PID</varname></term>
+
+        <para>See
+        <citerefentry><refentrytitle>sd_listen_fds</refentrytitle><manvolnum>3</manvolnum></citerefentry>.</para>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>$SYSTEMD_LOG_TARGET</varname></term>
+        <term><varname>$SYSTEMD_LOG_LEVEL</varname></term>
+        <term><varname>$SYSTEMD_LOG_COLOR</varname></term>
+        <term><varname>$SYSTEMD_LOG_LOCATION</varname></term>
+
+        <para>Same as in
+        <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>.</para>
+      </varlistentry>
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Example 1</title>
+
+    <programlisting>$ /usr/lib/systemd/systemd-activate -l 2000 -a cat</programlisting>
+
+    <para>This runs an echo server on port 2000.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>Example 2</title>
+
+    <programlisting>$ /usr/lib/systemd/systemd-activate -l 19531 /usr/lib/systemd/systemd-journal-gatewayd</programlisting>
+
+    <para>This runs a socket activated instance of
+    <citerefentry><refentrytitle>systemd-journal-gatewayd</refentrytitle><manvolnum>8</manvolnum></citerefentry>.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para>
+      <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>systemd.socket</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>systemd.service</refentrytitle><manvolnum>5</manvolnum></citerefentry>,
+      <citerefentry><refentrytitle>cat</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+    </para>
+  </refsect1>
+</refentry>
diff --git a/src/activate/activate.c b/src/activate/activate.c
new file mode 100644 (file)
index 0000000..7fcb0a9
--- /dev/null
@@ -0,0 +1,410 @@
+/*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/
+
+/***
+  This file is part of systemd.
+
+  Copyright 2013 Zbigniew Jędrzejewski-Szmek
+
+  systemd is free software; you can redistribute it and/or modify it
+  under the terms of the GNU Lesser General Public License as published by
+  the Free Software Foundation; either version 2.1 of the License, or
+  (at your option) any later version.
+
+  systemd 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
+  Lesser General Public License for more details.
+
+  You should have received a copy of the GNU Lesser General Public License
+  along with systemd; If not, see <http://www.gnu.org/licenses/>.
+***/
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/epoll.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <getopt.h>
+
+#include <systemd/sd-daemon.h>
+
+#include "socket-util.h"
+#include "build.h"
+#include "log.h"
+#include "strv.h"
+#include "macro.h"
+
+static char** arg_listen = NULL;
+static bool arg_accept = false;
+static char** arg_args = NULL;
+
+static int add_epoll(int epoll_fd, int fd) {
+        int r;
+        struct epoll_event ev = {EPOLLIN};
+        ev.data.fd = fd;
+
+        assert(epoll_fd >= 0);
+        assert(fd >= 0);
+
+        r = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev);
+        if (r < 0)
+                log_error("Failed to add event on epoll fd:%d for fd:%d: %s",
+                          epoll_fd, fd, strerror(-r));
+        return r;
+}
+
+static int set_nocloexec(int fd) {
+        int flags;
+
+        flags = fcntl(fd, F_GETFD);
+        if (flags < 0) {
+                log_error("Querying flags for fd:%d: %m", fd);
+                return -errno;
+        }
+
+        if (!(flags & FD_CLOEXEC))
+                return 0;
+
+        if (fcntl(fd, F_SETFD, flags & ~FD_CLOEXEC) < 0) {
+                log_error("Settings flags for fd:%d: %m", fd);
+                return -errno;
+        }
+
+        return 0;
+}
+
+static int print_socket(const char* desc, int fd) {
+        int r;
+        SocketAddress addr = {
+                .size = sizeof(union sockaddr_union),
+                .type = SOCK_STREAM,
+        };
+        int family;
+
+        r = getsockname(fd, &addr.sockaddr.sa, &addr.size);
+        if (r < 0) {
+                log_warning("Failed to query socket on fd:%d: %m", fd);
+                return 0;
+        }
+
+        family = socket_address_family(&addr);
+        switch(family) {
+        case AF_INET:
+        case AF_INET6: {
+                char* _cleanup_free_ a = NULL;
+                r = socket_address_print(&addr, &a);
+                if (r < 0)
+                        log_warning("socket_address_print(): %s", strerror(-r));
+                else
+                        log_info("%s %s address %s",
+                                 desc,
+                                 family == AF_INET ? "IP" : "IPv6",
+                                 a);
+                break;
+        }
+        default:
+                log_warning("Connection with unknown family %d", family);
+        }
+
+        return 0;
+}
+
+static int open_sockets(int *epoll_fd, bool accept) {
+        int n, fd;
+        int count = 0;
+        char **address;
+
+        n = sd_listen_fds(true);
+        if (n < 0) {
+                log_error("Failed to read listening file descriptors from environment: %s",
+                          strerror(-n));
+                return n;
+        }
+        log_info("Received %d descriptors", n);
+
+        for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + n; fd++) {
+                log_debug("Received descriptor fd:%d", fd);
+                print_socket("Listening on", fd);
+
+                if (!arg_accept) {
+                        int r = set_nocloexec(fd);
+                        if (r < 0)
+                                return r;
+                }
+
+                count ++;
+        }
+
+        STRV_FOREACH(address, arg_listen) {
+                log_info("Opening address %s", *address);
+
+                fd = make_socket_fd(*address, SOCK_STREAM | (arg_accept*SOCK_CLOEXEC));
+                if (fd < 0) {
+                        log_error("Failed to open '%s': %s", *address, strerror(-fd));
+                        return fd;
+                }
+
+                count ++;
+        }
+
+        *epoll_fd = epoll_create1(EPOLL_CLOEXEC);
+        if (*epoll_fd < 0) {
+                log_error("Failed to create epoll object: %m");
+                return -errno;
+        }
+
+        for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + count; fd++) {
+                int r = add_epoll(*epoll_fd, fd);
+                if (r < 0)
+                        return r;
+        }
+
+        return count;
+}
+
+static int launch(char* name, char **argv, char **environ, int fds) {
+        unsigned n_env = 0;
+        char* envp[7] = {NULL}; /* PATH, TERM, HOME, USER, LISTEN_FDS, LISTEN_PID */
+        static const char* tocopy[] = {"TERM=", "PATH=", "USER=", "HOME="};
+        char _cleanup_free_ *tmp = NULL;
+        unsigned i;
+
+        for (i = 0; i < ELEMENTSOF(tocopy); i++) {
+                envp[n_env] = strv_find_prefix(environ, tocopy[i]);
+                if (envp[n_env])
+                        n_env ++;
+        }
+
+        if ((asprintf((char**)(envp + n_env++), "LISTEN_FDS=%d", fds) < 0) ||
+            (asprintf((char**)(envp + n_env++), "LISTEN_PID=%d", getpid()) < 0))
+                return log_oom();
+
+        tmp = strv_join(argv, " ");
+        if (!tmp)
+                return log_oom();
+
+        log_info("Execing %s (%s)", name, tmp);
+        execvpe(name, argv, envp);
+        log_error("Failed to execp %s (%s): %m", name, tmp);
+        return -errno;
+}
+
+static int launch1(const char* child, char** argv, char **environ, int fd) {
+        pid_t parent_pid, child_pid;
+        int r;
+
+        char _cleanup_free_ *tmp = NULL;
+        tmp = strv_join(argv, " ");
+        if (!tmp)
+                return log_oom();
+
+        parent_pid = getpid();
+
+        child_pid = fork();
+        if (child_pid < 0) {
+                log_error("Failed to fork: %m");
+                return -errno;
+        }
+
+        /* In the child */
+        if (child_pid == 0) {
+                r = dup2(fd, STDIN_FILENO);
+                if (r < 0) {
+                        log_error("Failed to dup connection to stdin: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                r = dup2(fd, STDOUT_FILENO);
+                if (r < 0) {
+                        log_error("Failed to dup connection to stdout: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                r = close(fd);
+                if (r < 0) {
+                        log_error("Failed to close dupped connection: %m");
+                        _exit(EXIT_FAILURE);
+                }
+
+                /* Make sure the child goes away when the parent dies */
+                if (prctl(PR_SET_PDEATHSIG, SIGTERM) < 0)
+                        _exit(EXIT_FAILURE);
+
+                /* Check whether our parent died before we were able
+                 * to set the death signal */
+                if (getppid() != parent_pid)
+                        _exit(EXIT_SUCCESS);
+
+                execvp(child, argv);
+                log_error("Failed to exec child %s: %m", child);
+                _exit(EXIT_FAILURE);
+        }
+
+        log_info("Spawned %s (%s) as PID %d", child, tmp, child_pid);
+
+        return 0;
+}
+
+static int do_accept(const char* name, char **argv, char **envp, int fd) {
+        SocketAddress addr = {
+                .size = sizeof(union sockaddr_union),
+                .type = SOCK_STREAM,
+        };
+        int fd2, r;
+
+        fd2 = accept(fd, &addr.sockaddr.sa, &addr.size);
+        if (fd2 < 0) {
+                log_error("Failed to accept connection on fd:%d: %m", fd);
+                return fd2;
+        }
+
+        print_socket("Connection from", fd2);
+
+        r = launch1(name, argv, envp, fd2);
+        return r;
+}
+
+/* SIGCHLD handler. */
+static void sigchld_hdl(int sig, siginfo_t *t, void *data)
+{
+        log_info("Child %d died with code %d", t->si_pid, t->si_status);
+       /* Wait for a dead child. */
+       waitpid(t->si_pid, NULL, 0);
+}
+
+static int install_chld_handler(void) {
+        int r;
+       struct sigaction act;
+        zero(act);
+        act.sa_flags = SA_SIGINFO;
+        act.sa_sigaction = sigchld_hdl;
+
+        r = sigaction(SIGCHLD, &act, 0);
+        if (r < 0)
+                log_error("Failed to install SIGCHLD handler: %m");
+        return r;
+}
+
+static int help(void) {
+        printf("%s [OPTIONS...]\n\n"
+               "Listen on sockets and launch child on connection.\n\n"
+               "Options:\n"
+               "  -l --listen=ADDR     Listen for raw connections at ADDR\n"
+               "  -a --accept          Spawn separate child for each connection\n"
+               "  -h --help            Show this help and exit\n"
+               "  --version            Print version string and exit\n"
+               "\n"
+               "Note: file descriptors from sd_listen_fds() will be passed through.\n"
+               , program_invocation_short_name
+               );
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+        enum {
+                ARG_VERSION = 0x100,
+        };
+
+        static const struct option options[] = {
+                { "help",         no_argument,       NULL, 'h'           },
+                { "version",      no_argument,       NULL, ARG_VERSION   },
+                { "listen",       required_argument, NULL, 'l'           },
+                { "accept",       no_argument,       NULL, 'a'           },
+                { NULL,           0,                 NULL, 0             }
+        };
+
+        int c;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "+hl:sa", options, NULL)) >= 0)
+                switch(c) {
+                case 'h':
+                        help();
+                        return 0 /* done */;
+
+                case ARG_VERSION:
+                        puts(PACKAGE_STRING);
+                        puts(SYSTEMD_FEATURES);
+                        return 0 /* done */;
+
+                case 'l': {
+                        int r = strv_extend(&arg_listen, optarg);
+                        if (r < 0)
+                                return r;
+
+                        break;
+                }
+
+                case 'a':
+                        arg_accept = true;
+                        break;
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        log_error("Unknown option code %c", c);
+                        return -EINVAL;
+                }
+
+        if (optind == argc) {
+                log_error("Usage: %s [OPTION...] PROGRAM [OPTION...]",
+                          program_invocation_short_name);
+                return -EINVAL;
+        }
+
+        arg_args = argv + optind;
+
+        return 1 /* work to do */;
+}
+
+int main(int argc, char **argv, char **envp) {
+        int r, n;
+        int epoll_fd = -1;
+
+        log_set_max_level(LOG_DEBUG);
+        log_show_color(true);
+        log_parse_environment();
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                return r == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
+
+        r = install_chld_handler();
+        if (r < 0)
+                return EXIT_FAILURE;
+
+        n = open_sockets(&epoll_fd, arg_accept);
+        if (n < 0)
+                return EXIT_FAILURE;
+
+        while (true) {
+                struct epoll_event event;
+
+                r = epoll_wait(epoll_fd, &event, 1, -1);
+                if (r < 0) {
+                        if (errno == EINTR)
+                                continue;
+
+                        log_error("epoll_wait() failed: %m");
+                        return EXIT_FAILURE;
+                }
+
+                log_info("Communication attempt on fd:%d", event.data.fd);
+                if (arg_accept) {
+                        r = do_accept(argv[optind], argv + optind, envp,
+                                      event.data.fd);
+                        if (r < 0)
+                                return EXIT_FAILURE;
+                } else
+                        break;
+        }
+
+        launch(argv[optind], argv + optind, envp, n);
+
+        return EXIT_SUCCESS;
+}
index 6c94d69486806cdf5dea43c0c01f713cc38ab8de..f6ddea31838512e4435cf4cad1882f1280f13adf 100644 (file)
@@ -565,6 +565,45 @@ bool socket_address_matches_fd(const SocketAddress *a, int fd) {
         return false;
 }
 
+int make_socket_fd(const char* address, int flags) {
+        SocketAddress a;
+        int fd, r;
+        char _cleanup_free_ *p = NULL;
+
+        r = socket_address_parse(&a, address);
+        if (r < 0) {
+                log_error("failed to parse socket: %s", strerror(-r));
+                return r;
+        }
+
+        fd = socket(socket_address_family(&a), flags, 0);
+        if (fd < 0) {
+                log_error("socket(): %m");
+                return -errno;
+        }
+
+        r = socket_address_print(&a, &p);
+        if (r < 0) {
+                log_error("socket_address_print(): %s", strerror(-r));
+                return r;
+        }
+        log_info("Listening on %s", p);
+
+        r = bind(fd, &a.sockaddr.sa, a.size);
+        if (r < 0) {
+                log_error("bind to %s: %m", address);
+                return -errno;
+        }
+
+        r = listen(fd, SOMAXCONN);
+        if (r < 0) {
+                log_error("listen on %s: %m", address);
+                return -errno;
+        }
+
+        return fd;
+}
+
 static const char* const netlink_family_table[] = {
         [NETLINK_ROUTE] = "route",
         [NETLINK_FIREWALL] = "firewall",
index 771765d323d8c3235eac206987f1c9d8ec9d9629..33838345edf9d4ee9a73b8196841ea71d11b99ad 100644 (file)
@@ -88,6 +88,8 @@ bool socket_address_is_netlink(const SocketAddress *a, const char *s);
 
 bool socket_address_matches_fd(const SocketAddress *a, int fd);
 
+int make_socket_fd(const char* address, int flags);
+
 bool socket_address_equal(const SocketAddress *a, const SocketAddress *b);
 
 bool socket_address_needs_mount(const SocketAddress *a, const char *prefix);