##// END OF EJS Templates
commandserver: turn server debug messages into logs...
Yuya Nishihara -
r40863:25e9089c default
parent child Browse files
Show More
@@ -1,636 +1,637
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 hashlib
45 45 import inspect
46 46 import os
47 47 import re
48 48 import socket
49 49 import stat
50 50 import struct
51 51 import time
52 52
53 53 from .i18n import _
54 54
55 55 from . import (
56 56 commandserver,
57 57 encoding,
58 58 error,
59 59 extensions,
60 60 node,
61 61 pycompat,
62 62 util,
63 63 )
64 64
65 65 from .utils import (
66 66 procutil,
67 67 )
68 68
69 69 def _hashlist(items):
70 70 """return sha1 hexdigest for a list"""
71 71 return node.hex(hashlib.sha1(str(items)).digest())
72 72
73 73 # sensitive config sections affecting confighash
74 74 _configsections = [
75 75 'alias', # affects global state commands.table
76 76 'eol', # uses setconfig('eol', ...)
77 77 'extdiff', # uisetup will register new commands
78 78 'extensions',
79 79 ]
80 80
81 81 _configsectionitems = [
82 82 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
83 83 ]
84 84
85 85 # sensitive environment variables affecting confighash
86 86 _envre = re.compile(r'''\A(?:
87 87 CHGHG
88 88 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
89 89 |HG(?:ENCODING|PLAIN).*
90 90 |LANG(?:UAGE)?
91 91 |LC_.*
92 92 |LD_.*
93 93 |PATH
94 94 |PYTHON.*
95 95 |TERM(?:INFO)?
96 96 |TZ
97 97 )\Z''', re.X)
98 98
99 99 def _confighash(ui):
100 100 """return a quick hash for detecting config/env changes
101 101
102 102 confighash is the hash of sensitive config items and environment variables.
103 103
104 104 for chgserver, it is designed that once confighash changes, the server is
105 105 not qualified to serve its client and should redirect the client to a new
106 106 server. different from mtimehash, confighash change will not mark the
107 107 server outdated and exit since the user can have different configs at the
108 108 same time.
109 109 """
110 110 sectionitems = []
111 111 for section in _configsections:
112 112 sectionitems.append(ui.configitems(section))
113 113 for section, item in _configsectionitems:
114 114 sectionitems.append(ui.config(section, item))
115 115 sectionhash = _hashlist(sectionitems)
116 116 # If $CHGHG is set, the change to $HG should not trigger a new chg server
117 117 if 'CHGHG' in encoding.environ:
118 118 ignored = {'HG'}
119 119 else:
120 120 ignored = set()
121 121 envitems = [(k, v) for k, v in encoding.environ.iteritems()
122 122 if _envre.match(k) and k not in ignored]
123 123 envhash = _hashlist(sorted(envitems))
124 124 return sectionhash[:6] + envhash[:6]
125 125
126 126 def _getmtimepaths(ui):
127 127 """get a list of paths that should be checked to detect change
128 128
129 129 The list will include:
130 130 - extensions (will not cover all files for complex extensions)
131 131 - mercurial/__version__.py
132 132 - python binary
133 133 """
134 134 modules = [m for n, m in extensions.extensions(ui)]
135 135 try:
136 136 from . import __version__
137 137 modules.append(__version__)
138 138 except ImportError:
139 139 pass
140 140 files = [pycompat.sysexecutable]
141 141 for m in modules:
142 142 try:
143 143 files.append(inspect.getabsfile(m))
144 144 except TypeError:
145 145 pass
146 146 return sorted(set(files))
147 147
148 148 def _mtimehash(paths):
149 149 """return a quick hash for detecting file changes
150 150
151 151 mtimehash calls stat on given paths and calculate a hash based on size and
152 152 mtime of each file. mtimehash does not read file content because reading is
153 153 expensive. therefore it's not 100% reliable for detecting content changes.
154 154 it's possible to return different hashes for same file contents.
155 155 it's also possible to return a same hash for different file contents for
156 156 some carefully crafted situation.
157 157
158 158 for chgserver, it is designed that once mtimehash changes, the server is
159 159 considered outdated immediately and should no longer provide service.
160 160
161 161 mtimehash is not included in confighash because we only know the paths of
162 162 extensions after importing them (there is imp.find_module but that faces
163 163 race conditions). We need to calculate confighash without importing.
164 164 """
165 165 def trystat(path):
166 166 try:
167 167 st = os.stat(path)
168 168 return (st[stat.ST_MTIME], st.st_size)
169 169 except OSError:
170 170 # could be ENOENT, EPERM etc. not fatal in any case
171 171 pass
172 172 return _hashlist(map(trystat, paths))[:12]
173 173
174 174 class hashstate(object):
175 175 """a structure storing confighash, mtimehash, paths used for mtimehash"""
176 176 def __init__(self, confighash, mtimehash, mtimepaths):
177 177 self.confighash = confighash
178 178 self.mtimehash = mtimehash
179 179 self.mtimepaths = mtimepaths
180 180
181 181 @staticmethod
182 182 def fromui(ui, mtimepaths=None):
183 183 if mtimepaths is None:
184 184 mtimepaths = _getmtimepaths(ui)
185 185 confighash = _confighash(ui)
186 186 mtimehash = _mtimehash(mtimepaths)
187 187 ui.log('cmdserver', 'confighash = %s mtimehash = %s\n',
188 188 confighash, mtimehash)
189 189 return hashstate(confighash, mtimehash, mtimepaths)
190 190
191 191 def _newchgui(srcui, csystem, attachio):
192 192 class chgui(srcui.__class__):
193 193 def __init__(self, src=None):
194 194 super(chgui, self).__init__(src)
195 195 if src:
196 196 self._csystem = getattr(src, '_csystem', csystem)
197 197 else:
198 198 self._csystem = csystem
199 199
200 200 def _runsystem(self, cmd, environ, cwd, out):
201 201 # fallback to the original system method if
202 202 # a. the output stream is not stdout (e.g. stderr, cStringIO),
203 203 # b. or stdout is redirected by protectstdio(),
204 204 # because the chg client is not aware of these situations and
205 205 # will behave differently (i.e. write to stdout).
206 206 if (out is not self.fout
207 207 or not util.safehasattr(self.fout, 'fileno')
208 208 or self.fout.fileno() != procutil.stdout.fileno()
209 209 or self._finoutredirected):
210 210 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
211 211 self.flush()
212 212 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
213 213
214 214 def _runpager(self, cmd, env=None):
215 215 self._csystem(cmd, procutil.shellenviron(env), type='pager',
216 216 cmdtable={'attachio': attachio})
217 217 return True
218 218
219 219 return chgui(srcui)
220 220
221 221 def _loadnewui(srcui, args, cdebug):
222 222 from . import dispatch # avoid cycle
223 223
224 224 newui = srcui.__class__.load()
225 225 for a in ['fin', 'fout', 'ferr', 'environ']:
226 226 setattr(newui, a, getattr(srcui, a))
227 227 if util.safehasattr(srcui, '_csystem'):
228 228 newui._csystem = srcui._csystem
229 229
230 230 # command line args
231 231 options = dispatch._earlyparseopts(newui, args)
232 232 dispatch._parseconfig(newui, options['config'])
233 233
234 234 # stolen from tortoisehg.util.copydynamicconfig()
235 235 for section, name, value in srcui.walkconfig():
236 236 source = srcui.configsource(section, name)
237 237 if ':' in source or source == '--config' or source.startswith('$'):
238 238 # path:line or command line, or environ
239 239 continue
240 240 newui.setconfig(section, name, value, source)
241 241
242 242 # load wd and repo config, copied from dispatch.py
243 243 cwd = options['cwd']
244 244 cwd = cwd and os.path.realpath(cwd) or None
245 245 rpath = options['repository']
246 246 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
247 247
248 248 extensions.populateui(newui)
249 249 commandserver.setuplogging(newui, fp=cdebug)
250 250 if newui is not newlui:
251 251 extensions.populateui(newlui)
252 252 commandserver.setuplogging(newlui, fp=cdebug)
253 253
254 254 return (newui, newlui)
255 255
256 256 class channeledsystem(object):
257 257 """Propagate ui.system() request in the following format:
258 258
259 259 payload length (unsigned int),
260 260 type, '\0',
261 261 cmd, '\0',
262 262 cwd, '\0',
263 263 envkey, '=', val, '\0',
264 264 ...
265 265 envkey, '=', val
266 266
267 267 if type == 'system', waits for:
268 268
269 269 exitcode length (unsigned int),
270 270 exitcode (int)
271 271
272 272 if type == 'pager', repetitively waits for a command name ending with '\n'
273 273 and executes it defined by cmdtable, or exits the loop if the command name
274 274 is empty.
275 275 """
276 276 def __init__(self, in_, out, channel):
277 277 self.in_ = in_
278 278 self.out = out
279 279 self.channel = channel
280 280
281 281 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
282 282 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or '.')]
283 283 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
284 284 data = '\0'.join(args)
285 285 self.out.write(struct.pack('>cI', self.channel, len(data)))
286 286 self.out.write(data)
287 287 self.out.flush()
288 288
289 289 if type == 'system':
290 290 length = self.in_.read(4)
291 291 length, = struct.unpack('>I', length)
292 292 if length != 4:
293 293 raise error.Abort(_('invalid response'))
294 294 rc, = struct.unpack('>i', self.in_.read(4))
295 295 return rc
296 296 elif type == 'pager':
297 297 while True:
298 298 cmd = self.in_.readline()[:-1]
299 299 if not cmd:
300 300 break
301 301 if cmdtable and cmd in cmdtable:
302 302 cmdtable[cmd]()
303 303 else:
304 304 raise error.Abort(_('unexpected command: %s') % cmd)
305 305 else:
306 306 raise error.ProgrammingError('invalid S channel type: %s' % type)
307 307
308 308 _iochannels = [
309 309 # server.ch, ui.fp, mode
310 310 ('cin', 'fin', r'rb'),
311 311 ('cout', 'fout', r'wb'),
312 312 ('cerr', 'ferr', r'wb'),
313 313 ]
314 314
315 315 class chgcmdserver(commandserver.server):
316 316 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
317 317 super(chgcmdserver, self).__init__(
318 318 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
319 319 repo, fin, fout)
320 320 self.clientsock = sock
321 321 self._ioattached = False
322 322 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
323 323 self.hashstate = hashstate
324 324 self.baseaddress = baseaddress
325 325 if hashstate is not None:
326 326 self.capabilities = self.capabilities.copy()
327 327 self.capabilities['validate'] = chgcmdserver.validate
328 328
329 329 def cleanup(self):
330 330 super(chgcmdserver, self).cleanup()
331 331 # dispatch._runcatch() does not flush outputs if exception is not
332 332 # handled by dispatch._dispatch()
333 333 self.ui.flush()
334 334 self._restoreio()
335 335 self._ioattached = False
336 336
337 337 def attachio(self):
338 338 """Attach to client's stdio passed via unix domain socket; all
339 339 channels except cresult will no longer be used
340 340 """
341 341 # tell client to sendmsg() with 1-byte payload, which makes it
342 342 # distinctive from "attachio\n" command consumed by client.read()
343 343 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
344 344 clientfds = util.recvfds(self.clientsock.fileno())
345 345 self.ui.log('chgserver', 'received fds: %r\n', clientfds)
346 346
347 347 ui = self.ui
348 348 ui.flush()
349 349 self._saveio()
350 350 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
351 351 assert fd > 0
352 352 fp = getattr(ui, fn)
353 353 os.dup2(fd, fp.fileno())
354 354 os.close(fd)
355 355 if self._ioattached:
356 356 continue
357 357 # reset buffering mode when client is first attached. as we want
358 358 # to see output immediately on pager, the mode stays unchanged
359 359 # when client re-attached. ferr is unchanged because it should
360 360 # be unbuffered no matter if it is a tty or not.
361 361 if fn == 'ferr':
362 362 newfp = fp
363 363 else:
364 364 # make it line buffered explicitly because the default is
365 365 # decided on first write(), where fout could be a pager.
366 366 if fp.isatty():
367 367 bufsize = 1 # line buffered
368 368 else:
369 369 bufsize = -1 # system default
370 370 newfp = os.fdopen(fp.fileno(), mode, bufsize)
371 371 setattr(ui, fn, newfp)
372 372 setattr(self, cn, newfp)
373 373
374 374 self._ioattached = True
375 375 self.cresult.write(struct.pack('>i', len(clientfds)))
376 376
377 377 def _saveio(self):
378 378 if self._oldios:
379 379 return
380 380 ui = self.ui
381 381 for cn, fn, _mode in _iochannels:
382 382 ch = getattr(self, cn)
383 383 fp = getattr(ui, fn)
384 384 fd = os.dup(fp.fileno())
385 385 self._oldios.append((ch, fp, fd))
386 386
387 387 def _restoreio(self):
388 388 ui = self.ui
389 389 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
390 390 newfp = getattr(ui, fn)
391 391 # close newfp while it's associated with client; otherwise it
392 392 # would be closed when newfp is deleted
393 393 if newfp is not fp:
394 394 newfp.close()
395 395 # restore original fd: fp is open again
396 396 os.dup2(fd, fp.fileno())
397 397 os.close(fd)
398 398 setattr(self, cn, ch)
399 399 setattr(ui, fn, fp)
400 400 del self._oldios[:]
401 401
402 402 def validate(self):
403 403 """Reload the config and check if the server is up to date
404 404
405 405 Read a list of '\0' separated arguments.
406 406 Write a non-empty list of '\0' separated instruction strings or '\0'
407 407 if the list is empty.
408 408 An instruction string could be either:
409 409 - "unlink $path", the client should unlink the path to stop the
410 410 outdated server.
411 411 - "redirect $path", the client should attempt to connect to $path
412 412 first. If it does not work, start a new server. It implies
413 413 "reconnect".
414 414 - "exit $n", the client should exit directly with code n.
415 415 This may happen if we cannot parse the config.
416 416 - "reconnect", the client should close the connection and
417 417 reconnect.
418 418 If neither "reconnect" nor "redirect" is included in the instruction
419 419 list, the client can continue with this server after completing all
420 420 the instructions.
421 421 """
422 422 from . import dispatch # avoid cycle
423 423
424 424 args = self._readlist()
425 425 try:
426 426 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
427 427 except error.ParseError as inst:
428 428 dispatch._formatparse(self.ui.warn, inst)
429 429 self.ui.flush()
430 430 self.cresult.write('exit 255')
431 431 return
432 432 except error.Abort as inst:
433 433 self.ui.error(_("abort: %s\n") % inst)
434 434 if inst.hint:
435 435 self.ui.error(_("(%s)\n") % inst.hint)
436 436 self.ui.flush()
437 437 self.cresult.write('exit 255')
438 438 return
439 439 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
440 440 insts = []
441 441 if newhash.mtimehash != self.hashstate.mtimehash:
442 442 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
443 443 insts.append('unlink %s' % addr)
444 444 # mtimehash is empty if one or more extensions fail to load.
445 445 # to be compatible with hg, still serve the client this time.
446 446 if self.hashstate.mtimehash:
447 447 insts.append('reconnect')
448 448 if newhash.confighash != self.hashstate.confighash:
449 449 addr = _hashaddress(self.baseaddress, newhash.confighash)
450 450 insts.append('redirect %s' % addr)
451 451 self.ui.log('chgserver', 'validate: %s\n', insts)
452 452 self.cresult.write('\0'.join(insts) or '\0')
453 453
454 454 def chdir(self):
455 455 """Change current directory
456 456
457 457 Note that the behavior of --cwd option is bit different from this.
458 458 It does not affect --config parameter.
459 459 """
460 460 path = self._readstr()
461 461 if not path:
462 462 return
463 463 self.ui.log('chgserver', 'chdir to %r\n', path)
464 464 os.chdir(path)
465 465
466 466 def setumask(self):
467 467 """Change umask (DEPRECATED)"""
468 468 # BUG: this does not follow the message frame structure, but kept for
469 469 # backward compatibility with old chg clients for some time
470 470 self._setumask(self._read(4))
471 471
472 472 def setumask2(self):
473 473 """Change umask"""
474 474 data = self._readstr()
475 475 if len(data) != 4:
476 476 raise ValueError('invalid mask length in setumask2 request')
477 477 self._setumask(data)
478 478
479 479 def _setumask(self, data):
480 480 mask = struct.unpack('>I', data)[0]
481 481 self.ui.log('chgserver', 'setumask %r\n', mask)
482 482 os.umask(mask)
483 483
484 484 def runcommand(self):
485 485 # pager may be attached within the runcommand session, which should
486 486 # be detached at the end of the session. otherwise the pager wouldn't
487 487 # receive EOF.
488 488 globaloldios = self._oldios
489 489 self._oldios = []
490 490 try:
491 491 return super(chgcmdserver, self).runcommand()
492 492 finally:
493 493 self._restoreio()
494 494 self._oldios = globaloldios
495 495
496 496 def setenv(self):
497 497 """Clear and update os.environ
498 498
499 499 Note that not all variables can make an effect on the running process.
500 500 """
501 501 l = self._readlist()
502 502 try:
503 503 newenv = dict(s.split('=', 1) for s in l)
504 504 except ValueError:
505 505 raise ValueError('unexpected value in setenv request')
506 506 self.ui.log('chgserver', 'setenv: %r\n', sorted(newenv.keys()))
507 507 encoding.environ.clear()
508 508 encoding.environ.update(newenv)
509 509
510 510 capabilities = commandserver.server.capabilities.copy()
511 511 capabilities.update({'attachio': attachio,
512 512 'chdir': chdir,
513 513 'runcommand': runcommand,
514 514 'setenv': setenv,
515 515 'setumask': setumask,
516 516 'setumask2': setumask2})
517 517
518 518 if util.safehasattr(procutil, 'setprocname'):
519 519 def setprocname(self):
520 520 """Change process title"""
521 521 name = self._readstr()
522 522 self.ui.log('chgserver', 'setprocname: %r\n', name)
523 523 procutil.setprocname(name)
524 524 capabilities['setprocname'] = setprocname
525 525
526 526 def _tempaddress(address):
527 527 return '%s.%d.tmp' % (address, os.getpid())
528 528
529 529 def _hashaddress(address, hashstr):
530 530 # if the basename of address contains '.', use only the left part. this
531 531 # makes it possible for the client to pass 'server.tmp$PID' and follow by
532 532 # an atomic rename to avoid locking when spawning new servers.
533 533 dirname, basename = os.path.split(address)
534 534 basename = basename.split('.', 1)[0]
535 535 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
536 536
537 537 class chgunixservicehandler(object):
538 538 """Set of operations for chg services"""
539 539
540 540 pollinterval = 1 # [sec]
541 541
542 542 def __init__(self, ui):
543 543 self.ui = ui
544 544 self._idletimeout = ui.configint('chgserver', 'idletimeout')
545 545 self._lastactive = time.time()
546 546
547 547 def bindsocket(self, sock, address):
548 548 self._inithashstate(address)
549 549 self._checkextensions()
550 550 self._bind(sock)
551 551 self._createsymlink()
552 552 # no "listening at" message should be printed to simulate hg behavior
553 553
554 554 def _inithashstate(self, address):
555 555 self._baseaddress = address
556 556 if self.ui.configbool('chgserver', 'skiphash'):
557 557 self._hashstate = None
558 558 self._realaddress = address
559 559 return
560 560 self._hashstate = hashstate.fromui(self.ui)
561 561 self._realaddress = _hashaddress(address, self._hashstate.confighash)
562 562
563 563 def _checkextensions(self):
564 564 if not self._hashstate:
565 565 return
566 566 if extensions.notloaded():
567 567 # one or more extensions failed to load. mtimehash becomes
568 568 # meaningless because we do not know the paths of those extensions.
569 569 # set mtimehash to an illegal hash value to invalidate the server.
570 570 self._hashstate.mtimehash = ''
571 571
572 572 def _bind(self, sock):
573 573 # use a unique temp address so we can stat the file and do ownership
574 574 # check later
575 575 tempaddress = _tempaddress(self._realaddress)
576 576 util.bindunixsocket(sock, tempaddress)
577 577 self._socketstat = os.stat(tempaddress)
578 578 sock.listen(socket.SOMAXCONN)
579 579 # rename will replace the old socket file if exists atomically. the
580 580 # old server will detect ownership change and exit.
581 581 util.rename(tempaddress, self._realaddress)
582 582
583 583 def _createsymlink(self):
584 584 if self._baseaddress == self._realaddress:
585 585 return
586 586 tempaddress = _tempaddress(self._baseaddress)
587 587 os.symlink(os.path.basename(self._realaddress), tempaddress)
588 588 util.rename(tempaddress, self._baseaddress)
589 589
590 590 def _issocketowner(self):
591 591 try:
592 592 st = os.stat(self._realaddress)
593 593 return (st.st_ino == self._socketstat.st_ino and
594 594 st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
595 595 except OSError:
596 596 return False
597 597
598 598 def unlinksocket(self, address):
599 599 if not self._issocketowner():
600 600 return
601 601 # it is possible to have a race condition here that we may
602 602 # remove another server's socket file. but that's okay
603 603 # since that server will detect and exit automatically and
604 604 # the client will start a new server on demand.
605 605 util.tryunlink(self._realaddress)
606 606
607 607 def shouldexit(self):
608 608 if not self._issocketowner():
609 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
609 self.ui.log(b'chgserver', b'%s is not owned, exiting.\n',
610 self._realaddress)
610 611 return True
611 612 if time.time() - self._lastactive > self._idletimeout:
612 self.ui.debug('being idle too long. exiting.\n')
613 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
613 614 return True
614 615 return False
615 616
616 617 def newconnection(self):
617 618 self._lastactive = time.time()
618 619
619 620 def createcmdserver(self, repo, conn, fin, fout):
620 621 return chgcmdserver(self.ui, repo, fin, fout, conn,
621 622 self._hashstate, self._baseaddress)
622 623
623 624 def chgunixservice(ui, repo, opts):
624 625 # CHGINTERNALMARK is set by chg client. It is an indication of things are
625 626 # started by chg so other code can do things accordingly, like disabling
626 627 # demandimport or detecting chg client started by chg client. When executed
627 628 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
628 629 # environ cleaner.
629 630 if 'CHGINTERNALMARK' in encoding.environ:
630 631 del encoding.environ['CHGINTERNALMARK']
631 632
632 633 if repo:
633 634 # one chgserver can serve multiple repos. drop repo information
634 635 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
635 636 h = chgunixservicehandler(ui)
636 637 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,623 +1,624
1 1 # commandserver.py - communicate with Mercurial's API over a pipe
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import gc
12 12 import os
13 13 import random
14 14 import signal
15 15 import socket
16 16 import struct
17 17 import traceback
18 18
19 19 try:
20 20 import selectors
21 21 selectors.BaseSelector
22 22 except ImportError:
23 23 from .thirdparty import selectors2 as selectors
24 24
25 25 from .i18n import _
26 26 from . import (
27 27 encoding,
28 28 error,
29 29 loggingutil,
30 30 pycompat,
31 31 util,
32 32 vfs as vfsmod,
33 33 )
34 34 from .utils import (
35 35 cborutil,
36 36 procutil,
37 37 )
38 38
39 39 class channeledoutput(object):
40 40 """
41 41 Write data to out in the following format:
42 42
43 43 data length (unsigned int),
44 44 data
45 45 """
46 46 def __init__(self, out, channel):
47 47 self.out = out
48 48 self.channel = channel
49 49
50 50 @property
51 51 def name(self):
52 52 return '<%c-channel>' % self.channel
53 53
54 54 def write(self, data):
55 55 if not data:
56 56 return
57 57 # single write() to guarantee the same atomicity as the underlying file
58 58 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
59 59 self.out.flush()
60 60
61 61 def __getattr__(self, attr):
62 62 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
63 63 raise AttributeError(attr)
64 64 return getattr(self.out, attr)
65 65
66 66 class channeledmessage(object):
67 67 """
68 68 Write encoded message and metadata to out in the following format:
69 69
70 70 data length (unsigned int),
71 71 encoded message and metadata, as a flat key-value dict.
72 72
73 73 Each message should have 'type' attribute. Messages of unknown type
74 74 should be ignored.
75 75 """
76 76
77 77 # teach ui that write() can take **opts
78 78 structured = True
79 79
80 80 def __init__(self, out, channel, encodename, encodefn):
81 81 self._cout = channeledoutput(out, channel)
82 82 self.encoding = encodename
83 83 self._encodefn = encodefn
84 84
85 85 def write(self, data, **opts):
86 86 opts = pycompat.byteskwargs(opts)
87 87 if data is not None:
88 88 opts[b'data'] = data
89 89 self._cout.write(self._encodefn(opts))
90 90
91 91 def __getattr__(self, attr):
92 92 return getattr(self._cout, attr)
93 93
94 94 class channeledinput(object):
95 95 """
96 96 Read data from in_.
97 97
98 98 Requests for input are written to out in the following format:
99 99 channel identifier - 'I' for plain input, 'L' line based (1 byte)
100 100 how many bytes to send at most (unsigned int),
101 101
102 102 The client replies with:
103 103 data length (unsigned int), 0 meaning EOF
104 104 data
105 105 """
106 106
107 107 maxchunksize = 4 * 1024
108 108
109 109 def __init__(self, in_, out, channel):
110 110 self.in_ = in_
111 111 self.out = out
112 112 self.channel = channel
113 113
114 114 @property
115 115 def name(self):
116 116 return '<%c-channel>' % self.channel
117 117
118 118 def read(self, size=-1):
119 119 if size < 0:
120 120 # if we need to consume all the clients input, ask for 4k chunks
121 121 # so the pipe doesn't fill up risking a deadlock
122 122 size = self.maxchunksize
123 123 s = self._read(size, self.channel)
124 124 buf = s
125 125 while s:
126 126 s = self._read(size, self.channel)
127 127 buf += s
128 128
129 129 return buf
130 130 else:
131 131 return self._read(size, self.channel)
132 132
133 133 def _read(self, size, channel):
134 134 if not size:
135 135 return ''
136 136 assert size > 0
137 137
138 138 # tell the client we need at most size bytes
139 139 self.out.write(struct.pack('>cI', channel, size))
140 140 self.out.flush()
141 141
142 142 length = self.in_.read(4)
143 143 length = struct.unpack('>I', length)[0]
144 144 if not length:
145 145 return ''
146 146 else:
147 147 return self.in_.read(length)
148 148
149 149 def readline(self, size=-1):
150 150 if size < 0:
151 151 size = self.maxchunksize
152 152 s = self._read(size, 'L')
153 153 buf = s
154 154 # keep asking for more until there's either no more or
155 155 # we got a full line
156 156 while s and s[-1] != '\n':
157 157 s = self._read(size, 'L')
158 158 buf += s
159 159
160 160 return buf
161 161 else:
162 162 return self._read(size, 'L')
163 163
164 164 def __iter__(self):
165 165 return self
166 166
167 167 def next(self):
168 168 l = self.readline()
169 169 if not l:
170 170 raise StopIteration
171 171 return l
172 172
173 173 __next__ = next
174 174
175 175 def __getattr__(self, attr):
176 176 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
177 177 raise AttributeError(attr)
178 178 return getattr(self.in_, attr)
179 179
180 180 _messageencoders = {
181 181 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
182 182 }
183 183
184 184 def _selectmessageencoder(ui):
185 185 # experimental config: cmdserver.message-encodings
186 186 encnames = ui.configlist(b'cmdserver', b'message-encodings')
187 187 for n in encnames:
188 188 f = _messageencoders.get(n)
189 189 if f:
190 190 return n, f
191 191 raise error.Abort(b'no supported message encodings: %s'
192 192 % b' '.join(encnames))
193 193
194 194 class server(object):
195 195 """
196 196 Listens for commands on fin, runs them and writes the output on a channel
197 197 based stream to fout.
198 198 """
199 199 def __init__(self, ui, repo, fin, fout):
200 200 self.cwd = encoding.getcwd()
201 201
202 202 if repo:
203 203 # the ui here is really the repo ui so take its baseui so we don't
204 204 # end up with its local configuration
205 205 self.ui = repo.baseui
206 206 self.repo = repo
207 207 self.repoui = repo.ui
208 208 else:
209 209 self.ui = ui
210 210 self.repo = self.repoui = None
211 211
212 212 self.cdebug = channeledoutput(fout, 'd')
213 213 self.cerr = channeledoutput(fout, 'e')
214 214 self.cout = channeledoutput(fout, 'o')
215 215 self.cin = channeledinput(fin, fout, 'I')
216 216 self.cresult = channeledoutput(fout, 'r')
217 217
218 218 if self.ui.config(b'cmdserver', b'log') == b'-':
219 219 # switch log stream of server's ui to the 'd' (debug) channel
220 220 # (don't touch repo.ui as its lifetime is longer than the server)
221 221 self.ui = self.ui.copy()
222 222 setuplogging(self.ui, repo=None, fp=self.cdebug)
223 223
224 224 # TODO: add this to help/config.txt when stabilized
225 225 # ``channel``
226 226 # Use separate channel for structured output. (Command-server only)
227 227 self.cmsg = None
228 228 if ui.config(b'ui', b'message-output') == b'channel':
229 229 encname, encfn = _selectmessageencoder(ui)
230 230 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
231 231
232 232 self.client = fin
233 233
234 234 def cleanup(self):
235 235 """release and restore resources taken during server session"""
236 236
237 237 def _read(self, size):
238 238 if not size:
239 239 return ''
240 240
241 241 data = self.client.read(size)
242 242
243 243 # is the other end closed?
244 244 if not data:
245 245 raise EOFError
246 246
247 247 return data
248 248
249 249 def _readstr(self):
250 250 """read a string from the channel
251 251
252 252 format:
253 253 data length (uint32), data
254 254 """
255 255 length = struct.unpack('>I', self._read(4))[0]
256 256 if not length:
257 257 return ''
258 258 return self._read(length)
259 259
260 260 def _readlist(self):
261 261 """read a list of NULL separated strings from the channel"""
262 262 s = self._readstr()
263 263 if s:
264 264 return s.split('\0')
265 265 else:
266 266 return []
267 267
268 268 def runcommand(self):
269 269 """ reads a list of \0 terminated arguments, executes
270 270 and writes the return code to the result channel """
271 271 from . import dispatch # avoid cycle
272 272
273 273 args = self._readlist()
274 274
275 275 # copy the uis so changes (e.g. --config or --verbose) don't
276 276 # persist between requests
277 277 copiedui = self.ui.copy()
278 278 uis = [copiedui]
279 279 if self.repo:
280 280 self.repo.baseui = copiedui
281 281 # clone ui without using ui.copy because this is protected
282 282 repoui = self.repoui.__class__(self.repoui)
283 283 repoui.copy = copiedui.copy # redo copy protection
284 284 uis.append(repoui)
285 285 self.repo.ui = self.repo.dirstate._ui = repoui
286 286 self.repo.invalidateall()
287 287
288 288 for ui in uis:
289 289 ui.resetstate()
290 290 # any kind of interaction must use server channels, but chg may
291 291 # replace channels by fully functional tty files. so nontty is
292 292 # enforced only if cin is a channel.
293 293 if not util.safehasattr(self.cin, 'fileno'):
294 294 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
295 295
296 296 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
297 297 self.cout, self.cerr, self.cmsg)
298 298
299 299 try:
300 300 ret = dispatch.dispatch(req) & 255
301 301 self.cresult.write(struct.pack('>i', int(ret)))
302 302 finally:
303 303 # restore old cwd
304 304 if '--cwd' in args:
305 305 os.chdir(self.cwd)
306 306
307 307 def getencoding(self):
308 308 """ writes the current encoding to the result channel """
309 309 self.cresult.write(encoding.encoding)
310 310
311 311 def serveone(self):
312 312 cmd = self.client.readline()[:-1]
313 313 if cmd:
314 314 handler = self.capabilities.get(cmd)
315 315 if handler:
316 316 handler(self)
317 317 else:
318 318 # clients are expected to check what commands are supported by
319 319 # looking at the servers capabilities
320 320 raise error.Abort(_('unknown command %s') % cmd)
321 321
322 322 return cmd != ''
323 323
324 324 capabilities = {'runcommand': runcommand,
325 325 'getencoding': getencoding}
326 326
327 327 def serve(self):
328 328 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
329 329 hellomsg += '\n'
330 330 hellomsg += 'encoding: ' + encoding.encoding
331 331 hellomsg += '\n'
332 332 if self.cmsg:
333 333 hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
334 334 hellomsg += 'pid: %d' % procutil.getpid()
335 335 if util.safehasattr(os, 'getpgid'):
336 336 hellomsg += '\n'
337 337 hellomsg += 'pgid: %d' % os.getpgid(0)
338 338
339 339 # write the hello msg in -one- chunk
340 340 self.cout.write(hellomsg)
341 341
342 342 try:
343 343 while self.serveone():
344 344 pass
345 345 except EOFError:
346 346 # we'll get here if the client disconnected while we were reading
347 347 # its request
348 348 return 1
349 349
350 350 return 0
351 351
352 352 def setuplogging(ui, repo=None, fp=None):
353 353 """Set up server logging facility
354 354
355 355 If cmdserver.log is '-', log messages will be sent to the given fp.
356 356 It should be the 'd' channel while a client is connected, and otherwise
357 357 is the stderr of the server process.
358 358 """
359 359 # developer config: cmdserver.log
360 360 logpath = ui.config(b'cmdserver', b'log')
361 361 if not logpath:
362 362 return
363 363 # developer config: cmdserver.track-log
364 364 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
365 365
366 366 if logpath == b'-' and fp:
367 367 logger = loggingutil.fileobjectlogger(fp, tracked)
368 368 elif logpath == b'-':
369 369 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
370 370 else:
371 371 logpath = os.path.abspath(util.expandpath(logpath))
372 372 # developer config: cmdserver.max-log-files
373 373 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
374 374 # developer config: cmdserver.max-log-size
375 375 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
376 376 vfs = vfsmod.vfs(os.path.dirname(logpath))
377 377 logger = loggingutil.filelogger(vfs, os.path.basename(logpath), tracked,
378 378 maxfiles=maxfiles, maxsize=maxsize)
379 379
380 380 targetuis = {ui}
381 381 if repo:
382 382 targetuis.add(repo.baseui)
383 383 targetuis.add(repo.ui)
384 384 for u in targetuis:
385 385 u.setlogger(b'cmdserver', logger)
386 386
387 387 class pipeservice(object):
388 388 def __init__(self, ui, repo, opts):
389 389 self.ui = ui
390 390 self.repo = repo
391 391
392 392 def init(self):
393 393 pass
394 394
395 395 def run(self):
396 396 ui = self.ui
397 397 # redirect stdio to null device so that broken extensions or in-process
398 398 # hooks will never cause corruption of channel protocol.
399 399 with procutil.protectedstdio(ui.fin, ui.fout) as (fin, fout):
400 400 sv = server(ui, self.repo, fin, fout)
401 401 try:
402 402 return sv.serve()
403 403 finally:
404 404 sv.cleanup()
405 405
406 406 def _initworkerprocess():
407 407 # use a different process group from the master process, in order to:
408 408 # 1. make the current process group no longer "orphaned" (because the
409 409 # parent of this process is in a different process group while
410 410 # remains in a same session)
411 411 # according to POSIX 2.2.2.52, orphaned process group will ignore
412 412 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
413 413 # cause trouble for things like ncurses.
414 414 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
415 415 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
416 416 # processes like ssh will be killed properly, without affecting
417 417 # unrelated processes.
418 418 os.setpgid(0, 0)
419 419 # change random state otherwise forked request handlers would have a
420 420 # same state inherited from parent.
421 421 random.seed()
422 422
423 423 def _serverequest(ui, repo, conn, createcmdserver):
424 424 fin = conn.makefile(r'rb')
425 425 fout = conn.makefile(r'wb')
426 426 sv = None
427 427 try:
428 428 sv = createcmdserver(repo, conn, fin, fout)
429 429 try:
430 430 sv.serve()
431 431 # handle exceptions that may be raised by command server. most of
432 432 # known exceptions are caught by dispatch.
433 433 except error.Abort as inst:
434 434 ui.error(_('abort: %s\n') % inst)
435 435 except IOError as inst:
436 436 if inst.errno != errno.EPIPE:
437 437 raise
438 438 except KeyboardInterrupt:
439 439 pass
440 440 finally:
441 441 sv.cleanup()
442 442 except: # re-raises
443 443 # also write traceback to error channel. otherwise client cannot
444 444 # see it because it is written to server's stderr by default.
445 445 if sv:
446 446 cerr = sv.cerr
447 447 else:
448 448 cerr = channeledoutput(fout, 'e')
449 449 cerr.write(encoding.strtolocal(traceback.format_exc()))
450 450 raise
451 451 finally:
452 452 fin.close()
453 453 try:
454 454 fout.close() # implicit flush() may cause another EPIPE
455 455 except IOError as inst:
456 456 if inst.errno != errno.EPIPE:
457 457 raise
458 458
459 459 class unixservicehandler(object):
460 460 """Set of pluggable operations for unix-mode services
461 461
462 462 Almost all methods except for createcmdserver() are called in the main
463 463 process. You can't pass mutable resource back from createcmdserver().
464 464 """
465 465
466 466 pollinterval = None
467 467
468 468 def __init__(self, ui):
469 469 self.ui = ui
470 470
471 471 def bindsocket(self, sock, address):
472 472 util.bindunixsocket(sock, address)
473 473 sock.listen(socket.SOMAXCONN)
474 474 self.ui.status(_('listening at %s\n') % address)
475 475 self.ui.flush() # avoid buffering of status message
476 476
477 477 def unlinksocket(self, address):
478 478 os.unlink(address)
479 479
480 480 def shouldexit(self):
481 481 """True if server should shut down; checked per pollinterval"""
482 482 return False
483 483
484 484 def newconnection(self):
485 485 """Called when main process notices new connection"""
486 486
487 487 def createcmdserver(self, repo, conn, fin, fout):
488 488 """Create new command server instance; called in the process that
489 489 serves for the current connection"""
490 490 return server(self.ui, repo, fin, fout)
491 491
492 492 class unixforkingservice(object):
493 493 """
494 494 Listens on unix domain socket and forks server per connection
495 495 """
496 496
497 497 def __init__(self, ui, repo, opts, handler=None):
498 498 self.ui = ui
499 499 self.repo = repo
500 500 self.address = opts['address']
501 501 if not util.safehasattr(socket, 'AF_UNIX'):
502 502 raise error.Abort(_('unsupported platform'))
503 503 if not self.address:
504 504 raise error.Abort(_('no socket path specified with --address'))
505 505 self._servicehandler = handler or unixservicehandler(ui)
506 506 self._sock = None
507 507 self._oldsigchldhandler = None
508 508 self._workerpids = set() # updated by signal handler; do not iterate
509 509 self._socketunlinked = None
510 510
511 511 def init(self):
512 512 self._sock = socket.socket(socket.AF_UNIX)
513 513 self._servicehandler.bindsocket(self._sock, self.address)
514 514 if util.safehasattr(procutil, 'unblocksignal'):
515 515 procutil.unblocksignal(signal.SIGCHLD)
516 516 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
517 517 self._oldsigchldhandler = o
518 518 self._socketunlinked = False
519 519
520 520 def _unlinksocket(self):
521 521 if not self._socketunlinked:
522 522 self._servicehandler.unlinksocket(self.address)
523 523 self._socketunlinked = True
524 524
525 525 def _cleanup(self):
526 526 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
527 527 self._sock.close()
528 528 self._unlinksocket()
529 529 # don't kill child processes as they have active clients, just wait
530 530 self._reapworkers(0)
531 531
532 532 def run(self):
533 533 try:
534 534 self._mainloop()
535 535 finally:
536 536 self._cleanup()
537 537
538 538 def _mainloop(self):
539 539 exiting = False
540 540 h = self._servicehandler
541 541 selector = selectors.DefaultSelector()
542 542 selector.register(self._sock, selectors.EVENT_READ)
543 543 while True:
544 544 if not exiting and h.shouldexit():
545 545 # clients can no longer connect() to the domain socket, so
546 546 # we stop queuing new requests.
547 547 # for requests that are queued (connect()-ed, but haven't been
548 548 # accept()-ed), handle them before exit. otherwise, clients
549 549 # waiting for recv() will receive ECONNRESET.
550 550 self._unlinksocket()
551 551 exiting = True
552 552 try:
553 553 ready = selector.select(timeout=h.pollinterval)
554 554 except OSError as inst:
555 555 # selectors2 raises ETIMEDOUT if timeout exceeded while
556 556 # handling signal interrupt. That's probably wrong, but
557 557 # we can easily get around it.
558 558 if inst.errno != errno.ETIMEDOUT:
559 559 raise
560 560 ready = []
561 561 if not ready:
562 562 # only exit if we completed all queued requests
563 563 if exiting:
564 564 break
565 565 continue
566 566 try:
567 567 conn, _addr = self._sock.accept()
568 568 except socket.error as inst:
569 569 if inst.args[0] == errno.EINTR:
570 570 continue
571 571 raise
572 572
573 573 pid = os.fork()
574 574 if pid:
575 575 try:
576 self.ui.debug('forked worker process (pid=%d)\n' % pid)
576 self.ui.log(b'cmdserver',
577 b'forked worker process (pid=%d)\n', pid)
577 578 self._workerpids.add(pid)
578 579 h.newconnection()
579 580 finally:
580 581 conn.close() # release handle in parent process
581 582 else:
582 583 try:
583 584 selector.close()
584 585 self._sock.close()
585 586 self._runworker(conn)
586 587 conn.close()
587 588 os._exit(0)
588 589 except: # never return, hence no re-raises
589 590 try:
590 591 self.ui.traceback(force=True)
591 592 finally:
592 593 os._exit(255)
593 594 selector.close()
594 595
595 596 def _sigchldhandler(self, signal, frame):
596 597 self._reapworkers(os.WNOHANG)
597 598
598 599 def _reapworkers(self, options):
599 600 while self._workerpids:
600 601 try:
601 602 pid, _status = os.waitpid(-1, options)
602 603 except OSError as inst:
603 604 if inst.errno == errno.EINTR:
604 605 continue
605 606 if inst.errno != errno.ECHILD:
606 607 raise
607 608 # no child processes at all (reaped by other waitpid()?)
608 609 self._workerpids.clear()
609 610 return
610 611 if pid == 0:
611 612 # no waitable child processes
612 613 return
613 self.ui.debug('worker process exited (pid=%d)\n' % pid)
614 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
614 615 self._workerpids.discard(pid)
615 616
616 617 def _runworker(self, conn):
617 618 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
618 619 _initworkerprocess()
619 620 h = self._servicehandler
620 621 try:
621 622 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
622 623 finally:
623 624 gc.collect() # trigger __del__ since worker process uses os._exit
@@ -1,242 +1,242
1 1 #require chg
2 2
3 3 $ mkdir log
4 4 $ cat <<'EOF' >> $HGRCPATH
5 5 > [cmdserver]
6 6 > log = $TESTTMP/log/server.log
7 7 > max-log-files = 1
8 8 > max-log-size = 10 kB
9 9 > EOF
10 10 $ cp $HGRCPATH $HGRCPATH.orig
11 11
12 12 $ filterlog () {
13 13 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
14 14 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
15 15 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
16 16 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
17 17 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
18 18 > }
19 19
20 20 init repo
21 21
22 22 $ chg init foo
23 23 $ cd foo
24 24
25 25 ill-formed config
26 26
27 27 $ chg status
28 28 $ echo '=brokenconfig' >> $HGRCPATH
29 29 $ chg status
30 30 hg: parse error at * (glob)
31 31 [255]
32 32
33 33 $ cp $HGRCPATH.orig $HGRCPATH
34 34
35 35 long socket path
36 36
37 37 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
38 38 $ mkdir -p $sockpath
39 39 $ bakchgsockname=$CHGSOCKNAME
40 40 $ CHGSOCKNAME=$sockpath/server
41 41 $ export CHGSOCKNAME
42 42 $ chg root
43 43 $TESTTMP/foo
44 44 $ rm -rf $sockpath
45 45 $ CHGSOCKNAME=$bakchgsockname
46 46 $ export CHGSOCKNAME
47 47
48 48 $ cd ..
49 49
50 50 editor
51 51 ------
52 52
53 53 $ cat >> pushbuffer.py <<EOF
54 54 > def reposetup(ui, repo):
55 55 > repo.ui.pushbuffer(subproc=True)
56 56 > EOF
57 57
58 58 $ chg init editor
59 59 $ cd editor
60 60
61 61 by default, system() should be redirected to the client:
62 62
63 63 $ touch foo
64 64 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
65 65 > | egrep "HG:|run 'cat"
66 66 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
67 67 HG: Enter commit message. Lines beginning with 'HG:' are removed.
68 68 HG: Leave message empty to abort commit.
69 69 HG: --
70 70 HG: user: test
71 71 HG: branch 'default'
72 72 HG: added foo
73 73
74 74 but no redirection should be made if output is captured:
75 75
76 76 $ touch bar
77 77 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
78 78 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
79 79 > | egrep "HG:|run 'cat"
80 80 [1]
81 81
82 82 check that commit commands succeeded:
83 83
84 84 $ hg log -T '{rev}:{desc}\n'
85 85 1:bufferred
86 86 0:channeled
87 87
88 88 $ cd ..
89 89
90 90 pager
91 91 -----
92 92
93 93 $ cat >> fakepager.py <<EOF
94 94 > import sys
95 95 > for line in sys.stdin:
96 96 > sys.stdout.write('paged! %r\n' % line)
97 97 > EOF
98 98
99 99 enable pager extension globally, but spawns the master server with no tty:
100 100
101 101 $ chg init pager
102 102 $ cd pager
103 103 $ cat >> $HGRCPATH <<EOF
104 104 > [extensions]
105 105 > pager =
106 106 > [pager]
107 107 > pager = "$PYTHON" $TESTTMP/fakepager.py
108 108 > EOF
109 109 $ chg version > /dev/null
110 110 $ touch foo
111 111 $ chg ci -qAm foo
112 112
113 113 pager should be enabled if the attached client has a tty:
114 114
115 115 $ chg log -l1 -q --config ui.formatted=True
116 116 paged! '0:1f7b0de80e11\n'
117 117 $ chg log -l1 -q --config ui.formatted=False
118 118 0:1f7b0de80e11
119 119
120 120 chg waits for pager if runcommand raises
121 121
122 122 $ cat > $TESTTMP/crash.py <<EOF
123 123 > from mercurial import registrar
124 124 > cmdtable = {}
125 125 > command = registrar.command(cmdtable)
126 126 > @command(b'crash')
127 127 > def pagercrash(ui, repo, *pats, **opts):
128 128 > ui.write('going to crash\n')
129 129 > raise Exception('.')
130 130 > EOF
131 131
132 132 $ cat > $TESTTMP/fakepager.py <<EOF
133 133 > from __future__ import absolute_import
134 134 > import sys
135 135 > import time
136 136 > for line in iter(sys.stdin.readline, ''):
137 137 > if 'crash' in line: # only interested in lines containing 'crash'
138 138 > # if chg exits when pager is sleeping (incorrectly), the output
139 139 > # will be captured by the next test case
140 140 > time.sleep(1)
141 141 > sys.stdout.write('crash-pager: %s' % line)
142 142 > EOF
143 143
144 144 $ cat >> .hg/hgrc <<EOF
145 145 > [extensions]
146 146 > crash = $TESTTMP/crash.py
147 147 > EOF
148 148
149 149 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
150 150 crash-pager: going to crash
151 151 [255]
152 152
153 153 $ cd ..
154 154
155 155 server lifecycle
156 156 ----------------
157 157
158 158 chg server should be restarted on code change, and old server will shut down
159 159 automatically. In this test, we use the following time parameters:
160 160
161 161 - "sleep 1" to make mtime different
162 162 - "sleep 2" to notice mtime change (polling interval is 1 sec)
163 163
164 164 set up repository with an extension:
165 165
166 166 $ chg init extreload
167 167 $ cd extreload
168 168 $ touch dummyext.py
169 169 $ cat <<EOF >> .hg/hgrc
170 170 > [extensions]
171 171 > dummyext = dummyext.py
172 172 > EOF
173 173
174 174 isolate socket directory for stable result:
175 175
176 176 $ OLDCHGSOCKNAME=$CHGSOCKNAME
177 177 $ mkdir chgsock
178 178 $ CHGSOCKNAME=`pwd`/chgsock/server
179 179
180 180 warm up server:
181 181
182 182 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
183 183 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
184 184
185 185 new server should be started if extension modified:
186 186
187 187 $ sleep 1
188 188 $ touch dummyext.py
189 189 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
190 190 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
191 191 chg: debug: * instruction: reconnect (glob)
192 192 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
193 193
194 194 old server will shut down, while new server should still be reachable:
195 195
196 196 $ sleep 2
197 197 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
198 198
199 199 socket file should never be unlinked by old server:
200 200 (simulates unowned socket by updating mtime, which makes sure server exits
201 201 at polling cycle)
202 202
203 203 $ ls chgsock/server-*
204 204 chgsock/server-* (glob)
205 205 $ touch chgsock/server-*
206 206 $ sleep 2
207 207 $ ls chgsock/server-*
208 208 chgsock/server-* (glob)
209 209
210 210 since no server is reachable from socket file, new server should be started:
211 211 (this test makes sure that old server shut down automatically)
212 212
213 213 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
214 214 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
215 215
216 216 shut down servers and restore environment:
217 217
218 218 $ rm -R chgsock
219 219 $ sleep 2
220 220 $ CHGSOCKNAME=$OLDCHGSOCKNAME
221 221 $ cd ..
222 222
223 223 check that server events are recorded:
224 224
225 225 $ ls log
226 226 server.log
227 227 server.log.1
228 228
229 229 print only the last 10 lines, since we aren't sure how many records are
230 230 preserved:
231 231
232 232 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
233 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
234 YYYY/MM/DD HH:MM:SS (PID)> validate: []
235 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
233 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
236 234 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ...
237 235 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
238 236 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
239 237 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
240 238 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
241 239 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
242 240 YYYY/MM/DD HH:MM:SS (PID)> validate: []
241 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
242 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
General Comments 0
You need to be logged in to leave comments. Login now