##// END OF EJS Templates
chg: allows default hg path to be overridden...
Jun Wu -
r28605:baa07320 default
parent child Browse files
Show More
@@ -1,49 +1,52 b''
1 1 HG = $(CURDIR)/../../hg
2 2
3 3 TARGET = chg
4 4 SRCS = chg.c hgclient.c util.c
5 5 OBJS = $(SRCS:.c=.o)
6 6
7 7 CFLAGS ?= -O2 -Wall -Wextra -pedantic -g
8 8 CPPFLAGS ?= -D_FORTIFY_SOURCE=2
9 9 override CFLAGS += -std=gnu99
10 ifdef HGPATH
11 override CPPFLAGS += -DHGPATH=\"$(HGPATH)\"
12 endif
10 13
11 14 DESTDIR =
12 15 PREFIX = /usr/local
13 16 MANDIR = $(PREFIX)/share/man/man1
14 17
15 18 CHGSOCKDIR = /tmp/chg$(shell id -u)
16 19 CHGSOCKNAME = $(CHGSOCKDIR)/server
17 20
18 21 .PHONY: all
19 22 all: $(TARGET)
20 23
21 24 $(TARGET): $(OBJS)
22 25 $(CC) $(LDFLAGS) -o $@ $(OBJS)
23 26
24 27 chg.o: hgclient.h util.h
25 28 hgclient.o: hgclient.h util.h
26 29 util.o: util.h
27 30
28 31 .PHONY: install
29 32 install: $(TARGET)
30 33 install -d $(DESTDIR)$(PREFIX)/bin
31 34 install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin
32 35 install -d $(DESTDIR)$(MANDIR)
33 36 install -m 644 chg.1 $(DESTDIR)$(MANDIR)
34 37
35 38 .PHONY: serve
36 39 serve:
37 40 [ -d $(CHGSOCKDIR) ] || ( umask 077; mkdir $(CHGSOCKDIR) )
38 41 $(HG) serve --cwd / --cmdserver chgunix \
39 42 --address $(CHGSOCKNAME) \
40 43 --config extensions.chgserver= \
41 44 --config cmdserver.log=/dev/stderr
42 45
43 46 .PHONY: clean
44 47 clean:
45 48 $(RM) $(OBJS)
46 49
47 50 .PHONY: distclean
48 51 distclean:
49 52 $(RM) $(OBJS) $(TARGET)
@@ -1,568 +1,572 b''
1 1 /*
2 2 * A fast client for Mercurial command server
3 3 *
4 4 * Copyright (c) 2011 Yuya Nishihara <yuya@tcha.org>
5 5 *
6 6 * This software may be used and distributed according to the terms of the
7 7 * GNU General Public License version 2 or any later version.
8 8 */
9 9
10 10 #include <assert.h>
11 11 #include <errno.h>
12 12 #include <fcntl.h>
13 13 #include <signal.h>
14 14 #include <stdio.h>
15 15 #include <stdlib.h>
16 16 #include <string.h>
17 17 #include <sys/file.h>
18 18 #include <sys/stat.h>
19 19 #include <sys/types.h>
20 20 #include <sys/un.h>
21 21 #include <sys/wait.h>
22 22 #include <time.h>
23 23 #include <unistd.h>
24 24
25 25 #include "hgclient.h"
26 26 #include "util.h"
27 27
28 28 #ifndef UNIX_PATH_MAX
29 29 #define UNIX_PATH_MAX (sizeof(((struct sockaddr_un *)NULL)->sun_path))
30 30 #endif
31 31
32 32 struct cmdserveropts {
33 33 char sockname[UNIX_PATH_MAX];
34 34 char redirectsockname[UNIX_PATH_MAX];
35 35 char lockfile[UNIX_PATH_MAX];
36 36 size_t argsize;
37 37 const char **args;
38 38 int lockfd;
39 39 };
40 40
41 41 static void initcmdserveropts(struct cmdserveropts *opts) {
42 42 memset(opts, 0, sizeof(struct cmdserveropts));
43 43 opts->lockfd = -1;
44 44 }
45 45
46 46 static void freecmdserveropts(struct cmdserveropts *opts) {
47 47 free(opts->args);
48 48 opts->args = NULL;
49 49 opts->argsize = 0;
50 50 }
51 51
52 52 /*
53 53 * Test if an argument is a sensitive flag that should be passed to the server.
54 54 * Return 0 if not, otherwise the number of arguments starting from the current
55 55 * one that should be passed to the server.
56 56 */
57 57 static size_t testsensitiveflag(const char *arg)
58 58 {
59 59 static const struct {
60 60 const char *name;
61 61 size_t narg;
62 62 } flags[] = {
63 63 {"--config", 1},
64 64 {"--cwd", 1},
65 65 {"--repo", 1},
66 66 {"--repository", 1},
67 67 {"--traceback", 0},
68 68 {"-R", 1},
69 69 };
70 70 size_t i;
71 71 for (i = 0; i < sizeof(flags) / sizeof(flags[0]); ++i) {
72 72 size_t len = strlen(flags[i].name);
73 73 size_t narg = flags[i].narg;
74 74 if (memcmp(arg, flags[i].name, len) == 0) {
75 75 if (arg[len] == '\0') { /* --flag (value) */
76 76 return narg + 1;
77 77 } else if (arg[len] == '=' && narg > 0) { /* --flag=value */
78 78 return 1;
79 79 } else if (flags[i].name[1] != '-') { /* short flag */
80 80 return 1;
81 81 }
82 82 }
83 83 }
84 84 return 0;
85 85 }
86 86
87 87 /*
88 88 * Parse argv[] and put sensitive flags to opts->args
89 89 */
90 90 static void setcmdserverargs(struct cmdserveropts *opts,
91 91 int argc, const char *argv[])
92 92 {
93 93 size_t i, step;
94 94 opts->argsize = 0;
95 95 for (i = 0, step = 1; i < (size_t)argc; i += step, step = 1) {
96 96 if (!argv[i])
97 97 continue; /* pass clang-analyse */
98 98 if (strcmp(argv[i], "--") == 0)
99 99 break;
100 100 size_t n = testsensitiveflag(argv[i]);
101 101 if (n == 0 || i + n > (size_t)argc)
102 102 continue;
103 103 opts->args = reallocx(opts->args,
104 104 (n + opts->argsize) * sizeof(char *));
105 105 memcpy(opts->args + opts->argsize, argv + i,
106 106 sizeof(char *) * n);
107 107 opts->argsize += n;
108 108 step = n;
109 109 }
110 110 }
111 111
112 112 static void preparesockdir(const char *sockdir)
113 113 {
114 114 int r;
115 115 r = mkdir(sockdir, 0700);
116 116 if (r < 0 && errno != EEXIST)
117 117 abortmsg("cannot create sockdir %s (errno = %d)",
118 118 sockdir, errno);
119 119
120 120 struct stat st;
121 121 r = lstat(sockdir, &st);
122 122 if (r < 0)
123 123 abortmsg("cannot stat %s (errno = %d)", sockdir, errno);
124 124 if (!S_ISDIR(st.st_mode))
125 125 abortmsg("cannot create sockdir %s (file exists)", sockdir);
126 126 if (st.st_uid != geteuid() || st.st_mode & 0077)
127 127 abortmsg("insecure sockdir %s", sockdir);
128 128 }
129 129
130 130 static void setcmdserveropts(struct cmdserveropts *opts)
131 131 {
132 132 int r;
133 133 char sockdir[UNIX_PATH_MAX];
134 134 const char *envsockname = getenv("CHGSOCKNAME");
135 135 if (!envsockname) {
136 136 /* by default, put socket file in secure directory
137 137 * (permission of socket file may be ignored on some Unices) */
138 138 const char *tmpdir = getenv("TMPDIR");
139 139 if (!tmpdir)
140 140 tmpdir = "/tmp";
141 141 r = snprintf(sockdir, sizeof(sockdir), "%s/chg%d",
142 142 tmpdir, geteuid());
143 143 if (r < 0 || (size_t)r >= sizeof(sockdir))
144 144 abortmsg("too long TMPDIR (r = %d)", r);
145 145 preparesockdir(sockdir);
146 146 }
147 147
148 148 const char *basename = (envsockname) ? envsockname : sockdir;
149 149 const char *sockfmt = (envsockname) ? "%s" : "%s/server";
150 150 const char *lockfmt = (envsockname) ? "%s.lock" : "%s/lock";
151 151 r = snprintf(opts->sockname, sizeof(opts->sockname), sockfmt, basename);
152 152 if (r < 0 || (size_t)r >= sizeof(opts->sockname))
153 153 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
154 154 r = snprintf(opts->lockfile, sizeof(opts->lockfile), lockfmt, basename);
155 155 if (r < 0 || (size_t)r >= sizeof(opts->lockfile))
156 156 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
157 157 }
158 158
159 159 /*
160 160 * Acquire a file lock that indicates a client is trying to start and connect
161 161 * to a server, before executing a command. The lock is released upon exit or
162 162 * explicit unlock. Will block if the lock is held by another process.
163 163 */
164 164 static void lockcmdserver(struct cmdserveropts *opts)
165 165 {
166 166 if (opts->lockfd == -1) {
167 167 opts->lockfd = open(opts->lockfile, O_RDWR | O_CREAT | O_NOFOLLOW, 0600);
168 168 if (opts->lockfd == -1)
169 169 abortmsg("cannot create lock file %s", opts->lockfile);
170 170 }
171 171 int r = flock(opts->lockfd, LOCK_EX);
172 172 if (r == -1)
173 173 abortmsg("cannot acquire lock");
174 174 }
175 175
176 176 /*
177 177 * Release the file lock held by calling lockcmdserver. Will do nothing if
178 178 * lockcmdserver is not called.
179 179 */
180 180 static void unlockcmdserver(struct cmdserveropts *opts)
181 181 {
182 182 if (opts->lockfd == -1)
183 183 return;
184 184 flock(opts->lockfd, LOCK_UN);
185 185 close(opts->lockfd);
186 186 opts->lockfd = -1;
187 187 }
188 188
189 189 static const char *gethgcmd(void)
190 190 {
191 191 static const char *hgcmd = NULL;
192 192 if (!hgcmd) {
193 193 hgcmd = getenv("CHGHG");
194 194 if (!hgcmd || hgcmd[0] == '\0')
195 195 hgcmd = getenv("HG");
196 196 if (!hgcmd || hgcmd[0] == '\0')
197 #ifdef HGPATH
198 hgcmd = (HGPATH);
199 #else
197 200 hgcmd = "hg";
201 #endif
198 202 }
199 203 return hgcmd;
200 204 }
201 205
202 206 static void execcmdserver(const struct cmdserveropts *opts)
203 207 {
204 208 const char *hgcmd = gethgcmd();
205 209
206 210 const char *baseargv[] = {
207 211 hgcmd,
208 212 "serve",
209 213 "--cmdserver", "chgunix",
210 214 "--address", opts->sockname,
211 215 "--daemon-postexec", "chdir:/",
212 216 "--config", "extensions.chgserver=",
213 217 };
214 218 size_t baseargvsize = sizeof(baseargv) / sizeof(baseargv[0]);
215 219 size_t argsize = baseargvsize + opts->argsize + 1;
216 220
217 221 const char **argv = mallocx(sizeof(char *) * argsize);
218 222 memcpy(argv, baseargv, sizeof(baseargv));
219 223 memcpy(argv + baseargvsize, opts->args, sizeof(char *) * opts->argsize);
220 224 argv[argsize - 1] = NULL;
221 225
222 226 if (putenv("CHGINTERNALMARK=") != 0)
223 227 abortmsg("failed to putenv (errno = %d)", errno);
224 228 if (execvp(hgcmd, (char **)argv) < 0)
225 229 abortmsg("failed to exec cmdserver (errno = %d)", errno);
226 230 free(argv);
227 231 }
228 232
229 233 /* Retry until we can connect to the server. Give up after some time. */
230 234 static hgclient_t *retryconnectcmdserver(struct cmdserveropts *opts, pid_t pid)
231 235 {
232 236 static const struct timespec sleepreq = {0, 10 * 1000000};
233 237 int pst = 0;
234 238
235 239 for (unsigned int i = 0; i < 10 * 100; i++) {
236 240 hgclient_t *hgc = hgc_open(opts->sockname);
237 241 if (hgc)
238 242 return hgc;
239 243
240 244 if (pid > 0) {
241 245 /* collect zombie if child process fails to start */
242 246 int r = waitpid(pid, &pst, WNOHANG);
243 247 if (r != 0)
244 248 goto cleanup;
245 249 }
246 250
247 251 nanosleep(&sleepreq, NULL);
248 252 }
249 253
250 254 abortmsg("timed out waiting for cmdserver %s", opts->sockname);
251 255 return NULL;
252 256
253 257 cleanup:
254 258 if (WIFEXITED(pst)) {
255 259 debugmsg("cmdserver exited with status %d", WEXITSTATUS(pst));
256 260 exit(WEXITSTATUS(pst));
257 261 } else if (WIFSIGNALED(pst)) {
258 262 abortmsg("cmdserver killed by signal %d", WTERMSIG(pst));
259 263 } else {
260 264 abortmsg("error white waiting cmdserver");
261 265 }
262 266 return NULL;
263 267 }
264 268
265 269 /* Connect to a cmdserver. Will start a new server on demand. */
266 270 static hgclient_t *connectcmdserver(struct cmdserveropts *opts)
267 271 {
268 272 const char *sockname = opts->redirectsockname[0] ?
269 273 opts->redirectsockname : opts->sockname;
270 274 hgclient_t *hgc = hgc_open(sockname);
271 275 if (hgc)
272 276 return hgc;
273 277
274 278 lockcmdserver(opts);
275 279 hgc = hgc_open(sockname);
276 280 if (hgc) {
277 281 unlockcmdserver(opts);
278 282 debugmsg("cmdserver is started by another process");
279 283 return hgc;
280 284 }
281 285
282 286 /* prevent us from being connected to an outdated server: we were
283 287 * told by a server to redirect to opts->redirectsockname and that
284 288 * address does not work. we do not want to connect to the server
285 289 * again because it will probably tell us the same thing. */
286 290 if (sockname == opts->redirectsockname)
287 291 unlink(opts->sockname);
288 292
289 293 debugmsg("start cmdserver at %s", opts->sockname);
290 294
291 295 pid_t pid = fork();
292 296 if (pid < 0)
293 297 abortmsg("failed to fork cmdserver process");
294 298 if (pid == 0) {
295 299 /* do not leak lockfd to hg */
296 300 close(opts->lockfd);
297 301 execcmdserver(opts);
298 302 } else {
299 303 hgc = retryconnectcmdserver(opts, pid);
300 304 }
301 305
302 306 unlockcmdserver(opts);
303 307 return hgc;
304 308 }
305 309
306 310 static void killcmdserver(const struct cmdserveropts *opts)
307 311 {
308 312 /* resolve config hash */
309 313 char *resolvedpath = realpath(opts->sockname, NULL);
310 314 if (resolvedpath) {
311 315 unlink(resolvedpath);
312 316 free(resolvedpath);
313 317 }
314 318 }
315 319
316 320 static pid_t peerpid = 0;
317 321
318 322 static void forwardsignal(int sig)
319 323 {
320 324 assert(peerpid > 0);
321 325 if (kill(peerpid, sig) < 0)
322 326 abortmsg("cannot kill %d (errno = %d)", peerpid, errno);
323 327 debugmsg("forward signal %d", sig);
324 328 }
325 329
326 330 static void handlestopsignal(int sig)
327 331 {
328 332 sigset_t unblockset, oldset;
329 333 struct sigaction sa, oldsa;
330 334 if (sigemptyset(&unblockset) < 0)
331 335 goto error;
332 336 if (sigaddset(&unblockset, sig) < 0)
333 337 goto error;
334 338 memset(&sa, 0, sizeof(sa));
335 339 sa.sa_handler = SIG_DFL;
336 340 sa.sa_flags = SA_RESTART;
337 341 if (sigemptyset(&sa.sa_mask) < 0)
338 342 goto error;
339 343
340 344 forwardsignal(sig);
341 345 if (raise(sig) < 0) /* resend to self */
342 346 goto error;
343 347 if (sigaction(sig, &sa, &oldsa) < 0)
344 348 goto error;
345 349 if (sigprocmask(SIG_UNBLOCK, &unblockset, &oldset) < 0)
346 350 goto error;
347 351 /* resent signal will be handled before sigprocmask() returns */
348 352 if (sigprocmask(SIG_SETMASK, &oldset, NULL) < 0)
349 353 goto error;
350 354 if (sigaction(sig, &oldsa, NULL) < 0)
351 355 goto error;
352 356 return;
353 357
354 358 error:
355 359 abortmsg("failed to handle stop signal (errno = %d)", errno);
356 360 }
357 361
358 362 static void setupsignalhandler(pid_t pid)
359 363 {
360 364 if (pid <= 0)
361 365 return;
362 366 peerpid = pid;
363 367
364 368 struct sigaction sa;
365 369 memset(&sa, 0, sizeof(sa));
366 370 sa.sa_handler = forwardsignal;
367 371 sa.sa_flags = SA_RESTART;
368 372 if (sigemptyset(&sa.sa_mask) < 0)
369 373 goto error;
370 374
371 375 if (sigaction(SIGHUP, &sa, NULL) < 0)
372 376 goto error;
373 377 if (sigaction(SIGINT, &sa, NULL) < 0)
374 378 goto error;
375 379
376 380 /* terminate frontend by double SIGTERM in case of server freeze */
377 381 sa.sa_flags |= SA_RESETHAND;
378 382 if (sigaction(SIGTERM, &sa, NULL) < 0)
379 383 goto error;
380 384
381 385 /* propagate job control requests to worker */
382 386 sa.sa_handler = forwardsignal;
383 387 sa.sa_flags = SA_RESTART;
384 388 if (sigaction(SIGCONT, &sa, NULL) < 0)
385 389 goto error;
386 390 sa.sa_handler = handlestopsignal;
387 391 sa.sa_flags = SA_RESTART;
388 392 if (sigaction(SIGTSTP, &sa, NULL) < 0)
389 393 goto error;
390 394
391 395 return;
392 396
393 397 error:
394 398 abortmsg("failed to set up signal handlers (errno = %d)", errno);
395 399 }
396 400
397 401 /* This implementation is based on hgext/pager.py (pre 369741ef7253) */
398 402 static void setuppager(hgclient_t *hgc, const char *const args[],
399 403 size_t argsize)
400 404 {
401 405 const char *pagercmd = hgc_getpager(hgc, args, argsize);
402 406 if (!pagercmd)
403 407 return;
404 408
405 409 int pipefds[2];
406 410 if (pipe(pipefds) < 0)
407 411 return;
408 412 pid_t pid = fork();
409 413 if (pid < 0)
410 414 goto error;
411 415 if (pid == 0) {
412 416 close(pipefds[0]);
413 417 if (dup2(pipefds[1], fileno(stdout)) < 0)
414 418 goto error;
415 419 if (isatty(fileno(stderr))) {
416 420 if (dup2(pipefds[1], fileno(stderr)) < 0)
417 421 goto error;
418 422 }
419 423 close(pipefds[1]);
420 424 hgc_attachio(hgc); /* reattach to pager */
421 425 return;
422 426 } else {
423 427 dup2(pipefds[0], fileno(stdin));
424 428 close(pipefds[0]);
425 429 close(pipefds[1]);
426 430
427 431 int r = execlp("/bin/sh", "/bin/sh", "-c", pagercmd, NULL);
428 432 if (r < 0) {
429 433 abortmsg("cannot start pager '%s' (errno = %d)",
430 434 pagercmd, errno);
431 435 }
432 436 return;
433 437 }
434 438
435 439 error:
436 440 close(pipefds[0]);
437 441 close(pipefds[1]);
438 442 abortmsg("failed to prepare pager (errno = %d)", errno);
439 443 }
440 444
441 445 /* Run instructions sent from the server like unlink and set redirect path
442 446 * Return 1 if reconnect is needed, otherwise 0 */
443 447 static int runinstructions(struct cmdserveropts *opts, const char **insts)
444 448 {
445 449 int needreconnect = 0;
446 450 if (!insts)
447 451 return needreconnect;
448 452
449 453 assert(insts);
450 454 opts->redirectsockname[0] = '\0';
451 455 const char **pinst;
452 456 for (pinst = insts; *pinst; pinst++) {
453 457 debugmsg("instruction: %s", *pinst);
454 458 if (strncmp(*pinst, "unlink ", 7) == 0) {
455 459 unlink(*pinst + 7);
456 460 } else if (strncmp(*pinst, "redirect ", 9) == 0) {
457 461 int r = snprintf(opts->redirectsockname,
458 462 sizeof(opts->redirectsockname),
459 463 "%s", *pinst + 9);
460 464 if (r < 0 || r >= (int)sizeof(opts->redirectsockname))
461 465 abortmsg("redirect path is too long (%d)", r);
462 466 needreconnect = 1;
463 467 } else if (strncmp(*pinst, "exit ", 5) == 0) {
464 468 int n = 0;
465 469 if (sscanf(*pinst + 5, "%d", &n) != 1)
466 470 abortmsg("cannot read the exit code");
467 471 exit(n);
468 472 } else if (strcmp(*pinst, "reconnect") == 0) {
469 473 needreconnect = 1;
470 474 } else {
471 475 abortmsg("unknown instruction: %s", *pinst);
472 476 }
473 477 }
474 478 return needreconnect;
475 479 }
476 480
477 481 /*
478 482 * Test whether the command is unsupported or not. This is not designed to
479 483 * cover all cases. But it's fast, does not depend on the server and does
480 484 * not return false positives.
481 485 */
482 486 static int isunsupported(int argc, const char *argv[])
483 487 {
484 488 enum {
485 489 SERVE = 1,
486 490 DAEMON = 2,
487 491 SERVEDAEMON = SERVE | DAEMON,
488 492 TIME = 4,
489 493 };
490 494 unsigned int state = 0;
491 495 int i;
492 496 for (i = 0; i < argc; ++i) {
493 497 if (strcmp(argv[i], "--") == 0)
494 498 break;
495 499 if (i == 0 && strcmp("serve", argv[i]) == 0)
496 500 state |= SERVE;
497 501 else if (strcmp("-d", argv[i]) == 0 ||
498 502 strcmp("--daemon", argv[i]) == 0)
499 503 state |= DAEMON;
500 504 else if (strcmp("--time", argv[i]) == 0)
501 505 state |= TIME;
502 506 }
503 507 return (state & TIME) == TIME ||
504 508 (state & SERVEDAEMON) == SERVEDAEMON;
505 509 }
506 510
507 511 static void execoriginalhg(const char *argv[])
508 512 {
509 513 debugmsg("execute original hg");
510 514 if (execvp(gethgcmd(), (char **)argv) < 0)
511 515 abortmsg("failed to exec original hg (errno = %d)", errno);
512 516 }
513 517
514 518 int main(int argc, const char *argv[], const char *envp[])
515 519 {
516 520 if (getenv("CHGDEBUG"))
517 521 enabledebugmsg();
518 522
519 523 if (getenv("CHGINTERNALMARK"))
520 524 abortmsg("chg started by chg detected.\n"
521 525 "Please make sure ${HG:-hg} is not a symlink or "
522 526 "wrapper to chg. Alternatively, set $CHGHG to the "
523 527 "path of real hg.");
524 528
525 529 if (isunsupported(argc - 1, argv + 1))
526 530 execoriginalhg(argv);
527 531
528 532 struct cmdserveropts opts;
529 533 initcmdserveropts(&opts);
530 534 setcmdserveropts(&opts);
531 535 setcmdserverargs(&opts, argc, argv);
532 536
533 537 if (argc == 2) {
534 538 if (strcmp(argv[1], "--kill-chg-daemon") == 0) {
535 539 killcmdserver(&opts);
536 540 return 0;
537 541 }
538 542 }
539 543
540 544 hgclient_t *hgc;
541 545 size_t retry = 0;
542 546 while (1) {
543 547 hgc = connectcmdserver(&opts);
544 548 if (!hgc)
545 549 abortmsg("cannot open hg client");
546 550 hgc_setenv(hgc, envp);
547 551 const char **insts = hgc_validate(hgc, argv + 1, argc - 1);
548 552 int needreconnect = runinstructions(&opts, insts);
549 553 free(insts);
550 554 if (!needreconnect)
551 555 break;
552 556 hgc_close(hgc);
553 557 if (++retry > 10)
554 558 abortmsg("too many redirections.\n"
555 559 "Please make sure %s is not a wrapper which "
556 560 "changes sensitive environment variables "
557 561 "before executing hg. If you have to use a "
558 562 "wrapper, wrap chg instead of hg.",
559 563 gethgcmd());
560 564 }
561 565
562 566 setupsignalhandler(hgc_peerpid(hgc));
563 567 setuppager(hgc, argv + 1, argc - 1);
564 568 int exitcode = hgc_runcommand(hgc, argv + 1, argc - 1);
565 569 hgc_close(hgc);
566 570 freecmdserveropts(&opts);
567 571 return exitcode;
568 572 }
General Comments 0
You need to be logged in to leave comments. Login now