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