##// END OF EJS Templates
test-show: make it compatible with chg...
Jun Wu -
r34840:110040e7 default
parent child Browse files
Show More
@@ -1,581 +1,587
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
23 23 set umask
24 24
25 25 'validate' command
26 26 reload the config and check if the server is up to date
27 27
28 28 Config
29 29 ------
30 30
31 31 ::
32 32
33 33 [chgserver]
34 34 # how long (in seconds) should an idle chg server exit
35 35 idletimeout = 3600
36 36
37 37 # whether to skip config or env change checks
38 38 skiphash = False
39 39 """
40 40
41 41 from __future__ import absolute_import
42 42
43 43 import hashlib
44 44 import inspect
45 45 import os
46 46 import re
47 47 import socket
48 48 import struct
49 49 import time
50 50
51 51 from .i18n import _
52 52
53 53 from . import (
54 54 commandserver,
55 55 encoding,
56 56 error,
57 57 extensions,
58 58 pycompat,
59 59 util,
60 60 )
61 61
62 62 _log = commandserver.log
63 63
64 64 def _hashlist(items):
65 65 """return sha1 hexdigest for a list"""
66 66 return hashlib.sha1(str(items)).hexdigest()
67 67
68 68 # sensitive config sections affecting confighash
69 69 _configsections = [
70 70 'alias', # affects global state commands.table
71 71 'eol', # uses setconfig('eol', ...)
72 72 'extdiff', # uisetup will register new commands
73 73 'extensions',
74 74 ]
75 75
76 _configsectionitems = [
77 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
78 ]
79
76 80 # sensitive environment variables affecting confighash
77 81 _envre = re.compile(r'''\A(?:
78 82 CHGHG
79 83 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
80 84 |HG(?:ENCODING|PLAIN).*
81 85 |LANG(?:UAGE)?
82 86 |LC_.*
83 87 |LD_.*
84 88 |PATH
85 89 |PYTHON.*
86 90 |TERM(?:INFO)?
87 91 |TZ
88 92 )\Z''', re.X)
89 93
90 94 def _confighash(ui):
91 95 """return a quick hash for detecting config/env changes
92 96
93 97 confighash is the hash of sensitive config items and environment variables.
94 98
95 99 for chgserver, it is designed that once confighash changes, the server is
96 100 not qualified to serve its client and should redirect the client to a new
97 101 server. different from mtimehash, confighash change will not mark the
98 102 server outdated and exit since the user can have different configs at the
99 103 same time.
100 104 """
101 105 sectionitems = []
102 106 for section in _configsections:
103 107 sectionitems.append(ui.configitems(section))
108 for section, item in _configsectionitems:
109 sectionitems.append(ui.config(section, item))
104 110 sectionhash = _hashlist(sectionitems)
105 111 envitems = [(k, v) for k, v in encoding.environ.iteritems()
106 112 if _envre.match(k)]
107 113 envhash = _hashlist(sorted(envitems))
108 114 return sectionhash[:6] + envhash[:6]
109 115
110 116 def _getmtimepaths(ui):
111 117 """get a list of paths that should be checked to detect change
112 118
113 119 The list will include:
114 120 - extensions (will not cover all files for complex extensions)
115 121 - mercurial/__version__.py
116 122 - python binary
117 123 """
118 124 modules = [m for n, m in extensions.extensions(ui)]
119 125 try:
120 126 from . import __version__
121 127 modules.append(__version__)
122 128 except ImportError:
123 129 pass
124 130 files = [pycompat.sysexecutable]
125 131 for m in modules:
126 132 try:
127 133 files.append(inspect.getabsfile(m))
128 134 except TypeError:
129 135 pass
130 136 return sorted(set(files))
131 137
132 138 def _mtimehash(paths):
133 139 """return a quick hash for detecting file changes
134 140
135 141 mtimehash calls stat on given paths and calculate a hash based on size and
136 142 mtime of each file. mtimehash does not read file content because reading is
137 143 expensive. therefore it's not 100% reliable for detecting content changes.
138 144 it's possible to return different hashes for same file contents.
139 145 it's also possible to return a same hash for different file contents for
140 146 some carefully crafted situation.
141 147
142 148 for chgserver, it is designed that once mtimehash changes, the server is
143 149 considered outdated immediately and should no longer provide service.
144 150
145 151 mtimehash is not included in confighash because we only know the paths of
146 152 extensions after importing them (there is imp.find_module but that faces
147 153 race conditions). We need to calculate confighash without importing.
148 154 """
149 155 def trystat(path):
150 156 try:
151 157 st = os.stat(path)
152 158 return (st.st_mtime, st.st_size)
153 159 except OSError:
154 160 # could be ENOENT, EPERM etc. not fatal in any case
155 161 pass
156 162 return _hashlist(map(trystat, paths))[:12]
157 163
158 164 class hashstate(object):
159 165 """a structure storing confighash, mtimehash, paths used for mtimehash"""
160 166 def __init__(self, confighash, mtimehash, mtimepaths):
161 167 self.confighash = confighash
162 168 self.mtimehash = mtimehash
163 169 self.mtimepaths = mtimepaths
164 170
165 171 @staticmethod
166 172 def fromui(ui, mtimepaths=None):
167 173 if mtimepaths is None:
168 174 mtimepaths = _getmtimepaths(ui)
169 175 confighash = _confighash(ui)
170 176 mtimehash = _mtimehash(mtimepaths)
171 177 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
172 178 return hashstate(confighash, mtimehash, mtimepaths)
173 179
174 180 def _newchgui(srcui, csystem, attachio):
175 181 class chgui(srcui.__class__):
176 182 def __init__(self, src=None):
177 183 super(chgui, self).__init__(src)
178 184 if src:
179 185 self._csystem = getattr(src, '_csystem', csystem)
180 186 else:
181 187 self._csystem = csystem
182 188
183 189 def _runsystem(self, cmd, environ, cwd, out):
184 190 # fallback to the original system method if the output needs to be
185 191 # captured (to self._buffers), or the output stream is not stdout
186 192 # (e.g. stderr, cStringIO), because the chg client is not aware of
187 193 # these situations and will behave differently (write to stdout).
188 194 if (out is not self.fout
189 195 or not util.safehasattr(self.fout, 'fileno')
190 196 or self.fout.fileno() != util.stdout.fileno()):
191 197 return util.system(cmd, environ=environ, cwd=cwd, out=out)
192 198 self.flush()
193 199 return self._csystem(cmd, util.shellenviron(environ), cwd)
194 200
195 201 def _runpager(self, cmd, env=None):
196 202 self._csystem(cmd, util.shellenviron(env), type='pager',
197 203 cmdtable={'attachio': attachio})
198 204 return True
199 205
200 206 return chgui(srcui)
201 207
202 208 def _loadnewui(srcui, args):
203 209 from . import dispatch # avoid cycle
204 210
205 211 newui = srcui.__class__.load()
206 212 for a in ['fin', 'fout', 'ferr', 'environ']:
207 213 setattr(newui, a, getattr(srcui, a))
208 214 if util.safehasattr(srcui, '_csystem'):
209 215 newui._csystem = srcui._csystem
210 216
211 217 # command line args
212 218 args = args[:]
213 219 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
214 220
215 221 # stolen from tortoisehg.util.copydynamicconfig()
216 222 for section, name, value in srcui.walkconfig():
217 223 source = srcui.configsource(section, name)
218 224 if ':' in source or source == '--config' or source.startswith('$'):
219 225 # path:line or command line, or environ
220 226 continue
221 227 newui.setconfig(section, name, value, source)
222 228
223 229 # load wd and repo config, copied from dispatch.py
224 230 cwds = dispatch._earlygetopt(['--cwd'], args)
225 231 cwd = cwds and os.path.realpath(cwds[-1]) or None
226 232 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
227 233 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
228 234
229 235 return (newui, newlui)
230 236
231 237 class channeledsystem(object):
232 238 """Propagate ui.system() request in the following format:
233 239
234 240 payload length (unsigned int),
235 241 type, '\0',
236 242 cmd, '\0',
237 243 cwd, '\0',
238 244 envkey, '=', val, '\0',
239 245 ...
240 246 envkey, '=', val
241 247
242 248 if type == 'system', waits for:
243 249
244 250 exitcode length (unsigned int),
245 251 exitcode (int)
246 252
247 253 if type == 'pager', repetitively waits for a command name ending with '\n'
248 254 and executes it defined by cmdtable, or exits the loop if the command name
249 255 is empty.
250 256 """
251 257 def __init__(self, in_, out, channel):
252 258 self.in_ = in_
253 259 self.out = out
254 260 self.channel = channel
255 261
256 262 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
257 263 args = [type, util.quotecommand(cmd), os.path.abspath(cwd or '.')]
258 264 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
259 265 data = '\0'.join(args)
260 266 self.out.write(struct.pack('>cI', self.channel, len(data)))
261 267 self.out.write(data)
262 268 self.out.flush()
263 269
264 270 if type == 'system':
265 271 length = self.in_.read(4)
266 272 length, = struct.unpack('>I', length)
267 273 if length != 4:
268 274 raise error.Abort(_('invalid response'))
269 275 rc, = struct.unpack('>i', self.in_.read(4))
270 276 return rc
271 277 elif type == 'pager':
272 278 while True:
273 279 cmd = self.in_.readline()[:-1]
274 280 if not cmd:
275 281 break
276 282 if cmdtable and cmd in cmdtable:
277 283 _log('pager subcommand: %s' % cmd)
278 284 cmdtable[cmd]()
279 285 else:
280 286 raise error.Abort(_('unexpected command: %s') % cmd)
281 287 else:
282 288 raise error.ProgrammingError('invalid S channel type: %s' % type)
283 289
284 290 _iochannels = [
285 291 # server.ch, ui.fp, mode
286 292 ('cin', 'fin', pycompat.sysstr('rb')),
287 293 ('cout', 'fout', pycompat.sysstr('wb')),
288 294 ('cerr', 'ferr', pycompat.sysstr('wb')),
289 295 ]
290 296
291 297 class chgcmdserver(commandserver.server):
292 298 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
293 299 super(chgcmdserver, self).__init__(
294 300 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
295 301 repo, fin, fout)
296 302 self.clientsock = sock
297 303 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
298 304 self.hashstate = hashstate
299 305 self.baseaddress = baseaddress
300 306 if hashstate is not None:
301 307 self.capabilities = self.capabilities.copy()
302 308 self.capabilities['validate'] = chgcmdserver.validate
303 309
304 310 def cleanup(self):
305 311 super(chgcmdserver, self).cleanup()
306 312 # dispatch._runcatch() does not flush outputs if exception is not
307 313 # handled by dispatch._dispatch()
308 314 self.ui.flush()
309 315 self._restoreio()
310 316
311 317 def attachio(self):
312 318 """Attach to client's stdio passed via unix domain socket; all
313 319 channels except cresult will no longer be used
314 320 """
315 321 # tell client to sendmsg() with 1-byte payload, which makes it
316 322 # distinctive from "attachio\n" command consumed by client.read()
317 323 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
318 324 clientfds = util.recvfds(self.clientsock.fileno())
319 325 _log('received fds: %r\n' % clientfds)
320 326
321 327 ui = self.ui
322 328 ui.flush()
323 329 first = self._saveio()
324 330 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
325 331 assert fd > 0
326 332 fp = getattr(ui, fn)
327 333 os.dup2(fd, fp.fileno())
328 334 os.close(fd)
329 335 if not first:
330 336 continue
331 337 # reset buffering mode when client is first attached. as we want
332 338 # to see output immediately on pager, the mode stays unchanged
333 339 # when client re-attached. ferr is unchanged because it should
334 340 # be unbuffered no matter if it is a tty or not.
335 341 if fn == 'ferr':
336 342 newfp = fp
337 343 else:
338 344 # make it line buffered explicitly because the default is
339 345 # decided on first write(), where fout could be a pager.
340 346 if fp.isatty():
341 347 bufsize = 1 # line buffered
342 348 else:
343 349 bufsize = -1 # system default
344 350 newfp = os.fdopen(fp.fileno(), mode, bufsize)
345 351 setattr(ui, fn, newfp)
346 352 setattr(self, cn, newfp)
347 353
348 354 self.cresult.write(struct.pack('>i', len(clientfds)))
349 355
350 356 def _saveio(self):
351 357 if self._oldios:
352 358 return False
353 359 ui = self.ui
354 360 for cn, fn, _mode in _iochannels:
355 361 ch = getattr(self, cn)
356 362 fp = getattr(ui, fn)
357 363 fd = os.dup(fp.fileno())
358 364 self._oldios.append((ch, fp, fd))
359 365 return True
360 366
361 367 def _restoreio(self):
362 368 ui = self.ui
363 369 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
364 370 newfp = getattr(ui, fn)
365 371 # close newfp while it's associated with client; otherwise it
366 372 # would be closed when newfp is deleted
367 373 if newfp is not fp:
368 374 newfp.close()
369 375 # restore original fd: fp is open again
370 376 os.dup2(fd, fp.fileno())
371 377 os.close(fd)
372 378 setattr(self, cn, ch)
373 379 setattr(ui, fn, fp)
374 380 del self._oldios[:]
375 381
376 382 def validate(self):
377 383 """Reload the config and check if the server is up to date
378 384
379 385 Read a list of '\0' separated arguments.
380 386 Write a non-empty list of '\0' separated instruction strings or '\0'
381 387 if the list is empty.
382 388 An instruction string could be either:
383 389 - "unlink $path", the client should unlink the path to stop the
384 390 outdated server.
385 391 - "redirect $path", the client should attempt to connect to $path
386 392 first. If it does not work, start a new server. It implies
387 393 "reconnect".
388 394 - "exit $n", the client should exit directly with code n.
389 395 This may happen if we cannot parse the config.
390 396 - "reconnect", the client should close the connection and
391 397 reconnect.
392 398 If neither "reconnect" nor "redirect" is included in the instruction
393 399 list, the client can continue with this server after completing all
394 400 the instructions.
395 401 """
396 402 from . import dispatch # avoid cycle
397 403
398 404 args = self._readlist()
399 405 try:
400 406 self.ui, lui = _loadnewui(self.ui, args)
401 407 except error.ParseError as inst:
402 408 dispatch._formatparse(self.ui.warn, inst)
403 409 self.ui.flush()
404 410 self.cresult.write('exit 255')
405 411 return
406 412 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
407 413 insts = []
408 414 if newhash.mtimehash != self.hashstate.mtimehash:
409 415 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
410 416 insts.append('unlink %s' % addr)
411 417 # mtimehash is empty if one or more extensions fail to load.
412 418 # to be compatible with hg, still serve the client this time.
413 419 if self.hashstate.mtimehash:
414 420 insts.append('reconnect')
415 421 if newhash.confighash != self.hashstate.confighash:
416 422 addr = _hashaddress(self.baseaddress, newhash.confighash)
417 423 insts.append('redirect %s' % addr)
418 424 _log('validate: %s\n' % insts)
419 425 self.cresult.write('\0'.join(insts) or '\0')
420 426
421 427 def chdir(self):
422 428 """Change current directory
423 429
424 430 Note that the behavior of --cwd option is bit different from this.
425 431 It does not affect --config parameter.
426 432 """
427 433 path = self._readstr()
428 434 if not path:
429 435 return
430 436 _log('chdir to %r\n' % path)
431 437 os.chdir(path)
432 438
433 439 def setumask(self):
434 440 """Change umask"""
435 441 mask = struct.unpack('>I', self._read(4))[0]
436 442 _log('setumask %r\n' % mask)
437 443 os.umask(mask)
438 444
439 445 def runcommand(self):
440 446 return super(chgcmdserver, self).runcommand()
441 447
442 448 def setenv(self):
443 449 """Clear and update os.environ
444 450
445 451 Note that not all variables can make an effect on the running process.
446 452 """
447 453 l = self._readlist()
448 454 try:
449 455 newenv = dict(s.split('=', 1) for s in l)
450 456 except ValueError:
451 457 raise ValueError('unexpected value in setenv request')
452 458 _log('setenv: %r\n' % sorted(newenv.keys()))
453 459 encoding.environ.clear()
454 460 encoding.environ.update(newenv)
455 461
456 462 capabilities = commandserver.server.capabilities.copy()
457 463 capabilities.update({'attachio': attachio,
458 464 'chdir': chdir,
459 465 'runcommand': runcommand,
460 466 'setenv': setenv,
461 467 'setumask': setumask})
462 468
463 469 if util.safehasattr(util, 'setprocname'):
464 470 def setprocname(self):
465 471 """Change process title"""
466 472 name = self._readstr()
467 473 _log('setprocname: %r\n' % name)
468 474 util.setprocname(name)
469 475 capabilities['setprocname'] = setprocname
470 476
471 477 def _tempaddress(address):
472 478 return '%s.%d.tmp' % (address, os.getpid())
473 479
474 480 def _hashaddress(address, hashstr):
475 481 # if the basename of address contains '.', use only the left part. this
476 482 # makes it possible for the client to pass 'server.tmp$PID' and follow by
477 483 # an atomic rename to avoid locking when spawning new servers.
478 484 dirname, basename = os.path.split(address)
479 485 basename = basename.split('.', 1)[0]
480 486 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
481 487
482 488 class chgunixservicehandler(object):
483 489 """Set of operations for chg services"""
484 490
485 491 pollinterval = 1 # [sec]
486 492
487 493 def __init__(self, ui):
488 494 self.ui = ui
489 495 self._idletimeout = ui.configint('chgserver', 'idletimeout')
490 496 self._lastactive = time.time()
491 497
492 498 def bindsocket(self, sock, address):
493 499 self._inithashstate(address)
494 500 self._checkextensions()
495 501 self._bind(sock)
496 502 self._createsymlink()
497 503 # no "listening at" message should be printed to simulate hg behavior
498 504
499 505 def _inithashstate(self, address):
500 506 self._baseaddress = address
501 507 if self.ui.configbool('chgserver', 'skiphash'):
502 508 self._hashstate = None
503 509 self._realaddress = address
504 510 return
505 511 self._hashstate = hashstate.fromui(self.ui)
506 512 self._realaddress = _hashaddress(address, self._hashstate.confighash)
507 513
508 514 def _checkextensions(self):
509 515 if not self._hashstate:
510 516 return
511 517 if extensions.notloaded():
512 518 # one or more extensions failed to load. mtimehash becomes
513 519 # meaningless because we do not know the paths of those extensions.
514 520 # set mtimehash to an illegal hash value to invalidate the server.
515 521 self._hashstate.mtimehash = ''
516 522
517 523 def _bind(self, sock):
518 524 # use a unique temp address so we can stat the file and do ownership
519 525 # check later
520 526 tempaddress = _tempaddress(self._realaddress)
521 527 util.bindunixsocket(sock, tempaddress)
522 528 self._socketstat = os.stat(tempaddress)
523 529 sock.listen(socket.SOMAXCONN)
524 530 # rename will replace the old socket file if exists atomically. the
525 531 # old server will detect ownership change and exit.
526 532 util.rename(tempaddress, self._realaddress)
527 533
528 534 def _createsymlink(self):
529 535 if self._baseaddress == self._realaddress:
530 536 return
531 537 tempaddress = _tempaddress(self._baseaddress)
532 538 os.symlink(os.path.basename(self._realaddress), tempaddress)
533 539 util.rename(tempaddress, self._baseaddress)
534 540
535 541 def _issocketowner(self):
536 542 try:
537 543 stat = os.stat(self._realaddress)
538 544 return (stat.st_ino == self._socketstat.st_ino and
539 545 stat.st_mtime == self._socketstat.st_mtime)
540 546 except OSError:
541 547 return False
542 548
543 549 def unlinksocket(self, address):
544 550 if not self._issocketowner():
545 551 return
546 552 # it is possible to have a race condition here that we may
547 553 # remove another server's socket file. but that's okay
548 554 # since that server will detect and exit automatically and
549 555 # the client will start a new server on demand.
550 556 util.tryunlink(self._realaddress)
551 557
552 558 def shouldexit(self):
553 559 if not self._issocketowner():
554 560 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
555 561 return True
556 562 if time.time() - self._lastactive > self._idletimeout:
557 563 self.ui.debug('being idle too long. exiting.\n')
558 564 return True
559 565 return False
560 566
561 567 def newconnection(self):
562 568 self._lastactive = time.time()
563 569
564 570 def createcmdserver(self, repo, conn, fin, fout):
565 571 return chgcmdserver(self.ui, repo, fin, fout, conn,
566 572 self._hashstate, self._baseaddress)
567 573
568 574 def chgunixservice(ui, repo, opts):
569 575 # CHGINTERNALMARK is set by chg client. It is an indication of things are
570 576 # started by chg so other code can do things accordingly, like disabling
571 577 # demandimport or detecting chg client started by chg client. When executed
572 578 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
573 579 # environ cleaner.
574 580 if 'CHGINTERNALMARK' in encoding.environ:
575 581 del encoding.environ['CHGINTERNALMARK']
576 582
577 583 if repo:
578 584 # one chgserver can serve multiple repos. drop repo information
579 585 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
580 586 h = chgunixservicehandler(ui)
581 587 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now