mirror of
https://github.com/bdnugget/kcgi.git
synced 2026-11-07 05:47:27 +00:00
1136 lines
25 KiB
C
1136 lines
25 KiB
C
/* $Id$ */
|
|
/*
|
|
* Copyright (c) 2015--2016 Kristaps Dzonsons <kristaps@bsd.lv>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/un.h>
|
|
#include <sys/wait.h>
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <getopt.h>
|
|
#include <limits.h>
|
|
#include <pwd.h>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <syslog.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
/*
|
|
* This is used by the "variable pool" implementation to keep track of
|
|
* children, their input descriptor, and their active work.
|
|
*/
|
|
struct worker {
|
|
int fd; /* active request fd or -1 */
|
|
int ctrl; /* control socket */
|
|
pid_t pid; /* process */
|
|
time_t last; /* last deschedule or 0 if never */
|
|
uint64_t cookie;
|
|
};
|
|
|
|
/*
|
|
* Whether we're supposed to stop or whether we've had a child exit.
|
|
*/
|
|
static volatile sig_atomic_t stop = 0;
|
|
static volatile sig_atomic_t chld = 0;
|
|
static volatile sig_atomic_t hup = 0;
|
|
|
|
static int verbose = 0;
|
|
|
|
static void dbg(const char *fmt, ...)
|
|
__attribute__((format(printf, 1, 2)));
|
|
|
|
static void
|
|
sighandlehup(int sig)
|
|
{
|
|
|
|
hup = 1;
|
|
}
|
|
|
|
static void
|
|
sighandlestop(int sig)
|
|
{
|
|
|
|
stop = 1;
|
|
}
|
|
|
|
static void
|
|
sighandlechld(int sig)
|
|
{
|
|
|
|
chld = 1;
|
|
}
|
|
|
|
static void
|
|
dbg(const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
|
|
if (0 == verbose)
|
|
return;
|
|
va_start(ap, fmt);
|
|
vsyslog(LOG_DEBUG, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
/*
|
|
* Fully write a file descriptor and the non-optional buffer.
|
|
* Return 0 on failure, 1 on success.
|
|
*/
|
|
static int
|
|
fullwritefd(int fd, int sendfd, void *b, size_t bsz)
|
|
{
|
|
struct msghdr msg;
|
|
int rc;
|
|
char buf[CMSG_SPACE(sizeof(fd))];
|
|
struct iovec io;
|
|
struct cmsghdr *cmsg;
|
|
struct pollfd pfd;
|
|
|
|
assert(bsz && bsz <= 256);
|
|
|
|
memset(buf, 0, sizeof(buf));
|
|
memset(&msg, 0, sizeof(struct msghdr));
|
|
memset(&io, 0, sizeof(struct iovec));
|
|
|
|
io.iov_base = b;
|
|
io.iov_len = bsz;
|
|
|
|
msg.msg_iov = &io;
|
|
msg.msg_iovlen = 1;
|
|
msg.msg_control = buf;
|
|
msg.msg_controllen = sizeof(buf);
|
|
|
|
cmsg = CMSG_FIRSTHDR(&msg);
|
|
cmsg->cmsg_level = SOL_SOCKET;
|
|
cmsg->cmsg_type = SCM_RIGHTS;
|
|
cmsg->cmsg_len = CMSG_LEN(sizeof(fd));
|
|
|
|
*((int *)CMSG_DATA(cmsg)) = sendfd;
|
|
|
|
msg.msg_controllen = cmsg->cmsg_len;
|
|
|
|
pfd.fd = fd;
|
|
pfd.events = POLLOUT;
|
|
again:
|
|
if ((rc = poll(&pfd, 1, -1)) < 0) {
|
|
syslog(LOG_ERR, "poll: passing "
|
|
"connection to worker: %m");
|
|
return(0);
|
|
} else if (0 == rc) {
|
|
syslog(LOG_WARNING, "poll: passing "
|
|
"connection to worker: timeout!?");
|
|
goto again;
|
|
} else if ( ! (POLLOUT & pfd.revents)) {
|
|
syslog(LOG_ERR, "poll: passing "
|
|
"connection to worker: disconnect");
|
|
return(0);
|
|
} else if (sendmsg(fd, &msg, 0) < 0) {
|
|
syslog(LOG_ERR, "sendmsg: passing "
|
|
"connection to worker: %m");
|
|
return(0);
|
|
}
|
|
return(1);
|
|
}
|
|
|
|
/*
|
|
* Fully read a buffer of size "bufsz".
|
|
* Returns 0 on failure (soft and hard), 1 on success.
|
|
*/
|
|
static int
|
|
fullread(int fd, void *buf, size_t bufsz)
|
|
{
|
|
ssize_t ssz;
|
|
size_t sz;
|
|
struct pollfd pfd;
|
|
int rc;
|
|
|
|
pfd.fd = fd;
|
|
pfd.events = POLLIN;
|
|
|
|
for (sz = 0; sz < bufsz; sz += (size_t)ssz) {
|
|
if ((rc = poll(&pfd, 1, -1)) < 0) {
|
|
syslog(LOG_ERR, "poll: receiving "
|
|
"ack from worker: %m");
|
|
return(0);
|
|
} else if (0 == rc) {
|
|
syslog(LOG_WARNING, "poll: receiving "
|
|
"ack from worker: timeout");
|
|
ssz = 0;
|
|
continue;
|
|
} else if ( ! (POLLIN & pfd.revents)) {
|
|
syslog(LOG_ERR, "poll: receiving "
|
|
"ack from worker: disconnect");
|
|
return(0);
|
|
} else if ((ssz = read(fd, buf + sz, bufsz - sz)) < 0) {
|
|
syslog(LOG_ERR, "read: receiving "
|
|
"ack from worker: %m");
|
|
return(0);
|
|
} else if (0 == ssz && sz > 0) {
|
|
syslog(LOG_ERR, "read: receiving "
|
|
"ack from worker: short read");
|
|
return(0);
|
|
} else if (0 == ssz && sz == 0) {
|
|
syslog(LOG_ERR, "read: receiving "
|
|
"ack from worker: EOF");
|
|
return(0);
|
|
} else if (sz > SIZE_MAX - (size_t)ssz) {
|
|
syslog(LOG_ERR, "read: receiving "
|
|
"ack from worker: overflow");
|
|
return(0);
|
|
}
|
|
}
|
|
|
|
return(1);
|
|
}
|
|
|
|
/*
|
|
* Create a non-blocking socket pair.
|
|
* Remove 0 on failure, 1 on success.
|
|
*/
|
|
static int
|
|
xsocketpair(int *sock)
|
|
{
|
|
int rc, fl1, fl2;
|
|
|
|
sock[0] = sock[1] = -1;
|
|
rc = socketpair(AF_UNIX, SOCK_STREAM, 0, sock);
|
|
if (-1 == rc)
|
|
syslog(LOG_ERR, "socketpair: "
|
|
"connection to worker: %m");
|
|
else if (-1 == (fl1 = fcntl(sock[0], F_GETFL, 0)))
|
|
syslog(LOG_ERR, "fcntl: F_GETFL: "
|
|
"connection to worker: %m");
|
|
else if (-1 == (fl2 = fcntl(sock[1], F_GETFL, 0)))
|
|
syslog(LOG_ERR, "fcntl: F_GETFL: "
|
|
"connection to worker: %m");
|
|
else if (-1 == fcntl(sock[0], F_SETFL, fl1 | O_NONBLOCK))
|
|
syslog(LOG_ERR, "fcntl: F_SETFL: "
|
|
"connection to worker: %m");
|
|
else if (-1 == fcntl(sock[1], F_SETFL, fl2 | O_NONBLOCK))
|
|
syslog(LOG_ERR, "fcntl: F_SETFL: "
|
|
"connection to worker: %m");
|
|
else
|
|
return(1);
|
|
|
|
if (-1 != sock[0])
|
|
close(sock[0]);
|
|
if (-1 != sock[1])
|
|
close(sock[1]);
|
|
|
|
return(0);
|
|
}
|
|
|
|
/*
|
|
* Start a worker for the variable pool.
|
|
*/
|
|
static int
|
|
varpool_start(struct worker *w, const struct worker *ws,
|
|
size_t wsz, int fd, char **nargv)
|
|
{
|
|
int pair[2];
|
|
char buf[64];
|
|
size_t i;
|
|
|
|
w->ctrl = w->fd = -1;
|
|
w->pid = -1;
|
|
|
|
if ( ! xsocketpair(pair))
|
|
return(0);
|
|
w->ctrl = pair[0];
|
|
|
|
if (-1 == (w->pid = fork())) {
|
|
syslog(LOG_ERR, "fork: worker: %m");
|
|
close(pair[0]);
|
|
close(pair[1]);
|
|
w->ctrl = -1;
|
|
return(0);
|
|
} else if (0 == w->pid) {
|
|
/*
|
|
* Close out all current descriptors.
|
|
* Note that we're a "new-mode" FastCGI manager to the
|
|
* child via our environment variable.
|
|
*/
|
|
for (i = 0; i < wsz; i++)
|
|
if (-1 != ws[i].ctrl &&
|
|
-1 == close(ws[i].ctrl))
|
|
syslog(LOG_ERR, "close: "
|
|
"worker cleanup: %m");
|
|
|
|
close(fd);
|
|
snprintf(buf, sizeof(buf), "%d", pair[1]);
|
|
setenv("FCGI_LISTENSOCK_DESCRIPTORS", buf, 1);
|
|
execv(nargv[0], nargv);
|
|
syslog(LOG_ERR, "execv: %s: %m", nargv[0]);
|
|
_exit(EXIT_FAILURE);
|
|
}
|
|
|
|
/* Close the child descriptor. */
|
|
if (-1 == close(pair[1])) {
|
|
syslog(LOG_ERR, "close: worker-%u pipe: %m", w->pid);
|
|
return(0);
|
|
}
|
|
|
|
dbg("worker-%u: started", w->pid);
|
|
return(1);
|
|
}
|
|
|
|
/*
|
|
* A variable-sized pool of web application clients.
|
|
* A minimum of "wsz" applications are always running, and will grow to
|
|
* "maxwsz" at which point no new connections are accepted.
|
|
* This is an EXPERIMENTAL extension.
|
|
*/
|
|
static int
|
|
varpool(size_t wsz, size_t maxwsz, time_t waittime,
|
|
int fd, const char *sockpath, char *argv[])
|
|
{
|
|
struct worker *ws;
|
|
struct worker *slough;
|
|
size_t pfdsz, opfdsz, pfdmaxsz, i, j, minwsz,
|
|
sloughsz, sloughmaxsz;
|
|
int rc, exitcode, afd, accepting;
|
|
struct pollfd *pfd, *opfd;
|
|
struct sockaddr_storage ss;
|
|
socklen_t sslen;
|
|
void *pp;
|
|
time_t t;
|
|
sigset_t set;
|
|
uint64_t cookie;
|
|
|
|
dbg("YOU ARE RUNNING VARIABLE MODE AT YOUR OWN RISK.");
|
|
|
|
signal(SIGCHLD, sighandlechld);
|
|
signal(SIGTERM, sighandlestop);
|
|
signal(SIGHUP, sighandlehup);
|
|
sigemptyset(&set);
|
|
sigaddset(&set, SIGCHLD);
|
|
sigaddset(&set, SIGTERM);
|
|
sigaddset(&set, SIGHUP);
|
|
sigprocmask(SIG_BLOCK, &set, NULL);
|
|
|
|
again:
|
|
stop = chld = hup = 0;
|
|
|
|
/*
|
|
* Allocate worker array, polling descriptor array, and slough
|
|
* array (exiting workers).
|
|
* Set our initial exit code and our acceptance status.
|
|
*/
|
|
minwsz = wsz;
|
|
ws = calloc(wsz, sizeof(struct worker));
|
|
sloughmaxsz = (maxwsz - minwsz) * 2;
|
|
slough = calloc(sloughmaxsz, sizeof(struct worker));
|
|
pfdmaxsz = wsz + 1;
|
|
pfd = calloc(pfdmaxsz, sizeof(struct pollfd));
|
|
exitcode = 0;
|
|
|
|
if (NULL == ws || NULL == pfd || NULL == slough) {
|
|
/* Serious problems. */
|
|
syslog(LOG_ERR, "calloc: initialisation: %m");
|
|
close(fd);
|
|
unlink(sockpath);
|
|
free(ws);
|
|
free(slough);
|
|
free(pfd);
|
|
return(0);
|
|
}
|
|
|
|
/* Zeroth pfd is always the domain socket. */
|
|
pfd[0].fd = fd;
|
|
pfd[0].events = POLLIN;
|
|
|
|
/* Guard against bad de-allocation. */
|
|
for (i = 0; i < wsz; i++) {
|
|
ws[i].ctrl = ws[i].fd = -1;
|
|
ws[i].pid = -1;
|
|
}
|
|
|
|
sloughsz = 0;
|
|
accepting = 1;
|
|
pfdsz = 1;
|
|
|
|
/*
|
|
* Start up the [initial] worker processes.
|
|
* We'll later spin up workers when we need them.
|
|
* If these die immediately, we'll find out below when we
|
|
* unblock our signals.
|
|
*/
|
|
for (i = 0; i < wsz; i++)
|
|
if ( ! varpool_start(&ws[i], ws, wsz, fd, argv))
|
|
goto out;
|
|
pollagain:
|
|
/*
|
|
* Main part.
|
|
* Poll on our control socket (pfd[0]) and the children that
|
|
* have active connections.
|
|
* If we're not accepting new connections, avoid polling on the
|
|
* first entry by offsetting.
|
|
* We have a timeout if we get a termination signal whilst
|
|
* waiting.
|
|
*/
|
|
opfd = accepting ? pfd : pfd + 1;
|
|
opfdsz = accepting ? pfdsz : pfdsz - 1;
|
|
|
|
sigprocmask(SIG_UNBLOCK, &set, NULL);
|
|
rc = poll(opfd, opfdsz, 1000);
|
|
sigprocmask(SIG_BLOCK, &set, NULL);
|
|
|
|
/* Did an error occur? */
|
|
if (rc < 0 && EINTR != errno) {
|
|
syslog(LOG_ERR, "poll: main event: %m");
|
|
goto out;
|
|
}
|
|
|
|
if (stop) {
|
|
/*
|
|
* If we're being requested to stop, go to exit now.
|
|
* We'll immediately kill off our children and wait.
|
|
*/
|
|
dbg("servicing exit request");
|
|
exitcode = 1;
|
|
goto out;
|
|
} else if (chld) {
|
|
/*
|
|
* A child has exited.
|
|
* This can mean one of two things: either a worker has
|
|
* exited abnormally or one of the "sloughed" workers
|
|
* has finished its exit.
|
|
*/
|
|
if (0 == sloughsz) {
|
|
syslog(LOG_ERR, "worker unexpectedly exited");
|
|
goto out;
|
|
}
|
|
|
|
/* Look at the running children. */
|
|
for (i = 0; i < wsz; i++) {
|
|
assert(-1 != ws[i].pid);
|
|
rc = waitpid(ws[i].pid, NULL, WNOHANG);
|
|
if (rc < 0) {
|
|
syslog(LOG_ERR, "wait: worker-%u "
|
|
"check: %m", ws[i].pid);
|
|
goto out;
|
|
} else if (0 == rc)
|
|
continue;
|
|
/*
|
|
* We found a process that has already exited.
|
|
* Close its fd as well so that we know that the
|
|
* pid of -1 means it's completely finished.
|
|
*/
|
|
syslog(LOG_ERR, "worker-%u "
|
|
"unexpectedly exited", ws[i].pid);
|
|
if (-1 == close(ws[i].ctrl))
|
|
syslog(LOG_ERR, "close: worker-%u "
|
|
"control socket: %m",
|
|
ws[wsz - 1].pid);
|
|
ws[i].pid = -1;
|
|
goto out;
|
|
}
|
|
|
|
/* Ok... an exiting child can be reaped. */
|
|
chld = 0;
|
|
for (i = 0; i < sloughsz; ) {
|
|
rc = waitpid(slough[i].pid, NULL, WNOHANG);
|
|
if (0 == rc) {
|
|
i++;
|
|
continue;
|
|
} else if (rc < 0) {
|
|
syslog(LOG_ERR, "wait: sloughed "
|
|
"worker-%u check: %m",
|
|
slough[i].pid);
|
|
goto out;
|
|
}
|
|
dbg("slough: releasing worker-%u\n",
|
|
slough[i].pid);
|
|
if (i < sloughsz - 1)
|
|
slough[i] = slough[sloughsz - 1];
|
|
sloughsz--;
|
|
}
|
|
} else if (hup) {
|
|
dbg("servicing restart request");
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* See if we should reduce our pool size.
|
|
* We only do this if we've grown beyond the minimum.
|
|
*/
|
|
if (wsz > minwsz) {
|
|
t = time(NULL);
|
|
/*
|
|
* We only reduce if the last worker is (1) not running
|
|
* and (2) was last descheduled a while ago.
|
|
*/
|
|
assert(-1 != ws[wsz - 1].fd || ws[wsz - 1].last);
|
|
assert(wsz > 1);
|
|
|
|
if (-1 == ws[wsz - 1].fd &&
|
|
t - ws[wsz - 1].last > waittime) {
|
|
assert(-1 != ws[wsz - 1].ctrl);
|
|
assert(-1 != ws[wsz - 1].pid);
|
|
assert(-1 == ws[wsz - 1].fd);
|
|
assert(-1 == pfd[pfdmaxsz - 1].fd);
|
|
|
|
if (sloughsz >= sloughmaxsz) {
|
|
syslog(LOG_ERR, "slough pool "
|
|
"maximum size reached");
|
|
goto out;
|
|
}
|
|
|
|
/* Close down the worker in the usual way. */
|
|
if (-1 == close(ws[wsz - 1].ctrl)) {
|
|
syslog(LOG_ERR, "close: worker-%u "
|
|
"control socket: %m",
|
|
ws[wsz - 1].pid);
|
|
goto out;
|
|
}
|
|
if (-1 == kill(ws[wsz - 1].pid, SIGTERM)) {
|
|
syslog(LOG_ERR, "kill: worker-%u: %m",
|
|
ws[wsz - 1].pid);
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Append the dying client to the slough array,
|
|
* since workers may take time to die.
|
|
*/
|
|
dbg("slough: acquiring worker-%u\n",
|
|
ws[wsz - 1].pid);
|
|
slough[sloughsz++] = ws[wsz - 1];
|
|
|
|
/* Reallocations. */
|
|
pp = reallocarray(ws, wsz - 1,
|
|
sizeof(struct worker));
|
|
if (NULL == pp) {
|
|
syslog(LOG_ERR, "reallocarray: "
|
|
"worker array: %m");
|
|
goto out;
|
|
}
|
|
ws = pp;
|
|
wsz--;
|
|
pp = reallocarray(pfd, pfdmaxsz - 1,
|
|
sizeof(struct pollfd));
|
|
if (NULL == pp) {
|
|
syslog(LOG_ERR, "reallocarray: "
|
|
"descriptor array: %m");
|
|
goto out;
|
|
}
|
|
pfd = pp;
|
|
pfdmaxsz--;
|
|
}
|
|
}
|
|
|
|
if (0 == rc)
|
|
goto pollagain;
|
|
|
|
/*
|
|
* Now we see which of the workers has exited.
|
|
* We do this until we've processed all of them.
|
|
*/
|
|
assert(rc > 0);
|
|
for (i = 1; i < pfdsz && rc > 0; ) {
|
|
if (POLLHUP & pfd[i].revents ||
|
|
POLLERR & pfd[i].revents) {
|
|
syslog(LOG_ERR, "poll: worker disconnect");
|
|
goto out;
|
|
} else if ( ! (POLLIN & pfd[i].revents)) {
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* Read the "identifier" that the child process gives
|
|
* to us.
|
|
*/
|
|
if ( ! fullread(pfd[i].fd, &cookie, sizeof(uint64_t)))
|
|
goto out;
|
|
|
|
/*
|
|
* First, look up the worker that's now free.
|
|
* Mark its active descriptor as free.
|
|
* TODO: use a queue of "working" processes to prevent
|
|
* this from being so expensive.
|
|
*/
|
|
for (j = 0; j < wsz; j++)
|
|
if (ws[j].cookie == cookie)
|
|
break;
|
|
|
|
if (j == wsz) {
|
|
syslog(LOG_ERR, "poll: bad worker response");
|
|
goto out;
|
|
}
|
|
|
|
dbg("worker-%u: release %d", ws[j].pid, ws[j].fd);
|
|
|
|
/*
|
|
* Close the descriptor (that we still hold) and mark
|
|
* this worker as no longer working.
|
|
*/
|
|
rc--;
|
|
close(ws[j].fd);
|
|
if (0 == accepting) {
|
|
accepting = 1;
|
|
dbg("rate-limiting: disabled");
|
|
}
|
|
ws[j].fd = -1;
|
|
ws[j].last = time(NULL);
|
|
|
|
/*
|
|
* Now, clear the active descriptor from the file
|
|
* descriptor array.
|
|
* Do so by flipping it into the last slot then
|
|
* truncating the array size.
|
|
* Obviously, we only do this if we're not the current
|
|
* end of array...
|
|
*/
|
|
if (pfdsz - 1 != i)
|
|
pfd[i] = pfd[pfdsz - 1];
|
|
pfd[pfdsz - 1].fd = -1;
|
|
pfdsz--;
|
|
}
|
|
|
|
if (0 == accepting)
|
|
goto pollagain;
|
|
|
|
if (POLLHUP & pfd[0].revents) {
|
|
syslog(LOG_ERR, "poll: control hangup");
|
|
goto out;
|
|
} else if (POLLERR & pfd[0].revents) {
|
|
syslog(LOG_ERR, "poll: control error?");
|
|
goto out;
|
|
} else if ( ! (POLLIN & pfd[0].revents))
|
|
goto pollagain;
|
|
|
|
/*
|
|
* We have a new request.
|
|
* First, see if we need to allocate more workers.
|
|
*/
|
|
if (pfdsz == pfdmaxsz) {
|
|
if (wsz + 1 > maxwsz) {
|
|
accepting = 0;
|
|
dbg("rate-limiting: enabled");
|
|
goto pollagain;
|
|
}
|
|
pp = reallocarray(pfd, pfdmaxsz + 1,
|
|
sizeof(struct pollfd));
|
|
if (NULL == pp) {
|
|
syslog(LOG_ERR, "reallocarray: workers: %m");
|
|
goto out;
|
|
}
|
|
pfd = pp;
|
|
memset(&pfd[pfdmaxsz], 0, sizeof(struct pollfd));
|
|
pfd[pfdmaxsz].fd = -1;
|
|
|
|
pp = reallocarray(ws, wsz + 1,
|
|
sizeof(struct worker));
|
|
if (NULL == pp) {
|
|
syslog(LOG_ERR, "reallocarray: descriptors: %m");
|
|
goto out;
|
|
}
|
|
ws = pp;
|
|
memset(&ws[wsz], 0, sizeof(struct worker));
|
|
|
|
if ( ! varpool_start(&ws[wsz], ws, wsz + 1, fd, argv))
|
|
goto out;
|
|
pfdmaxsz++;
|
|
wsz++;
|
|
}
|
|
|
|
/*
|
|
* Actually accept the socket.
|
|
* Don't do anything with it, however.
|
|
*/
|
|
sslen = sizeof(ss);
|
|
afd = accept(fd, (struct sockaddr *)&ss, &sslen);
|
|
if (afd < 0) {
|
|
if (EAGAIN == errno ||
|
|
EWOULDBLOCK == errno)
|
|
goto pollagain;
|
|
syslog(LOG_ERR, "accept: new connection: %m");
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Look up the next unavailable worker.
|
|
*/
|
|
for (i = 0; i < wsz; i++)
|
|
if (-1 == ws[i].fd)
|
|
break;
|
|
|
|
assert(i < wsz);
|
|
ws[i].fd = afd;
|
|
#if HAVE_ARC4RANDOM
|
|
ws[i].cookie = arc4random();
|
|
#else
|
|
ws[i].cookie = random();
|
|
#endif
|
|
dbg("worker-%u: acquire %d "
|
|
"(pollers %zu/%zu: workers %zu/%zu)",
|
|
ws[i].pid, afd, pfdsz, pfdmaxsz, wsz, maxwsz);
|
|
pfd[pfdsz].events = POLLIN;
|
|
pfd[pfdsz].fd = ws[i].ctrl;
|
|
pfdsz++;
|
|
|
|
if (fullwritefd(ws[i].ctrl, ws[i].fd, &ws[i].cookie, sizeof(uint64_t)))
|
|
goto pollagain;
|
|
|
|
out:
|
|
if ( ! hup) {
|
|
/*
|
|
* If we're not going to restart, then close the FastCGI
|
|
* file descriptor as soon as possible.
|
|
*/
|
|
dbg("closing control socket");
|
|
if (-1 == close(fd))
|
|
syslog(LOG_ERR, "close: control: %m");
|
|
fd = -1;
|
|
}
|
|
|
|
/*
|
|
* Close the application's control socket; then, if that doesn't
|
|
* make it exit (kcgi(3) will, but other applications may not),
|
|
* we also deliver a SIGTERM.
|
|
*/
|
|
for (i = 0; i < wsz; i++) {
|
|
if (-1 == ws[i].pid)
|
|
continue;
|
|
dbg("worker-%u: terminating", ws[i].pid);
|
|
if (-1 == close(ws[i].ctrl))
|
|
syslog(LOG_ERR, "close: "
|
|
"worker-%u control: %m", ws[i].pid);
|
|
if (-1 == kill(ws[i].pid, SIGTERM))
|
|
syslog(LOG_ERR, "kill: "
|
|
"worker-%u: %m", ws[i].pid);
|
|
}
|
|
|
|
/*
|
|
* Now wait for the children and pending children.
|
|
*/
|
|
for (i = 0; i < wsz; i++) {
|
|
if (-1 == ws[i].pid)
|
|
continue;
|
|
dbg("worker-%u: reaping", ws[i].pid);
|
|
if (-1 == waitpid(ws[i].pid, NULL, 0))
|
|
syslog(LOG_ERR, "wait: "
|
|
"worker-%u: %m", ws[i].pid);
|
|
}
|
|
|
|
for (i = 0; i < sloughsz; i++) {
|
|
dbg("sloughed worker-%u: reaping", slough[i].pid);
|
|
if (-1 == waitpid(slough[i].pid, NULL, 0))
|
|
syslog(LOG_ERR, "wait: sloughed "
|
|
"worker-%u: %m", slough[i].pid);
|
|
}
|
|
|
|
free(ws);
|
|
free(slough);
|
|
free(pfd);
|
|
|
|
if (hup)
|
|
goto again;
|
|
|
|
/*
|
|
* Now we're really exiting.
|
|
* Do our final cleanup if we didn't already...
|
|
*/
|
|
if (-1 != fd) {
|
|
dbg("closing control socket");
|
|
if (-1 == close(fd))
|
|
syslog(LOG_ERR, "close: control: %m");
|
|
}
|
|
return(exitcode);
|
|
}
|
|
|
|
static int
|
|
fixedpool(size_t wsz, int fd, const char *sockpath, char *argv[])
|
|
{
|
|
pid_t *ws;
|
|
size_t i;
|
|
sigset_t set, oset;
|
|
void (*sigfp)(int);
|
|
|
|
/*
|
|
* Dying children should notify us that something is horribly
|
|
* wrong and we should exit.
|
|
* Also handle SIGTERM in the same way.
|
|
*/
|
|
signal(SIGTERM, sighandlestop);
|
|
signal(SIGCHLD, sighandlechld);
|
|
signal(SIGHUP, sighandlehup);
|
|
sigemptyset(&set);
|
|
sigaddset(&set, SIGCHLD);
|
|
sigaddset(&set, SIGTERM);
|
|
sigaddset(&set, SIGHUP);
|
|
sigprocmask(SIG_BLOCK, &set, &oset);
|
|
|
|
again:
|
|
hup = stop = chld = 0;
|
|
|
|
/* Allocate worker array. */
|
|
if (NULL == (ws = calloc(wsz, sizeof(pid_t)))) {
|
|
syslog(LOG_ERR, "calloc: initialisation: %m");
|
|
close(fd);
|
|
unlink(sockpath);
|
|
return(0);
|
|
}
|
|
|
|
/*
|
|
* "Zero" the workers.
|
|
* This is in case the initialisation fails.
|
|
*/
|
|
for (i = 0; i < wsz; i++)
|
|
ws[i] = -1;
|
|
|
|
for (i = 0; i < wsz; i++) {
|
|
if (-1 == (ws[i] = fork())) {
|
|
syslog(LOG_ERR, "fork: worker: %m");
|
|
goto out;
|
|
} else if (0 == ws[i]) {
|
|
/*
|
|
* Assign stdin to be the socket over which
|
|
* we're going to transfer request descriptors
|
|
* when we get them.
|
|
*/
|
|
if (-1 == dup2(fd, STDIN_FILENO)) {
|
|
syslog(LOG_ERR, "dup2: worker: %m");
|
|
_exit(EXIT_FAILURE);
|
|
}
|
|
close(fd);
|
|
execv(argv[0], argv);
|
|
syslog(LOG_ERR, "execve: %s: %m", argv[0]);
|
|
_exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
sigsuspend(&oset);
|
|
|
|
if (stop)
|
|
dbg("servicing exit request");
|
|
else if (chld)
|
|
syslog(LOG_ERR, "worker unexpectedly exited");
|
|
else
|
|
dbg("servicing restart request");
|
|
out:
|
|
if ( ! hup) {
|
|
/*
|
|
* If we're not going to restart, then close the FastCGI
|
|
* file descriptor as soon as possible.
|
|
*/
|
|
if (-1 == close(fd))
|
|
syslog(LOG_ERR, "close: control: %m");
|
|
fd = -1;
|
|
}
|
|
|
|
/* Suppress child exit signals whilst we kill them. */
|
|
sigfp = signal(SIGCHLD, SIG_DFL);
|
|
|
|
/*
|
|
* Now wait on the children.
|
|
* This can take forever, but properly-written children will
|
|
* exit when receiving SIGTERM.
|
|
*/
|
|
for (i = 0; i < wsz; i++)
|
|
if (-1 != ws[i] && -1 == kill(ws[i], SIGTERM))
|
|
syslog(LOG_ERR, "kill: worker-%u: %m", ws[i]);
|
|
|
|
for (i = 0; i < wsz; i++)
|
|
if (-1 != ws[i] && -1 == waitpid(ws[i], NULL, 0))
|
|
syslog(LOG_ERR, "wait: worker-%u: %m", ws[i]);
|
|
|
|
signal(SIGCHLD, sigfp);
|
|
|
|
free(ws);
|
|
|
|
if (hup)
|
|
goto again;
|
|
|
|
/*
|
|
* Now we're really exiting.
|
|
* Do our final cleanup if we didn't already...
|
|
*/
|
|
if (-1 != fd && -1 == close(fd))
|
|
syslog(LOG_ERR, "close: control: %m");
|
|
return(1);
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
int c, fd, varp, usemax, useq, nod, logop;
|
|
struct passwd *pw;
|
|
size_t i, wsz, sz, lsz, maxwsz;
|
|
time_t waittime;
|
|
const char *pname, *sockpath, *chpath,
|
|
*sockuser, *procuser, *errstr;
|
|
struct sockaddr_un un;
|
|
mode_t old_umask;
|
|
uid_t sockuid, procuid;
|
|
gid_t sockgid, procgid;
|
|
char **nargv;
|
|
|
|
if ((pname = strrchr(argv[0], '/')) == NULL)
|
|
pname = argv[0];
|
|
else
|
|
++pname;
|
|
|
|
if (0 != geteuid()) {
|
|
fprintf(stderr, "%s: need root privileges\n", pname);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
|
|
sockuid = procuid = sockgid = procgid = -1;
|
|
wsz = 5;
|
|
usemax = useq = 0;
|
|
sockpath = "/var/www/run/httpd.sock";
|
|
chpath = "/var/www";
|
|
sockuser = procuser = NULL;
|
|
varp = 0;
|
|
nod = 0;
|
|
maxwsz = lsz = 0;
|
|
waittime = 60 * 5;
|
|
|
|
while (-1 != (c = getopt(argc, argv, "l:p:n:N:s:u:U:rvdw:")))
|
|
switch (c) {
|
|
case ('l'):
|
|
useq = 1;
|
|
lsz = strtonum(optarg, 1, INT_MAX, &errstr);
|
|
if (NULL == errstr)
|
|
break;
|
|
fprintf(stderr, "-l must be "
|
|
"between 1 and %d\n", INT_MAX);
|
|
return(EXIT_FAILURE);
|
|
case ('n'):
|
|
wsz = strtonum(optarg, 0, INT_MAX, &errstr);
|
|
if (NULL == errstr)
|
|
break;
|
|
fprintf(stderr, "-n must be "
|
|
"between 0 and %d\n", INT_MAX);
|
|
return(EXIT_FAILURE);
|
|
case ('N'):
|
|
usemax = 1;
|
|
maxwsz = strtonum(optarg, 0, INT_MAX, &errstr);
|
|
if (NULL == errstr)
|
|
break;
|
|
fprintf(stderr, "-N must be "
|
|
"between 0 and %d\n", INT_MAX);
|
|
return(EXIT_FAILURE);
|
|
case ('p'):
|
|
chpath = optarg;
|
|
break;
|
|
case ('s'):
|
|
sockpath = optarg;
|
|
break;
|
|
case ('u'):
|
|
sockuser = optarg;
|
|
break;
|
|
case ('U'):
|
|
procuser = optarg;
|
|
break;
|
|
case ('r'):
|
|
varp = 1;
|
|
break;
|
|
case ('v'):
|
|
verbose = 1;
|
|
break;
|
|
case ('d'):
|
|
nod = 1;
|
|
break;
|
|
case ('w'):
|
|
waittime = strtonum(optarg, 0, INT_MAX, &errstr);
|
|
if (NULL == errstr)
|
|
break;
|
|
fprintf(stderr, "-w must be "
|
|
"between 0 and %d\n", INT_MAX);
|
|
return(EXIT_FAILURE);
|
|
default:
|
|
goto usage;
|
|
}
|
|
|
|
argc -= optind;
|
|
argv += optind;
|
|
|
|
if (0 == argc)
|
|
goto usage;
|
|
|
|
if (usemax && varp) {
|
|
if (maxwsz == wsz)
|
|
varp = 0;
|
|
else if (maxwsz < wsz)
|
|
goto usage;
|
|
} else
|
|
maxwsz = wsz * 2;
|
|
|
|
if (0 == useq)
|
|
lsz = (varp ? maxwsz : wsz) * 2;
|
|
|
|
assert(lsz);
|
|
|
|
pw = NULL;
|
|
if (NULL != procuser && NULL == (pw = getpwnam(procuser))) {
|
|
fprintf(stderr, "%s: no such user\n", procuser);
|
|
return(EXIT_FAILURE);
|
|
} else if (NULL != pw) {
|
|
procuid = pw->pw_uid;
|
|
procgid = pw->pw_gid;
|
|
}
|
|
|
|
pw = NULL;
|
|
if (NULL != sockuser && NULL == (pw = getpwnam(sockuser))) {
|
|
fprintf(stderr, "%s: no such user\n", sockuser);
|
|
return(EXIT_FAILURE);
|
|
} else if (NULL != pw) {
|
|
sockuid = pw->pw_uid;
|
|
sockgid = pw->pw_gid;
|
|
}
|
|
|
|
/* Do the usual dance to set up UNIX sockets. */
|
|
memset(&un, 0, sizeof(un));
|
|
un.sun_family = AF_UNIX;
|
|
sz = strlcpy(un.sun_path, sockpath, sizeof(un.sun_path));
|
|
if (sz >= sizeof(un.sun_path)) {
|
|
fprintf(stderr, "socket path to long\n");
|
|
return(EXIT_FAILURE);
|
|
}
|
|
#if !defined(__linux__) && !defined(__sun)
|
|
un.sun_len = sz;
|
|
#endif
|
|
|
|
/*
|
|
* Prepare the socket then unlink any dead existing ones.
|
|
* This is because we want to control the socket.
|
|
*/
|
|
if (-1 == (fd = socket(AF_UNIX, SOCK_STREAM, 0))) {
|
|
perror("socket");
|
|
return(EXIT_FAILURE);
|
|
} else if (-1 == unlink(sockpath)) {
|
|
if (ENOENT != errno) {
|
|
perror(sockpath);
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH);
|
|
|
|
/*
|
|
* Now actually bind to the FastCGI socket set up our
|
|
* listeners, and make sure that we're not blocking.
|
|
* If necessary, change the file's ownership.
|
|
* We buffer up to the number of available workers.
|
|
*/
|
|
if (-1 == bind(fd, (struct sockaddr *)&un, sizeof(un))) {
|
|
perror("bind");
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
umask(old_umask);
|
|
|
|
if (NULL != sockuser)
|
|
if (chown(sockpath, sockuid, sockgid) == -1) {
|
|
perror(sockpath);
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
|
|
if (-1 == listen(fd, lsz)) {
|
|
perror(sockpath);
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
|
|
/*
|
|
* Jail our file-system.
|
|
*/
|
|
if (-1 == chroot(chpath)) {
|
|
perror("chroot");
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
} else if (-1 == chdir("/")) {
|
|
perror("chdir");
|
|
close(fd);
|
|
unlink(sockpath);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
|
|
if (NULL != procuser) {
|
|
if (0 != setgid(procgid)) {
|
|
perror(procuser);
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
} else if (0 != setuid(procuid)) {
|
|
perror(procuser);
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
} else if (-1 != setuid(0)) {
|
|
fprintf(stderr, "%s: managed to regain "
|
|
"root privileges: aborting\n", pname);
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
nargv = calloc(argc + 1, sizeof(char *));
|
|
if (NULL == nargv) {
|
|
perror(NULL);
|
|
close(fd);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
|
|
for (i = 0; i < (size_t)argc; i++)
|
|
nargv[i] = argv[i];
|
|
|
|
if ( ! nod && -1 == daemon(1, 0)) {
|
|
perror("daemon");
|
|
close(fd);
|
|
unlink(sockpath);
|
|
free(nargv);
|
|
return(EXIT_FAILURE);
|
|
}
|
|
|
|
logop = LOG_PID;
|
|
#ifdef LOG_PERROR
|
|
if (nod)
|
|
logop |= LOG_PERROR;
|
|
#endif
|
|
openlog(pname, logop, LOG_DAEMON);
|
|
|
|
c = varp ?
|
|
varpool(wsz, maxwsz, waittime, fd, sockpath, nargv) :
|
|
fixedpool(wsz, fd, sockpath, nargv);
|
|
|
|
free(nargv);
|
|
return(c ? EXIT_SUCCESS : EXIT_FAILURE);
|
|
usage:
|
|
fprintf(stderr, "usage: %s "
|
|
"[-l backlog] "
|
|
"[-n workers] "
|
|
"[-p chroot] "
|
|
"[-s sockpath] "
|
|
"[-u sockuser] "
|
|
"[-U procuser] "
|
|
"-- prog [arg1...]\n", pname);
|
|
return(EXIT_FAILURE);
|
|
}
|