##// END OF EJS Templates
chg: force-set LC_CTYPE on server start to actual value from the environment...
Kyle Lippincott -
r44733:04a3ae7a default
parent child Browse files
Show More
@@ -1,456 +1,466
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 "procutil.h"
27 27 #include "util.h"
28 28
29 29 #ifndef PATH_MAX
30 30 #define PATH_MAX 4096
31 31 #endif
32 32
33 33 struct cmdserveropts {
34 34 char sockname[PATH_MAX];
35 35 char initsockname[PATH_MAX];
36 36 char redirectsockname[PATH_MAX];
37 37 size_t argsize;
38 38 const char **args;
39 39 };
40 40
41 41 static void initcmdserveropts(struct cmdserveropts *opts)
42 42 {
43 43 memset(opts, 0, sizeof(struct cmdserveropts));
44 44 }
45 45
46 46 static void freecmdserveropts(struct cmdserveropts *opts)
47 47 {
48 48 free(opts->args);
49 49 opts->args = NULL;
50 50 opts->argsize = 0;
51 51 }
52 52
53 53 /*
54 54 * Test if an argument is a sensitive flag that should be passed to the server.
55 55 * Return 0 if not, otherwise the number of arguments starting from the current
56 56 * one that should be passed to the server.
57 57 */
58 58 static size_t testsensitiveflag(const char *arg)
59 59 {
60 60 static const struct {
61 61 const char *name;
62 62 size_t narg;
63 63 } flags[] = {
64 64 {"--config", 1}, {"--cwd", 1}, {"--repo", 1},
65 65 {"--repository", 1}, {"--traceback", 0}, {"-R", 1},
66 66 };
67 67 size_t i;
68 68 for (i = 0; i < sizeof(flags) / sizeof(flags[0]); ++i) {
69 69 size_t len = strlen(flags[i].name);
70 70 size_t narg = flags[i].narg;
71 71 if (memcmp(arg, flags[i].name, len) == 0) {
72 72 if (arg[len] == '\0') {
73 73 /* --flag (value) */
74 74 return narg + 1;
75 75 } else if (arg[len] == '=' && narg > 0) {
76 76 /* --flag=value */
77 77 return 1;
78 78 } else if (flags[i].name[1] != '-') {
79 79 /* 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, int argc,
91 91 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 =
104 104 reallocx(opts->args, (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 abortmsgerrno("cannot create sockdir %s", sockdir);
118 118
119 119 struct stat st;
120 120 r = lstat(sockdir, &st);
121 121 if (r < 0)
122 122 abortmsgerrno("cannot stat %s", sockdir);
123 123 if (!S_ISDIR(st.st_mode))
124 124 abortmsg("cannot create sockdir %s (file exists)", sockdir);
125 125 if (st.st_uid != geteuid() || st.st_mode & 0077)
126 126 abortmsg("insecure sockdir %s", sockdir);
127 127 }
128 128
129 129 /*
130 130 * Check if a socket directory exists and is only owned by the current user.
131 131 * Return 1 if so, 0 if not. This is used to check if XDG_RUNTIME_DIR can be
132 132 * used or not. According to the specification [1], XDG_RUNTIME_DIR should be
133 133 * ignored if the directory is not owned by the user with mode 0700.
134 134 * [1]: https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
135 135 */
136 136 static int checkruntimedir(const char *sockdir)
137 137 {
138 138 struct stat st;
139 139 int r = lstat(sockdir, &st);
140 140 if (r < 0) /* ex. does not exist */
141 141 return 0;
142 142 if (!S_ISDIR(st.st_mode)) /* ex. is a file, not a directory */
143 143 return 0;
144 144 return st.st_uid == geteuid() && (st.st_mode & 0777) == 0700;
145 145 }
146 146
147 147 static void getdefaultsockdir(char sockdir[], size_t size)
148 148 {
149 149 /* by default, put socket file in secure directory
150 150 * (${XDG_RUNTIME_DIR}/chg, or /${TMPDIR:-tmp}/chg$UID)
151 151 * (permission of socket file may be ignored on some Unices) */
152 152 const char *runtimedir = getenv("XDG_RUNTIME_DIR");
153 153 int r;
154 154 if (runtimedir && checkruntimedir(runtimedir)) {
155 155 r = snprintf(sockdir, size, "%s/chg", runtimedir);
156 156 } else {
157 157 const char *tmpdir = getenv("TMPDIR");
158 158 if (!tmpdir)
159 159 tmpdir = "/tmp";
160 160 r = snprintf(sockdir, size, "%s/chg%d", tmpdir, geteuid());
161 161 }
162 162 if (r < 0 || (size_t)r >= size)
163 163 abortmsg("too long TMPDIR (r = %d)", r);
164 164 }
165 165
166 166 static void setcmdserveropts(struct cmdserveropts *opts)
167 167 {
168 168 int r;
169 169 char sockdir[PATH_MAX];
170 170 const char *envsockname = getenv("CHGSOCKNAME");
171 171 if (!envsockname) {
172 172 getdefaultsockdir(sockdir, sizeof(sockdir));
173 173 preparesockdir(sockdir);
174 174 }
175 175
176 176 const char *basename = (envsockname) ? envsockname : sockdir;
177 177 const char *sockfmt = (envsockname) ? "%s" : "%s/server";
178 178 r = snprintf(opts->sockname, sizeof(opts->sockname), sockfmt, basename);
179 179 if (r < 0 || (size_t)r >= sizeof(opts->sockname))
180 180 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
181 181 r = snprintf(opts->initsockname, sizeof(opts->initsockname), "%s.%u",
182 182 opts->sockname, (unsigned)getpid());
183 183 if (r < 0 || (size_t)r >= sizeof(opts->initsockname))
184 184 abortmsg("too long TMPDIR or CHGSOCKNAME (r = %d)", r);
185 185 }
186 186
187 187 static const char *gethgcmd(void)
188 188 {
189 189 static const char *hgcmd = NULL;
190 190 if (!hgcmd) {
191 191 hgcmd = getenv("CHGHG");
192 192 if (!hgcmd || hgcmd[0] == '\0')
193 193 hgcmd = getenv("HG");
194 194 if (!hgcmd || hgcmd[0] == '\0')
195 195 #ifdef HGPATH
196 196 hgcmd = (HGPATH);
197 197 #else
198 198 hgcmd = "hg";
199 199 #endif
200 200 }
201 201 return hgcmd;
202 202 }
203 203
204 204 static void execcmdserver(const struct cmdserveropts *opts)
205 205 {
206 206 const char *hgcmd = gethgcmd();
207 207
208 208 const char *baseargv[] = {
209 209 hgcmd,
210 210 "serve",
211 211 "--cmdserver",
212 212 "chgunix",
213 213 "--address",
214 214 opts->initsockname,
215 215 "--daemon-postexec",
216 216 "chdir:/",
217 217 };
218 218 size_t baseargvsize = sizeof(baseargv) / sizeof(baseargv[0]);
219 219 size_t argsize = baseargvsize + opts->argsize + 1;
220 220
221 221 const char **argv = mallocx(sizeof(char *) * argsize);
222 222 memcpy(argv, baseargv, sizeof(baseargv));
223 223 if (opts->args) {
224 224 size_t size = sizeof(char *) * opts->argsize;
225 225 memcpy(argv + baseargvsize, opts->args, size);
226 226 }
227 227 argv[argsize - 1] = NULL;
228 228
229 const char *lc_ctype_env = getenv("LC_CTYPE");
230 if (lc_ctype_env == NULL) {
231 if (putenv("CHG_CLEAR_LC_CTYPE=") != 0)
232 abortmsgerrno("failed to putenv CHG_CLEAR_LC_CTYPE");
233 } else {
234 if (setenv("CHGORIG_LC_CTYPE", lc_ctype_env, 1) != 0) {
235 abortmsgerrno("failed to setenv CHGORIG_LC_CTYYPE");
236 }
237 }
238
229 239 if (putenv("CHGINTERNALMARK=") != 0)
230 240 abortmsgerrno("failed to putenv");
231 241 if (execvp(hgcmd, (char **)argv) < 0)
232 242 abortmsgerrno("failed to exec cmdserver");
233 243 free(argv);
234 244 }
235 245
236 246 /* Retry until we can connect to the server. Give up after some time. */
237 247 static hgclient_t *retryconnectcmdserver(struct cmdserveropts *opts, pid_t pid)
238 248 {
239 249 static const struct timespec sleepreq = {0, 10 * 1000000};
240 250 int pst = 0;
241 251
242 252 debugmsg("try connect to %s repeatedly", opts->initsockname);
243 253
244 254 unsigned int timeoutsec = 60; /* default: 60 seconds */
245 255 const char *timeoutenv = getenv("CHGTIMEOUT");
246 256 if (timeoutenv)
247 257 sscanf(timeoutenv, "%u", &timeoutsec);
248 258
249 259 for (unsigned int i = 0; !timeoutsec || i < timeoutsec * 100; i++) {
250 260 hgclient_t *hgc = hgc_open(opts->initsockname);
251 261 if (hgc) {
252 262 debugmsg("rename %s to %s", opts->initsockname,
253 263 opts->sockname);
254 264 int r = rename(opts->initsockname, opts->sockname);
255 265 if (r != 0)
256 266 abortmsgerrno("cannot rename");
257 267 return hgc;
258 268 }
259 269
260 270 if (pid > 0) {
261 271 /* collect zombie if child process fails to start */
262 272 int r = waitpid(pid, &pst, WNOHANG);
263 273 if (r != 0)
264 274 goto cleanup;
265 275 }
266 276
267 277 nanosleep(&sleepreq, NULL);
268 278 }
269 279
270 280 abortmsg("timed out waiting for cmdserver %s", opts->initsockname);
271 281 return NULL;
272 282
273 283 cleanup:
274 284 if (WIFEXITED(pst)) {
275 285 if (WEXITSTATUS(pst) == 0)
276 286 abortmsg("could not connect to cmdserver "
277 287 "(exited with status 0)");
278 288 debugmsg("cmdserver exited with status %d", WEXITSTATUS(pst));
279 289 exit(WEXITSTATUS(pst));
280 290 } else if (WIFSIGNALED(pst)) {
281 291 abortmsg("cmdserver killed by signal %d", WTERMSIG(pst));
282 292 } else {
283 293 abortmsg("error while waiting for cmdserver");
284 294 }
285 295 return NULL;
286 296 }
287 297
288 298 /* Connect to a cmdserver. Will start a new server on demand. */
289 299 static hgclient_t *connectcmdserver(struct cmdserveropts *opts)
290 300 {
291 301 const char *sockname =
292 302 opts->redirectsockname[0] ? opts->redirectsockname : opts->sockname;
293 303 debugmsg("try connect to %s", sockname);
294 304 hgclient_t *hgc = hgc_open(sockname);
295 305 if (hgc)
296 306 return hgc;
297 307
298 308 /* prevent us from being connected to an outdated server: we were
299 309 * told by a server to redirect to opts->redirectsockname and that
300 310 * address does not work. we do not want to connect to the server
301 311 * again because it will probably tell us the same thing. */
302 312 if (sockname == opts->redirectsockname)
303 313 unlink(opts->sockname);
304 314
305 315 debugmsg("start cmdserver at %s", opts->initsockname);
306 316
307 317 pid_t pid = fork();
308 318 if (pid < 0)
309 319 abortmsg("failed to fork cmdserver process");
310 320 if (pid == 0) {
311 321 execcmdserver(opts);
312 322 } else {
313 323 hgc = retryconnectcmdserver(opts, pid);
314 324 }
315 325
316 326 return hgc;
317 327 }
318 328
319 329 static void killcmdserver(const struct cmdserveropts *opts)
320 330 {
321 331 /* resolve config hash */
322 332 char *resolvedpath = realpath(opts->sockname, NULL);
323 333 if (resolvedpath) {
324 334 unlink(resolvedpath);
325 335 free(resolvedpath);
326 336 }
327 337 }
328 338
329 339 /* Run instructions sent from the server like unlink and set redirect path
330 340 * Return 1 if reconnect is needed, otherwise 0 */
331 341 static int runinstructions(struct cmdserveropts *opts, const char **insts)
332 342 {
333 343 int needreconnect = 0;
334 344 if (!insts)
335 345 return needreconnect;
336 346
337 347 assert(insts);
338 348 opts->redirectsockname[0] = '\0';
339 349 const char **pinst;
340 350 for (pinst = insts; *pinst; pinst++) {
341 351 debugmsg("instruction: %s", *pinst);
342 352 if (strncmp(*pinst, "unlink ", 7) == 0) {
343 353 unlink(*pinst + 7);
344 354 } else if (strncmp(*pinst, "redirect ", 9) == 0) {
345 355 int r = snprintf(opts->redirectsockname,
346 356 sizeof(opts->redirectsockname), "%s",
347 357 *pinst + 9);
348 358 if (r < 0 || r >= (int)sizeof(opts->redirectsockname))
349 359 abortmsg("redirect path is too long (%d)", r);
350 360 needreconnect = 1;
351 361 } else if (strncmp(*pinst, "exit ", 5) == 0) {
352 362 int n = 0;
353 363 if (sscanf(*pinst + 5, "%d", &n) != 1)
354 364 abortmsg("cannot read the exit code");
355 365 exit(n);
356 366 } else if (strcmp(*pinst, "reconnect") == 0) {
357 367 needreconnect = 1;
358 368 } else {
359 369 abortmsg("unknown instruction: %s", *pinst);
360 370 }
361 371 }
362 372 return needreconnect;
363 373 }
364 374
365 375 /*
366 376 * Test whether the command is unsupported or not. This is not designed to
367 377 * cover all cases. But it's fast, does not depend on the server and does
368 378 * not return false positives.
369 379 */
370 380 static int isunsupported(int argc, const char *argv[])
371 381 {
372 382 enum { SERVE = 1,
373 383 DAEMON = 2,
374 384 SERVEDAEMON = SERVE | DAEMON,
375 385 };
376 386 unsigned int state = 0;
377 387 int i;
378 388 for (i = 0; i < argc; ++i) {
379 389 if (strcmp(argv[i], "--") == 0)
380 390 break;
381 391 if (i == 0 && strcmp("serve", argv[i]) == 0)
382 392 state |= SERVE;
383 393 else if (strcmp("-d", argv[i]) == 0 ||
384 394 strcmp("--daemon", argv[i]) == 0)
385 395 state |= DAEMON;
386 396 }
387 397 return (state & SERVEDAEMON) == SERVEDAEMON;
388 398 }
389 399
390 400 static void execoriginalhg(const char *argv[])
391 401 {
392 402 debugmsg("execute original hg");
393 403 if (execvp(gethgcmd(), (char **)argv) < 0)
394 404 abortmsgerrno("failed to exec original hg");
395 405 }
396 406
397 407 int main(int argc, const char *argv[], const char *envp[])
398 408 {
399 409 if (getenv("CHGDEBUG"))
400 410 enabledebugmsg();
401 411
402 412 if (!getenv("HGPLAIN") && isatty(fileno(stderr)))
403 413 enablecolor();
404 414
405 415 if (getenv("CHGINTERNALMARK"))
406 416 abortmsg("chg started by chg detected.\n"
407 417 "Please make sure ${HG:-hg} is not a symlink or "
408 418 "wrapper to chg. Alternatively, set $CHGHG to the "
409 419 "path of real hg.");
410 420
411 421 if (isunsupported(argc - 1, argv + 1))
412 422 execoriginalhg(argv);
413 423
414 424 struct cmdserveropts opts;
415 425 initcmdserveropts(&opts);
416 426 setcmdserveropts(&opts);
417 427 setcmdserverargs(&opts, argc, argv);
418 428
419 429 if (argc == 2) {
420 430 if (strcmp(argv[1], "--kill-chg-daemon") == 0) {
421 431 killcmdserver(&opts);
422 432 return 0;
423 433 }
424 434 }
425 435
426 436 hgclient_t *hgc;
427 437 size_t retry = 0;
428 438 while (1) {
429 439 hgc = connectcmdserver(&opts);
430 440 if (!hgc)
431 441 abortmsg("cannot open hg client");
432 442 hgc_setenv(hgc, envp);
433 443 const char **insts = hgc_validate(hgc, argv + 1, argc - 1);
434 444 int needreconnect = runinstructions(&opts, insts);
435 445 free(insts);
436 446 if (!needreconnect)
437 447 break;
438 448 hgc_close(hgc);
439 449 if (++retry > 10)
440 450 abortmsg("too many redirections.\n"
441 451 "Please make sure %s is not a wrapper which "
442 452 "changes sensitive environment variables "
443 453 "before executing hg. If you have to use a "
444 454 "wrapper, wrap chg instead of hg.",
445 455 gethgcmd());
446 456 }
447 457
448 458 setupsignalhandler(hgc_peerpid(hgc), hgc_peerpgid(hgc));
449 459 atexit(waitpager);
450 460 int exitcode = hgc_runcommand(hgc, argv + 1, argc - 1);
451 461 restoresignalhandler();
452 462 hgc_close(hgc);
453 463 freecmdserveropts(&opts);
454 464
455 465 return exitcode;
456 466 }
@@ -1,738 +1,714
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
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 'setenv' command
20 20 replace os.environ completely
21 21
22 22 'setumask' command (DEPRECATED)
23 23 'setumask2' command
24 24 set umask
25 25
26 26 'validate' command
27 27 reload the config and check if the server is up to date
28 28
29 29 Config
30 30 ------
31 31
32 32 ::
33 33
34 34 [chgserver]
35 35 # how long (in seconds) should an idle chg server exit
36 36 idletimeout = 3600
37 37
38 38 # whether to skip config or env change checks
39 39 skiphash = False
40 40 """
41 41
42 42 from __future__ import absolute_import
43 43
44 44 import inspect
45 45 import os
46 46 import re
47 47 import socket
48 48 import stat
49 49 import struct
50 50 import time
51 51
52 52 from .i18n import _
53 53 from .pycompat import (
54 54 getattr,
55 55 setattr,
56 56 )
57 57
58 58 from . import (
59 59 commandserver,
60 60 encoding,
61 61 error,
62 62 extensions,
63 63 node,
64 64 pycompat,
65 65 util,
66 66 )
67 67
68 68 from .utils import (
69 69 hashutil,
70 70 procutil,
71 71 stringutil,
72 72 )
73 73
74 74
75 75 def _hashlist(items):
76 76 """return sha1 hexdigest for a list"""
77 77 return node.hex(hashutil.sha1(stringutil.pprint(items)).digest())
78 78
79 79
80 80 # sensitive config sections affecting confighash
81 81 _configsections = [
82 82 b'alias', # affects global state commands.table
83 83 b'eol', # uses setconfig('eol', ...)
84 84 b'extdiff', # uisetup will register new commands
85 85 b'extensions',
86 86 ]
87 87
88 88 _configsectionitems = [
89 89 (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup
90 90 ]
91 91
92 92 # sensitive environment variables affecting confighash
93 93 _envre = re.compile(
94 94 br'''\A(?:
95 95 CHGHG
96 96 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
97 97 |HG(?:ENCODING|PLAIN).*
98 98 |LANG(?:UAGE)?
99 99 |LC_.*
100 100 |LD_.*
101 101 |PATH
102 102 |PYTHON.*
103 103 |TERM(?:INFO)?
104 104 |TZ
105 105 )\Z''',
106 106 re.X,
107 107 )
108 108
109 109
110 110 def _confighash(ui):
111 111 """return a quick hash for detecting config/env changes
112 112
113 113 confighash is the hash of sensitive config items and environment variables.
114 114
115 115 for chgserver, it is designed that once confighash changes, the server is
116 116 not qualified to serve its client and should redirect the client to a new
117 117 server. different from mtimehash, confighash change will not mark the
118 118 server outdated and exit since the user can have different configs at the
119 119 same time.
120 120 """
121 121 sectionitems = []
122 122 for section in _configsections:
123 123 sectionitems.append(ui.configitems(section))
124 124 for section, item in _configsectionitems:
125 125 sectionitems.append(ui.config(section, item))
126 126 sectionhash = _hashlist(sectionitems)
127 127 # If $CHGHG is set, the change to $HG should not trigger a new chg server
128 128 if b'CHGHG' in encoding.environ:
129 129 ignored = {b'HG'}
130 130 else:
131 131 ignored = set()
132 132 envitems = [
133 133 (k, v)
134 134 for k, v in pycompat.iteritems(encoding.environ)
135 135 if _envre.match(k) and k not in ignored
136 136 ]
137 137 envhash = _hashlist(sorted(envitems))
138 138 return sectionhash[:6] + envhash[:6]
139 139
140 140
141 141 def _getmtimepaths(ui):
142 142 """get a list of paths that should be checked to detect change
143 143
144 144 The list will include:
145 145 - extensions (will not cover all files for complex extensions)
146 146 - mercurial/__version__.py
147 147 - python binary
148 148 """
149 149 modules = [m for n, m in extensions.extensions(ui)]
150 150 try:
151 151 from . import __version__
152 152
153 153 modules.append(__version__)
154 154 except ImportError:
155 155 pass
156 156 files = []
157 157 if pycompat.sysexecutable:
158 158 files.append(pycompat.sysexecutable)
159 159 for m in modules:
160 160 try:
161 161 files.append(pycompat.fsencode(inspect.getabsfile(m)))
162 162 except TypeError:
163 163 pass
164 164 return sorted(set(files))
165 165
166 166
167 167 def _mtimehash(paths):
168 168 """return a quick hash for detecting file changes
169 169
170 170 mtimehash calls stat on given paths and calculate a hash based on size and
171 171 mtime of each file. mtimehash does not read file content because reading is
172 172 expensive. therefore it's not 100% reliable for detecting content changes.
173 173 it's possible to return different hashes for same file contents.
174 174 it's also possible to return a same hash for different file contents for
175 175 some carefully crafted situation.
176 176
177 177 for chgserver, it is designed that once mtimehash changes, the server is
178 178 considered outdated immediately and should no longer provide service.
179 179
180 180 mtimehash is not included in confighash because we only know the paths of
181 181 extensions after importing them (there is imp.find_module but that faces
182 182 race conditions). We need to calculate confighash without importing.
183 183 """
184 184
185 185 def trystat(path):
186 186 try:
187 187 st = os.stat(path)
188 188 return (st[stat.ST_MTIME], st.st_size)
189 189 except OSError:
190 190 # could be ENOENT, EPERM etc. not fatal in any case
191 191 pass
192 192
193 193 return _hashlist(pycompat.maplist(trystat, paths))[:12]
194 194
195 195
196 196 class hashstate(object):
197 197 """a structure storing confighash, mtimehash, paths used for mtimehash"""
198 198
199 199 def __init__(self, confighash, mtimehash, mtimepaths):
200 200 self.confighash = confighash
201 201 self.mtimehash = mtimehash
202 202 self.mtimepaths = mtimepaths
203 203
204 204 @staticmethod
205 205 def fromui(ui, mtimepaths=None):
206 206 if mtimepaths is None:
207 207 mtimepaths = _getmtimepaths(ui)
208 208 confighash = _confighash(ui)
209 209 mtimehash = _mtimehash(mtimepaths)
210 210 ui.log(
211 211 b'cmdserver',
212 212 b'confighash = %s mtimehash = %s\n',
213 213 confighash,
214 214 mtimehash,
215 215 )
216 216 return hashstate(confighash, mtimehash, mtimepaths)
217 217
218 218
219 219 def _newchgui(srcui, csystem, attachio):
220 220 class chgui(srcui.__class__):
221 221 def __init__(self, src=None):
222 222 super(chgui, self).__init__(src)
223 223 if src:
224 224 self._csystem = getattr(src, '_csystem', csystem)
225 225 else:
226 226 self._csystem = csystem
227 227
228 228 def _runsystem(self, cmd, environ, cwd, out):
229 229 # fallback to the original system method if
230 230 # a. the output stream is not stdout (e.g. stderr, cStringIO),
231 231 # b. or stdout is redirected by protectfinout(),
232 232 # because the chg client is not aware of these situations and
233 233 # will behave differently (i.e. write to stdout).
234 234 if (
235 235 out is not self.fout
236 236 or not util.safehasattr(self.fout, b'fileno')
237 237 or self.fout.fileno() != procutil.stdout.fileno()
238 238 or self._finoutredirected
239 239 ):
240 240 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
241 241 self.flush()
242 242 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
243 243
244 244 def _runpager(self, cmd, env=None):
245 245 self._csystem(
246 246 cmd,
247 247 procutil.shellenviron(env),
248 248 type=b'pager',
249 249 cmdtable={b'attachio': attachio},
250 250 )
251 251 return True
252 252
253 253 return chgui(srcui)
254 254
255 255
256 256 def _loadnewui(srcui, args, cdebug):
257 257 from . import dispatch # avoid cycle
258 258
259 259 newui = srcui.__class__.load()
260 260 for a in [b'fin', b'fout', b'ferr', b'environ']:
261 261 setattr(newui, a, getattr(srcui, a))
262 262 if util.safehasattr(srcui, b'_csystem'):
263 263 newui._csystem = srcui._csystem
264 264
265 265 # command line args
266 266 options = dispatch._earlyparseopts(newui, args)
267 267 dispatch._parseconfig(newui, options[b'config'])
268 268
269 269 # stolen from tortoisehg.util.copydynamicconfig()
270 270 for section, name, value in srcui.walkconfig():
271 271 source = srcui.configsource(section, name)
272 272 if b':' in source or source == b'--config' or source.startswith(b'$'):
273 273 # path:line or command line, or environ
274 274 continue
275 275 newui.setconfig(section, name, value, source)
276 276
277 277 # load wd and repo config, copied from dispatch.py
278 278 cwd = options[b'cwd']
279 279 cwd = cwd and os.path.realpath(cwd) or None
280 280 rpath = options[b'repository']
281 281 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
282 282
283 283 extensions.populateui(newui)
284 284 commandserver.setuplogging(newui, fp=cdebug)
285 285 if newui is not newlui:
286 286 extensions.populateui(newlui)
287 287 commandserver.setuplogging(newlui, fp=cdebug)
288 288
289 289 return (newui, newlui)
290 290
291 291
292 292 class channeledsystem(object):
293 293 """Propagate ui.system() request in the following format:
294 294
295 295 payload length (unsigned int),
296 296 type, '\0',
297 297 cmd, '\0',
298 298 cwd, '\0',
299 299 envkey, '=', val, '\0',
300 300 ...
301 301 envkey, '=', val
302 302
303 303 if type == 'system', waits for:
304 304
305 305 exitcode length (unsigned int),
306 306 exitcode (int)
307 307
308 308 if type == 'pager', repetitively waits for a command name ending with '\n'
309 309 and executes it defined by cmdtable, or exits the loop if the command name
310 310 is empty.
311 311 """
312 312
313 313 def __init__(self, in_, out, channel):
314 314 self.in_ = in_
315 315 self.out = out
316 316 self.channel = channel
317 317
318 318 def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
319 319 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or b'.')]
320 320 args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
321 321 data = b'\0'.join(args)
322 322 self.out.write(struct.pack(b'>cI', self.channel, len(data)))
323 323 self.out.write(data)
324 324 self.out.flush()
325 325
326 326 if type == b'system':
327 327 length = self.in_.read(4)
328 328 (length,) = struct.unpack(b'>I', length)
329 329 if length != 4:
330 330 raise error.Abort(_(b'invalid response'))
331 331 (rc,) = struct.unpack(b'>i', self.in_.read(4))
332 332 return rc
333 333 elif type == b'pager':
334 334 while True:
335 335 cmd = self.in_.readline()[:-1]
336 336 if not cmd:
337 337 break
338 338 if cmdtable and cmd in cmdtable:
339 339 cmdtable[cmd]()
340 340 else:
341 341 raise error.Abort(_(b'unexpected command: %s') % cmd)
342 342 else:
343 343 raise error.ProgrammingError(b'invalid S channel type: %s' % type)
344 344
345 345
346 346 _iochannels = [
347 347 # server.ch, ui.fp, mode
348 348 (b'cin', b'fin', 'rb'),
349 349 (b'cout', b'fout', 'wb'),
350 350 (b'cerr', b'ferr', 'wb'),
351 351 ]
352 352
353 353
354 354 class chgcmdserver(commandserver.server):
355 355 def __init__(
356 356 self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
357 357 ):
358 358 super(chgcmdserver, self).__init__(
359 359 _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
360 360 repo,
361 361 fin,
362 362 fout,
363 363 prereposetups,
364 364 )
365 365 self.clientsock = sock
366 366 self._ioattached = False
367 367 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
368 368 self.hashstate = hashstate
369 369 self.baseaddress = baseaddress
370 370 if hashstate is not None:
371 371 self.capabilities = self.capabilities.copy()
372 372 self.capabilities[b'validate'] = chgcmdserver.validate
373 373
374 374 def cleanup(self):
375 375 super(chgcmdserver, self).cleanup()
376 376 # dispatch._runcatch() does not flush outputs if exception is not
377 377 # handled by dispatch._dispatch()
378 378 self.ui.flush()
379 379 self._restoreio()
380 380 self._ioattached = False
381 381
382 382 def attachio(self):
383 383 """Attach to client's stdio passed via unix domain socket; all
384 384 channels except cresult will no longer be used
385 385 """
386 386 # tell client to sendmsg() with 1-byte payload, which makes it
387 387 # distinctive from "attachio\n" command consumed by client.read()
388 388 self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
389 389 clientfds = util.recvfds(self.clientsock.fileno())
390 390 self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
391 391
392 392 ui = self.ui
393 393 ui.flush()
394 394 self._saveio()
395 395 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
396 396 assert fd > 0
397 397 fp = getattr(ui, fn)
398 398 os.dup2(fd, fp.fileno())
399 399 os.close(fd)
400 400 if self._ioattached:
401 401 continue
402 402 # reset buffering mode when client is first attached. as we want
403 403 # to see output immediately on pager, the mode stays unchanged
404 404 # when client re-attached. ferr is unchanged because it should
405 405 # be unbuffered no matter if it is a tty or not.
406 406 if fn == b'ferr':
407 407 newfp = fp
408 408 else:
409 409 # make it line buffered explicitly because the default is
410 410 # decided on first write(), where fout could be a pager.
411 411 if fp.isatty():
412 412 bufsize = 1 # line buffered
413 413 else:
414 414 bufsize = -1 # system default
415 415 newfp = os.fdopen(fp.fileno(), mode, bufsize)
416 416 setattr(ui, fn, newfp)
417 417 setattr(self, cn, newfp)
418 418
419 419 self._ioattached = True
420 420 self.cresult.write(struct.pack(b'>i', len(clientfds)))
421 421
422 422 def _saveio(self):
423 423 if self._oldios:
424 424 return
425 425 ui = self.ui
426 426 for cn, fn, _mode in _iochannels:
427 427 ch = getattr(self, cn)
428 428 fp = getattr(ui, fn)
429 429 fd = os.dup(fp.fileno())
430 430 self._oldios.append((ch, fp, fd))
431 431
432 432 def _restoreio(self):
433 433 ui = self.ui
434 434 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
435 435 newfp = getattr(ui, fn)
436 436 # close newfp while it's associated with client; otherwise it
437 437 # would be closed when newfp is deleted
438 438 if newfp is not fp:
439 439 newfp.close()
440 440 # restore original fd: fp is open again
441 441 os.dup2(fd, fp.fileno())
442 442 os.close(fd)
443 443 setattr(self, cn, ch)
444 444 setattr(ui, fn, fp)
445 445 del self._oldios[:]
446 446
447 447 def validate(self):
448 448 """Reload the config and check if the server is up to date
449 449
450 450 Read a list of '\0' separated arguments.
451 451 Write a non-empty list of '\0' separated instruction strings or '\0'
452 452 if the list is empty.
453 453 An instruction string could be either:
454 454 - "unlink $path", the client should unlink the path to stop the
455 455 outdated server.
456 456 - "redirect $path", the client should attempt to connect to $path
457 457 first. If it does not work, start a new server. It implies
458 458 "reconnect".
459 459 - "exit $n", the client should exit directly with code n.
460 460 This may happen if we cannot parse the config.
461 461 - "reconnect", the client should close the connection and
462 462 reconnect.
463 463 If neither "reconnect" nor "redirect" is included in the instruction
464 464 list, the client can continue with this server after completing all
465 465 the instructions.
466 466 """
467 467 from . import dispatch # avoid cycle
468 468
469 469 args = self._readlist()
470 470 try:
471 471 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
472 472 except error.ParseError as inst:
473 473 dispatch._formatparse(self.ui.warn, inst)
474 474 self.ui.flush()
475 475 self.cresult.write(b'exit 255')
476 476 return
477 477 except error.Abort as inst:
478 478 self.ui.error(_(b"abort: %s\n") % inst)
479 479 if inst.hint:
480 480 self.ui.error(_(b"(%s)\n") % inst.hint)
481 481 self.ui.flush()
482 482 self.cresult.write(b'exit 255')
483 483 return
484 484 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
485 485 insts = []
486 486 if newhash.mtimehash != self.hashstate.mtimehash:
487 487 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
488 488 insts.append(b'unlink %s' % addr)
489 489 # mtimehash is empty if one or more extensions fail to load.
490 490 # to be compatible with hg, still serve the client this time.
491 491 if self.hashstate.mtimehash:
492 492 insts.append(b'reconnect')
493 493 if newhash.confighash != self.hashstate.confighash:
494 494 addr = _hashaddress(self.baseaddress, newhash.confighash)
495 495 insts.append(b'redirect %s' % addr)
496 496 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
497 497 self.cresult.write(b'\0'.join(insts) or b'\0')
498 498
499 499 def chdir(self):
500 500 """Change current directory
501 501
502 502 Note that the behavior of --cwd option is bit different from this.
503 503 It does not affect --config parameter.
504 504 """
505 505 path = self._readstr()
506 506 if not path:
507 507 return
508 508 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
509 509 os.chdir(path)
510 510
511 511 def setumask(self):
512 512 """Change umask (DEPRECATED)"""
513 513 # BUG: this does not follow the message frame structure, but kept for
514 514 # backward compatibility with old chg clients for some time
515 515 self._setumask(self._read(4))
516 516
517 517 def setumask2(self):
518 518 """Change umask"""
519 519 data = self._readstr()
520 520 if len(data) != 4:
521 521 raise ValueError(b'invalid mask length in setumask2 request')
522 522 self._setumask(data)
523 523
524 524 def _setumask(self, data):
525 525 mask = struct.unpack(b'>I', data)[0]
526 526 self.ui.log(b'chgserver', b'setumask %r\n', mask)
527 527 os.umask(mask)
528 528
529 529 def runcommand(self):
530 530 # pager may be attached within the runcommand session, which should
531 531 # be detached at the end of the session. otherwise the pager wouldn't
532 532 # receive EOF.
533 533 globaloldios = self._oldios
534 534 self._oldios = []
535 535 try:
536 536 return super(chgcmdserver, self).runcommand()
537 537 finally:
538 538 self._restoreio()
539 539 self._oldios = globaloldios
540 540
541 541 def setenv(self):
542 542 """Clear and update os.environ
543 543
544 544 Note that not all variables can make an effect on the running process.
545 545 """
546 546 l = self._readlist()
547 547 try:
548 548 newenv = dict(s.split(b'=', 1) for s in l)
549 549 except ValueError:
550 550 raise ValueError(b'unexpected value in setenv request')
551 551 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
552 552
553 # Python3 has some logic to "coerce" the C locale to a UTF-8 capable
554 # one, and it sets LC_CTYPE in the environment to C.UTF-8 if none of
555 # 'LC_CTYPE', 'LC_ALL' or 'LANG' are set (to any value). This can be
556 # disabled with PYTHONCOERCECLOCALE=0 in the environment.
557 #
558 # When fromui is called via _inithashstate, python has already set
559 # this, so that's in the environment right when we start up the hg
560 # process. Then chg will call us and tell us to set the environment to
561 # the one it has; this might NOT have LC_CTYPE, so we'll need to
562 # carry-forward the LC_CTYPE that was coerced in these situations.
563 #
564 # If this is not handled, we will fail config+env validation and fail
565 # to start chg. If this is just ignored instead of carried forward, we
566 # may have different behavior between chg and non-chg.
567 if pycompat.ispy3:
568 # Rename for wordwrapping purposes
569 oldenv = encoding.environ
570 if not any(
571 e.get(b'PYTHONCOERCECLOCALE') == b'0' for e in [oldenv, newenv]
572 ):
573 keys = [b'LC_CTYPE', b'LC_ALL', b'LANG']
574 old_keys = [k for k, v in oldenv.items() if k in keys and v]
575 new_keys = [k for k, v in newenv.items() if k in keys and v]
576 # If the user's environment (from chg) doesn't have ANY of the
577 # keys that python looks for, and the environment (from
578 # initialization) has ONLY LC_CTYPE and it's set to C.UTF-8,
579 # carry it forward.
580 if (
581 not new_keys
582 and old_keys == [b'LC_CTYPE']
583 and oldenv[b'LC_CTYPE'] == b'C.UTF-8'
584 ):
585 newenv[b'LC_CTYPE'] = oldenv[b'LC_CTYPE']
586
587 553 encoding.environ.clear()
588 554 encoding.environ.update(newenv)
589 555
590 556 capabilities = commandserver.server.capabilities.copy()
591 557 capabilities.update(
592 558 {
593 559 b'attachio': attachio,
594 560 b'chdir': chdir,
595 561 b'runcommand': runcommand,
596 562 b'setenv': setenv,
597 563 b'setumask': setumask,
598 564 b'setumask2': setumask2,
599 565 }
600 566 )
601 567
602 568 if util.safehasattr(procutil, b'setprocname'):
603 569
604 570 def setprocname(self):
605 571 """Change process title"""
606 572 name = self._readstr()
607 573 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
608 574 procutil.setprocname(name)
609 575
610 576 capabilities[b'setprocname'] = setprocname
611 577
612 578
613 579 def _tempaddress(address):
614 580 return b'%s.%d.tmp' % (address, os.getpid())
615 581
616 582
617 583 def _hashaddress(address, hashstr):
618 584 # if the basename of address contains '.', use only the left part. this
619 585 # makes it possible for the client to pass 'server.tmp$PID' and follow by
620 586 # an atomic rename to avoid locking when spawning new servers.
621 587 dirname, basename = os.path.split(address)
622 588 basename = basename.split(b'.', 1)[0]
623 589 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
624 590
625 591
626 592 class chgunixservicehandler(object):
627 593 """Set of operations for chg services"""
628 594
629 595 pollinterval = 1 # [sec]
630 596
631 597 def __init__(self, ui):
632 598 self.ui = ui
633 599 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
634 600 self._lastactive = time.time()
635 601
636 602 def bindsocket(self, sock, address):
637 603 self._inithashstate(address)
638 604 self._checkextensions()
639 605 self._bind(sock)
640 606 self._createsymlink()
641 607 # no "listening at" message should be printed to simulate hg behavior
642 608
643 609 def _inithashstate(self, address):
644 610 self._baseaddress = address
645 611 if self.ui.configbool(b'chgserver', b'skiphash'):
646 612 self._hashstate = None
647 613 self._realaddress = address
648 614 return
649 615 self._hashstate = hashstate.fromui(self.ui)
650 616 self._realaddress = _hashaddress(address, self._hashstate.confighash)
651 617
652 618 def _checkextensions(self):
653 619 if not self._hashstate:
654 620 return
655 621 if extensions.notloaded():
656 622 # one or more extensions failed to load. mtimehash becomes
657 623 # meaningless because we do not know the paths of those extensions.
658 624 # set mtimehash to an illegal hash value to invalidate the server.
659 625 self._hashstate.mtimehash = b''
660 626
661 627 def _bind(self, sock):
662 628 # use a unique temp address so we can stat the file and do ownership
663 629 # check later
664 630 tempaddress = _tempaddress(self._realaddress)
665 631 util.bindunixsocket(sock, tempaddress)
666 632 self._socketstat = os.stat(tempaddress)
667 633 sock.listen(socket.SOMAXCONN)
668 634 # rename will replace the old socket file if exists atomically. the
669 635 # old server will detect ownership change and exit.
670 636 util.rename(tempaddress, self._realaddress)
671 637
672 638 def _createsymlink(self):
673 639 if self._baseaddress == self._realaddress:
674 640 return
675 641 tempaddress = _tempaddress(self._baseaddress)
676 642 os.symlink(os.path.basename(self._realaddress), tempaddress)
677 643 util.rename(tempaddress, self._baseaddress)
678 644
679 645 def _issocketowner(self):
680 646 try:
681 647 st = os.stat(self._realaddress)
682 648 return (
683 649 st.st_ino == self._socketstat.st_ino
684 650 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
685 651 )
686 652 except OSError:
687 653 return False
688 654
689 655 def unlinksocket(self, address):
690 656 if not self._issocketowner():
691 657 return
692 658 # it is possible to have a race condition here that we may
693 659 # remove another server's socket file. but that's okay
694 660 # since that server will detect and exit automatically and
695 661 # the client will start a new server on demand.
696 662 util.tryunlink(self._realaddress)
697 663
698 664 def shouldexit(self):
699 665 if not self._issocketowner():
700 666 self.ui.log(
701 667 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
702 668 )
703 669 return True
704 670 if time.time() - self._lastactive > self._idletimeout:
705 671 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
706 672 return True
707 673 return False
708 674
709 675 def newconnection(self):
710 676 self._lastactive = time.time()
711 677
712 678 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
713 679 return chgcmdserver(
714 680 self.ui,
715 681 repo,
716 682 fin,
717 683 fout,
718 684 conn,
719 685 prereposetups,
720 686 self._hashstate,
721 687 self._baseaddress,
722 688 )
723 689
724 690
725 691 def chgunixservice(ui, repo, opts):
726 692 # CHGINTERNALMARK is set by chg client. It is an indication of things are
727 693 # started by chg so other code can do things accordingly, like disabling
728 694 # demandimport or detecting chg client started by chg client. When executed
729 695 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
730 696 # environ cleaner.
731 697 if b'CHGINTERNALMARK' in encoding.environ:
732 698 del encoding.environ[b'CHGINTERNALMARK']
699 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
700 # it thinks the current value is "C". This breaks the hash computation and
701 # causes chg to restart loop.
702 if b'CHGORIG_LC_CTYPE' in encoding.environ:
703 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
704 del encoding.environ[b'CHGORIG_LC_CTYPE']
705 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
706 if b'LC_CTYPE' in encoding.environ:
707 del encoding.environ[b'LC_CTYPE']
708 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
733 709
734 710 if repo:
735 711 # one chgserver can serve multiple repos. drop repo information
736 712 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
737 713 h = chgunixservicehandler(ui)
738 714 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,355 +1,368
1 1 #require chg
2 2
3 3 $ mkdir log
4 4 $ cp $HGRCPATH $HGRCPATH.unconfigured
5 5 $ cat <<'EOF' >> $HGRCPATH
6 6 > [cmdserver]
7 7 > log = $TESTTMP/log/server.log
8 8 > max-log-files = 1
9 9 > max-log-size = 10 kB
10 10 > EOF
11 11 $ cp $HGRCPATH $HGRCPATH.orig
12 12
13 13 $ filterlog () {
14 14 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
15 15 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
16 16 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
17 17 > -e 's!\(in \)[0-9.]*s\b!\1 ...s!g' \
18 18 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
19 19 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
20 20 > }
21 21
22 22 init repo
23 23
24 24 $ chg init foo
25 25 $ cd foo
26 26
27 27 ill-formed config
28 28
29 29 $ chg status
30 30 $ echo '=brokenconfig' >> $HGRCPATH
31 31 $ chg status
32 32 hg: parse error at * (glob)
33 33 [255]
34 34
35 35 $ cp $HGRCPATH.orig $HGRCPATH
36 36
37 37 long socket path
38 38
39 39 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
40 40 $ mkdir -p $sockpath
41 41 $ bakchgsockname=$CHGSOCKNAME
42 42 $ CHGSOCKNAME=$sockpath/server
43 43 $ export CHGSOCKNAME
44 44 $ chg root
45 45 $TESTTMP/foo
46 46 $ rm -rf $sockpath
47 47 $ CHGSOCKNAME=$bakchgsockname
48 48 $ export CHGSOCKNAME
49 49
50 50 $ cd ..
51 51
52 52 editor
53 53 ------
54 54
55 55 $ cat >> pushbuffer.py <<EOF
56 56 > def reposetup(ui, repo):
57 57 > repo.ui.pushbuffer(subproc=True)
58 58 > EOF
59 59
60 60 $ chg init editor
61 61 $ cd editor
62 62
63 63 by default, system() should be redirected to the client:
64 64
65 65 $ touch foo
66 66 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
67 67 > | egrep "HG:|run 'cat"
68 68 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
69 69 HG: Enter commit message. Lines beginning with 'HG:' are removed.
70 70 HG: Leave message empty to abort commit.
71 71 HG: --
72 72 HG: user: test
73 73 HG: branch 'default'
74 74 HG: added foo
75 75
76 76 but no redirection should be made if output is captured:
77 77
78 78 $ touch bar
79 79 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
80 80 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
81 81 > | egrep "HG:|run 'cat"
82 82 [1]
83 83
84 84 check that commit commands succeeded:
85 85
86 86 $ hg log -T '{rev}:{desc}\n'
87 87 1:bufferred
88 88 0:channeled
89 89
90 90 $ cd ..
91 91
92 92 pager
93 93 -----
94 94
95 95 $ cat >> fakepager.py <<EOF
96 96 > import sys
97 97 > for line in sys.stdin:
98 98 > sys.stdout.write('paged! %r\n' % line)
99 99 > EOF
100 100
101 101 enable pager extension globally, but spawns the master server with no tty:
102 102
103 103 $ chg init pager
104 104 $ cd pager
105 105 $ cat >> $HGRCPATH <<EOF
106 106 > [extensions]
107 107 > pager =
108 108 > [pager]
109 109 > pager = "$PYTHON" $TESTTMP/fakepager.py
110 110 > EOF
111 111 $ chg version > /dev/null
112 112 $ touch foo
113 113 $ chg ci -qAm foo
114 114
115 115 pager should be enabled if the attached client has a tty:
116 116
117 117 $ chg log -l1 -q --config ui.formatted=True
118 118 paged! '0:1f7b0de80e11\n'
119 119 $ chg log -l1 -q --config ui.formatted=False
120 120 0:1f7b0de80e11
121 121
122 122 chg waits for pager if runcommand raises
123 123
124 124 $ cat > $TESTTMP/crash.py <<EOF
125 125 > from mercurial import registrar
126 126 > cmdtable = {}
127 127 > command = registrar.command(cmdtable)
128 128 > @command(b'crash')
129 129 > def pagercrash(ui, repo, *pats, **opts):
130 130 > ui.write(b'going to crash\n')
131 131 > raise Exception('.')
132 132 > EOF
133 133
134 134 $ cat > $TESTTMP/fakepager.py <<EOF
135 135 > from __future__ import absolute_import
136 136 > import sys
137 137 > import time
138 138 > for line in iter(sys.stdin.readline, ''):
139 139 > if 'crash' in line: # only interested in lines containing 'crash'
140 140 > # if chg exits when pager is sleeping (incorrectly), the output
141 141 > # will be captured by the next test case
142 142 > time.sleep(1)
143 143 > sys.stdout.write('crash-pager: %s' % line)
144 144 > EOF
145 145
146 146 $ cat >> .hg/hgrc <<EOF
147 147 > [extensions]
148 148 > crash = $TESTTMP/crash.py
149 149 > EOF
150 150
151 151 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
152 152 crash-pager: going to crash
153 153 [255]
154 154
155 155 $ cd ..
156 156
157 157 server lifecycle
158 158 ----------------
159 159
160 160 chg server should be restarted on code change, and old server will shut down
161 161 automatically. In this test, we use the following time parameters:
162 162
163 163 - "sleep 1" to make mtime different
164 164 - "sleep 2" to notice mtime change (polling interval is 1 sec)
165 165
166 166 set up repository with an extension:
167 167
168 168 $ chg init extreload
169 169 $ cd extreload
170 170 $ touch dummyext.py
171 171 $ cat <<EOF >> .hg/hgrc
172 172 > [extensions]
173 173 > dummyext = dummyext.py
174 174 > EOF
175 175
176 176 isolate socket directory for stable result:
177 177
178 178 $ OLDCHGSOCKNAME=$CHGSOCKNAME
179 179 $ mkdir chgsock
180 180 $ CHGSOCKNAME=`pwd`/chgsock/server
181 181
182 182 warm up server:
183 183
184 184 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
185 185 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
186 186
187 187 new server should be started if extension modified:
188 188
189 189 $ sleep 1
190 190 $ touch dummyext.py
191 191 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
192 192 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
193 193 chg: debug: * instruction: reconnect (glob)
194 194 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
195 195
196 196 old server will shut down, while new server should still be reachable:
197 197
198 198 $ sleep 2
199 199 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
200 200
201 201 socket file should never be unlinked by old server:
202 202 (simulates unowned socket by updating mtime, which makes sure server exits
203 203 at polling cycle)
204 204
205 205 $ ls chgsock/server-*
206 206 chgsock/server-* (glob)
207 207 $ touch chgsock/server-*
208 208 $ sleep 2
209 209 $ ls chgsock/server-*
210 210 chgsock/server-* (glob)
211 211
212 212 since no server is reachable from socket file, new server should be started:
213 213 (this test makes sure that old server shut down automatically)
214 214
215 215 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
216 216 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
217 217
218 218 shut down servers and restore environment:
219 219
220 220 $ rm -R chgsock
221 221 $ sleep 2
222 222 $ CHGSOCKNAME=$OLDCHGSOCKNAME
223 223 $ cd ..
224 224
225 225 check that server events are recorded:
226 226
227 227 $ ls log
228 228 server.log
229 229 server.log.1
230 230
231 231 print only the last 10 lines, since we aren't sure how many records are
232 232 preserved (since setprocname isn't available on py3, the 10th-most-recent line
233 233 is different when using py3):
234 234
235 235 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
236 236 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ... (py3 !)
237 237 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
238 238 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ... (no-py3 !)
239 239 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
240 240 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
241 241 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
242 242 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
243 243 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
244 244 YYYY/MM/DD HH:MM:SS (PID)> validate: []
245 245 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
246 246 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
247 247
248 248 repository cache
249 249 ----------------
250 250
251 251 $ rm log/server.log*
252 252 $ cp $HGRCPATH.unconfigured $HGRCPATH
253 253 $ cat <<'EOF' >> $HGRCPATH
254 254 > [cmdserver]
255 255 > log = $TESTTMP/log/server.log
256 256 > max-repo-cache = 1
257 257 > track-log = command, repocache
258 258 > EOF
259 259
260 260 isolate socket directory for stable result:
261 261
262 262 $ OLDCHGSOCKNAME=$CHGSOCKNAME
263 263 $ mkdir chgsock
264 264 $ CHGSOCKNAME=`pwd`/chgsock/server
265 265
266 266 create empty repo and cache it:
267 267
268 268 $ hg init cached
269 269 $ hg id -R cached
270 270 000000000000 tip
271 271 $ sleep 1
272 272
273 273 modify repo (and cache will be invalidated):
274 274
275 275 $ touch cached/a
276 276 $ hg ci -R cached -Am 'add a'
277 277 adding a
278 278 $ sleep 1
279 279
280 280 read cached repo:
281 281
282 282 $ hg log -R cached
283 283 changeset: 0:ac82d8b1f7c4
284 284 tag: tip
285 285 user: test
286 286 date: Thu Jan 01 00:00:00 1970 +0000
287 287 summary: add a
288 288
289 289 $ sleep 1
290 290
291 291 discard cached from LRU cache:
292 292
293 293 $ hg clone cached cached2
294 294 updating to branch default
295 295 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
296 296 $ hg id -R cached2
297 297 ac82d8b1f7c4 tip
298 298 $ sleep 1
299 299
300 300 read uncached repo:
301 301
302 302 $ hg log -R cached
303 303 changeset: 0:ac82d8b1f7c4
304 304 tag: tip
305 305 user: test
306 306 date: Thu Jan 01 00:00:00 1970 +0000
307 307 summary: add a
308 308
309 309 $ sleep 1
310 310
311 311 shut down servers and restore environment:
312 312
313 313 $ rm -R chgsock
314 314 $ sleep 2
315 315 $ CHGSOCKNAME=$OLDCHGSOCKNAME
316 316
317 317 check server log:
318 318
319 319 $ cat log/server.log | filterlog
320 320 YYYY/MM/DD HH:MM:SS (PID)> init cached
321 321 YYYY/MM/DD HH:MM:SS (PID)> id -R cached
322 322 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
323 323 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
324 324 YYYY/MM/DD HH:MM:SS (PID)> ci -R cached -Am 'add a'
325 325 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
326 326 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
327 327 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
328 328 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
329 329 YYYY/MM/DD HH:MM:SS (PID)> clone cached cached2
330 330 YYYY/MM/DD HH:MM:SS (PID)> id -R cached2
331 331 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached2 (in ...s)
332 332 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
333 333 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
334 334
335 Test that chg works even when python "coerces" the locale (py3.7+, which is done
336 by default if none of LC_ALL, LC_CTYPE, or LANG are set in the environment)
335 Test that chg works (sets to the user's actual LC_CTYPE) even when python
336 "coerces" the locale (py3.7+)
337 337
338 338 $ cat > $TESTTMP/debugenv.py <<EOF
339 339 > from mercurial import encoding
340 340 > from mercurial import registrar
341 341 > cmdtable = {}
342 342 > command = registrar.command(cmdtable)
343 343 > @command(b'debugenv', [], b'', norepo=True)
344 344 > def debugenv(ui):
345 345 > for k in [b'LC_ALL', b'LC_CTYPE', b'LANG']:
346 346 > v = encoding.environ.get(k)
347 347 > if v is not None:
348 348 > ui.write(b'%s=%s\n' % (k, encoding.environ[k]))
349 349 > EOF
350 (hg keeps python's modified LC_CTYPE, chg doesn't)
351 $ (unset LC_ALL; unset LANG; LC_CTYPE= "$CHGHG" \
352 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
353 LC_CTYPE=C.UTF-8 (py37 !)
354 LC_CTYPE= (no-py37 !)
355 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
356 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
357 LC_CTYPE=
358 $ (unset LC_ALL; unset LANG; LC_CTYPE=unsupported_value chg \
359 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
360 LC_CTYPE=unsupported_value
361 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
362 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
363 LC_CTYPE=
350 364 $ LANG= LC_ALL= LC_CTYPE= chg \
351 365 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv
352 366 LC_ALL=
353 LC_CTYPE=C.UTF-8 (py37 !)
354 LC_CTYPE= (no-py37 !)
367 LC_CTYPE=
355 368 LANG=
General Comments 0
You need to be logged in to leave comments. Login now