Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,17 @@ jobs:
BUILDCONFIG="--with-regen-expr"
MFLAGS=
# -------------------------------------------------------------------------
### TODO: if: *condition_not_24x
- name: Configured w/reduced exports
config: --enable-reduced-exports --enable-maintainer-mode --enable-systemd
- name: mod_systemd, reduced exports
config: --enable-reduced-exports --enable-maintainer-mode --enable-systemd=shared
pkgs: libsystemd-dev
config-output: HAVE_SYSTEMD
config-no-output: AP_FORCE_EXPORTS
env: |
SKIP_TESTING=1
TEST_INSTALL=1
NO_TEST_FRAMEWORK=1
TEST_PYTEST=1
PYHTTPD_TARGETS=modules/arch
PYTEST_ARGS=--only=pyhttpd
# -------------------------------------------------------------------------
### TODO if: *condition_not_24x
### TODO: Fails because :i386 packages are not being found.
Expand Down
7 changes: 6 additions & 1 deletion docs/manual/mod/mod_systemd.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ WantedBy=multi-user.target
href="https://www.freedesktop.org/software/systemd/man/systemd.kill.html">systemd.kill(5)</a>
for more information.</p>

<p>This module does not provide support for Systemd socket activation.</p>
<p>Systemd socket activation is supported if httpd was built with
it. Each <directive module="mpm_common">Listen</directive> port
must then be one passed in by systemd; a port which was not is a
fatal configuration error rather than one httpd opens for itself.
Socket activation is used only if this module is loaded, so it can
be built in and left unused.</p>

<p><directive module="core">ExtendedStatus</directive> is
enabled by default if the module is loaded. If <directive
Expand Down
97 changes: 80 additions & 17 deletions modules/arch/unix/mod_systemd.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/

#include <stdint.h>
#include <time.h>
#include <ap_config.h>
#include "ap_mpm.h"
#include "ap_listen.h"
Expand All @@ -39,12 +40,36 @@
#include <unistd.h>
#endif

/* Microseconds on the clock systemd compares RELOADING=1 against, or
* zero if it cannot be read. */
static apr_uint64_t monotonic_usec(void)
{
struct timespec ts;

if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
return 0;
}
return (apr_uint64_t)ts.tv_sec * APR_USEC_PER_SEC + ts.tv_nsec / 1000;
}

static int systemd_pre_config(apr_pool_t *pconf, apr_pool_t *plog,
apr_pool_t *ptemp)
{
sd_notify(0,
"RELOADING=1\n"
"STATUS=Reading configuration...\n");
apr_uint64_t usec = monotonic_usec();

/* A Type=notify-reload service ignores a reload notification which
* does not say when it was sent. */
if (usec) {
sd_notifyf(0,
"RELOADING=1\n"
"MONOTONIC_USEC=%" APR_UINT64_T_FMT "\n"
"STATUS=Reading configuration...\n", usec);
}
else {
sd_notify(0,
"RELOADING=1\n"
"STATUS=Reading configuration...\n");
}
ap_extended_status = 1;
return OK;
}
Expand All @@ -63,6 +88,17 @@ static void log_selinux_context(void)
}
#endif

/* pconf is also cleared on a restart, where the service is not stopping
* at all, so distinguish the two by the state of the process. */
static apr_status_t systemd_stopping(void *unused)
{
if (ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_EXITING) {
sd_notify(0, "STOPPING=1\n"
"STATUS=Shutting down.\n");
}
return APR_SUCCESS;
}

/* Report the service is ready in post_config, which could be during
* startup or after a reload. The server could still hit a fatal
* startup error after this point during ap_run_mpm(), so this is
Expand All @@ -80,6 +116,11 @@ static int systemd_post_config(apr_pool_t *pconf, apr_pool_t *plog,
log_selinux_context();
#endif

/* Not reached by "httpd -k stop" and friends, which signal the
* running server and exit before post_config. */
apr_pool_cleanup_register(pconf, NULL, systemd_stopping,
apr_pool_cleanup_null);

