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