chgserver: make it a core module and drop extension flags
It was an extension just because there were several dependency cycles I
needed to address.
I don't add 'chgserver' to extensions._builtin since chgserver is considered
an internal extension so nobody should enable it by their config.
--- a/contrib/chg/Makefile Sat Oct 15 14:37:18 2016 +0900
+++ b/contrib/chg/Makefile Sat Oct 15 14:30:16 2016 +0900
@@ -40,7 +40,6 @@
[ -d $(CHGSOCKDIR) ] || ( umask 077; mkdir $(CHGSOCKDIR) )
$(HG) serve --cwd / --cmdserver chgunix \
--address $(CHGSOCKNAME) \
- --config extensions.chgserver= \
--config cmdserver.log=/dev/stderr
.PHONY: clean
--- a/contrib/chg/chg.c Sat Oct 15 14:37:18 2016 +0900
+++ b/contrib/chg/chg.c Sat Oct 15 14:30:16 2016 +0900
@@ -225,7 +225,6 @@
"--cmdserver", "chgunix",
"--address", opts->sockname,
"--daemon-postexec", "chdir:/",
- "--config", "extensions.chgserver=",
};
size_t baseargvsize = sizeof(baseargv) / sizeof(baseargv[0]);
size_t argsize = baseargvsize + opts->argsize + 1;
--- a/hgext/chgserver.py Sat Oct 15 14:37:18 2016 +0900
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,650 +0,0 @@
-# chgserver.py - command server extension for cHg
-#
-# Copyright 2011 Yuya Nishihara <yuya@tcha.org>
-#
-# This software may be used and distributed according to the terms of the
-# GNU General Public License version 2 or any later version.
-
-"""command server extension for cHg (EXPERIMENTAL)
-
-'S' channel (read/write)
- propagate ui.system() request to client
-
-'attachio' command
- attach client's stdio passed by sendmsg()
-
-'chdir' command
- change current directory
-
-'getpager' command
- checks if pager is enabled and which pager should be executed
-
-'setenv' command
- replace os.environ completely
-
-'setumask' command
- set umask
-
-'validate' command
- reload the config and check if the server is up to date
-
-Config
-------
-
-::
-
- [chgserver]
- idletimeout = 3600 # seconds, after which an idle server will exit
- skiphash = False # whether to skip config or env change checks
-"""
-
-from __future__ import absolute_import
-
-import errno
-import hashlib
-import inspect
-import os
-import re
-import signal
-import struct
-import sys
-import time
-
-from mercurial.i18n import _
-
-from mercurial import (
- cmdutil,
- commandserver,
- error,
- extensions,
- osutil,
- server,
- util,
-)
-
-# Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
-# extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
-# be specifying the version(s) of Mercurial they are tested with, or
-# leave the attribute unspecified.
-testedwith = 'ships-with-hg-core'
-
-_log = commandserver.log
-
-def _hashlist(items):
- """return sha1 hexdigest for a list"""
- return hashlib.sha1(str(items)).hexdigest()
-
-# sensitive config sections affecting confighash
-_configsections = [
- 'alias', # affects global state commands.table
- 'extdiff', # uisetup will register new commands
- 'extensions',
-]
-
-# sensitive environment variables affecting confighash
-_envre = re.compile(r'''\A(?:
- CHGHG
- |HG.*
- |LANG(?:UAGE)?
- |LC_.*
- |LD_.*
- |PATH
- |PYTHON.*
- |TERM(?:INFO)?
- |TZ
- )\Z''', re.X)
-
-def _confighash(ui):
- """return a quick hash for detecting config/env changes
-
- confighash is the hash of sensitive config items and environment variables.
-
- for chgserver, it is designed that once confighash changes, the server is
- not qualified to serve its client and should redirect the client to a new
- server. different from mtimehash, confighash change will not mark the
- server outdated and exit since the user can have different configs at the
- same time.
- """
- sectionitems = []
- for section in _configsections:
- sectionitems.append(ui.configitems(section))
- sectionhash = _hashlist(sectionitems)
- envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
- envhash = _hashlist(sorted(envitems))
- return sectionhash[:6] + envhash[:6]
-
-def _getmtimepaths(ui):
- """get a list of paths that should be checked to detect change
-
- The list will include:
- - extensions (will not cover all files for complex extensions)
- - mercurial/__version__.py
- - python binary
- """
- modules = [m for n, m in extensions.extensions(ui)]
- try:
- from mercurial import __version__
- modules.append(__version__)
- except ImportError:
- pass
- files = [sys.executable]
- for m in modules:
- try:
- files.append(inspect.getabsfile(m))
- except TypeError:
- pass
- return sorted(set(files))
-
-def _mtimehash(paths):
- """return a quick hash for detecting file changes
-
- mtimehash calls stat on given paths and calculate a hash based on size and
- mtime of each file. mtimehash does not read file content because reading is
- expensive. therefore it's not 100% reliable for detecting content changes.
- it's possible to return different hashes for same file contents.
- it's also possible to return a same hash for different file contents for
- some carefully crafted situation.
-
- for chgserver, it is designed that once mtimehash changes, the server is
- considered outdated immediately and should no longer provide service.
-
- mtimehash is not included in confighash because we only know the paths of
- extensions after importing them (there is imp.find_module but that faces
- race conditions). We need to calculate confighash without importing.
- """
- def trystat(path):
- try:
- st = os.stat(path)
- return (st.st_mtime, st.st_size)
- except OSError:
- # could be ENOENT, EPERM etc. not fatal in any case
- pass
- return _hashlist(map(trystat, paths))[:12]
-
-class hashstate(object):
- """a structure storing confighash, mtimehash, paths used for mtimehash"""
- def __init__(self, confighash, mtimehash, mtimepaths):
- self.confighash = confighash
- self.mtimehash = mtimehash
- self.mtimepaths = mtimepaths
-
- @staticmethod
- def fromui(ui, mtimepaths=None):
- if mtimepaths is None:
- mtimepaths = _getmtimepaths(ui)
- confighash = _confighash(ui)
- mtimehash = _mtimehash(mtimepaths)
- _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
- return hashstate(confighash, mtimehash, mtimepaths)
-
-# copied from hgext/pager.py:uisetup()
-def _setuppagercmd(ui, options, cmd):
- from mercurial import commands # avoid cycle
-
- if not ui.formatted():
- return
-
- p = ui.config("pager", "pager", os.environ.get("PAGER"))
- usepager = False
- always = util.parsebool(options['pager'])
- auto = options['pager'] == 'auto'
-
- if not p:
- pass
- elif always:
- usepager = True
- elif not auto:
- usepager = False
- else:
- attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
- attend = ui.configlist('pager', 'attend', attended)
- ignore = ui.configlist('pager', 'ignore')
- cmds, _ = cmdutil.findcmd(cmd, commands.table)
-
- for cmd in cmds:
- var = 'attend-%s' % cmd
- if ui.config('pager', var):
- usepager = ui.configbool('pager', var)
- break
- if (cmd in attend or
- (cmd not in ignore and not attend)):
- usepager = True
- break
-
- if usepager:
- ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
- ui.setconfig('ui', 'interactive', False, 'pager')
- return p
-
-def _newchgui(srcui, csystem):
- class chgui(srcui.__class__):
- def __init__(self, src=None):
- super(chgui, self).__init__(src)
- if src:
- self._csystem = getattr(src, '_csystem', csystem)
- else:
- self._csystem = csystem
-
- def system(self, cmd, environ=None, cwd=None, onerr=None,
- errprefix=None):
- # fallback to the original system method if the output needs to be
- # captured (to self._buffers), or the output stream is not stdout
- # (e.g. stderr, cStringIO), because the chg client is not aware of
- # these situations and will behave differently (write to stdout).
- if (any(s[1] for s in self._bufferstates)
- or not util.safehasattr(self.fout, 'fileno')
- or self.fout.fileno() != util.stdout.fileno()):
- return super(chgui, self).system(cmd, environ, cwd, onerr,
- errprefix)
- # copied from mercurial/util.py:system()
- self.flush()
- def py2shell(val):
- if val is None or val is False:
- return '0'
- if val is True:
- return '1'
- return str(val)
- env = os.environ.copy()
- if environ:
- env.update((k, py2shell(v)) for k, v in environ.iteritems())
- env['HG'] = util.hgexecutable()
- rc = self._csystem(cmd, env, cwd)
- if rc and onerr:
- errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
- util.explainexit(rc)[0])
- if errprefix:
- errmsg = '%s: %s' % (errprefix, errmsg)
- raise onerr(errmsg)
- return rc
-
- return chgui(srcui)
-
-def _loadnewui(srcui, args):
- from mercurial import dispatch # avoid cycle
-
- newui = srcui.__class__()
- for a in ['fin', 'fout', 'ferr', 'environ']:
- setattr(newui, a, getattr(srcui, a))
- if util.safehasattr(srcui, '_csystem'):
- newui._csystem = srcui._csystem
-
- # internal config: extensions.chgserver
- newui.setconfig('extensions', 'chgserver',
- srcui.config('extensions', 'chgserver'), '--config')
-
- # command line args
- args = args[:]
- dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
-
- # stolen from tortoisehg.util.copydynamicconfig()
- for section, name, value in srcui.walkconfig():
- source = srcui.configsource(section, name)
- if ':' in source or source == '--config':
- # path:line or command line
- continue
- if source == 'none':
- # ui.configsource returns 'none' by default
- source = ''
- newui.setconfig(section, name, value, source)
-
- # load wd and repo config, copied from dispatch.py
- cwds = dispatch._earlygetopt(['--cwd'], args)
- cwd = cwds and os.path.realpath(cwds[-1]) or None
- rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
- path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
-
- return (newui, newlui)
-
-class channeledsystem(object):
- """Propagate ui.system() request in the following format:
-
- payload length (unsigned int),
- cmd, '\0',
- cwd, '\0',
- envkey, '=', val, '\0',
- ...
- envkey, '=', val
-
- and waits:
-
- exitcode length (unsigned int),
- exitcode (int)
- """
- def __init__(self, in_, out, channel):
- self.in_ = in_
- self.out = out
- self.channel = channel
-
- def __call__(self, cmd, environ, cwd):
- args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
- args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
- data = '\0'.join(args)
- self.out.write(struct.pack('>cI', self.channel, len(data)))
- self.out.write(data)
- self.out.flush()
-
- length = self.in_.read(4)
- length, = struct.unpack('>I', length)
- if length != 4:
- raise error.Abort(_('invalid response'))
- rc, = struct.unpack('>i', self.in_.read(4))
- return rc
-
-_iochannels = [
- # server.ch, ui.fp, mode
- ('cin', 'fin', 'rb'),
- ('cout', 'fout', 'wb'),
- ('cerr', 'ferr', 'wb'),
-]
-
-class chgcmdserver(commandserver.server):
- def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
- super(chgcmdserver, self).__init__(
- _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
- self.clientsock = sock
- self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
- self.hashstate = hashstate
- self.baseaddress = baseaddress
- if hashstate is not None:
- self.capabilities = self.capabilities.copy()
- self.capabilities['validate'] = chgcmdserver.validate
-
- def cleanup(self):
- super(chgcmdserver, self).cleanup()
- # dispatch._runcatch() does not flush outputs if exception is not
- # handled by dispatch._dispatch()
- self.ui.flush()
- self._restoreio()
-
- def attachio(self):
- """Attach to client's stdio passed via unix domain socket; all
- channels except cresult will no longer be used
- """
- # tell client to sendmsg() with 1-byte payload, which makes it
- # distinctive from "attachio\n" command consumed by client.read()
- self.clientsock.sendall(struct.pack('>cI', 'I', 1))
- clientfds = osutil.recvfds(self.clientsock.fileno())
- _log('received fds: %r\n' % clientfds)
-
- ui = self.ui
- ui.flush()
- first = self._saveio()
- for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
- assert fd > 0
- fp = getattr(ui, fn)
- os.dup2(fd, fp.fileno())
- os.close(fd)
- if not first:
- continue
- # reset buffering mode when client is first attached. as we want
- # to see output immediately on pager, the mode stays unchanged
- # when client re-attached. ferr is unchanged because it should
- # be unbuffered no matter if it is a tty or not.
- if fn == 'ferr':
- newfp = fp
- else:
- # make it line buffered explicitly because the default is
- # decided on first write(), where fout could be a pager.
- if fp.isatty():
- bufsize = 1 # line buffered
- else:
- bufsize = -1 # system default
- newfp = os.fdopen(fp.fileno(), mode, bufsize)
- setattr(ui, fn, newfp)
- setattr(self, cn, newfp)
-
- self.cresult.write(struct.pack('>i', len(clientfds)))
-
- def _saveio(self):
- if self._oldios:
- return False
- ui = self.ui
- for cn, fn, _mode in _iochannels:
- ch = getattr(self, cn)
- fp = getattr(ui, fn)
- fd = os.dup(fp.fileno())
- self._oldios.append((ch, fp, fd))
- return True
-
- def _restoreio(self):
- ui = self.ui
- for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
- newfp = getattr(ui, fn)
- # close newfp while it's associated with client; otherwise it
- # would be closed when newfp is deleted
- if newfp is not fp:
- newfp.close()
- # restore original fd: fp is open again
- os.dup2(fd, fp.fileno())
- os.close(fd)
- setattr(self, cn, ch)
- setattr(ui, fn, fp)
- del self._oldios[:]
-
- def validate(self):
- """Reload the config and check if the server is up to date
-
- Read a list of '\0' separated arguments.
- Write a non-empty list of '\0' separated instruction strings or '\0'
- if the list is empty.
- An instruction string could be either:
- - "unlink $path", the client should unlink the path to stop the
- outdated server.
- - "redirect $path", the client should attempt to connect to $path
- first. If it does not work, start a new server. It implies
- "reconnect".
- - "exit $n", the client should exit directly with code n.
- This may happen if we cannot parse the config.
- - "reconnect", the client should close the connection and
- reconnect.
- If neither "reconnect" nor "redirect" is included in the instruction
- list, the client can continue with this server after completing all
- the instructions.
- """
- from mercurial import dispatch # avoid cycle
-
- args = self._readlist()
- try:
- self.ui, lui = _loadnewui(self.ui, args)
- except error.ParseError as inst:
- dispatch._formatparse(self.ui.warn, inst)
- self.ui.flush()
- self.cresult.write('exit 255')
- return
- newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
- insts = []
- if newhash.mtimehash != self.hashstate.mtimehash:
- addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
- insts.append('unlink %s' % addr)
- # mtimehash is empty if one or more extensions fail to load.
- # to be compatible with hg, still serve the client this time.
- if self.hashstate.mtimehash:
- insts.append('reconnect')
- if newhash.confighash != self.hashstate.confighash:
- addr = _hashaddress(self.baseaddress, newhash.confighash)
- insts.append('redirect %s' % addr)
- _log('validate: %s\n' % insts)
- self.cresult.write('\0'.join(insts) or '\0')
-
- def chdir(self):
- """Change current directory
-
- Note that the behavior of --cwd option is bit different from this.
- It does not affect --config parameter.
- """
- path = self._readstr()
- if not path:
- return
- _log('chdir to %r\n' % path)
- os.chdir(path)
-
- def setumask(self):
- """Change umask"""
- mask = struct.unpack('>I', self._read(4))[0]
- _log('setumask %r\n' % mask)
- os.umask(mask)
-
- def getpager(self):
- """Read cmdargs and write pager command to r-channel if enabled
-
- If pager isn't enabled, this writes '\0' because channeledoutput
- does not allow to write empty data.
- """
- from mercurial import dispatch # avoid cycle
-
- args = self._readlist()
- try:
- cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
- args)
- except (error.Abort, error.AmbiguousCommand, error.CommandError,
- error.UnknownCommand):
- cmd = None
- options = {}
- if not cmd or 'pager' not in options:
- self.cresult.write('\0')
- return
-
- pagercmd = _setuppagercmd(self.ui, options, cmd)
- if pagercmd:
- # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
- # we can exit if the pipe to the pager is closed
- if util.safehasattr(signal, 'SIGPIPE') and \
- signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
- signal.signal(signal.SIGPIPE, signal.SIG_DFL)
- self.cresult.write(pagercmd)
- else:
- self.cresult.write('\0')
-
- def setenv(self):
- """Clear and update os.environ
-
- Note that not all variables can make an effect on the running process.
- """
- l = self._readlist()
- try:
- newenv = dict(s.split('=', 1) for s in l)
- except ValueError:
- raise ValueError('unexpected value in setenv request')
- _log('setenv: %r\n' % sorted(newenv.keys()))
- os.environ.clear()
- os.environ.update(newenv)
-
- capabilities = commandserver.server.capabilities.copy()
- capabilities.update({'attachio': attachio,
- 'chdir': chdir,
- 'getpager': getpager,
- 'setenv': setenv,
- 'setumask': setumask})
-
-def _tempaddress(address):
- return '%s.%d.tmp' % (address, os.getpid())
-
-def _hashaddress(address, hashstr):
- return '%s-%s' % (address, hashstr)
-
-class chgunixservicehandler(object):
- """Set of operations for chg services"""
-
- pollinterval = 1 # [sec]
-
- def __init__(self, ui):
- self.ui = ui
- self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
- self._lastactive = time.time()
-
- def bindsocket(self, sock, address):
- self._inithashstate(address)
- self._checkextensions()
- self._bind(sock)
- self._createsymlink()
-
- def _inithashstate(self, address):
- self._baseaddress = address
- if self.ui.configbool('chgserver', 'skiphash', False):
- self._hashstate = None
- self._realaddress = address
- return
- self._hashstate = hashstate.fromui(self.ui)
- self._realaddress = _hashaddress(address, self._hashstate.confighash)
-
- def _checkextensions(self):
- if not self._hashstate:
- return
- if extensions.notloaded():
- # one or more extensions failed to load. mtimehash becomes
- # meaningless because we do not know the paths of those extensions.
- # set mtimehash to an illegal hash value to invalidate the server.
- self._hashstate.mtimehash = ''
-
- def _bind(self, sock):
- # use a unique temp address so we can stat the file and do ownership
- # check later
- tempaddress = _tempaddress(self._realaddress)
- util.bindunixsocket(sock, tempaddress)
- self._socketstat = os.stat(tempaddress)
- # rename will replace the old socket file if exists atomically. the
- # old server will detect ownership change and exit.
- util.rename(tempaddress, self._realaddress)
-
- def _createsymlink(self):
- if self._baseaddress == self._realaddress:
- return
- tempaddress = _tempaddress(self._baseaddress)
- os.symlink(os.path.basename(self._realaddress), tempaddress)
- util.rename(tempaddress, self._baseaddress)
-
- def _issocketowner(self):
- try:
- stat = os.stat(self._realaddress)
- return (stat.st_ino == self._socketstat.st_ino and
- stat.st_mtime == self._socketstat.st_mtime)
- except OSError:
- return False
-
- def unlinksocket(self, address):
- if not self._issocketowner():
- return
- # it is possible to have a race condition here that we may
- # remove another server's socket file. but that's okay
- # since that server will detect and exit automatically and
- # the client will start a new server on demand.
- try:
- os.unlink(self._realaddress)
- except OSError as exc:
- if exc.errno != errno.ENOENT:
- raise
-
- def printbanner(self, address):
- # no "listening at" message should be printed to simulate hg behavior
- pass
-
- def shouldexit(self):
- if not self._issocketowner():
- self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
- return True
- if time.time() - self._lastactive > self._idletimeout:
- self.ui.debug('being idle too long. exiting.\n')
- return True
- return False
-
- def newconnection(self):
- self._lastactive = time.time()
-
- def createcmdserver(self, repo, conn, fin, fout):
- return chgcmdserver(self.ui, repo, fin, fout, conn,
- self._hashstate, self._baseaddress)
-
-def chgunixservice(ui, repo, opts):
- # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
- # start another chg. drop it to avoid possible side effects.
- if 'CHGINTERNALMARK' in os.environ:
- del os.environ['CHGINTERNALMARK']
-
- if repo:
- # one chgserver can serve multiple repos. drop repo information
- ui.setconfig('bundle', 'mainreporoot', '', 'repo')
- h = chgunixservicehandler(ui)
- return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
-
-def uisetup(ui):
- server._cmdservicemap['chgunix'] = chgunixservice
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mercurial/chgserver.py Sat Oct 15 14:30:16 2016 +0900
@@ -0,0 +1,636 @@
+# chgserver.py - command server extension for cHg
+#
+# Copyright 2011 Yuya Nishihara <yuya@tcha.org>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+"""command server extension for cHg
+
+'S' channel (read/write)
+ propagate ui.system() request to client
+
+'attachio' command
+ attach client's stdio passed by sendmsg()
+
+'chdir' command
+ change current directory
+
+'getpager' command
+ checks if pager is enabled and which pager should be executed
+
+'setenv' command
+ replace os.environ completely
+
+'setumask' command
+ set umask
+
+'validate' command
+ reload the config and check if the server is up to date
+
+Config
+------
+
+::
+
+ [chgserver]
+ idletimeout = 3600 # seconds, after which an idle server will exit
+ skiphash = False # whether to skip config or env change checks
+"""
+
+from __future__ import absolute_import
+
+import errno
+import hashlib
+import inspect
+import os
+import re
+import signal
+import struct
+import sys
+import time
+
+from .i18n import _
+
+from . import (
+ cmdutil,
+ commandserver,
+ error,
+ extensions,
+ osutil,
+ util,
+)
+
+_log = commandserver.log
+
+def _hashlist(items):
+ """return sha1 hexdigest for a list"""
+ return hashlib.sha1(str(items)).hexdigest()
+
+# sensitive config sections affecting confighash
+_configsections = [
+ 'alias', # affects global state commands.table
+ 'extdiff', # uisetup will register new commands
+ 'extensions',
+]
+
+# sensitive environment variables affecting confighash
+_envre = re.compile(r'''\A(?:
+ CHGHG
+ |HG.*
+ |LANG(?:UAGE)?
+ |LC_.*
+ |LD_.*
+ |PATH
+ |PYTHON.*
+ |TERM(?:INFO)?
+ |TZ
+ )\Z''', re.X)
+
+def _confighash(ui):
+ """return a quick hash for detecting config/env changes
+
+ confighash is the hash of sensitive config items and environment variables.
+
+ for chgserver, it is designed that once confighash changes, the server is
+ not qualified to serve its client and should redirect the client to a new
+ server. different from mtimehash, confighash change will not mark the
+ server outdated and exit since the user can have different configs at the
+ same time.
+ """
+ sectionitems = []
+ for section in _configsections:
+ sectionitems.append(ui.configitems(section))
+ sectionhash = _hashlist(sectionitems)
+ envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
+ envhash = _hashlist(sorted(envitems))
+ return sectionhash[:6] + envhash[:6]
+
+def _getmtimepaths(ui):
+ """get a list of paths that should be checked to detect change
+
+ The list will include:
+ - extensions (will not cover all files for complex extensions)
+ - mercurial/__version__.py
+ - python binary
+ """
+ modules = [m for n, m in extensions.extensions(ui)]
+ try:
+ from . import __version__
+ modules.append(__version__)
+ except ImportError:
+ pass
+ files = [sys.executable]
+ for m in modules:
+ try:
+ files.append(inspect.getabsfile(m))
+ except TypeError:
+ pass
+ return sorted(set(files))
+
+def _mtimehash(paths):
+ """return a quick hash for detecting file changes
+
+ mtimehash calls stat on given paths and calculate a hash based on size and
+ mtime of each file. mtimehash does not read file content because reading is
+ expensive. therefore it's not 100% reliable for detecting content changes.
+ it's possible to return different hashes for same file contents.
+ it's also possible to return a same hash for different file contents for
+ some carefully crafted situation.
+
+ for chgserver, it is designed that once mtimehash changes, the server is
+ considered outdated immediately and should no longer provide service.
+
+ mtimehash is not included in confighash because we only know the paths of
+ extensions after importing them (there is imp.find_module but that faces
+ race conditions). We need to calculate confighash without importing.
+ """
+ def trystat(path):
+ try:
+ st = os.stat(path)
+ return (st.st_mtime, st.st_size)
+ except OSError:
+ # could be ENOENT, EPERM etc. not fatal in any case
+ pass
+ return _hashlist(map(trystat, paths))[:12]
+
+class hashstate(object):
+ """a structure storing confighash, mtimehash, paths used for mtimehash"""
+ def __init__(self, confighash, mtimehash, mtimepaths):
+ self.confighash = confighash
+ self.mtimehash = mtimehash
+ self.mtimepaths = mtimepaths
+
+ @staticmethod
+ def fromui(ui, mtimepaths=None):
+ if mtimepaths is None:
+ mtimepaths = _getmtimepaths(ui)
+ confighash = _confighash(ui)
+ mtimehash = _mtimehash(mtimepaths)
+ _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
+ return hashstate(confighash, mtimehash, mtimepaths)
+
+# copied from hgext/pager.py:uisetup()
+def _setuppagercmd(ui, options, cmd):
+ from . import commands # avoid cycle
+
+ if not ui.formatted():
+ return
+
+ p = ui.config("pager", "pager", os.environ.get("PAGER"))
+ usepager = False
+ always = util.parsebool(options['pager'])
+ auto = options['pager'] == 'auto'
+
+ if not p:
+ pass
+ elif always:
+ usepager = True
+ elif not auto:
+ usepager = False
+ else:
+ attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
+ attend = ui.configlist('pager', 'attend', attended)
+ ignore = ui.configlist('pager', 'ignore')
+ cmds, _ = cmdutil.findcmd(cmd, commands.table)
+
+ for cmd in cmds:
+ var = 'attend-%s' % cmd
+ if ui.config('pager', var):
+ usepager = ui.configbool('pager', var)
+ break
+ if (cmd in attend or
+ (cmd not in ignore and not attend)):
+ usepager = True
+ break
+
+ if usepager:
+ ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
+ ui.setconfig('ui', 'interactive', False, 'pager')
+ return p
+
+def _newchgui(srcui, csystem):
+ class chgui(srcui.__class__):
+ def __init__(self, src=None):
+ super(chgui, self).__init__(src)
+ if src:
+ self._csystem = getattr(src, '_csystem', csystem)
+ else:
+ self._csystem = csystem
+
+ def system(self, cmd, environ=None, cwd=None, onerr=None,
+ errprefix=None):
+ # fallback to the original system method if the output needs to be
+ # captured (to self._buffers), or the output stream is not stdout
+ # (e.g. stderr, cStringIO), because the chg client is not aware of
+ # these situations and will behave differently (write to stdout).
+ if (any(s[1] for s in self._bufferstates)
+ or not util.safehasattr(self.fout, 'fileno')
+ or self.fout.fileno() != util.stdout.fileno()):
+ return super(chgui, self).system(cmd, environ, cwd, onerr,
+ errprefix)
+ # copied from mercurial/util.py:system()
+ self.flush()
+ def py2shell(val):
+ if val is None or val is False:
+ return '0'
+ if val is True:
+ return '1'
+ return str(val)
+ env = os.environ.copy()
+ if environ:
+ env.update((k, py2shell(v)) for k, v in environ.iteritems())
+ env['HG'] = util.hgexecutable()
+ rc = self._csystem(cmd, env, cwd)
+ if rc and onerr:
+ errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
+ util.explainexit(rc)[0])
+ if errprefix:
+ errmsg = '%s: %s' % (errprefix, errmsg)
+ raise onerr(errmsg)
+ return rc
+
+ return chgui(srcui)
+
+def _loadnewui(srcui, args):
+ from . import dispatch # avoid cycle
+
+ newui = srcui.__class__()
+ for a in ['fin', 'fout', 'ferr', 'environ']:
+ setattr(newui, a, getattr(srcui, a))
+ if util.safehasattr(srcui, '_csystem'):
+ newui._csystem = srcui._csystem
+
+ # command line args
+ args = args[:]
+ dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
+
+ # stolen from tortoisehg.util.copydynamicconfig()
+ for section, name, value in srcui.walkconfig():
+ source = srcui.configsource(section, name)
+ if ':' in source or source == '--config':
+ # path:line or command line
+ continue
+ if source == 'none':
+ # ui.configsource returns 'none' by default
+ source = ''
+ newui.setconfig(section, name, value, source)
+
+ # load wd and repo config, copied from dispatch.py
+ cwds = dispatch._earlygetopt(['--cwd'], args)
+ cwd = cwds and os.path.realpath(cwds[-1]) or None
+ rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
+ path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
+
+ return (newui, newlui)
+
+class channeledsystem(object):
+ """Propagate ui.system() request in the following format:
+
+ payload length (unsigned int),
+ cmd, '\0',
+ cwd, '\0',
+ envkey, '=', val, '\0',
+ ...
+ envkey, '=', val
+
+ and waits:
+
+ exitcode length (unsigned int),
+ exitcode (int)
+ """
+ def __init__(self, in_, out, channel):
+ self.in_ = in_
+ self.out = out
+ self.channel = channel
+
+ def __call__(self, cmd, environ, cwd):
+ args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
+ args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
+ data = '\0'.join(args)
+ self.out.write(struct.pack('>cI', self.channel, len(data)))
+ self.out.write(data)
+ self.out.flush()
+
+ length = self.in_.read(4)
+ length, = struct.unpack('>I', length)
+ if length != 4:
+ raise error.Abort(_('invalid response'))
+ rc, = struct.unpack('>i', self.in_.read(4))
+ return rc
+
+_iochannels = [
+ # server.ch, ui.fp, mode
+ ('cin', 'fin', 'rb'),
+ ('cout', 'fout', 'wb'),
+ ('cerr', 'ferr', 'wb'),
+]
+
+class chgcmdserver(commandserver.server):
+ def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
+ super(chgcmdserver, self).__init__(
+ _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
+ self.clientsock = sock
+ self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
+ self.hashstate = hashstate
+ self.baseaddress = baseaddress
+ if hashstate is not None:
+ self.capabilities = self.capabilities.copy()
+ self.capabilities['validate'] = chgcmdserver.validate
+
+ def cleanup(self):
+ super(chgcmdserver, self).cleanup()
+ # dispatch._runcatch() does not flush outputs if exception is not
+ # handled by dispatch._dispatch()
+ self.ui.flush()
+ self._restoreio()
+
+ def attachio(self):
+ """Attach to client's stdio passed via unix domain socket; all
+ channels except cresult will no longer be used
+ """
+ # tell client to sendmsg() with 1-byte payload, which makes it
+ # distinctive from "attachio\n" command consumed by client.read()
+ self.clientsock.sendall(struct.pack('>cI', 'I', 1))
+ clientfds = osutil.recvfds(self.clientsock.fileno())
+ _log('received fds: %r\n' % clientfds)
+
+ ui = self.ui
+ ui.flush()
+ first = self._saveio()
+ for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
+ assert fd > 0
+ fp = getattr(ui, fn)
+ os.dup2(fd, fp.fileno())
+ os.close(fd)
+ if not first:
+ continue
+ # reset buffering mode when client is first attached. as we want
+ # to see output immediately on pager, the mode stays unchanged
+ # when client re-attached. ferr is unchanged because it should
+ # be unbuffered no matter if it is a tty or not.
+ if fn == 'ferr':
+ newfp = fp
+ else:
+ # make it line buffered explicitly because the default is
+ # decided on first write(), where fout could be a pager.
+ if fp.isatty():
+ bufsize = 1 # line buffered
+ else:
+ bufsize = -1 # system default
+ newfp = os.fdopen(fp.fileno(), mode, bufsize)
+ setattr(ui, fn, newfp)
+ setattr(self, cn, newfp)
+
+ self.cresult.write(struct.pack('>i', len(clientfds)))
+
+ def _saveio(self):
+ if self._oldios:
+ return False
+ ui = self.ui
+ for cn, fn, _mode in _iochannels:
+ ch = getattr(self, cn)
+ fp = getattr(ui, fn)
+ fd = os.dup(fp.fileno())
+ self._oldios.append((ch, fp, fd))
+ return True
+
+ def _restoreio(self):
+ ui = self.ui
+ for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
+ newfp = getattr(ui, fn)
+ # close newfp while it's associated with client; otherwise it
+ # would be closed when newfp is deleted
+ if newfp is not fp:
+ newfp.close()
+ # restore original fd: fp is open again
+ os.dup2(fd, fp.fileno())
+ os.close(fd)
+ setattr(self, cn, ch)
+ setattr(ui, fn, fp)
+ del self._oldios[:]
+
+ def validate(self):
+ """Reload the config and check if the server is up to date
+
+ Read a list of '\0' separated arguments.
+ Write a non-empty list of '\0' separated instruction strings or '\0'
+ if the list is empty.
+ An instruction string could be either:
+ - "unlink $path", the client should unlink the path to stop the
+ outdated server.
+ - "redirect $path", the client should attempt to connect to $path
+ first. If it does not work, start a new server. It implies
+ "reconnect".
+ - "exit $n", the client should exit directly with code n.
+ This may happen if we cannot parse the config.
+ - "reconnect", the client should close the connection and
+ reconnect.
+ If neither "reconnect" nor "redirect" is included in the instruction
+ list, the client can continue with this server after completing all
+ the instructions.
+ """
+ from . import dispatch # avoid cycle
+
+ args = self._readlist()
+ try:
+ self.ui, lui = _loadnewui(self.ui, args)
+ except error.ParseError as inst:
+ dispatch._formatparse(self.ui.warn, inst)
+ self.ui.flush()
+ self.cresult.write('exit 255')
+ return
+ newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
+ insts = []
+ if newhash.mtimehash != self.hashstate.mtimehash:
+ addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
+ insts.append('unlink %s' % addr)
+ # mtimehash is empty if one or more extensions fail to load.
+ # to be compatible with hg, still serve the client this time.
+ if self.hashstate.mtimehash:
+ insts.append('reconnect')
+ if newhash.confighash != self.hashstate.confighash:
+ addr = _hashaddress(self.baseaddress, newhash.confighash)
+ insts.append('redirect %s' % addr)
+ _log('validate: %s\n' % insts)
+ self.cresult.write('\0'.join(insts) or '\0')
+
+ def chdir(self):
+ """Change current directory
+
+ Note that the behavior of --cwd option is bit different from this.
+ It does not affect --config parameter.
+ """
+ path = self._readstr()
+ if not path:
+ return
+ _log('chdir to %r\n' % path)
+ os.chdir(path)
+
+ def setumask(self):
+ """Change umask"""
+ mask = struct.unpack('>I', self._read(4))[0]
+ _log('setumask %r\n' % mask)
+ os.umask(mask)
+
+ def getpager(self):
+ """Read cmdargs and write pager command to r-channel if enabled
+
+ If pager isn't enabled, this writes '\0' because channeledoutput
+ does not allow to write empty data.
+ """
+ from . import dispatch # avoid cycle
+
+ args = self._readlist()
+ try:
+ cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
+ args)
+ except (error.Abort, error.AmbiguousCommand, error.CommandError,
+ error.UnknownCommand):
+ cmd = None
+ options = {}
+ if not cmd or 'pager' not in options:
+ self.cresult.write('\0')
+ return
+
+ pagercmd = _setuppagercmd(self.ui, options, cmd)
+ if pagercmd:
+ # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
+ # we can exit if the pipe to the pager is closed
+ if util.safehasattr(signal, 'SIGPIPE') and \
+ signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
+ signal.signal(signal.SIGPIPE, signal.SIG_DFL)
+ self.cresult.write(pagercmd)
+ else:
+ self.cresult.write('\0')
+
+ def setenv(self):
+ """Clear and update os.environ
+
+ Note that not all variables can make an effect on the running process.
+ """
+ l = self._readlist()
+ try:
+ newenv = dict(s.split('=', 1) for s in l)
+ except ValueError:
+ raise ValueError('unexpected value in setenv request')
+ _log('setenv: %r\n' % sorted(newenv.keys()))
+ os.environ.clear()
+ os.environ.update(newenv)
+
+ capabilities = commandserver.server.capabilities.copy()
+ capabilities.update({'attachio': attachio,
+ 'chdir': chdir,
+ 'getpager': getpager,
+ 'setenv': setenv,
+ 'setumask': setumask})
+
+def _tempaddress(address):
+ return '%s.%d.tmp' % (address, os.getpid())
+
+def _hashaddress(address, hashstr):
+ return '%s-%s' % (address, hashstr)
+
+class chgunixservicehandler(object):
+ """Set of operations for chg services"""
+
+ pollinterval = 1 # [sec]
+
+ def __init__(self, ui):
+ self.ui = ui
+ self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
+ self._lastactive = time.time()
+
+ def bindsocket(self, sock, address):
+ self._inithashstate(address)
+ self._checkextensions()
+ self._bind(sock)
+ self._createsymlink()
+
+ def _inithashstate(self, address):
+ self._baseaddress = address
+ if self.ui.configbool('chgserver', 'skiphash', False):
+ self._hashstate = None
+ self._realaddress = address
+ return
+ self._hashstate = hashstate.fromui(self.ui)
+ self._realaddress = _hashaddress(address, self._hashstate.confighash)
+
+ def _checkextensions(self):
+ if not self._hashstate:
+ return
+ if extensions.notloaded():
+ # one or more extensions failed to load. mtimehash becomes
+ # meaningless because we do not know the paths of those extensions.
+ # set mtimehash to an illegal hash value to invalidate the server.
+ self._hashstate.mtimehash = ''
+
+ def _bind(self, sock):
+ # use a unique temp address so we can stat the file and do ownership
+ # check later
+ tempaddress = _tempaddress(self._realaddress)
+ util.bindunixsocket(sock, tempaddress)
+ self._socketstat = os.stat(tempaddress)
+ # rename will replace the old socket file if exists atomically. the
+ # old server will detect ownership change and exit.
+ util.rename(tempaddress, self._realaddress)
+
+ def _createsymlink(self):
+ if self._baseaddress == self._realaddress:
+ return
+ tempaddress = _tempaddress(self._baseaddress)
+ os.symlink(os.path.basename(self._realaddress), tempaddress)
+ util.rename(tempaddress, self._baseaddress)
+
+ def _issocketowner(self):
+ try:
+ stat = os.stat(self._realaddress)
+ return (stat.st_ino == self._socketstat.st_ino and
+ stat.st_mtime == self._socketstat.st_mtime)
+ except OSError:
+ return False
+
+ def unlinksocket(self, address):
+ if not self._issocketowner():
+ return
+ # it is possible to have a race condition here that we may
+ # remove another server's socket file. but that's okay
+ # since that server will detect and exit automatically and
+ # the client will start a new server on demand.
+ try:
+ os.unlink(self._realaddress)
+ except OSError as exc:
+ if exc.errno != errno.ENOENT:
+ raise
+
+ def printbanner(self, address):
+ # no "listening at" message should be printed to simulate hg behavior
+ pass
+
+ def shouldexit(self):
+ if not self._issocketowner():
+ self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
+ return True
+ if time.time() - self._lastactive > self._idletimeout:
+ self.ui.debug('being idle too long. exiting.\n')
+ return True
+ return False
+
+ def newconnection(self):
+ self._lastactive = time.time()
+
+ def createcmdserver(self, repo, conn, fin, fout):
+ return chgcmdserver(self.ui, repo, fin, fout, conn,
+ self._hashstate, self._baseaddress)
+
+def chgunixservice(ui, repo, opts):
+ # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
+ # start another chg. drop it to avoid possible side effects.
+ if 'CHGINTERNALMARK' in os.environ:
+ del os.environ['CHGINTERNALMARK']
+
+ if repo:
+ # one chgserver can serve multiple repos. drop repo information
+ ui.setconfig('bundle', 'mainreporoot', '', 'repo')
+ h = chgunixservicehandler(ui)
+ return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
--- a/mercurial/server.py Sat Oct 15 14:37:18 2016 +0900
+++ b/mercurial/server.py Sat Oct 15 14:30:16 2016 +0900
@@ -15,6 +15,7 @@
from .i18n import _
from . import (
+ chgserver,
commandserver,
error,
hgweb,
@@ -109,6 +110,7 @@
return runfn()
_cmdservicemap = {
+ 'chgunix': chgserver.chgunixservice,
'pipe': commandserver.pipeservice,
'unix': commandserver.unixforkingservice,
}
--- a/tests/test-basic.t Sat Oct 15 14:37:18 2016 +0900
+++ b/tests/test-basic.t Sat Oct 15 14:30:16 2016 +0900
@@ -6,7 +6,6 @@
defaults.shelve=--date "0 0"
defaults.tag=-d "0 0"
devel.all-warnings=true
- extensions.chgserver= (?)
largefiles.usercache=$TESTTMP/.cache/largefiles (glob)
ui.slash=True
ui.interactive=False
--- a/tests/test-help.t Sat Oct 15 14:37:18 2016 +0900
+++ b/tests/test-help.t Sat Oct 15 14:30:16 2016 +0900
@@ -241,7 +241,6 @@
enabled extensions:
- chgserver command server extension for cHg (EXPERIMENTAL) (?)
children command to display child changesets (DEPRECATED)
rebase command to move sets of revisions to a different ancestor
--- a/tests/test-hgrc.t Sat Oct 15 14:37:18 2016 +0900
+++ b/tests/test-hgrc.t Sat Oct 15 14:30:16 2016 +0900
@@ -46,7 +46,6 @@
default = $TESTTMP/foo%bar (glob)
$ hg showconfig
bundle.mainreporoot=$TESTTMP/foobar (glob)
- extensions.chgserver= (?)
paths.default=$TESTTMP/foo%bar (glob)
$ cd ..
@@ -81,7 +80,6 @@
$ hg showconfig --config ui.verbose=True --quiet
bundle.mainreporoot=$TESTTMP
- extensions.chgserver= (?)
ui.verbose=False
ui.debug=False
ui.quiet=True
@@ -113,7 +111,6 @@
$ hg showconfig
bundle.mainreporoot=$TESTTMP
- extensions.chgserver= (?)
ui.username=$FAKEUSER
$ unset FAKEUSER
@@ -159,7 +156,6 @@
$TESTTMP/hgrc:13: alias.log=log -g
repo: bundle.mainreporoot=$TESTTMP
$TESTTMP/hgrc:11: defaults.identify=-n
- --config: extensions.chgserver= (?)
$TESTTMP/hgrc:2: ui.debug=true
$TESTTMP/hgrc:3: ui.fallbackencoding=ASCII
$TESTTMP/hgrc:4: ui.quiet=true
@@ -175,7 +171,6 @@
$ hg showconfig --config ui.traceback=True --debug
read config from: $TESTTMP/hgrc
repo: bundle.mainreporoot=$TESTTMP
- --config: extensions.chgserver= (?)
--config: ui.traceback=True
--verbose: ui.verbose=False
--debug: ui.debug=True
@@ -199,7 +194,6 @@
read config from: $TESTTMP/hgrc
repo: bundle.mainreporoot=$TESTTMP
$TESTTMP/hgrc:15: extensions.plain=./plain.py
- --config: extensions.chgserver= (?)
--config: ui.traceback=True
--verbose: ui.verbose=False
--debug: ui.debug=True
@@ -210,7 +204,6 @@
read config from: $TESTTMP/hgrc
repo: bundle.mainreporoot=$TESTTMP
$TESTTMP/hgrc:15: extensions.plain=./plain.py
- --config: extensions.chgserver= (?)
--config: ui.traceback=True
--verbose: ui.verbose=False
--debug: ui.debug=True
@@ -221,7 +214,6 @@
read config from: $TESTTMP/hgrc
repo: bundle.mainreporoot=$TESTTMP
$TESTTMP/hgrc:15: extensions.plain=./plain.py
- --config: extensions.chgserver= (?)
--config: ui.traceback=True
--verbose: ui.verbose=False
--debug: ui.debug=True