sd_notify(0, "READY=1\n"
"STATUS=Configuration loaded.\n");
return OK;
Expand All @@ -106,25 +147,52 @@ static int systemd_monitor(apr_pool_t *p, server_rec *s)
}

ap_get_sload(&sload);
/* up_time in seconds */
up_time = (apr_uint32_t) apr_time_sec(apr_time_now() -
ap_scoreboard_image->global->restart_time);
/* up_time in seconds, and never zero: a restart resets restart_time,
* so this hook can run in the same second it was set. */
up_time = apr_time_sec(apr_time_now() -
ap_scoreboard_image->global->restart_time);
if (up_time < 1) {
up_time = 1;
}

apr_strfsize((unsigned long)((float) (sload.bytes_served)
/ (float) up_time), bps);
apr_strfsize(sload.bytes_served / up_time, bps);

/* ap_get_sload() gives idle and busy as percentages of the workers
* available, not as counts. */
sd_notifyf(0, "READY=1\n"
"STATUS=Total requests: %lu; Idle/Busy workers %d/%d; "
"STATUS=Total requests: %lu; Idle/Busy workers %d%%/%d%%; "
"Requests/sec: %.3g; Bytes served/sec: %sB/sec\n",
sload.access_count, sload.idle, sload.busy,
((float) sload.access_count) / (float) up_time, bps);

return DECLINED;
}

/* The number of sockets passed by the service manager has to be
* remembered: the configuration is read again on restart, by which time
* the environment sd_listen_fds() reads has been cleared, and the module
* itself has been unloaded and loaded again. Hence retained data rather
* than a static. */
static const char *const retained_key = "mod_systemd_listen_fds";

static int ap_systemd_listen_fds(int unset_environment)
{
int *fds = ap_retained_data_get(retained_key);

if (fds == NULL) {
fds = ap_retained_data_create(retained_key, sizeof(*fds));
*fds = sd_listen_fds(0);
}
if (unset_environment) {
/* Take the variables out of the environment, keeping the count. */
sd_listen_fds(1);
}
return *fds;
}

