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