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