"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
"Omar Polo" <op@omarpolo.com>
Subject:
Re: add gotwebctl(8)
To:
Stefan Sperling <stsp@stsp.name>
Cc:
gameoftrees@openbsd.org
Date:
Sun, 18 Jan 2026 17:41:30 +0100

Download raw body.

Thread
Hello,

Sorry for the delay.

Stefan Sperling <stsp@stsp.name> wrote:
> This patch adds a new utility called gotwebctl(8). It communicates with
> gotwebd(8) via a new control socket which only accepts connections from
> the root user.
> 
> In this initial version only 2 commands are supported: info and stop
> 
> The info command reports the process ID of the gotwebd parent process
> and the current verbosity level.
> 
> The stop command stops gotwebd gracefully.
> Earlier today I committed a change which makes gotwebd stop gracefully
> when a TERM signal is received, closing listening sockets while still
> allowing existing connections to process to completion before terminating.
> 
> I will need this functionality in order to control gotwebd from gotsysd.
> Making this command available via a unix socket avoids having to find
> the process ID of the running gotwebd instance. And allowing gotwebd
> to stop gracefully minimizes the changes of service disruption whenever
> gotsysd needs to restart gotwebd to apply configuration changes.
> 
> And I believe having this new tool is valuable in itself.
> We could extend it with more functionality later. The tool could report
> additional run-time information, change the verbository level on the fly,
> or whatever else we come up with.
> 
> ok?

looks good to me.  ok op@

maybe just remove

> [...]
> --- /dev/null
> +++ gotwebctl/gotwebctl.c
> [...]
> +#define GOTCTL_CMD_INFO "info"
> +#define GOTCTL_CMD_STOP "stop"

because they seem unused.

Also, this was missing as well i think:

--- Makefile
+++ Makefile
@@ -7,7 +7,7 @@ SUBDIR += regress
 .endif

 .if make(clean) || make(obj) || make(release)
-SUBDIR += gotwebd gotd gotsh gotctl template gitwrapper
+SUBDIR += gotwebd gotwebctl gotd gotsh gotctl template gitwrapper
 SUBDIR += gotsysd gotsys gotsysctl
 .endif


Oh, there is a conflict in gotwebd/parse.y now but was trivial to fix :P

If it saves you some minutes, I'm reattaching here your diff rebased and
with the points above addressed.


diff /home/op/w/got
path + /home/op/w/got
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - 59785cb66e3647ab727137e1f1d919208c5848c9
file + Makefile
--- Makefile
+++ Makefile
@@ -7,7 +7,7 @@ SUBDIR += regress
 .endif
 
 .if make(clean) || make(obj) || make(release)
-SUBDIR += gotwebd gotd gotsh gotctl template gitwrapper
+SUBDIR += gotwebd gotwebctl gotd gotsh gotctl template gitwrapper
 SUBDIR += gotsysd gotsys gotsysctl
 .endif
 
@@ -45,9 +45,11 @@ tmpl-regress:
 	${MAKE} -C regress/template
 
 webd: tmpl
+	${MAKE} -C gotwebctl
 	${MAKE} -C gotwebd
 
 webd-install:
