##// END OF EJS Templates
chg: drop progress.assume-tty config...
Jun Wu -
r28327:3ab370f8 default
parent child Browse files
Show More
@@ -1,50 +1,49 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 10
11 11 DESTDIR =
12 12 PREFIX = /usr/local
13 13 MANDIR = $(PREFIX)/share/man/man1
14 14
15 15 CHGSOCKDIR = /tmp/chg$(shell id -u)
16 16 CHGSOCKNAME = $(CHGSOCKDIR)/server
17 17
18 18 .PHONY: all
19 19 all: $(TARGET)
20 20
21 21 $(TARGET): $(OBJS)
22 22 $(CC) $(LDFLAGS) -o $@ $(OBJS)
23 23
24 24 chg.o: hgclient.h util.h
25 25 hgclient.o: hgclient.h util.h
26 26 util.o: util.h
27 27
28 28 .PHONY: install
29 29 install: $(TARGET)
30 30 install -d $(DESTDIR)$(PREFIX)/bin
31 31 install -m 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin
32 32 install -d $(DESTDIR)$(MANDIR)
33 33 install -m 644 chg.1 $(DESTDIR)$(MANDIR)
34 34
35 35 .PHONY: serve
36 36 serve:
37 37 [ -d $(CHGSOCKDIR) ] || ( umask 077; mkdir $(CHGSOCKDIR) )
38 38 $(HG) serve --cwd / --cmdserver chgunix \
39 39 --address $(CHGSOCKNAME) \
40 40 --config extensions.chgserver= \
41 --config progress.assume-tty=1 \
42 41 --config cmdserver.log=/dev/stderr
43 42
44 43 .PHONY: clean
45 44 clean:
46 45 $(RM) $(OBJS)
47 46
48 47 .PHONY: distclean
49 48 distclean:
50 49 $(RM) $(OBJS) $(TARGET)
@@ -1,532 +1,530 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 lockfile[UNIX_PATH_MAX];
35 35 char pidfile[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 const char *pidfmt = (envsockname) ? "%s.pid" : "%s/pid";
152 152 r = snprintf(opts->sockname, sizeof(opts->sockname), sockfmt, basename);
153 153 if (r < 0 || (size_t)r >= sizeof(opts->sockname))
154 154 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
155 155 r = snprintf(opts->lockfile, sizeof(opts->lockfile), lockfmt, basename);
156 156 if (r < 0 || (size_t)r >= sizeof(opts->lockfile))
157 157 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
158 158 r = snprintf(opts->pidfile, sizeof(opts->pidfile), pidfmt, basename);
159 159 if (r < 0 || (size_t)r >= sizeof(opts->pidfile))
160 160 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
161 161 }
162 162
163 163 /*
164 164 * Acquire a file lock that indicates a client is trying to start and connect
165 165 * to a server, before executing a command. The lock is released upon exit or
166 166 * explicit unlock. Will block if the lock is held by another process.
167 167 */
168 168 static void lockcmdserver(struct cmdserveropts *opts)
169 169 {
170 170 if (opts->lockfd == -1) {
171 171 opts->lockfd = open(opts->lockfile, O_RDWR | O_CREAT | O_NOFOLLOW, 0600);
172 172 if (opts->lockfd == -1)
173 173 abortmsg("cannot create lock file %s", opts->lockfile);
174 174 }
175 175 int r = flock(opts->lockfd, LOCK_EX);
176 176 if (r == -1)
177 177 abortmsg("cannot acquire lock");
178 178 }
179 179
180 180 /*
181 181 * Release the file lock held by calling lockcmdserver. Will do nothing if
182 182 * lockcmdserver is not called.
183 183 */
184 184 static void unlockcmdserver(struct cmdserveropts *opts)
185 185 {
186 186 if (opts->lockfd == -1)
187 187 return;
188 188 flock(opts->lockfd, LOCK_UN);
189 189 close(opts->lockfd);
190 190 opts->lockfd = -1;
191 191 }
192 192
193 193 static const char *gethgcmd(void)
194 194 {
195 195 static const char *hgcmd = NULL;
196 196 if (!hgcmd) {
197 197 hgcmd = getenv("CHGHG");
198 198 if (!hgcmd || hgcmd[0] == '\0')
199 199 hgcmd = getenv("HG");
200 200 if (!hgcmd || hgcmd[0] == '\0')
201 201 hgcmd = "hg";
202 202 }
203 203 return hgcmd;
204 204 }
205 205
206 206 static void execcmdserver(const struct cmdserveropts *opts)
207 207 {
208 208 const char *hgcmd = gethgcmd();
209 209
210 210 const char *baseargv[] = {
211 211 hgcmd,
212 212 "serve",
213 213 "--cwd", "/",
214 214 "--cmdserver", "chgunix",
215 215 "--address", opts->sockname,
216 216 "--daemon-postexec", "none",
217 217 "--pid-file", opts->pidfile,
218 218 "--config", "extensions.chgserver=",
219 /* wrap root ui so that it can be disabled/enabled by config */
220 "--config", "progress.assume-tty=1",
221 219 };
222 220 size_t baseargvsize = sizeof(baseargv) / sizeof(baseargv[0]);
223 221 size_t argsize = baseargvsize + opts->argsize + 1;
224 222
225 223 const char **argv = mallocx(sizeof(char *) * argsize);
226 224 memcpy(argv, baseargv, sizeof(baseargv));
227 225 memcpy(argv + baseargvsize, opts->args, sizeof(char *) * opts->argsize);
228 226 argv[argsize - 1] = NULL;
229 227
230 228 if (putenv("CHGINTERNALMARK=") != 0)
231 229 abortmsg("failed to putenv (errno = %d)", errno);
232 230 if (execvp(hgcmd, (char **)argv) < 0)
233 231 abortmsg("failed to exec cmdserver (errno = %d)", errno);
234 232 free(argv);
235 233 }
236 234
237 235 /* Retry until we can connect to the server. Give up after some time. */
238 236 static hgclient_t *retryconnectcmdserver(struct cmdserveropts *opts, pid_t pid)
239 237 {
240 238 static const struct timespec sleepreq = {0, 10 * 1000000};
241 239 int pst = 0;
242 240
243 241 for (unsigned int i = 0; i < 10 * 100; i++) {
244 242 hgclient_t *hgc = hgc_open(opts->sockname);
245 243 if (hgc)
246 244 return hgc;
247 245
248 246 if (pid > 0) {
249 247 /* collect zombie if child process fails to start */
250 248 int r = waitpid(pid, &pst, WNOHANG);
251 249 if (r != 0)
252 250 goto cleanup;
253 251 }
254 252
255 253 nanosleep(&sleepreq, NULL);
256 254 }
257 255
258 256 abortmsg("timed out waiting for cmdserver %s", opts->sockname);
259 257 return NULL;
260 258
261 259 cleanup:
262 260 if (WIFEXITED(pst)) {
263 261 abortmsg("cmdserver exited with status %d", WEXITSTATUS(pst));
264 262 } else if (WIFSIGNALED(pst)) {
265 263 abortmsg("cmdserver killed by signal %d", WTERMSIG(pst));
266 264 } else {
267 265 abortmsg("error white waiting cmdserver");
268 266 }
269 267 return NULL;
270 268 }
271 269
272 270 /* Connect to a cmdserver. Will start a new server on demand. */
273 271 static hgclient_t *connectcmdserver(struct cmdserveropts *opts)
274 272 {
275 273 hgclient_t *hgc = hgc_open(opts->sockname);
276 274 if (hgc)
277 275 return hgc;
278 276
279 277 lockcmdserver(opts);
280 278 hgc = hgc_open(opts->sockname);
281 279 if (hgc) {
282 280 unlockcmdserver(opts);
283 281 debugmsg("cmdserver is started by another process");
284 282 return hgc;
285 283 }
286 284
287 285 debugmsg("start cmdserver at %s", opts->sockname);
288 286
289 287 pid_t pid = fork();
290 288 if (pid < 0)
291 289 abortmsg("failed to fork cmdserver process");
292 290 if (pid == 0) {
293 291 /* do not leak lockfd to hg */
294 292 close(opts->lockfd);
295 293 /* bypass uisetup() of pager extension */
296 294 int nullfd = open("/dev/null", O_WRONLY);
297 295 if (nullfd >= 0) {
298 296 dup2(nullfd, fileno(stdout));
299 297 close(nullfd);
300 298 }
301 299 execcmdserver(opts);
302 300 } else {
303 301 hgc = retryconnectcmdserver(opts, pid);
304 302 }
305 303
306 304 unlockcmdserver(opts);
307 305 return hgc;
308 306 }
309 307
310 308 static void killcmdserver(const struct cmdserveropts *opts, int sig)
311 309 {
312 310 FILE *fp = fopen(opts->pidfile, "r");
313 311 if (!fp)
314 312 abortmsg("cannot open %s (errno = %d)", opts->pidfile, errno);
315 313 int pid = 0;
316 314 int n = fscanf(fp, "%d", &pid);
317 315 fclose(fp);
318 316 if (n != 1 || pid <= 0)
319 317 abortmsg("cannot read pid from %s", opts->pidfile);
320 318
321 319 if (kill((pid_t)pid, sig) < 0) {
322 320 if (errno == ESRCH)
323 321 return;
324 322 abortmsg("cannot kill %d (errno = %d)", pid, errno);
325 323 }
326 324 }
327 325
328 326 static pid_t peerpid = 0;
329 327
330 328 static void forwardsignal(int sig)
331 329 {
332 330 assert(peerpid > 0);
333 331 if (kill(peerpid, sig) < 0)
334 332 abortmsg("cannot kill %d (errno = %d)", peerpid, errno);
335 333 debugmsg("forward signal %d", sig);
336 334 }
337 335
338 336 static void handlestopsignal(int sig)
339 337 {
340 338 sigset_t unblockset, oldset;
341 339 struct sigaction sa, oldsa;
342 340 if (sigemptyset(&unblockset) < 0)
343 341 goto error;
344 342 if (sigaddset(&unblockset, sig) < 0)
345 343 goto error;
346 344 memset(&sa, 0, sizeof(sa));
347 345 sa.sa_handler = SIG_DFL;
348 346 sa.sa_flags = SA_RESTART;
349 347 if (sigemptyset(&sa.sa_mask) < 0)
350 348 goto error;
351 349
352 350 forwardsignal(sig);
353 351 if (raise(sig) < 0) /* resend to self */
354 352 goto error;
355 353 if (sigaction(sig, &sa, &oldsa) < 0)
356 354 goto error;
357 355 if (sigprocmask(SIG_UNBLOCK, &unblockset, &oldset) < 0)
358 356 goto error;
359 357 /* resent signal will be handled before sigprocmask() returns */
360 358 if (sigprocmask(SIG_SETMASK, &oldset, NULL) < 0)
361 359 goto error;
362 360 if (sigaction(sig, &oldsa, NULL) < 0)
363 361 goto error;
364 362 return;
365 363
366 364 error:
367 365 abortmsg("failed to handle stop signal (errno = %d)", errno);
368 366 }
369 367
370 368 static void setupsignalhandler(pid_t pid)
371 369 {
372 370 if (pid <= 0)
373 371 return;
374 372 peerpid = pid;
375 373
376 374 struct sigaction sa;
377 375 memset(&sa, 0, sizeof(sa));
378 376 sa.sa_handler = forwardsignal;
379 377 sa.sa_flags = SA_RESTART;
380 378 if (sigemptyset(&sa.sa_mask) < 0)
381 379 goto error;
382 380
383 381 if (sigaction(SIGHUP, &sa, NULL) < 0)
384 382 goto error;
385 383 if (sigaction(SIGINT, &sa, NULL) < 0)
386 384 goto error;
387 385
388 386 /* terminate frontend by double SIGTERM in case of server freeze */
389 387 sa.sa_flags |= SA_RESETHAND;
390 388 if (sigaction(SIGTERM, &sa, NULL) < 0)
391 389 goto error;
392 390
393 391 /* propagate job control requests to worker */
394 392 sa.sa_handler = forwardsignal;
395 393 sa.sa_flags = SA_RESTART;
396 394 if (sigaction(SIGCONT, &sa, NULL) < 0)
397 395 goto error;
398 396 sa.sa_handler = handlestopsignal;
399 397 sa.sa_flags = SA_RESTART;
400 398 if (sigaction(SIGTSTP, &sa, NULL) < 0)
401 399 goto error;
402 400
403 401 return;
404 402
405 403 error:
406 404 abortmsg("failed to set up signal handlers (errno = %d)", errno);
407 405 }
408 406
409 407 /* This implementation is based on hgext/pager.py (pre 369741ef7253) */
410 408 static void setuppager(hgclient_t *hgc, const char *const args[],
411 409 size_t argsize)
412 410 {
413 411 const char *pagercmd = hgc_getpager(hgc, args, argsize);
414 412 if (!pagercmd)
415 413 return;
416 414
417 415 int pipefds[2];
418 416 if (pipe(pipefds) < 0)
419 417 return;
420 418 pid_t pid = fork();
421 419 if (pid < 0)
422 420 goto error;
423 421 if (pid == 0) {
424 422 close(pipefds[0]);
425 423 if (dup2(pipefds[1], fileno(stdout)) < 0)
426 424 goto error;
427 425 if (isatty(fileno(stderr))) {
428 426 if (dup2(pipefds[1], fileno(stderr)) < 0)
429 427 goto error;
430 428 }
431 429 close(pipefds[1]);
432 430 hgc_attachio(hgc); /* reattach to pager */
433 431 return;
434 432 } else {
435 433 dup2(pipefds[0], fileno(stdin));
436 434 close(pipefds[0]);
437 435 close(pipefds[1]);
438 436
439 437 int r = execlp("/bin/sh", "/bin/sh", "-c", pagercmd, NULL);
440 438 if (r < 0) {
441 439 abortmsg("cannot start pager '%s' (errno = %d)",
442 440 pagercmd, errno);
443 441 }
444 442 return;
445 443 }
446 444
447 445 error:
448 446 close(pipefds[0]);
449 447 close(pipefds[1]);
450 448 abortmsg("failed to prepare pager (errno = %d)", errno);
451 449 }
452 450
453 451 /*
454 452 * Test whether the command is unsupported or not. This is not designed to
455 453 * cover all cases. But it's fast, does not depend on the server and does
456 454 * not return false positives.
457 455 */
458 456 static int isunsupported(int argc, const char *argv[])
459 457 {
460 458 enum {
461 459 SERVE = 1,
462 460 DAEMON = 2,
463 461 SERVEDAEMON = SERVE | DAEMON,
464 462 TIME = 4,
465 463 };
466 464 unsigned int state = 0;
467 465 int i;
468 466 for (i = 0; i < argc; ++i) {
469 467 if (strcmp(argv[i], "--") == 0)
470 468 break;
471 469 if (i == 0 && strcmp("serve", argv[i]) == 0)
472 470 state |= SERVE;
473 471 else if (strcmp("-d", argv[i]) == 0 ||
474 472 strcmp("--daemon", argv[i]) == 0)
475 473 state |= DAEMON;
476 474 else if (strcmp("--time", argv[i]) == 0)
477 475 state |= TIME;
478 476 }
479 477 return (state & TIME) == TIME ||
480 478 (state & SERVEDAEMON) == SERVEDAEMON;
481 479 }
482 480
483 481 static void execoriginalhg(const char *argv[])
484 482 {
485 483 debugmsg("execute original hg");
486 484 if (execvp(gethgcmd(), (char **)argv) < 0)
487 485 abortmsg("failed to exec original hg (errno = %d)", errno);
488 486 }
489 487
490 488 int main(int argc, const char *argv[], const char *envp[])
491 489 {
492 490 if (getenv("CHGDEBUG"))
493 491 enabledebugmsg();
494 492
495 493 if (getenv("CHGINTERNALMARK"))
496 494 abortmsg("chg started by chg detected.\n"
497 495 "Please make sure ${HG:-hg} is not a symlink or "
498 496 "wrapper to chg. Alternatively, set $CHGHG to the "
499 497 "path of real hg.");
500 498
501 499 if (isunsupported(argc - 1, argv + 1))
502 500 execoriginalhg(argv);
503 501
504 502 struct cmdserveropts opts;
505 503 initcmdserveropts(&opts);
506 504 setcmdserveropts(&opts);
507 505 setcmdserverargs(&opts, argc, argv);
508 506
509 507 if (argc == 2) {
510 508 int sig = 0;
511 509 if (strcmp(argv[1], "--kill-chg-daemon") == 0)
512 510 sig = SIGTERM;
513 511 if (strcmp(argv[1], "--reload-chg-daemon") == 0)
514 512 sig = SIGHUP;
515 513 if (sig > 0) {
516 514 killcmdserver(&opts, sig);
517 515 return 0;
518 516 }
519 517 }
520 518
521 519 hgclient_t *hgc = connectcmdserver(&opts);
522 520 if (!hgc)
523 521 abortmsg("cannot open hg client");
524 522
525 523 setupsignalhandler(hgc_peerpid(hgc));
526 524 hgc_setenv(hgc, envp);
527 525 setuppager(hgc, argv + 1, argc - 1);
528 526 int exitcode = hgc_runcommand(hgc, argv + 1, argc - 1);
529 527 hgc_close(hgc);
530 528 freecmdserveropts(&opts);
531 529 return exitcode;
532 530 }
@@ -1,631 +1,629 b''
1 1 # chgserver.py - command server extension for cHg
2 2 #
3 3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """command server extension for cHg (EXPERIMENTAL)
9 9
10 10 'S' channel (read/write)
11 11 propagate ui.system() request to client
12 12
13 13 'attachio' command
14 14 attach client's stdio passed by sendmsg()
15 15
16 16 'chdir' command
17 17 change current directory
18 18
19 19 'getpager' command
20 20 checks if pager is enabled and which pager should be executed
21 21
22 22 'setenv' command
23 23 replace os.environ completely
24 24
25 25 'setumask' command
26 26 set umask
27 27
28 28 'SIGHUP' signal
29 29 reload configuration files
30 30
31 31 Config
32 32 ------
33 33
34 34 ::
35 35
36 36 [chgserver]
37 37 idletimeout = 3600 # seconds, after which an idle server will exit
38 38 skiphash = False # whether to skip config or env change checks
39 39 """
40 40
41 41 from __future__ import absolute_import
42 42
43 43 import SocketServer
44 44 import errno
45 45 import inspect
46 46 import os
47 47 import re
48 48 import signal
49 49 import struct
50 50 import sys
51 51 import threading
52 52 import time
53 53 import traceback
54 54
55 55 from mercurial.i18n import _
56 56
57 57 from mercurial import (
58 58 cmdutil,
59 59 commands,
60 60 commandserver,
61 61 dispatch,
62 62 error,
63 63 extensions,
64 64 osutil,
65 65 util,
66 66 )
67 67
68 68 # Note for extension authors: ONLY specify testedwith = 'internal' for
69 69 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
70 70 # be specifying the version(s) of Mercurial they are tested with, or
71 71 # leave the attribute unspecified.
72 72 testedwith = 'internal'
73 73
74 74 _log = commandserver.log
75 75
76 76 def _hashlist(items):
77 77 """return sha1 hexdigest for a list"""
78 78 return util.sha1(str(items)).hexdigest()
79 79
80 80 # sensitive config sections affecting confighash
81 81 _configsections = ['extensions']
82 82
83 83 # sensitive environment variables affecting confighash
84 84 _envre = re.compile(r'''\A(?:
85 85 CHGHG
86 86 |HG.*
87 87 |LANG(?:UAGE)?
88 88 |LC_.*
89 89 |LD_.*
90 90 |PATH
91 91 |PYTHON.*
92 92 |TERM(?:INFO)?
93 93 |TZ
94 94 )\Z''', re.X)
95 95
96 96 def _confighash(ui):
97 97 """return a quick hash for detecting config/env changes
98 98
99 99 confighash is the hash of sensitive config items and environment variables.
100 100
101 101 for chgserver, it is designed that once confighash changes, the server is
102 102 not qualified to serve its client and should redirect the client to a new
103 103 server. different from mtimehash, confighash change will not mark the
104 104 server outdated and exit since the user can have different configs at the
105 105 same time.
106 106 """
107 107 sectionitems = []
108 108 for section in _configsections:
109 109 sectionitems.append(ui.configitems(section))
110 110 sectionhash = _hashlist(sectionitems)
111 111 envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
112 112 envhash = _hashlist(sorted(envitems))
113 113 return sectionhash[:6] + envhash[:6]
114 114
115 115 def _getmtimepaths(ui):
116 116 """get a list of paths that should be checked to detect change
117 117
118 118 The list will include:
119 119 - extensions (will not cover all files for complex extensions)
120 120 - mercurial/__version__.py
121 121 - python binary
122 122 """
123 123 modules = [m for n, m in extensions.extensions(ui)]
124 124 try:
125 125 from mercurial import __version__
126 126 modules.append(__version__)
127 127 except ImportError:
128 128 pass
129 129 files = [sys.executable]
130 130 for m in modules:
131 131 try:
132 132 files.append(inspect.getabsfile(m))
133 133 except TypeError:
134 134 pass
135 135 return sorted(set(files))
136 136
137 137 def _mtimehash(paths):
138 138 """return a quick hash for detecting file changes
139 139
140 140 mtimehash calls stat on given paths and calculate a hash based on size and
141 141 mtime of each file. mtimehash does not read file content because reading is
142 142 expensive. therefore it's not 100% reliable for detecting content changes.
143 143 it's possible to return different hashes for same file contents.
144 144 it's also possible to return a same hash for different file contents for
145 145 some carefully crafted situation.
146 146
147 147 for chgserver, it is designed that once mtimehash changes, the server is
148 148 considered outdated immediately and should no longer provide service.
149 149 """
150 150 def trystat(path):
151 151 try:
152 152 st = os.stat(path)
153 153 return (st.st_mtime, st.st_size)
154 154 except OSError:
155 155 # could be ENOENT, EPERM etc. not fatal in any case
156 156 pass
157 157 return _hashlist(map(trystat, paths))[:12]
158 158
159 159 class hashstate(object):
160 160 """a structure storing confighash, mtimehash, paths used for mtimehash"""
161 161 def __init__(self, confighash, mtimehash, mtimepaths):
162 162 self.confighash = confighash
163 163 self.mtimehash = mtimehash
164 164 self.mtimepaths = mtimepaths
165 165
166 166 @staticmethod
167 167 def fromui(ui, mtimepaths=None):
168 168 if mtimepaths is None:
169 169 mtimepaths = _getmtimepaths(ui)
170 170 confighash = _confighash(ui)
171 171 mtimehash = _mtimehash(mtimepaths)
172 172 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
173 173 return hashstate(confighash, mtimehash, mtimepaths)
174 174
175 175 # copied from hgext/pager.py:uisetup()
176 176 def _setuppagercmd(ui, options, cmd):
177 177 if not ui.formatted():
178 178 return
179 179
180 180 p = ui.config("pager", "pager", os.environ.get("PAGER"))
181 181 usepager = False
182 182 always = util.parsebool(options['pager'])
183 183 auto = options['pager'] == 'auto'
184 184
185 185 if not p:
186 186 pass
187 187 elif always:
188 188 usepager = True
189 189 elif not auto:
190 190 usepager = False
191 191 else:
192 192 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
193 193 attend = ui.configlist('pager', 'attend', attended)
194 194 ignore = ui.configlist('pager', 'ignore')
195 195 cmds, _ = cmdutil.findcmd(cmd, commands.table)
196 196
197 197 for cmd in cmds:
198 198 var = 'attend-%s' % cmd
199 199 if ui.config('pager', var):
200 200 usepager = ui.configbool('pager', var)
201 201 break
202 202 if (cmd in attend or
203 203 (cmd not in ignore and not attend)):
204 204 usepager = True
205 205 break
206 206
207 207 if usepager:
208 208 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
209 209 ui.setconfig('ui', 'interactive', False, 'pager')
210 210 return p
211 211
212 212 _envvarre = re.compile(r'\$[a-zA-Z_]+')
213 213
214 214 def _clearenvaliases(cmdtable):
215 215 """Remove stale command aliases referencing env vars; variable expansion
216 216 is done at dispatch.addaliases()"""
217 217 for name, tab in cmdtable.items():
218 218 cmddef = tab[0]
219 219 if (isinstance(cmddef, dispatch.cmdalias) and
220 220 not cmddef.definition.startswith('!') and # shell alias
221 221 _envvarre.search(cmddef.definition)):
222 222 del cmdtable[name]
223 223
224 224 def _newchgui(srcui, csystem):
225 225 class chgui(srcui.__class__):
226 226 def __init__(self, src=None):
227 227 super(chgui, self).__init__(src)
228 228 if src:
229 229 self._csystem = getattr(src, '_csystem', csystem)
230 230 else:
231 231 self._csystem = csystem
232 232
233 233 def system(self, cmd, environ=None, cwd=None, onerr=None,
234 234 errprefix=None):
235 235 # copied from mercurial/util.py:system()
236 236 self.flush()
237 237 def py2shell(val):
238 238 if val is None or val is False:
239 239 return '0'
240 240 if val is True:
241 241 return '1'
242 242 return str(val)
243 243 env = os.environ.copy()
244 244 if environ:
245 245 env.update((k, py2shell(v)) for k, v in environ.iteritems())
246 246 env['HG'] = util.hgexecutable()
247 247 rc = self._csystem(cmd, env, cwd)
248 248 if rc and onerr:
249 249 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
250 250 util.explainexit(rc)[0])
251 251 if errprefix:
252 252 errmsg = '%s: %s' % (errprefix, errmsg)
253 253 raise onerr(errmsg)
254 254 return rc
255 255
256 256 return chgui(srcui)
257 257
258 258 def _renewui(srcui, args=None):
259 259 if not args:
260 260 args = []
261 261
262 262 newui = srcui.__class__()
263 263 for a in ['fin', 'fout', 'ferr', 'environ']:
264 264 setattr(newui, a, getattr(srcui, a))
265 265 if util.safehasattr(srcui, '_csystem'):
266 266 newui._csystem = srcui._csystem
267 267
268 268 # load wd and repo config, copied from dispatch.py
269 269 cwds = dispatch._earlygetopt(['--cwd'], args)
270 270 cwd = cwds and os.path.realpath(cwds[-1]) or None
271 271 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
272 272 path, newui = dispatch._getlocal(newui, rpath, wd=cwd)
273 273
274 274 # internal config: extensions.chgserver
275 275 # copy it. it can only be overrided from command line.
276 276 newui.setconfig('extensions', 'chgserver',
277 277 srcui.config('extensions', 'chgserver'), '--config')
278 278
279 279 # command line args
280 280 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
281 281
282 282 # stolen from tortoisehg.util.copydynamicconfig()
283 283 for section, name, value in srcui.walkconfig():
284 284 source = srcui.configsource(section, name)
285 285 if ':' in source or source == '--config':
286 286 # path:line or command line
287 287 continue
288 288 if source == 'none':
289 289 # ui.configsource returns 'none' by default
290 290 source = ''
291 291 newui.setconfig(section, name, value, source)
292 292 return newui
293 293
294 294 class channeledsystem(object):
295 295 """Propagate ui.system() request in the following format:
296 296
297 297 payload length (unsigned int),
298 298 cmd, '\0',
299 299 cwd, '\0',
300 300 envkey, '=', val, '\0',
301 301 ...
302 302 envkey, '=', val
303 303
304 304 and waits:
305 305
306 306 exitcode length (unsigned int),
307 307 exitcode (int)
308 308 """
309 309 def __init__(self, in_, out, channel):
310 310 self.in_ = in_
311 311 self.out = out
312 312 self.channel = channel
313 313
314 314 def __call__(self, cmd, environ, cwd):
315 315 args = [util.quotecommand(cmd), cwd or '.']
316 316 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
317 317 data = '\0'.join(args)
318 318 self.out.write(struct.pack('>cI', self.channel, len(data)))
319 319 self.out.write(data)
320 320 self.out.flush()
321 321
322 322 length = self.in_.read(4)
323 323 length, = struct.unpack('>I', length)
324 324 if length != 4:
325 325 raise error.Abort(_('invalid response'))
326 326 rc, = struct.unpack('>i', self.in_.read(4))
327 327 return rc
328 328
329 329 _iochannels = [
330 330 # server.ch, ui.fp, mode
331 331 ('cin', 'fin', 'rb'),
332 332 ('cout', 'fout', 'wb'),
333 333 ('cerr', 'ferr', 'wb'),
334 334 ]
335 335
336 336 class chgcmdserver(commandserver.server):
337 337 def __init__(self, ui, repo, fin, fout, sock):
338 338 super(chgcmdserver, self).__init__(
339 339 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
340 340 self.clientsock = sock
341 341 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
342 342
343 343 def cleanup(self):
344 344 # dispatch._runcatch() does not flush outputs if exception is not
345 345 # handled by dispatch._dispatch()
346 346 self.ui.flush()
347 347 self._restoreio()
348 348
349 349 def attachio(self):
350 350 """Attach to client's stdio passed via unix domain socket; all
351 351 channels except cresult will no longer be used
352 352 """
353 353 # tell client to sendmsg() with 1-byte payload, which makes it
354 354 # distinctive from "attachio\n" command consumed by client.read()
355 355 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
356 356 clientfds = osutil.recvfds(self.clientsock.fileno())
357 357 _log('received fds: %r\n' % clientfds)
358 358
359 359 ui = self.ui
360 360 ui.flush()
361 361 first = self._saveio()
362 362 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
363 363 assert fd > 0
364 364 fp = getattr(ui, fn)
365 365 os.dup2(fd, fp.fileno())
366 366 os.close(fd)
367 367 if not first:
368 368 continue
369 369 # reset buffering mode when client is first attached. as we want
370 370 # to see output immediately on pager, the mode stays unchanged
371 371 # when client re-attached. ferr is unchanged because it should
372 372 # be unbuffered no matter if it is a tty or not.
373 373 if fn == 'ferr':
374 374 newfp = fp
375 375 else:
376 376 # make it line buffered explicitly because the default is
377 377 # decided on first write(), where fout could be a pager.
378 378 if fp.isatty():
379 379 bufsize = 1 # line buffered
380 380 else:
381 381 bufsize = -1 # system default
382 382 newfp = os.fdopen(fp.fileno(), mode, bufsize)
383 383 setattr(ui, fn, newfp)
384 384 setattr(self, cn, newfp)
385 385
386 386 self.cresult.write(struct.pack('>i', len(clientfds)))
387 387
388 388 def _saveio(self):
389 389 if self._oldios:
390 390 return False
391 391 ui = self.ui
392 392 for cn, fn, _mode in _iochannels:
393 393 ch = getattr(self, cn)
394 394 fp = getattr(ui, fn)
395 395 fd = os.dup(fp.fileno())
396 396 self._oldios.append((ch, fp, fd))
397 397 return True
398 398
399 399 def _restoreio(self):
400 400 ui = self.ui
401 401 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
402 402 newfp = getattr(ui, fn)
403 403 # close newfp while it's associated with client; otherwise it
404 404 # would be closed when newfp is deleted
405 405 if newfp is not fp:
406 406 newfp.close()
407 407 # restore original fd: fp is open again
408 408 os.dup2(fd, fp.fileno())
409 409 os.close(fd)
410 410 setattr(self, cn, ch)
411 411 setattr(ui, fn, fp)
412 412 del self._oldios[:]
413 413
414 414 def chdir(self):
415 415 """Change current directory
416 416
417 417 Note that the behavior of --cwd option is bit different from this.
418 418 It does not affect --config parameter.
419 419 """
420 420 path = self._readstr()
421 421 if not path:
422 422 return
423 423 _log('chdir to %r\n' % path)
424 424 os.chdir(path)
425 425
426 426 def setumask(self):
427 427 """Change umask"""
428 428 mask = struct.unpack('>I', self._read(4))[0]
429 429 _log('setumask %r\n' % mask)
430 430 os.umask(mask)
431 431
432 432 def getpager(self):
433 433 """Read cmdargs and write pager command to r-channel if enabled
434 434
435 435 If pager isn't enabled, this writes '\0' because channeledoutput
436 436 does not allow to write empty data.
437 437 """
438 438 args = self._readlist()
439 439 try:
440 440 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
441 441 args)
442 442 except (error.Abort, error.AmbiguousCommand, error.CommandError,
443 443 error.UnknownCommand):
444 444 cmd = None
445 445 options = {}
446 446 if not cmd or 'pager' not in options:
447 447 self.cresult.write('\0')
448 448 return
449 449
450 450 pagercmd = _setuppagercmd(self.ui, options, cmd)
451 451 if pagercmd:
452 452 self.cresult.write(pagercmd)
453 453 else:
454 454 self.cresult.write('\0')
455 455
456 456 def setenv(self):
457 457 """Clear and update os.environ
458 458
459 459 Note that not all variables can make an effect on the running process.
460 460 """
461 461 l = self._readlist()
462 462 try:
463 463 newenv = dict(s.split('=', 1) for s in l)
464 464 except ValueError:
465 465 raise ValueError('unexpected value in setenv request')
466 466
467 467 diffkeys = set(k for k in set(os.environ.keys() + newenv.keys())
468 468 if os.environ.get(k) != newenv.get(k))
469 469 _log('change env: %r\n' % sorted(diffkeys))
470 470
471 471 os.environ.clear()
472 472 os.environ.update(newenv)
473 473
474 474 if set(['HGPLAIN', 'HGPLAINEXCEPT']) & diffkeys:
475 475 # reload config so that ui.plain() takes effect
476 476 self.ui = _renewui(self.ui)
477 477
478 478 _clearenvaliases(commands.table)
479 479
480 480 capabilities = commandserver.server.capabilities.copy()
481 481 capabilities.update({'attachio': attachio,
482 482 'chdir': chdir,
483 483 'getpager': getpager,
484 484 'setenv': setenv,
485 485 'setumask': setumask})
486 486
487 487 # copied from mercurial/commandserver.py
488 488 class _requesthandler(SocketServer.StreamRequestHandler):
489 489 def handle(self):
490 490 # use a different process group from the master process, making this
491 491 # process pass kernel "is_current_pgrp_orphaned" check so signals like
492 492 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
493 493 os.setpgid(0, 0)
494 494 ui = self.server.ui
495 495 repo = self.server.repo
496 496 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection)
497 497 try:
498 498 try:
499 499 sv.serve()
500 500 # handle exceptions that may be raised by command server. most of
501 501 # known exceptions are caught by dispatch.
502 502 except error.Abort as inst:
503 503 ui.warn(_('abort: %s\n') % inst)
504 504 except IOError as inst:
505 505 if inst.errno != errno.EPIPE:
506 506 raise
507 507 except KeyboardInterrupt:
508 508 pass
509 509 finally:
510 510 sv.cleanup()
511 511 except: # re-raises
512 512 # also write traceback to error channel. otherwise client cannot
513 513 # see it because it is written to server's stderr by default.
514 514 traceback.print_exc(file=sv.cerr)
515 515 raise
516 516
517 517 def _tempaddress(address):
518 518 return '%s.%d.tmp' % (address, os.getpid())
519 519
520 520 def _hashaddress(address, hashstr):
521 521 return '%s-%s' % (address, hashstr)
522 522
523 523 class AutoExitMixIn: # use old-style to comply with SocketServer design
524 524 lastactive = time.time()
525 525 idletimeout = 3600 # default 1 hour
526 526
527 527 def startautoexitthread(self):
528 528 # note: the auto-exit check here is cheap enough to not use a thread,
529 529 # be done in serve_forever. however SocketServer is hook-unfriendly,
530 530 # you simply cannot hook serve_forever without copying a lot of code.
531 531 # besides, serve_forever's docstring suggests using thread.
532 532 thread = threading.Thread(target=self._autoexitloop)
533 533 thread.daemon = True
534 534 thread.start()
535 535
536 536 def _autoexitloop(self, interval=1):
537 537 while True:
538 538 time.sleep(interval)
539 539 if not self.issocketowner():
540 540 _log('%s is not owned, exiting.\n' % self.server_address)
541 541 break
542 542 if time.time() - self.lastactive > self.idletimeout:
543 543 _log('being idle too long. exiting.\n')
544 544 break
545 545 self.shutdown()
546 546
547 547 def process_request(self, request, address):
548 548 self.lastactive = time.time()
549 549 return SocketServer.ForkingMixIn.process_request(
550 550 self, request, address)
551 551
552 552 def server_bind(self):
553 553 # use a unique temp address so we can stat the file and do ownership
554 554 # check later
555 555 tempaddress = _tempaddress(self.server_address)
556 556 self.socket.bind(tempaddress)
557 557 self._socketstat = os.stat(tempaddress)
558 558 # rename will replace the old socket file if exists atomically. the
559 559 # old server will detect ownership change and exit.
560 560 util.rename(tempaddress, self.server_address)
561 561
562 562 def issocketowner(self):
563 563 try:
564 564 stat = os.stat(self.server_address)
565 565 return (stat.st_ino == self._socketstat.st_ino and
566 566 stat.st_mtime == self._socketstat.st_mtime)
567 567 except OSError:
568 568 return False
569 569
570 570 def unlinksocketfile(self):
571 571 if not self.issocketowner():
572 572 return
573 573 # it is possible to have a race condition here that we may
574 574 # remove another server's socket file. but that's okay
575 575 # since that server will detect and exit automatically and
576 576 # the client will start a new server on demand.
577 577 try:
578 578 os.unlink(self.server_address)
579 579 except OSError as exc:
580 580 if exc.errno != errno.ENOENT:
581 581 raise
582 582
583 583 class chgunixservice(commandserver.unixservice):
584 584 def init(self):
585 # drop options set for "hg serve --cmdserver" command
586 self.ui.setconfig('progress', 'assume-tty', None)
587 585 signal.signal(signal.SIGHUP, self._reloadconfig)
588 586 self._inithashstate()
589 587 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
590 588 SocketServer.UnixStreamServer):
591 589 ui = self.ui
592 590 repo = self.repo
593 591 self.server = cls(self.address, _requesthandler)
594 592 self.server.idletimeout = self.ui.configint(
595 593 'chgserver', 'idletimeout', self.server.idletimeout)
596 594 self.server.startautoexitthread()
597 595 self._createsymlink()
598 596 # avoid writing "listening at" message to stdout before attachio
599 597 # request, which calls setvbuf()
600 598
601 599 def _inithashstate(self):
602 600 self.baseaddress = self.address
603 601 if self.ui.configbool('chgserver', 'skiphash', False):
604 602 self.hashstate = None
605 603 return
606 604 self.hashstate = hashstate.fromui(self.ui)
607 605 self.address = _hashaddress(self.address, self.hashstate.confighash)
608 606
609 607 def _createsymlink(self):
610 608 if self.baseaddress == self.address:
611 609 return
612 610 tempaddress = _tempaddress(self.baseaddress)
613 611 os.symlink(self.address, tempaddress)
614 612 util.rename(tempaddress, self.baseaddress)
615 613
616 614 def _reloadconfig(self, signum, frame):
617 615 self.ui = self.server.ui = _renewui(self.ui)
618 616
619 617 def run(self):
620 618 try:
621 619 self.server.serve_forever()
622 620 finally:
623 621 self.server.unlinksocketfile()
624 622
625 623 def uisetup(ui):
626 624 commandserver._servicemap['chgunix'] = chgunixservice
627 625
628 626 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
629 627 # start another chg. drop it to avoid possible side effects.
630 628 if 'CHGINTERNALMARK' in os.environ:
631 629 del os.environ['CHGINTERNALMARK']
General Comments 0
You need to be logged in to leave comments. Login now