mercurial/chgserver.py
changeset 43076 2372284d9457
parent 42828 0cbe17335857
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    65 from .utils import (
    65 from .utils import (
    66     procutil,
    66     procutil,
    67     stringutil,
    67     stringutil,
    68 )
    68 )
    69 
    69 
       
    70 
    70 def _hashlist(items):
    71 def _hashlist(items):
    71     """return sha1 hexdigest for a list"""
    72     """return sha1 hexdigest for a list"""
    72     return node.hex(hashlib.sha1(stringutil.pprint(items)).digest())
    73     return node.hex(hashlib.sha1(stringutil.pprint(items)).digest())
    73 
    74 
       
    75 
    74 # sensitive config sections affecting confighash
    76 # sensitive config sections affecting confighash
    75 _configsections = [
    77 _configsections = [
    76     'alias',  # affects global state commands.table
    78     'alias',  # affects global state commands.table
    77     'eol',    # uses setconfig('eol', ...)
    79     'eol',  # uses setconfig('eol', ...)
    78     'extdiff',  # uisetup will register new commands
    80     'extdiff',  # uisetup will register new commands
    79     'extensions',
    81     'extensions',
    80 ]
    82 ]
    81 
    83 
    82 _configsectionitems = [
    84 _configsectionitems = [
    83     ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
    85     ('commands', 'show.aliasprefix'),  # show.py reads it in extsetup
    84 ]
    86 ]
    85 
    87 
    86 # sensitive environment variables affecting confighash
    88 # sensitive environment variables affecting confighash
    87 _envre = re.compile(br'''\A(?:
    89 _envre = re.compile(
       
    90     br'''\A(?:
    88                     CHGHG
    91                     CHGHG
    89                     |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
    92                     |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
    90                     |HG(?:ENCODING|PLAIN).*
    93                     |HG(?:ENCODING|PLAIN).*
    91                     |LANG(?:UAGE)?
    94                     |LANG(?:UAGE)?
    92                     |LC_.*
    95                     |LC_.*
    93                     |LD_.*
    96                     |LD_.*
    94                     |PATH
    97                     |PATH
    95                     |PYTHON.*
    98                     |PYTHON.*
    96                     |TERM(?:INFO)?
    99                     |TERM(?:INFO)?
    97                     |TZ
   100                     |TZ
    98                     )\Z''', re.X)
   101                     )\Z''',
       
   102     re.X,
       
   103 )
       
   104 
    99 
   105 
   100 def _confighash(ui):
   106 def _confighash(ui):
   101     """return a quick hash for detecting config/env changes
   107     """return a quick hash for detecting config/env changes
   102 
   108 
   103     confighash is the hash of sensitive config items and environment variables.
   109     confighash is the hash of sensitive config items and environment variables.
   117     # If $CHGHG is set, the change to $HG should not trigger a new chg server
   123     # If $CHGHG is set, the change to $HG should not trigger a new chg server
   118     if 'CHGHG' in encoding.environ:
   124     if 'CHGHG' in encoding.environ:
   119         ignored = {'HG'}
   125         ignored = {'HG'}
   120     else:
   126     else:
   121         ignored = set()
   127         ignored = set()
   122     envitems = [(k, v) for k, v in encoding.environ.iteritems()
   128     envitems = [
   123                 if _envre.match(k) and k not in ignored]
   129         (k, v)
       
   130         for k, v in encoding.environ.iteritems()
       
   131         if _envre.match(k) and k not in ignored
       
   132     ]
   124     envhash = _hashlist(sorted(envitems))
   133     envhash = _hashlist(sorted(envitems))
   125     return sectionhash[:6] + envhash[:6]
   134     return sectionhash[:6] + envhash[:6]
       
   135 
   126 
   136 
   127 def _getmtimepaths(ui):
   137 def _getmtimepaths(ui):
   128     """get a list of paths that should be checked to detect change
   138     """get a list of paths that should be checked to detect change
   129 
   139 
   130     The list will include:
   140     The list will include:
   133     - python binary
   143     - python binary
   134     """
   144     """
   135     modules = [m for n, m in extensions.extensions(ui)]
   145     modules = [m for n, m in extensions.extensions(ui)]
   136     try:
   146     try:
   137         from . import __version__
   147         from . import __version__
       
   148 
   138         modules.append(__version__)
   149         modules.append(__version__)
   139     except ImportError:
   150     except ImportError:
   140         pass
   151         pass
   141     files = []
   152     files = []
   142     if pycompat.sysexecutable:
   153     if pycompat.sysexecutable:
   146             files.append(pycompat.fsencode(inspect.getabsfile(m)))
   157             files.append(pycompat.fsencode(inspect.getabsfile(m)))
   147         except TypeError:
   158         except TypeError:
   148             pass
   159             pass
   149     return sorted(set(files))
   160     return sorted(set(files))
   150 
   161 
       
   162 
   151 def _mtimehash(paths):
   163 def _mtimehash(paths):
   152     """return a quick hash for detecting file changes
   164     """return a quick hash for detecting file changes
   153 
   165 
   154     mtimehash calls stat on given paths and calculate a hash based on size and
   166     mtimehash calls stat on given paths and calculate a hash based on size and
   155     mtime of each file. mtimehash does not read file content because reading is
   167     mtime of each file. mtimehash does not read file content because reading is
   163 
   175 
   164     mtimehash is not included in confighash because we only know the paths of
   176     mtimehash is not included in confighash because we only know the paths of
   165     extensions after importing them (there is imp.find_module but that faces
   177     extensions after importing them (there is imp.find_module but that faces
   166     race conditions). We need to calculate confighash without importing.
   178     race conditions). We need to calculate confighash without importing.
   167     """
   179     """
       
   180 
   168     def trystat(path):
   181     def trystat(path):
   169         try:
   182         try:
   170             st = os.stat(path)
   183             st = os.stat(path)
   171             return (st[stat.ST_MTIME], st.st_size)
   184             return (st[stat.ST_MTIME], st.st_size)
   172         except OSError:
   185         except OSError:
   173             # could be ENOENT, EPERM etc. not fatal in any case
   186             # could be ENOENT, EPERM etc. not fatal in any case
   174             pass
   187             pass
       
   188 
   175     return _hashlist(pycompat.maplist(trystat, paths))[:12]
   189     return _hashlist(pycompat.maplist(trystat, paths))[:12]
       
   190 
   176 
   191 
   177 class hashstate(object):
   192 class hashstate(object):
   178     """a structure storing confighash, mtimehash, paths used for mtimehash"""
   193     """a structure storing confighash, mtimehash, paths used for mtimehash"""
       
   194 
   179     def __init__(self, confighash, mtimehash, mtimepaths):
   195     def __init__(self, confighash, mtimehash, mtimepaths):
   180         self.confighash = confighash
   196         self.confighash = confighash
   181         self.mtimehash = mtimehash
   197         self.mtimehash = mtimehash
   182         self.mtimepaths = mtimepaths
   198         self.mtimepaths = mtimepaths
   183 
   199 
   185     def fromui(ui, mtimepaths=None):
   201     def fromui(ui, mtimepaths=None):
   186         if mtimepaths is None:
   202         if mtimepaths is None:
   187             mtimepaths = _getmtimepaths(ui)
   203             mtimepaths = _getmtimepaths(ui)
   188         confighash = _confighash(ui)
   204         confighash = _confighash(ui)
   189         mtimehash = _mtimehash(mtimepaths)
   205         mtimehash = _mtimehash(mtimepaths)
   190         ui.log('cmdserver', 'confighash = %s mtimehash = %s\n',
   206         ui.log(
   191                confighash, mtimehash)
   207             'cmdserver',
       
   208             'confighash = %s mtimehash = %s\n',
       
   209             confighash,
       
   210             mtimehash,
       
   211         )
   192         return hashstate(confighash, mtimehash, mtimepaths)
   212         return hashstate(confighash, mtimehash, mtimepaths)
       
   213 
   193 
   214 
   194 def _newchgui(srcui, csystem, attachio):
   215 def _newchgui(srcui, csystem, attachio):
   195     class chgui(srcui.__class__):
   216     class chgui(srcui.__class__):
   196         def __init__(self, src=None):
   217         def __init__(self, src=None):
   197             super(chgui, self).__init__(src)
   218             super(chgui, self).__init__(src)
   204             # fallback to the original system method if
   225             # fallback to the original system method if
   205             #  a. the output stream is not stdout (e.g. stderr, cStringIO),
   226             #  a. the output stream is not stdout (e.g. stderr, cStringIO),
   206             #  b. or stdout is redirected by protectfinout(),
   227             #  b. or stdout is redirected by protectfinout(),
   207             # because the chg client is not aware of these situations and
   228             # because the chg client is not aware of these situations and
   208             # will behave differently (i.e. write to stdout).
   229             # will behave differently (i.e. write to stdout).
   209             if (out is not self.fout
   230             if (
       
   231                 out is not self.fout
   210                 or not util.safehasattr(self.fout, 'fileno')
   232                 or not util.safehasattr(self.fout, 'fileno')
   211                 or self.fout.fileno() != procutil.stdout.fileno()
   233                 or self.fout.fileno() != procutil.stdout.fileno()
   212                 or self._finoutredirected):
   234                 or self._finoutredirected
       
   235             ):
   213                 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
   236                 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
   214             self.flush()
   237             self.flush()
   215             return self._csystem(cmd, procutil.shellenviron(environ), cwd)
   238             return self._csystem(cmd, procutil.shellenviron(environ), cwd)
   216 
   239 
   217         def _runpager(self, cmd, env=None):
   240         def _runpager(self, cmd, env=None):
   218             self._csystem(cmd, procutil.shellenviron(env), type='pager',
   241             self._csystem(
   219                           cmdtable={'attachio': attachio})
   242                 cmd,
       
   243                 procutil.shellenviron(env),
       
   244                 type='pager',
       
   245                 cmdtable={'attachio': attachio},
       
   246             )
   220             return True
   247             return True
   221 
   248 
   222     return chgui(srcui)
   249     return chgui(srcui)
       
   250 
   223 
   251 
   224 def _loadnewui(srcui, args, cdebug):
   252 def _loadnewui(srcui, args, cdebug):
   225     from . import dispatch  # avoid cycle
   253     from . import dispatch  # avoid cycle
   226 
   254 
   227     newui = srcui.__class__.load()
   255     newui = srcui.__class__.load()
   254         extensions.populateui(newlui)
   282         extensions.populateui(newlui)
   255         commandserver.setuplogging(newlui, fp=cdebug)
   283         commandserver.setuplogging(newlui, fp=cdebug)
   256 
   284 
   257     return (newui, newlui)
   285     return (newui, newlui)
   258 
   286 
       
   287 
   259 class channeledsystem(object):
   288 class channeledsystem(object):
   260     """Propagate ui.system() request in the following format:
   289     """Propagate ui.system() request in the following format:
   261 
   290 
   262     payload length (unsigned int),
   291     payload length (unsigned int),
   263     type, '\0',
   292     type, '\0',
   274 
   303 
   275     if type == 'pager', repetitively waits for a command name ending with '\n'
   304     if type == 'pager', repetitively waits for a command name ending with '\n'
   276     and executes it defined by cmdtable, or exits the loop if the command name
   305     and executes it defined by cmdtable, or exits the loop if the command name
   277     is empty.
   306     is empty.
   278     """
   307     """
       
   308 
   279     def __init__(self, in_, out, channel):
   309     def __init__(self, in_, out, channel):
   280         self.in_ = in_
   310         self.in_ = in_
   281         self.out = out
   311         self.out = out
   282         self.channel = channel
   312         self.channel = channel
   283 
   313 
   289         self.out.write(data)
   319         self.out.write(data)
   290         self.out.flush()
   320         self.out.flush()
   291 
   321 
   292         if type == 'system':
   322         if type == 'system':
   293             length = self.in_.read(4)
   323             length = self.in_.read(4)
   294             length, = struct.unpack('>I', length)
   324             (length,) = struct.unpack('>I', length)
   295             if length != 4:
   325             if length != 4:
   296                 raise error.Abort(_('invalid response'))
   326                 raise error.Abort(_('invalid response'))
   297             rc, = struct.unpack('>i', self.in_.read(4))
   327             (rc,) = struct.unpack('>i', self.in_.read(4))
   298             return rc
   328             return rc
   299         elif type == 'pager':
   329         elif type == 'pager':
   300             while True:
   330             while True:
   301                 cmd = self.in_.readline()[:-1]
   331                 cmd = self.in_.readline()[:-1]
   302                 if not cmd:
   332                 if not cmd:
   306                 else:
   336                 else:
   307                     raise error.Abort(_('unexpected command: %s') % cmd)
   337                     raise error.Abort(_('unexpected command: %s') % cmd)
   308         else:
   338         else:
   309             raise error.ProgrammingError('invalid S channel type: %s' % type)
   339             raise error.ProgrammingError('invalid S channel type: %s' % type)
   310 
   340 
       
   341 
   311 _iochannels = [
   342 _iochannels = [
   312     # server.ch, ui.fp, mode
   343     # server.ch, ui.fp, mode
   313     ('cin', 'fin', r'rb'),
   344     ('cin', 'fin', r'rb'),
   314     ('cout', 'fout', r'wb'),
   345     ('cout', 'fout', r'wb'),
   315     ('cerr', 'ferr', r'wb'),
   346     ('cerr', 'ferr', r'wb'),
   316 ]
   347 ]
   317 
   348 
       
   349 
   318 class chgcmdserver(commandserver.server):
   350 class chgcmdserver(commandserver.server):
   319     def __init__(self, ui, repo, fin, fout, sock, prereposetups,
   351     def __init__(
   320                  hashstate, baseaddress):
   352         self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
       
   353     ):
   321         super(chgcmdserver, self).__init__(
   354         super(chgcmdserver, self).__init__(
   322             _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
   355             _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
   323             repo, fin, fout, prereposetups)
   356             repo,
       
   357             fin,
       
   358             fout,
       
   359             prereposetups,
       
   360         )
   324         self.clientsock = sock
   361         self.clientsock = sock
   325         self._ioattached = False
   362         self._ioattached = False
   326         self._oldios = []  # original (self.ch, ui.fp, fd) before "attachio"
   363         self._oldios = []  # original (self.ch, ui.fp, fd) before "attachio"
   327         self.hashstate = hashstate
   364         self.hashstate = hashstate
   328         self.baseaddress = baseaddress
   365         self.baseaddress = baseaddress
   510         self.ui.log('chgserver', 'setenv: %r\n', sorted(newenv.keys()))
   547         self.ui.log('chgserver', 'setenv: %r\n', sorted(newenv.keys()))
   511         encoding.environ.clear()
   548         encoding.environ.clear()
   512         encoding.environ.update(newenv)
   549         encoding.environ.update(newenv)
   513 
   550 
   514     capabilities = commandserver.server.capabilities.copy()
   551     capabilities = commandserver.server.capabilities.copy()
   515     capabilities.update({'attachio': attachio,
   552     capabilities.update(
   516                          'chdir': chdir,
   553         {
   517                          'runcommand': runcommand,
   554             'attachio': attachio,
   518                          'setenv': setenv,
   555             'chdir': chdir,
   519                          'setumask': setumask,
   556             'runcommand': runcommand,
   520                          'setumask2': setumask2})
   557             'setenv': setenv,
       
   558             'setumask': setumask,
       
   559             'setumask2': setumask2,
       
   560         }
       
   561     )
   521 
   562 
   522     if util.safehasattr(procutil, 'setprocname'):
   563     if util.safehasattr(procutil, 'setprocname'):
       
   564 
   523         def setprocname(self):
   565         def setprocname(self):
   524             """Change process title"""
   566             """Change process title"""
   525             name = self._readstr()
   567             name = self._readstr()
   526             self.ui.log('chgserver', 'setprocname: %r\n', name)
   568             self.ui.log('chgserver', 'setprocname: %r\n', name)
   527             procutil.setprocname(name)
   569             procutil.setprocname(name)
       
   570 
   528         capabilities['setprocname'] = setprocname
   571         capabilities['setprocname'] = setprocname
       
   572 
   529 
   573 
   530 def _tempaddress(address):
   574 def _tempaddress(address):
   531     return '%s.%d.tmp' % (address, os.getpid())
   575     return '%s.%d.tmp' % (address, os.getpid())
       
   576 
   532 
   577 
   533 def _hashaddress(address, hashstr):
   578 def _hashaddress(address, hashstr):
   534     # if the basename of address contains '.', use only the left part. this
   579     # if the basename of address contains '.', use only the left part. this
   535     # makes it possible for the client to pass 'server.tmp$PID' and follow by
   580     # makes it possible for the client to pass 'server.tmp$PID' and follow by
   536     # an atomic rename to avoid locking when spawning new servers.
   581     # an atomic rename to avoid locking when spawning new servers.
   537     dirname, basename = os.path.split(address)
   582     dirname, basename = os.path.split(address)
   538     basename = basename.split('.', 1)[0]
   583     basename = basename.split('.', 1)[0]
   539     return '%s-%s' % (os.path.join(dirname, basename), hashstr)
   584     return '%s-%s' % (os.path.join(dirname, basename), hashstr)
       
   585 
   540 
   586 
   541 class chgunixservicehandler(object):
   587 class chgunixservicehandler(object):
   542     """Set of operations for chg services"""
   588     """Set of operations for chg services"""
   543 
   589 
   544     pollinterval = 1  # [sec]
   590     pollinterval = 1  # [sec]
   592         util.rename(tempaddress, self._baseaddress)
   638         util.rename(tempaddress, self._baseaddress)
   593 
   639 
   594     def _issocketowner(self):
   640     def _issocketowner(self):
   595         try:
   641         try:
   596             st = os.stat(self._realaddress)
   642             st = os.stat(self._realaddress)
   597             return (st.st_ino == self._socketstat.st_ino and
   643             return (
   598                     st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
   644                 st.st_ino == self._socketstat.st_ino
       
   645                 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
       
   646             )
   599         except OSError:
   647         except OSError:
   600             return False
   648             return False
   601 
   649 
   602     def unlinksocket(self, address):
   650     def unlinksocket(self, address):
   603         if not self._issocketowner():
   651         if not self._issocketowner():
   608         # the client will start a new server on demand.
   656         # the client will start a new server on demand.
   609         util.tryunlink(self._realaddress)
   657         util.tryunlink(self._realaddress)
   610 
   658 
   611     def shouldexit(self):
   659     def shouldexit(self):
   612         if not self._issocketowner():
   660         if not self._issocketowner():
   613             self.ui.log(b'chgserver', b'%s is not owned, exiting.\n',
   661             self.ui.log(
   614                         self._realaddress)
   662                 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
       
   663             )
   615             return True
   664             return True
   616         if time.time() - self._lastactive > self._idletimeout:
   665         if time.time() - self._lastactive > self._idletimeout:
   617             self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
   666             self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
   618             return True
   667             return True
   619         return False
   668         return False
   620 
   669 
   621     def newconnection(self):
   670     def newconnection(self):
   622         self._lastactive = time.time()
   671         self._lastactive = time.time()
   623 
   672 
   624     def createcmdserver(self, repo, conn, fin, fout, prereposetups):
   673     def createcmdserver(self, repo, conn, fin, fout, prereposetups):
   625         return chgcmdserver(self.ui, repo, fin, fout, conn, prereposetups,
   674         return chgcmdserver(
   626                             self._hashstate, self._baseaddress)
   675             self.ui,
       
   676             repo,
       
   677             fin,
       
   678             fout,
       
   679             conn,
       
   680             prereposetups,
       
   681             self._hashstate,
       
   682             self._baseaddress,
       
   683         )
       
   684 
   627 
   685 
   628 def chgunixservice(ui, repo, opts):
   686 def chgunixservice(ui, repo, opts):
   629     # CHGINTERNALMARK is set by chg client. It is an indication of things are
   687     # CHGINTERNALMARK is set by chg client. It is an indication of things are
   630     # started by chg so other code can do things accordingly, like disabling
   688     # started by chg so other code can do things accordingly, like disabling
   631     # demandimport or detecting chg client started by chg client. When executed
   689     # demandimport or detecting chg client started by chg client. When executed