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