chg.c
439 lines
| 11.3 KiB
| text/x-c
|
CLexer
|
r28060 | /* | ||
* A fast client for Mercurial command server | ||||
* | ||||
* Copyright (c) 2011 Yuya Nishihara <yuya@tcha.org> | ||||
* | ||||
* This software may be used and distributed according to the terms of the | ||||
* GNU General Public License version 2 or any later version. | ||||
*/ | ||||
#include <assert.h> | ||||
#include <errno.h> | ||||
#include <fcntl.h> | ||||
#include <signal.h> | ||||
#include <stdio.h> | ||||
#include <stdlib.h> | ||||
#include <string.h> | ||||
|
r28196 | #include <sys/file.h> | ||
|
r28060 | #include <sys/stat.h> | ||
#include <sys/types.h> | ||||
#include <sys/un.h> | ||||
#include <sys/wait.h> | ||||
#include <time.h> | ||||
#include <unistd.h> | ||||
#include "hgclient.h" | ||||
|
r30693 | #include "procutil.h" | ||
|
r28060 | #include "util.h" | ||
|
r30677 | #ifndef PATH_MAX | ||
#define PATH_MAX 4096 | ||||
|
r28060 | #endif | ||
struct cmdserveropts { | ||||
|
r30677 | char sockname[PATH_MAX]; | ||
char initsockname[PATH_MAX]; | ||||
char redirectsockname[PATH_MAX]; | ||||
|
r28167 | size_t argsize; | ||
const char **args; | ||||
|
r28060 | }; | ||
|
r28167 | static void initcmdserveropts(struct cmdserveropts *opts) { | ||
memset(opts, 0, sizeof(struct cmdserveropts)); | ||||
} | ||||
static void freecmdserveropts(struct cmdserveropts *opts) { | ||||
free(opts->args); | ||||
opts->args = NULL; | ||||
opts->argsize = 0; | ||||
} | ||||
/* | ||||
* Test if an argument is a sensitive flag that should be passed to the server. | ||||
* Return 0 if not, otherwise the number of arguments starting from the current | ||||
* one that should be passed to the server. | ||||
*/ | ||||
static size_t testsensitiveflag(const char *arg) | ||||
{ | ||||
static const struct { | ||||
const char *name; | ||||
size_t narg; | ||||
} flags[] = { | ||||
{"--config", 1}, | ||||
{"--cwd", 1}, | ||||
{"--repo", 1}, | ||||
{"--repository", 1}, | ||||
{"--traceback", 0}, | ||||
{"-R", 1}, | ||||
}; | ||||
size_t i; | ||||
for (i = 0; i < sizeof(flags) / sizeof(flags[0]); ++i) { | ||||
size_t len = strlen(flags[i].name); | ||||
size_t narg = flags[i].narg; | ||||
if (memcmp(arg, flags[i].name, len) == 0) { | ||||
|
r28790 | if (arg[len] == '\0') { | ||
/* --flag (value) */ | ||||
|
r28167 | return narg + 1; | ||
|
r28790 | } else if (arg[len] == '=' && narg > 0) { | ||
/* --flag=value */ | ||||
|
r28167 | return 1; | ||
|
r28790 | } else if (flags[i].name[1] != '-') { | ||
/* short flag */ | ||||
|
r28167 | return 1; | ||
} | ||||
} | ||||
} | ||||
return 0; | ||||
} | ||||
/* | ||||
* Parse argv[] and put sensitive flags to opts->args | ||||
*/ | ||||
static void setcmdserverargs(struct cmdserveropts *opts, | ||||
int argc, const char *argv[]) | ||||
{ | ||||
size_t i, step; | ||||
opts->argsize = 0; | ||||
for (i = 0, step = 1; i < (size_t)argc; i += step, step = 1) { | ||||
if (!argv[i]) | ||||
continue; /* pass clang-analyse */ | ||||
if (strcmp(argv[i], "--") == 0) | ||||
break; | ||||
size_t n = testsensitiveflag(argv[i]); | ||||
if (n == 0 || i + n > (size_t)argc) | ||||
continue; | ||||
opts->args = reallocx(opts->args, | ||||
(n + opts->argsize) * sizeof(char *)); | ||||
memcpy(opts->args + opts->argsize, argv + i, | ||||
sizeof(char *) * n); | ||||
opts->argsize += n; | ||||
step = n; | ||||
} | ||||
} | ||||
|
r28060 | static void preparesockdir(const char *sockdir) | ||
{ | ||||
int r; | ||||
r = mkdir(sockdir, 0700); | ||||
if (r < 0 && errno != EEXIST) | ||||
|
r28789 | abortmsgerrno("cannot create sockdir %s", sockdir); | ||
|
r28060 | |||
struct stat st; | ||||
r = lstat(sockdir, &st); | ||||
if (r < 0) | ||||
|
r28789 | abortmsgerrno("cannot stat %s", sockdir); | ||
|
r28060 | 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); | ||||
} | ||||
|
r30680 | static void getdefaultsockdir(char sockdir[], size_t size) | ||
{ | ||||
/* by default, put socket file in secure directory | ||||
|
r30681 | * (${XDG_RUNTIME_DIR}/chg, or /${TMPDIR:-tmp}/chg$UID) | ||
|
r30680 | * (permission of socket file may be ignored on some Unices) */ | ||
|
r30681 | const char *runtimedir = getenv("XDG_RUNTIME_DIR"); | ||
int r; | ||||
if (runtimedir) { | ||||
r = snprintf(sockdir, size, "%s/chg", runtimedir); | ||||
} else { | ||||
const char *tmpdir = getenv("TMPDIR"); | ||||
if (!tmpdir) | ||||
tmpdir = "/tmp"; | ||||
r = snprintf(sockdir, size, "%s/chg%d", tmpdir, geteuid()); | ||||
} | ||||
|
r30680 | if (r < 0 || (size_t)r >= size) | ||
abortmsg("too long TMPDIR (r = %d)", r); | ||||
} | ||||
|
r28060 | static void setcmdserveropts(struct cmdserveropts *opts) | ||
{ | ||||
int r; | ||||
|
r30677 | char sockdir[PATH_MAX]; | ||
|
r28060 | const char *envsockname = getenv("CHGSOCKNAME"); | ||
if (!envsockname) { | ||||
|
r30680 | getdefaultsockdir(sockdir, sizeof(sockdir)); | ||
|
r28060 | preparesockdir(sockdir); | ||
} | ||||
const char *basename = (envsockname) ? envsockname : sockdir; | ||||
const char *sockfmt = (envsockname) ? "%s" : "%s/server"; | ||||
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); | ||||
|
r30620 | r = snprintf(opts->initsockname, sizeof(opts->initsockname), | ||
"%s.%u", opts->sockname, (unsigned)getpid()); | ||||
if (r < 0 || (size_t)r >= sizeof(opts->initsockname)) | ||||
abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r); | ||||
|
r28060 | } | ||
|
r28237 | static const char *gethgcmd(void) | ||
{ | ||||
static const char *hgcmd = NULL; | ||||
if (!hgcmd) { | ||||
hgcmd = getenv("CHGHG"); | ||||
if (!hgcmd || hgcmd[0] == '\0') | ||||
hgcmd = getenv("HG"); | ||||
if (!hgcmd || hgcmd[0] == '\0') | ||||
|
r28605 | #ifdef HGPATH | ||
hgcmd = (HGPATH); | ||||
#else | ||||
|
r28237 | hgcmd = "hg"; | ||
|
r28605 | #endif | ||
|
r28237 | } | ||
return hgcmd; | ||||
} | ||||
|
r28060 | static void execcmdserver(const struct cmdserveropts *opts) | ||
{ | ||||
|
r28237 | const char *hgcmd = gethgcmd(); | ||
|
r28060 | |||
|
r28167 | const char *baseargv[] = { | ||
|
r28060 | hgcmd, | ||
"serve", | ||||
"--cmdserver", "chgunix", | ||||
|
r30620 | "--address", opts->initsockname, | ||
|
r28453 | "--daemon-postexec", "chdir:/", | ||
|
r28060 | }; | ||
|
r28167 | size_t baseargvsize = sizeof(baseargv) / sizeof(baseargv[0]); | ||
size_t argsize = baseargvsize + opts->argsize + 1; | ||||
const char **argv = mallocx(sizeof(char *) * argsize); | ||||
memcpy(argv, baseargv, sizeof(baseargv)); | ||||
memcpy(argv + baseargvsize, opts->args, sizeof(char *) * opts->argsize); | ||||
argv[argsize - 1] = NULL; | ||||
|
r28261 | if (putenv("CHGINTERNALMARK=") != 0) | ||
|
r28789 | abortmsgerrno("failed to putenv"); | ||
|
r28060 | if (execvp(hgcmd, (char **)argv) < 0) | ||
|
r28789 | abortmsgerrno("failed to exec cmdserver"); | ||
|
r28167 | free(argv); | ||
|
r28060 | } | ||
|
r28196 | /* Retry until we can connect to the server. Give up after some time. */ | ||
static hgclient_t *retryconnectcmdserver(struct cmdserveropts *opts, pid_t pid) | ||||
|
r28060 | { | ||
static const struct timespec sleepreq = {0, 10 * 1000000}; | ||||
int pst = 0; | ||||
|
r30620 | debugmsg("try connect to %s repeatedly", opts->initsockname); | ||
|
r29345 | |||
|
r29357 | unsigned int timeoutsec = 60; /* default: 60 seconds */ | ||
|
r29345 | const char *timeoutenv = getenv("CHGTIMEOUT"); | ||
if (timeoutenv) | ||||
sscanf(timeoutenv, "%u", &timeoutsec); | ||||
for (unsigned int i = 0; !timeoutsec || i < timeoutsec * 100; i++) { | ||||
|
r30620 | hgclient_t *hgc = hgc_open(opts->initsockname); | ||
if (hgc) { | ||||
debugmsg("rename %s to %s", opts->initsockname, | ||||
opts->sockname); | ||||
int r = rename(opts->initsockname, opts->sockname); | ||||
if (r != 0) | ||||
abortmsgerrno("cannot rename"); | ||||
|
r28196 | return hgc; | ||
|
r30620 | } | ||
|
r28060 | |||
if (pid > 0) { | ||||
/* collect zombie if child process fails to start */ | ||||
|
r28196 | int r = waitpid(pid, &pst, WNOHANG); | ||
|
r28060 | if (r != 0) | ||
goto cleanup; | ||||
} | ||||
nanosleep(&sleepreq, NULL); | ||||
} | ||||
|
r30620 | abortmsg("timed out waiting for cmdserver %s", opts->initsockname); | ||
|
r28196 | return NULL; | ||
|
r28060 | |||
cleanup: | ||||
if (WIFEXITED(pst)) { | ||||
|
r28863 | if (WEXITSTATUS(pst) == 0) | ||
abortmsg("could not connect to cmdserver " | ||||
"(exited with status 0)"); | ||||
|
r28477 | debugmsg("cmdserver exited with status %d", WEXITSTATUS(pst)); | ||
exit(WEXITSTATUS(pst)); | ||||
|
r28060 | } else if (WIFSIGNALED(pst)) { | ||
abortmsg("cmdserver killed by signal %d", WTERMSIG(pst)); | ||||
} else { | ||||
|
r28851 | abortmsg("error while waiting for cmdserver"); | ||
|
r28060 | } | ||
|
r28196 | return NULL; | ||
|
r28060 | } | ||
|
r28196 | /* Connect to a cmdserver. Will start a new server on demand. */ | ||
static hgclient_t *connectcmdserver(struct cmdserveropts *opts) | ||||
|
r28060 | { | ||
|
r28357 | const char *sockname = opts->redirectsockname[0] ? | ||
opts->redirectsockname : opts->sockname; | ||||
|
r28769 | debugmsg("try connect to %s", sockname); | ||
|
r28357 | hgclient_t *hgc = hgc_open(sockname); | ||
|
r28196 | if (hgc) | ||
return hgc; | ||||
|
r28060 | |||
|
r28357 | /* prevent us from being connected to an outdated server: we were | ||
* told by a server to redirect to opts->redirectsockname and that | ||||
* address does not work. we do not want to connect to the server | ||||
* again because it will probably tell us the same thing. */ | ||||
if (sockname == opts->redirectsockname) | ||||
unlink(opts->sockname); | ||||
|
r30620 | debugmsg("start cmdserver at %s", opts->initsockname); | ||
|
r28060 | |||
pid_t pid = fork(); | ||||
if (pid < 0) | ||||
abortmsg("failed to fork cmdserver process"); | ||||
if (pid == 0) { | ||||
execcmdserver(opts); | ||||
} else { | ||||
|
r28196 | hgc = retryconnectcmdserver(opts, pid); | ||
|
r28060 | } | ||
|
r28196 | |||
return hgc; | ||||
|
r28060 | } | ||
|
r28455 | static void killcmdserver(const struct cmdserveropts *opts) | ||
|
r28060 | { | ||
|
r28455 | /* resolve config hash */ | ||
char *resolvedpath = realpath(opts->sockname, NULL); | ||||
if (resolvedpath) { | ||||
unlink(resolvedpath); | ||||
free(resolvedpath); | ||||
|
r28060 | } | ||
} | ||||
|
r28535 | /* Run instructions sent from the server like unlink and set redirect path | ||
* Return 1 if reconnect is needed, otherwise 0 */ | ||||
static int runinstructions(struct cmdserveropts *opts, const char **insts) | ||||
|
r28357 | { | ||
|
r28535 | int needreconnect = 0; | ||
if (!insts) | ||||
return needreconnect; | ||||
|
r28357 | assert(insts); | ||
opts->redirectsockname[0] = '\0'; | ||||
const char **pinst; | ||||
for (pinst = insts; *pinst; pinst++) { | ||||
debugmsg("instruction: %s", *pinst); | ||||
if (strncmp(*pinst, "unlink ", 7) == 0) { | ||||
unlink(*pinst + 7); | ||||
} else if (strncmp(*pinst, "redirect ", 9) == 0) { | ||||
int r = snprintf(opts->redirectsockname, | ||||
sizeof(opts->redirectsockname), | ||||
"%s", *pinst + 9); | ||||
if (r < 0 || r >= (int)sizeof(opts->redirectsockname)) | ||||
abortmsg("redirect path is too long (%d)", r); | ||||
|
r28535 | needreconnect = 1; | ||
|
r28516 | } else if (strncmp(*pinst, "exit ", 5) == 0) { | ||
int n = 0; | ||||
if (sscanf(*pinst + 5, "%d", &n) != 1) | ||||
abortmsg("cannot read the exit code"); | ||||
exit(n); | ||||
|
r28535 | } else if (strcmp(*pinst, "reconnect") == 0) { | ||
needreconnect = 1; | ||||
|
r28357 | } else { | ||
abortmsg("unknown instruction: %s", *pinst); | ||||
} | ||||
} | ||||
|
r28535 | return needreconnect; | ||
|
r28357 | } | ||
|
r28260 | /* | ||
* Test whether the command is unsupported or not. This is not designed to | ||||
* cover all cases. But it's fast, does not depend on the server and does | ||||
* not return false positives. | ||||
*/ | ||||
static int isunsupported(int argc, const char *argv[]) | ||||
{ | ||||
enum { | ||||
SERVE = 1, | ||||
DAEMON = 2, | ||||
SERVEDAEMON = SERVE | DAEMON, | ||||
TIME = 4, | ||||
}; | ||||
unsigned int state = 0; | ||||
int i; | ||||
for (i = 0; i < argc; ++i) { | ||||
if (strcmp(argv[i], "--") == 0) | ||||
break; | ||||
if (i == 0 && strcmp("serve", argv[i]) == 0) | ||||
state |= SERVE; | ||||
else if (strcmp("-d", argv[i]) == 0 || | ||||
strcmp("--daemon", argv[i]) == 0) | ||||
state |= DAEMON; | ||||
else if (strcmp("--time", argv[i]) == 0) | ||||
state |= TIME; | ||||
} | ||||
return (state & TIME) == TIME || | ||||
(state & SERVEDAEMON) == SERVEDAEMON; | ||||
} | ||||
static void execoriginalhg(const char *argv[]) | ||||
{ | ||||
debugmsg("execute original hg"); | ||||
if (execvp(gethgcmd(), (char **)argv) < 0) | ||||
|
r28789 | abortmsgerrno("failed to exec original hg"); | ||
|
r28260 | } | ||
|
r28060 | int main(int argc, const char *argv[], const char *envp[]) | ||
{ | ||||
if (getenv("CHGDEBUG")) | ||||
enabledebugmsg(); | ||||
|
r28787 | if (!getenv("HGPLAIN") && isatty(fileno(stderr))) | ||
enablecolor(); | ||||
|
r28261 | if (getenv("CHGINTERNALMARK")) | ||
abortmsg("chg started by chg detected.\n" | ||||
"Please make sure ${HG:-hg} is not a symlink or " | ||||
"wrapper to chg. Alternatively, set $CHGHG to the " | ||||
"path of real hg."); | ||||
|
r28260 | if (isunsupported(argc - 1, argv + 1)) | ||
execoriginalhg(argv); | ||||
|
r28060 | struct cmdserveropts opts; | ||
|
r28167 | initcmdserveropts(&opts); | ||
|
r28060 | setcmdserveropts(&opts); | ||
|
r28167 | setcmdserverargs(&opts, argc, argv); | ||
|
r28060 | |||
if (argc == 2) { | ||||
|
r28455 | if (strcmp(argv[1], "--kill-chg-daemon") == 0) { | ||
killcmdserver(&opts); | ||||
|
r28060 | return 0; | ||
} | ||||
} | ||||
|
r28357 | hgclient_t *hgc; | ||
|
r28358 | size_t retry = 0; | ||
|
r28357 | while (1) { | ||
hgc = connectcmdserver(&opts); | ||||
if (!hgc) | ||||
abortmsg("cannot open hg client"); | ||||
hgc_setenv(hgc, envp); | ||||
const char **insts = hgc_validate(hgc, argv + 1, argc - 1); | ||||
|
r28535 | int needreconnect = runinstructions(&opts, insts); | ||
free(insts); | ||||
if (!needreconnect) | ||||
|
r28357 | break; | ||
hgc_close(hgc); | ||||
|
r28358 | if (++retry > 10) | ||
abortmsg("too many redirections.\n" | ||||
"Please make sure %s is not a wrapper which " | ||||
"changes sensitive environment variables " | ||||
"before executing hg. If you have to use a " | ||||
"wrapper, wrap chg instead of hg.", | ||||
gethgcmd()); | ||||
|
r28357 | } | ||
|
r28060 | |||
|
r30690 | setupsignalhandler(hgc_peerpid(hgc), hgc_peerpgid(hgc)); | ||
|
r28060 | int exitcode = hgc_runcommand(hgc, argv + 1, argc - 1); | ||
|
r29369 | restoresignalhandler(); | ||
|
r28060 | hgc_close(hgc); | ||
|
r28167 | freecmdserveropts(&opts); | ||
|
r30692 | waitpager(); | ||
|
r29344 | |||
|
r28060 | return exitcode; | ||
} | ||||