##// END OF EJS Templates
chgserver: do not ignore SIGPIPE if pager is used...
Jun Wu -
r29428:247ea0df default
parent child Browse files
Show More
@@ -1,701 +1,707
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 (EXPERIMENTAL)
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 'getpager' command
20 20 checks if pager is enabled and which pager should be executed
21 21
22 22 'setenv' command
23 23 replace os.environ completely
24 24
25 25 'setumask' command
26 26 set umask
27 27
28 28 'validate' command
29 29 reload the config and check if the server is up to date
30 30
31 31 Config
32 32 ------
33 33
34 34 ::
35 35
36 36 [chgserver]
37 37 idletimeout = 3600 # seconds, after which an idle server will exit
38 38 skiphash = False # whether to skip config or env change checks
39 39 """
40 40
41 41 from __future__ import absolute_import
42 42
43 43 import SocketServer
44 44 import errno
45 45 import gc
46 46 import hashlib
47 47 import inspect
48 48 import os
49 49 import random
50 50 import re
51 import signal
51 52 import struct
52 53 import sys
53 54 import threading
54 55 import time
55 56 import traceback
56 57
57 58 from mercurial.i18n import _
58 59
59 60 from mercurial import (
60 61 cmdutil,
61 62 commands,
62 63 commandserver,
63 64 dispatch,
64 65 error,
65 66 extensions,
66 67 osutil,
67 68 util,
68 69 )
69 70
70 71 # Note for extension authors: ONLY specify testedwith = 'internal' for
71 72 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
72 73 # be specifying the version(s) of Mercurial they are tested with, or
73 74 # leave the attribute unspecified.
74 75 testedwith = 'internal'
75 76
76 77 _log = commandserver.log
77 78
78 79 def _hashlist(items):
79 80 """return sha1 hexdigest for a list"""
80 81 return hashlib.sha1(str(items)).hexdigest()
81 82
82 83 # sensitive config sections affecting confighash
83 84 _configsections = [
84 85 'alias', # affects global state commands.table
85 86 'extdiff', # uisetup will register new commands
86 87 'extensions',
87 88 ]
88 89
89 90 # sensitive environment variables affecting confighash
90 91 _envre = re.compile(r'''\A(?:
91 92 CHGHG
92 93 |HG.*
93 94 |LANG(?:UAGE)?
94 95 |LC_.*
95 96 |LD_.*
96 97 |PATH
97 98 |PYTHON.*
98 99 |TERM(?:INFO)?
99 100 |TZ
100 101 )\Z''', re.X)
101 102
102 103 def _confighash(ui):
103 104 """return a quick hash for detecting config/env changes
104 105
105 106 confighash is the hash of sensitive config items and environment variables.
106 107
107 108 for chgserver, it is designed that once confighash changes, the server is
108 109 not qualified to serve its client and should redirect the client to a new
109 110 server. different from mtimehash, confighash change will not mark the
110 111 server outdated and exit since the user can have different configs at the
111 112 same time.
112 113 """
113 114 sectionitems = []
114 115 for section in _configsections:
115 116 sectionitems.append(ui.configitems(section))
116 117 sectionhash = _hashlist(sectionitems)
117 118 envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
118 119 envhash = _hashlist(sorted(envitems))
119 120 return sectionhash[:6] + envhash[:6]
120 121
121 122 def _getmtimepaths(ui):
122 123 """get a list of paths that should be checked to detect change
123 124
124 125 The list will include:
125 126 - extensions (will not cover all files for complex extensions)
126 127 - mercurial/__version__.py
127 128 - python binary
128 129 """
129 130 modules = [m for n, m in extensions.extensions(ui)]
130 131 try:
131 132 from mercurial import __version__
132 133 modules.append(__version__)
133 134 except ImportError:
134 135 pass
135 136 files = [sys.executable]
136 137 for m in modules:
137 138 try:
138 139 files.append(inspect.getabsfile(m))
139 140 except TypeError:
140 141 pass
141 142 return sorted(set(files))
142 143
143 144 def _mtimehash(paths):
144 145 """return a quick hash for detecting file changes
145 146
146 147 mtimehash calls stat on given paths and calculate a hash based on size and
147 148 mtime of each file. mtimehash does not read file content because reading is
148 149 expensive. therefore it's not 100% reliable for detecting content changes.
149 150 it's possible to return different hashes for same file contents.
150 151 it's also possible to return a same hash for different file contents for
151 152 some carefully crafted situation.
152 153
153 154 for chgserver, it is designed that once mtimehash changes, the server is
154 155 considered outdated immediately and should no longer provide service.
155 156 """
156 157 def trystat(path):
157 158 try:
158 159 st = os.stat(path)
159 160 return (st.st_mtime, st.st_size)
160 161 except OSError:
161 162 # could be ENOENT, EPERM etc. not fatal in any case
162 163 pass
163 164 return _hashlist(map(trystat, paths))[:12]
164 165
165 166 class hashstate(object):
166 167 """a structure storing confighash, mtimehash, paths used for mtimehash"""
167 168 def __init__(self, confighash, mtimehash, mtimepaths):
168 169 self.confighash = confighash
169 170 self.mtimehash = mtimehash
170 171 self.mtimepaths = mtimepaths
171 172
172 173 @staticmethod
173 174 def fromui(ui, mtimepaths=None):
174 175 if mtimepaths is None:
175 176 mtimepaths = _getmtimepaths(ui)
176 177 confighash = _confighash(ui)
177 178 mtimehash = _mtimehash(mtimepaths)
178 179 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
179 180 return hashstate(confighash, mtimehash, mtimepaths)
180 181
181 182 # copied from hgext/pager.py:uisetup()
182 183 def _setuppagercmd(ui, options, cmd):
183 184 if not ui.formatted():
184 185 return
185 186
186 187 p = ui.config("pager", "pager", os.environ.get("PAGER"))
187 188 usepager = False
188 189 always = util.parsebool(options['pager'])
189 190 auto = options['pager'] == 'auto'
190 191
191 192 if not p:
192 193 pass
193 194 elif always:
194 195 usepager = True
195 196 elif not auto:
196 197 usepager = False
197 198 else:
198 199 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
199 200 attend = ui.configlist('pager', 'attend', attended)
200 201 ignore = ui.configlist('pager', 'ignore')
201 202 cmds, _ = cmdutil.findcmd(cmd, commands.table)
202 203
203 204 for cmd in cmds:
204 205 var = 'attend-%s' % cmd
205 206 if ui.config('pager', var):
206 207 usepager = ui.configbool('pager', var)
207 208 break
208 209 if (cmd in attend or
209 210 (cmd not in ignore and not attend)):
210 211 usepager = True
211 212 break
212 213
213 214 if usepager:
214 215 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
215 216 ui.setconfig('ui', 'interactive', False, 'pager')
216 217 return p
217 218
218 219 def _newchgui(srcui, csystem):
219 220 class chgui(srcui.__class__):
220 221 def __init__(self, src=None):
221 222 super(chgui, self).__init__(src)
222 223 if src:
223 224 self._csystem = getattr(src, '_csystem', csystem)
224 225 else:
225 226 self._csystem = csystem
226 227
227 228 def system(self, cmd, environ=None, cwd=None, onerr=None,
228 229 errprefix=None):
229 230 # fallback to the original system method if the output needs to be
230 231 # captured (to self._buffers), or the output stream is not stdout
231 232 # (e.g. stderr, cStringIO), because the chg client is not aware of
232 233 # these situations and will behave differently (write to stdout).
233 234 if (any(s[1] for s in self._bufferstates)
234 235 or not util.safehasattr(self.fout, 'fileno')
235 236 or self.fout.fileno() != sys.stdout.fileno()):
236 237 return super(chgui, self).system(cmd, environ, cwd, onerr,
237 238 errprefix)
238 239 # copied from mercurial/util.py:system()
239 240 self.flush()
240 241 def py2shell(val):
241 242 if val is None or val is False:
242 243 return '0'
243 244 if val is True:
244 245 return '1'
245 246 return str(val)
246 247 env = os.environ.copy()
247 248 if environ:
248 249 env.update((k, py2shell(v)) for k, v in environ.iteritems())
249 250 env['HG'] = util.hgexecutable()
250 251 rc = self._csystem(cmd, env, cwd)
251 252 if rc and onerr:
252 253 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
253 254 util.explainexit(rc)[0])
254 255 if errprefix:
255 256 errmsg = '%s: %s' % (errprefix, errmsg)
256 257 raise onerr(errmsg)
257 258 return rc
258 259
259 260 return chgui(srcui)
260 261
261 262 def _loadnewui(srcui, args):
262 263 newui = srcui.__class__()
263 264 for a in ['fin', 'fout', 'ferr', 'environ']:
264 265 setattr(newui, a, getattr(srcui, a))
265 266 if util.safehasattr(srcui, '_csystem'):
266 267 newui._csystem = srcui._csystem
267 268
268 269 # internal config: extensions.chgserver
269 270 newui.setconfig('extensions', 'chgserver',
270 271 srcui.config('extensions', 'chgserver'), '--config')
271 272
272 273 # command line args
273 274 args = args[:]
274 275 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
275 276
276 277 # stolen from tortoisehg.util.copydynamicconfig()
277 278 for section, name, value in srcui.walkconfig():
278 279 source = srcui.configsource(section, name)
279 280 if ':' in source or source == '--config':
280 281 # path:line or command line
281 282 continue
282 283 if source == 'none':
283 284 # ui.configsource returns 'none' by default
284 285 source = ''
285 286 newui.setconfig(section, name, value, source)
286 287
287 288 # load wd and repo config, copied from dispatch.py
288 289 cwds = dispatch._earlygetopt(['--cwd'], args)
289 290 cwd = cwds and os.path.realpath(cwds[-1]) or None
290 291 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
291 292 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
292 293
293 294 return (newui, newlui)
294 295
295 296 class channeledsystem(object):
296 297 """Propagate ui.system() request in the following format:
297 298
298 299 payload length (unsigned int),
299 300 cmd, '\0',
300 301 cwd, '\0',
301 302 envkey, '=', val, '\0',
302 303 ...
303 304 envkey, '=', val
304 305
305 306 and waits:
306 307
307 308 exitcode length (unsigned int),
308 309 exitcode (int)
309 310 """
310 311 def __init__(self, in_, out, channel):
311 312 self.in_ = in_
312 313 self.out = out
313 314 self.channel = channel
314 315
315 316 def __call__(self, cmd, environ, cwd):
316 317 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
317 318 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
318 319 data = '\0'.join(args)
319 320 self.out.write(struct.pack('>cI', self.channel, len(data)))
320 321 self.out.write(data)
321 322 self.out.flush()
322 323
323 324 length = self.in_.read(4)
324 325 length, = struct.unpack('>I', length)
325 326 if length != 4:
326 327 raise error.Abort(_('invalid response'))
327 328 rc, = struct.unpack('>i', self.in_.read(4))
328 329 return rc
329 330
330 331 _iochannels = [
331 332 # server.ch, ui.fp, mode
332 333 ('cin', 'fin', 'rb'),
333 334 ('cout', 'fout', 'wb'),
334 335 ('cerr', 'ferr', 'wb'),
335 336 ]
336 337
337 338 class chgcmdserver(commandserver.server):
338 339 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
339 340 super(chgcmdserver, self).__init__(
340 341 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
341 342 self.clientsock = sock
342 343 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
343 344 self.hashstate = hashstate
344 345 self.baseaddress = baseaddress
345 346 if hashstate is not None:
346 347 self.capabilities = self.capabilities.copy()
347 348 self.capabilities['validate'] = chgcmdserver.validate
348 349
349 350 def cleanup(self):
350 351 # dispatch._runcatch() does not flush outputs if exception is not
351 352 # handled by dispatch._dispatch()
352 353 self.ui.flush()
353 354 self._restoreio()
354 355
355 356 def attachio(self):
356 357 """Attach to client's stdio passed via unix domain socket; all
357 358 channels except cresult will no longer be used
358 359 """
359 360 # tell client to sendmsg() with 1-byte payload, which makes it
360 361 # distinctive from "attachio\n" command consumed by client.read()
361 362 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
362 363 clientfds = osutil.recvfds(self.clientsock.fileno())
363 364 _log('received fds: %r\n' % clientfds)
364 365
365 366 ui = self.ui
366 367 ui.flush()
367 368 first = self._saveio()
368 369 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
369 370 assert fd > 0
370 371 fp = getattr(ui, fn)
371 372 os.dup2(fd, fp.fileno())
372 373 os.close(fd)
373 374 if not first:
374 375 continue
375 376 # reset buffering mode when client is first attached. as we want
376 377 # to see output immediately on pager, the mode stays unchanged
377 378 # when client re-attached. ferr is unchanged because it should
378 379 # be unbuffered no matter if it is a tty or not.
379 380 if fn == 'ferr':
380 381 newfp = fp
381 382 else:
382 383 # make it line buffered explicitly because the default is
383 384 # decided on first write(), where fout could be a pager.
384 385 if fp.isatty():
385 386 bufsize = 1 # line buffered
386 387 else:
387 388 bufsize = -1 # system default
388 389 newfp = os.fdopen(fp.fileno(), mode, bufsize)
389 390 setattr(ui, fn, newfp)
390 391 setattr(self, cn, newfp)
391 392
392 393 self.cresult.write(struct.pack('>i', len(clientfds)))
393 394
394 395 def _saveio(self):
395 396 if self._oldios:
396 397 return False
397 398 ui = self.ui
398 399 for cn, fn, _mode in _iochannels:
399 400 ch = getattr(self, cn)
400 401 fp = getattr(ui, fn)
401 402 fd = os.dup(fp.fileno())
402 403 self._oldios.append((ch, fp, fd))
403 404 return True
404 405
405 406 def _restoreio(self):
406 407 ui = self.ui
407 408 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
408 409 newfp = getattr(ui, fn)
409 410 # close newfp while it's associated with client; otherwise it
410 411 # would be closed when newfp is deleted
411 412 if newfp is not fp:
412 413 newfp.close()
413 414 # restore original fd: fp is open again
414 415 os.dup2(fd, fp.fileno())
415 416 os.close(fd)
416 417 setattr(self, cn, ch)
417 418 setattr(ui, fn, fp)
418 419 del self._oldios[:]
419 420
420 421 def validate(self):
421 422 """Reload the config and check if the server is up to date
422 423
423 424 Read a list of '\0' separated arguments.
424 425 Write a non-empty list of '\0' separated instruction strings or '\0'
425 426 if the list is empty.
426 427 An instruction string could be either:
427 428 - "unlink $path", the client should unlink the path to stop the
428 429 outdated server.
429 430 - "redirect $path", the client should attempt to connect to $path
430 431 first. If it does not work, start a new server. It implies
431 432 "reconnect".
432 433 - "exit $n", the client should exit directly with code n.
433 434 This may happen if we cannot parse the config.
434 435 - "reconnect", the client should close the connection and
435 436 reconnect.
436 437 If neither "reconnect" nor "redirect" is included in the instruction
437 438 list, the client can continue with this server after completing all
438 439 the instructions.
439 440 """
440 441 args = self._readlist()
441 442 try:
442 443 self.ui, lui = _loadnewui(self.ui, args)
443 444 except error.ParseError as inst:
444 445 dispatch._formatparse(self.ui.warn, inst)
445 446 self.ui.flush()
446 447 self.cresult.write('exit 255')
447 448 return
448 449 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
449 450 insts = []
450 451 if newhash.mtimehash != self.hashstate.mtimehash:
451 452 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
452 453 insts.append('unlink %s' % addr)
453 454 # mtimehash is empty if one or more extensions fail to load.
454 455 # to be compatible with hg, still serve the client this time.
455 456 if self.hashstate.mtimehash:
456 457 insts.append('reconnect')
457 458 if newhash.confighash != self.hashstate.confighash:
458 459 addr = _hashaddress(self.baseaddress, newhash.confighash)
459 460 insts.append('redirect %s' % addr)
460 461 _log('validate: %s\n' % insts)
461 462 self.cresult.write('\0'.join(insts) or '\0')
462 463
463 464 def chdir(self):
464 465 """Change current directory
465 466
466 467 Note that the behavior of --cwd option is bit different from this.
467 468 It does not affect --config parameter.
468 469 """
469 470 path = self._readstr()
470 471 if not path:
471 472 return
472 473 _log('chdir to %r\n' % path)
473 474 os.chdir(path)
474 475
475 476 def setumask(self):
476 477 """Change umask"""
477 478 mask = struct.unpack('>I', self._read(4))[0]
478 479 _log('setumask %r\n' % mask)
479 480 os.umask(mask)
480 481
481 482 def getpager(self):
482 483 """Read cmdargs and write pager command to r-channel if enabled
483 484
484 485 If pager isn't enabled, this writes '\0' because channeledoutput
485 486 does not allow to write empty data.
486 487 """
487 488 args = self._readlist()
488 489 try:
489 490 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
490 491 args)
491 492 except (error.Abort, error.AmbiguousCommand, error.CommandError,
492 493 error.UnknownCommand):
493 494 cmd = None
494 495 options = {}
495 496 if not cmd or 'pager' not in options:
496 497 self.cresult.write('\0')
497 498 return
498 499
499 500 pagercmd = _setuppagercmd(self.ui, options, cmd)
500 501 if pagercmd:
502 # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
503 # we can exit if the pipe to the pager is closed
504 if util.safehasattr(signal, 'SIGPIPE') and \
505 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
506 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
501 507 self.cresult.write(pagercmd)
502 508 else:
503 509 self.cresult.write('\0')
504 510
505 511 def setenv(self):
506 512 """Clear and update os.environ
507 513
508 514 Note that not all variables can make an effect on the running process.
509 515 """
510 516 l = self._readlist()
511 517 try:
512 518 newenv = dict(s.split('=', 1) for s in l)
513 519 except ValueError:
514 520 raise ValueError('unexpected value in setenv request')
515 521 _log('setenv: %r\n' % sorted(newenv.keys()))
516 522 os.environ.clear()
517 523 os.environ.update(newenv)
518 524
519 525 capabilities = commandserver.server.capabilities.copy()
520 526 capabilities.update({'attachio': attachio,
521 527 'chdir': chdir,
522 528 'getpager': getpager,
523 529 'setenv': setenv,
524 530 'setumask': setumask})
525 531
526 532 # copied from mercurial/commandserver.py
527 533 class _requesthandler(SocketServer.StreamRequestHandler):
528 534 def handle(self):
529 535 # use a different process group from the master process, making this
530 536 # process pass kernel "is_current_pgrp_orphaned" check so signals like
531 537 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
532 538 os.setpgid(0, 0)
533 539 # change random state otherwise forked request handlers would have a
534 540 # same state inherited from parent.
535 541 random.seed()
536 542 ui = self.server.ui
537 543 repo = self.server.repo
538 544 sv = None
539 545 try:
540 546 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
541 547 self.server.hashstate, self.server.baseaddress)
542 548 try:
543 549 sv.serve()
544 550 # handle exceptions that may be raised by command server. most of
545 551 # known exceptions are caught by dispatch.
546 552 except error.Abort as inst:
547 553 ui.warn(_('abort: %s\n') % inst)
548 554 except IOError as inst:
549 555 if inst.errno != errno.EPIPE:
550 556 raise
551 557 except KeyboardInterrupt:
552 558 pass
553 559 finally:
554 560 sv.cleanup()
555 561 except: # re-raises
556 562 # also write traceback to error channel. otherwise client cannot
557 563 # see it because it is written to server's stderr by default.
558 564 if sv:
559 565 cerr = sv.cerr
560 566 else:
561 567 cerr = commandserver.channeledoutput(self.wfile, 'e')
562 568 traceback.print_exc(file=cerr)
563 569 raise
564 570 finally:
565 571 # trigger __del__ since ForkingMixIn uses os._exit
566 572 gc.collect()
567 573
568 574 def _tempaddress(address):
569 575 return '%s.%d.tmp' % (address, os.getpid())
570 576
571 577 def _hashaddress(address, hashstr):
572 578 return '%s-%s' % (address, hashstr)
573 579
574 580 class AutoExitMixIn: # use old-style to comply with SocketServer design
575 581 lastactive = time.time()
576 582 idletimeout = 3600 # default 1 hour
577 583
578 584 def startautoexitthread(self):
579 585 # note: the auto-exit check here is cheap enough to not use a thread,
580 586 # be done in serve_forever. however SocketServer is hook-unfriendly,
581 587 # you simply cannot hook serve_forever without copying a lot of code.
582 588 # besides, serve_forever's docstring suggests using thread.
583 589 thread = threading.Thread(target=self._autoexitloop)
584 590 thread.daemon = True
585 591 thread.start()
586 592
587 593 def _autoexitloop(self, interval=1):
588 594 while True:
589 595 time.sleep(interval)
590 596 if not self.issocketowner():
591 597 _log('%s is not owned, exiting.\n' % self.server_address)
592 598 break
593 599 if time.time() - self.lastactive > self.idletimeout:
594 600 _log('being idle too long. exiting.\n')
595 601 break
596 602 self.shutdown()
597 603
598 604 def process_request(self, request, address):
599 605 self.lastactive = time.time()
600 606 return SocketServer.ForkingMixIn.process_request(
601 607 self, request, address)
602 608
603 609 def server_bind(self):
604 610 # use a unique temp address so we can stat the file and do ownership
605 611 # check later
606 612 tempaddress = _tempaddress(self.server_address)
607 613 # use relative path instead of full path at bind() if possible, since
608 614 # AF_UNIX path has very small length limit (107 chars) on common
609 615 # platforms (see sys/un.h)
610 616 dirname, basename = os.path.split(tempaddress)
611 617 bakwdfd = None
612 618 if dirname:
613 619 bakwdfd = os.open('.', os.O_DIRECTORY)
614 620 os.chdir(dirname)
615 621 self.socket.bind(basename)
616 622 self._socketstat = os.stat(basename)
617 623 # rename will replace the old socket file if exists atomically. the
618 624 # old server will detect ownership change and exit.
619 625 util.rename(basename, self.server_address)
620 626 if bakwdfd:
621 627 os.fchdir(bakwdfd)
622 628 os.close(bakwdfd)
623 629
624 630 def issocketowner(self):
625 631 try:
626 632 stat = os.stat(self.server_address)
627 633 return (stat.st_ino == self._socketstat.st_ino and
628 634 stat.st_mtime == self._socketstat.st_mtime)
629 635 except OSError:
630 636 return False
631 637
632 638 def unlinksocketfile(self):
633 639 if not self.issocketowner():
634 640 return
635 641 # it is possible to have a race condition here that we may
636 642 # remove another server's socket file. but that's okay
637 643 # since that server will detect and exit automatically and
638 644 # the client will start a new server on demand.
639 645 try:
640 646 os.unlink(self.server_address)
641 647 except OSError as exc:
642 648 if exc.errno != errno.ENOENT:
643 649 raise
644 650
645 651 class chgunixservice(commandserver.unixservice):
646 652 def init(self):
647 653 if self.repo:
648 654 # one chgserver can serve multiple repos. drop repo infomation
649 655 self.ui.setconfig('bundle', 'mainreporoot', '', 'repo')
650 656 self.repo = None
651 657 self._inithashstate()
652 658 self._checkextensions()
653 659 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
654 660 SocketServer.UnixStreamServer):
655 661 ui = self.ui
656 662 repo = self.repo
657 663 hashstate = self.hashstate
658 664 baseaddress = self.baseaddress
659 665 self.server = cls(self.address, _requesthandler)
660 666 self.server.idletimeout = self.ui.configint(
661 667 'chgserver', 'idletimeout', self.server.idletimeout)
662 668 self.server.startautoexitthread()
663 669 self._createsymlink()
664 670
665 671 def _inithashstate(self):
666 672 self.baseaddress = self.address
667 673 if self.ui.configbool('chgserver', 'skiphash', False):
668 674 self.hashstate = None
669 675 return
670 676 self.hashstate = hashstate.fromui(self.ui)
671 677 self.address = _hashaddress(self.address, self.hashstate.confighash)
672 678
673 679 def _checkextensions(self):
674 680 if not self.hashstate:
675 681 return
676 682 if extensions.notloaded():
677 683 # one or more extensions failed to load. mtimehash becomes
678 684 # meaningless because we do not know the paths of those extensions.
679 685 # set mtimehash to an illegal hash value to invalidate the server.
680 686 self.hashstate.mtimehash = ''
681 687
682 688 def _createsymlink(self):
683 689 if self.baseaddress == self.address:
684 690 return
685 691 tempaddress = _tempaddress(self.baseaddress)
686 692 os.symlink(os.path.basename(self.address), tempaddress)
687 693 util.rename(tempaddress, self.baseaddress)
688 694
689 695 def run(self):
690 696 try:
691 697 self.server.serve_forever()
692 698 finally:
693 699 self.server.unlinksocketfile()
694 700
695 701 def uisetup(ui):
696 702 commandserver._servicemap['chgunix'] = chgunixservice
697 703
698 704 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
699 705 # start another chg. drop it to avoid possible side effects.
700 706 if 'CHGINTERNALMARK' in os.environ:
701 707 del os.environ['CHGINTERNALMARK']
General Comments 0
You need to be logged in to leave comments. Login now