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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
add gotwebctl(8)
To:
gameoftrees@openbsd.org
Date:
Wed, 7 Jan 2026 18:15:46 +0100

Download raw body.

Thread
  • Stefan Sperling:

    add gotwebctl(8)

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?

M  Makefile                |    2+  0-
A  gotwebctl/Makefile      |   29+  0-
A  gotwebctl/gotwebctl.8   |   85+  0-
A  gotwebctl/gotwebctl.c   |  353+  0-
M  gotwebd/gotwebd.8       |   13+  0-
M  gotwebd/gotwebd.c       |  328+  4-
M  gotwebd/gotwebd.conf.5  |   12+  0-
M  gotwebd/gotwebd.h       |   15+  0-
M  gotwebd/parse.y         |   29+  1-

9 files changed, 866 insertions(+), 5 deletions(-)

commit - 006e69a720de8190c3a538975a8d974b815396e3
commit + 30e7353c83279cd3a678aba393e713c6246ca691
blob - 59785cb66e3647ab727137e1f1d919208c5848c9
blob + 806e02b9a62b07650e864eabd1e4660524156ebf
--- Makefile
+++ Makefile
@@ -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:
blob - 4c6c4cdca05841e58fab572e2bdc7fd2221e01eb
blob + caa187ef6c305a2ecebdb16e1b2653b8c8845694
--- 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
blob - f868bed8cfb869337fbd6d4150b9ca347187f066
blob + 958ce01dd4ca880c853680a912c0bcc44d23ab97
--- 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);
 }
blob - 5707f5abffcada5369e98d6a90bab60320b6f6b2
blob + e88a24f9cb2a566179b6dae86235703fb7abac58
--- 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
@@ -911,5 +922,6 @@ server "secure.example.com" {
 .Xr got 1 ,
 .Xr httpd.conf 5 ,
 .Xr services 5 ,
+.Xr gotwebctl 8 ,
 .Xr gotwebd 8 ,
 .Xr httpd 8
blob - 13d030bc095cf095c657d696ae716f9fa1dbc782
blob + b252c043f96d432ba1d0a205c7954f335de4a660
--- 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;
@@ -476,6 +485,8 @@ TAILQ_HEAD(socketlist, socket);
 
 struct passwd;
 struct gotwebd {
+	pid_t			pid;
+
 	struct serverlist	servers;
 	struct socketlist	sockets;
 	struct addresslist	addresses;
@@ -483,6 +494,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;
 
blob - 57beb717a572f78e1e1ac9452a9bcfd38e2bf1cd
blob + 82e68554a2de461c66dd974138bb84b3b9af2a6e
--- 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
-%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 {
@@ -1054,6 +1068,7 @@ lookup(char *s)
 		{ "authentication",		AUTHENTICATION },
 		{ "branch",			BRANCH },
 		{ "chroot",			CHROOT },
+		{ "control",			GOTWEBD_CONTROL },
 		{ "custom_css",			CUSTOM_CSS },
 		{ "deny",			DENY },
 		{ "disable",			DISABLE },
@@ -1623,6 +1638,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
blob - /dev/null
blob + 15a0de406274555cb7e51da7665f351fe2647aa2 (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>
blob - /dev/null
blob + e26d1cec340d8adcda4f29f49d81581bfd5e7e9e (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
blob - /dev/null
blob + 84fa0276c831ff68f1dbe581f5b22db1548b368b (mode 644)
--- /dev/null
+++ gotwebctl/gotwebctl.c
@@ -0,0 +1,353 @@
+/*
+ * 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
+
+#define GOTCTL_CMD_INFO "info"
+#define GOTCTL_CMD_STOP "stop"
+
+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;
+}