##// END OF EJS Templates
commandserver: install logger to record server events through canonical API...
Yuya Nishihara -
r40859:82210d88 default
parent child Browse files
Show More
@@ -1,636 +1,638
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 (DEPRECATED)
23 23 'setumask2' command
24 24 set umask
25 25
26 26 'validate' command
27 27 reload the config and check if the server is up to date
28 28
29 29 Config
30 30 ------
31 31
32 32 ::
33 33
34 34 [chgserver]
35 35 # how long (in seconds) should an idle chg server exit
36 36 idletimeout = 3600
37 37
38 38 # whether to skip config or env change checks
39 39 skiphash = False
40 40 """
41 41
42 42 from __future__ import absolute_import
43 43
44 44 import hashlib
45 45 import inspect
46 46 import os
47 47 import re
48 48 import socket
49 49 import stat
50 50 import struct
51 51 import time
52 52
53 53 from .i18n import _
54 54
55 55 from . import (
56 56 commandserver,
57 57 encoding,
58 58 error,
59 59 extensions,
60 60 node,
61 61 pycompat,
62 62 util,
63 63 )
64 64
65 65 from .utils import (
66 66 procutil,
67 67 )
68 68
69 69 _log = commandserver.log
70 70
71 71 def _hashlist(items):
72 72 """return sha1 hexdigest for a list"""
73 73 return node.hex(hashlib.sha1(str(items)).digest())
74 74
75 75 # sensitive config sections affecting confighash
76 76 _configsections = [
77 77 'alias', # affects global state commands.table
78 78 'eol', # uses setconfig('eol', ...)
79 79 'extdiff', # uisetup will register new commands
80 80 'extensions',
81 81 ]
82 82
83 83 _configsectionitems = [
84 84 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
85 85 ]
86 86
87 87 # sensitive environment variables affecting confighash
88 88 _envre = re.compile(r'''\A(?:
89 89 CHGHG
90 90 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
91 91 |HG(?:ENCODING|PLAIN).*
92 92 |LANG(?:UAGE)?
93 93 |LC_.*
94 94 |LD_.*
95 95 |PATH
96 96 |PYTHON.*
97 97 |TERM(?:INFO)?
98 98 |TZ
99 99 )\Z''', re.X)
100 100
101 101 def _confighash(ui):
102 102 """return a quick hash for detecting config/env changes
103 103
104 104 confighash is the hash of sensitive config items and environment variables.
105 105
106 106 for chgserver, it is designed that once confighash changes, the server is
107 107 not qualified to serve its client and should redirect the client to a new
108 108 server. different from mtimehash, confighash change will not mark the
109 109 server outdated and exit since the user can have different configs at the
110 110 same time.
111 111 """
112 112 sectionitems = []
113 113 for section in _configsections:
114 114 sectionitems.append(ui.configitems(section))
115 115 for section, item in _configsectionitems:
116 116 sectionitems.append(ui.config(section, item))
117 117 sectionhash = _hashlist(sectionitems)
118 118 # If $CHGHG is set, the change to $HG should not trigger a new chg server
119 119 if 'CHGHG' in encoding.environ:
120 120 ignored = {'HG'}
121 121 else:
122 122 ignored = set()
123 123 envitems = [(k, v) for k, v in encoding.environ.iteritems()
124 124 if _envre.match(k) and k not in ignored]
125 125 envhash = _hashlist(sorted(envitems))
126 126 return sectionhash[:6] + envhash[:6]
127 127
128 128 def _getmtimepaths(ui):
129 129 """get a list of paths that should be checked to detect change
130 130
131 131 The list will include:
132 132 - extensions (will not cover all files for complex extensions)
133 133 - mercurial/__version__.py
134 134 - python binary
135 135 """
136 136 modules = [m for n, m in extensions.extensions(ui)]
137 137 try:
138 138 from . import __version__
139 139 modules.append(__version__)
140 140 except ImportError:
141 141 pass
142 142 files = [pycompat.sysexecutable]
143 143 for m in modules:
144 144 try:
145 145 files.append(inspect.getabsfile(m))
146 146 except TypeError:
147 147 pass
148 148 return sorted(set(files))
149 149
150 150 def _mtimehash(paths):
151 151 """return a quick hash for detecting file changes
152 152
153 153 mtimehash calls stat on given paths and calculate a hash based on size and
154 154 mtime of each file. mtimehash does not read file content because reading is
155 155 expensive. therefore it's not 100% reliable for detecting content changes.
156 156 it's possible to return different hashes for same file contents.
157 157 it's also possible to return a same hash for different file contents for
158 158 some carefully crafted situation.
159 159
160 160 for chgserver, it is designed that once mtimehash changes, the server is
161 161 considered outdated immediately and should no longer provide service.
162 162
163 163 mtimehash is not included in confighash because we only know the paths of
164 164 extensions after importing them (there is imp.find_module but that faces
165 165 race conditions). We need to calculate confighash without importing.
166 166 """
167 167 def trystat(path):
168 168 try:
169 169 st = os.stat(path)
170 170 return (st[stat.ST_MTIME], st.st_size)
171 171 except OSError:
172 172 # could be ENOENT, EPERM etc. not fatal in any case
173 173 pass
174 174 return _hashlist(map(trystat, paths))[:12]
175 175
176 176 class hashstate(object):
177 177 """a structure storing confighash, mtimehash, paths used for mtimehash"""
178 178 def __init__(self, confighash, mtimehash, mtimepaths):
179 179 self.confighash = confighash
180 180 self.mtimehash = mtimehash
181 181 self.mtimepaths = mtimepaths
182 182
183 183 @staticmethod
184 184 def fromui(ui, mtimepaths=None):
185 185 if mtimepaths is None:
186 186 mtimepaths = _getmtimepaths(ui)
187 187 confighash = _confighash(ui)
188 188 mtimehash = _mtimehash(mtimepaths)
189 189 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
190 190 return hashstate(confighash, mtimehash, mtimepaths)
191 191
192 192 def _newchgui(srcui, csystem, attachio):
193 193 class chgui(srcui.__class__):
194 194 def __init__(self, src=None):
195 195 super(chgui, self).__init__(src)
196 196 if src:
197 197 self._csystem = getattr(src, '_csystem', csystem)
198 198 else:
199 199 self._csystem = csystem
200 200
201 201 def _runsystem(self, cmd, environ, cwd, out):
202 202 # fallback to the original system method if
203 203 # a. the output stream is not stdout (e.g. stderr, cStringIO),
204 204 # b. or stdout is redirected by protectstdio(),
205 205 # because the chg client is not aware of these situations and
206 206 # will behave differently (i.e. write to stdout).
207 207 if (out is not self.fout
208 208 or not util.safehasattr(self.fout, 'fileno')
209 209 or self.fout.fileno() != procutil.stdout.fileno()
210 210 or self._finoutredirected):
211 211 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
212 212 self.flush()
213 213 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
214 214
215 215 def _runpager(self, cmd, env=None):
216 216 self._csystem(cmd, procutil.shellenviron(env), type='pager',
217 217 cmdtable={'attachio': attachio})
218 218 return True
219 219
220 220 return chgui(srcui)
221 221
222 def _loadnewui(srcui, args):
222 def _loadnewui(srcui, args, cdebug):
223 223 from . import dispatch # avoid cycle
224 224
225 225 newui = srcui.__class__.load()
226 226 for a in ['fin', 'fout', 'ferr', 'environ']:
227 227 setattr(newui, a, getattr(srcui, a))
228 228 if util.safehasattr(srcui, '_csystem'):
229 229 newui._csystem = srcui._csystem
230 230
231 231 # command line args
232 232 options = dispatch._earlyparseopts(newui, args)
233 233 dispatch._parseconfig(newui, options['config'])
234 234
235 235 # stolen from tortoisehg.util.copydynamicconfig()
236 236 for section, name, value in srcui.walkconfig():
237 237 source = srcui.configsource(section, name)
238 238 if ':' in source or source == '--config' or source.startswith('$'):
239 239 # path:line or command line, or environ
240 240 continue
241 241 newui.setconfig(section, name, value, source)
242 242
243 243 # load wd and repo config, copied from dispatch.py
244 244 cwd = options['cwd']
245 245 cwd = cwd and os.path.realpath(cwd) or None
246 246 rpath = options['repository']
247 247 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
248 248
249 249 extensions.populateui(newui)
250 commandserver.setuplogging(newui, fp=cdebug)
250 251 if newui is not newlui:
251 252 extensions.populateui(newlui)
253 commandserver.setuplogging(newlui, fp=cdebug)
252 254
253 255 return (newui, newlui)
254 256
255 257 class channeledsystem(object):
256 258 """Propagate ui.system() request in the following format:
257 259
258 260 payload length (unsigned int),
259 261 type, '\0',
260 262 cmd, '\0',
261 263 cwd, '\0',
262 264 envkey, '=', val, '\0',
263 265 ...
264 266 envkey, '=', val
265 267
266 268 if type == 'system', waits for:
267 269
268 270 exitcode length (unsigned int),
269 271 exitcode (int)
270 272
271 273 if type == 'pager', repetitively waits for a command name ending with '\n'
272 274 and executes it defined by cmdtable, or exits the loop if the command name
273 275 is empty.
274 276 """
275 277 def __init__(self, in_, out, channel):
276 278 self.in_ = in_
277 279 self.out = out
278 280 self.channel = channel
279 281
280 282 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
281 283 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or '.')]
282 284 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
283 285 data = '\0'.join(args)
284 286 self.out.write(struct.pack('>cI', self.channel, len(data)))
285 287 self.out.write(data)
286 288 self.out.flush()
287 289
288 290 if type == 'system':
289 291 length = self.in_.read(4)
290 292 length, = struct.unpack('>I', length)
291 293 if length != 4:
292 294 raise error.Abort(_('invalid response'))
293 295 rc, = struct.unpack('>i', self.in_.read(4))
294 296 return rc
295 297 elif type == 'pager':
296 298 while True:
297 299 cmd = self.in_.readline()[:-1]
298 300 if not cmd:
299 301 break
300 302 if cmdtable and cmd in cmdtable:
301 303 _log('pager subcommand: %s' % cmd)
302 304 cmdtable[cmd]()
303 305 else:
304 306 raise error.Abort(_('unexpected command: %s') % cmd)
305 307 else:
306 308 raise error.ProgrammingError('invalid S channel type: %s' % type)
307 309
308 310 _iochannels = [
309 311 # server.ch, ui.fp, mode
310 312 ('cin', 'fin', r'rb'),
311 313 ('cout', 'fout', r'wb'),
312 314 ('cerr', 'ferr', r'wb'),
313 315 ]
314 316
315 317 class chgcmdserver(commandserver.server):
316 318 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
317 319 super(chgcmdserver, self).__init__(
318 320 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
319 321 repo, fin, fout)
320 322 self.clientsock = sock
321 323 self._ioattached = False
322 324 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
323 325 self.hashstate = hashstate
324 326 self.baseaddress = baseaddress
325 327 if hashstate is not None:
326 328 self.capabilities = self.capabilities.copy()
327 329 self.capabilities['validate'] = chgcmdserver.validate
328 330
329 331 def cleanup(self):
330 332 super(chgcmdserver, self).cleanup()
331 333 # dispatch._runcatch() does not flush outputs if exception is not
332 334 # handled by dispatch._dispatch()
333 335 self.ui.flush()
334 336 self._restoreio()
335 337 self._ioattached = False
336 338
337 339 def attachio(self):
338 340 """Attach to client's stdio passed via unix domain socket; all
339 341 channels except cresult will no longer be used
340 342 """
341 343 # tell client to sendmsg() with 1-byte payload, which makes it
342 344 # distinctive from "attachio\n" command consumed by client.read()
343 345 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
344 346 clientfds = util.recvfds(self.clientsock.fileno())
345 347 _log('received fds: %r\n' % clientfds)
346 348
347 349 ui = self.ui
348 350 ui.flush()
349 351 self._saveio()
350 352 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
351 353 assert fd > 0
352 354 fp = getattr(ui, fn)
353 355 os.dup2(fd, fp.fileno())
354 356 os.close(fd)
355 357 if self._ioattached:
356 358 continue
357 359 # reset buffering mode when client is first attached. as we want
358 360 # to see output immediately on pager, the mode stays unchanged
359 361 # when client re-attached. ferr is unchanged because it should
360 362 # be unbuffered no matter if it is a tty or not.
361 363 if fn == 'ferr':
362 364 newfp = fp
363 365 else:
364 366 # make it line buffered explicitly because the default is
365 367 # decided on first write(), where fout could be a pager.
366 368 if fp.isatty():
367 369 bufsize = 1 # line buffered
368 370 else:
369 371 bufsize = -1 # system default
370 372 newfp = os.fdopen(fp.fileno(), mode, bufsize)
371 373 setattr(ui, fn, newfp)
372 374 setattr(self, cn, newfp)
373 375
374 376 self._ioattached = True
375 377 self.cresult.write(struct.pack('>i', len(clientfds)))
376 378
377 379 def _saveio(self):
378 380 if self._oldios:
379 381 return
380 382 ui = self.ui
381 383 for cn, fn, _mode in _iochannels:
382 384 ch = getattr(self, cn)
383 385 fp = getattr(ui, fn)
384 386 fd = os.dup(fp.fileno())
385 387 self._oldios.append((ch, fp, fd))
386 388
387 389 def _restoreio(self):
388 390 ui = self.ui
389 391 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
390 392 newfp = getattr(ui, fn)
391 393 # close newfp while it's associated with client; otherwise it
392 394 # would be closed when newfp is deleted
393 395 if newfp is not fp:
394 396 newfp.close()
395 397 # restore original fd: fp is open again
396 398 os.dup2(fd, fp.fileno())
397 399 os.close(fd)
398 400 setattr(self, cn, ch)
399 401 setattr(ui, fn, fp)
400 402 del self._oldios[:]
401 403
402 404 def validate(self):
403 405 """Reload the config and check if the server is up to date
404 406
405 407 Read a list of '\0' separated arguments.
406 408 Write a non-empty list of '\0' separated instruction strings or '\0'
407 409 if the list is empty.
408 410 An instruction string could be either:
409 411 - "unlink $path", the client should unlink the path to stop the
410 412 outdated server.
411 413 - "redirect $path", the client should attempt to connect to $path
412 414 first. If it does not work, start a new server. It implies
413 415 "reconnect".
414 416 - "exit $n", the client should exit directly with code n.
415 417 This may happen if we cannot parse the config.
416 418 - "reconnect", the client should close the connection and
417 419 reconnect.
418 420 If neither "reconnect" nor "redirect" is included in the instruction
419 421 list, the client can continue with this server after completing all
420 422 the instructions.
421 423 """
422 424 from . import dispatch # avoid cycle
423 425
424 426 args = self._readlist()
425 427 try:
426 self.ui, lui = _loadnewui(self.ui, args)
428 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
427 429 except error.ParseError as inst:
428 430 dispatch._formatparse(self.ui.warn, inst)
429 431 self.ui.flush()
430 432 self.cresult.write('exit 255')
431 433 return
432 434 except error.Abort as inst:
433 435 self.ui.error(_("abort: %s\n") % inst)
434 436 if inst.hint:
435 437 self.ui.error(_("(%s)\n") % inst.hint)
436 438 self.ui.flush()
437 439 self.cresult.write('exit 255')
438 440 return
439 441 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
440 442 insts = []
441 443 if newhash.mtimehash != self.hashstate.mtimehash:
442 444 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
443 445 insts.append('unlink %s' % addr)
444 446 # mtimehash is empty if one or more extensions fail to load.
445 447 # to be compatible with hg, still serve the client this time.
446 448 if self.hashstate.mtimehash:
447 449 insts.append('reconnect')
448 450 if newhash.confighash != self.hashstate.confighash:
449 451 addr = _hashaddress(self.baseaddress, newhash.confighash)
450 452 insts.append('redirect %s' % addr)
451 453 _log('validate: %s\n' % insts)
452 454 self.cresult.write('\0'.join(insts) or '\0')
453 455
454 456 def chdir(self):
455 457 """Change current directory
456 458
457 459 Note that the behavior of --cwd option is bit different from this.
458 460 It does not affect --config parameter.
459 461 """
460 462 path = self._readstr()
461 463 if not path:
462 464 return
463 465 _log('chdir to %r\n' % path)
464 466 os.chdir(path)
465 467
466 468 def setumask(self):
467 469 """Change umask (DEPRECATED)"""
468 470 # BUG: this does not follow the message frame structure, but kept for
469 471 # backward compatibility with old chg clients for some time
470 472 self._setumask(self._read(4))
471 473
472 474 def setumask2(self):
473 475 """Change umask"""
474 476 data = self._readstr()
475 477 if len(data) != 4:
476 478 raise ValueError('invalid mask length in setumask2 request')
477 479 self._setumask(data)
478 480
479 481 def _setumask(self, data):
480 482 mask = struct.unpack('>I', data)[0]
481 483 _log('setumask %r\n' % mask)
482 484 os.umask(mask)
483 485
484 486 def runcommand(self):
485 487 # pager may be attached within the runcommand session, which should
486 488 # be detached at the end of the session. otherwise the pager wouldn't
487 489 # receive EOF.
488 490 globaloldios = self._oldios
489 491 self._oldios = []
490 492 try:
491 493 return super(chgcmdserver, self).runcommand()
492 494 finally:
493 495 self._restoreio()
494 496 self._oldios = globaloldios
495 497
496 498 def setenv(self):
497 499 """Clear and update os.environ
498 500
499 501 Note that not all variables can make an effect on the running process.
500 502 """
501 503 l = self._readlist()
502 504 try:
503 505 newenv = dict(s.split('=', 1) for s in l)
504 506 except ValueError:
505 507 raise ValueError('unexpected value in setenv request')
506 508 _log('setenv: %r\n' % sorted(newenv.keys()))
507 509 encoding.environ.clear()
508 510 encoding.environ.update(newenv)
509 511
510 512 capabilities = commandserver.server.capabilities.copy()
511 513 capabilities.update({'attachio': attachio,
512 514 'chdir': chdir,
513 515 'runcommand': runcommand,
514 516 'setenv': setenv,
515 517 'setumask': setumask,
516 518 'setumask2': setumask2})
517 519
518 520 if util.safehasattr(procutil, 'setprocname'):
519 521 def setprocname(self):
520 522 """Change process title"""
521 523 name = self._readstr()
522 524 _log('setprocname: %r\n' % name)
523 525 procutil.setprocname(name)
524 526 capabilities['setprocname'] = setprocname
525 527
526 528 def _tempaddress(address):
527 529 return '%s.%d.tmp' % (address, os.getpid())
528 530
529 531 def _hashaddress(address, hashstr):
530 532 # if the basename of address contains '.', use only the left part. this
531 533 # makes it possible for the client to pass 'server.tmp$PID' and follow by
532 534 # an atomic rename to avoid locking when spawning new servers.
533 535 dirname, basename = os.path.split(address)
534 536 basename = basename.split('.', 1)[0]
535 537 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
536 538
537 539 class chgunixservicehandler(object):
538 540 """Set of operations for chg services"""
539 541
540 542 pollinterval = 1 # [sec]
541 543
542 544 def __init__(self, ui):
543 545 self.ui = ui
544 546 self._idletimeout = ui.configint('chgserver', 'idletimeout')
545 547 self._lastactive = time.time()
546 548
547 549 def bindsocket(self, sock, address):
548 550 self._inithashstate(address)
549 551 self._checkextensions()
550 552 self._bind(sock)
551 553 self._createsymlink()
552 554 # no "listening at" message should be printed to simulate hg behavior
553 555
554 556 def _inithashstate(self, address):
555 557 self._baseaddress = address
556 558 if self.ui.configbool('chgserver', 'skiphash'):
557 559 self._hashstate = None
558 560 self._realaddress = address
559 561 return
560 562 self._hashstate = hashstate.fromui(self.ui)
561 563 self._realaddress = _hashaddress(address, self._hashstate.confighash)
562 564
563 565 def _checkextensions(self):
564 566 if not self._hashstate:
565 567 return
566 568 if extensions.notloaded():
567 569 # one or more extensions failed to load. mtimehash becomes
568 570 # meaningless because we do not know the paths of those extensions.
569 571 # set mtimehash to an illegal hash value to invalidate the server.
570 572 self._hashstate.mtimehash = ''
571 573
572 574 def _bind(self, sock):
573 575 # use a unique temp address so we can stat the file and do ownership
574 576 # check later
575 577 tempaddress = _tempaddress(self._realaddress)
576 578 util.bindunixsocket(sock, tempaddress)
577 579 self._socketstat = os.stat(tempaddress)
578 580 sock.listen(socket.SOMAXCONN)
579 581 # rename will replace the old socket file if exists atomically. the
580 582 # old server will detect ownership change and exit.
581 583 util.rename(tempaddress, self._realaddress)
582 584
583 585 def _createsymlink(self):
584 586 if self._baseaddress == self._realaddress:
585 587 return
586 588 tempaddress = _tempaddress(self._baseaddress)
587 589 os.symlink(os.path.basename(self._realaddress), tempaddress)
588 590 util.rename(tempaddress, self._baseaddress)
589 591
590 592 def _issocketowner(self):
591 593 try:
592 594 st = os.stat(self._realaddress)
593 595 return (st.st_ino == self._socketstat.st_ino and
594 596 st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
595 597 except OSError:
596 598 return False
597 599
598 600 def unlinksocket(self, address):
599 601 if not self._issocketowner():
600 602 return
601 603 # it is possible to have a race condition here that we may
602 604 # remove another server's socket file. but that's okay
603 605 # since that server will detect and exit automatically and
604 606 # the client will start a new server on demand.
605 607 util.tryunlink(self._realaddress)
606 608
607 609 def shouldexit(self):
608 610 if not self._issocketowner():
609 611 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
610 612 return True
611 613 if time.time() - self._lastactive > self._idletimeout:
612 614 self.ui.debug('being idle too long. exiting.\n')
613 615 return True
614 616 return False
615 617
616 618 def newconnection(self):
617 619 self._lastactive = time.time()
618 620
619 621 def createcmdserver(self, repo, conn, fin, fout):
620 622 return chgcmdserver(self.ui, repo, fin, fout, conn,
621 623 self._hashstate, self._baseaddress)
622 624
623 625 def chgunixservice(ui, repo, opts):
624 626 # CHGINTERNALMARK is set by chg client. It is an indication of things are
625 627 # started by chg so other code can do things accordingly, like disabling
626 628 # demandimport or detecting chg client started by chg client. When executed
627 629 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
628 630 # environ cleaner.
629 631 if 'CHGINTERNALMARK' in encoding.environ:
630 632 del encoding.environ['CHGINTERNALMARK']
631 633
632 634 if repo:
633 635 # one chgserver can serve multiple repos. drop repo information
634 636 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
635 637 h = chgunixservicehandler(ui)
636 638 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,613 +1,639
1 1 # commandserver.py - communicate with Mercurial's API over a pipe
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import gc
12 12 import os
13 13 import random
14 14 import signal
15 15 import socket
16 16 import struct
17 17 import traceback
18 18
19 19 try:
20 20 import selectors
21 21 selectors.BaseSelector
22 22 except ImportError:
23 23 from .thirdparty import selectors2 as selectors
24 24
25 25 from .i18n import _
26 26 from . import (
27 27 encoding,
28 28 error,
29 loggingutil,
29 30 pycompat,
30 31 util,
32 vfs as vfsmod,
31 33 )
32 34 from .utils import (
33 35 cborutil,
34 36 procutil,
35 37 )
36 38
37 39 logfile = None
38 40
39 41 def log(*args):
40 42 if not logfile:
41 43 return
42 44
43 45 for a in args:
44 46 logfile.write(str(a))
45 47
46 48 logfile.flush()
47 49
48 50 class channeledoutput(object):
49 51 """
50 52 Write data to out in the following format:
51 53
52 54 data length (unsigned int),
53 55 data
54 56 """
55 57 def __init__(self, out, channel):
56 58 self.out = out
57 59 self.channel = channel
58 60
59 61 @property
60 62 def name(self):
61 63 return '<%c-channel>' % self.channel
62 64
63 65 def write(self, data):
64 66 if not data:
65 67 return
66 68 # single write() to guarantee the same atomicity as the underlying file
67 69 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
68 70 self.out.flush()
69 71
70 72 def __getattr__(self, attr):
71 73 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
72 74 raise AttributeError(attr)
73 75 return getattr(self.out, attr)
74 76
75 77 class channeledmessage(object):
76 78 """
77 79 Write encoded message and metadata to out in the following format:
78 80
79 81 data length (unsigned int),
80 82 encoded message and metadata, as a flat key-value dict.
81 83
82 84 Each message should have 'type' attribute. Messages of unknown type
83 85 should be ignored.
84 86 """
85 87
86 88 # teach ui that write() can take **opts
87 89 structured = True
88 90
89 91 def __init__(self, out, channel, encodename, encodefn):
90 92 self._cout = channeledoutput(out, channel)
91 93 self.encoding = encodename
92 94 self._encodefn = encodefn
93 95
94 96 def write(self, data, **opts):
95 97 opts = pycompat.byteskwargs(opts)
96 98 if data is not None:
97 99 opts[b'data'] = data
98 100 self._cout.write(self._encodefn(opts))
99 101
100 102 def __getattr__(self, attr):
101 103 return getattr(self._cout, attr)
102 104
103 105 class channeledinput(object):
104 106 """
105 107 Read data from in_.
106 108
107 109 Requests for input are written to out in the following format:
108 110 channel identifier - 'I' for plain input, 'L' line based (1 byte)
109 111 how many bytes to send at most (unsigned int),
110 112
111 113 The client replies with:
112 114 data length (unsigned int), 0 meaning EOF
113 115 data
114 116 """
115 117
116 118 maxchunksize = 4 * 1024
117 119
118 120 def __init__(self, in_, out, channel):
119 121 self.in_ = in_
120 122 self.out = out
121 123 self.channel = channel
122 124
123 125 @property
124 126 def name(self):
125 127 return '<%c-channel>' % self.channel
126 128
127 129 def read(self, size=-1):
128 130 if size < 0:
129 131 # if we need to consume all the clients input, ask for 4k chunks
130 132 # so the pipe doesn't fill up risking a deadlock
131 133 size = self.maxchunksize
132 134 s = self._read(size, self.channel)
133 135 buf = s
134 136 while s:
135 137 s = self._read(size, self.channel)
136 138 buf += s
137 139
138 140 return buf
139 141 else:
140 142 return self._read(size, self.channel)
141 143
142 144 def _read(self, size, channel):
143 145 if not size:
144 146 return ''
145 147 assert size > 0
146 148
147 149 # tell the client we need at most size bytes
148 150 self.out.write(struct.pack('>cI', channel, size))
149 151 self.out.flush()
150 152
151 153 length = self.in_.read(4)
152 154 length = struct.unpack('>I', length)[0]
153 155 if not length:
154 156 return ''
155 157 else:
156 158 return self.in_.read(length)
157 159
158 160 def readline(self, size=-1):
159 161 if size < 0:
160 162 size = self.maxchunksize
161 163 s = self._read(size, 'L')
162 164 buf = s
163 165 # keep asking for more until there's either no more or
164 166 # we got a full line
165 167 while s and s[-1] != '\n':
166 168 s = self._read(size, 'L')
167 169 buf += s
168 170
169 171 return buf
170 172 else:
171 173 return self._read(size, 'L')
172 174
173 175 def __iter__(self):
174 176 return self
175 177
176 178 def next(self):
177 179 l = self.readline()
178 180 if not l:
179 181 raise StopIteration
180 182 return l
181 183
182 184 __next__ = next
183 185
184 186 def __getattr__(self, attr):
185 187 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
186 188 raise AttributeError(attr)
187 189 return getattr(self.in_, attr)
188 190
189 191 _messageencoders = {
190 192 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
191 193 }
192 194
193 195 def _selectmessageencoder(ui):
194 196 # experimental config: cmdserver.message-encodings
195 197 encnames = ui.configlist(b'cmdserver', b'message-encodings')
196 198 for n in encnames:
197 199 f = _messageencoders.get(n)
198 200 if f:
199 201 return n, f
200 202 raise error.Abort(b'no supported message encodings: %s'
201 203 % b' '.join(encnames))
202 204
203 205 class server(object):
204 206 """
205 207 Listens for commands on fin, runs them and writes the output on a channel
206 208 based stream to fout.
207 209 """
208 210 def __init__(self, ui, repo, fin, fout):
209 211 self.cwd = encoding.getcwd()
210 212
211 213 if ui.config("cmdserver", "log") == '-':
212 214 global logfile
213 215 # switch log stream to the 'd' (debug) channel
214 216 logfile = channeledoutput(fout, 'd')
215 217
216 218 if repo:
217 219 # the ui here is really the repo ui so take its baseui so we don't
218 220 # end up with its local configuration
219 221 self.ui = repo.baseui
220 222 self.repo = repo
221 223 self.repoui = repo.ui
222 224 else:
223 225 self.ui = ui
224 226 self.repo = self.repoui = None
225 227
228 self.cdebug = logfile
226 229 self.cerr = channeledoutput(fout, 'e')
227 230 self.cout = channeledoutput(fout, 'o')
228 231 self.cin = channeledinput(fin, fout, 'I')
229 232 self.cresult = channeledoutput(fout, 'r')
230 233
234 if self.ui.config(b'cmdserver', b'log') == b'-':
235 # switch log stream of server's ui to the 'd' (debug) channel
236 # (don't touch repo.ui as its lifetime is longer than the server)
237 self.ui = self.ui.copy()
238 setuplogging(self.ui, repo=None, fp=self.cdebug)
239
231 240 # TODO: add this to help/config.txt when stabilized
232 241 # ``channel``
233 242 # Use separate channel for structured output. (Command-server only)
234 243 self.cmsg = None
235 244 if ui.config(b'ui', b'message-output') == b'channel':
236 245 encname, encfn = _selectmessageencoder(ui)
237 246 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
238 247
239 248 self.client = fin
240 249
241 250 def cleanup(self):
242 251 """release and restore resources taken during server session"""
243 252
244 253 def _read(self, size):
245 254 if not size:
246 255 return ''
247 256
248 257 data = self.client.read(size)
249 258
250 259 # is the other end closed?
251 260 if not data:
252 261 raise EOFError
253 262
254 263 return data
255 264
256 265 def _readstr(self):
257 266 """read a string from the channel
258 267
259 268 format:
260 269 data length (uint32), data
261 270 """
262 271 length = struct.unpack('>I', self._read(4))[0]
263 272 if not length:
264 273 return ''
265 274 return self._read(length)
266 275
267 276 def _readlist(self):
268 277 """read a list of NULL separated strings from the channel"""
269 278 s = self._readstr()
270 279 if s:
271 280 return s.split('\0')
272 281 else:
273 282 return []
274 283
275 284 def runcommand(self):
276 285 """ reads a list of \0 terminated arguments, executes
277 286 and writes the return code to the result channel """
278 287 from . import dispatch # avoid cycle
279 288
280 289 args = self._readlist()
281 290
282 291 # copy the uis so changes (e.g. --config or --verbose) don't
283 292 # persist between requests
284 293 copiedui = self.ui.copy()
285 294 uis = [copiedui]
286 295 if self.repo:
287 296 self.repo.baseui = copiedui
288 297 # clone ui without using ui.copy because this is protected
289 298 repoui = self.repoui.__class__(self.repoui)
290 299 repoui.copy = copiedui.copy # redo copy protection
291 300 uis.append(repoui)
292 301 self.repo.ui = self.repo.dirstate._ui = repoui
293 302 self.repo.invalidateall()
294 303
295 304 for ui in uis:
296 305 ui.resetstate()
297 306 # any kind of interaction must use server channels, but chg may
298 307 # replace channels by fully functional tty files. so nontty is
299 308 # enforced only if cin is a channel.
300 309 if not util.safehasattr(self.cin, 'fileno'):
301 310 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
302 311
303 312 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
304 313 self.cout, self.cerr, self.cmsg)
305 314
306 315 try:
307 316 ret = dispatch.dispatch(req) & 255
308 317 self.cresult.write(struct.pack('>i', int(ret)))
309 318 finally:
310 319 # restore old cwd
311 320 if '--cwd' in args:
312 321 os.chdir(self.cwd)
313 322
314 323 def getencoding(self):
315 324 """ writes the current encoding to the result channel """
316 325 self.cresult.write(encoding.encoding)
317 326
318 327 def serveone(self):
319 328 cmd = self.client.readline()[:-1]
320 329 if cmd:
321 330 handler = self.capabilities.get(cmd)
322 331 if handler:
323 332 handler(self)
324 333 else:
325 334 # clients are expected to check what commands are supported by
326 335 # looking at the servers capabilities
327 336 raise error.Abort(_('unknown command %s') % cmd)
328 337
329 338 return cmd != ''
330 339
331 340 capabilities = {'runcommand': runcommand,
332 341 'getencoding': getencoding}
333 342
334 343 def serve(self):
335 344 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
336 345 hellomsg += '\n'
337 346 hellomsg += 'encoding: ' + encoding.encoding
338 347 hellomsg += '\n'
339 348 if self.cmsg:
340 349 hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
341 350 hellomsg += 'pid: %d' % procutil.getpid()
342 351 if util.safehasattr(os, 'getpgid'):
343 352 hellomsg += '\n'
344 353 hellomsg += 'pgid: %d' % os.getpgid(0)
345 354
346 355 # write the hello msg in -one- chunk
347 356 self.cout.write(hellomsg)
348 357
349 358 try:
350 359 while self.serveone():
351 360 pass
352 361 except EOFError:
353 362 # we'll get here if the client disconnected while we were reading
354 363 # its request
355 364 return 1
356 365
357 366 return 0
358 367
359 def setuplogging(ui):
368 def setuplogging(ui, repo=None, fp=None):
360 369 """Set up server logging facility
361 370
362 If cmdserver.log is '-', log messages will be sent to the 'd' channel
363 while a client is connected. Otherwise, messages will be written to
364 the stderr of the server process.
371 If cmdserver.log is '-', log messages will be sent to the given fp.
372 It should be the 'd' channel while a client is connected, and otherwise
373 is the stderr of the server process.
365 374 """
366 375 # developer config: cmdserver.log
367 376 logpath = ui.config(b'cmdserver', b'log')
368 377 if not logpath:
369 378 return
379 tracked = {b'cmdserver'}
370 380
371 381 global logfile
372 382 if logpath == b'-':
373 383 logfile = ui.ferr
374 384 else:
375 385 logfile = open(logpath, 'ab')
376 386
387 if logpath == b'-' and fp:
388 logger = loggingutil.fileobjectlogger(fp, tracked)
389 elif logpath == b'-':
390 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
391 else:
392 logpath = os.path.abspath(logpath)
393 vfs = vfsmod.vfs(os.path.dirname(logpath))
394 logger = loggingutil.filelogger(vfs, os.path.basename(logpath), tracked)
395
396 targetuis = {ui}
397 if repo:
398 targetuis.add(repo.baseui)
399 targetuis.add(repo.ui)
400 for u in targetuis:
401 u.setlogger(b'cmdserver', logger)
402
377 403 class pipeservice(object):
378 404 def __init__(self, ui, repo, opts):
379 405 self.ui = ui
380 406 self.repo = repo
381 407
382 408 def init(self):
383 409 pass
384 410
385 411 def run(self):
386 412 ui = self.ui
387 413 # redirect stdio to null device so that broken extensions or in-process
388 414 # hooks will never cause corruption of channel protocol.
389 415 with procutil.protectedstdio(ui.fin, ui.fout) as (fin, fout):
390 416 sv = server(ui, self.repo, fin, fout)
391 417 try:
392 418 return sv.serve()
393 419 finally:
394 420 sv.cleanup()
395 421
396 422 def _initworkerprocess():
397 423 # use a different process group from the master process, in order to:
398 424 # 1. make the current process group no longer "orphaned" (because the
399 425 # parent of this process is in a different process group while
400 426 # remains in a same session)
401 427 # according to POSIX 2.2.2.52, orphaned process group will ignore
402 428 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
403 429 # cause trouble for things like ncurses.
404 430 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
405 431 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
406 432 # processes like ssh will be killed properly, without affecting
407 433 # unrelated processes.
408 434 os.setpgid(0, 0)
409 435 # change random state otherwise forked request handlers would have a
410 436 # same state inherited from parent.
411 437 random.seed()
412 438
413 439 def _serverequest(ui, repo, conn, createcmdserver):
414 440 fin = conn.makefile(r'rb')
415 441 fout = conn.makefile(r'wb')
416 442 sv = None
417 443 try:
418 444 sv = createcmdserver(repo, conn, fin, fout)
419 445 try:
420 446 sv.serve()
421 447 # handle exceptions that may be raised by command server. most of
422 448 # known exceptions are caught by dispatch.
423 449 except error.Abort as inst:
424 450 ui.error(_('abort: %s\n') % inst)
425 451 except IOError as inst:
426 452 if inst.errno != errno.EPIPE:
427 453 raise
428 454 except KeyboardInterrupt:
429 455 pass
430 456 finally:
431 457 sv.cleanup()
432 458 except: # re-raises
433 459 # also write traceback to error channel. otherwise client cannot
434 460 # see it because it is written to server's stderr by default.
435 461 if sv:
436 462 cerr = sv.cerr
437 463 else:
438 464 cerr = channeledoutput(fout, 'e')
439 465 cerr.write(encoding.strtolocal(traceback.format_exc()))
440 466 raise
441 467 finally:
442 468 fin.close()
443 469 try:
444 470 fout.close() # implicit flush() may cause another EPIPE
445 471 except IOError as inst:
446 472 if inst.errno != errno.EPIPE:
447 473 raise
448 474
449 475 class unixservicehandler(object):
450 476 """Set of pluggable operations for unix-mode services
451 477
452 478 Almost all methods except for createcmdserver() are called in the main
453 479 process. You can't pass mutable resource back from createcmdserver().
454 480 """
455 481
456 482 pollinterval = None
457 483
458 484 def __init__(self, ui):
459 485 self.ui = ui
460 486
461 487 def bindsocket(self, sock, address):
462 488 util.bindunixsocket(sock, address)
463 489 sock.listen(socket.SOMAXCONN)
464 490 self.ui.status(_('listening at %s\n') % address)
465 491 self.ui.flush() # avoid buffering of status message
466 492
467 493 def unlinksocket(self, address):
468 494 os.unlink(address)
469 495
470 496 def shouldexit(self):
471 497 """True if server should shut down; checked per pollinterval"""
472 498 return False
473 499
474 500 def newconnection(self):
475 501 """Called when main process notices new connection"""
476 502
477 503 def createcmdserver(self, repo, conn, fin, fout):
478 504 """Create new command server instance; called in the process that
479 505 serves for the current connection"""
480 506 return server(self.ui, repo, fin, fout)
481 507
482 508 class unixforkingservice(object):
483 509 """
484 510 Listens on unix domain socket and forks server per connection
485 511 """
486 512
487 513 def __init__(self, ui, repo, opts, handler=None):
488 514 self.ui = ui
489 515 self.repo = repo
490 516 self.address = opts['address']
491 517 if not util.safehasattr(socket, 'AF_UNIX'):
492 518 raise error.Abort(_('unsupported platform'))
493 519 if not self.address:
494 520 raise error.Abort(_('no socket path specified with --address'))
495 521 self._servicehandler = handler or unixservicehandler(ui)
496 522 self._sock = None
497 523 self._oldsigchldhandler = None
498 524 self._workerpids = set() # updated by signal handler; do not iterate
499 525 self._socketunlinked = None
500 526
501 527 def init(self):
502 528 self._sock = socket.socket(socket.AF_UNIX)
503 529 self._servicehandler.bindsocket(self._sock, self.address)
504 530 if util.safehasattr(procutil, 'unblocksignal'):
505 531 procutil.unblocksignal(signal.SIGCHLD)
506 532 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
507 533 self._oldsigchldhandler = o
508 534 self._socketunlinked = False
509 535
510 536 def _unlinksocket(self):
511 537 if not self._socketunlinked:
512 538 self._servicehandler.unlinksocket(self.address)
513 539 self._socketunlinked = True
514 540
515 541 def _cleanup(self):
516 542 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
517 543 self._sock.close()
518 544 self._unlinksocket()
519 545 # don't kill child processes as they have active clients, just wait
520 546 self._reapworkers(0)
521 547
522 548 def run(self):
523 549 try:
524 550 self._mainloop()
525 551 finally:
526 552 self._cleanup()
527 553
528 554 def _mainloop(self):
529 555 exiting = False
530 556 h = self._servicehandler
531 557 selector = selectors.DefaultSelector()
532 558 selector.register(self._sock, selectors.EVENT_READ)
533 559 while True:
534 560 if not exiting and h.shouldexit():
535 561 # clients can no longer connect() to the domain socket, so
536 562 # we stop queuing new requests.
537 563 # for requests that are queued (connect()-ed, but haven't been
538 564 # accept()-ed), handle them before exit. otherwise, clients
539 565 # waiting for recv() will receive ECONNRESET.
540 566 self._unlinksocket()
541 567 exiting = True
542 568 try:
543 569 ready = selector.select(timeout=h.pollinterval)
544 570 except OSError as inst:
545 571 # selectors2 raises ETIMEDOUT if timeout exceeded while
546 572 # handling signal interrupt. That's probably wrong, but
547 573 # we can easily get around it.
548 574 if inst.errno != errno.ETIMEDOUT:
549 575 raise
550 576 ready = []
551 577 if not ready:
552 578 # only exit if we completed all queued requests
553 579 if exiting:
554 580 break
555 581 continue
556 582 try:
557 583 conn, _addr = self._sock.accept()
558 584 except socket.error as inst:
559 585 if inst.args[0] == errno.EINTR:
560 586 continue
561 587 raise
562 588
563 589 pid = os.fork()
564 590 if pid:
565 591 try:
566 592 self.ui.debug('forked worker process (pid=%d)\n' % pid)
567 593 self._workerpids.add(pid)
568 594 h.newconnection()
569 595 finally:
570 596 conn.close() # release handle in parent process
571 597 else:
572 598 try:
573 599 selector.close()
574 600 self._sock.close()
575 601 self._runworker(conn)
576 602 conn.close()
577 603 os._exit(0)
578 604 except: # never return, hence no re-raises
579 605 try:
580 606 self.ui.traceback(force=True)
581 607 finally:
582 608 os._exit(255)
583 609 selector.close()
584 610
585 611 def _sigchldhandler(self, signal, frame):
586 612 self._reapworkers(os.WNOHANG)
587 613
588 614 def _reapworkers(self, options):
589 615 while self._workerpids:
590 616 try:
591 617 pid, _status = os.waitpid(-1, options)
592 618 except OSError as inst:
593 619 if inst.errno == errno.EINTR:
594 620 continue
595 621 if inst.errno != errno.ECHILD:
596 622 raise
597 623 # no child processes at all (reaped by other waitpid()?)
598 624 self._workerpids.clear()
599 625 return
600 626 if pid == 0:
601 627 # no waitable child processes
602 628 return
603 629 self.ui.debug('worker process exited (pid=%d)\n' % pid)
604 630 self._workerpids.discard(pid)
605 631
606 632 def _runworker(self, conn):
607 633 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
608 634 _initworkerprocess()
609 635 h = self._servicehandler
610 636 try:
611 637 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
612 638 finally:
613 639 gc.collect() # trigger __del__ since worker process uses os._exit
@@ -1,212 +1,212
1 1 # server.py - utility and factory of server
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
9 9
10 10 import os
11 11
12 12 from .i18n import _
13 13
14 14 from . import (
15 15 chgserver,
16 16 cmdutil,
17 17 commandserver,
18 18 error,
19 19 hgweb,
20 20 pycompat,
21 21 util,
22 22 )
23 23
24 24 from .utils import (
25 25 procutil,
26 26 )
27 27
28 28 def runservice(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
29 29 runargs=None, appendpid=False):
30 30 '''Run a command as a service.'''
31 31
32 32 postexecargs = {}
33 33
34 34 if opts['daemon_postexec']:
35 35 for inst in opts['daemon_postexec']:
36 36 if inst.startswith('unlink:'):
37 37 postexecargs['unlink'] = inst[7:]
38 38 elif inst.startswith('chdir:'):
39 39 postexecargs['chdir'] = inst[6:]
40 40 elif inst != 'none':
41 41 raise error.Abort(_('invalid value for --daemon-postexec: %s')
42 42 % inst)
43 43
44 44 # When daemonized on Windows, redirect stdout/stderr to the lockfile (which
45 45 # gets cleaned up after the child is up and running), so that the parent can
46 46 # read and print the error if this child dies early. See 594dd384803c. On
47 47 # other platforms, the child can write to the parent's stdio directly, until
48 48 # it is redirected prior to runfn().
49 49 if pycompat.iswindows and opts['daemon_postexec']:
50 50 if 'unlink' in postexecargs and os.path.exists(postexecargs['unlink']):
51 51 procutil.stdout.flush()
52 52 procutil.stderr.flush()
53 53
54 54 fd = os.open(postexecargs['unlink'],
55 55 os.O_WRONLY | os.O_APPEND | os.O_BINARY)
56 56 try:
57 57 os.dup2(fd, procutil.stdout.fileno())
58 58 os.dup2(fd, procutil.stderr.fileno())
59 59 finally:
60 60 os.close(fd)
61 61
62 62 def writepid(pid):
63 63 if opts['pid_file']:
64 64 if appendpid:
65 65 mode = 'ab'
66 66 else:
67 67 mode = 'wb'
68 68 fp = open(opts['pid_file'], mode)
69 69 fp.write('%d\n' % pid)
70 70 fp.close()
71 71
72 72 if opts['daemon'] and not opts['daemon_postexec']:
73 73 # Signal child process startup with file removal
74 74 lockfd, lockpath = pycompat.mkstemp(prefix='hg-service-')
75 75 os.close(lockfd)
76 76 try:
77 77 if not runargs:
78 78 runargs = procutil.hgcmd() + pycompat.sysargv[1:]
79 79 runargs.append('--daemon-postexec=unlink:%s' % lockpath)
80 80 # Don't pass --cwd to the child process, because we've already
81 81 # changed directory.
82 82 for i in pycompat.xrange(1, len(runargs)):
83 83 if runargs[i].startswith('--cwd='):
84 84 del runargs[i]
85 85 break
86 86 elif runargs[i].startswith('--cwd'):
87 87 del runargs[i:i + 2]
88 88 break
89 89 def condfn():
90 90 return not os.path.exists(lockpath)
91 91 pid = procutil.rundetached(runargs, condfn)
92 92 if pid < 0:
93 93 # If the daemonized process managed to write out an error msg,
94 94 # report it.
95 95 if pycompat.iswindows and os.path.exists(lockpath):
96 96 with open(lockpath, 'rb') as log:
97 97 for line in log:
98 98 procutil.stderr.write(line)
99 99 raise error.Abort(_('child process failed to start'))
100 100 writepid(pid)
101 101 finally:
102 102 util.tryunlink(lockpath)
103 103 if parentfn:
104 104 return parentfn(pid)
105 105 else:
106 106 return
107 107
108 108 if initfn:
109 109 initfn()
110 110
111 111 if not opts['daemon']:
112 112 writepid(procutil.getpid())
113 113
114 114 if opts['daemon_postexec']:
115 115 try:
116 116 os.setsid()
117 117 except AttributeError:
118 118 pass
119 119
120 120 if 'chdir' in postexecargs:
121 121 os.chdir(postexecargs['chdir'])
122 122 procutil.hidewindow()
123 123 procutil.stdout.flush()
124 124 procutil.stderr.flush()
125 125
126 126 nullfd = os.open(os.devnull, os.O_RDWR)
127 127 logfilefd = nullfd
128 128 if logfile:
129 129 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND,
130 130 0o666)
131 131 os.dup2(nullfd, procutil.stdin.fileno())
132 132 os.dup2(logfilefd, procutil.stdout.fileno())
133 133 os.dup2(logfilefd, procutil.stderr.fileno())
134 134 stdio = (procutil.stdin.fileno(), procutil.stdout.fileno(),
135 135 procutil.stderr.fileno())
136 136 if nullfd not in stdio:
137 137 os.close(nullfd)
138 138 if logfile and logfilefd not in stdio:
139 139 os.close(logfilefd)
140 140
141 141 # Only unlink after redirecting stdout/stderr, so Windows doesn't
142 142 # complain about a sharing violation.
143 143 if 'unlink' in postexecargs:
144 144 os.unlink(postexecargs['unlink'])
145 145
146 146 if runfn:
147 147 return runfn()
148 148
149 149 _cmdservicemap = {
150 150 'chgunix': chgserver.chgunixservice,
151 151 'pipe': commandserver.pipeservice,
152 152 'unix': commandserver.unixforkingservice,
153 153 }
154 154
155 155 def _createcmdservice(ui, repo, opts):
156 156 mode = opts['cmdserver']
157 157 try:
158 158 servicefn = _cmdservicemap[mode]
159 159 except KeyError:
160 160 raise error.Abort(_('unknown mode %s') % mode)
161 commandserver.setuplogging(ui)
161 commandserver.setuplogging(ui, repo)
162 162 return servicefn(ui, repo, opts)
163 163
164 164 def _createhgwebservice(ui, repo, opts):
165 165 # this way we can check if something was given in the command-line
166 166 if opts.get('port'):
167 167 opts['port'] = util.getport(opts.get('port'))
168 168
169 169 alluis = {ui}
170 170 if repo:
171 171 baseui = repo.baseui
172 172 alluis.update([repo.baseui, repo.ui])
173 173 else:
174 174 baseui = ui
175 175 webconf = opts.get('web_conf') or opts.get('webdir_conf')
176 176 if webconf:
177 177 if opts.get('subrepos'):
178 178 raise error.Abort(_('--web-conf cannot be used with --subrepos'))
179 179
180 180 # load server settings (e.g. web.port) to "copied" ui, which allows
181 181 # hgwebdir to reload webconf cleanly
182 182 servui = ui.copy()
183 183 servui.readconfig(webconf, sections=['web'])
184 184 alluis.add(servui)
185 185 elif opts.get('subrepos'):
186 186 servui = ui
187 187
188 188 # If repo is None, hgweb.createapp() already raises a proper abort
189 189 # message as long as webconf is None.
190 190 if repo:
191 191 webconf = dict()
192 192 cmdutil.addwebdirpath(repo, "", webconf)
193 193 else:
194 194 servui = ui
195 195
196 196 optlist = ("name templates style address port prefix ipv6"
197 197 " accesslog errorlog certificate encoding")
198 198 for o in optlist.split():
199 199 val = opts.get(o, '')
200 200 if val in (None, ''): # should check against default options instead
201 201 continue
202 202 for u in alluis:
203 203 u.setconfig("web", o, val, 'serve')
204 204
205 205 app = hgweb.createapp(baseui, repo, webconf)
206 206 return hgweb.httpservice(servui, app, opts)
207 207
208 208 def createservice(ui, repo, opts):
209 209 if opts["cmdserver"]:
210 210 return _createcmdservice(ui, repo, opts)
211 211 else:
212 212 return _createhgwebservice(ui, repo, opts)
General Comments 0
You need to be logged in to leave comments. Login now