+	${MAKE} -C gotwebctl install
 	${MAKE} -C gotwebd install
 
 server:
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - /dev/null
file + gotwebctl/Makefile (mode 644)
--- /dev/null
+++ gotwebctl/Makefile
@@ -0,0 +1,29 @@
+.PATH:${.CURDIR}/../lib ${.CURDIR}/../gotwebd
+
+.include "../got-version.mk"
+
+PROG=		gotwebctl
+SRCS=		gotwebctl.c error.c hash.c
+
+MAN =		${PROG}.8
+
+CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}/../gotwebd
+
+.if defined(PROFILE)
+LDADD = -lutil_p -levent_p
+.else
+LDADD = -lutil -levent
+.endif
+DPADD = ${LIBUTIL} ${LIBEVENT}
+
+.if ${GOT_RELEASE} != "Yes"
+NOMAN = Yes
+.else
+BINDIR = ${PREFIX}/sbin
+.endif
+
+realinstall:
+	${INSTALL} ${INSTALL_COPY} -o ${BINOWN} -g ${BINGRP} \
+	-m ${BINMODE} ${PROG} ${BINDIR}/${PROG}
+
+.include <bsd.prog.mk>
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - /dev/null
file + gotwebctl/gotwebctl.8 (mode 644)
--- /dev/null
+++ gotwebctl/gotwebctl.8
@@ -0,0 +1,85 @@
+.\"
+.\" Copyright (c) 2026 Stefan Sperling
+.\"
+.\" 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.
+.\"
+.Dd $Mdocdate$
+.Dt GOTWEBCTL 8
+.Os
+.Sh NAME
+.Nm gotwebctl
+.Nd control the Game of Trees Web Daemon
+.Sh SYNOPSIS
+.Nm
+.Op Fl hV
+.Op Fl f Ar path
+.Ar command
+.Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+controls the
+.Xr gotwebd 8
+daemon.
+.Pp
+Access to the
+.Xr gotwebd 8
+control socket requires root privileges.
+.Pp
+The options for
+.Nm
+are as follows:
+.Bl -tag -width Ds
+.It Fl h
+Display usage information and exit immediately.
+.It Fl f Ar path
+Set the
+.Ar path
+to the control socket which
+.Xr gotwebd 8
+is listening on.
+If not specified, the default path
+.Pa /var/run/gotwebd.sock
+will be used.
+.It Fl V , -version
+Display program version and exit immediately.
+.El
+.Pp
+The commands for
+.Nm
+are as follows:
+.Bl -tag -width Ds
+.It Cm info
+Display information about a running
+.Xr gotwebd 8
+instance.
+.It Cm stop
+Ask a running
+.Xr gotwebd 8
+instance to gracefully stop itself.
+This is equivalent to sending a TERM signal to
+.Xr gotwebd 8
+without requiring knowledge of the process ID to send the signal to.
+.Pp
+The control socket will be closed by
+.Xr gotwebd 8 ,
+allowing a new instance of
+.Xr gotwebd 8
+to take over.
+New FastCGI socket connections will no longer be accepted, but FastCGI
+requests currently in progress are still processed to completion.
+.Sh SEE ALSO
+.Xr got 1 ,
+.Xr gotwebd.conf 5 ,
+.Xr gotwebd 8
+.Sh AUTHORS
+.An Stefan Sperling Aq Mt stsp@openbsd.org
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - /dev/null
file + gotwebctl/gotwebctl.c (mode 644)
--- /dev/null
+++ gotwebctl/gotwebctl.c
@@ -0,0 +1,350 @@
+/*
+ * Copyright (c) 2026 Stefan Sperling <stsp@openbsd.org>
+ *
+ * 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 <sys/queue.h>
+#include <sys/tree.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/stat.h>
+
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <fcntl.h>
+#include <imsg.h>
+#include <limits.h>
+#include <locale.h>
+#include <sha1.h>
+#include <sha2.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syslog.h>
+#include <getopt.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "got_object.h"
+#include "got_version.h"
+#include "got_path.h"
+#include "got_reference.h"
+
+#include "media.h"
+#include "gotwebd.h"
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+struct gotwebctl_cmd {
+	const char	*cmd_name;
+	const struct got_error *(*cmd_main)(int, char *[], int);
+	void		(*cmd_usage)(int);
+};
+
+__dead static void	usage(int, int);
+
+__dead static void	usage_info(int);
+__dead static void	usage_stop(int);
+
+static const struct got_error*		cmd_info(int, char *[], int);
+static const struct got_error*		cmd_stop(int, char *[], int);
+
+static const struct gotwebctl_cmd gotwebctl_commands[] = {
+	{ "info",	cmd_info,	usage_info },
+	{ "stop",	cmd_stop,	usage_stop },
+};
+
+__dead static void
+usage_info(int status)
+{
+	FILE *fp = (status == 0) ? stdout : stderr;
+	fprintf(fp, "usage: %s info\n", getprogname());
+	exit(status);
+}
+
+static const struct got_error *
+show_info(struct imsg *imsg)
+{
+	struct gotwebd_imsg_info info;
+	size_t datalen;
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen != sizeof(info))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&info, imsg->data, sizeof(info));
+
+	printf("gotwebd PID: %d\n", info.pid);
+	printf("verbosity: %d\n", info.verbosity);
+	return NULL;
+}
+
+static const struct got_error *
+cmd_info(int argc, char *argv[], int gotwebd_sock)
+{
+	const struct got_error *err = NULL;
+	struct imsgbuf ibuf;
+	struct imsg imsg;
+	ssize_t n;
+	int done = 0;
+
+	if (unveil(NULL, NULL) != 0)
+		return got_error_from_errno("unveil");
+#ifndef PROFILE
+	if (pledge("stdio", NULL) == -1)
+		return got_error_from_errno("pledge");
+#endif
+	if (imsgbuf_init(&ibuf, gotwebd_sock) == -1)
+		return got_error_from_errno("imsgbuf_init");
+
+	if (imsg_compose(&ibuf, GOTWEBD_IMSG_CTL_INFO, 0, 0, -1,
+	    NULL, 0) == -1) {
+		imsgbuf_clear(&ibuf);
+		return got_error_from_errno("imsg_compose INFO");
+	}
+
+	if (imsgbuf_flush(&ibuf) == -1) {
+		imsgbuf_clear(&ibuf);
+		return got_error_from_errno("imsgbuf_flush");
+	}
+
+	while (!done && err == NULL) {
+		n = imsgbuf_read(&ibuf);
+		if (n == -1) {
+			if  (errno != EAGAIN) {
+				err = got_error_from_errno("imsgbuf_read");
+				break;
+			}
+				
+			sleep(1);
+			continue;
+		}
+		if (n == 0)
+			break;
+
+		n = imsg_get(&ibuf, &imsg);
+		if (n == -1) {
+			err = got_error_from_errno("imsg_get");
+			break;
+		}
+
+		if (n == 0)
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOTWEBD_IMSG_CTL_INFO:
+			err = show_info(&imsg);
+			done = 1;
+			break;
+		default:
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	imsgbuf_clear(&ibuf);
+	return err;
+}
+
+__dead static void
+usage_stop(int status)
+{
+	FILE *fp = (status == 0) ? stdout : stderr;
+	fprintf(fp, "usage: %s stop\n", getprogname());
+	exit(status);
+}
+
+static const struct got_error *
+cmd_stop(int argc, char *argv[], int gotwebd_sock)
+{
+	const struct got_error *err;
+	struct imsgbuf ibuf;
+	struct imsg imsg;
+	ssize_t n;
+
+	if (unveil(NULL, NULL) != 0)
+		return got_error_from_errno("unveil");
+#ifndef PROFILE
+	if (pledge("stdio", NULL) == -1)
+		return got_error_from_errno("pledge");
+#endif
+	if (imsgbuf_init(&ibuf, gotwebd_sock) == -1)
+		return got_error_from_errno("imsgbuf_init");
+
+	if (imsg_compose(&ibuf, GOTWEBD_IMSG_CTL_STOP, 0, 0, -1,
+	    NULL, 0) == -1) {
+		imsgbuf_clear(&ibuf);
+		return got_error_from_errno("imsg_compose STOP");
+	}
+
+	if (imsgbuf_flush(&ibuf) == -1) {
+		imsgbuf_clear(&ibuf);
+		return got_error_from_errno("imsgbuf_flush");
+	}
+
+	for (;;) {
+		n = imsg_get(&ibuf, &imsg);
+		if (n == -1) {
+			err = got_error_from_errno("imsg_get");
+			break;
+		}
+
+		if (n == 0)
+			break;
+
+		switch (imsg.hdr.type) {
+		default:
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	imsgbuf_clear(&ibuf);
+	return err;
+}
+
+static void
+list_commands(FILE *fp)
+{
+	size_t i;
+
+	fprintf(fp, "commands:");
+	for (i = 0; i < nitems(gotwebctl_commands); i++) {
+		const struct gotwebctl_cmd *cmd = &gotwebctl_commands[i];
+		fprintf(fp, " %s", cmd->cmd_name);
+	}
+	fputc('\n', fp);
+}
+
+__dead static void
+usage(int hflag, int status)
+{
+	FILE *fp = (status == 0) ? stdout : stderr;
+
+	fprintf(fp, "usage: %s [-hV] [-f path] command [arg ...]\n",
+	    getprogname());
+	if (hflag)
+		list_commands(fp);
+	exit(status);
+}
+
+static int
+connect_gotwebd(const char *socket_path)
+{
+	int gotwebd_sock = -1;
+	struct sockaddr_un sun;
+
+	if (unveil(socket_path, "w") != 0)
+		err(1, "unveil %s", socket_path);
+
+	if ((gotwebd_sock = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
+		err(1, "socket");
+
+	memset(&sun, 0, sizeof(sun));
+	sun.sun_family = AF_UNIX;
+	if (strlcpy(sun.sun_path, socket_path, sizeof(sun.sun_path)) >=
+	    sizeof(sun.sun_path))
+		errx(1, "gotd socket path too long");
+	if (connect(gotwebd_sock, (struct sockaddr *)&sun, sizeof(sun)) == -1)
+		err(1, "connect: %s", socket_path);
+
+	return gotwebd_sock;
+}
+
+int
+main(int argc, char *argv[])
+{
+	const struct gotwebctl_cmd *cmd;
+	int gotwebd_sock = -1, i;
+	int ch;
+	int hflag = 0, Vflag = 0;
+	static const struct option longopts[] = {
+	    { "version", no_argument, NULL, 'V' },
+	    { NULL, 0, NULL, 0 }
+	};
+	const char *socket_path = GOTWEBD_CONTROL_SOCKET;
+
+	setlocale(LC_CTYPE, "");
+
+#ifndef PROFILE
+	if (pledge("stdio rpath unix unveil", NULL) == -1)
+		err(1, "pledge");
+#endif
+	while ((ch = getopt_long(argc, argv, "+hf:V", longopts, NULL)) != -1) {
+		switch (ch) {
+		case 'h':
+			hflag = 1;
+			break;
+		case 'f':
+			socket_path = optarg;
+			break;
+		case 'V':
+			Vflag = 1;
+			break;
+		default:
+			usage(hflag, 1);
+			/* NOTREACHED */
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+	optind = 1;
+	optreset = 1;
+
+	if (Vflag) {
+		got_version_print_str();
+		return 0;
+	}
+
+	if (argc <= 0)
+		usage(hflag, hflag ? 0 : 1);
+
+	for (i = 0; i < nitems(gotwebctl_commands); i++) {
+		const struct got_error *error;
+
+		cmd = &gotwebctl_commands[i];
+
+		if (strncmp(cmd->cmd_name, argv[0], strlen(argv[0])) != 0)
+			continue;
+
+		if (hflag)
+			cmd->cmd_usage(0);
+#ifdef PROFILE
+		if (unveil("gmon.out", "rwc") != 0)
+			err(1, "unveil", "gmon.out");
+#endif
+		gotwebd_sock = connect_gotwebd(socket_path);
+		if (gotwebd_sock == -1)
+			return 1;
+		error = cmd->cmd_main(argc, argv, gotwebd_sock);
+		close(gotwebd_sock);
+		if (error && error->msg[0] != '\0') {
+			fprintf(stderr, "%s: %s\n", getprogname(), error->msg);
+			return 1;
+		}
+
+		return 0;
+	}
+
+	fprintf(stderr, "%s: unknown command '%s'\n", getprogname(), argv[0]);
+	list_commands(stderr);
+	return 1;
+}
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - 4c6c4cdca05841e58fab572e2bdc7fd2221e01eb
file + gotwebd/gotwebd.8
--- gotwebd/gotwebd.8
+++ gotwebd/gotwebd.8
@@ -133,6 +133,18 @@ Directory containing HTML, CSS, and image files used b
 Default location for the
 .Nm
 listening socket.
