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