##// END OF EJS Templates
chgserver: refactor initialization of real/base addresses...
Yuya Nishihara -
r29597:581c0c7c default
parent child Browse files
Show More
@@ -1,643 +1,643 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 util,
64 64 )
65 65
66 66 # Note for extension authors: ONLY specify testedwith = 'internal' for
67 67 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
68 68 # be specifying the version(s) of Mercurial they are tested with, or
69 69 # leave the attribute unspecified.
70 70 testedwith = 'internal'
71 71
72 72 _log = commandserver.log
73 73
74 74 def _hashlist(items):
75 75 """return sha1 hexdigest for a list"""
76 76 return hashlib.sha1(str(items)).hexdigest()
77 77
78 78 # sensitive config sections affecting confighash
79 79 _configsections = [
80 80 'alias', # affects global state commands.table
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 mtimehash is not included in confighash because we only know the paths of
153 153 extensions after importing them (there is imp.find_module but that faces
154 154 race conditions). We need to calculate confighash without importing.
155 155 """
156 156 def trystat(path):
157 157 try:
158 158 st = os.stat(path)
159 159 return (st.st_mtime, st.st_size)
160 160 except OSError:
161 161 # could be ENOENT, EPERM etc. not fatal in any case
162 162 pass
163 163 return _hashlist(map(trystat, paths))[:12]
164 164
165 165 class hashstate(object):
166 166 """a structure storing confighash, mtimehash, paths used for mtimehash"""
167 167 def __init__(self, confighash, mtimehash, mtimepaths):
168 168 self.confighash = confighash
169 169 self.mtimehash = mtimehash
170 170 self.mtimepaths = mtimepaths
171 171
172 172 @staticmethod
173 173 def fromui(ui, mtimepaths=None):
174 174 if mtimepaths is None:
175 175 mtimepaths = _getmtimepaths(ui)
176 176 confighash = _confighash(ui)
177 177 mtimehash = _mtimehash(mtimepaths)
178 178 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
179 179 return hashstate(confighash, mtimehash, mtimepaths)
180 180
181 181 # copied from hgext/pager.py:uisetup()
182 182 def _setuppagercmd(ui, options, cmd):
183 183 if not ui.formatted():
184 184 return
185 185
186 186 p = ui.config("pager", "pager", os.environ.get("PAGER"))
187 187 usepager = False
188 188 always = util.parsebool(options['pager'])
189 189 auto = options['pager'] == 'auto'
190 190
191 191 if not p:
192 192 pass
193 193 elif always:
194 194 usepager = True
195 195 elif not auto:
196 196 usepager = False
197 197 else:
198 198 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
199 199 attend = ui.configlist('pager', 'attend', attended)
200 200 ignore = ui.configlist('pager', 'ignore')
201 201 cmds, _ = cmdutil.findcmd(cmd, commands.table)
202 202
203 203 for cmd in cmds:
204 204 var = 'attend-%s' % cmd
205 205 if ui.config('pager', var):
206 206 usepager = ui.configbool('pager', var)
207 207 break
208 208 if (cmd in attend or
209 209 (cmd not in ignore and not attend)):
210 210 usepager = True
211 211 break
212 212
213 213 if usepager:
214 214 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
215 215 ui.setconfig('ui', 'interactive', False, 'pager')
216 216 return p
217 217
218 218 def _newchgui(srcui, csystem):
219 219 class chgui(srcui.__class__):
220 220 def __init__(self, src=None):
221 221 super(chgui, self).__init__(src)
222 222 if src:
223 223 self._csystem = getattr(src, '_csystem', csystem)
224 224 else:
225 225 self._csystem = csystem
226 226
227 227 def system(self, cmd, environ=None, cwd=None, onerr=None,
228 228 errprefix=None):
229 229 # fallback to the original system method if the output needs to be
230 230 # captured (to self._buffers), or the output stream is not stdout
231 231 # (e.g. stderr, cStringIO), because the chg client is not aware of
232 232 # these situations and will behave differently (write to stdout).
233 233 if (any(s[1] for s in self._bufferstates)
234 234 or not util.safehasattr(self.fout, 'fileno')
235 235 or self.fout.fileno() != sys.stdout.fileno()):
236 236 return super(chgui, self).system(cmd, environ, cwd, onerr,
237 237 errprefix)
238 238 # copied from mercurial/util.py:system()
239 239 self.flush()
240 240 def py2shell(val):
241 241 if val is None or val is False:
242 242 return '0'
243 243 if val is True:
244 244 return '1'
245 245 return str(val)
246 246 env = os.environ.copy()
247 247 if environ:
248 248 env.update((k, py2shell(v)) for k, v in environ.iteritems())
249 249 env['HG'] = util.hgexecutable()
250 250 rc = self._csystem(cmd, env, cwd)
251 251 if rc and onerr:
252 252 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
253 253 util.explainexit(rc)[0])
254 254 if errprefix:
255 255 errmsg = '%s: %s' % (errprefix, errmsg)
256 256 raise onerr(errmsg)
257 257 return rc
258 258
259 259 return chgui(srcui)
260 260
261 261 def _loadnewui(srcui, args):
262 262 newui = srcui.__class__()
263 263 for a in ['fin', 'fout', 'ferr', 'environ']:
264 264 setattr(newui, a, getattr(srcui, a))
265 265 if util.safehasattr(srcui, '_csystem'):
266 266 newui._csystem = srcui._csystem
267 267
268 268 # internal config: extensions.chgserver
269 269 newui.setconfig('extensions', 'chgserver',
270 270 srcui.config('extensions', 'chgserver'), '--config')
271 271
272 272 # command line args
273 273 args = args[:]
274 274 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
275 275
276 276 # stolen from tortoisehg.util.copydynamicconfig()
277 277 for section, name, value in srcui.walkconfig():
278 278 source = srcui.configsource(section, name)
279 279 if ':' in source or source == '--config':
280 280 # path:line or command line
281 281 continue
282 282 if source == 'none':
283 283 # ui.configsource returns 'none' by default
284 284 source = ''
285 285 newui.setconfig(section, name, value, source)
286 286
287 287 # load wd and repo config, copied from dispatch.py
288 288 cwds = dispatch._earlygetopt(['--cwd'], args)
289 289 cwd = cwds and os.path.realpath(cwds[-1]) or None
290 290 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
291 291 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
292 292
293 293 return (newui, newlui)
294 294
295 295 class channeledsystem(object):
296 296 """Propagate ui.system() request in the following format:
297 297
298 298 payload length (unsigned int),
299 299 cmd, '\0',
300 300 cwd, '\0',
301 301 envkey, '=', val, '\0',
302 302 ...
303 303 envkey, '=', val
304 304
305 305 and waits:
306 306
307 307 exitcode length (unsigned int),
308 308 exitcode (int)
309 309 """
310 310 def __init__(self, in_, out, channel):
311 311 self.in_ = in_
312 312 self.out = out
313 313 self.channel = channel
314 314
315 315 def __call__(self, cmd, environ, cwd):
316 316 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
317 317 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
318 318 data = '\0'.join(args)
319 319 self.out.write(struct.pack('>cI', self.channel, len(data)))
320 320 self.out.write(data)
321 321 self.out.flush()
322 322
323 323 length = self.in_.read(4)
324 324 length, = struct.unpack('>I', length)
325 325 if length != 4:
326 326 raise error.Abort(_('invalid response'))
327 327 rc, = struct.unpack('>i', self.in_.read(4))
328 328 return rc
329 329
330 330 _iochannels = [
331 331 # server.ch, ui.fp, mode
332 332 ('cin', 'fin', 'rb'),
333 333 ('cout', 'fout', 'wb'),
334 334 ('cerr', 'ferr', 'wb'),
335 335 ]
336 336
337 337 class chgcmdserver(commandserver.server):
338 338 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
339 339 super(chgcmdserver, self).__init__(
340 340 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
341 341 self.clientsock = sock
342 342 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
343 343 self.hashstate = hashstate
344 344 self.baseaddress = baseaddress
345 345 if hashstate is not None:
346 346 self.capabilities = self.capabilities.copy()
347 347 self.capabilities['validate'] = chgcmdserver.validate
348 348
349 349 def cleanup(self):
350 350 super(chgcmdserver, self).cleanup()
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 attempt to connect to $path
431 431 first. If it does not work, start a new server. It implies
432 432 "reconnect".
433 433 - "exit $n", the client should exit directly with code n.
434 434 This may happen if we cannot parse the config.
435 435 - "reconnect", the client should close the connection and
436 436 reconnect.
437 437 If neither "reconnect" nor "redirect" is included in the instruction
438 438 list, the client can continue with this server after completing all
439 439 the instructions.
440 440 """
441 441 args = self._readlist()
442 442 try:
443 443 self.ui, lui = _loadnewui(self.ui, args)
444 444 except error.ParseError as inst:
445 445 dispatch._formatparse(self.ui.warn, inst)
446 446 self.ui.flush()
447 447 self.cresult.write('exit 255')
448 448 return
449 449 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
450 450 insts = []
451 451 if newhash.mtimehash != self.hashstate.mtimehash:
452 452 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
453 453 insts.append('unlink %s' % addr)
454 454 # mtimehash is empty if one or more extensions fail to load.
455 455 # to be compatible with hg, still serve the client this time.
456 456 if self.hashstate.mtimehash:
457 457 insts.append('reconnect')
458 458 if newhash.confighash != self.hashstate.confighash:
459 459 addr = _hashaddress(self.baseaddress, newhash.confighash)
460 460 insts.append('redirect %s' % addr)
461 461 _log('validate: %s\n' % insts)
462 462 self.cresult.write('\0'.join(insts) or '\0')
463 463
464 464 def chdir(self):
465 465 """Change current directory
466 466
467 467 Note that the behavior of --cwd option is bit different from this.
468 468 It does not affect --config parameter.
469 469 """
470 470 path = self._readstr()
471 471 if not path:
472 472 return
473 473 _log('chdir to %r\n' % path)
474 474 os.chdir(path)
475 475
476 476 def setumask(self):
477 477 """Change umask"""
478 478 mask = struct.unpack('>I', self._read(4))[0]
479 479 _log('setumask %r\n' % mask)
480 480 os.umask(mask)
481 481
482 482 def getpager(self):
483 483 """Read cmdargs and write pager command to r-channel if enabled
484 484
485 485 If pager isn't enabled, this writes '\0' because channeledoutput
486 486 does not allow to write empty data.
487 487 """
488 488 args = self._readlist()
489 489 try:
490 490 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
491 491 args)
492 492 except (error.Abort, error.AmbiguousCommand, error.CommandError,
493 493 error.UnknownCommand):
494 494 cmd = None
495 495 options = {}
496 496 if not cmd or 'pager' not in options:
497 497 self.cresult.write('\0')
498 498 return
499 499
500 500 pagercmd = _setuppagercmd(self.ui, options, cmd)
501 501 if pagercmd:
502 502 # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
503 503 # we can exit if the pipe to the pager is closed
504 504 if util.safehasattr(signal, 'SIGPIPE') and \
505 505 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
506 506 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
507 507 self.cresult.write(pagercmd)
508 508 else:
509 509 self.cresult.write('\0')
510 510
511 511 def setenv(self):
512 512 """Clear and update os.environ
513 513
514 514 Note that not all variables can make an effect on the running process.
515 515 """
516 516 l = self._readlist()
517 517 try:
518 518 newenv = dict(s.split('=', 1) for s in l)
519 519 except ValueError:
520 520 raise ValueError('unexpected value in setenv request')
521 521 _log('setenv: %r\n' % sorted(newenv.keys()))
522 522 os.environ.clear()
523 523 os.environ.update(newenv)
524 524
525 525 capabilities = commandserver.server.capabilities.copy()
526 526 capabilities.update({'attachio': attachio,
527 527 'chdir': chdir,
528 528 'getpager': getpager,
529 529 'setenv': setenv,
530 530 'setumask': setumask})
531 531
532 532 def _tempaddress(address):
533 533 return '%s.%d.tmp' % (address, os.getpid())
534 534
535 535 def _hashaddress(address, hashstr):
536 536 return '%s-%s' % (address, hashstr)
537 537
538 538 class chgunixservicehandler(object):
539 539 """Set of operations for chg services"""
540 540
541 541 pollinterval = 1 # [sec]
542 542
543 543 def __init__(self, ui):
544 544 self.ui = ui
545 545 self.idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
546 546 self.lastactive = time.time()
547 547
548 548 def bindsocket(self, sock, address):
549 self.address = address
550 self._inithashstate()
549 self._inithashstate(address)
551 550 self._checkextensions()
552 551 self._bind(sock)
553 552 self._createsymlink()
554 553
555 def _inithashstate(self):
556 self.baseaddress = self.address
554 def _inithashstate(self, address):
555 self.baseaddress = address
557 556 if self.ui.configbool('chgserver', 'skiphash', False):
558 557 self.hashstate = None
558 self.address = address
559 559 return
560 560 self.hashstate = hashstate.fromui(self.ui)
561 self.address = _hashaddress(self.address, self.hashstate.confighash)
561 self.address = _hashaddress(address, self.hashstate.confighash)
562 562
563 563 def _checkextensions(self):
564 564 if not self.hashstate:
565 565 return
566 566 if extensions.notloaded():
567 567 # one or more extensions failed to load. mtimehash becomes
568 568 # meaningless because we do not know the paths of those extensions.
569 569 # set mtimehash to an illegal hash value to invalidate the server.
570 570 self.hashstate.mtimehash = ''
571 571
572 572 def _bind(self, sock):
573 573 # use a unique temp address so we can stat the file and do ownership
574 574 # check later
575 575 tempaddress = _tempaddress(self.address)
576 576 util.bindunixsocket(sock, tempaddress)
577 577 self._socketstat = os.stat(tempaddress)
578 578 # rename will replace the old socket file if exists atomically. the
579 579 # old server will detect ownership change and exit.
580 580 util.rename(tempaddress, self.address)
581 581
582 582 def _createsymlink(self):
583 583 if self.baseaddress == self.address:
584 584 return
585 585 tempaddress = _tempaddress(self.baseaddress)
586 586 os.symlink(os.path.basename(self.address), tempaddress)
587 587 util.rename(tempaddress, self.baseaddress)
588 588
589 589 def issocketowner(self):
590 590 try:
591 591 stat = os.stat(self.address)
592 592 return (stat.st_ino == self._socketstat.st_ino and
593 593 stat.st_mtime == self._socketstat.st_mtime)
594 594 except OSError:
595 595 return False
596 596
597 597 def unlinksocket(self, address):
598 598 if not self.issocketowner():
599 599 return
600 600 # it is possible to have a race condition here that we may
601 601 # remove another server's socket file. but that's okay
602 602 # since that server will detect and exit automatically and
603 603 # the client will start a new server on demand.
604 604 try:
605 605 os.unlink(self.address)
606 606 except OSError as exc:
607 607 if exc.errno != errno.ENOENT:
608 608 raise
609 609
610 610 def printbanner(self, address):
611 611 # no "listening at" message should be printed to simulate hg behavior
612 612 pass
613 613
614 614 def shouldexit(self):
615 615 if not self.issocketowner():
616 616 self.ui.debug('%s is not owned, exiting.\n' % self.address)
617 617 return True
618 618 if time.time() - self.lastactive > self.idletimeout:
619 619 self.ui.debug('being idle too long. exiting.\n')
620 620 return True
621 621 return False
622 622
623 623 def newconnection(self):
624 624 self.lastactive = time.time()
625 625
626 626 def createcmdserver(self, repo, conn, fin, fout):
627 627 return chgcmdserver(self.ui, repo, fin, fout, conn,
628 628 self.hashstate, self.baseaddress)
629 629
630 630 def chgunixservice(ui, repo, opts):
631 631 if repo:
632 632 # one chgserver can serve multiple repos. drop repo infomation
633 633 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
634 634 h = chgunixservicehandler(ui)
635 635 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
636 636
637 637 def uisetup(ui):
638 638 commandserver._servicemap['chgunix'] = chgunixservice
639 639
640 640 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
641 641 # start another chg. drop it to avoid possible side effects.
642 642 if 'CHGINTERNALMARK' in os.environ:
643 643 del os.environ['CHGINTERNALMARK']
General Comments 0
You need to be logged in to leave comments. Login now