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