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