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