# HG changeset patch # User Yuya Nishihara # Date 1476509416 -32400 # Node ID ff7df4bb75de0d4c4af9a4420d339f70125709c0 # Parent cc374292a561ffd54958a36e67a6bebbf575ad2f 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. diff -r cc374292a561 -r ff7df4bb75de contrib/chg/Makefile --- 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 diff -r cc374292a561 -r ff7df4bb75de contrib/chg/chg.c --- 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; diff -r cc374292a561 -r ff7df4bb75de hgext/chgserver.py --- 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 -# -# 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 diff -r cc374292a561 -r ff7df4bb75de mercurial/chgserver.py --- /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 +# +# 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) diff -r cc374292a561 -r ff7df4bb75de mercurial/server.py --- 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, } diff -r cc374292a561 -r ff7df4bb75de tests/test-basic.t --- 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 diff -r cc374292a561 -r ff7df4bb75de tests/test-help.t --- 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 diff -r cc374292a561 -r ff7df4bb75de tests/test-hgrc.t --- 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