Commit Diff


commit - a0603cf4f59bf80a3c8d65c4fa38da371dba8c5b
commit + c902213d3f8bc01899af3801fc2521a702fc703c
blob - 89c442c3a1faa744027e0511d4fd421f9474ee20
blob + b970ee903b827d35f5b176c68200ec092a13d4e8
--- README
+++ README
@@ -66,12 +66,14 @@ To compile the Got server tool suite on OpenBSD, run:
 This will install the following commands:
 
  gotd, the repository server program
+ gotctl, the server control utility
  gotsh, the login shell for users accessing the server via the network
 
 See the following manual page files for information about server setup:
 
  $ man -l gotd/gotd.8
  $ man -l gotd/gotd.conf.5
+ $ man -l gotctl/gotctl.8
  $ man -l gotsh/gotsh.1
 
 
blob - 112ff42ae9054a24468d0844bcfb59ac93075d9c
blob + 3ac0c97028424e20588671698c7ed94711ddc881
--- gotd/gotd.c
+++ gotd/gotd.c
@@ -95,6 +95,7 @@ static int inflight;
 static struct gotd gotd;
 
 void gotd_sighdlr(int sig, short event, void *arg);
+static void gotd_shutdown(void);
 
 __dead static void
 usage()
@@ -264,10 +265,7 @@ get_client_proc(struct gotd_client *client)
 		return client->repo_read;
 	else if (client->repo_write)
 		return client->repo_write;
-	else {
-		fatal("uid %d is neither reading nor writing", client->euid);
-		/* NOTREACHED */
-	}
+
 	return NULL;
 }
 
@@ -341,11 +339,12 @@ disconnect(struct gotd_client *client)
 	log_debug("uid %d: disconnecting", client->euid);
 
 	idisconnect.client_id = client->id;
-	if (gotd_imsg_compose_event(&proc->iev,
-	    GOTD_IMSG_DISCONNECT, PROC_GOTD, -1,
-	    &idisconnect, sizeof(idisconnect)) == -1)
-		log_warn("imsg compose DISCONNECT");
-
+	if (proc) {
+		if (gotd_imsg_compose_event(&proc->iev,
+		    GOTD_IMSG_DISCONNECT, PROC_GOTD, -1,
+		    &idisconnect, sizeof(idisconnect)) == -1)
+			log_warn("imsg compose DISCONNECT");
+	}
 	slot = client_hash(client->id) % nitems(gotd_clients);
 	STAILQ_REMOVE(&gotd_clients[slot], client, gotd_client, entry);
 	imsg_clear(&client->iev.ibuf);
@@ -382,8 +381,169 @@ disconnect_on_error(struct gotd_client *client, const 
 		imsg_clear(&ibuf);
 	}
 	disconnect(client);
+}
+
+static const struct got_error *
+send_repo_info(struct gotd_imsgev *iev, struct gotd_repo *repo)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_info_repo irepo;
+
+	memset(&irepo, 0, sizeof(irepo));
+
+	if (strlcpy(irepo.repo_name, repo->name, sizeof(irepo.repo_name))
+	    >= sizeof(irepo.repo_name))
+		return got_error_msg(GOT_ERR_NO_SPACE, "repo name too long");
+	if (strlcpy(irepo.repo_path, repo->path, sizeof(irepo.repo_path))
+	    >= sizeof(irepo.repo_path))
+		return got_error_msg(GOT_ERR_NO_SPACE, "repo path too long");
+
+	if (gotd_imsg_compose_event(iev, GOTD_IMSG_INFO_REPO, PROC_GOTD, -1,
+	    &irepo, sizeof(irepo)) == -1) {
+		err = got_error_from_errno("imsg compose INFO_REPO");
+		if (err)
+			return err;
+	}
+
+	return NULL;
 }
 