static int ap_find_systemd_socket(process_rec * process, apr_port_t port) {
int fdcount, fd;
int sdc = sd_listen_fds(0);
int fd;
int sdc = ap_systemd_listen_fds(0);

if (sdc < 0) {
ap_log_perror(APLOG_MARK, APLOG_CRIT, sdc, process->pool, APLOGNO(02486)
Expand All @@ -139,8 +207,7 @@ static int ap_find_systemd_socket(process_rec * process, apr_port_t port) {
return -1;
}

fdcount = atoi(getenv("LISTEN_FDS"));
for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + fdcount; fd++) {
for (fd = SD_LISTEN_FDS_START; fd < SD_LISTEN_FDS_START + sdc; fd++) {
if (sd_is_socket_inet(fd, 0, 0, -1, port) > 0) {
return fd;
}
Expand All @@ -149,10 +216,6 @@ static int ap_find_systemd_socket(process_rec * process, apr_port_t port) {
return -1;
}

static int ap_systemd_listen_fds(int unset_environment){
return sd_listen_fds(unset_environment);
}

static void systemd_register_hooks(apr_pool_t *p)
{
APR_REGISTER_OPTIONAL_FN(ap_systemd_listen_fds);
Expand Down
Empty file added test/modules/arch/__init__.py
Empty file.
79 changes: 79 additions & 0 deletions test/modules/arch/linux/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
mod_systemd tests
=================

What is different about this module
-----------------------------------
mod_systemd has no directives. Everything it does is driven by the
environment httpd was started with, and almost everything it produces goes
to the service manager rather than to a client:

- sd_notify(3) datagrams sent to $NOTIFY_SOCKET at four points: reading
the configuration (pre_config), configuration loaded (post_config),
the MPM starting (pre_mpm, the only notification carrying MAINPID),
and a status line refreshed by the monitor hook.
- listening sockets inherited from the service manager, found through
$LISTEN_FDS. mod_systemd registers the two optional functions
server/listen.c calls, so loading the module is what enables socket
activation and not loading it is what disables it.
- ap_extended_status forced on in pre_config, so that the monitor hook
has request counts to report.

None of that is observable over HTTP, so the tests observe it directly.

How the tests run without systemd, and without privileges
---------------------------------------------------------
There is no need for a service manager to exercise the protocol. Only
test_005 involves systemd at all; the rest run anywhere, and need nothing
from the systemd package beyond the libsystemd httpd itself is linked
against.

test_001_notify.py $NOTIFY_SOCKET is an ordinary AF_UNIX datagram
socket the test binds itself (env.NotifyListener).
sd_notify writes to whatever that variable names, so
the test reads httpd's notifications straight off the
socket. HttpdTestEnv.set_httpd_env() puts the
variable into the environment apachectl passes on.

test_002_monitor.py The periodic status line. ap_run_monitor() is
called once every ten turns of the parent's ~1s
loop, so these tests wait up to 25 seconds.

test_003_extended_status.py
The ExtendedStatus side effect, observed through
mod_status.

test_004_socket_activation.py
The test opens the listening socket itself and
hands it to httpd as descriptor 3 with LISTEN_FDS
and LISTEN_PID set, which is the whole protocol.
systemd-socket-activate would do the same, but its
--now option is newer than the systemd on some
distributions, and doing it directly needs no
systemd tooling at all.

test_005_service.py The real thing: a transient Type=notify unit run
with "systemd-run --user". This is what checks that
systemd holds the unit in "activating" until READY=1
arrives, tracks the right MainPID, and shows the
reported STATUS= as the unit's status text.
Skipped when the user has no systemd manager.

The whole package is skipped unless mod_systemd was built, which needs
configure --enable-systemd. A static module is enough for everything
except the test which has to leave mod_systemd out of the configuration;
that one needs --enable-systemd=shared.

Running the last suite where there is no user session
-----------------------------------------------------
"systemctl --user" needs a per-user manager, which a login session has but
a CI container does not; enabling lingering needs privileges. Where that
is a problem, run the tests inside a container with systemd as pid 1,
which rootless podman supports on a cgroup v2 host:

podman run --rm -it --systemd=always \
-v $PWD:/src:z -w /src registry.fedoraproject.org/fedora:latest \
/usr/sbin/init

then, in another terminal, "podman exec" into it and run pytest as a
non-root user with XDG_RUNTIME_DIR set. The other four suites need none of
this and run anywhere.
Empty file.
40 changes: 40 additions & 0 deletions test/modules/arch/linux/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import logging
import os
import sys

import pytest

sys.path.append(os.path.join(os.path.dirname(__file__), '../../..'))

from .env import SystemdTestEnv


def pytest_report_header(config, start_path):
env = SystemdTestEnv()
return f"mod_systemd [apache: {env.get_httpd_version()}, " \
f"mpm: {env.mpm_module}, {env.prefix}]"


@pytest.fixture(scope="package")
def env(pytestconfig) -> SystemdTestEnv:
level = logging.INFO
console = logging.StreamHandler()
console.setLevel(level)
console.setFormatter(logging.Formatter('%(levelname)s: %(message)s'))
logging.getLogger('').addHandler(console)
logging.getLogger('').setLevel(level=level)
env = SystemdTestEnv(pytestconfig=pytestconfig)
if not env.has_systemd_module:
pytest.skip("mod_systemd is not built, configure with --enable-systemd")
env.setup_httpd()
env.apache_access_log_clear()
env.httpd_error_log.clear_log()
env.start_notify_listener()
yield env
env.stop_notify_listener()


@pytest.fixture(autouse=True, scope="package")
def _stop_package_scope(env):
yield
assert env.apache_stop() == 0
Loading