# HG changeset patch # User Yuya Nishihara # Date 2016-01-03 03:39:27 # Node ID 726f8d6cc324ca7f8929c640e140cb6784b8ece1 # Parent 740208f6f6af21329568acd287563bc9d437b253 chg: import frontend sources These files are copied from https://bitbucket.org/yuja/chg/ -r f897faa79687 diff --git a/contrib/chg/Makefile b/contrib/chg/Makefile new file mode 100644 --- /dev/null +++ b/contrib/chg/Makefile @@ -0,0 +1,50 @@ +HG = hg + +TARGET = chg +SRCS = chg.c hgclient.c util.c +OBJS = $(SRCS:.c=.o) + +CFLAGS ?= -O2 -Wall -Wextra -pedantic -g +CPPFLAGS ?= -D_FORTIFY_SOURCE=2 +override CFLAGS += -std=gnu99 + +DESTDIR = +PREFIX = /usr/local +MANDIR = $(PREFIX)/share/man/man1 + +CHGSOCKDIR = /tmp/chg$(shell id -u) +CHGSOCKNAME = $(CHGSOCKDIR)/server + +.PHONY: all +all: $(TARGET) + +$(TARGET): $(OBJS) + $(CC) $(LDFLAGS) -o $@ $(OBJS) + +chg.o: hgclient.h util.h +hgclient.o: hgclient.h util.h +util.o: util.h + +.PHONY: install +install: $(TARGET) + install -d $(DESTDIR)$(PREFIX)/bin + install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin + install -d $(DESTDIR)$(MANDIR) + install -m 644 chg.1 $(DESTDIR)$(MANDIR) + +.PHONY: serve +serve: + [ -d $(CHGSOCKDIR) ] || ( umask 077; mkdir $(CHGSOCKDIR) ) + $(HG) serve --cwd / --cmdserver chgunix \ + --address $(CHGSOCKNAME) \ + --config extensions.chgserver= \ + --config progress.assume-tty=1 \ + --config cmdserver.log=/dev/stderr + +.PHONY: clean +clean: + $(RM) $(OBJS) + +.PHONY: distclean +distclean: + $(RM) $(OBJS) $(TARGET) diff --git a/contrib/chg/README b/contrib/chg/README new file mode 100644 --- /dev/null +++ b/contrib/chg/README @@ -0,0 +1,32 @@ +cHg +=== + +A fast client for Mercurial command server running on Unix. + +Install: + + $ make + $ make install + +Usage: + + $ chg help # show help of Mercurial + $ alias hg=chg # replace hg command + $ chg --kill-chg-daemon # terminate background server + $ chg --reload-chg-daemon # reload configuration files + +Environment variables: + +Although cHg tries to update environment variables, some of them cannot be +changed after spawning the server. The following variables are specially +handled: + + * configuration files are reloaded if HGPLAIN or HGPLAINEXCEPT changed, but + some behaviors won't change correctly. + * CHGHG or HG specifies the path to the hg executable spawned as the + background command server. + +The following variables are available for testing: + + * CHGDEBUG enables debug messages. + * CHGSOCKNAME specifies the socket path of the background cmdserver. diff --git a/contrib/chg/chg.1 b/contrib/chg/chg.1 new file mode 100644 --- /dev/null +++ b/contrib/chg/chg.1 @@ -0,0 +1,44 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.TH CHG 1 "March 3, 2013" +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp insert n+1 empty lines +.\" for manpage-specific macros, see man(7) +.SH NAME +chg \- a fast client for Mercurial command server +.SH SYNOPSIS +.B chg +.IR command " [" options "] [" arguments "]..." +.br +.SH DESCRIPTION +The +.B chg +command is the wrapper for +.B hg +command. +It uses the Mercurial command server to reduce start-up overhead. +.SH OPTIONS +This program accepts the same command line syntax as the +.B hg +command. Additionally it accepts the following options. +.TP +.B \-\-kill\-chg\-daemon +Terminate the background command servers. +.TP +.B \-\-reload\-chg\-daemon +Reload configuration files. +.SH SEE ALSO +.BR hg (1), +.SH AUTHOR +Written by Yuya Nishihara . diff --git a/contrib/chg/chg.c b/contrib/chg/chg.c new file mode 100644 --- /dev/null +++ b/contrib/chg/chg.c @@ -0,0 +1,331 @@ +/* + * A fast client for Mercurial command server + * + * Copyright (c) 2011 Yuya Nishihara + * + * This software may be used and distributed according to the terms of the + * GNU General Public License version 2 or any later version. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hgclient.h" +#include "util.h" + +#ifndef UNIX_PATH_MAX +#define UNIX_PATH_MAX (sizeof(((struct sockaddr_un *)NULL)->sun_path)) +#endif + +struct cmdserveropts { + char sockname[UNIX_PATH_MAX]; + char lockfile[UNIX_PATH_MAX]; + char pidfile[UNIX_PATH_MAX]; +}; + +static void preparesockdir(const char *sockdir) +{ + int r; + r = mkdir(sockdir, 0700); + if (r < 0 && errno != EEXIST) + abortmsg("cannot create sockdir %s (errno = %d)", + sockdir, errno); + + struct stat st; + r = lstat(sockdir, &st); + if (r < 0) + abortmsg("cannot stat %s (errno = %d)", sockdir, errno); + if (!S_ISDIR(st.st_mode)) + abortmsg("cannot create sockdir %s (file exists)", sockdir); + if (st.st_uid != geteuid() || st.st_mode & 0077) + abortmsg("insecure sockdir %s", sockdir); +} + +static void setcmdserveropts(struct cmdserveropts *opts) +{ + int r; + char sockdir[UNIX_PATH_MAX]; + const char *envsockname = getenv("CHGSOCKNAME"); + if (!envsockname) { + /* by default, put socket file in secure directory + * (permission of socket file may be ignored on some Unices) */ + const char *tmpdir = getenv("TMPDIR"); + if (!tmpdir) + tmpdir = "/tmp"; + r = snprintf(sockdir, sizeof(sockdir), "%s/chg%d", + tmpdir, geteuid()); + if (r < 0 || (size_t)r >= sizeof(sockdir)) + abortmsg("too long TMPDIR (r = %d)", r); + preparesockdir(sockdir); + } + + const char *basename = (envsockname) ? envsockname : sockdir; + const char *sockfmt = (envsockname) ? "%s" : "%s/server"; + const char *lockfmt = (envsockname) ? "%s.lock" : "%s/lock"; + const char *pidfmt = (envsockname) ? "%s.pid" : "%s/pid"; + r = snprintf(opts->sockname, sizeof(opts->sockname), sockfmt, basename); + if (r < 0 || (size_t)r >= sizeof(opts->sockname)) + abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r); + r = snprintf(opts->lockfile, sizeof(opts->lockfile), lockfmt, basename); + if (r < 0 || (size_t)r >= sizeof(opts->lockfile)) + abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r); + r = snprintf(opts->pidfile, sizeof(opts->pidfile), pidfmt, basename); + if (r < 0 || (size_t)r >= sizeof(opts->pidfile)) + abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r); +} + +/* + * Make lock file that indicates cmdserver process is about to start. Created + * lock file will be deleted by server. (0: success, -1: lock exists) + */ +static int lockcmdserver(const struct cmdserveropts *opts) +{ + int r; + char info[32]; + r = snprintf(info, sizeof(info), "%d", getpid()); + if (r < 0 || (size_t)r >= sizeof(info)) + abortmsg("failed to format lock info"); + r = symlink(info, opts->lockfile); + if (r < 0 && errno != EEXIST) + abortmsg("failed to make lock %s (errno = %d)", + opts->lockfile, errno); + return r; +} + +static void execcmdserver(const struct cmdserveropts *opts) +{ + const char *hgcmd = getenv("CHGHG"); + if (!hgcmd || hgcmd[0] == '\0') + hgcmd = getenv("HG"); + if (!hgcmd || hgcmd[0] == '\0') + hgcmd = "hg"; + + const char *argv[] = { + hgcmd, + "serve", + "--cwd", "/", + "--cmdserver", "chgunix", + "--address", opts->sockname, + "--daemon-pipefds", opts->lockfile, + "--pid-file", opts->pidfile, + "--config", "extensions.chgserver=", + /* wrap root ui so that it can be disabled/enabled by config */ + "--config", "progress.assume-tty=1", + NULL, + }; + if (execvp(hgcmd, (char **)argv) < 0) + abortmsg("failed to exec cmdserver (errno = %d)", errno); +} + +/* + * Sleep until lock file is deleted, i.e. cmdserver process starts listening. + * If pid is given, it also checks if the child process fails to start. + */ +static void waitcmdserver(const struct cmdserveropts *opts, pid_t pid) +{ + static const struct timespec sleepreq = {0, 10 * 1000000}; + int pst = 0; + + for (unsigned int i = 0; i < 10 * 100; i++) { + int r; + struct stat lst; + + r = lstat(opts->lockfile, &lst); + if (r < 0 && errno == ENOENT) + return; /* lock file deleted by server */ + if (r < 0) + goto cleanup; + + if (pid > 0) { + /* collect zombie if child process fails to start */ + r = waitpid(pid, &pst, WNOHANG); + if (r != 0) + goto cleanup; + } + + nanosleep(&sleepreq, NULL); + } + + abortmsg("timed out waiting for cmdserver %s", opts->lockfile); + return; + +cleanup: + if (pid > 0) + /* lockfile should be made by this process */ + unlink(opts->lockfile); + if (WIFEXITED(pst)) { + abortmsg("cmdserver exited with status %d", WEXITSTATUS(pst)); + } else if (WIFSIGNALED(pst)) { + abortmsg("cmdserver killed by signal %d", WTERMSIG(pst)); + } else { + abortmsg("error white waiting cmdserver"); + } +} + +/* Spawn new background cmdserver */ +static void startcmdserver(const struct cmdserveropts *opts) +{ + debugmsg("start cmdserver at %s", opts->sockname); + + if (lockcmdserver(opts) < 0) { + debugmsg("lock file exists, waiting..."); + waitcmdserver(opts, 0); + return; + } + + /* remove dead cmdserver socket if any */ + unlink(opts->sockname); + + pid_t pid = fork(); + if (pid < 0) + abortmsg("failed to fork cmdserver process"); + if (pid == 0) { + /* bypass uisetup() of pager extension */ + int nullfd = open("/dev/null", O_WRONLY); + if (nullfd >= 0) { + dup2(nullfd, fileno(stdout)); + close(nullfd); + } + execcmdserver(opts); + } else { + waitcmdserver(opts, pid); + } +} + +static void killcmdserver(const struct cmdserveropts *opts, int sig) +{ + FILE *fp = fopen(opts->pidfile, "r"); + if (!fp) + abortmsg("cannot open %s (errno = %d)", opts->pidfile, errno); + int pid = 0; + int n = fscanf(fp, "%d", &pid); + fclose(fp); + if (n != 1 || pid <= 0) + abortmsg("cannot read pid from %s", opts->pidfile); + + if (kill((pid_t)pid, sig) < 0) { + if (errno == ESRCH) + return; + abortmsg("cannot kill %d (errno = %d)", pid, errno); + } +} + +static pid_t peerpid = 0; + +static void forwardsignal(int sig) +{ + assert(peerpid > 0); + if (kill(peerpid, sig) < 0) + abortmsg("cannot kill %d (errno = %d)", peerpid, errno); + debugmsg("forward signal %d", sig); +} + +static void setupsignalhandler(pid_t pid) +{ + if (pid <= 0) + return; + peerpid = pid; + + struct sigaction sa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = forwardsignal; + sa.sa_flags = SA_RESTART; + + sigaction(SIGHUP, &sa, NULL); + sigaction(SIGINT, &sa, NULL); + + /* terminate frontend by double SIGTERM in case of server freeze */ + sa.sa_flags |= SA_RESETHAND; + sigaction(SIGTERM, &sa, NULL); +} + +/* This implementation is based on hgext/pager.py (pre 369741ef7253) */ +static void setuppager(hgclient_t *hgc, const char *const args[], + size_t argsize) +{ + const char *pagercmd = hgc_getpager(hgc, args, argsize); + if (!pagercmd) + return; + + int pipefds[2]; + if (pipe(pipefds) < 0) + return; + pid_t pid = fork(); + if (pid < 0) + goto error; + if (pid == 0) { + close(pipefds[0]); + if (dup2(pipefds[1], fileno(stdout)) < 0) + goto error; + if (isatty(fileno(stderr))) { + if (dup2(pipefds[1], fileno(stderr)) < 0) + goto error; + } + close(pipefds[1]); + hgc_attachio(hgc); /* reattach to pager */ + return; + } else { + dup2(pipefds[0], fileno(stdin)); + close(pipefds[0]); + close(pipefds[1]); + + int r = execlp("/bin/sh", "/bin/sh", "-c", pagercmd, NULL); + if (r < 0) { + abortmsg("cannot start pager '%s' (errno = %d)", + pagercmd, errno); + } + return; + } + +error: + close(pipefds[0]); + close(pipefds[1]); + abortmsg("failed to prepare pager (errno = %d)", errno); +} + +int main(int argc, const char *argv[], const char *envp[]) +{ + if (getenv("CHGDEBUG")) + enabledebugmsg(); + + struct cmdserveropts opts; + setcmdserveropts(&opts); + + if (argc == 2) { + int sig = 0; + if (strcmp(argv[1], "--kill-chg-daemon") == 0) + sig = SIGTERM; + if (strcmp(argv[1], "--reload-chg-daemon") == 0) + sig = SIGHUP; + if (sig > 0) { + killcmdserver(&opts, sig); + return 0; + } + } + + hgclient_t *hgc = hgc_open(opts.sockname); + if (!hgc) { + startcmdserver(&opts); + hgc = hgc_open(opts.sockname); + } + if (!hgc) + abortmsg("cannot open hg client"); + + setupsignalhandler(hgc_peerpid(hgc)); + hgc_setenv(hgc, envp); + setuppager(hgc, argv + 1, argc - 1); + int exitcode = hgc_runcommand(hgc, argv + 1, argc - 1); + hgc_close(hgc); + return exitcode; +} diff --git a/contrib/chg/hgclient.c b/contrib/chg/hgclient.c new file mode 100644 --- /dev/null +++ b/contrib/chg/hgclient.c @@ -0,0 +1,527 @@ +/* + * A command server client that uses Unix domain socket + * + * Copyright (c) 2011 Yuya Nishihara + * + * This software may be used and distributed according to the terms of the + * GNU General Public License version 2 or any later version. + */ + +#include /* for ntohl(), htonl() */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "hgclient.h" +#include "util.h" + +enum { + CAP_GETENCODING = 0x0001, + CAP_RUNCOMMAND = 0x0002, + /* cHg extension: */ + CAP_ATTACHIO = 0x0100, + CAP_CHDIR = 0x0200, + CAP_GETPAGER = 0x0400, + CAP_SETENV = 0x0800, +}; + +typedef struct { + const char *name; + unsigned int flag; +} cappair_t; + +static const cappair_t captable[] = { + {"getencoding", CAP_GETENCODING}, + {"runcommand", CAP_RUNCOMMAND}, + {"attachio", CAP_ATTACHIO}, + {"chdir", CAP_CHDIR}, + {"getpager", CAP_GETPAGER}, + {"setenv", CAP_SETENV}, + {NULL, 0}, /* terminator */ +}; + +typedef struct { + char ch; + char *data; + size_t maxdatasize; + size_t datasize; +} context_t; + +struct hgclient_tag_ { + int sockfd; + pid_t pid; + context_t ctx; + unsigned int capflags; +}; + +static const size_t defaultdatasize = 4096; + +static void initcontext(context_t *ctx) +{ + ctx->ch = '\0'; + ctx->data = malloc(defaultdatasize); + ctx->maxdatasize = (ctx->data) ? defaultdatasize : 0; + ctx->datasize = 0; + debugmsg("initialize context buffer with size %zu", ctx->maxdatasize); +} + +static void enlargecontext(context_t *ctx, size_t newsize) +{ + if (newsize <= ctx->maxdatasize) + return; + + newsize = defaultdatasize + * ((newsize + defaultdatasize - 1) / defaultdatasize); + char *p = realloc(ctx->data, newsize); + if (!p) + abortmsg("failed to allocate buffer"); + ctx->data = p; + ctx->maxdatasize = newsize; + debugmsg("enlarge context buffer to %zu", ctx->maxdatasize); +} + +static void freecontext(context_t *ctx) +{ + debugmsg("free context buffer"); + free(ctx->data); + ctx->data = NULL; + ctx->maxdatasize = 0; + ctx->datasize = 0; +} + +/* Read channeled response from cmdserver */ +static void readchannel(hgclient_t *hgc) +{ + assert(hgc); + + ssize_t rsize = recv(hgc->sockfd, &hgc->ctx.ch, sizeof(hgc->ctx.ch), 0); + if (rsize != sizeof(hgc->ctx.ch)) + abortmsg("failed to read channel"); + + uint32_t datasize_n; + rsize = recv(hgc->sockfd, &datasize_n, sizeof(datasize_n), 0); + if (rsize != sizeof(datasize_n)) + abortmsg("failed to read data size"); + + /* datasize denotes the maximum size to write if input request */ + hgc->ctx.datasize = ntohl(datasize_n); + enlargecontext(&hgc->ctx, hgc->ctx.datasize); + + if (isupper(hgc->ctx.ch) && hgc->ctx.ch != 'S') + return; /* assumes input request */ + + size_t cursize = 0; + while (cursize < hgc->ctx.datasize) { + rsize = recv(hgc->sockfd, hgc->ctx.data + cursize, + hgc->ctx.datasize - cursize, 0); + if (rsize < 0) + abortmsg("failed to read data block"); + cursize += rsize; + } +} + +static void sendall(int sockfd, const void *data, size_t datasize) +{ + const char *p = data; + const char *const endp = p + datasize; + while (p < endp) { + ssize_t r = send(sockfd, p, endp - p, 0); + if (r < 0) + abortmsg("cannot communicate (errno = %d)", errno); + p += r; + } +} + +/* Write lengh-data block to cmdserver */ +static void writeblock(const hgclient_t *hgc) +{ + assert(hgc); + + const uint32_t datasize_n = htonl(hgc->ctx.datasize); + sendall(hgc->sockfd, &datasize_n, sizeof(datasize_n)); + + sendall(hgc->sockfd, hgc->ctx.data, hgc->ctx.datasize); +} + +static void writeblockrequest(const hgclient_t *hgc, const char *chcmd) +{ + debugmsg("request %s, block size %zu", chcmd, hgc->ctx.datasize); + + char buf[strlen(chcmd) + 1]; + memcpy(buf, chcmd, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\n'; + sendall(hgc->sockfd, buf, sizeof(buf)); + + writeblock(hgc); +} + +/* Build '\0'-separated list of args. argsize < 0 denotes that args are + * terminated by NULL. */ +static void packcmdargs(context_t *ctx, const char *const args[], + ssize_t argsize) +{ + ctx->datasize = 0; + const char *const *const end = (argsize >= 0) ? args + argsize : NULL; + for (const char *const *it = args; it != end && *it; ++it) { + const size_t n = strlen(*it) + 1; /* include '\0' */ + enlargecontext(ctx, ctx->datasize + n); + memcpy(ctx->data + ctx->datasize, *it, n); + ctx->datasize += n; + } + + if (ctx->datasize > 0) + --ctx->datasize; /* strip last '\0' */ +} + +/* Extract '\0'-separated list of args to new buffer, terminated by NULL */ +static const char **unpackcmdargsnul(const context_t *ctx) +{ + const char **args = NULL; + size_t nargs = 0, maxnargs = 0; + const char *s = ctx->data; + const char *e = ctx->data + ctx->datasize; + for (;;) { + if (nargs + 1 >= maxnargs) { /* including last NULL */ + maxnargs += 256; + args = realloc(args, maxnargs * sizeof(args[0])); + if (!args) + abortmsg("failed to allocate args buffer"); + } + args[nargs] = s; + nargs++; + s = memchr(s, '\0', e - s); + if (!s) + break; + s++; + } + args[nargs] = NULL; + return args; +} + +static void handlereadrequest(hgclient_t *hgc) +{ + context_t *ctx = &hgc->ctx; + size_t r = fread(ctx->data, sizeof(ctx->data[0]), ctx->datasize, stdin); + ctx->datasize = r; + writeblock(hgc); +} + +/* Read single-line */ +static void handlereadlinerequest(hgclient_t *hgc) +{ + context_t *ctx = &hgc->ctx; + if (!fgets(ctx->data, ctx->datasize, stdin)) + ctx->data[0] = '\0'; + ctx->datasize = strlen(ctx->data); + writeblock(hgc); +} + +/* Execute the requested command and write exit code */ +static void handlesystemrequest(hgclient_t *hgc) +{ + context_t *ctx = &hgc->ctx; + enlargecontext(ctx, ctx->datasize + 1); + ctx->data[ctx->datasize] = '\0'; /* terminate last string */ + + const char **args = unpackcmdargsnul(ctx); + if (!args[0] || !args[1]) + abortmsg("missing command or cwd in system request"); + debugmsg("run '%s' at '%s'", args[0], args[1]); + int32_t r = runshellcmd(args[0], args + 2, args[1]); + free(args); + + uint32_t r_n = htonl(r); + memcpy(ctx->data, &r_n, sizeof(r_n)); + ctx->datasize = sizeof(r_n); + writeblock(hgc); +} + +/* Read response of command execution until receiving 'r'-esult */ +static void handleresponse(hgclient_t *hgc) +{ + for (;;) { + readchannel(hgc); + context_t *ctx = &hgc->ctx; + debugmsg("response read from channel %c, size %zu", + ctx->ch, ctx->datasize); + switch (ctx->ch) { + case 'o': + fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize, + stdout); + break; + case 'e': + fwrite(ctx->data, sizeof(ctx->data[0]), ctx->datasize, + stderr); + break; + case 'd': + /* assumes last char is '\n' */ + ctx->data[ctx->datasize - 1] = '\0'; + debugmsg("server: %s", ctx->data); + break; + case 'r': + return; + case 'I': + handlereadrequest(hgc); + break; + case 'L': + handlereadlinerequest(hgc); + break; + case 'S': + handlesystemrequest(hgc); + break; + default: + if (isupper(ctx->ch)) + abortmsg("cannot handle response (ch = %c)", + ctx->ch); + } + } +} + +static unsigned int parsecapabilities(const char *s, const char *e) +{ + unsigned int flags = 0; + while (s < e) { + const char *t = strchr(s, ' '); + if (!t || t > e) + t = e; + const cappair_t *cap; + for (cap = captable; cap->flag; ++cap) { + size_t n = t - s; + if (strncmp(s, cap->name, n) == 0 && + strlen(cap->name) == n) { + flags |= cap->flag; + break; + } + } + s = t + 1; + } + return flags; +} + +static void readhello(hgclient_t *hgc) +{ + readchannel(hgc); + context_t *ctx = &hgc->ctx; + if (ctx->ch != 'o') + abortmsg("unexpected channel of hello message (ch = %c)", + ctx->ch); + enlargecontext(ctx, ctx->datasize + 1); + ctx->data[ctx->datasize] = '\0'; + debugmsg("hello received: %s (size = %zu)", ctx->data, ctx->datasize); + + const char *s = ctx->data; + const char *const dataend = ctx->data + ctx->datasize; + while (s < dataend) { + const char *t = strchr(s, ':'); + if (!t || t[1] != ' ') + break; + const char *u = strchr(t + 2, '\n'); + if (!u) + u = dataend; + if (strncmp(s, "capabilities:", t - s + 1) == 0) { + hgc->capflags = parsecapabilities(t + 2, u); + } else if (strncmp(s, "pid:", t - s + 1) == 0) { + hgc->pid = strtol(t + 2, NULL, 10); + } + s = u + 1; + } + debugmsg("capflags=0x%04x, pid=%d", hgc->capflags, hgc->pid); +} + +static void attachio(hgclient_t *hgc) +{ + debugmsg("request attachio"); + static const char chcmd[] = "attachio\n"; + sendall(hgc->sockfd, chcmd, sizeof(chcmd) - 1); + readchannel(hgc); + context_t *ctx = &hgc->ctx; + if (ctx->ch != 'I') + abortmsg("unexpected response for attachio (ch = %c)", ctx->ch); + + static const int fds[3] = {STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO}; + struct msghdr msgh; + memset(&msgh, 0, sizeof(msgh)); + struct iovec iov = {ctx->data, ctx->datasize}; /* dummy payload */ + msgh.msg_iov = &iov; + msgh.msg_iovlen = 1; + char fdbuf[CMSG_SPACE(sizeof(fds))]; + msgh.msg_control = fdbuf; + msgh.msg_controllen = sizeof(fdbuf); + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msgh); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + cmsg->cmsg_len = CMSG_LEN(sizeof(fds)); + memcpy(CMSG_DATA(cmsg), fds, sizeof(fds)); + msgh.msg_controllen = cmsg->cmsg_len; + ssize_t r = sendmsg(hgc->sockfd, &msgh, 0); + if (r < 0) + abortmsg("sendmsg failed (errno = %d)", errno); + + handleresponse(hgc); + int32_t n; + if (ctx->datasize != sizeof(n)) + abortmsg("unexpected size of attachio result"); + memcpy(&n, ctx->data, sizeof(n)); + n = ntohl(n); + if (n != sizeof(fds) / sizeof(fds[0])) + abortmsg("failed to send fds (n = %d)", n); +} + +static void chdirtocwd(hgclient_t *hgc) +{ + if (!getcwd(hgc->ctx.data, hgc->ctx.maxdatasize)) + abortmsg("failed to getcwd (errno = %d)", errno); + hgc->ctx.datasize = strlen(hgc->ctx.data); + writeblockrequest(hgc, "chdir"); +} + +/*! + * Open connection to per-user cmdserver + * + * If no background server running, returns NULL. + */ +hgclient_t *hgc_open(const char *sockname) +{ + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) + abortmsg("cannot create socket (errno = %d)", errno); + + /* don't keep fd on fork(), so that it can be closed when the parent + * process get terminated. */ + int flags = fcntl(fd, F_GETFD); + if (flags < 0) + abortmsg("cannot get flags of socket (errno = %d)", errno); + if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) + abortmsg("cannot set flags of socket (errno = %d)", errno); + + struct sockaddr_un addr; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, sockname, sizeof(addr.sun_path)); + addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; + + debugmsg("connect to %s", addr.sun_path); + int r = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (r < 0) { + close(fd); + if (errno == ENOENT || errno == ECONNREFUSED) + return NULL; + abortmsg("cannot connect to %s (errno = %d)", + addr.sun_path, errno); + } + + hgclient_t *hgc = malloc(sizeof(hgclient_t)); + if (!hgc) + abortmsg("failed to allocate hgclient object"); + memset(hgc, 0, sizeof(*hgc)); + hgc->sockfd = fd; + initcontext(&hgc->ctx); + + readhello(hgc); + if (!(hgc->capflags & CAP_RUNCOMMAND)) + abortmsg("insufficient capability: runcommand"); + if (hgc->capflags & CAP_ATTACHIO) + attachio(hgc); + if (hgc->capflags & CAP_CHDIR) + chdirtocwd(hgc); + + return hgc; +} + +/*! + * Close connection and free allocated memory + */ +void hgc_close(hgclient_t *hgc) +{ + assert(hgc); + freecontext(&hgc->ctx); + close(hgc->sockfd); + free(hgc); +} + +pid_t hgc_peerpid(const hgclient_t *hgc) +{ + assert(hgc); + return hgc->pid; +} + +/*! + * Execute the specified Mercurial command + * + * @return result code + */ +int hgc_runcommand(hgclient_t *hgc, const char *const args[], size_t argsize) +{ + assert(hgc); + + packcmdargs(&hgc->ctx, args, argsize); + writeblockrequest(hgc, "runcommand"); + handleresponse(hgc); + + int32_t exitcode_n; + if (hgc->ctx.datasize != sizeof(exitcode_n)) { + abortmsg("unexpected size of exitcode"); + } + memcpy(&exitcode_n, hgc->ctx.data, sizeof(exitcode_n)); + return ntohl(exitcode_n); +} + +/*! + * (Re-)send client's stdio channels so that the server can access to tty + */ +void hgc_attachio(hgclient_t *hgc) +{ + assert(hgc); + if (!(hgc->capflags & CAP_ATTACHIO)) + return; + attachio(hgc); +} + +/*! + * Get pager command for the given Mercurial command args + * + * If no pager enabled, returns NULL. The return value becomes invalid + * once you run another request to hgc. + */ +const char *hgc_getpager(hgclient_t *hgc, const char *const args[], + size_t argsize) +{ + assert(hgc); + + if (!(hgc->capflags & CAP_GETPAGER)) + return NULL; + + packcmdargs(&hgc->ctx, args, argsize); + writeblockrequest(hgc, "getpager"); + handleresponse(hgc); + + if (hgc->ctx.datasize < 1 || hgc->ctx.data[0] == '\0') + return NULL; + enlargecontext(&hgc->ctx, hgc->ctx.datasize + 1); + hgc->ctx.data[hgc->ctx.datasize] = '\0'; + return hgc->ctx.data; +} + +/*! + * Update server's environment variables + * + * @param envp list of environment variables in "NAME=VALUE" format, + * terminated by NULL. + */ +void hgc_setenv(hgclient_t *hgc, const char *const envp[]) +{ + assert(hgc && envp); + if (!(hgc->capflags & CAP_SETENV)) + return; + packcmdargs(&hgc->ctx, envp, /*argsize*/ -1); + writeblockrequest(hgc, "setenv"); +} diff --git a/contrib/chg/hgclient.h b/contrib/chg/hgclient.h new file mode 100644 --- /dev/null +++ b/contrib/chg/hgclient.h @@ -0,0 +1,29 @@ +/* + * A command server client that uses Unix domain socket + * + * Copyright (c) 2011 Yuya Nishihara + * + * This software may be used and distributed according to the terms of the + * GNU General Public License version 2 or any later version. + */ + +#ifndef HGCLIENT_H_ +#define HGCLIENT_H_ + +#include + +struct hgclient_tag_; +typedef struct hgclient_tag_ hgclient_t; + +hgclient_t *hgc_open(const char *sockname); +void hgc_close(hgclient_t *hgc); + +pid_t hgc_peerpid(const hgclient_t *hgc); + +int hgc_runcommand(hgclient_t *hgc, const char *const args[], size_t argsize); +void hgc_attachio(hgclient_t *hgc); +const char *hgc_getpager(hgclient_t *hgc, const char *const args[], + size_t argsize); +void hgc_setenv(hgclient_t *hgc, const char *const envp[]); + +#endif /* HGCLIENT_H_ */ diff --git a/contrib/chg/util.c b/contrib/chg/util.c new file mode 100644 --- /dev/null +++ b/contrib/chg/util.c @@ -0,0 +1,121 @@ +/* + * Utility functions + * + * Copyright (c) 2011 Yuya Nishihara + * + * This software may be used and distributed according to the terms of the + * GNU General Public License version 2 or any later version. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "util.h" + +void abortmsg(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + fputs("\033[1;31mchg: abort: ", stderr); + vfprintf(stderr, fmt, args); + fputs("\033[m\n", stderr); + va_end(args); + + exit(255); +} + +static int debugmsgenabled = 0; + +void enabledebugmsg(void) +{ + debugmsgenabled = 1; +} + +void debugmsg(const char *fmt, ...) +{ + if (!debugmsgenabled) + return; + + va_list args; + va_start(args, fmt); + fputs("\033[1;30mchg: debug: ", stderr); + vfprintf(stderr, fmt, args); + fputs("\033[m\n", stderr); + va_end(args); +} + +/* + * Execute a shell command in mostly the same manner as system(), with the + * give environment variables, after chdir to the given cwd. Returns a status + * code compatible with the Python subprocess module. + */ +int runshellcmd(const char *cmd, const char *envp[], const char *cwd) +{ + enum { F_SIGINT = 1, F_SIGQUIT = 2, F_SIGMASK = 4, F_WAITPID = 8 }; + unsigned int doneflags = 0; + int status = 0; + struct sigaction newsa, oldsaint, oldsaquit; + sigset_t oldmask; + + /* block or mask signals just as system() does */ + newsa.sa_handler = SIG_IGN; + newsa.sa_flags = 0; + if (sigemptyset(&newsa.sa_mask) < 0) + goto done; + if (sigaction(SIGINT, &newsa, &oldsaint) < 0) + goto done; + doneflags |= F_SIGINT; + if (sigaction(SIGQUIT, &newsa, &oldsaquit) < 0) + goto done; + doneflags |= F_SIGQUIT; + + if (sigaddset(&newsa.sa_mask, SIGCHLD) < 0) + goto done; + if (sigprocmask(SIG_BLOCK, &newsa.sa_mask, &oldmask) < 0) + goto done; + doneflags |= F_SIGMASK; + + pid_t pid = fork(); + if (pid < 0) + goto done; + if (pid == 0) { + sigaction(SIGINT, &oldsaint, NULL); + sigaction(SIGQUIT, &oldsaquit, NULL); + sigprocmask(SIG_SETMASK, &oldmask, NULL); + if (cwd && chdir(cwd) < 0) + _exit(127); + const char *argv[] = {"sh", "-c", cmd, NULL}; + if (envp) { + execve("/bin/sh", (char **)argv, (char **)envp); + } else { + execv("/bin/sh", (char **)argv); + } + _exit(127); + } else { + if (waitpid(pid, &status, 0) < 0) + goto done; + doneflags |= F_WAITPID; + } + +done: + if (doneflags & F_SIGINT) + sigaction(SIGINT, &oldsaint, NULL); + if (doneflags & F_SIGQUIT) + sigaction(SIGQUIT, &oldsaquit, NULL); + if (doneflags & F_SIGMASK) + sigprocmask(SIG_SETMASK, &oldmask, NULL); + + /* no way to report other errors, use 127 (= shell termination) */ + if (!(doneflags & F_WAITPID)) + return 127; + if (WIFEXITED(status)) + return WEXITSTATUS(status); + if (WIFSIGNALED(status)) + return -WTERMSIG(status); + return 127; +} diff --git a/contrib/chg/util.h b/contrib/chg/util.h new file mode 100644 --- /dev/null +++ b/contrib/chg/util.h @@ -0,0 +1,24 @@ +/* + * Utility functions + * + * Copyright (c) 2011 Yuya Nishihara + * + * This software may be used and distributed according to the terms of the + * GNU General Public License version 2 or any later version. + */ + +#ifndef UTIL_H_ +#define UTIL_H_ + +#ifdef __GNUC__ +#define PRINTF_FORMAT_ __attribute__((format(printf, 1, 2))) +#endif + +void abortmsg(const char *fmt, ...) PRINTF_FORMAT_; + +void enabledebugmsg(void); +void debugmsg(const char *fmt, ...) PRINTF_FORMAT_; + +int runshellcmd(const char *cmd, const char *envp[], const char *cwd); + +#endif /* UTIL_H_ */