chgserver.py
768 lines
| 26.2 KiB
| text/x-python
|
PythonLexer
/ mercurial / chgserver.py
|
r30513 | # 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 | ||||
'setenv' command | ||||
replace os.environ completely | ||||
|
r40144 | 'setumask' command (DEPRECATED) | ||
'setumask2' command | ||||
|
r30513 | set umask | ||
'validate' command | ||||
reload the config and check if the server is up to date | ||||
Config | ||||
------ | ||||
:: | ||||
[chgserver] | ||||
|
r30990 | # how long (in seconds) should an idle chg server exit | ||
idletimeout = 3600 | ||||
# whether to skip config or env change checks | ||||
skiphash = False | ||||
|
r30513 | """ | ||
from __future__ import absolute_import | ||||
import inspect | ||||
import os | ||||
import re | ||||
|
r32232 | import socket | ||
|
r36799 | import stat | ||
|
r30513 | import struct | ||
import time | ||||
from .i18n import _ | ||||
|
r43359 | from .pycompat import ( | ||
getattr, | ||||
setattr, | ||||
) | ||||
|
r46729 | from .node import hex | ||
|
r30513 | |||
from . import ( | ||||
commandserver, | ||||
|
r30635 | encoding, | ||
|
r30513 | error, | ||
extensions, | ||||
|
r30669 | pycompat, | ||
|
r30513 | util, | ||
) | ||||
|
r37137 | from .utils import ( | ||
|
r44517 | hashutil, | ||
|
r37137 | procutil, | ||
|
r41994 | stringutil, | ||
|
r37137 | ) | ||
|
r43346 | |||
|
r30513 | def _hashlist(items): | ||
"""return sha1 hexdigest for a list""" | ||||
|
r46729 | return hex(hashutil.sha1(stringutil.pprint(items)).digest()) | ||
|
r30513 | |||
|
r43346 | |||
|
r30513 | # sensitive config sections affecting confighash | ||
_configsections = [ | ||||
|
r43347 | b'alias', # affects global state commands.table | ||
|
r45106 | b'diff-tools', # affects whether gui or not in extdiff's uisetup | ||
|
r43347 | b'eol', # uses setconfig('eol', ...) | ||
b'extdiff', # uisetup will register new commands | ||||
b'extensions', | ||||
|
r45102 | b'fastannotate', # affects annotate command and adds fastannonate cmd | ||
|
r45106 | b'merge-tools', # affects whether gui or not in extdiff's uisetup | ||
|
r44832 | b'schemes', # extsetup will update global hg.schemes | ||
|
r30513 | ] | ||
|
r34840 | _configsectionitems = [ | ||
|
r43347 | (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup | ||
|
r34840 | ] | ||
|
r30513 | # sensitive environment variables affecting confighash | ||
|
r43346 | _envre = re.compile( | ||
br'''\A(?: | ||||
|
r30513 | CHGHG | ||
|
r32271 | |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)? | ||
|HG(?:ENCODING|PLAIN).* | ||||
|
r30513 | |LANG(?:UAGE)? | ||
|LC_.* | ||||
|LD_.* | ||||
|PATH | ||||
|PYTHON.* | ||||
|TERM(?:INFO)? | ||||
|TZ | ||||
|
r43346 | )\Z''', | ||
re.X, | ||||
) | ||||
|
r30513 | |||
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)) | ||||
|
r34840 | for section, item in _configsectionitems: | ||
sectionitems.append(ui.config(section, item)) | ||||
|
r30513 | sectionhash = _hashlist(sectionitems) | ||
|
r34888 | # If $CHGHG is set, the change to $HG should not trigger a new chg server | ||
|
r43347 | if b'CHGHG' in encoding.environ: | ||
ignored = {b'HG'} | ||||
|
r34888 | else: | ||
ignored = set() | ||||
|
r43346 | envitems = [ | ||
(k, v) | ||||
|
r43376 | for k, v in pycompat.iteritems(encoding.environ) | ||
|
r43346 | if _envre.match(k) and k not in ignored | ||
] | ||||
|
r30513 | envhash = _hashlist(sorted(envitems)) | ||
return sectionhash[:6] + envhash[:6] | ||||
|
r43346 | |||
|
r30513 | 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__ | ||||
|
r43346 | |||
|
r30513 | modules.append(__version__) | ||
except ImportError: | ||||
pass | ||||
|
r42723 | files = [] | ||
if pycompat.sysexecutable: | ||||
files.append(pycompat.sysexecutable) | ||||
|
r30513 | for m in modules: | ||
try: | ||||
|
r41982 | files.append(pycompat.fsencode(inspect.getabsfile(m))) | ||
|
r30513 | except TypeError: | ||
pass | ||||
return sorted(set(files)) | ||||
|
r43346 | |||
|
r30513 | 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. | ||||
""" | ||||
|
r43346 | |||
|
r30513 | def trystat(path): | ||
try: | ||||
st = os.stat(path) | ||||
|
r36799 | return (st[stat.ST_MTIME], st.st_size) | ||
|
r30513 | except OSError: | ||
# could be ENOENT, EPERM etc. not fatal in any case | ||||
pass | ||||
|
r43346 | |||
|
r43086 | return _hashlist(pycompat.maplist(trystat, paths))[:12] | ||
|
r30513 | |||
|
r43346 | |||
|
r30513 | class hashstate(object): | ||
"""a structure storing confighash, mtimehash, paths used for mtimehash""" | ||||
|
r43346 | |||
|
r30513 | 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) | ||||
|
r43346 | ui.log( | ||
|
r43347 | b'cmdserver', | ||
b'confighash = %s mtimehash = %s\n', | ||||
|
r43346 | confighash, | ||
mtimehash, | ||||
) | ||||
|
r30513 | return hashstate(confighash, mtimehash, mtimepaths) | ||
|
r43346 | |||
|
r30740 | def _newchgui(srcui, csystem, attachio): | ||
|
r30513 | 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 | ||||
|
r31108 | def _runsystem(self, cmd, environ, cwd, out): | ||
|
r39874 | # fallback to the original system method if | ||
# a. the output stream is not stdout (e.g. stderr, cStringIO), | ||||
|
r41321 | # b. or stdout is redirected by protectfinout(), | ||
|
r39874 | # because the chg client is not aware of these situations and | ||
# will behave differently (i.e. write to stdout). | ||||
|
r43346 | if ( | ||
out is not self.fout | ||||
|
r43347 | or not util.safehasattr(self.fout, b'fileno') | ||
|
r39875 | or self.fout.fileno() != procutil.stdout.fileno() | ||
|
r43346 | or self._finoutredirected | ||
): | ||||
|
r37138 | return procutil.system(cmd, environ=environ, cwd=cwd, out=out) | ||
|
r30513 | self.flush() | ||
|
r37138 | return self._csystem(cmd, procutil.shellenviron(environ), cwd) | ||
|
r30513 | |||
|
r31954 | def _runpager(self, cmd, env=None): | ||
|
r43346 | self._csystem( | ||
cmd, | ||||
procutil.shellenviron(env), | ||||
|
r43347 | type=b'pager', | ||
cmdtable={b'attachio': attachio}, | ||||
|
r43346 | ) | ||
|
r31690 | return True | ||
|
r30740 | |||
|
r30513 | return chgui(srcui) | ||
|
r43346 | |||
|
r40859 | def _loadnewui(srcui, args, cdebug): | ||
|
r30513 | from . import dispatch # avoid cycle | ||
|
r30572 | newui = srcui.__class__.load() | ||
|
r43347 | for a in [b'fin', b'fout', b'ferr', b'environ']: | ||
|
r30513 | setattr(newui, a, getattr(srcui, a)) | ||
|
r43347 | if util.safehasattr(srcui, b'_csystem'): | ||
|
r30513 | newui._csystem = srcui._csystem | ||
# command line args | ||||
|
r35224 | options = dispatch._earlyparseopts(newui, args) | ||
|
r43347 | dispatch._parseconfig(newui, options[b'config']) | ||
|
r30513 | |||
# stolen from tortoisehg.util.copydynamicconfig() | ||||
for section, name, value in srcui.walkconfig(): | ||||
source = srcui.configsource(section, name) | ||||
|
r43347 | if b':' in source or source == b'--config' or source.startswith(b'$'): | ||
|
r31695 | # path:line or command line, or environ | ||
|
r30513 | continue | ||
newui.setconfig(section, name, value, source) | ||||
# load wd and repo config, copied from dispatch.py | ||||
|
r43347 | cwd = options[b'cwd'] | ||
|
r35180 | cwd = cwd and os.path.realpath(cwd) or None | ||
|
r43347 | rpath = options[b'repository'] | ||
|
r30513 | path, newlui = dispatch._getlocal(newui, rpath, wd=cwd) | ||
|
r40760 | extensions.populateui(newui) | ||
|
r40859 | commandserver.setuplogging(newui, fp=cdebug) | ||
|
r40760 | if newui is not newlui: | ||
extensions.populateui(newlui) | ||||
|
r40859 | commandserver.setuplogging(newlui, fp=cdebug) | ||
|
r40760 | |||
|
r30513 | return (newui, newlui) | ||
|
r43346 | |||
|
r30513 | class channeledsystem(object): | ||
"""Propagate ui.system() request in the following format: | ||||
payload length (unsigned int), | ||||
|
r30726 | type, '\0', | ||
|
r30513 | cmd, '\0', | ||
cwd, '\0', | ||||
envkey, '=', val, '\0', | ||||
... | ||||
envkey, '=', val | ||||
|
r30726 | if type == 'system', waits for: | ||
|
r30513 | |||
exitcode length (unsigned int), | ||||
exitcode (int) | ||||
|
r30739 | |||
if type == 'pager', repetitively waits for a command name ending with '\n' | ||||
and executes it defined by cmdtable, or exits the loop if the command name | ||||
is empty. | ||||
|
r30513 | """ | ||
|
r43346 | |||
|
r30513 | def __init__(self, in_, out, channel): | ||
self.in_ = in_ | ||||
self.out = out | ||||
self.channel = channel | ||||
|
r43347 | def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None): | ||
r48423 | args = [type, cmd, util.abspath(cwd or b'.')] | |||
|
r43376 | args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ)) | ||
|
r43347 | data = b'\0'.join(args) | ||
self.out.write(struct.pack(b'>cI', self.channel, len(data))) | ||||
|
r30513 | self.out.write(data) | ||
self.out.flush() | ||||
|
r43347 | if type == b'system': | ||
|
r30727 | length = self.in_.read(4) | ||
|
r43347 | (length,) = struct.unpack(b'>I', length) | ||
|
r30727 | if length != 4: | ||
|
r43347 | raise error.Abort(_(b'invalid response')) | ||
(rc,) = struct.unpack(b'>i', self.in_.read(4)) | ||||
|
r30727 | return rc | ||
|
r43347 | elif type == b'pager': | ||
|
r30739 | while True: | ||
cmd = self.in_.readline()[:-1] | ||||
if not cmd: | ||||
break | ||||
if cmdtable and cmd in cmdtable: | ||||
cmdtable[cmd]() | ||||
else: | ||||
|
r43347 | raise error.Abort(_(b'unexpected command: %s') % cmd) | ||
|
r30727 | else: | ||
|
r43347 | raise error.ProgrammingError(b'invalid S channel type: %s' % type) | ||
|
r30513 | |||
|
r43346 | |||
|
r30513 | _iochannels = [ | ||
# server.ch, ui.fp, mode | ||||
|
r43906 | (b'cin', b'fin', 'rb'), | ||
(b'cout', b'fout', 'wb'), | ||||
(b'cerr', b'ferr', 'wb'), | ||||
|
r30513 | ] | ||
|
r43346 | |||
|
r30513 | class chgcmdserver(commandserver.server): | ||
|
r43346 | def __init__( | ||
self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress | ||||
): | ||||
|
r30513 | super(chgcmdserver, self).__init__( | ||
|
r43347 | _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio), | ||
|
r43346 | repo, | ||
fin, | ||||
fout, | ||||
prereposetups, | ||||
) | ||||
|
r30513 | self.clientsock = sock | ||
|
r39774 | self._ioattached = False | ||
|
r30513 | 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() | ||||
|
r43347 | self.capabilities[b'validate'] = chgcmdserver.validate | ||
|
r30513 | |||
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() | ||||
|
r39774 | self._ioattached = False | ||
|
r30513 | |||
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() | ||||
|
r43347 | self.clientsock.sendall(struct.pack(b'>cI', b'I', 1)) | ||
|
r32203 | clientfds = util.recvfds(self.clientsock.fileno()) | ||
|
r43347 | self.ui.log(b'chgserver', b'received fds: %r\n', clientfds) | ||
|
r30513 | |||
ui = self.ui | ||||
ui.flush() | ||||
|
r39774 | self._saveio() | ||
|
r30513 | for fd, (cn, fn, mode) in zip(clientfds, _iochannels): | ||
assert fd > 0 | ||||
fp = getattr(ui, fn) | ||||
os.dup2(fd, fp.fileno()) | ||||
os.close(fd) | ||||
|
r39774 | if self._ioattached: | ||
|
r30513 | 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. | ||||
|
r43347 | if fn == b'ferr': | ||
|
r30513 | newfp = fp | ||
|
r46452 | elif pycompat.ispy3: | ||
# On Python 3, the standard library doesn't offer line-buffered | ||||
# binary streams, so wrap/unwrap it. | ||||
if fp.isatty(): | ||||
newfp = procutil.make_line_buffered(fp) | ||||
else: | ||||
newfp = procutil.unwrap_line_buffered(fp) | ||||
|
r30513 | else: | ||
|
r46452 | # Python 2 uses the I/O streams provided by the C library, so | ||
# make it line-buffered explicitly. Otherwise the default would | ||||
# be decided on first write(), where fout could be a pager. | ||||
|
r30513 | if fp.isatty(): | ||
bufsize = 1 # line buffered | ||||
else: | ||||
bufsize = -1 # system default | ||||
newfp = os.fdopen(fp.fileno(), mode, bufsize) | ||||
|
r46452 | if newfp is not fp: | ||
|
r30513 | setattr(ui, fn, newfp) | ||
setattr(self, cn, newfp) | ||||
|
r39774 | self._ioattached = True | ||
|
r43347 | self.cresult.write(struct.pack(b'>i', len(clientfds))) | ||
|
r30513 | |||
def _saveio(self): | ||||
if self._oldios: | ||||
|
r39774 | return | ||
|
r30513 | 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)) | ||||
def _restoreio(self): | ||||
|
r45745 | if not self._oldios: | ||
return | ||||
nullfd = os.open(os.devnull, os.O_WRONLY) | ||||
|
r30513 | ui = self.ui | ||
|
r45745 | for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels): | ||
|
r30513 | newfp = getattr(ui, fn) | ||
|
r46452 | # On Python 2, newfp and fp may be separate file objects associated | ||
# with the same fd, so we must close newfp while it's associated | ||||
# with the client. Otherwise the new associated fd would be closed | ||||
# when newfp gets deleted. On Python 3, newfp is just a wrapper | ||||
# around fp even if newfp is not fp, so deleting newfp is safe. | ||||
if not (pycompat.ispy3 or newfp is fp): | ||||
|
r30513 | newfp.close() | ||
# restore original fd: fp is open again | ||||
|
r45580 | try: | ||
|
r46452 | if (pycompat.ispy3 or newfp is fp) and 'w' in mode: | ||
|
r45745 | # Discard buffered data which couldn't be flushed because | ||
# of EPIPE. The data should belong to the current session | ||||
# and should never persist. | ||||
os.dup2(nullfd, fp.fileno()) | ||||
fp.flush() | ||||
|
r45580 | os.dup2(fd, fp.fileno()) | ||
except OSError as err: | ||||
# According to issue6330, running chg on heavy loaded systems | ||||
# can lead to EBUSY. [man dup2] indicates that, on Linux, | ||||
# EBUSY comes from a race condition between open() and dup2(). | ||||
# However it's not clear why open() race occurred for | ||||
# newfd=stdin/out/err. | ||||
self.ui.log( | ||||
b'chgserver', | ||||
b'got %s while duplicating %s\n', | ||||
stringutil.forcebytestr(err), | ||||
fn, | ||||
) | ||||
|
r30513 | os.close(fd) | ||
setattr(self, cn, ch) | ||||
setattr(ui, fn, fp) | ||||
|
r45745 | os.close(nullfd) | ||
|
r30513 | 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. | ||||
""" | ||||
args = self._readlist() | ||||
|
r46616 | errorraised = False | ||
|
r46759 | detailed_exit_code = 255 | ||
|
r30513 | try: | ||
|
r40859 | self.ui, lui = _loadnewui(self.ui, args, self.cdebug) | ||
|
r46616 | except error.RepoError as inst: | ||
# RepoError can be raised while trying to read shared source | ||||
# configuration | ||||
self.ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst)) | ||||
if inst.hint: | ||||
self.ui.error(_(b"(%s)\n") % inst.hint) | ||||
errorraised = True | ||||
|
r48073 | except error.Error as inst: | ||
|
r48069 | if inst.detailed_exit_code is not None: | ||
detailed_exit_code = inst.detailed_exit_code | ||||
|
r46497 | self.ui.error(inst.format()) | ||
|
r46616 | errorraised = True | ||
if errorraised: | ||||
|
r40146 | self.ui.flush() | ||
|
r46759 | exit_code = 255 | ||
if self.ui.configbool(b'ui', b'detailed-exit-code'): | ||||
exit_code = detailed_exit_code | ||||
self.cresult.write(b'exit %d' % exit_code) | ||||
|
r40146 | return | ||
|
r30513 | newhash = hashstate.fromui(lui, self.hashstate.mtimepaths) | ||
insts = [] | ||||
if newhash.mtimehash != self.hashstate.mtimehash: | ||||
addr = _hashaddress(self.baseaddress, self.hashstate.confighash) | ||||
|
r43347 | insts.append(b'unlink %s' % addr) | ||
|
r30513 | # 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: | ||||
|
r43347 | insts.append(b'reconnect') | ||
|
r30513 | if newhash.confighash != self.hashstate.confighash: | ||
addr = _hashaddress(self.baseaddress, newhash.confighash) | ||||
|
r43347 | insts.append(b'redirect %s' % addr) | ||
self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts)) | ||||
self.cresult.write(b'\0'.join(insts) or b'\0') | ||||
|
r30513 | |||
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 | ||||
|
r44020 | self.ui.log(b'chgserver', b"chdir to '%s'\n", path) | ||
|
r30513 | os.chdir(path) | ||
def setumask(self): | ||||
|
r40144 | """Change umask (DEPRECATED)""" | ||
# BUG: this does not follow the message frame structure, but kept for | ||||
# backward compatibility with old chg clients for some time | ||||
self._setumask(self._read(4)) | ||||
def setumask2(self): | ||||
|
r30513 | """Change umask""" | ||
|
r40144 | data = self._readstr() | ||
if len(data) != 4: | ||||
|
r43347 | raise ValueError(b'invalid mask length in setumask2 request') | ||
|
r40144 | self._setumask(data) | ||
def _setumask(self, data): | ||||
|
r43347 | mask = struct.unpack(b'>I', data)[0] | ||
self.ui.log(b'chgserver', b'setumask %r\n', mask) | ||||
|
r45119 | util.setumask(mask) | ||
|
r30513 | |||
|
r30644 | def runcommand(self): | ||
|
r39775 | # pager may be attached within the runcommand session, which should | ||
# be detached at the end of the session. otherwise the pager wouldn't | ||||
# receive EOF. | ||||
globaloldios = self._oldios | ||||
self._oldios = [] | ||||
try: | ||||
return super(chgcmdserver, self).runcommand() | ||||
finally: | ||||
self._restoreio() | ||||
self._oldios = globaloldios | ||||
|
r30644 | |||
|
r30513 | 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: | ||||
|
r43347 | newenv = dict(s.split(b'=', 1) for s in l) | ||
|
r30513 | except ValueError: | ||
|
r43347 | raise ValueError(b'unexpected value in setenv request') | ||
self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys())) | ||||
|
r44312 | |||
|
r30635 | encoding.environ.clear() | ||
encoding.environ.update(newenv) | ||||
|
r30513 | |||
capabilities = commandserver.server.capabilities.copy() | ||||
|
r43346 | capabilities.update( | ||
{ | ||||
|
r43347 | b'attachio': attachio, | ||
b'chdir': chdir, | ||||
b'runcommand': runcommand, | ||||
b'setenv': setenv, | ||||
b'setumask': setumask, | ||||
b'setumask2': setumask2, | ||||
|
r43346 | } | ||
) | ||||
|
r30513 | |||
|
r43347 | if util.safehasattr(procutil, b'setprocname'): | ||
|
r43346 | |||
|
r30750 | def setprocname(self): | ||
"""Change process title""" | ||||
name = self._readstr() | ||||
|
r43347 | self.ui.log(b'chgserver', b'setprocname: %r\n', name) | ||
|
r37138 | procutil.setprocname(name) | ||
|
r43346 | |||
|
r43347 | capabilities[b'setprocname'] = setprocname | ||
|
r30750 | |||
|
r43346 | |||
|
r30513 | def _tempaddress(address): | ||
|
r43347 | return b'%s.%d.tmp' % (address, os.getpid()) | ||
|
r30513 | |||
|
r43346 | |||
|
r30513 | def _hashaddress(address, hashstr): | ||
|
r30619 | # if the basename of address contains '.', use only the left part. this | ||
# makes it possible for the client to pass 'server.tmp$PID' and follow by | ||||
# an atomic rename to avoid locking when spawning new servers. | ||||
dirname, basename = os.path.split(address) | ||||
|
r43347 | basename = basename.split(b'.', 1)[0] | ||
return b'%s-%s' % (os.path.join(dirname, basename), hashstr) | ||||
|
r30513 | |||
|
r43346 | |||
|
r30513 | class chgunixservicehandler(object): | ||
"""Set of operations for chg services""" | ||||
pollinterval = 1 # [sec] | ||||
def __init__(self, ui): | ||||
self.ui = ui | ||||
|
r49317 | |||
# TODO: use PEP 526 syntax (`_hashstate: hashstate` at the class level) | ||||
# when 3.5 support is dropped. | ||||
self._hashstate = None # type: hashstate | ||||
self._baseaddress = None # type: bytes | ||||
self._realaddress = None # type: bytes | ||||
|
r43347 | self._idletimeout = ui.configint(b'chgserver', b'idletimeout') | ||
|
r30513 | self._lastactive = time.time() | ||
def bindsocket(self, sock, address): | ||||
self._inithashstate(address) | ||||
self._checkextensions() | ||||
self._bind(sock) | ||||
self._createsymlink() | ||||
|
r32233 | # no "listening at" message should be printed to simulate hg behavior | ||
|
r30513 | |||
def _inithashstate(self, address): | ||||
self._baseaddress = address | ||||
|
r43347 | if self.ui.configbool(b'chgserver', b'skiphash'): | ||
|
r30513 | 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. | ||||
|
r43347 | self._hashstate.mtimehash = b'' | ||
|
r30513 | |||
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) | ||||
|
r32232 | sock.listen(socket.SOMAXCONN) | ||
|
r30513 | # 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: | ||||
|
r36799 | st = os.stat(self._realaddress) | ||
|
r43346 | return ( | ||
st.st_ino == self._socketstat.st_ino | ||||
and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME] | ||||
) | ||||
|
r30513 | 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. | ||||
|
r31545 | util.tryunlink(self._realaddress) | ||
|
r30513 | |||
def shouldexit(self): | ||||
if not self._issocketowner(): | ||||
|
r43346 | self.ui.log( | ||
b'chgserver', b'%s is not owned, exiting.\n', self._realaddress | ||||
) | ||||
|
r30513 | return True | ||
if time.time() - self._lastactive > self._idletimeout: | ||||
|
r40863 | self.ui.log(b'chgserver', b'being idle too long. exiting.\n') | ||
|
r30513 | return True | ||
return False | ||||
def newconnection(self): | ||||
self._lastactive = time.time() | ||||
|
r40911 | def createcmdserver(self, repo, conn, fin, fout, prereposetups): | ||
|
r43346 | return chgcmdserver( | ||
self.ui, | ||||
repo, | ||||
fin, | ||||
fout, | ||||
conn, | ||||
prereposetups, | ||||
self._hashstate, | ||||
self._baseaddress, | ||||
) | ||||
|
r30513 | |||
def chgunixservice(ui, repo, opts): | ||||
|
r33862 | # CHGINTERNALMARK is set by chg client. It is an indication of things are | ||
# started by chg so other code can do things accordingly, like disabling | ||||
# demandimport or detecting chg client started by chg client. When executed | ||||
# here, CHGINTERNALMARK is no longer useful and hence dropped to make | ||||
# environ cleaner. | ||||
|
r43347 | if b'CHGINTERNALMARK' in encoding.environ: | ||
del encoding.environ[b'CHGINTERNALMARK'] | ||||
|
r44733 | # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if | ||
# it thinks the current value is "C". This breaks the hash computation and | ||||
# causes chg to restart loop. | ||||
if b'CHGORIG_LC_CTYPE' in encoding.environ: | ||||
encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE'] | ||||
del encoding.environ[b'CHGORIG_LC_CTYPE'] | ||||
elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ: | ||||
if b'LC_CTYPE' in encoding.environ: | ||||
del encoding.environ[b'LC_CTYPE'] | ||||
del encoding.environ[b'CHG_CLEAR_LC_CTYPE'] | ||||
|
r30513 | |||
if repo: | ||||
# one chgserver can serve multiple repos. drop repo information | ||||
|
r43347 | ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo') | ||
|
r30513 | h = chgunixservicehandler(ui) | ||
return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h) | ||||