systemd-activate: add a socket-activation test tool

This commit is contained in:
Zbigniew Jędrzejewski-Szmek 2013-02-23 17:52:04 +01:00
parent 5674767ec2
commit 2ca0435be9
7 changed files with 637 additions and 2 deletions

View 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 \

View 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:

2
TODO
View File

@ -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.

171
man/systemd-activate.xml Normal file
View File

@ -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>

410
src/activate/activate.c Normal file
View File

@ -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;
}

View 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",

View 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);