+.It Pa /var/run/gotweb-login.sock
+Default location for the
+.Nm
+login socket.
+.It Pa /var/run/gotwebd.sock
+Default location for the
+.Nm
+control socket.
+.Xr gotwebctl 8
+can be used to send commands to
+.Nm
+via this socket.
 .It Pa /tmp/
 Directory for temporary files created by
 .Nm .
@@ -219,6 +231,7 @@ server "example.com" {
 .Xr git-repository 5 ,
 .Xr gotwebd.conf 5 ,
 .Xr httpd.conf 5 ,
+.Xr gotwebctl 8 ,
 .Xr httpd 8
 .Sh AUTHORS
 .An Omar Polo Aq Mt op@openbsd.org
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - f868bed8cfb869337fbd6d4150b9ca347187f066
file + gotwebd/gotwebd.c
--- gotwebd/gotwebd.c
+++ gotwebd/gotwebd.c
@@ -18,6 +18,7 @@
 #include <sys/param.h>
 #include <sys/queue.h>
 #include <sys/tree.h>
+#include <sys/stat.h>
 #include <sys/socket.h>
 #include <sys/wait.h>
 
@@ -66,6 +67,9 @@ void	 gotwebd_dispatch_gotweb(int, short, void *);
 
 struct gotwebd	*gotwebd_env;
 
+static volatile int client_cnt;
+static volatile int stopping;
+
 void
 imsg_event_add(struct imsgev *iev)
 {
@@ -424,6 +428,26 @@ gotwebd_dispatch_gotweb(int fd, short event, void *arg
 }
 
 static void
+control_socket_destroy(void)
+{
+	struct gotwebd *env = gotwebd_env;
+
+	if (env->iev_control == NULL)
+		return;
+
+	event_del(&env->iev_control->ev);
+	imsgbuf_clear(&env->iev_control->ibuf);
+	if (env->iev_control->ibuf.fd != -1)
+		close(env->iev_control->ibuf.fd);
+
+	free(env->iev_control);
+	env->iev_control = NULL;
+
+	free(env->control_sock);
+	env->control_sock = NULL;
+}
+
+static void
 gotwebd_stop(void)
 {
 	if (main_compose_sockets(gotwebd_env, GOTWEBD_IMSG_CTL_STOP,
@@ -433,6 +457,9 @@ gotwebd_stop(void)
 	if (main_compose_login(gotwebd_env, GOTWEBD_IMSG_CTL_STOP,
 	    -1, NULL, 0) == -1)
 		fatal("send_imsg GOTWEBD_IMSG_CTL_STOP");
+
+	control_socket_destroy();
+	stopping = 1;
 }
 
 void
@@ -451,10 +478,14 @@ gotwebd_sighdlr(int sig, short event, void *arg)
 		log_info("%s: ignoring SIGUSR1", __func__);
 		break;
 	case SIGTERM:
+		if (stopping)
+			break;
 		gotwebd_stop();
 		break;
 	case SIGINT:
 		gotwebd_shutdown();
+		exit(0);
+		/* NOTREACHED */
 		break;
 	default:
 		log_warn("unexpected signal %d", sig);
@@ -577,6 +608,293 @@ get_usernames(const char **gotwebd_username, const cha
 	*www_username = colon + 1;
 }
 
+static int
+control_socket_listen(struct gotwebd *env, struct socket *sock,
+    uid_t uid, gid_t gid)
+{
+	int u_fd = -1;
+	mode_t old_umask, mode;
+
+	u_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK| SOCK_CLOEXEC, 0);
+	if (u_fd == -1) {
+		log_warn("socket");
+		return -1;
+	}
+
+	if (unlink(sock->conf.unix_socket_name) == -1) {
+		if (errno != ENOENT) {
+			log_warn("unlink %s", sock->conf.unix_socket_name);
+			close(u_fd);
+			return -1;
+		}
+	}
+
+	old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH);
+	mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH;
+
+	if (bind(u_fd, (struct sockaddr *)&sock->conf.addr.ss,
+	    sock->conf.addr.slen) == -1) {
+		log_warn("bind: %s", sock->conf.unix_socket_name);
+		close(u_fd);
+		(void)umask(old_umask);
+		return -1;
+	}
+
+	(void)umask(old_umask);
+
+	if (chmod(sock->conf.unix_socket_name, mode) == -1) {
+		log_warn("chmod: %s", sock->conf.unix_socket_name);
+		close(u_fd);
+		(void)unlink(sock->conf.unix_socket_name);
+		return -1;
+	}
+
+	if (chown(sock->conf.unix_socket_name, uid, gid) == -1) {
+		log_warn("chown: %s", sock->conf.unix_socket_name);
+		close(u_fd);
+		(void)unlink(sock->conf.unix_socket_name);
+		return -1;
+	}
+
+	if (listen(u_fd, 1) == -1) {
+		log_warn("listen: %s", sock->conf.unix_socket_name);
+		return -1;
+	}
+
+	return u_fd;
+}
+
+struct gotwebd_control_client {
+	int fd;
+	struct imsgev iev;
+	struct event tmo;
+};
+
+static void
+disconnect(struct gotwebd_control_client *client)
+{
+	if (client->fd != -1)
+		close(client->fd);
+	free(client);
+	client_cnt--;
+}
+
+static void
+control_timeout(int fd, short events, void *arg)
+{
+	struct gotwebd_control_client *client = arg;
+
+	log_debug("disconnecting control socket due to timeout");
+
+	disconnect(client);
+}
+
+static void
+send_info(struct gotwebd_control_client *client)
+{
+	struct gotwebd_imsg_info info;
+
+	info.pid = gotwebd_env->pid;
+	info.verbosity = gotwebd_env->gotwebd_verbose;
+
+	if (imsg_compose_event(&client->iev, GOTWEBD_IMSG_CTL_INFO, 0,
+	    gotwebd_env->pid, -1, &info, sizeof(info)) == -1)
+		log_warn("imsg compose INFO");
+}
+
+static void
+control_request(int fd, short event, void *arg)
+{
+	struct gotwebd_control_client *client = arg;
+	struct imsg imsg;
+	ssize_t n;
+
+	if (event & EV_WRITE) {
+		if (imsgbuf_write(&client->iev.ibuf) == -1) {
+			log_warn("imsgbuf_write");
+			disconnect(client);
+			return;
+		}
+
+		if (stopping) {
+			disconnect(client);
+			return;
+		}
+	}
+
+	if (event & EV_READ) {
+		if ((n = imsgbuf_read(&client->iev.ibuf)) == -1)
+			fatal("imsgbuf_read error");
+		if (n == 0) {
+			/* Connection closed. */
+			disconnect(client);
+			return;
+		}
+	}
+
+	for (;;) {
+		n = imsg_get(&client->iev.ibuf, &imsg);
+		if (n == -1) {
+			disconnect(client);
+			return;
+		}
+
+		if (n == 0)
+			break;
+
+		evtimer_del(&client->tmo);
+
+		switch (imsg.hdr.type) {
+		case GOTWEBD_IMSG_CTL_INFO:
+			send_info(client);
+			break;
+		case GOTWEBD_IMSG_CTL_STOP:
+			if (!stopping)
+				gotwebd_stop();
+			disconnect(client);
+			imsg_free(&imsg);
+			return;
+		default:
+			log_warnx("unexpected imsg %d", imsg.hdr.type);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	imsg_event_add(&client->iev);
+}
+
+static void
+control_accept(int fd, short event, void *arg)
+{
+	struct imsgev *iev = arg;
+	struct gotwebd *env = gotwebd_env;
+	struct sockaddr_storage ss;
+	struct timeval backoff;
+	socklen_t len;
+	int s = -1;
+	struct gotwebd_control_client *client = NULL;
+	uid_t euid;
+	gid_t egid;
+
+	backoff.tv_sec = 1;
+	backoff.tv_usec = 0;
+
+	if (stopping)
+		return;
+
+	if (event_add(&iev->ev, NULL) == -1) {
+		log_warn("event_add");
+		return;
+	}
+	if (event & EV_TIMEOUT)
+		return;
+
+	len = sizeof(ss);
+
+	s = accept4(fd, (struct sockaddr *)&ss, &len, 
+	    SOCK_NONBLOCK | SOCK_CLOEXEC);
+	if (s == -1) {
+		switch (errno) {
+		case EINTR:
+		case EWOULDBLOCK:
+		case ECONNABORTED:
+			return;
+		case EMFILE:
+		case ENFILE:
+			event_del(&iev->ev);
+			if (!stopping)
+				evtimer_add(&env->control_pause_ev, &backoff);
+			return;
+		default:
+			log_warn("accept");
+			return;
+		}
+	}
+
+	if (client_cnt >= 1)
+		goto err;
+
+	if (getpeereid(s, &euid, &egid) == -1) {
+		log_warn("getpeerid");
+		goto err;
+	}
+
+	if (euid != 0) {
+		log_warnx("control connection from UID %d denied", euid);
+		goto err;
+	}
+
+	client = calloc(1, sizeof(*client));
+	if (client == NULL) {
+		log_warn("%s: calloc", __func__);
+		goto err;
+	}
+	client->fd = s;
+	s = -1;
+
+	client->iev.handler = control_request;
+	client->iev.events = EV_READ;
+	client->iev.data = client;
+
+	imsgbuf_init(&client->iev.ibuf, client->fd);
+	event_set(&client->iev.ev, client->fd, EV_READ, control_request,
+	    client);
+	imsg_event_add(&client->iev);
+
+	evtimer_set(&client->tmo, control_timeout, client);
+
+	log_debug("%s: control connection on fd %d uid %d gid %d", __func__,
+	    client->fd, euid, egid);
+	client_cnt++;
+	return;
+err:
+	if (client) {
+		close(client->fd);
+		free(client);
+	}
+	if (s != -1)
+		close(s);
+}
+
+static void
+accept_paused(int fd, short event, void *arg)
+{
+	struct gotwebd *env = gotwebd_env;
+
+	if (!stopping)
+		event_add(&env->iev_control->ev, NULL);
+}
+
+static void
+control_socket_init(struct gotwebd *env, uid_t uid, gid_t gid)
+{
+	struct imsgev *iev;
+	int fd;
+
+	log_info("initializing control socket %s",
+	    env->control_sock->conf.unix_socket_name);
+
+	iev = calloc(1, sizeof(*iev));
+	if (iev == NULL)
+		fatal("calloc");
+
+	fd = control_socket_listen(env, env->control_sock, uid, gid);
+	if (fd == -1)
+		exit(1);
+
+	if (imsgbuf_init(&iev->ibuf, fd) == -1)
+		fatal("imsgbuf_init");
+
+	iev->data = iev;
+	event_set(&iev->ev, fd, EV_READ, control_accept, iev);
+	event_add(&iev->ev, NULL);
+	evtimer_set(&env->control_pause_ev, accept_paused, NULL);
+
+	env->iev_control = iev;
+}
+
 int
 main(int argc, char **argv)
 {
@@ -783,11 +1101,15 @@ main(int argc, char **argv)
 		break;
 	}
 
+	env->pid = getpid();
+
 	if (!env->gotwebd_debug && daemon(1, 0) == -1)
 		fatal("daemon");
 
 	evb = event_init();
 
+	control_socket_init(env, pw->pw_uid, pw->pw_gid);
+
 	env->iev_sockets = calloc(1, sizeof(*env->iev_sockets));
 	if (env->iev_sockets == NULL)
 		fatal("calloc");
@@ -867,15 +1189,16 @@ main(int argc, char **argv)
 		err(1, "unveil");
 
 #ifndef PROFILE
-	if (pledge("stdio", NULL) == -1)
+	if (pledge("stdio unix", NULL) == -1)
 		err(1, "pledge");
 #endif
 
 	event_dispatch();
+
+	gotwebd_shutdown();
+
 	event_base_free(evb);
 
-	log_debug("%s gotwebd exiting", getprogname());
-
 	return (0);
 }
 
@@ -1151,6 +1474,8 @@ gotwebd_shutdown(void)
 
 	free(env->login_sock);
 
+	control_socket_destroy();
+
 	do {
 		pid = waitpid(WAIT_ANY, &status, 0);
 		if (pid <= 0)
@@ -1188,5 +1513,4 @@ gotwebd_shutdown(void)
 	free(gotwebd_env);
 
 	log_warnx("gotwebd terminating");
-	exit(0);
 }
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - 0e88f461cc2d5660929306c6218098113ead9fbd
file + gotwebd/gotwebd.conf.5
--- gotwebd/gotwebd.conf.5
+++ gotwebd/gotwebd.conf.5
@@ -218,6 +218,17 @@ commands.
 By default the path
 .Pa /var/run/gotweb-login.sock
 will be used.
+.It Ic control socket Ar path
+Set the
+.Ar path
+to the
+.Ux Ns -domain
+socket for
+.Xr gotwebctl 8
+commands.
+By default the path
+.Pa /var/run/gotwebd.sock
+will be used.
 .It Ic prefork Ar number
 Spawn enough processes such that
 .Ar number
@@ -927,5 +938,6 @@ server "secure.example.com" {
 .Xr got 1 ,
 .Xr httpd.conf 5 ,
 .Xr services 5 ,
+.Xr gotwebctl 8 ,
 .Xr gotwebd 8 ,
 .Xr httpd 8
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - d33a473211f9b6d1f97dac15ca9b453a9ec4035b
file + gotwebd/gotwebd.h
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -43,6 +43,8 @@
 #define GOTWEBD_LOGIN_SOCKET	 "/var/run/gotweb-login.sock"
 #define GOTWEBD_LOGIN_TIMEOUT	 300 /* in seconds */
 
+#define GOTWEBD_CONTROL_SOCKET	 "/var/run/gotwebd.sock"
+
 #define GOTWEBD_MAXDESCRSZ	 1024
 #define GOTWEBD_MAXCLONEURLSZ	 1024
 #define GOTWEBD_CACHESIZE	 1024
@@ -150,6 +152,7 @@ enum imsg_type {
 	GOTWEBD_IMSG_CTL_PIPE,
 	GOTWEBD_IMSG_CTL_START,
 	GOTWEBD_IMSG_CTL_STOP,
+	GOTWEBD_IMSG_CTL_INFO,
 	GOTWEBD_IMSG_LOGIN_SECRET,
 	GOTWEBD_IMSG_AUTH_SECRET,
 	GOTWEBD_IMSG_AUTH_CONF,
@@ -170,6 +173,12 @@ struct imsgev {
 
 #define IMSG_DATA_SIZE(imsg)	((imsg)->hdr.len - IMSG_HEADER_SIZE)
 
+/* Structure for GOTWEBD_IMSG_CTL_INFO. */
+struct gotwebd_imsg_info {
+	pid_t pid;
+	int verbosity;
+};
+
 struct env_val {
 	SLIST_ENTRY(env_val)	 entry;
 	char			*val;
@@ -477,6 +486,8 @@ TAILQ_HEAD(socketlist, socket);
 
 struct passwd;
 struct gotwebd {
+	pid_t			pid;
+
 	struct serverlist	servers;
 	struct socketlist	sockets;
 	struct addresslist	addresses;
@@ -484,6 +495,10 @@ struct gotwebd {
 	struct socket	*login_sock;
 	struct event	 login_pause_ev;
 
+	struct socket	*control_sock;
+	struct event	 control_pause_ev;
+	struct imsgev	*iev_control;
+
 	enum gotwebd_auth_config auth_config;
 	struct gotwebd_access_rule_list access_rules;
 
commit - f5e64618f74a49a9167bec1f7db97000c2b8557f
blob - d7ad16d93f68bb3f742baac27bc72c4373600b62
file + gotwebd/parse.y
--- gotwebd/parse.y
+++ gotwebd/parse.y
@@ -156,7 +156,7 @@ mediatype_ok(const char *s)
 %token	SUMMARY_COMMITS_DISPLAY SUMMARY_TAGS_DISPLAY USER AUTHENTICATION
 %token	ENABLE DISABLE INSECURE REPOSITORY REPOSITORIES PERMIT DENY HIDE
 %token	WEBSITE PATH BRANCH REPOS_URL_PATH DESCRIPTION
-%token	TYPES INCLUDE
+%token	TYPES INCLUDE GOTWEBD_CONTROL
 
 %token	<v.string>	STRING
 %token	<v.number>	NUMBER
@@ -481,6 +481,20 @@ main		: PREFORK NUMBER {
 
 			free($2);
 		}
+		| GOTWEBD_CONTROL SOCKET STRING {
+			struct address *h;
+			h = get_unix_addr($3);
+			if (h == NULL) {
+				yyerror("can't listen on %s", $3);
+				free($3);
+				YYERROR;
+			}
+			if (gotwebd->control_sock != NULL)
+				free(gotwebd->control_sock);
+			gotwebd->control_sock = sockets_conf_new_socket(-1, h);
+			free(h);
+			free($3);
+		}
 		;
 
 server		: SERVER STRING {
@@ -1065,6 +1079,7 @@ lookup(char *s)
 		{ "authentication",		AUTHENTICATION },
 		{ "branch",			BRANCH },
 		{ "chroot",			CHROOT },
+		{ "control",			GOTWEBD_CONTROL },
 		{ "custom_css",			CUSTOM_CSS },
 		{ "deny",			DENY },
 		{ "description",		DESCRIPTION },
@@ -1635,6 +1650,19 @@ parse_config(const char *filename, struct gotwebd *env
 		free(h);
 	}
 
+	/* Add implicit control socket */
+	if (gotwebd->control_sock == NULL) {
+		struct address *h;
+		h = get_unix_addr(GOTWEBD_CONTROL_SOCKET);
+		if (h == NULL) {
+			fprintf(stderr, "cannot listen on %s",
+			    GOTWEBD_CONTROL_SOCKET);
+			return (-1);
+		}
+		gotwebd->control_sock = sockets_conf_new_socket(-1, h);
+		free(h);
+	}
+
 	/*
 	 * Disable authentication if not explicitly configured.
 	 * Authentication requires access rules to be configured, and we want