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