tuxreaperd: send SIGWINCH instead of SIGTERM to Apache (apache2/httpd)
Some checks failed
Build & Upload tux-dock / build (push) Has been cancelled
Some checks failed
Build & Upload tux-dock / build (push) Has been cancelled
This commit is contained in:
parent
cdf530334f
commit
dcda127465
5 changed files with 323 additions and 24 deletions
|
|
@ -10,7 +10,8 @@ This release introduces `tuxreaperd`, a micro init system for containers.
|
|||
- `tuxreaperdgnu.c`: the original standard POSIX/libc daemon used as PID 1 inside containers created by tux-dock.
|
||||
- `tuxreaperdasm.c`: a new freestanding `-nostdlib` syscall engine for `x86_64` and `aarch64` with a tiny static footprint.
|
||||
- Both run as subreapers (`PR_SET_CHILD_SUBREAPER`) with a SIGCHLD handler that reaps every reparented/terminated descendant, so orphaned processes never accumulate as zombies.
|
||||
- Both forward SIGTERM/SIGINT/SIGHUP to the primary workload and propagate its exit status (128 + signal for signal deaths).
|
||||
- Both proxy lifecycle signals (SIGTERM, SIGQUIT, SIGINT, SIGHUP, SIGUSR1, SIGUSR2) to the child process tree and propagate the primary workload's exit status (128 + signal for signal deaths).
|
||||
- Special-cases Apache's questionable use of `SIGWINCH` for graceful shutdown: when `SIGTERM` is received, any process whose `/proc/<pid>/exe` is `/usr/sbin/apache2`, `/usr/sbin/httpd`, or `/usr/local/apache2/bin/httpd` gets `SIGWINCH` instead, while every other process receives `SIGTERM`.
|
||||
- `compile.sh` detects the host architecture and builds the freestanding implementation when possible, falling back to `tuxreaperdgnu.c` on failure or unsupported hosts.
|
||||
- `DockerManager::createContainer` bind-mounts `tuxreaperd` into the container as PID 1 and resolves the binary next to the `tux-dock` executable when it is not in the current directory.
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ It gives you a guided, keyboard-first TUI for common Docker operations without m
|
|||
- Direct `fork`/`exec` process execution for CLI-backed streaming and interactive commands.
|
||||
- Persistent container listings that retain exited containers.
|
||||
- Robust stop handling with state polling, timeout retry, and idempotent stop responses.
|
||||
- `tuxreaperd` micro init system: a tiny statically-linked C daemon mounted into created containers as PID 1, acting as a subreaper so orphaned processes never linger as zombies, forwarding signals, and propagating exit status.
|
||||
- `tuxreaperd` micro init system: a tiny statically-linked C daemon mounted into created containers as PID 1, acting as a subreaper so orphaned processes never linger as zombies, broadcasting lifecycle signals (SIGTERM, SIGQUIT, SIGINT, SIGHUP, SIGUSR1, SIGUSR2) to the whole process tree via `kill(-1, sig)`, and propagating exit status. [Processes don't fear tuxreaperd!](tuxreaperdpromo.jpg)
|
||||
- About screen in-app with project/version/repository info.
|
||||
|
||||
---
|
||||
|
|
|
|||
261
tuxreaperdasm.c
261
tuxreaperdasm.c
|
|
@ -5,6 +5,14 @@ struct k_sigaction {
|
|||
unsigned long mask;
|
||||
};
|
||||
|
||||
#if defined(__x86_64__)
|
||||
#define SA_RESTORER_FLAG 0x04000000UL
|
||||
void restore_rt(void);
|
||||
#elif defined(__aarch64__)
|
||||
#define SA_RESTORER_FLAG 0UL
|
||||
#define restore_rt ((void (*)(void))0)
|
||||
#endif
|
||||
|
||||
#if defined(__x86_64__)
|
||||
|
||||
static inline long sys_write(int fd, const void *buf, long n) {
|
||||
|
|
@ -125,6 +133,51 @@ static inline long sys_rt_sigsuspend(const unsigned long *mask, unsigned long si
|
|||
return ret;
|
||||
}
|
||||
|
||||
static inline long sys_openat(int dirfd, const char *path, int flags, int mode) {
|
||||
long ret;
|
||||
register long r10 __asm__("r10") = mode;
|
||||
__asm__ volatile (
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(257), "D"((long)dirfd), "S"(path), "d"(flags), "r"(r10)
|
||||
: "rcx", "r11", "memory"
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline long sys_getdents64(int fd, void *dirp, unsigned int count) {
|
||||
long ret;
|
||||
__asm__ volatile (
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(217), "D"((long)fd), "S"(dirp), "d"(count)
|
||||
: "rcx", "r11", "memory"
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline long sys_readlink(const char *path, char *buf, long bufsiz) {
|
||||
long ret;
|
||||
__asm__ volatile (
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(89), "D"(path), "S"(buf), "d"(bufsiz)
|
||||
: "rcx", "r11", "memory"
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static inline long sys_close(int fd) {
|
||||
long ret;
|
||||
__asm__ volatile (
|
||||
"syscall"
|
||||
: "=a"(ret)
|
||||
: "a"(3), "D"((long)fd)
|
||||
: "rcx", "r11", "memory"
|
||||
);
|
||||
return ret;
|
||||
}
|
||||
|
||||
__asm__(
|
||||
".text\n"
|
||||
".globl restore_rt\n"
|
||||
|
|
@ -299,13 +352,62 @@ static inline long sys_rt_sigsuspend(const unsigned long *mask, unsigned long si
|
|||
return x0;
|
||||
}
|
||||
|
||||
__asm__(
|
||||
".text\n"
|
||||
".globl restore_rt\n"
|
||||
"restore_rt:\n"
|
||||
" mov x8, #139\n"
|
||||
" svc #0\n"
|
||||
);
|
||||
static inline long sys_openat(int dirfd, const char *path, int flags, int mode) {
|
||||
register long x8 __asm__("x8") = 56;
|
||||
register long x0 __asm__("x0") = dirfd;
|
||||
register long x1 __asm__("x1") = (long)path;
|
||||
register long x2 __asm__("x2") = flags;
|
||||
register long x3 __asm__("x3") = mode;
|
||||
__asm__ volatile (
|
||||
"svc #0"
|
||||
: "+r"(x0)
|
||||
: "r"(x8), "r"(x1), "r"(x2), "r"(x3)
|
||||
: "memory", "cc"
|
||||
);
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long sys_getdents64(int fd, void *dirp, unsigned int count) {
|
||||
register long x8 __asm__("x8") = 61;
|
||||
register long x0 __asm__("x0") = fd;
|
||||
register long x1 __asm__("x1") = (long)dirp;
|
||||
register long x2 __asm__("x2") = count;
|
||||
__asm__ volatile (
|
||||
"svc #0"
|
||||
: "+r"(x0)
|
||||
: "r"(x8), "r"(x1), "r"(x2)
|
||||
: "memory", "cc"
|
||||
);
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long sys_readlink(const char *path, char *buf, long bufsiz) {
|
||||
/* AArch64 has no plain readlink syscall; readlinkat is the ABI. */
|
||||
register long x8 __asm__("x8") = 78; /* __NR_readlinkat */
|
||||
register long x0 __asm__("x0") = -100; /* AT_FDCWD */
|
||||
register long x1 __asm__("x1") = (long)path;
|
||||
register long x2 __asm__("x2") = (long)buf;
|
||||
register long x3 __asm__("x3") = bufsiz;
|
||||
__asm__ volatile (
|
||||
"svc #0"
|
||||
: "+r"(x0)
|
||||
: "r"(x8), "r"(x1), "r"(x2), "r"(x3)
|
||||
: "memory", "cc"
|
||||
);
|
||||
return x0;
|
||||
}
|
||||
|
||||
static inline long sys_close(int fd) {
|
||||
register long x8 __asm__("x8") = 57;
|
||||
register long x0 __asm__("x0") = fd;
|
||||
__asm__ volatile (
|
||||
"svc #0"
|
||||
: "+r"(x0)
|
||||
: "r"(x8)
|
||||
: "memory", "cc"
|
||||
);
|
||||
return x0;
|
||||
}
|
||||
|
||||
__asm__(
|
||||
".text\n"
|
||||
|
|
@ -398,14 +500,120 @@ static void my_execvp(const char *file, char *const argv[], char *const envp[])
|
|||
}
|
||||
}
|
||||
|
||||
/* Apache hijacks SIGWINCH (window resize) for graceful shutdown. Yes,
|
||||
really. So when the outside world sends SIGTERM, we have to translate
|
||||
it to SIGWINCH for anything whose /proc/<pid>/exe smells like apache2.
|
||||
This is insane, but Apache is a good web server so I give it a pass. */
|
||||
static const char * const apache_exes[] = {
|
||||
"/usr/sbin/apache2",
|
||||
"/usr/sbin/httpd",
|
||||
"/usr/local/apache2/bin/httpd",
|
||||
0
|
||||
};
|
||||
#define APACHE_IN_SIG 15 /* SIGTERM */
|
||||
#define APACHE_OUT_SIG 28 /* SIGWINCH */
|
||||
|
||||
#define AT_FDCWD -100
|
||||
#define O_RDONLY 0
|
||||
#define O_DIRECTORY 00200000
|
||||
|
||||
struct linux_dirent64 {
|
||||
unsigned long long d_ino;
|
||||
long long d_off;
|
||||
unsigned short d_reclen;
|
||||
unsigned char d_type;
|
||||
char d_name[];
|
||||
};
|
||||
|
||||
static int my_strcmp(const char *a, const char *b) {
|
||||
while (*a && (*a == *b)) {
|
||||
a++;
|
||||
b++;
|
||||
}
|
||||
return (unsigned char)*a - (unsigned char)*b;
|
||||
}
|
||||
|
||||
static int is_digit(char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
static long parse_pid(const char *s) {
|
||||
long pid = 0;
|
||||
while (*s) {
|
||||
if (!is_digit(*s)) return -1;
|
||||
pid = pid * 10 + (*s - '0');
|
||||
s++;
|
||||
}
|
||||
return pid;
|
||||
}
|
||||
|
||||
/* Iterate /proc, read each process's /proc/<pid>/exe symlink, and send
|
||||
out_sig to matching process groups while sending in_sig to everyone
|
||||
else. Skip PID 1 (the reaper itself). */
|
||||
static void proc_remap_signal(int in_sig, int out_sig, const char *const *target_exes) {
|
||||
int procfd = (int)sys_openat(AT_FDCWD, "/proc", O_RDONLY | O_DIRECTORY, 0);
|
||||
if (procfd < 0) return;
|
||||
|
||||
char dirbuf[4096];
|
||||
|
||||
for (;;) {
|
||||
long n = sys_getdents64(procfd, dirbuf, sizeof(dirbuf));
|
||||
if (n <= 0) break;
|
||||
|
||||
for (long pos = 0; pos < n;) {
|
||||
struct linux_dirent64 *de = (struct linux_dirent64 *)(dirbuf + pos);
|
||||
pos += de->d_reclen;
|
||||
|
||||
const char *name = de->d_name;
|
||||
if (name[0] == '.') continue;
|
||||
|
||||
long pid = parse_pid(name);
|
||||
if (pid <= 1) continue;
|
||||
|
||||
char path[32];
|
||||
char *p = path;
|
||||
const char *prefix = "/proc/";
|
||||
for (long i = 0; prefix[i]; i++) *p++ = prefix[i];
|
||||
const char *np = name;
|
||||
while (*np) *p++ = *np++;
|
||||
const char *suffix = "/exe";
|
||||
for (long i = 0; suffix[i]; i++) *p++ = suffix[i];
|
||||
*p = '\0';
|
||||
|
||||
char linkbuf[256];
|
||||
long linklen = sys_readlink(path, linkbuf, sizeof(linkbuf) - 1);
|
||||
if (linklen <= 0) continue;
|
||||
linkbuf[linklen] = '\0';
|
||||
|
||||
int matched = 0;
|
||||
for (int i = 0; target_exes[i]; i++) {
|
||||
long target_len = my_strlen(target_exes[i]);
|
||||
if (linklen == target_len && my_strcmp(linkbuf, target_exes[i]) == 0) {
|
||||
matched = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
sys_kill(pid, out_sig);
|
||||
} else {
|
||||
sys_kill(pid, in_sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sys_close(procfd);
|
||||
}
|
||||
|
||||
static volatile long g_main_child = 0;
|
||||
static volatile int g_child_exited = 0;
|
||||
static volatile int g_main_status = 0;
|
||||
static volatile int g_pending_signal = 0;
|
||||
|
||||
static void forward_sig(int sig) {
|
||||
if (g_main_child > 0) {
|
||||
sys_kill(g_main_child, sig);
|
||||
}
|
||||
static void proxy_sig_handler(int sig) {
|
||||
/* Just record the signal; the main loop does the /proc scan so we
|
||||
stay async-signal-safe here. */
|
||||
g_pending_signal = sig;
|
||||
}
|
||||
|
||||
static void sigchld_handler(int sig) {
|
||||
|
|
@ -426,8 +634,6 @@ static void sigchld_handler(int sig) {
|
|||
#define WIFSIGNALED(s) ((((s) & 0x7f) != 0) && (((s) & 0x7f) != 0x7f))
|
||||
#define WTERMSIG(s) ((s) & 0x7f)
|
||||
|
||||
void restore_rt(void);
|
||||
|
||||
int main(int argc, char **argv, char **envp) {
|
||||
if (argc < 2) {
|
||||
static const char usage[] = "Usage: tuxreaperd <command> [args...]\n";
|
||||
|
|
@ -443,22 +649,30 @@ int main(int argc, char **argv, char **envp) {
|
|||
|
||||
struct k_sigaction sa;
|
||||
sa.handler = sigchld_handler;
|
||||
sa.flags = 0x10000000 /* SA_RESTART */ | 1 /* SA_NOCLDSTOP */ | 0x04000000 /* SA_RESTORER */;
|
||||
sa.flags = 0x10000000 /* SA_RESTART */ | 1 /* SA_NOCLDSTOP */ | SA_RESTORER_FLAG;
|
||||
sa.restorer = restore_rt;
|
||||
sa.mask = 0;
|
||||
sys_rt_sigaction(17 /* SIGCHLD */, &sa, 0, sizeof(sa.mask));
|
||||
|
||||
sa.handler = forward_sig;
|
||||
sa.flags = 0x10000000 /* SA_RESTART */ | 0x04000000 /* SA_RESTORER */;
|
||||
sa.handler = proxy_sig_handler;
|
||||
sa.flags = 0x10000000 /* SA_RESTART */ | SA_RESTORER_FLAG;
|
||||
sa.restorer = restore_rt;
|
||||
sa.mask = 0;
|
||||
sys_rt_sigaction(15 /* SIGTERM */, &sa, 0, sizeof(sa.mask));
|
||||
sys_rt_sigaction(3 /* SIGQUIT */, &sa, 0, sizeof(sa.mask));
|
||||
sys_rt_sigaction(2 /* SIGINT */, &sa, 0, sizeof(sa.mask));
|
||||
sys_rt_sigaction(1 /* SIGHUP */, &sa, 0, sizeof(sa.mask));
|
||||
sys_rt_sigaction(10 /* SIGUSR1 */, &sa, 0, sizeof(sa.mask));
|
||||
sys_rt_sigaction(12 /* SIGUSR2 */, &sa, 0, sizeof(sa.mask));
|
||||
|
||||
unsigned long block_mask =
|
||||
(1UL << (17 - 1)) | (1UL << (15 - 1)) |
|
||||
(1UL << (2 - 1)) | (1UL << (1 - 1));
|
||||
(1UL << (17 - 1)) | /* SIGCHLD */
|
||||
(1UL << (15 - 1)) | /* SIGTERM */
|
||||
(1UL << (3 - 1)) | /* SIGQUIT */
|
||||
(1UL << (2 - 1)) | /* SIGINT */
|
||||
(1UL << (1 - 1)) | /* SIGHUP */
|
||||
(1UL << (10 - 1)) | /* SIGUSR1 */
|
||||
(1UL << (12 - 1)); /* SIGUSR2 */
|
||||
unsigned long old_mask;
|
||||
sys_rt_sigprocmask(0 /* SIG_BLOCK */, &block_mask, &old_mask, sizeof(block_mask));
|
||||
|
||||
|
|
@ -473,6 +687,17 @@ int main(int argc, char **argv, char **envp) {
|
|||
|
||||
while (!g_child_exited) {
|
||||
sys_rt_sigsuspend(&old_mask, sizeof(old_mask));
|
||||
|
||||
if (g_pending_signal != 0) {
|
||||
int sig = g_pending_signal;
|
||||
g_pending_signal = 0;
|
||||
if (sig == APACHE_IN_SIG) {
|
||||
proc_remap_signal(APACHE_IN_SIG, APACHE_OUT_SIG, apache_exes);
|
||||
} else {
|
||||
/* Unmapped signals get the old broadcast treatment. */
|
||||
sys_kill(-1, sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int status;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,31 @@
|
|||
#define _GNU_SOURCE
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <sys/wait.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* Apache hijacks SIGWINCH (window resize) for graceful shutdown. Yes,
|
||||
really. So when the outside world sends SIGTERM, we have to translate
|
||||
it to SIGWINCH for anything whose /proc/<pid>/exe smells like apache2.
|
||||
This is insane, but Apache is a good web server so I give it a pass. */
|
||||
static const char * const apache_exes[] = {
|
||||
"/usr/sbin/apache2",
|
||||
"/usr/sbin/httpd",
|
||||
"/usr/local/apache2/bin/httpd",
|
||||
0
|
||||
};
|
||||
#define APACHE_IN_SIG SIGTERM
|
||||
#define APACHE_OUT_SIG SIGWINCH
|
||||
|
||||
static volatile pid_t g_main_child = 0;
|
||||
static volatile sig_atomic_t g_child_exited = 0;
|
||||
static volatile int g_main_status = 0;
|
||||
static volatile sig_atomic_t g_pending_signal = 0;
|
||||
|
||||
static void sigchld_handler(int sig) {
|
||||
(void)sig;
|
||||
|
|
@ -25,10 +41,50 @@ static void sigchld_handler(int sig) {
|
|||
}
|
||||
}
|
||||
|
||||
static void forward_signal(int sig) {
|
||||
if (g_main_child > 0) {
|
||||
kill(g_main_child, sig);
|
||||
static void proxy_signal_handler(int sig) {
|
||||
// Just record the signal; the main loop does the /proc scan so we
|
||||
// stay async-signal-safe here.
|
||||
g_pending_signal = sig;
|
||||
}
|
||||
|
||||
static void proc_remap_signal(int in_sig, int out_sig, const char *const *target_exes) {
|
||||
DIR *proc = opendir("/proc");
|
||||
if (!proc) return;
|
||||
|
||||
struct dirent *de;
|
||||
|
||||
while ((de = readdir(proc)) != NULL) {
|
||||
if (de->d_name[0] == '.') continue;
|
||||
|
||||
char *endptr;
|
||||
long pid = strtol(de->d_name, &endptr, 10);
|
||||
if (*endptr != '\0' || pid <= 1) continue;
|
||||
|
||||
char path[64];
|
||||
snprintf(path, sizeof(path), "/proc/%ld/exe", pid);
|
||||
|
||||
char linkbuf[256];
|
||||
ssize_t linklen = readlink(path, linkbuf, sizeof(linkbuf) - 1);
|
||||
if (linklen <= 0) continue;
|
||||
linkbuf[linklen] = '\0';
|
||||
|
||||
int matched = 0;
|
||||
for (int i = 0; target_exes[i]; i++) {
|
||||
size_t target_len = strlen(target_exes[i]);
|
||||
if ((size_t)linklen == target_len && strcmp(linkbuf, target_exes[i]) == 0) {
|
||||
matched = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
kill(pid, out_sig);
|
||||
} else {
|
||||
kill(pid, in_sig);
|
||||
}
|
||||
}
|
||||
|
||||
closedir(proc);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
|
|
@ -45,12 +101,15 @@ int main(int argc, char *argv[]) {
|
|||
|
||||
// Set up signal forwarding
|
||||
struct sigaction sa_forward;
|
||||
sa_forward.sa_handler = forward_signal;
|
||||
sa_forward.sa_handler = proxy_signal_handler;
|
||||
sigemptyset(&sa_forward.sa_mask);
|
||||
sa_forward.sa_flags = SA_RESTART;
|
||||
sigaction(SIGTERM, &sa_forward, NULL);
|
||||
sigaction(SIGQUIT, &sa_forward, NULL);
|
||||
sigaction(SIGINT, &sa_forward, NULL);
|
||||
sigaction(SIGHUP, &sa_forward, NULL);
|
||||
sigaction(SIGUSR1, &sa_forward, NULL);
|
||||
sigaction(SIGUSR2, &sa_forward, NULL);
|
||||
|
||||
// Set up SIGCHLD reaper handler
|
||||
struct sigaction sa_chld;
|
||||
|
|
@ -64,8 +123,11 @@ int main(int argc, char *argv[]) {
|
|||
sigemptyset(&block_mask);
|
||||
sigaddset(&block_mask, SIGCHLD);
|
||||
sigaddset(&block_mask, SIGTERM);
|
||||
sigaddset(&block_mask, SIGQUIT);
|
||||
sigaddset(&block_mask, SIGINT);
|
||||
sigaddset(&block_mask, SIGHUP);
|
||||
sigaddset(&block_mask, SIGUSR1);
|
||||
sigaddset(&block_mask, SIGUSR2);
|
||||
sigprocmask(SIG_BLOCK, &block_mask, &old_mask);
|
||||
|
||||
// Spawn the primary workload
|
||||
|
|
@ -85,6 +147,17 @@ int main(int argc, char *argv[]) {
|
|||
// Block until the primary process exits
|
||||
while (!g_child_exited) {
|
||||
sigsuspend(&old_mask);
|
||||
|
||||
if (g_pending_signal != 0) {
|
||||
int sig = g_pending_signal;
|
||||
g_pending_signal = 0;
|
||||
if (sig == APACHE_IN_SIG) {
|
||||
proc_remap_signal(APACHE_IN_SIG, APACHE_OUT_SIG, apache_exes);
|
||||
} else {
|
||||
// Unmapped signals get the old broadcast treatment.
|
||||
kill(-1, sig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final sweep of any remaining lingering zombies
|
||||
|
|
|
|||
BIN
tuxreaperdpromo.jpg
Normal file
BIN
tuxreaperdpromo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Loading…
Add table
Add a link
Reference in a new issue