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