##// END OF EJS Templates
pytype: stop excluding chgserver.py...
Matt Harbison -
r49317:7caaefa4 default
parent child Browse files
Show More
@@ -1,761 +1,768 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 from .node import hex
58 58
59 59 from . import (
60 60 commandserver,
61 61 encoding,
62 62 error,
63 63 extensions,
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 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, util.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 elif pycompat.ispy3:
413 413 # On Python 3, the standard library doesn't offer line-buffered
414 414 # binary streams, so wrap/unwrap it.
415 415 if fp.isatty():
416 416 newfp = procutil.make_line_buffered(fp)
417 417 else:
418 418 newfp = procutil.unwrap_line_buffered(fp)
419 419 else:
420 420 # Python 2 uses the I/O streams provided by the C library, so
421 421 # make it line-buffered explicitly. Otherwise the default would
422 422 # be decided on first write(), where fout could be a pager.
423 423 if fp.isatty():
424 424 bufsize = 1 # line buffered
425 425 else:
426 426 bufsize = -1 # system default
427 427 newfp = os.fdopen(fp.fileno(), mode, bufsize)
428 428 if newfp is not fp:
429 429 setattr(ui, fn, newfp)
430 430 setattr(self, cn, newfp)
431 431
432 432 self._ioattached = True
433 433 self.cresult.write(struct.pack(b'>i', len(clientfds)))
434 434
435 435 def _saveio(self):
436 436 if self._oldios:
437 437 return
438 438 ui = self.ui
439 439 for cn, fn, _mode in _iochannels:
440 440 ch = getattr(self, cn)
441 441 fp = getattr(ui, fn)
442 442 fd = os.dup(fp.fileno())
443 443 self._oldios.append((ch, fp, fd))
444 444
445 445 def _restoreio(self):
446 446 if not self._oldios:
447 447 return
448 448 nullfd = os.open(os.devnull, os.O_WRONLY)
449 449 ui = self.ui
450 450 for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
451 451 newfp = getattr(ui, fn)
452 452 # On Python 2, newfp and fp may be separate file objects associated
453 453 # with the same fd, so we must close newfp while it's associated
454 454 # with the client. Otherwise the new associated fd would be closed
455 455 # when newfp gets deleted. On Python 3, newfp is just a wrapper
456 456 # around fp even if newfp is not fp, so deleting newfp is safe.
457 457 if not (pycompat.ispy3 or newfp is fp):
458 458 newfp.close()
459 459 # restore original fd: fp is open again
460 460 try:
461 461 if (pycompat.ispy3 or newfp is fp) and 'w' in mode:
462 462 # Discard buffered data which couldn't be flushed because
463 463 # of EPIPE. The data should belong to the current session
464 464 # and should never persist.
465 465 os.dup2(nullfd, fp.fileno())
466 466 fp.flush()
467 467 os.dup2(fd, fp.fileno())
468 468 except OSError as err:
469 469 # According to issue6330, running chg on heavy loaded systems
470 470 # can lead to EBUSY. [man dup2] indicates that, on Linux,
471 471 # EBUSY comes from a race condition between open() and dup2().
472 472 # However it's not clear why open() race occurred for
473 473 # newfd=stdin/out/err.
474 474 self.ui.log(
475 475 b'chgserver',
476 476 b'got %s while duplicating %s\n',
477 477 stringutil.forcebytestr(err),
478 478 fn,
479 479 )
480 480 os.close(fd)
481 481 setattr(self, cn, ch)
482 482 setattr(ui, fn, fp)
483 483 os.close(nullfd)
484 484 del self._oldios[:]
485 485
486 486 def validate(self):
487 487 """Reload the config and check if the server is up to date
488 488
489 489 Read a list of '\0' separated arguments.
490 490 Write a non-empty list of '\0' separated instruction strings or '\0'
491 491 if the list is empty.
492 492 An instruction string could be either:
493 493 - "unlink $path", the client should unlink the path to stop the
494 494 outdated server.
495 495 - "redirect $path", the client should attempt to connect to $path
496 496 first. If it does not work, start a new server. It implies
497 497 "reconnect".
498 498 - "exit $n", the client should exit directly with code n.
499 499 This may happen if we cannot parse the config.
500 500 - "reconnect", the client should close the connection and
501 501 reconnect.
502 502 If neither "reconnect" nor "redirect" is included in the instruction
503 503 list, the client can continue with this server after completing all
504 504 the instructions.
505 505 """
506 506 args = self._readlist()
507 507 errorraised = False
508 508 detailed_exit_code = 255
509 509 try:
510 510 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
511 511 except error.RepoError as inst:
512 512 # RepoError can be raised while trying to read shared source
513 513 # configuration
514 514 self.ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
515 515 if inst.hint:
516 516 self.ui.error(_(b"(%s)\n") % inst.hint)
517 517 errorraised = True
518 518 except error.Error as inst:
519 519 if inst.detailed_exit_code is not None:
520 520 detailed_exit_code = inst.detailed_exit_code
521 521 self.ui.error(inst.format())
522 522 errorraised = True
523 523
524 524 if errorraised:
525 525 self.ui.flush()
526 526 exit_code = 255
527 527 if self.ui.configbool(b'ui', b'detailed-exit-code'):
528 528 exit_code = detailed_exit_code
529 529 self.cresult.write(b'exit %d' % exit_code)
530 530 return
531 531 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
532 532 insts = []
533 533 if newhash.mtimehash != self.hashstate.mtimehash:
534 534 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
535 535 insts.append(b'unlink %s' % addr)
536 536 # mtimehash is empty if one or more extensions fail to load.
537 537 # to be compatible with hg, still serve the client this time.
538 538 if self.hashstate.mtimehash:
539 539 insts.append(b'reconnect')
540 540 if newhash.confighash != self.hashstate.confighash:
541 541 addr = _hashaddress(self.baseaddress, newhash.confighash)
542 542 insts.append(b'redirect %s' % addr)
543 543 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
544 544 self.cresult.write(b'\0'.join(insts) or b'\0')
545 545
546 546 def chdir(self):
547 547 """Change current directory
548 548
549 549 Note that the behavior of --cwd option is bit different from this.
550 550 It does not affect --config parameter.
551 551 """
552 552 path = self._readstr()
553 553 if not path:
554 554 return
555 555 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
556 556 os.chdir(path)
557 557
558 558 def setumask(self):
559 559 """Change umask (DEPRECATED)"""
560 560 # BUG: this does not follow the message frame structure, but kept for
561 561 # backward compatibility with old chg clients for some time
562 562 self._setumask(self._read(4))
563 563
564 564 def setumask2(self):
565 565 """Change umask"""
566 566 data = self._readstr()
567 567 if len(data) != 4:
568 568 raise ValueError(b'invalid mask length in setumask2 request')
569 569 self._setumask(data)
570 570
571 571 def _setumask(self, data):
572 572 mask = struct.unpack(b'>I', data)[0]
573 573 self.ui.log(b'chgserver', b'setumask %r\n', mask)
574 574 util.setumask(mask)
575 575
576 576 def runcommand(self):
577 577 # pager may be attached within the runcommand session, which should
578 578 # be detached at the end of the session. otherwise the pager wouldn't
579 579 # receive EOF.
580 580 globaloldios = self._oldios
581 581 self._oldios = []
582 582 try:
583 583 return super(chgcmdserver, self).runcommand()
584 584 finally:
585 585 self._restoreio()
586 586 self._oldios = globaloldios
587 587
588 588 def setenv(self):
589 589 """Clear and update os.environ
590 590
591 591 Note that not all variables can make an effect on the running process.
592 592 """
593 593 l = self._readlist()
594 594 try:
595 595 newenv = dict(s.split(b'=', 1) for s in l)
596 596 except ValueError:
597 597 raise ValueError(b'unexpected value in setenv request')
598 598 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
599 599
600 600 encoding.environ.clear()
601 601 encoding.environ.update(newenv)
602 602
603 603 capabilities = commandserver.server.capabilities.copy()
604 604 capabilities.update(
605 605 {
606 606 b'attachio': attachio,
607 607 b'chdir': chdir,
608 608 b'runcommand': runcommand,
609 609 b'setenv': setenv,
610 610 b'setumask': setumask,
611 611 b'setumask2': setumask2,
612 612 }
613 613 )
614 614
615 615 if util.safehasattr(procutil, b'setprocname'):
616 616
617 617 def setprocname(self):
618 618 """Change process title"""
619 619 name = self._readstr()
620 620 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
621 621 procutil.setprocname(name)
622 622
623 623 capabilities[b'setprocname'] = setprocname
624 624
625 625
626 626 def _tempaddress(address):
627 627 return b'%s.%d.tmp' % (address, os.getpid())
628 628
629 629
630 630 def _hashaddress(address, hashstr):
631 631 # if the basename of address contains '.', use only the left part. this
632 632 # makes it possible for the client to pass 'server.tmp$PID' and follow by
633 633 # an atomic rename to avoid locking when spawning new servers.
634 634 dirname, basename = os.path.split(address)
635 635 basename = basename.split(b'.', 1)[0]
636 636 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
637 637
638 638
639 639 class chgunixservicehandler(object):
640 640 """Set of operations for chg services"""
641 641
642 642 pollinterval = 1 # [sec]
643 643
644 644 def __init__(self, ui):
645 645 self.ui = ui
646
647 # TODO: use PEP 526 syntax (`_hashstate: hashstate` at the class level)
648 # when 3.5 support is dropped.
649 self._hashstate = None # type: hashstate
650 self._baseaddress = None # type: bytes
651 self._realaddress = None # type: bytes
652
646 653 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
647 654 self._lastactive = time.time()
648 655
649 656 def bindsocket(self, sock, address):
650 657 self._inithashstate(address)
651 658 self._checkextensions()
652 659 self._bind(sock)
653 660 self._createsymlink()
654 661 # no "listening at" message should be printed to simulate hg behavior
655 662
656 663 def _inithashstate(self, address):
657 664 self._baseaddress = address
658 665 if self.ui.configbool(b'chgserver', b'skiphash'):
659 666 self._hashstate = None
660 667 self._realaddress = address
661 668 return
662 669 self._hashstate = hashstate.fromui(self.ui)
663 670 self._realaddress = _hashaddress(address, self._hashstate.confighash)
664 671
665 672 def _checkextensions(self):
666 673 if not self._hashstate:
667 674 return
668 675 if extensions.notloaded():
669 676 # one or more extensions failed to load. mtimehash becomes
670 677 # meaningless because we do not know the paths of those extensions.
671 678 # set mtimehash to an illegal hash value to invalidate the server.
672 679 self._hashstate.mtimehash = b''
673 680
674 681 def _bind(self, sock):
675 682 # use a unique temp address so we can stat the file and do ownership
676 683 # check later
677 684 tempaddress = _tempaddress(self._realaddress)
678 685 util.bindunixsocket(sock, tempaddress)
679 686 self._socketstat = os.stat(tempaddress)
680 687 sock.listen(socket.SOMAXCONN)
681 688 # rename will replace the old socket file if exists atomically. the
682 689 # old server will detect ownership change and exit.
683 690 util.rename(tempaddress, self._realaddress)
684 691
685 692 def _createsymlink(self):
686 693 if self._baseaddress == self._realaddress:
687 694 return
688 695 tempaddress = _tempaddress(self._baseaddress)
689 696 os.symlink(os.path.basename(self._realaddress), tempaddress)
690 697 util.rename(tempaddress, self._baseaddress)
691 698
692 699 def _issocketowner(self):
693 700 try:
694 701 st = os.stat(self._realaddress)
695 702 return (
696 703 st.st_ino == self._socketstat.st_ino
697 704 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
698 705 )
699 706 except OSError:
700 707 return False
701 708
702 709 def unlinksocket(self, address):
703 710 if not self._issocketowner():
704 711 return
705 712 # it is possible to have a race condition here that we may
706 713 # remove another server's socket file. but that's okay
707 714 # since that server will detect and exit automatically and
708 715 # the client will start a new server on demand.
709 716 util.tryunlink(self._realaddress)
710 717
711 718 def shouldexit(self):
712 719 if not self._issocketowner():
713 720 self.ui.log(
714 721 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
715 722 )
716 723 return True
717 724 if time.time() - self._lastactive > self._idletimeout:
718 725 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
719 726 return True
720 727 return False
721 728
722 729 def newconnection(self):
723 730 self._lastactive = time.time()
724 731
725 732 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
726 733 return chgcmdserver(
727 734 self.ui,
728 735 repo,
729 736 fin,
730 737 fout,
731 738 conn,
732 739 prereposetups,
733 740 self._hashstate,
734 741 self._baseaddress,
735 742 )
736 743
737 744
738 745 def chgunixservice(ui, repo, opts):
739 746 # CHGINTERNALMARK is set by chg client. It is an indication of things are
740 747 # started by chg so other code can do things accordingly, like disabling
741 748 # demandimport or detecting chg client started by chg client. When executed
742 749 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
743 750 # environ cleaner.
744 751 if b'CHGINTERNALMARK' in encoding.environ:
745 752 del encoding.environ[b'CHGINTERNALMARK']
746 753 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
747 754 # it thinks the current value is "C". This breaks the hash computation and
748 755 # causes chg to restart loop.
749 756 if b'CHGORIG_LC_CTYPE' in encoding.environ:
750 757 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
751 758 del encoding.environ[b'CHGORIG_LC_CTYPE']
752 759 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
753 760 if b'LC_CTYPE' in encoding.environ:
754 761 del encoding.environ[b'LC_CTYPE']
755 762 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
756 763
757 764 if repo:
758 765 # one chgserver can serve multiple repos. drop repo information
759 766 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
760 767 h = chgunixservicehandler(ui)
761 768 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,83 +1,81 b''
1 1 #require pytype py3 slow
2 2
3 3 $ cd $RUNTESTDIR/..
4 4
5 5 Many of the individual files that are excluded here confuse pytype
6 6 because they do a mix of Python 2 and Python 3 things
7 7 conditionally. There's no good way to help it out with that as far as
8 8 I can tell, so let's just hide those files from it for now. We should
9 9 endeavor to empty this list out over time, as some of these are
10 10 probably hiding real problems.
11 11
12 12 mercurial/bundlerepo.py # no vfs and ui attrs on bundlerepo
13 mercurial/chgserver.py # [attribute-error]
14 13 mercurial/context.py # many [attribute-error]
15 14 mercurial/crecord.py # tons of [attribute-error], [module-attr]
16 15 mercurial/debugcommands.py # [wrong-arg-types]
17 16 mercurial/dispatch.py # initstdio: No attribute ... on TextIO [attribute-error]
18 17 mercurial/exchange.py # [attribute-error]
19 18 mercurial/hgweb/hgweb_mod.py # [attribute-error], [name-error], [wrong-arg-types]
20 19 mercurial/hgweb/server.py # [attribute-error], [name-error], [module-attr]
21 20 mercurial/hgweb/wsgicgi.py # confused values in os.environ
22 21 mercurial/httppeer.py # [attribute-error], [wrong-arg-types]
23 22 mercurial/interfaces # No attribute 'capabilities' on peer [attribute-error]
24 23 mercurial/keepalive.py # [attribute-error]
25 24 mercurial/localrepo.py # [attribute-error]
26 25 mercurial/manifest.py # [unsupported-operands], [wrong-arg-types]
27 26 mercurial/minirst.py # [unsupported-operands], [attribute-error]
28 27 mercurial/pure/osutil.py # [invalid-typevar], [not-callable]
29 28 mercurial/pure/parsers.py # [attribute-error]
30 29 mercurial/pycompat.py # bytes vs str issues
31 30 mercurial/repoview.py # [attribute-error]
32 31 mercurial/sslutil.py # [attribute-error]
33 32 mercurial/testing/storage.py # tons of [attribute-error]
34 33 mercurial/ui.py # [attribute-error], [wrong-arg-types]
35 34 mercurial/unionrepo.py # ui, svfs, unfiltered [attribute-error]
36 35 mercurial/utils/procutil.py # [attribute-error], [module-attr], [bad-return-type]
37 36 mercurial/utils/memorytop.py # not 3.6 compatible
38 37 mercurial/win32.py # [not-callable]
39 38 mercurial/wireprotoframing.py # [unsupported-operands], [attribute-error], [import-error]
40 39 mercurial/wireprotoserver.py # line 253, in _availableapis: No attribute '__iter__' on Callable[[Any, Any], Any] [attribute-error]
41 40 mercurial/wireprotov1peer.py # [attribute-error]
42 41 mercurial/wireprotov1server.py # BUG?: BundleValueError handler accesses subclass's attrs
43 42
44 43 TODO: use --no-cache on test server? Caching the files locally helps during
45 44 development, but may be a hinderance for CI testing.
46 45
47 46 $ pytype -V 3.6 --keep-going --jobs auto mercurial \
48 47 > -x mercurial/bundlerepo.py \
49 > -x mercurial/chgserver.py \
50 48 > -x mercurial/context.py \
51 49 > -x mercurial/crecord.py \
52 50 > -x mercurial/debugcommands.py \
53 51 > -x mercurial/dispatch.py \
54 52 > -x mercurial/exchange.py \
55 53 > -x mercurial/hgweb/hgweb_mod.py \
56 54 > -x mercurial/hgweb/server.py \
57 55 > -x mercurial/hgweb/wsgicgi.py \
58 56 > -x mercurial/httppeer.py \
59 57 > -x mercurial/interfaces \
60 58 > -x mercurial/keepalive.py \
61 59 > -x mercurial/localrepo.py \
62 60 > -x mercurial/manifest.py \
63 61 > -x mercurial/minirst.py \
64 62 > -x mercurial/pure/osutil.py \
65 63 > -x mercurial/pure/parsers.py \
66 64 > -x mercurial/pycompat.py \
67 65 > -x mercurial/repoview.py \
68 66 > -x mercurial/sslutil.py \
69 67 > -x mercurial/testing/storage.py \
70 68 > -x mercurial/thirdparty \
71 69 > -x mercurial/ui.py \
72 70 > -x mercurial/unionrepo.py \
73 71 > -x mercurial/utils/procutil.py \
74 72 > -x mercurial/utils/memorytop.py \
75 73 > -x mercurial/win32.py \
76 74 > -x mercurial/wireprotoframing.py \
77 75 > -x mercurial/wireprotoserver.py \
78 76 > -x mercurial/wireprotov1peer.py \
79 77 > -x mercurial/wireprotov1server.py \
80 78 > > $TESTTMP/pytype-output.txt || cat $TESTTMP/pytype-output.txt
81 79
82 80 Only show the results on a failure, because the output on success is also
83 81 voluminous and variable.
General Comments 0
You need to be logged in to leave comments. Login now