mercurial/chgserver.py
changeset 30522 ff7df4bb75de
parent 30521 cc374292a561
child 30577 cfb227016d01
equal deleted inserted replaced
30521:cc374292a561 30522:ff7df4bb75de
       
     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)