1# chgserver.py - command server extension for cHg
2#
3# Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4#
5# This software may be used and distributed according to the terms of the
6# GNU General Public License version 2 or any later version.
7
8"""command server extension for cHg
9
10'S' channel (read/write)
11    propagate ui.system() request to client
12
13'attachio' command
14    attach client's stdio passed by sendmsg()
15
16'chdir' command
17    change current directory
18
19'setenv' command
20    replace os.environ completely
21
22'setumask' command (DEPRECATED)
23'setumask2' command
24    set umask
25
26'validate' command
27    reload the config and check if the server is up to date
28
29Config
30------
31
32::
33
34  [chgserver]
35  # how long (in seconds) should an idle chg server exit
36  idletimeout = 3600
37
38  # whether to skip config or env change checks
39  skiphash = False
40"""
41
42from __future__ import absolute_import
43
44import inspect
45import os
46import re
47import socket
48import stat
49import struct
50import time
51
52from .i18n import _
53from .pycompat import (
54    getattr,
55    setattr,
56)
57from .node import hex
58
59from . import (
60    commandserver,
61    encoding,
62    error,
63    extensions,
64    pycompat,
65    util,
66)
67
68from .utils import (
69    hashutil,
70    procutil,
71    stringutil,
72)
73
74
75def _hashlist(items):
76    """return sha1 hexdigest for a list"""
77    return hex(hashutil.sha1(stringutil.pprint(items)).digest())
78
79
80# sensitive config sections affecting confighash
81_configsections = [
82    b'alias',  # affects global state commands.table
83    b'diff-tools',  # affects whether gui or not in extdiff's uisetup
84    b'eol',  # uses setconfig('eol', ...)
85    b'extdiff',  # uisetup will register new commands
86    b'extensions',
87    b'fastannotate',  # affects annotate command and adds fastannonate cmd
88    b'merge-tools',  # affects whether gui or not in extdiff's uisetup
89    b'schemes',  # extsetup will update global hg.schemes
90]
91
92_configsectionitems = [
93    (b'commands', b'show.aliasprefix'),  # show.py reads it in extsetup
94]
95
96# sensitive environment variables affecting confighash
97_envre = re.compile(
98    br'''\A(?:
99                    CHGHG
100                    |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
101                    |HG(?:ENCODING|PLAIN).*
102                    |LANG(?:UAGE)?
103                    |LC_.*
104                    |LD_.*
105                    |PATH
106                    |PYTHON.*
107                    |TERM(?:INFO)?
108                    |TZ
109                    )\Z''',
110    re.X,
111)
112
113
114def _confighash(ui):
115    """return a quick hash for detecting config/env changes
116
117    confighash is the hash of sensitive config items and environment variables.
118
119    for chgserver, it is designed that once confighash changes, the server is
120    not qualified to serve its client and should redirect the client to a new
121    server. different from mtimehash, confighash change will not mark the
122    server outdated and exit since the user can have different configs at the
123    same time.
124    """
125    sectionitems = []
126    for section in _configsections:
127        sectionitems.append(ui.configitems(section))
128    for section, item in _configsectionitems:
129        sectionitems.append(ui.config(section, item))
130    sectionhash = _hashlist(sectionitems)
131    # If $CHGHG is set, the change to $HG should not trigger a new chg server
132    if b'CHGHG' in encoding.environ:
133        ignored = {b'HG'}
134    else:
135        ignored = set()
136    envitems = [
137        (k, v)
138        for k, v in pycompat.iteritems(encoding.environ)
139        if _envre.match(k) and k not in ignored
140    ]
141    envhash = _hashlist(sorted(envitems))
142    return sectionhash[:6] + envhash[:6]
143
144
145def _getmtimepaths(ui):
146    """get a list of paths that should be checked to detect change
147
148    The list will include:
149    - extensions (will not cover all files for complex extensions)
150    - mercurial/__version__.py
151    - python binary
152    """
153    modules = [m for n, m in extensions.extensions(ui)]
154    try:
155        from . import __version__
156
157        modules.append(__version__)
158    except ImportError:
159        pass
160    files = []
161    if pycompat.sysexecutable:
162        files.append(pycompat.sysexecutable)
163    for m in modules:
164        try:
165            files.append(pycompat.fsencode(inspect.getabsfile(m)))
166        except TypeError:
167            pass
168    return sorted(set(files))
169
170
171def _mtimehash(paths):
172    """return a quick hash for detecting file changes
173
174    mtimehash calls stat on given paths and calculate a hash based on size and
175    mtime of each file. mtimehash does not read file content because reading is
176    expensive. therefore it's not 100% reliable for detecting content changes.
177    it's possible to return different hashes for same file contents.
178    it's also possible to return a same hash for different file contents for
179    some carefully crafted situation.
180
181    for chgserver, it is designed that once mtimehash changes, the server is
182    considered outdated immediately and should no longer provide service.
183
184    mtimehash is not included in confighash because we only know the paths of
185    extensions after importing them (there is imp.find_module but that faces
186    race conditions). We need to calculate confighash without importing.
187    """
188
189    def trystat(path):
190        try:
191            st = os.stat(path)
192            return (st[stat.ST_MTIME], st.st_size)
193        except OSError:
194            # could be ENOENT, EPERM etc. not fatal in any case
195            pass
196
197    return _hashlist(pycompat.maplist(trystat, paths))[:12]
198
199
200class hashstate(object):
201    """a structure storing confighash, mtimehash, paths used for mtimehash"""
202
203    def __init__(self, confighash, mtimehash, mtimepaths):
204        self.confighash = confighash
205        self.mtimehash = mtimehash
206        self.mtimepaths = mtimepaths
207
208    @staticmethod
209    def fromui(ui, mtimepaths=None):
210        if mtimepaths is None:
211            mtimepaths = _getmtimepaths(ui)
212        confighash = _confighash(ui)
213        mtimehash = _mtimehash(mtimepaths)
214        ui.log(
215            b'cmdserver',
216            b'confighash = %s mtimehash = %s\n',
217            confighash,
218            mtimehash,
219        )
220        return hashstate(confighash, mtimehash, mtimepaths)
221
222
223def _newchgui(srcui, csystem, attachio):
224    class chgui(srcui.__class__):
225        def __init__(self, src=None):
226            super(chgui, self).__init__(src)
227            if src:
228                self._csystem = getattr(src, '_csystem', csystem)
229            else:
230                self._csystem = csystem
231
232        def _runsystem(self, cmd, environ, cwd, out):
233            # fallback to the original system method if
234            #  a. the output stream is not stdout (e.g. stderr, cStringIO),
235            #  b. or stdout is redirected by protectfinout(),
236            # because the chg client is not aware of these situations and
237            # will behave differently (i.e. write to stdout).
238            if (
239                out is not self.fout
240                or not util.safehasattr(self.fout, b'fileno')
241                or self.fout.fileno() != procutil.stdout.fileno()
242                or self._finoutredirected
243            ):
244                return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
245            self.flush()
246            return self._csystem(cmd, procutil.shellenviron(environ), cwd)
247
248        def _runpager(self, cmd, env=None):
249            self._csystem(
250                cmd,
251                procutil.shellenviron(env),
252                type=b'pager',
253                cmdtable={b'attachio': attachio},
254            )
255            return True
256
257    return chgui(srcui)
258
259
260def _loadnewui(srcui, args, cdebug):
261    from . import dispatch  # avoid cycle
262
263    newui = srcui.__class__.load()
264    for a in [b'fin', b'fout', b'ferr', b'environ']:
265        setattr(newui, a, getattr(srcui, a))
266    if util.safehasattr(srcui, b'_csystem'):
267        newui._csystem = srcui._csystem
268
269    # command line args
270    options = dispatch._earlyparseopts(newui, args)
271    dispatch._parseconfig(newui, options[b'config'])
272
273    # stolen from tortoisehg.util.copydynamicconfig()
274    for section, name, value in srcui.walkconfig():
275        source = srcui.configsource(section, name)
276        if b':' in source or source == b'--config' or source.startswith(b'$'):
277            # path:line or command line, or environ
278            continue
279        newui.setconfig(section, name, value, source)
280
281    # load wd and repo config, copied from dispatch.py
282    cwd = options[b'cwd']
283    cwd = cwd and os.path.realpath(cwd) or None
284    rpath = options[b'repository']
285    path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
286
287    extensions.populateui(newui)
288    commandserver.setuplogging(newui, fp=cdebug)
289    if newui is not newlui:
290        extensions.populateui(newlui)
291        commandserver.setuplogging(newlui, fp=cdebug)
292
293    return (newui, newlui)
294
295
296class channeledsystem(object):
297    """Propagate ui.system() request in the following format:
298
299    payload length (unsigned int),
300    type, '\0',
301    cmd, '\0',
302    cwd, '\0',
303    envkey, '=', val, '\0',
304    ...
305    envkey, '=', val
306
307    if type == 'system', waits for:
308
309    exitcode length (unsigned int),
310    exitcode (int)
311
312    if type == 'pager', repetitively waits for a command name ending with '\n'
313    and executes it defined by cmdtable, or exits the loop if the command name
314    is empty.
315    """
316
317    def __init__(self, in_, out, channel):
318        self.in_ = in_
319        self.out = out
320        self.channel = channel
321
322    def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
323        args = [type, cmd, util.abspath(cwd or b'.')]
324        args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
325        data = b'\0'.join(args)
326        self.out.write(struct.pack(b'>cI', self.channel, len(data)))
327        self.out.write(data)
328        self.out.flush()
329
330        if type == b'system':
331            length = self.in_.read(4)
332            (length,) = struct.unpack(b'>I', length)
333            if length != 4:
334                raise error.Abort(_(b'invalid response'))
335            (rc,) = struct.unpack(b'>i', self.in_.read(4))
336            return rc
337        elif type == b'pager':
338            while True:
339                cmd = self.in_.readline()[:-1]
340                if not cmd:
341                    break
342                if cmdtable and cmd in cmdtable:
343                    cmdtable[cmd]()
344                else:
345                    raise error.Abort(_(b'unexpected command: %s') % cmd)
346        else:
347            raise error.ProgrammingError(b'invalid S channel type: %s' % type)
348
349
350_iochannels = [
351    # server.ch, ui.fp, mode
352    (b'cin', b'fin', 'rb'),
353    (b'cout', b'fout', 'wb'),
354    (b'cerr', b'ferr', 'wb'),
355]
356
357
358class chgcmdserver(commandserver.server):
359    def __init__(
360        self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
361    ):
362        super(chgcmdserver, self).__init__(
363            _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
364            repo,
365            fin,
366            fout,
367            prereposetups,
368        )
369        self.clientsock = sock
370        self._ioattached = False
371        self._oldios = []  # original (self.ch, ui.fp, fd) before "attachio"
372        self.hashstate = hashstate
373        self.baseaddress = baseaddress
374        if hashstate is not None:
375            self.capabilities = self.capabilities.copy()
376            self.capabilities[b'validate'] = chgcmdserver.validate
377
378    def cleanup(self):
379        super(chgcmdserver, self).cleanup()
380        # dispatch._runcatch() does not flush outputs if exception is not
381        # handled by dispatch._dispatch()
382        self.ui.flush()
383        self._restoreio()
384        self._ioattached = False
385
386    def attachio(self):
387        """Attach to client's stdio passed via unix domain socket; all
388        channels except cresult will no longer be used
389        """
390        # tell client to sendmsg() with 1-byte payload, which makes it
391        # distinctive from "attachio\n" command consumed by client.read()
392        self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
393        clientfds = util.recvfds(self.clientsock.fileno())
394        self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
395
396        ui = self.ui
397        ui.flush()
398        self._saveio()
399        for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
400            assert fd > 0
401            fp = getattr(ui, fn)
402            os.dup2(fd, fp.fileno())
403            os.close(fd)
404            if self._ioattached:
405                continue
406            # reset buffering mode when client is first attached. as we want
407            # to see output immediately on pager, the mode stays unchanged
408            # when client re-attached. ferr is unchanged because it should
409            # be unbuffered no matter if it is a tty or not.
410            if fn == b'ferr':
411                newfp = fp
412            elif pycompat.ispy3:
413                # On Python 3, the standard library doesn't offer line-buffered
414                # binary streams, so wrap/unwrap it.
415                if fp.isatty():
416                    newfp = procutil.make_line_buffered(fp)
417                else:
418                    newfp = procutil.unwrap_line_buffered(fp)
419            else:
420                # Python 2 uses the I/O streams provided by the C library, so
421                # make it line-buffered explicitly. Otherwise the default would
422                # be decided on first write(), where fout could be a pager.
423                if fp.isatty():
424                    bufsize = 1  # line buffered
425                else:
426                    bufsize = -1  # system default
427                newfp = os.fdopen(fp.fileno(), mode, bufsize)
428            if newfp is not fp:
429                setattr(ui, fn, newfp)
430            setattr(self, cn, newfp)
431
432        self._ioattached = True
433        self.cresult.write(struct.pack(b'>i', len(clientfds)))
434
435    def _saveio(self):
436        if self._oldios:
437            return
438        ui = self.ui
439        for cn, fn, _mode in _iochannels:
440            ch = getattr(self, cn)
441            fp = getattr(ui, fn)
442            fd = os.dup(fp.fileno())
443            self._oldios.append((ch, fp, fd))
444
445    def _restoreio(self):
446        if not self._oldios:
447            return
448        nullfd = os.open(os.devnull, os.O_WRONLY)
449        ui = self.ui
450        for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
451            newfp = getattr(ui, fn)
452            # On Python 2, newfp and fp may be separate file objects associated
453            # with the same fd, so we must close newfp while it's associated
454            # with the client. Otherwise the new associated fd would be closed
455            # when newfp gets deleted. On Python 3, newfp is just a wrapper
456            # around fp even if newfp is not fp, so deleting newfp is safe.
457            if not (pycompat.ispy3 or newfp is fp):
458                newfp.close()
459            # restore original fd: fp is open again
460            try:
461                if (pycompat.ispy3 or newfp is fp) and 'w' in mode:
462                    # Discard buffered data which couldn't be flushed because
463                    # of EPIPE. The data should belong to the current session
464                    # and should never persist.
465                    os.dup2(nullfd, fp.fileno())
466                    fp.flush()
467                os.dup2(fd, fp.fileno())
468            except OSError as err:
469                # According to issue6330, running chg on heavy loaded systems
470                # can lead to EBUSY. [man dup2] indicates that, on Linux,
471                # EBUSY comes from a race condition between open() and dup2().
472                # However it's not clear why open() race occurred for
473                # newfd=stdin/out/err.
474                self.ui.log(
475                    b'chgserver',
476                    b'got %s while duplicating %s\n',
477                    stringutil.forcebytestr(err),
478                    fn,
479                )
480            os.close(fd)
481            setattr(self, cn, ch)
482            setattr(ui, fn, fp)
483        os.close(nullfd)
484        del self._oldios[:]
485
486    def validate(self):
487        """Reload the config and check if the server is up to date
488
489        Read a list of '\0' separated arguments.
490        Write a non-empty list of '\0' separated instruction strings or '\0'
491        if the list is empty.
492        An instruction string could be either:
493            - "unlink $path", the client should unlink the path to stop the
494              outdated server.
495            - "redirect $path", the client should attempt to connect to $path
496              first. If it does not work, start a new server. It implies
497              "reconnect".
498            - "exit $n", the client should exit directly with code n.
499              This may happen if we cannot parse the config.
500            - "reconnect", the client should close the connection and
501              reconnect.
502        If neither "reconnect" nor "redirect" is included in the instruction
503        list, the client can continue with this server after completing all
504        the instructions.
505        """
506        args = self._readlist()
507        errorraised = False
508        detailed_exit_code = 255
509        try:
510            self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
511        except error.RepoError as inst:
512            # RepoError can be raised while trying to read shared source
513            # configuration
514            self.ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
515            if inst.hint:
516                self.ui.error(_(b"(%s)\n") % inst.hint)
517            errorraised = True
518        except error.Error as inst:
519            if inst.detailed_exit_code is not None:
520                detailed_exit_code = inst.detailed_exit_code
521            self.ui.error(inst.format())
522            errorraised = True
523
524        if errorraised:
525            self.ui.flush()
526            exit_code = 255
527            if self.ui.configbool(b'ui', b'detailed-exit-code'):
528                exit_code = detailed_exit_code
529            self.cresult.write(b'exit %d' % exit_code)
530            return
531        newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
532        insts = []
533        if newhash.mtimehash != self.hashstate.mtimehash:
534            addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
535            insts.append(b'unlink %s' % addr)
536            # mtimehash is empty if one or more extensions fail to load.
537            # to be compatible with hg, still serve the client this time.
538            if self.hashstate.mtimehash:
539                insts.append(b'reconnect')
540        if newhash.confighash != self.hashstate.confighash:
541            addr = _hashaddress(self.baseaddress, newhash.confighash)
542            insts.append(b'redirect %s' % addr)
543        self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
544        self.cresult.write(b'\0'.join(insts) or b'\0')
545
546    def chdir(self):
547        """Change current directory
548
549        Note that the behavior of --cwd option is bit different from this.
550        It does not affect --config parameter.
551        """
552        path = self._readstr()
553        if not path:
554            return
555        self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
556        os.chdir(path)
557
558    def setumask(self):
559        """Change umask (DEPRECATED)"""
560        # BUG: this does not follow the message frame structure, but kept for
561        # backward compatibility with old chg clients for some time
562        self._setumask(self._read(4))
563
564    def setumask2(self):
565        """Change umask"""
566        data = self._readstr()
567        if len(data) != 4:
568            raise ValueError(b'invalid mask length in setumask2 request')
569        self._setumask(data)
570
571    def _setumask(self, data):
572        mask = struct.unpack(b'>I', data)[0]
573        self.ui.log(b'chgserver', b'setumask %r\n', mask)
574        util.setumask(mask)
575
576    def runcommand(self):
577        # pager may be attached within the runcommand session, which should
578        # be detached at the end of the session. otherwise the pager wouldn't
579        # receive EOF.
580        globaloldios = self._oldios
581        self._oldios = []
582        try:
583            return super(chgcmdserver, self).runcommand()
584        finally:
585            self._restoreio()
586            self._oldios = globaloldios
587
588    def setenv(self):
589        """Clear and update os.environ
590
591        Note that not all variables can make an effect on the running process.
592        """
593        l = self._readlist()
594        try:
595            newenv = dict(s.split(b'=', 1) for s in l)
596        except ValueError:
597            raise ValueError(b'unexpected value in setenv request')
598        self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
599
600        encoding.environ.clear()
601        encoding.environ.update(newenv)
602
603    capabilities = commandserver.server.capabilities.copy()
604    capabilities.update(
605        {
606            b'attachio': attachio,
607            b'chdir': chdir,
608            b'runcommand': runcommand,
609            b'setenv': setenv,
610            b'setumask': setumask,
611            b'setumask2': setumask2,
612        }
613    )
614
615    if util.safehasattr(procutil, b'setprocname'):
616
617        def setprocname(self):
618            """Change process title"""
619            name = self._readstr()
620            self.ui.log(b'chgserver', b'setprocname: %r\n', name)
621            procutil.setprocname(name)
622
623        capabilities[b'setprocname'] = setprocname
624
625
626def _tempaddress(address):
627    return b'%s.%d.tmp' % (address, os.getpid())
628
629
630def _hashaddress(address, hashstr):
631    # if the basename of address contains '.', use only the left part. this
632    # makes it possible for the client to pass 'server.tmp$PID' and follow by
633    # an atomic rename to avoid locking when spawning new servers.
634    dirname, basename = os.path.split(address)
635    basename = basename.split(b'.', 1)[0]
636    return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
637
638
639class chgunixservicehandler(object):
640    """Set of operations for chg services"""
641
642    pollinterval = 1  # [sec]
643
644    def __init__(self, ui):
645        self.ui = ui
646        self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
647        self._lastactive = time.time()
648
649    def bindsocket(self, sock, address):
650        self._inithashstate(address)
651        self._checkextensions()
652        self._bind(sock)
653        self._createsymlink()
654        # no "listening at" message should be printed to simulate hg behavior
655
656    def _inithashstate(self, address):
657        self._baseaddress = address
658        if self.ui.configbool(b'chgserver', b'skiphash'):
659            self._hashstate = None
660            self._realaddress = address
661            return
662        self._hashstate = hashstate.fromui(self.ui)
663        self._realaddress = _hashaddress(address, self._hashstate.confighash)
664
665    def _checkextensions(self):
666        if not self._hashstate:
667            return
668        if extensions.notloaded():
669            # one or more extensions failed to load. mtimehash becomes
670            # meaningless because we do not know the paths of those extensions.
671            # set mtimehash to an illegal hash value to invalidate the server.
672            self._hashstate.mtimehash = b''
673
674    def _bind(self, sock):
675        # use a unique temp address so we can stat the file and do ownership
676        # check later
677        tempaddress = _tempaddress(self._realaddress)
678        util.bindunixsocket(sock, tempaddress)
679        self._socketstat = os.stat(tempaddress)
680        sock.listen(socket.SOMAXCONN)
681        # rename will replace the old socket file if exists atomically. the
682        # old server will detect ownership change and exit.
683        util.rename(tempaddress, self._realaddress)
684
685    def _createsymlink(self):
686        if self._baseaddress == self._realaddress:
687            return
688        tempaddress = _tempaddress(self._baseaddress)
689        os.symlink(os.path.basename(self._realaddress), tempaddress)
690        util.rename(tempaddress, self._baseaddress)
691
692    def _issocketowner(self):
693        try:
694            st = os.stat(self._realaddress)
695            return (
696                st.st_ino == self._socketstat.st_ino
697                and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
698            )
699        except OSError:
700            return False
701
702    def unlinksocket(self, address):
703        if not self._issocketowner():
704            return
705        # it is possible to have a race condition here that we may
706        # remove another server's socket file. but that's okay
707        # since that server will detect and exit automatically and
708        # the client will start a new server on demand.
709        util.tryunlink(self._realaddress)
710
711    def shouldexit(self):
712        if not self._issocketowner():
713            self.ui.log(
714                b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
715            )
716            return True
717        if time.time() - self._lastactive > self._idletimeout:
718            self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
719            return True
720        return False
721
722    def newconnection(self):
723        self._lastactive = time.time()
724
725    def createcmdserver(self, repo, conn, fin, fout, prereposetups):
726        return chgcmdserver(
727            self.ui,
728            repo,
729            fin,
730            fout,
731            conn,
732            prereposetups,
733            self._hashstate,
734            self._baseaddress,
735        )
736
737
738def chgunixservice(ui, repo, opts):
739    # CHGINTERNALMARK is set by chg client. It is an indication of things are
740    # started by chg so other code can do things accordingly, like disabling
741    # demandimport or detecting chg client started by chg client. When executed
742    # here, CHGINTERNALMARK is no longer useful and hence dropped to make
743    # environ cleaner.
744    if b'CHGINTERNALMARK' in encoding.environ:
745        del encoding.environ[b'CHGINTERNALMARK']
746    # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
747    # it thinks the current value is "C". This breaks the hash computation and
748    # causes chg to restart loop.
749    if b'CHGORIG_LC_CTYPE' in encoding.environ:
750        encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
751        del encoding.environ[b'CHGORIG_LC_CTYPE']
752    elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
753        if b'LC_CTYPE' in encoding.environ:
754            del encoding.environ[b'LC_CTYPE']
755        del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
756
757    if repo:
758        # one chgserver can serve multiple repos. drop repo information
759        ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
760    h = chgunixservicehandler(ui)
761    return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
762