+static const struct got_error *
+send_capability(struct gotd_client_capability *capa, struct gotd_imsgev* iev)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_capability icapa;
+	size_t len;
+	struct ibuf *wbuf;
+
+	memset(&icapa, 0, sizeof(icapa));
+
+	icapa.key_len = strlen(capa->key);
+	len = sizeof(icapa) + icapa.key_len;
+	if (capa->value) {
+		icapa.value_len = strlen(capa->value);
+		len += icapa.value_len;
+	}
+
+	wbuf = imsg_create(&iev->ibuf, GOTD_IMSG_CAPABILITY, 0, 0, len);
+	if (wbuf == NULL) {
+		err = got_error_from_errno("imsg_create CAPABILITY");
+		return err;
+	}
+
+	if (imsg_add(wbuf, &icapa, sizeof(icapa)) == -1)
+		return got_error_from_errno("imsg_add CAPABILITY");
+	if (imsg_add(wbuf, capa->key, icapa.key_len) == -1)
+		return got_error_from_errno("imsg_add CAPABILITY");
+	if (capa->value) {
+		if (imsg_add(wbuf, capa->value, icapa.value_len) == -1)
+			return got_error_from_errno("imsg_add CAPABILITY");
+	}
+
+	wbuf->fd = -1;
+	imsg_close(&iev->ibuf, wbuf);
+
+	gotd_imsg_event_add(iev);
+
+	return NULL;
+}
+
+static const struct got_error *
+send_client_info(struct gotd_imsgev *iev, struct gotd_client *client)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_info_client iclient;
+	struct gotd_child_proc *proc;
+	size_t i;
+
+	memset(&iclient, 0, sizeof(iclient));
+	iclient.euid = client->euid;
+	iclient.egid = client->egid;
+
+	proc = get_client_proc(client);
+	if (proc) {
+		if (strlcpy(iclient.repo_name, proc->chroot_path,
+		    sizeof(iclient.repo_name)) >= sizeof(iclient.repo_name)) {
+			return got_error_msg(GOT_ERR_NO_SPACE,
+			    "repo name too long");
+		}
+		if (client_is_writing(client))
+			iclient.is_writing = 1;
+	}
+
+	iclient.state = client->state;
+	iclient.ncapabilities = client->ncapabilities;
+
+	if (gotd_imsg_compose_event(iev, GOTD_IMSG_INFO_CLIENT, PROC_GOTD, -1,
+	    &iclient, sizeof(iclient)) == -1) {
+		err = got_error_from_errno("imsg compose INFO_CLIENT");
+		if (err)
+			return err;
+	}
+
+	for (i = 0; i < client->ncapabilities; i++) {
+		struct gotd_client_capability *capa;
+		capa = &client->capabilities[i];
+		err = send_capability(capa, iev);
+		if (err)
+			return err;
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+send_info(struct gotd_client *client)
+{
+	const struct got_error *err = NULL;
+	struct gotd_imsg_info info;
+	uint64_t slot;
+	struct gotd_repo *repo;
+
+	info.pid = gotd.pid;
+	info.verbosity = gotd.verbosity;
+	info.nrepos = gotd.nrepos;
+	info.nclients = client_cnt - 1;
+
+	if (gotd_imsg_compose_event(&client->iev, GOTD_IMSG_INFO, PROC_GOTD, -1,
+	    &info, sizeof(info)) == -1) {
+		err = got_error_from_errno("imsg compose INFO");
+		if (err)
+			return err;
+	}
+
+	TAILQ_FOREACH(repo, &gotd.repos, entry) {
+		err = send_repo_info(&client->iev, repo);
+		if (err)
+			return err;
+	}
+
+	for (slot = 0; slot < nitems(gotd_clients); slot++) {
+		struct gotd_client *c;
+		STAILQ_FOREACH(c, &gotd_clients[slot], entry) {
+			if (c->id == client->id)
+				continue;
+			err = send_client_info(&client->iev, c);
+			if (err)
+				return err;
+		}
+	}
+
+	return NULL;
+}
+
+static const struct got_error *
+stop_gotd(struct gotd_client *client)
+{
+
+	if (client->euid != 0)
+		return got_error_set_errno(EPERM, "stop");
+
+	gotd_shutdown();
+	/* NOTREACHED */
+	return NULL;
+}
+
 static struct gotd_child_proc *
 find_proc_by_repo_name(enum gotd_procid proc_id, const char *repo_name)
 {
@@ -465,6 +625,8 @@ forward_list_refs_request(struct gotd_client *client, 
 		return got_error_from_errno("dup");
 
 	proc = get_client_proc(client);
+	if (proc == NULL)
+		fatalx("no process found for uid %d", client->euid);
 	if (gotd_imsg_compose_event(&proc->iev,
 	    GOTD_IMSG_LIST_REFS_INTERNAL, PROC_GOTD, fd,
 	    &ilref, sizeof(ilref)) == -1) {
@@ -837,6 +999,12 @@ gotd_request(int fd, short events, void *arg)
 				return;
 			}
 		}
+
+		/* Disconnect gotctl(8) now that messages have been sent. */
+		if (!client_is_reading(client) && !client_is_writing(client)) {
+			disconnect(client);
+			return;
+		}
 	}
 
 	if ((events & EV_READ) == 0)
@@ -855,6 +1023,12 @@ gotd_request(int fd, short events, void *arg)
 		evtimer_del(&client->tmo);
 
 		switch (imsg.hdr.type) {
+		case GOTD_IMSG_INFO:
+			err = send_info(client);
+			break;
+		case GOTD_IMSG_STOP:
+			err = stop_gotd(client);
+			break;
 		case GOTD_IMSG_LIST_REFS:
 			if (client->state != GOTD_STATE_EXPECT_LIST_REFS) {
 				err = got_error_msg(GOT_ERR_BAD_REQUEST,
@@ -1245,6 +1419,9 @@ verify_imsg_src(struct gotd_client *client, struct got
 	int ret = 0;
 
 	client_proc = get_client_proc(client);
+	if (client_proc == NULL)
+		fatalx("no process found for uid %d", client->euid);
+
 	if (proc->pid != client_proc->pid) {
 		kill_proc(proc, 1);
 		log_warnx("received message from PID %d for uid %d, while "
blob - 181834ce44e984caf7169755b86ad50d182325fd
blob + 6654c7116a6483e7457c581646809a9e5b09c6ae
--- gotd/gotd.h
+++ gotd/gotd.h
@@ -103,6 +103,12 @@ enum gotd_imsg_type {
 	/* An error occured while processing a request. */
 	GOTD_IMSG_ERROR,
 
+	/* Commands used by gotctl(8). */
+	GOTD_IMSG_INFO,
+	GOTD_IMSG_INFO_REPO,
+	GOTD_IMSG_INFO_CLIENT,
+	GOTD_IMSG_STOP,
+
 	/* Request a list of references. */
 	GOTD_IMSG_LIST_REFS,
 	GOTD_IMSG_LIST_REFS_INTERNAL,
@@ -155,6 +161,35 @@ struct gotd_imsg_error {
 	char msg[GOT_ERR_MAX_MSG_SIZE];
 } __attribute__((__packed__));
 
+/* Structure for GOTD_IMSG_INFO. */
+struct gotd_imsg_info {
+	pid_t pid;
+	int verbosity;
+	int nrepos;
+	int nclients;
+
+	/* Followed by nrepos GOTD_IMSG_INFO_REPO messages. */
+	/* Followed by nclients GOTD_IMSG_INFO_CLIENT messages. */
+};
+
+/* Structure for GOTD_IMSG_INFO_REPO. */
+struct gotd_imsg_info_repo {
+	char repo_name[NAME_MAX];
+	char repo_path[PATH_MAX];
+};
+
+/* Structure for GOTD_IMSG_INFO_CLIENT */
+struct gotd_imsg_info_client {
+	uid_t euid;
+	gid_t egid;
+	char repo_name[NAME_MAX];
+	int is_writing;
+	enum gotd_client_state state;
+	size_t ncapabilities;
+
+	/* Followed by ncapabilities GOTD_IMSG_CAPABILITY. */
+};
+
 /* Structure for GOTD_IMSG_LIST_REFS. */
 struct gotd_imsg_list_refs {
 	char repo_name[NAME_MAX];
blob - /dev/null
blob + de99e60620ea466b3e56f258e643d00a974595d6 (mode 644)
--- /dev/null
+++ gotctl/Makefile
@@ -0,0 +1,29 @@
+.PATH:${.CURDIR}/../lib ${.CURDIR}/../gotd
+
+.include "../got-version.mk"
+
+PROG=		gotctl
+SRCS=		gotctl.c error.c imsg.c pollfd.c sha1.c
+
+MAN =		${PROG}.8
+
+CPPFLAGS = -I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}/../gotd
+
+.if defined(PROFILE)
+LDADD = -lutil_p -lz_p -lm_p -lc_p -levent_p
+.else
+LDADD = -lutil -lz -lm -levent
+.endif
+DPADD = ${LIBZ} ${LIBUTIL}
+
+.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 + 1f836aa9f48869909e61c1d1c6ca6c9e8677b4f2 (mode 644)
--- /dev/null
+++ gotctl/gotctl.8
@@ -0,0 +1,71 @@
+.\"
+.\" Copyright (c) 2022 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 GOTCTL 8
+.Os
+.Sh NAME
+.Nm gotctl
+.Nd control the Game of Trees Daemon
+.Sh SYNOPSIS
+.Nm
+.Op Fl hV
+.Op Fl f Ar path
+.Ar command
+.Op Ar arg ...
+.Sh DESCRIPTION
+.Nm
+controls the
+.Xr gotd 8
+daemon.
+.Pp
+The options for
+.Nm
+are as follows:
+.Bl -tag -width Ds
+.Bl -tag -width tenletters
+.It Fl h
+Display usage information and exit immediately.
+.It Fl f Ar path
+Set the
+.Ar path
+to the unix socket which
+.Xr gotd 8
+is listening on.
+If not specified, the default path
+.Pa /var/run/gotd.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 checkout
+.It Cm info
+Display information about a running
+.Xr gotd 8
+instance.
+.It Cm stop
+Stop a running
+.Xr gotd 8
+instance.
+This operation requires root privileges.
+.Sh SEE ALSO
+.Xr got 1 ,
+.Xr gotd 8
+.Sh AUTHORS
+.An Stefan Sperling Aq Mt stsp@openbsd.org
blob - /dev/null
blob + 821e75bd339a40b002fc603fcf67ccd42e2a0578 (mode 644)
--- /dev/null
+++ gotctl/gotctl.c
@@ -0,0 +1,461 @@
+/*
+ * Copyright (c) 2022 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/socket.h>
+#include <sys/un.h>
+
+#include <err.h>
+#include <event.h>
+#include <imsg.h>
+#include <limits.h>
+#include <locale.h>
+#include <sha1.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <getopt.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "got_version.h"
+
+#include "got_lib_gitproto.h"
+
+#include "gotd.h"
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+#define GOTCTL_CMD_INFO "info"
+#define GOTCTL_CMD_STOP "stop"
+
+struct gotctl_cmd {
+	const char	*cmd_name;
+	const struct got_error *(*cmd_main)(int, char *[], int);
+	void		(*cmd_usage)(void);
+};
+
+__dead static void	usage(int, int);
+
+__dead static void	usage_info(void);
+__dead static void	usage_stop(void);
+
+static const struct got_error*		cmd_info(int, char *[], int);
+static const struct got_error*		cmd_stop(int, char *[], int);
+
+static const struct gotctl_cmd gotctl_commands[] = {
+	{ "info",	cmd_info,	usage_info },
+	{ "stop",	cmd_stop,	usage_stop },
+};
+
+__dead static void
+usage_info(void)
+{
+	fprintf(stderr, "usage: %s info\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+show_info(struct imsg *imsg)
+{
+	struct gotd_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("gotd PID: %d\n", info.pid);
+	printf("verbosity: %d\n", info.verbosity);
+	printf("number of repositories: %d\n", info.nrepos);
+	printf("number of connected clients: %d\n", info.nclients);
+	return NULL;
+}
+
+static const struct got_error *
+show_repo_info(struct imsg *imsg)
+{
+	struct gotd_imsg_info_repo 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("repository \"%s\", path %s\n", info.repo_name, info.repo_path);
+	return NULL;
+}
+
+static const char *
+get_state_name(enum gotd_client_state state)
+{
+	static char unknown_state[64];
+
+	switch (state) {
+	case GOTD_STATE_EXPECT_LIST_REFS:
+		return "list-refs";
+	case GOTD_STATE_EXPECT_CAPABILITIES:
+		return "expect-capabilities";
+	case GOTD_STATE_EXPECT_WANT:
+		return "expect-want";
+	case GOTD_STATE_EXPECT_REF_UPDATE:
+		return "expect-ref-update";
+	case GOTD_STATE_EXPECT_MORE_REF_UPDATES:
+		return "expect-more-ref-updates";
+	case GOTD_STATE_EXPECT_HAVE:
+		return "expect-have";
+	case GOTD_STATE_EXPECT_PACKFILE:
+		return "expect-packfile";
+	case GOTD_STATE_EXPECT_DONE:
+		return "expect-done";
+	case GOTD_STATE_DONE:
+		return "done";
+	}
+
+	snprintf(unknown_state, sizeof(unknown_state),
+	    "unknown state %d", state);
+	return unknown_state;
+}
+
+static const struct got_error *
+show_client_info(struct imsg *imsg)
+{
+	struct gotd_imsg_info_client 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("client UID %d, GID %d, protocol state '%s', ",
+	    info.euid, info.egid, get_state_name(info.state));
+	if (info.is_writing)
+		printf("writing to %s\n", info.repo_name);
+	else
+		printf("reading from %s\n", info.repo_name);
+
+	return NULL;
+}
+
+static const struct got_error *
+show_capability(struct imsg *imsg)
+{
+	struct gotd_imsg_capability icapa;
+	size_t datalen;
+	char *key, *value = NULL;
+
+	memset(&icapa, 0, sizeof(icapa));
+
+	datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	if (datalen < sizeof(icapa))
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+	memcpy(&icapa, imsg->data, sizeof(icapa));
+
+	if (datalen != sizeof(icapa) + icapa.key_len + icapa.value_len)
+		return got_error(GOT_ERR_PRIVSEP_LEN);
+
+	key = malloc(icapa.key_len + 1);
+	if (key == NULL)
+		return got_error_from_errno("malloc");
+	if (icapa.value_len > 0) {
+		value = malloc(icapa.value_len + 1);
+		if (value == NULL) {
+			free(key);
+			return got_error_from_errno("malloc");
+		}
+	}
+
+	memcpy(key, imsg->data + sizeof(icapa), icapa.key_len);
+	key[icapa.key_len] = '\0';
+	if (value) {
+		memcpy(value, imsg->data + sizeof(icapa) + icapa.key_len,
+		    icapa.value_len);
+		value[icapa.value_len] = '\0';
+	}
+
+	if (strcmp(key, GOT_CAPA_AGENT) == 0)
+		printf("  client user agent: %s\n", value);
+	else if (value)
+		printf("  client supports %s=%s\n", key, value);
+	else
+		printf("  client supports %s\n", key);
+
+	free(key);
+	free(value);
+	return NULL;
+}
+
+static const struct got_error *
+cmd_info(int argc, char *argv[], int gotd_sock)
+{
+	const struct got_error *err;
+	struct imsgbuf ibuf;
+	struct imsg imsg;
+
+	imsg_init(&ibuf, gotd_sock);
+
+	if (imsg_compose(&ibuf, GOTD_IMSG_INFO, 0, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("imsg_compose INFO");
+
+	err = gotd_imsg_flush(&ibuf);
+	while (err == NULL) {
+		err = gotd_imsg_poll_recv(&imsg, &ibuf, 0);
+		if (err) {
+			if (err->code == GOT_ERR_EOF)
+				err = NULL;
+			break;
+		}
+		
+		switch (imsg.hdr.type) {
+		case GOTD_IMSG_ERROR:
+			err = gotd_imsg_recv_error(NULL, &imsg);
+			break;
+		case GOTD_IMSG_INFO:
+			err = show_info(&imsg);
+			break;
+		case GOTD_IMSG_INFO_REPO:
+			err = show_repo_info(&imsg);
+			break;
+		case GOTD_IMSG_INFO_CLIENT:
+			err = show_client_info(&imsg);
+			break;
+		case GOTD_IMSG_CAPABILITY:
+			err = show_capability(&imsg);
+			break;
+		default:
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	imsg_clear(&ibuf);
+	return err;
+}
+
+__dead static void
+usage_stop(void)
+{
+	fprintf(stderr, "usage: %s stop\n", getprogname());
+	exit(1);
+}
+
+static const struct got_error *
+cmd_stop(int argc, char *argv[], int gotd_sock)
+{
+	const struct got_error *err;
+	struct imsgbuf ibuf;
+	struct imsg imsg;
+
+	imsg_init(&ibuf, gotd_sock);
+
+	if (imsg_compose(&ibuf, GOTD_IMSG_STOP, 0, 0, -1, NULL, 0) == -1)
+		return got_error_from_errno("imsg_compose STOP");
+
+	err = gotd_imsg_flush(&ibuf);
+	while (err == NULL) {
+		err = gotd_imsg_poll_recv(&imsg, &ibuf, 0);
+		if (err) {
+			if (err->code == GOT_ERR_EOF)
+				err = NULL;
+			break;
+		}
+		
+		switch (imsg.hdr.type) {
+		case GOTD_IMSG_ERROR:
+			err = gotd_imsg_recv_error(NULL, &imsg);
+			break;
+		default:
+			err = got_error(GOT_ERR_PRIVSEP_MSG);
+			break;
+		}
+
+		imsg_free(&imsg);
+	}
+
+	imsg_clear(&ibuf);
+	return err;
+}
+
+static void
+list_commands(FILE *fp)
+{
+	size_t i;
+
+	fprintf(fp, "commands:");
+	for (i = 0; i < nitems(gotctl_commands); i++) {
+		const struct gotctl_cmd *cmd = &gotctl_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] command [arg ...]\n",
+	    getprogname());
+	if (hflag)
+		list_commands(fp);
+	exit(status);
+}
+
+static const struct got_error *
+apply_unveil(const char *unix_socket_path)
+{
+#ifdef PROFILE
+	if (unveil("gmon.out", "rwc") != 0)
+		return got_error_from_errno2("unveil", "gmon.out");
+#endif
+	if (unveil(unix_socket_path, "w") != 0)
+		return got_error_from_errno2("unveil", unix_socket_path);
+
+	if (unveil(NULL, NULL) != 0)
+		return got_error_from_errno("unveil");
+
+	return NULL;
+}
+
+static int
+connect_gotd(const char *socket_path)
+{
+	const struct got_error *error = NULL;
+	char unix_socket_path[PATH_MAX];
+	int gotd_sock = -1;
+	struct sockaddr_un sun;
+
+	if (socket_path) {
+		if (strlcpy(unix_socket_path, socket_path,
+		    sizeof(unix_socket_path)) >= sizeof(unix_socket_path)) 
+			errx(1, "gotd socket path too long");
+	} else {
+		strlcpy(unix_socket_path, GOTD_UNIX_SOCKET,
+		    sizeof(unix_socket_path));
+	}
+
+	error = apply_unveil(unix_socket_path);
+	if (error)
+		errx(1, "%s", error->msg);
+
+#ifndef PROFILE
+	if (pledge("stdio unix", NULL) == -1)
+		err(1, "pledge");
+#endif
+	if ((gotd_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, unix_socket_path,
+	    sizeof(sun.sun_path)) >= sizeof(sun.sun_path))
+		errx(1, "gotd socket path too long");
+	if (connect(gotd_sock, (struct sockaddr *)&sun, sizeof(sun)) == -1)
+		err(1, "connect: %s", unix_socket_path);
+
+#ifndef PROFILE
+	if (pledge("stdio", NULL) == -1)
+		err(1, "pledge");
+#endif
+
+	return gotd_sock;
+}
+
+int
+main(int argc, char *argv[])
+{
+	const struct gotctl_cmd *cmd;
+	int gotd_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 = NULL;
+
+	setlocale(LC_CTYPE, "");
+
+#ifndef PROFILE
+	if (pledge("stdio 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(gotctl_commands); i++) {
+		const struct got_error *error;
+
+		cmd = &gotctl_commands[i];
+
+		if (strncmp(cmd->cmd_name, argv[0], strlen(argv[0])) != 0)
+			continue;
+
+		if (hflag)
+			cmd->cmd_usage();
+
+		gotd_sock = connect_gotd(socket_path);
+		if (gotd_sock == -1)
+			return 1;
+		error = cmd->cmd_main(argc, argv, gotd_sock);
+		close(gotd_sock);
+		if (error) {
+			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;
+}