##// END OF EJS Templates
chgserver: use util.shellenviron...
Jun Wu -
r30737:29574094 default
parent child Browse files
Show More
@@ -1,648 +1,637 b''
1 1 # chgserver.py - command server extension for cHg
2 2 #
3 3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """command server extension for cHg
9 9
10 10 'S' channel (read/write)
11 11 propagate ui.system() request to client
12 12
13 13 'attachio' command
14 14 attach client's stdio passed by sendmsg()
15 15
16 16 'chdir' command
17 17 change current directory
18 18
19 19 'getpager' command
20 20 checks if pager is enabled and which pager should be executed
21 21
22 22 'setenv' command
23 23 replace os.environ completely
24 24
25 25 'setumask' command
26 26 set umask
27 27
28 28 'validate' command
29 29 reload the config and check if the server is up to date
30 30
31 31 Config
32 32 ------
33 33
34 34 ::
35 35
36 36 [chgserver]
37 37 idletimeout = 3600 # seconds, after which an idle server will exit
38 38 skiphash = False # whether to skip config or env change checks
39 39 """
40 40
41 41 from __future__ import absolute_import
42 42
43 43 import errno
44 44 import hashlib
45 45 import inspect
46 46 import os
47 47 import re
48 48 import signal
49 49 import struct
50 50 import time
51 51
52 52 from .i18n import _
53 53
54 54 from . import (
55 55 cmdutil,
56 56 commandserver,
57 57 encoding,
58 58 error,
59 59 extensions,
60 60 osutil,
61 61 pycompat,
62 62 util,
63 63 )
64 64
65 65 _log = commandserver.log
66 66
67 67 def _hashlist(items):
68 68 """return sha1 hexdigest for a list"""
69 69 return hashlib.sha1(str(items)).hexdigest()
70 70
71 71 # sensitive config sections affecting confighash
72 72 _configsections = [
73 73 'alias', # affects global state commands.table
74 74 'extdiff', # uisetup will register new commands
75 75 'extensions',
76 76 ]
77 77
78 78 # sensitive environment variables affecting confighash
79 79 _envre = re.compile(r'''\A(?:
80 80 CHGHG
81 81 |HG(?:[A-Z].*)?
82 82 |LANG(?:UAGE)?
83 83 |LC_.*
84 84 |LD_.*
85 85 |PATH
86 86 |PYTHON.*
87 87 |TERM(?:INFO)?
88 88 |TZ
89 89 )\Z''', re.X)
90 90
91 91 def _confighash(ui):
92 92 """return a quick hash for detecting config/env changes
93 93
94 94 confighash is the hash of sensitive config items and environment variables.
95 95
96 96 for chgserver, it is designed that once confighash changes, the server is
97 97 not qualified to serve its client and should redirect the client to a new
98 98 server. different from mtimehash, confighash change will not mark the
99 99 server outdated and exit since the user can have different configs at the
100 100 same time.
101 101 """
102 102 sectionitems = []
103 103 for section in _configsections:
104 104 sectionitems.append(ui.configitems(section))
105 105 sectionhash = _hashlist(sectionitems)
106 106 envitems = [(k, v) for k, v in encoding.environ.iteritems()
107 107 if _envre.match(k)]
108 108 envhash = _hashlist(sorted(envitems))
109 109 return sectionhash[:6] + envhash[:6]
110 110
111 111 def _getmtimepaths(ui):
112 112 """get a list of paths that should be checked to detect change
113 113
114 114 The list will include:
115 115 - extensions (will not cover all files for complex extensions)
116 116 - mercurial/__version__.py
117 117 - python binary
118 118 """
119 119 modules = [m for n, m in extensions.extensions(ui)]
120 120 try:
121 121 from . import __version__
122 122 modules.append(__version__)
123 123 except ImportError:
124 124 pass
125 125 files = [pycompat.sysexecutable]
126 126 for m in modules:
127 127 try:
128 128 files.append(inspect.getabsfile(m))
129 129 except TypeError:
130 130 pass
131 131 return sorted(set(files))
132 132
133 133 def _mtimehash(paths):
134 134 """return a quick hash for detecting file changes
135 135
136 136 mtimehash calls stat on given paths and calculate a hash based on size and
137 137 mtime of each file. mtimehash does not read file content because reading is
138 138 expensive. therefore it's not 100% reliable for detecting content changes.
139 139 it's possible to return different hashes for same file contents.
140 140 it's also possible to return a same hash for different file contents for
141 141 some carefully crafted situation.
142 142
143 143 for chgserver, it is designed that once mtimehash changes, the server is
144 144 considered outdated immediately and should no longer provide service.
145 145
146 146 mtimehash is not included in confighash because we only know the paths of
147 147 extensions after importing them (there is imp.find_module but that faces
148 148 race conditions). We need to calculate confighash without importing.
149 149 """
150 150 def trystat(path):
151 151 try:
152 152 st = os.stat(path)
153 153 return (st.st_mtime, st.st_size)
154 154 except OSError:
155 155 # could be ENOENT, EPERM etc. not fatal in any case
156 156 pass
157 157 return _hashlist(map(trystat, paths))[:12]
158 158
159 159 class hashstate(object):
160 160 """a structure storing confighash, mtimehash, paths used for mtimehash"""
161 161 def __init__(self, confighash, mtimehash, mtimepaths):
162 162 self.confighash = confighash
163 163 self.mtimehash = mtimehash
164 164 self.mtimepaths = mtimepaths
165 165
166 166 @staticmethod
167 167 def fromui(ui, mtimepaths=None):
168 168 if mtimepaths is None:
169 169 mtimepaths = _getmtimepaths(ui)
170 170 confighash = _confighash(ui)
171 171 mtimehash = _mtimehash(mtimepaths)
172 172 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
173 173 return hashstate(confighash, mtimehash, mtimepaths)
174 174
175 175 # copied from hgext/pager.py:uisetup()
176 176 def _setuppagercmd(ui, options, cmd):
177 177 from . import commands # avoid cycle
178 178
179 179 if not ui.formatted():
180 180 return
181 181
182 182 p = ui.config("pager", "pager", encoding.environ.get("PAGER"))
183 183 usepager = False
184 184 always = util.parsebool(options['pager'])
185 185 auto = options['pager'] == 'auto'
186 186
187 187 if not p:
188 188 pass
189 189 elif always:
190 190 usepager = True
191 191 elif not auto:
192 192 usepager = False
193 193 else:
194 194 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
195 195 attend = ui.configlist('pager', 'attend', attended)
196 196 ignore = ui.configlist('pager', 'ignore')
197 197 cmds, _ = cmdutil.findcmd(cmd, commands.table)
198 198
199 199 for cmd in cmds:
200 200 var = 'attend-%s' % cmd
201 201 if ui.config('pager', var):
202 202 usepager = ui.configbool('pager', var)
203 203 break
204 204 if (cmd in attend or
205 205 (cmd not in ignore and not attend)):
206 206 usepager = True
207 207 break
208 208
209 209 if usepager:
210 210 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
211 211 ui.setconfig('ui', 'interactive', False, 'pager')
212 212 return p
213 213
214 214 def _newchgui(srcui, csystem):
215 215 class chgui(srcui.__class__):
216 216 def __init__(self, src=None):
217 217 super(chgui, self).__init__(src)
218 218 if src:
219 219 self._csystem = getattr(src, '_csystem', csystem)
220 220 else:
221 221 self._csystem = csystem
222 222
223 223 def system(self, cmd, environ=None, cwd=None, onerr=None,
224 224 errprefix=None):
225 225 # fallback to the original system method if the output needs to be
226 226 # captured (to self._buffers), or the output stream is not stdout
227 227 # (e.g. stderr, cStringIO), because the chg client is not aware of
228 228 # these situations and will behave differently (write to stdout).
229 229 if (any(s[1] for s in self._bufferstates)
230 230 or not util.safehasattr(self.fout, 'fileno')
231 231 or self.fout.fileno() != util.stdout.fileno()):
232 232 return super(chgui, self).system(cmd, environ, cwd, onerr,
233 233 errprefix)
234 # copied from mercurial/util.py:system()
235 234 self.flush()
236 def py2shell(val):
237 if val is None or val is False:
238 return '0'
239 if val is True:
240 return '1'
241 return str(val)
242 env = encoding.environ.copy()
243 if environ:
244 env.update((k, py2shell(v)) for k, v in environ.iteritems())
245 env['HG'] = util.hgexecutable()
246 rc = self._csystem(cmd, env, cwd)
235 rc = self._csystem(cmd, util.shellenviron(environ), cwd)
247 236 if rc and onerr:
248 237 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
249 238 util.explainexit(rc)[0])
250 239 if errprefix:
251 240 errmsg = '%s: %s' % (errprefix, errmsg)
252 241 raise onerr(errmsg)
253 242 return rc
254 243
255 244 return chgui(srcui)
256 245
257 246 def _loadnewui(srcui, args):
258 247 from . import dispatch # avoid cycle
259 248
260 249 newui = srcui.__class__.load()
261 250 for a in ['fin', 'fout', 'ferr', 'environ']:
262 251 setattr(newui, a, getattr(srcui, a))
263 252 if util.safehasattr(srcui, '_csystem'):
264 253 newui._csystem = srcui._csystem
265 254
266 255 # command line args
267 256 args = args[:]
268 257 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
269 258
270 259 # stolen from tortoisehg.util.copydynamicconfig()
271 260 for section, name, value in srcui.walkconfig():
272 261 source = srcui.configsource(section, name)
273 262 if ':' in source or source == '--config':
274 263 # path:line or command line
275 264 continue
276 265 newui.setconfig(section, name, value, source)
277 266
278 267 # load wd and repo config, copied from dispatch.py
279 268 cwds = dispatch._earlygetopt(['--cwd'], args)
280 269 cwd = cwds and os.path.realpath(cwds[-1]) or None
281 270 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
282 271 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
283 272
284 273 return (newui, newlui)
285 274
286 275 class channeledsystem(object):
287 276 """Propagate ui.system() request in the following format:
288 277
289 278 payload length (unsigned int),
290 279 type, '\0',
291 280 cmd, '\0',
292 281 cwd, '\0',
293 282 envkey, '=', val, '\0',
294 283 ...
295 284 envkey, '=', val
296 285
297 286 if type == 'system', waits for:
298 287
299 288 exitcode length (unsigned int),
300 289 exitcode (int)
301 290 """
302 291 def __init__(self, in_, out, channel):
303 292 self.in_ = in_
304 293 self.out = out
305 294 self.channel = channel
306 295
307 296 def __call__(self, cmd, environ, cwd, type='system'):
308 297 args = [type, util.quotecommand(cmd), os.path.abspath(cwd or '.')]
309 298 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
310 299 data = '\0'.join(args)
311 300 self.out.write(struct.pack('>cI', self.channel, len(data)))
312 301 self.out.write(data)
313 302 self.out.flush()
314 303
315 304 if type == 'system':
316 305 length = self.in_.read(4)
317 306 length, = struct.unpack('>I', length)
318 307 if length != 4:
319 308 raise error.Abort(_('invalid response'))
320 309 rc, = struct.unpack('>i', self.in_.read(4))
321 310 return rc
322 311 else:
323 312 raise error.ProgrammingError('invalid S channel type: %s' % type)
324 313
325 314 _iochannels = [
326 315 # server.ch, ui.fp, mode
327 316 ('cin', 'fin', 'rb'),
328 317 ('cout', 'fout', 'wb'),
329 318 ('cerr', 'ferr', 'wb'),
330 319 ]
331 320
332 321 class chgcmdserver(commandserver.server):
333 322 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
334 323 super(chgcmdserver, self).__init__(
335 324 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
336 325 self.clientsock = sock
337 326 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
338 327 self.hashstate = hashstate
339 328 self.baseaddress = baseaddress
340 329 if hashstate is not None:
341 330 self.capabilities = self.capabilities.copy()
342 331 self.capabilities['validate'] = chgcmdserver.validate
343 332
344 333 def cleanup(self):
345 334 super(chgcmdserver, self).cleanup()
346 335 # dispatch._runcatch() does not flush outputs if exception is not
347 336 # handled by dispatch._dispatch()
348 337 self.ui.flush()
349 338 self._restoreio()
350 339
351 340 def attachio(self):
352 341 """Attach to client's stdio passed via unix domain socket; all
353 342 channels except cresult will no longer be used
354 343 """
355 344 # tell client to sendmsg() with 1-byte payload, which makes it
356 345 # distinctive from "attachio\n" command consumed by client.read()
357 346 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
358 347 clientfds = osutil.recvfds(self.clientsock.fileno())
359 348 _log('received fds: %r\n' % clientfds)
360 349
361 350 ui = self.ui
362 351 ui.flush()
363 352 first = self._saveio()
364 353 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
365 354 assert fd > 0
366 355 fp = getattr(ui, fn)
367 356 os.dup2(fd, fp.fileno())
368 357 os.close(fd)
369 358 if not first:
370 359 continue
371 360 # reset buffering mode when client is first attached. as we want
372 361 # to see output immediately on pager, the mode stays unchanged
373 362 # when client re-attached. ferr is unchanged because it should
374 363 # be unbuffered no matter if it is a tty or not.
375 364 if fn == 'ferr':
376 365 newfp = fp
377 366 else:
378 367 # make it line buffered explicitly because the default is
379 368 # decided on first write(), where fout could be a pager.
380 369 if fp.isatty():
381 370 bufsize = 1 # line buffered
382 371 else:
383 372 bufsize = -1 # system default
384 373 newfp = os.fdopen(fp.fileno(), mode, bufsize)
385 374 setattr(ui, fn, newfp)
386 375 setattr(self, cn, newfp)
387 376
388 377 self.cresult.write(struct.pack('>i', len(clientfds)))
389 378
390 379 def _saveio(self):
391 380 if self._oldios:
392 381 return False
393 382 ui = self.ui
394 383 for cn, fn, _mode in _iochannels:
395 384 ch = getattr(self, cn)
396 385 fp = getattr(ui, fn)
397 386 fd = os.dup(fp.fileno())
398 387 self._oldios.append((ch, fp, fd))
399 388 return True
400 389
401 390 def _restoreio(self):
402 391 ui = self.ui
403 392 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
404 393 newfp = getattr(ui, fn)
405 394 # close newfp while it's associated with client; otherwise it
406 395 # would be closed when newfp is deleted
407 396 if newfp is not fp:
408 397 newfp.close()
409 398 # restore original fd: fp is open again
410 399 os.dup2(fd, fp.fileno())
411 400 os.close(fd)
412 401 setattr(self, cn, ch)
413 402 setattr(ui, fn, fp)
414 403 del self._oldios[:]
415 404
416 405 def validate(self):
417 406 """Reload the config and check if the server is up to date
418 407
419 408 Read a list of '\0' separated arguments.
420 409 Write a non-empty list of '\0' separated instruction strings or '\0'
421 410 if the list is empty.
422 411 An instruction string could be either:
423 412 - "unlink $path", the client should unlink the path to stop the
424 413 outdated server.
425 414 - "redirect $path", the client should attempt to connect to $path
426 415 first. If it does not work, start a new server. It implies
427 416 "reconnect".
428 417 - "exit $n", the client should exit directly with code n.
429 418 This may happen if we cannot parse the config.
430 419 - "reconnect", the client should close the connection and
431 420 reconnect.
432 421 If neither "reconnect" nor "redirect" is included in the instruction
433 422 list, the client can continue with this server after completing all
434 423 the instructions.
435 424 """
436 425 from . import dispatch # avoid cycle
437 426
438 427 args = self._readlist()
439 428 try:
440 429 self.ui, lui = _loadnewui(self.ui, args)
441 430 except error.ParseError as inst:
442 431 dispatch._formatparse(self.ui.warn, inst)
443 432 self.ui.flush()
444 433 self.cresult.write('exit 255')
445 434 return
446 435 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
447 436 insts = []
448 437 if newhash.mtimehash != self.hashstate.mtimehash:
449 438 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
450 439 insts.append('unlink %s' % addr)
451 440 # mtimehash is empty if one or more extensions fail to load.
452 441 # to be compatible with hg, still serve the client this time.
453 442 if self.hashstate.mtimehash:
454 443 insts.append('reconnect')
455 444 if newhash.confighash != self.hashstate.confighash:
456 445 addr = _hashaddress(self.baseaddress, newhash.confighash)
457 446 insts.append('redirect %s' % addr)
458 447 _log('validate: %s\n' % insts)
459 448 self.cresult.write('\0'.join(insts) or '\0')
460 449
461 450 def chdir(self):
462 451 """Change current directory
463 452
464 453 Note that the behavior of --cwd option is bit different from this.
465 454 It does not affect --config parameter.
466 455 """
467 456 path = self._readstr()
468 457 if not path:
469 458 return
470 459 _log('chdir to %r\n' % path)
471 460 os.chdir(path)
472 461
473 462 def setumask(self):
474 463 """Change umask"""
475 464 mask = struct.unpack('>I', self._read(4))[0]
476 465 _log('setumask %r\n' % mask)
477 466 os.umask(mask)
478 467
479 468 def getpager(self):
480 469 """Read cmdargs and write pager command to r-channel if enabled
481 470
482 471 If pager isn't enabled, this writes '\0' because channeledoutput
483 472 does not allow to write empty data.
484 473 """
485 474 from . import dispatch # avoid cycle
486 475
487 476 args = self._readlist()
488 477 try:
489 478 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
490 479 args)
491 480 except (error.Abort, error.AmbiguousCommand, error.CommandError,
492 481 error.UnknownCommand):
493 482 cmd = None
494 483 options = {}
495 484 if not cmd or 'pager' not in options:
496 485 self.cresult.write('\0')
497 486 return
498 487
499 488 pagercmd = _setuppagercmd(self.ui, options, cmd)
500 489 if pagercmd:
501 490 # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
502 491 # we can exit if the pipe to the pager is closed
503 492 if util.safehasattr(signal, 'SIGPIPE') and \
504 493 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
505 494 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
506 495 self.cresult.write(pagercmd)
507 496 else:
508 497 self.cresult.write('\0')
509 498
510 499 def runcommand(self):
511 500 return super(chgcmdserver, self).runcommand()
512 501
513 502 def setenv(self):
514 503 """Clear and update os.environ
515 504
516 505 Note that not all variables can make an effect on the running process.
517 506 """
518 507 l = self._readlist()
519 508 try:
520 509 newenv = dict(s.split('=', 1) for s in l)
521 510 except ValueError:
522 511 raise ValueError('unexpected value in setenv request')
523 512 _log('setenv: %r\n' % sorted(newenv.keys()))
524 513 encoding.environ.clear()
525 514 encoding.environ.update(newenv)
526 515
527 516 capabilities = commandserver.server.capabilities.copy()
528 517 capabilities.update({'attachio': attachio,
529 518 'chdir': chdir,
530 519 'getpager': getpager,
531 520 'runcommand': runcommand,
532 521 'setenv': setenv,
533 522 'setumask': setumask})
534 523
535 524 def _tempaddress(address):
536 525 return '%s.%d.tmp' % (address, os.getpid())
537 526
538 527 def _hashaddress(address, hashstr):
539 528 # if the basename of address contains '.', use only the left part. this
540 529 # makes it possible for the client to pass 'server.tmp$PID' and follow by
541 530 # an atomic rename to avoid locking when spawning new servers.
542 531 dirname, basename = os.path.split(address)
543 532 basename = basename.split('.', 1)[0]
544 533 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
545 534
546 535 class chgunixservicehandler(object):
547 536 """Set of operations for chg services"""
548 537
549 538 pollinterval = 1 # [sec]
550 539
551 540 def __init__(self, ui):
552 541 self.ui = ui
553 542 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
554 543 self._lastactive = time.time()
555 544
556 545 def bindsocket(self, sock, address):
557 546 self._inithashstate(address)
558 547 self._checkextensions()
559 548 self._bind(sock)
560 549 self._createsymlink()
561 550
562 551 def _inithashstate(self, address):
563 552 self._baseaddress = address
564 553 if self.ui.configbool('chgserver', 'skiphash', False):
565 554 self._hashstate = None
566 555 self._realaddress = address
567 556 return
568 557 self._hashstate = hashstate.fromui(self.ui)
569 558 self._realaddress = _hashaddress(address, self._hashstate.confighash)
570 559
571 560 def _checkextensions(self):
572 561 if not self._hashstate:
573 562 return
574 563 if extensions.notloaded():
575 564 # one or more extensions failed to load. mtimehash becomes
576 565 # meaningless because we do not know the paths of those extensions.
577 566 # set mtimehash to an illegal hash value to invalidate the server.
578 567 self._hashstate.mtimehash = ''
579 568
580 569 def _bind(self, sock):
581 570 # use a unique temp address so we can stat the file and do ownership
582 571 # check later
583 572 tempaddress = _tempaddress(self._realaddress)
584 573 util.bindunixsocket(sock, tempaddress)
585 574 self._socketstat = os.stat(tempaddress)
586 575 # rename will replace the old socket file if exists atomically. the
587 576 # old server will detect ownership change and exit.
588 577 util.rename(tempaddress, self._realaddress)
589 578
590 579 def _createsymlink(self):
591 580 if self._baseaddress == self._realaddress:
592 581 return
593 582 tempaddress = _tempaddress(self._baseaddress)
594 583 os.symlink(os.path.basename(self._realaddress), tempaddress)
595 584 util.rename(tempaddress, self._baseaddress)
596 585
597 586 def _issocketowner(self):
598 587 try:
599 588 stat = os.stat(self._realaddress)
600 589 return (stat.st_ino == self._socketstat.st_ino and
601 590 stat.st_mtime == self._socketstat.st_mtime)
602 591 except OSError:
603 592 return False
604 593
605 594 def unlinksocket(self, address):
606 595 if not self._issocketowner():
607 596 return
608 597 # it is possible to have a race condition here that we may
609 598 # remove another server's socket file. but that's okay
610 599 # since that server will detect and exit automatically and
611 600 # the client will start a new server on demand.
612 601 try:
613 602 os.unlink(self._realaddress)
614 603 except OSError as exc:
615 604 if exc.errno != errno.ENOENT:
616 605 raise
617 606
618 607 def printbanner(self, address):
619 608 # no "listening at" message should be printed to simulate hg behavior
620 609 pass
621 610
622 611 def shouldexit(self):
623 612 if not self._issocketowner():
624 613 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
625 614 return True
626 615 if time.time() - self._lastactive > self._idletimeout:
627 616 self.ui.debug('being idle too long. exiting.\n')
628 617 return True
629 618 return False
630 619
631 620 def newconnection(self):
632 621 self._lastactive = time.time()
633 622
634 623 def createcmdserver(self, repo, conn, fin, fout):
635 624 return chgcmdserver(self.ui, repo, fin, fout, conn,
636 625 self._hashstate, self._baseaddress)
637 626
638 627 def chgunixservice(ui, repo, opts):
639 628 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
640 629 # start another chg. drop it to avoid possible side effects.
641 630 if 'CHGINTERNALMARK' in encoding.environ:
642 631 del encoding.environ['CHGINTERNALMARK']
643 632
644 633 if repo:
645 634 # one chgserver can serve multiple repos. drop repo information
646 635 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
647 636 h = chgunixservicehandler(ui)
648 637 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now