##// END OF EJS Templates
chgserver: discard buffered output before restoring fds (issue6207)...
Yuya Nishihara -
r45745:a17454a1 stable
parent child Browse files
Show More
@@ -1,731 +1,741 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
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'diff-tools', # affects whether gui or not in extdiff's uisetup
84 84 b'eol', # uses setconfig('eol', ...)
85 85 b'extdiff', # uisetup will register new commands
86 86 b'extensions',
87 87 b'fastannotate', # affects annotate command and adds fastannonate cmd
88 88 b'merge-tools', # affects whether gui or not in extdiff's uisetup
89 89 b'schemes', # extsetup will update global hg.schemes
90 90 ]
91 91
92 92 _configsectionitems = [
93 93 (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup
94 94 ]
95 95
96 96 # sensitive environment variables affecting confighash
97 97 _envre = re.compile(
98 98 br'''\A(?:
99 99 CHGHG
100 100 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
101 101 |HG(?:ENCODING|PLAIN).*
102 102 |LANG(?:UAGE)?
103 103 |LC_.*
104 104 |LD_.*
105 105 |PATH
106 106 |PYTHON.*
107 107 |TERM(?:INFO)?
108 108 |TZ
109 109 )\Z''',
110 110 re.X,
111 111 )
112 112
113 113
114 114 def _confighash(ui):
115 115 """return a quick hash for detecting config/env changes
116 116
117 117 confighash is the hash of sensitive config items and environment variables.
118 118
119 119 for chgserver, it is designed that once confighash changes, the server is
120 120 not qualified to serve its client and should redirect the client to a new
121 121 server. different from mtimehash, confighash change will not mark the
122 122 server outdated and exit since the user can have different configs at the
123 123 same time.
124 124 """
125 125 sectionitems = []
126 126 for section in _configsections:
127 127 sectionitems.append(ui.configitems(section))
128 128 for section, item in _configsectionitems:
129 129 sectionitems.append(ui.config(section, item))
130 130 sectionhash = _hashlist(sectionitems)
131 131 # If $CHGHG is set, the change to $HG should not trigger a new chg server
132 132 if b'CHGHG' in encoding.environ:
133 133 ignored = {b'HG'}
134 134 else:
135 135 ignored = set()
136 136 envitems = [
137 137 (k, v)
138 138 for k, v in pycompat.iteritems(encoding.environ)
139 139 if _envre.match(k) and k not in ignored
140 140 ]
141 141 envhash = _hashlist(sorted(envitems))
142 142 return sectionhash[:6] + envhash[:6]
143 143
144 144
145 145 def _getmtimepaths(ui):
146 146 """get a list of paths that should be checked to detect change
147 147
148 148 The list will include:
149 149 - extensions (will not cover all files for complex extensions)
150 150 - mercurial/__version__.py
151 151 - python binary
152 152 """
153 153 modules = [m for n, m in extensions.extensions(ui)]
154 154 try:
155 155 from . import __version__
156 156
157 157 modules.append(__version__)
158 158 except ImportError:
159 159 pass
160 160 files = []
161 161 if pycompat.sysexecutable:
162 162 files.append(pycompat.sysexecutable)
163 163 for m in modules:
164 164 try:
165 165 files.append(pycompat.fsencode(inspect.getabsfile(m)))
166 166 except TypeError:
167 167 pass
168 168 return sorted(set(files))
169 169
170 170
171 171 def _mtimehash(paths):
172 172 """return a quick hash for detecting file changes
173 173
174 174 mtimehash calls stat on given paths and calculate a hash based on size and
175 175 mtime of each file. mtimehash does not read file content because reading is
176 176 expensive. therefore it's not 100% reliable for detecting content changes.
177 177 it's possible to return different hashes for same file contents.
178 178 it's also possible to return a same hash for different file contents for
179 179 some carefully crafted situation.
180 180
181 181 for chgserver, it is designed that once mtimehash changes, the server is
182 182 considered outdated immediately and should no longer provide service.
183 183
184 184 mtimehash is not included in confighash because we only know the paths of
185 185 extensions after importing them (there is imp.find_module but that faces
186 186 race conditions). We need to calculate confighash without importing.
187 187 """
188 188
189 189 def trystat(path):
190 190 try:
191 191 st = os.stat(path)
192 192 return (st[stat.ST_MTIME], st.st_size)
193 193 except OSError:
194 194 # could be ENOENT, EPERM etc. not fatal in any case
195 195 pass
196 196
197 197 return _hashlist(pycompat.maplist(trystat, paths))[:12]
198 198
199 199
200 200 class hashstate(object):
201 201 """a structure storing confighash, mtimehash, paths used for mtimehash"""
202 202
203 203 def __init__(self, confighash, mtimehash, mtimepaths):
204 204 self.confighash = confighash
205 205 self.mtimehash = mtimehash
206 206 self.mtimepaths = mtimepaths
207 207
208 208 @staticmethod
209 209 def fromui(ui, mtimepaths=None):
210 210 if mtimepaths is None:
211 211 mtimepaths = _getmtimepaths(ui)
212 212 confighash = _confighash(ui)
213 213 mtimehash = _mtimehash(mtimepaths)
214 214 ui.log(
215 215 b'cmdserver',
216 216 b'confighash = %s mtimehash = %s\n',
217 217 confighash,
218 218 mtimehash,
219 219 )
220 220 return hashstate(confighash, mtimehash, mtimepaths)
221 221
222 222
223 223 def _newchgui(srcui, csystem, attachio):
224 224 class chgui(srcui.__class__):
225 225 def __init__(self, src=None):
226 226 super(chgui, self).__init__(src)
227 227 if src:
228 228 self._csystem = getattr(src, '_csystem', csystem)
229 229 else:
230 230 self._csystem = csystem
231 231
232 232 def _runsystem(self, cmd, environ, cwd, out):
233 233 # fallback to the original system method if
234 234 # a. the output stream is not stdout (e.g. stderr, cStringIO),
235 235 # b. or stdout is redirected by protectfinout(),
236 236 # because the chg client is not aware of these situations and
237 237 # will behave differently (i.e. write to stdout).
238 238 if (
239 239 out is not self.fout
240 240 or not util.safehasattr(self.fout, b'fileno')
241 241 or self.fout.fileno() != procutil.stdout.fileno()
242 242 or self._finoutredirected
243 243 ):
244 244 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
245 245 self.flush()
246 246 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
247 247
248 248 def _runpager(self, cmd, env=None):
249 249 self._csystem(
250 250 cmd,
251 251 procutil.shellenviron(env),
252 252 type=b'pager',
253 253 cmdtable={b'attachio': attachio},
254 254 )
255 255 return True
256 256
257 257 return chgui(srcui)
258 258
259 259
260 260 def _loadnewui(srcui, args, cdebug):
261 261 from . import dispatch # avoid cycle
262 262
263 263 newui = srcui.__class__.load()
264 264 for a in [b'fin', b'fout', b'ferr', b'environ']:
265 265 setattr(newui, a, getattr(srcui, a))
266 266 if util.safehasattr(srcui, b'_csystem'):
267 267 newui._csystem = srcui._csystem
268 268
269 269 # command line args
270 270 options = dispatch._earlyparseopts(newui, args)
271 271 dispatch._parseconfig(newui, options[b'config'])
272 272
273 273 # stolen from tortoisehg.util.copydynamicconfig()
274 274 for section, name, value in srcui.walkconfig():
275 275 source = srcui.configsource(section, name)
276 276 if b':' in source or source == b'--config' or source.startswith(b'$'):
277 277 # path:line or command line, or environ
278 278 continue
279 279 newui.setconfig(section, name, value, source)
280 280
281 281 # load wd and repo config, copied from dispatch.py
282 282 cwd = options[b'cwd']
283 283 cwd = cwd and os.path.realpath(cwd) or None
284 284 rpath = options[b'repository']
285 285 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
286 286
287 287 extensions.populateui(newui)
288 288 commandserver.setuplogging(newui, fp=cdebug)
289 289 if newui is not newlui:
290 290 extensions.populateui(newlui)
291 291 commandserver.setuplogging(newlui, fp=cdebug)
292 292
293 293 return (newui, newlui)
294 294
295 295
296 296 class channeledsystem(object):
297 297 """Propagate ui.system() request in the following format:
298 298
299 299 payload length (unsigned int),
300 300 type, '\0',
301 301 cmd, '\0',
302 302 cwd, '\0',
303 303 envkey, '=', val, '\0',
304 304 ...
305 305 envkey, '=', val
306 306
307 307 if type == 'system', waits for:
308 308
309 309 exitcode length (unsigned int),
310 310 exitcode (int)
311 311
312 312 if type == 'pager', repetitively waits for a command name ending with '\n'
313 313 and executes it defined by cmdtable, or exits the loop if the command name
314 314 is empty.
315 315 """
316 316
317 317 def __init__(self, in_, out, channel):
318 318 self.in_ = in_
319 319 self.out = out
320 320 self.channel = channel
321 321
322 322 def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
323 323 args = [type, cmd, os.path.abspath(cwd or b'.')]
324 324 args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
325 325 data = b'\0'.join(args)
326 326 self.out.write(struct.pack(b'>cI', self.channel, len(data)))
327 327 self.out.write(data)
328 328 self.out.flush()
329 329
330 330 if type == b'system':
331 331 length = self.in_.read(4)
332 332 (length,) = struct.unpack(b'>I', length)
333 333 if length != 4:
334 334 raise error.Abort(_(b'invalid response'))
335 335 (rc,) = struct.unpack(b'>i', self.in_.read(4))
336 336 return rc
337 337 elif type == b'pager':
338 338 while True:
339 339 cmd = self.in_.readline()[:-1]
340 340 if not cmd:
341 341 break
342 342 if cmdtable and cmd in cmdtable:
343 343 cmdtable[cmd]()
344 344 else:
345 345 raise error.Abort(_(b'unexpected command: %s') % cmd)
346 346 else:
347 347 raise error.ProgrammingError(b'invalid S channel type: %s' % type)
348 348
349 349
350 350 _iochannels = [
351 351 # server.ch, ui.fp, mode
352 352 (b'cin', b'fin', 'rb'),
353 353 (b'cout', b'fout', 'wb'),
354 354 (b'cerr', b'ferr', 'wb'),
355 355 ]
356 356
357 357
358 358 class chgcmdserver(commandserver.server):
359 359 def __init__(
360 360 self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
361 361 ):
362 362 super(chgcmdserver, self).__init__(
363 363 _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
364 364 repo,
365 365 fin,
366 366 fout,
367 367 prereposetups,
368 368 )
369 369 self.clientsock = sock
370 370 self._ioattached = False
371 371 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
372 372 self.hashstate = hashstate
373 373 self.baseaddress = baseaddress
374 374 if hashstate is not None:
375 375 self.capabilities = self.capabilities.copy()
376 376 self.capabilities[b'validate'] = chgcmdserver.validate
377 377
378 378 def cleanup(self):
379 379 super(chgcmdserver, self).cleanup()
380 380 # dispatch._runcatch() does not flush outputs if exception is not
381 381 # handled by dispatch._dispatch()
382 382 self.ui.flush()
383 383 self._restoreio()
384 384 self._ioattached = False
385 385
386 386 def attachio(self):
387 387 """Attach to client's stdio passed via unix domain socket; all
388 388 channels except cresult will no longer be used
389 389 """
390 390 # tell client to sendmsg() with 1-byte payload, which makes it
391 391 # distinctive from "attachio\n" command consumed by client.read()
392 392 self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
393 393 clientfds = util.recvfds(self.clientsock.fileno())
394 394 self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
395 395
396 396 ui = self.ui
397 397 ui.flush()
398 398 self._saveio()
399 399 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
400 400 assert fd > 0
401 401 fp = getattr(ui, fn)
402 402 os.dup2(fd, fp.fileno())
403 403 os.close(fd)
404 404 if self._ioattached:
405 405 continue
406 406 # reset buffering mode when client is first attached. as we want
407 407 # to see output immediately on pager, the mode stays unchanged
408 408 # when client re-attached. ferr is unchanged because it should
409 409 # be unbuffered no matter if it is a tty or not.
410 410 if fn == b'ferr':
411 411 newfp = fp
412 412 else:
413 413 # make it line buffered explicitly because the default is
414 414 # decided on first write(), where fout could be a pager.
415 415 if fp.isatty():
416 416 bufsize = 1 # line buffered
417 417 else:
418 418 bufsize = -1 # system default
419 419 newfp = os.fdopen(fp.fileno(), mode, bufsize)
420 420 setattr(ui, fn, newfp)
421 421 setattr(self, cn, newfp)
422 422
423 423 self._ioattached = True
424 424 self.cresult.write(struct.pack(b'>i', len(clientfds)))
425 425
426 426 def _saveio(self):
427 427 if self._oldios:
428 428 return
429 429 ui = self.ui
430 430 for cn, fn, _mode in _iochannels:
431 431 ch = getattr(self, cn)
432 432 fp = getattr(ui, fn)
433 433 fd = os.dup(fp.fileno())
434 434 self._oldios.append((ch, fp, fd))
435 435
436 436 def _restoreio(self):
437 if not self._oldios:
438 return
439 nullfd = os.open(os.devnull, os.O_WRONLY)
437 440 ui = self.ui
438 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
441 for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
439 442 newfp = getattr(ui, fn)
440 443 # close newfp while it's associated with client; otherwise it
441 444 # would be closed when newfp is deleted
442 445 if newfp is not fp:
443 446 newfp.close()
444 447 # restore original fd: fp is open again
445 448 try:
449 if newfp is fp and 'w' in mode:
450 # Discard buffered data which couldn't be flushed because
451 # of EPIPE. The data should belong to the current session
452 # and should never persist.
453 os.dup2(nullfd, fp.fileno())
454 fp.flush()
446 455 os.dup2(fd, fp.fileno())
447 456 except OSError as err:
448 457 # According to issue6330, running chg on heavy loaded systems
449 458 # can lead to EBUSY. [man dup2] indicates that, on Linux,
450 459 # EBUSY comes from a race condition between open() and dup2().
451 460 # However it's not clear why open() race occurred for
452 461 # newfd=stdin/out/err.
453 462 self.ui.log(
454 463 b'chgserver',
455 464 b'got %s while duplicating %s\n',
456 465 stringutil.forcebytestr(err),
457 466 fn,
458 467 )
459 468 os.close(fd)
460 469 setattr(self, cn, ch)
461 470 setattr(ui, fn, fp)
471 os.close(nullfd)
462 472 del self._oldios[:]
463 473
464 474 def validate(self):
465 475 """Reload the config and check if the server is up to date
466 476
467 477 Read a list of '\0' separated arguments.
468 478 Write a non-empty list of '\0' separated instruction strings or '\0'
469 479 if the list is empty.
470 480 An instruction string could be either:
471 481 - "unlink $path", the client should unlink the path to stop the
472 482 outdated server.
473 483 - "redirect $path", the client should attempt to connect to $path
474 484 first. If it does not work, start a new server. It implies
475 485 "reconnect".
476 486 - "exit $n", the client should exit directly with code n.
477 487 This may happen if we cannot parse the config.
478 488 - "reconnect", the client should close the connection and
479 489 reconnect.
480 490 If neither "reconnect" nor "redirect" is included in the instruction
481 491 list, the client can continue with this server after completing all
482 492 the instructions.
483 493 """
484 494 from . import dispatch # avoid cycle
485 495
486 496 args = self._readlist()
487 497 try:
488 498 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
489 499 except error.ParseError as inst:
490 500 dispatch._formatparse(self.ui.warn, inst)
491 501 self.ui.flush()
492 502 self.cresult.write(b'exit 255')
493 503 return
494 504 except error.Abort as inst:
495 505 self.ui.error(_(b"abort: %s\n") % inst)
496 506 if inst.hint:
497 507 self.ui.error(_(b"(%s)\n") % inst.hint)
498 508 self.ui.flush()
499 509 self.cresult.write(b'exit 255')
500 510 return
501 511 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
502 512 insts = []
503 513 if newhash.mtimehash != self.hashstate.mtimehash:
504 514 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
505 515 insts.append(b'unlink %s' % addr)
506 516 # mtimehash is empty if one or more extensions fail to load.
507 517 # to be compatible with hg, still serve the client this time.
508 518 if self.hashstate.mtimehash:
509 519 insts.append(b'reconnect')
510 520 if newhash.confighash != self.hashstate.confighash:
511 521 addr = _hashaddress(self.baseaddress, newhash.confighash)
512 522 insts.append(b'redirect %s' % addr)
513 523 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
514 524 self.cresult.write(b'\0'.join(insts) or b'\0')
515 525
516 526 def chdir(self):
517 527 """Change current directory
518 528
519 529 Note that the behavior of --cwd option is bit different from this.
520 530 It does not affect --config parameter.
521 531 """
522 532 path = self._readstr()
523 533 if not path:
524 534 return
525 535 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
526 536 os.chdir(path)
527 537
528 538 def setumask(self):
529 539 """Change umask (DEPRECATED)"""
530 540 # BUG: this does not follow the message frame structure, but kept for
531 541 # backward compatibility with old chg clients for some time
532 542 self._setumask(self._read(4))
533 543
534 544 def setumask2(self):
535 545 """Change umask"""
536 546 data = self._readstr()
537 547 if len(data) != 4:
538 548 raise ValueError(b'invalid mask length in setumask2 request')
539 549 self._setumask(data)
540 550
541 551 def _setumask(self, data):
542 552 mask = struct.unpack(b'>I', data)[0]
543 553 self.ui.log(b'chgserver', b'setumask %r\n', mask)
544 554 util.setumask(mask)
545 555
546 556 def runcommand(self):
547 557 # pager may be attached within the runcommand session, which should
548 558 # be detached at the end of the session. otherwise the pager wouldn't
549 559 # receive EOF.
550 560 globaloldios = self._oldios
551 561 self._oldios = []
552 562 try:
553 563 return super(chgcmdserver, self).runcommand()
554 564 finally:
555 565 self._restoreio()
556 566 self._oldios = globaloldios
557 567
558 568 def setenv(self):
559 569 """Clear and update os.environ
560 570
561 571 Note that not all variables can make an effect on the running process.
562 572 """
563 573 l = self._readlist()
564 574 try:
565 575 newenv = dict(s.split(b'=', 1) for s in l)
566 576 except ValueError:
567 577 raise ValueError(b'unexpected value in setenv request')
568 578 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
569 579
570 580 encoding.environ.clear()
571 581 encoding.environ.update(newenv)
572 582
573 583 capabilities = commandserver.server.capabilities.copy()
574 584 capabilities.update(
575 585 {
576 586 b'attachio': attachio,
577 587 b'chdir': chdir,
578 588 b'runcommand': runcommand,
579 589 b'setenv': setenv,
580 590 b'setumask': setumask,
581 591 b'setumask2': setumask2,
582 592 }
583 593 )
584 594
585 595 if util.safehasattr(procutil, b'setprocname'):
586 596
587 597 def setprocname(self):
588 598 """Change process title"""
589 599 name = self._readstr()
590 600 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
591 601 procutil.setprocname(name)
592 602
593 603 capabilities[b'setprocname'] = setprocname
594 604
595 605
596 606 def _tempaddress(address):
597 607 return b'%s.%d.tmp' % (address, os.getpid())
598 608
599 609
600 610 def _hashaddress(address, hashstr):
601 611 # if the basename of address contains '.', use only the left part. this
602 612 # makes it possible for the client to pass 'server.tmp$PID' and follow by
603 613 # an atomic rename to avoid locking when spawning new servers.
604 614 dirname, basename = os.path.split(address)
605 615 basename = basename.split(b'.', 1)[0]
606 616 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
607 617
608 618
609 619 class chgunixservicehandler(object):
610 620 """Set of operations for chg services"""
611 621
612 622 pollinterval = 1 # [sec]
613 623
614 624 def __init__(self, ui):
615 625 self.ui = ui
616 626 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
617 627 self._lastactive = time.time()
618 628
619 629 def bindsocket(self, sock, address):
620 630 self._inithashstate(address)
621 631 self._checkextensions()
622 632 self._bind(sock)
623 633 self._createsymlink()
624 634 # no "listening at" message should be printed to simulate hg behavior
625 635
626 636 def _inithashstate(self, address):
627 637 self._baseaddress = address
628 638 if self.ui.configbool(b'chgserver', b'skiphash'):
629 639 self._hashstate = None
630 640 self._realaddress = address
631 641 return
632 642 self._hashstate = hashstate.fromui(self.ui)
633 643 self._realaddress = _hashaddress(address, self._hashstate.confighash)
634 644
635 645 def _checkextensions(self):
636 646 if not self._hashstate:
637 647 return
638 648 if extensions.notloaded():
639 649 # one or more extensions failed to load. mtimehash becomes
640 650 # meaningless because we do not know the paths of those extensions.
641 651 # set mtimehash to an illegal hash value to invalidate the server.
642 652 self._hashstate.mtimehash = b''
643 653
644 654 def _bind(self, sock):
645 655 # use a unique temp address so we can stat the file and do ownership
646 656 # check later
647 657 tempaddress = _tempaddress(self._realaddress)
648 658 util.bindunixsocket(sock, tempaddress)
649 659 self._socketstat = os.stat(tempaddress)
650 660 sock.listen(socket.SOMAXCONN)
651 661 # rename will replace the old socket file if exists atomically. the
652 662 # old server will detect ownership change and exit.
653 663 util.rename(tempaddress, self._realaddress)
654 664
655 665 def _createsymlink(self):
656 666 if self._baseaddress == self._realaddress:
657 667 return
658 668 tempaddress = _tempaddress(self._baseaddress)
659 669 os.symlink(os.path.basename(self._realaddress), tempaddress)
660 670 util.rename(tempaddress, self._baseaddress)
661 671
662 672 def _issocketowner(self):
663 673 try:
664 674 st = os.stat(self._realaddress)
665 675 return (
666 676 st.st_ino == self._socketstat.st_ino
667 677 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
668 678 )
669 679 except OSError:
670 680 return False
671 681
672 682 def unlinksocket(self, address):
673 683 if not self._issocketowner():
674 684 return
675 685 # it is possible to have a race condition here that we may
676 686 # remove another server's socket file. but that's okay
677 687 # since that server will detect and exit automatically and
678 688 # the client will start a new server on demand.
679 689 util.tryunlink(self._realaddress)
680 690
681 691 def shouldexit(self):
682 692 if not self._issocketowner():
683 693 self.ui.log(
684 694 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
685 695 )
686 696 return True
687 697 if time.time() - self._lastactive > self._idletimeout:
688 698 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
689 699 return True
690 700 return False
691 701
692 702 def newconnection(self):
693 703 self._lastactive = time.time()
694 704
695 705 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
696 706 return chgcmdserver(
697 707 self.ui,
698 708 repo,
699 709 fin,
700 710 fout,
701 711 conn,
702 712 prereposetups,
703 713 self._hashstate,
704 714 self._baseaddress,
705 715 )
706 716
707 717
708 718 def chgunixservice(ui, repo, opts):
709 719 # CHGINTERNALMARK is set by chg client. It is an indication of things are
710 720 # started by chg so other code can do things accordingly, like disabling
711 721 # demandimport or detecting chg client started by chg client. When executed
712 722 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
713 723 # environ cleaner.
714 724 if b'CHGINTERNALMARK' in encoding.environ:
715 725 del encoding.environ[b'CHGINTERNALMARK']
716 726 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
717 727 # it thinks the current value is "C". This breaks the hash computation and
718 728 # causes chg to restart loop.
719 729 if b'CHGORIG_LC_CTYPE' in encoding.environ:
720 730 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
721 731 del encoding.environ[b'CHGORIG_LC_CTYPE']
722 732 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
723 733 if b'LC_CTYPE' in encoding.environ:
724 734 del encoding.environ[b'LC_CTYPE']
725 735 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
726 736
727 737 if repo:
728 738 # one chgserver can serve multiple repos. drop repo information
729 739 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
730 740 h = chgunixservicehandler(ui)
731 741 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,418 +1,461 b''
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 no stdout data should be printed after pager quits, and the buffered data
156 should never persist (issue6207)
157
158 "killed!" may be printed if terminated by SIGPIPE, which isn't important
159 in this test.
160
161 $ cat > $TESTTMP/bulkwrite.py <<'EOF'
162 > import time
163 > from mercurial import error, registrar
164 > cmdtable = {}
165 > command = registrar.command(cmdtable)
166 > @command(b'bulkwrite')
167 > def bulkwrite(ui, repo, *pats, **opts):
168 > ui.write(b'going to write massive data\n')
169 > ui.flush()
170 > t = time.time()
171 > while time.time() - t < 2:
172 > ui.write(b'x' * 1023 + b'\n') # will be interrupted by SIGPIPE
173 > raise error.Abort(b"write() doesn't block")
174 > EOF
175
176 $ cat > $TESTTMP/fakepager.py <<'EOF'
177 > import sys
178 > import time
179 > sys.stdout.write('paged! %r\n' % sys.stdin.readline())
180 > time.sleep(1) # new data will be written
181 > EOF
182
183 $ cat >> .hg/hgrc <<EOF
184 > [extensions]
185 > bulkwrite = $TESTTMP/bulkwrite.py
186 > EOF
187
188 $ chg bulkwrite --pager=on --color no --config ui.formatted=True
189 paged! 'going to write massive data\n'
190 killed! (?)
191 [255]
192
193 $ chg bulkwrite --pager=on --color no --config ui.formatted=True
194 paged! 'going to write massive data\n'
195 killed! (?)
196 [255]
197
155 198 $ cd ..
156 199
157 200 server lifecycle
158 201 ----------------
159 202
160 203 chg server should be restarted on code change, and old server will shut down
161 204 automatically. In this test, we use the following time parameters:
162 205
163 206 - "sleep 1" to make mtime different
164 207 - "sleep 2" to notice mtime change (polling interval is 1 sec)
165 208
166 209 set up repository with an extension:
167 210
168 211 $ chg init extreload
169 212 $ cd extreload
170 213 $ touch dummyext.py
171 214 $ cat <<EOF >> .hg/hgrc
172 215 > [extensions]
173 216 > dummyext = dummyext.py
174 217 > EOF
175 218
176 219 isolate socket directory for stable result:
177 220
178 221 $ OLDCHGSOCKNAME=$CHGSOCKNAME
179 222 $ mkdir chgsock
180 223 $ CHGSOCKNAME=`pwd`/chgsock/server
181 224
182 225 warm up server:
183 226
184 227 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
185 228 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
186 229
187 230 new server should be started if extension modified:
188 231
189 232 $ sleep 1
190 233 $ touch dummyext.py
191 234 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
192 235 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
193 236 chg: debug: * instruction: reconnect (glob)
194 237 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
195 238
196 239 old server will shut down, while new server should still be reachable:
197 240
198 241 $ sleep 2
199 242 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
200 243
201 244 socket file should never be unlinked by old server:
202 245 (simulates unowned socket by updating mtime, which makes sure server exits
203 246 at polling cycle)
204 247
205 248 $ ls chgsock/server-*
206 249 chgsock/server-* (glob)
207 250 $ touch chgsock/server-*
208 251 $ sleep 2
209 252 $ ls chgsock/server-*
210 253 chgsock/server-* (glob)
211 254
212 255 since no server is reachable from socket file, new server should be started:
213 256 (this test makes sure that old server shut down automatically)
214 257
215 258 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
216 259 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
217 260
218 261 shut down servers and restore environment:
219 262
220 263 $ rm -R chgsock
221 264 $ sleep 2
222 265 $ CHGSOCKNAME=$OLDCHGSOCKNAME
223 266 $ cd ..
224 267
225 268 check that server events are recorded:
226 269
227 270 $ ls log
228 271 server.log
229 272 server.log.1
230 273
231 274 print only the last 10 lines, since we aren't sure how many records are
232 275 preserved (since setprocname isn't available on py3 and pure version,
233 276 the 10th-most-recent line is different when using py3):
234 277
235 278 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
236 279 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ... (no-setprocname !)
237 280 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
238 281 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ... (setprocname !)
239 282 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
240 283 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
241 284 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
242 285 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
243 286 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
244 287 YYYY/MM/DD HH:MM:SS (PID)> validate: []
245 288 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
246 289 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
247 290
248 291 global data mutated by schems
249 292 -----------------------------
250 293
251 294 $ hg init schemes
252 295 $ cd schemes
253 296
254 297 initial state
255 298
256 299 $ cat > .hg/hgrc <<'EOF'
257 300 > [extensions]
258 301 > schemes =
259 302 > [schemes]
260 303 > foo = https://foo.example.org/
261 304 > EOF
262 305 $ hg debugexpandscheme foo://expanded
263 306 https://foo.example.org/expanded
264 307 $ hg debugexpandscheme bar://unexpanded
265 308 bar://unexpanded
266 309
267 310 add bar
268 311
269 312 $ cat > .hg/hgrc <<'EOF'
270 313 > [extensions]
271 314 > schemes =
272 315 > [schemes]
273 316 > foo = https://foo.example.org/
274 317 > bar = https://bar.example.org/
275 318 > EOF
276 319 $ hg debugexpandscheme foo://expanded
277 320 https://foo.example.org/expanded
278 321 $ hg debugexpandscheme bar://expanded
279 322 https://bar.example.org/expanded
280 323
281 324 remove foo
282 325
283 326 $ cat > .hg/hgrc <<'EOF'
284 327 > [extensions]
285 328 > schemes =
286 329 > [schemes]
287 330 > bar = https://bar.example.org/
288 331 > EOF
289 332 $ hg debugexpandscheme foo://unexpanded
290 333 foo://unexpanded
291 334 $ hg debugexpandscheme bar://expanded
292 335 https://bar.example.org/expanded
293 336
294 337 $ cd ..
295 338
296 339 repository cache
297 340 ----------------
298 341
299 342 $ rm log/server.log*
300 343 $ cp $HGRCPATH.unconfigured $HGRCPATH
301 344 $ cat <<'EOF' >> $HGRCPATH
302 345 > [cmdserver]
303 346 > log = $TESTTMP/log/server.log
304 347 > max-repo-cache = 1
305 348 > track-log = command, repocache
306 349 > EOF
307 350
308 351 isolate socket directory for stable result:
309 352
310 353 $ OLDCHGSOCKNAME=$CHGSOCKNAME
311 354 $ mkdir chgsock
312 355 $ CHGSOCKNAME=`pwd`/chgsock/server
313 356
314 357 create empty repo and cache it:
315 358
316 359 $ hg init cached
317 360 $ hg id -R cached
318 361 000000000000 tip
319 362 $ sleep 1
320 363
321 364 modify repo (and cache will be invalidated):
322 365
323 366 $ touch cached/a
324 367 $ hg ci -R cached -Am 'add a'
325 368 adding a
326 369 $ sleep 1
327 370
328 371 read cached repo:
329 372
330 373 $ hg log -R cached
331 374 changeset: 0:ac82d8b1f7c4
332 375 tag: tip
333 376 user: test
334 377 date: Thu Jan 01 00:00:00 1970 +0000
335 378 summary: add a
336 379
337 380 $ sleep 1
338 381
339 382 discard cached from LRU cache:
340 383
341 384 $ hg clone cached cached2
342 385 updating to branch default
343 386 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
344 387 $ hg id -R cached2
345 388 ac82d8b1f7c4 tip
346 389 $ sleep 1
347 390
348 391 read uncached repo:
349 392
350 393 $ hg log -R cached
351 394 changeset: 0:ac82d8b1f7c4
352 395 tag: tip
353 396 user: test
354 397 date: Thu Jan 01 00:00:00 1970 +0000
355 398 summary: add a
356 399
357 400 $ sleep 1
358 401
359 402 shut down servers and restore environment:
360 403
361 404 $ rm -R chgsock
362 405 $ sleep 2
363 406 $ CHGSOCKNAME=$OLDCHGSOCKNAME
364 407
365 408 check server log:
366 409
367 410 $ cat log/server.log | filterlog
368 411 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
369 412 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
370 413 YYYY/MM/DD HH:MM:SS (PID)> init cached
371 414 YYYY/MM/DD HH:MM:SS (PID)> id -R cached
372 415 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
373 416 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
374 417 YYYY/MM/DD HH:MM:SS (PID)> ci -R cached -Am 'add a'
375 418 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
376 419 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
377 420 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
378 421 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
379 422 YYYY/MM/DD HH:MM:SS (PID)> clone cached cached2
380 423 YYYY/MM/DD HH:MM:SS (PID)> id -R cached2
381 424 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached2 (in ...s)
382 425 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
383 426 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
384 427
385 428 Test that chg works (sets to the user's actual LC_CTYPE) even when python
386 429 "coerces" the locale (py3.7+)
387 430
388 431 $ cat > $TESTTMP/debugenv.py <<EOF
389 432 > from mercurial import encoding
390 433 > from mercurial import registrar
391 434 > cmdtable = {}
392 435 > command = registrar.command(cmdtable)
393 436 > @command(b'debugenv', [], b'', norepo=True)
394 437 > def debugenv(ui):
395 438 > for k in [b'LC_ALL', b'LC_CTYPE', b'LANG']:
396 439 > v = encoding.environ.get(k)
397 440 > if v is not None:
398 441 > ui.write(b'%s=%s\n' % (k, encoding.environ[k]))
399 442 > EOF
400 443 (hg keeps python's modified LC_CTYPE, chg doesn't)
401 444 $ (unset LC_ALL; unset LANG; LC_CTYPE= "$CHGHG" \
402 445 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
403 446 LC_CTYPE=C.UTF-8 (py37 !)
404 447 LC_CTYPE= (no-py37 !)
405 448 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
406 449 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
407 450 LC_CTYPE=
408 451 $ (unset LC_ALL; unset LANG; LC_CTYPE=unsupported_value chg \
409 452 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
410 453 LC_CTYPE=unsupported_value
411 454 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
412 455 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
413 456 LC_CTYPE=
414 457 $ LANG= LC_ALL= LC_CTYPE= chg \
415 458 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv
416 459 LC_ALL=
417 460 LC_CTYPE=
418 461 LANG=
General Comments 0
You need to be logged in to leave comments. Login now