##// END OF EJS Templates
chgserver: change random state after fork...
Jun Wu -
r28770:97c8da2f default
parent child Browse files
Show More
@@ -1,708 +1,712 b''
1 # chgserver.py - command server extension for cHg
1 # chgserver.py - command server extension for cHg
2 #
2 #
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """command server extension for cHg (EXPERIMENTAL)
8 """command server extension for cHg (EXPERIMENTAL)
9
9
10 'S' channel (read/write)
10 'S' channel (read/write)
11 propagate ui.system() request to client
11 propagate ui.system() request to client
12
12
13 'attachio' command
13 'attachio' command
14 attach client's stdio passed by sendmsg()
14 attach client's stdio passed by sendmsg()
15
15
16 'chdir' command
16 'chdir' command
17 change current directory
17 change current directory
18
18
19 'getpager' command
19 'getpager' command
20 checks if pager is enabled and which pager should be executed
20 checks if pager is enabled and which pager should be executed
21
21
22 'setenv' command
22 'setenv' command
23 replace os.environ completely
23 replace os.environ completely
24
24
25 'setumask' command
25 'setumask' command
26 set umask
26 set umask
27
27
28 'validate' command
28 'validate' command
29 reload the config and check if the server is up to date
29 reload the config and check if the server is up to date
30
30
31 Config
31 Config
32 ------
32 ------
33
33
34 ::
34 ::
35
35
36 [chgserver]
36 [chgserver]
37 idletimeout = 3600 # seconds, after which an idle server will exit
37 idletimeout = 3600 # seconds, after which an idle server will exit
38 skiphash = False # whether to skip config or env change checks
38 skiphash = False # whether to skip config or env change checks
39 """
39 """
40
40
41 from __future__ import absolute_import
41 from __future__ import absolute_import
42
42
43 import SocketServer
43 import SocketServer
44 import errno
44 import errno
45 import gc
45 import gc
46 import inspect
46 import inspect
47 import os
47 import os
48 import random
48 import re
49 import re
49 import struct
50 import struct
50 import sys
51 import sys
51 import threading
52 import threading
52 import time
53 import time
53 import traceback
54 import traceback
54
55
55 from mercurial.i18n import _
56 from mercurial.i18n import _
56
57
57 from mercurial import (
58 from mercurial import (
58 cmdutil,
59 cmdutil,
59 commands,
60 commands,
60 commandserver,
61 commandserver,
61 dispatch,
62 dispatch,
62 error,
63 error,
63 extensions,
64 extensions,
64 osutil,
65 osutil,
65 util,
66 util,
66 )
67 )
67
68
68 # Note for extension authors: ONLY specify testedwith = 'internal' for
69 # Note for extension authors: ONLY specify testedwith = 'internal' for
69 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
70 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
70 # be specifying the version(s) of Mercurial they are tested with, or
71 # be specifying the version(s) of Mercurial they are tested with, or
71 # leave the attribute unspecified.
72 # leave the attribute unspecified.
72 testedwith = 'internal'
73 testedwith = 'internal'
73
74
74 _log = commandserver.log
75 _log = commandserver.log
75
76
76 def _hashlist(items):
77 def _hashlist(items):
77 """return sha1 hexdigest for a list"""
78 """return sha1 hexdigest for a list"""
78 return util.sha1(str(items)).hexdigest()
79 return util.sha1(str(items)).hexdigest()
79
80
80 # sensitive config sections affecting confighash
81 # sensitive config sections affecting confighash
81 _configsections = [
82 _configsections = [
82 'extdiff', # uisetup will register new commands
83 'extdiff', # uisetup will register new commands
83 'extensions',
84 'extensions',
84 ]
85 ]
85
86
86 # sensitive environment variables affecting confighash
87 # sensitive environment variables affecting confighash
87 _envre = re.compile(r'''\A(?:
88 _envre = re.compile(r'''\A(?:
88 CHGHG
89 CHGHG
89 |HG.*
90 |HG.*
90 |LANG(?:UAGE)?
91 |LANG(?:UAGE)?
91 |LC_.*
92 |LC_.*
92 |LD_.*
93 |LD_.*
93 |PATH
94 |PATH
94 |PYTHON.*
95 |PYTHON.*
95 |TERM(?:INFO)?
96 |TERM(?:INFO)?
96 |TZ
97 |TZ
97 )\Z''', re.X)
98 )\Z''', re.X)
98
99
99 def _confighash(ui):
100 def _confighash(ui):
100 """return a quick hash for detecting config/env changes
101 """return a quick hash for detecting config/env changes
101
102
102 confighash is the hash of sensitive config items and environment variables.
103 confighash is the hash of sensitive config items and environment variables.
103
104
104 for chgserver, it is designed that once confighash changes, the server is
105 for chgserver, it is designed that once confighash changes, the server is
105 not qualified to serve its client and should redirect the client to a new
106 not qualified to serve its client and should redirect the client to a new
106 server. different from mtimehash, confighash change will not mark the
107 server. different from mtimehash, confighash change will not mark the
107 server outdated and exit since the user can have different configs at the
108 server outdated and exit since the user can have different configs at the
108 same time.
109 same time.
109 """
110 """
110 sectionitems = []
111 sectionitems = []
111 for section in _configsections:
112 for section in _configsections:
112 sectionitems.append(ui.configitems(section))
113 sectionitems.append(ui.configitems(section))
113 sectionhash = _hashlist(sectionitems)
114 sectionhash = _hashlist(sectionitems)
114 envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
115 envitems = [(k, v) for k, v in os.environ.iteritems() if _envre.match(k)]
115 envhash = _hashlist(sorted(envitems))
116 envhash = _hashlist(sorted(envitems))
116 return sectionhash[:6] + envhash[:6]
117 return sectionhash[:6] + envhash[:6]
117
118
118 def _getmtimepaths(ui):
119 def _getmtimepaths(ui):
119 """get a list of paths that should be checked to detect change
120 """get a list of paths that should be checked to detect change
120
121
121 The list will include:
122 The list will include:
122 - extensions (will not cover all files for complex extensions)
123 - extensions (will not cover all files for complex extensions)
123 - mercurial/__version__.py
124 - mercurial/__version__.py
124 - python binary
125 - python binary
125 """
126 """
126 modules = [m for n, m in extensions.extensions(ui)]
127 modules = [m for n, m in extensions.extensions(ui)]
127 try:
128 try:
128 from mercurial import __version__
129 from mercurial import __version__
129 modules.append(__version__)
130 modules.append(__version__)
130 except ImportError:
131 except ImportError:
131 pass
132 pass
132 files = [sys.executable]
133 files = [sys.executable]
133 for m in modules:
134 for m in modules:
134 try:
135 try:
135 files.append(inspect.getabsfile(m))
136 files.append(inspect.getabsfile(m))
136 except TypeError:
137 except TypeError:
137 pass
138 pass
138 return sorted(set(files))
139 return sorted(set(files))
139
140
140 def _mtimehash(paths):
141 def _mtimehash(paths):
141 """return a quick hash for detecting file changes
142 """return a quick hash for detecting file changes
142
143
143 mtimehash calls stat on given paths and calculate a hash based on size and
144 mtimehash calls stat on given paths and calculate a hash based on size and
144 mtime of each file. mtimehash does not read file content because reading is
145 mtime of each file. mtimehash does not read file content because reading is
145 expensive. therefore it's not 100% reliable for detecting content changes.
146 expensive. therefore it's not 100% reliable for detecting content changes.
146 it's possible to return different hashes for same file contents.
147 it's possible to return different hashes for same file contents.
147 it's also possible to return a same hash for different file contents for
148 it's also possible to return a same hash for different file contents for
148 some carefully crafted situation.
149 some carefully crafted situation.
149
150
150 for chgserver, it is designed that once mtimehash changes, the server is
151 for chgserver, it is designed that once mtimehash changes, the server is
151 considered outdated immediately and should no longer provide service.
152 considered outdated immediately and should no longer provide service.
152 """
153 """
153 def trystat(path):
154 def trystat(path):
154 try:
155 try:
155 st = os.stat(path)
156 st = os.stat(path)
156 return (st.st_mtime, st.st_size)
157 return (st.st_mtime, st.st_size)
157 except OSError:
158 except OSError:
158 # could be ENOENT, EPERM etc. not fatal in any case
159 # could be ENOENT, EPERM etc. not fatal in any case
159 pass
160 pass
160 return _hashlist(map(trystat, paths))[:12]
161 return _hashlist(map(trystat, paths))[:12]
161
162
162 class hashstate(object):
163 class hashstate(object):
163 """a structure storing confighash, mtimehash, paths used for mtimehash"""
164 """a structure storing confighash, mtimehash, paths used for mtimehash"""
164 def __init__(self, confighash, mtimehash, mtimepaths):
165 def __init__(self, confighash, mtimehash, mtimepaths):
165 self.confighash = confighash
166 self.confighash = confighash
166 self.mtimehash = mtimehash
167 self.mtimehash = mtimehash
167 self.mtimepaths = mtimepaths
168 self.mtimepaths = mtimepaths
168
169
169 @staticmethod
170 @staticmethod
170 def fromui(ui, mtimepaths=None):
171 def fromui(ui, mtimepaths=None):
171 if mtimepaths is None:
172 if mtimepaths is None:
172 mtimepaths = _getmtimepaths(ui)
173 mtimepaths = _getmtimepaths(ui)
173 confighash = _confighash(ui)
174 confighash = _confighash(ui)
174 mtimehash = _mtimehash(mtimepaths)
175 mtimehash = _mtimehash(mtimepaths)
175 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
176 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
176 return hashstate(confighash, mtimehash, mtimepaths)
177 return hashstate(confighash, mtimehash, mtimepaths)
177
178
178 # copied from hgext/pager.py:uisetup()
179 # copied from hgext/pager.py:uisetup()
179 def _setuppagercmd(ui, options, cmd):
180 def _setuppagercmd(ui, options, cmd):
180 if not ui.formatted():
181 if not ui.formatted():
181 return
182 return
182
183
183 p = ui.config("pager", "pager", os.environ.get("PAGER"))
184 p = ui.config("pager", "pager", os.environ.get("PAGER"))
184 usepager = False
185 usepager = False
185 always = util.parsebool(options['pager'])
186 always = util.parsebool(options['pager'])
186 auto = options['pager'] == 'auto'
187 auto = options['pager'] == 'auto'
187
188
188 if not p:
189 if not p:
189 pass
190 pass
190 elif always:
191 elif always:
191 usepager = True
192 usepager = True
192 elif not auto:
193 elif not auto:
193 usepager = False
194 usepager = False
194 else:
195 else:
195 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
196 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
196 attend = ui.configlist('pager', 'attend', attended)
197 attend = ui.configlist('pager', 'attend', attended)
197 ignore = ui.configlist('pager', 'ignore')
198 ignore = ui.configlist('pager', 'ignore')
198 cmds, _ = cmdutil.findcmd(cmd, commands.table)
199 cmds, _ = cmdutil.findcmd(cmd, commands.table)
199
200
200 for cmd in cmds:
201 for cmd in cmds:
201 var = 'attend-%s' % cmd
202 var = 'attend-%s' % cmd
202 if ui.config('pager', var):
203 if ui.config('pager', var):
203 usepager = ui.configbool('pager', var)
204 usepager = ui.configbool('pager', var)
204 break
205 break
205 if (cmd in attend or
206 if (cmd in attend or
206 (cmd not in ignore and not attend)):
207 (cmd not in ignore and not attend)):
207 usepager = True
208 usepager = True
208 break
209 break
209
210
210 if usepager:
211 if usepager:
211 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
212 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
212 ui.setconfig('ui', 'interactive', False, 'pager')
213 ui.setconfig('ui', 'interactive', False, 'pager')
213 return p
214 return p
214
215
215 _envvarre = re.compile(r'\$[a-zA-Z_]+')
216 _envvarre = re.compile(r'\$[a-zA-Z_]+')
216
217
217 def _clearenvaliases(cmdtable):
218 def _clearenvaliases(cmdtable):
218 """Remove stale command aliases referencing env vars; variable expansion
219 """Remove stale command aliases referencing env vars; variable expansion
219 is done at dispatch.addaliases()"""
220 is done at dispatch.addaliases()"""
220 for name, tab in cmdtable.items():
221 for name, tab in cmdtable.items():
221 cmddef = tab[0]
222 cmddef = tab[0]
222 if (isinstance(cmddef, dispatch.cmdalias) and
223 if (isinstance(cmddef, dispatch.cmdalias) and
223 not cmddef.definition.startswith('!') and # shell alias
224 not cmddef.definition.startswith('!') and # shell alias
224 _envvarre.search(cmddef.definition)):
225 _envvarre.search(cmddef.definition)):
225 del cmdtable[name]
226 del cmdtable[name]
226
227
227 def _newchgui(srcui, csystem):
228 def _newchgui(srcui, csystem):
228 class chgui(srcui.__class__):
229 class chgui(srcui.__class__):
229 def __init__(self, src=None):
230 def __init__(self, src=None):
230 super(chgui, self).__init__(src)
231 super(chgui, self).__init__(src)
231 if src:
232 if src:
232 self._csystem = getattr(src, '_csystem', csystem)
233 self._csystem = getattr(src, '_csystem', csystem)
233 else:
234 else:
234 self._csystem = csystem
235 self._csystem = csystem
235
236
236 def system(self, cmd, environ=None, cwd=None, onerr=None,
237 def system(self, cmd, environ=None, cwd=None, onerr=None,
237 errprefix=None):
238 errprefix=None):
238 # fallback to the original system method if the output needs to be
239 # fallback to the original system method if the output needs to be
239 # captured (to self._buffers), or the output stream is not stdout
240 # captured (to self._buffers), or the output stream is not stdout
240 # (e.g. stderr, cStringIO), because the chg client is not aware of
241 # (e.g. stderr, cStringIO), because the chg client is not aware of
241 # these situations and will behave differently (write to stdout).
242 # these situations and will behave differently (write to stdout).
242 if (any(s[1] for s in self._bufferstates)
243 if (any(s[1] for s in self._bufferstates)
243 or not util.safehasattr(self.fout, 'fileno')
244 or not util.safehasattr(self.fout, 'fileno')
244 or self.fout.fileno() != sys.stdout.fileno()):
245 or self.fout.fileno() != sys.stdout.fileno()):
245 return super(chgui, self).system(cmd, environ, cwd, onerr,
246 return super(chgui, self).system(cmd, environ, cwd, onerr,
246 errprefix)
247 errprefix)
247 # copied from mercurial/util.py:system()
248 # copied from mercurial/util.py:system()
248 self.flush()
249 self.flush()
249 def py2shell(val):
250 def py2shell(val):
250 if val is None or val is False:
251 if val is None or val is False:
251 return '0'
252 return '0'
252 if val is True:
253 if val is True:
253 return '1'
254 return '1'
254 return str(val)
255 return str(val)
255 env = os.environ.copy()
256 env = os.environ.copy()
256 if environ:
257 if environ:
257 env.update((k, py2shell(v)) for k, v in environ.iteritems())
258 env.update((k, py2shell(v)) for k, v in environ.iteritems())
258 env['HG'] = util.hgexecutable()
259 env['HG'] = util.hgexecutable()
259 rc = self._csystem(cmd, env, cwd)
260 rc = self._csystem(cmd, env, cwd)
260 if rc and onerr:
261 if rc and onerr:
261 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
262 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
262 util.explainexit(rc)[0])
263 util.explainexit(rc)[0])
263 if errprefix:
264 if errprefix:
264 errmsg = '%s: %s' % (errprefix, errmsg)
265 errmsg = '%s: %s' % (errprefix, errmsg)
265 raise onerr(errmsg)
266 raise onerr(errmsg)
266 return rc
267 return rc
267
268
268 return chgui(srcui)
269 return chgui(srcui)
269
270
270 def _loadnewui(srcui, args):
271 def _loadnewui(srcui, args):
271 newui = srcui.__class__()
272 newui = srcui.__class__()
272 for a in ['fin', 'fout', 'ferr', 'environ']:
273 for a in ['fin', 'fout', 'ferr', 'environ']:
273 setattr(newui, a, getattr(srcui, a))
274 setattr(newui, a, getattr(srcui, a))
274 if util.safehasattr(srcui, '_csystem'):
275 if util.safehasattr(srcui, '_csystem'):
275 newui._csystem = srcui._csystem
276 newui._csystem = srcui._csystem
276
277
277 # internal config: extensions.chgserver
278 # internal config: extensions.chgserver
278 newui.setconfig('extensions', 'chgserver',
279 newui.setconfig('extensions', 'chgserver',
279 srcui.config('extensions', 'chgserver'), '--config')
280 srcui.config('extensions', 'chgserver'), '--config')
280
281
281 # command line args
282 # command line args
282 args = args[:]
283 args = args[:]
283 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
284 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
284
285
285 # stolen from tortoisehg.util.copydynamicconfig()
286 # stolen from tortoisehg.util.copydynamicconfig()
286 for section, name, value in srcui.walkconfig():
287 for section, name, value in srcui.walkconfig():
287 source = srcui.configsource(section, name)
288 source = srcui.configsource(section, name)
288 if ':' in source or source == '--config':
289 if ':' in source or source == '--config':
289 # path:line or command line
290 # path:line or command line
290 continue
291 continue
291 if source == 'none':
292 if source == 'none':
292 # ui.configsource returns 'none' by default
293 # ui.configsource returns 'none' by default
293 source = ''
294 source = ''
294 newui.setconfig(section, name, value, source)
295 newui.setconfig(section, name, value, source)
295
296
296 # load wd and repo config, copied from dispatch.py
297 # load wd and repo config, copied from dispatch.py
297 cwds = dispatch._earlygetopt(['--cwd'], args)
298 cwds = dispatch._earlygetopt(['--cwd'], args)
298 cwd = cwds and os.path.realpath(cwds[-1]) or None
299 cwd = cwds and os.path.realpath(cwds[-1]) or None
299 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
300 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
300 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
301 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
301
302
302 return (newui, newlui)
303 return (newui, newlui)
303
304
304 class channeledsystem(object):
305 class channeledsystem(object):
305 """Propagate ui.system() request in the following format:
306 """Propagate ui.system() request in the following format:
306
307
307 payload length (unsigned int),
308 payload length (unsigned int),
308 cmd, '\0',
309 cmd, '\0',
309 cwd, '\0',
310 cwd, '\0',
310 envkey, '=', val, '\0',
311 envkey, '=', val, '\0',
311 ...
312 ...
312 envkey, '=', val
313 envkey, '=', val
313
314
314 and waits:
315 and waits:
315
316
316 exitcode length (unsigned int),
317 exitcode length (unsigned int),
317 exitcode (int)
318 exitcode (int)
318 """
319 """
319 def __init__(self, in_, out, channel):
320 def __init__(self, in_, out, channel):
320 self.in_ = in_
321 self.in_ = in_
321 self.out = out
322 self.out = out
322 self.channel = channel
323 self.channel = channel
323
324
324 def __call__(self, cmd, environ, cwd):
325 def __call__(self, cmd, environ, cwd):
325 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
326 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
326 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
327 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
327 data = '\0'.join(args)
328 data = '\0'.join(args)
328 self.out.write(struct.pack('>cI', self.channel, len(data)))
329 self.out.write(struct.pack('>cI', self.channel, len(data)))
329 self.out.write(data)
330 self.out.write(data)
330 self.out.flush()
331 self.out.flush()
331
332
332 length = self.in_.read(4)
333 length = self.in_.read(4)
333 length, = struct.unpack('>I', length)
334 length, = struct.unpack('>I', length)
334 if length != 4:
335 if length != 4:
335 raise error.Abort(_('invalid response'))
336 raise error.Abort(_('invalid response'))
336 rc, = struct.unpack('>i', self.in_.read(4))
337 rc, = struct.unpack('>i', self.in_.read(4))
337 return rc
338 return rc
338
339
339 _iochannels = [
340 _iochannels = [
340 # server.ch, ui.fp, mode
341 # server.ch, ui.fp, mode
341 ('cin', 'fin', 'rb'),
342 ('cin', 'fin', 'rb'),
342 ('cout', 'fout', 'wb'),
343 ('cout', 'fout', 'wb'),
343 ('cerr', 'ferr', 'wb'),
344 ('cerr', 'ferr', 'wb'),
344 ]
345 ]
345
346
346 class chgcmdserver(commandserver.server):
347 class chgcmdserver(commandserver.server):
347 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
348 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
348 super(chgcmdserver, self).__init__(
349 super(chgcmdserver, self).__init__(
349 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
350 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
350 self.clientsock = sock
351 self.clientsock = sock
351 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
352 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
352 self.hashstate = hashstate
353 self.hashstate = hashstate
353 self.baseaddress = baseaddress
354 self.baseaddress = baseaddress
354 if hashstate is not None:
355 if hashstate is not None:
355 self.capabilities = self.capabilities.copy()
356 self.capabilities = self.capabilities.copy()
356 self.capabilities['validate'] = chgcmdserver.validate
357 self.capabilities['validate'] = chgcmdserver.validate
357
358
358 def cleanup(self):
359 def cleanup(self):
359 # dispatch._runcatch() does not flush outputs if exception is not
360 # dispatch._runcatch() does not flush outputs if exception is not
360 # handled by dispatch._dispatch()
361 # handled by dispatch._dispatch()
361 self.ui.flush()
362 self.ui.flush()
362 self._restoreio()
363 self._restoreio()
363
364
364 def attachio(self):
365 def attachio(self):
365 """Attach to client's stdio passed via unix domain socket; all
366 """Attach to client's stdio passed via unix domain socket; all
366 channels except cresult will no longer be used
367 channels except cresult will no longer be used
367 """
368 """
368 # tell client to sendmsg() with 1-byte payload, which makes it
369 # tell client to sendmsg() with 1-byte payload, which makes it
369 # distinctive from "attachio\n" command consumed by client.read()
370 # distinctive from "attachio\n" command consumed by client.read()
370 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
371 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
371 clientfds = osutil.recvfds(self.clientsock.fileno())
372 clientfds = osutil.recvfds(self.clientsock.fileno())
372 _log('received fds: %r\n' % clientfds)
373 _log('received fds: %r\n' % clientfds)
373
374
374 ui = self.ui
375 ui = self.ui
375 ui.flush()
376 ui.flush()
376 first = self._saveio()
377 first = self._saveio()
377 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
378 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
378 assert fd > 0
379 assert fd > 0
379 fp = getattr(ui, fn)
380 fp = getattr(ui, fn)
380 os.dup2(fd, fp.fileno())
381 os.dup2(fd, fp.fileno())
381 os.close(fd)
382 os.close(fd)
382 if not first:
383 if not first:
383 continue
384 continue
384 # reset buffering mode when client is first attached. as we want
385 # reset buffering mode when client is first attached. as we want
385 # to see output immediately on pager, the mode stays unchanged
386 # to see output immediately on pager, the mode stays unchanged
386 # when client re-attached. ferr is unchanged because it should
387 # when client re-attached. ferr is unchanged because it should
387 # be unbuffered no matter if it is a tty or not.
388 # be unbuffered no matter if it is a tty or not.
388 if fn == 'ferr':
389 if fn == 'ferr':
389 newfp = fp
390 newfp = fp
390 else:
391 else:
391 # make it line buffered explicitly because the default is
392 # make it line buffered explicitly because the default is
392 # decided on first write(), where fout could be a pager.
393 # decided on first write(), where fout could be a pager.
393 if fp.isatty():
394 if fp.isatty():
394 bufsize = 1 # line buffered
395 bufsize = 1 # line buffered
395 else:
396 else:
396 bufsize = -1 # system default
397 bufsize = -1 # system default
397 newfp = os.fdopen(fp.fileno(), mode, bufsize)
398 newfp = os.fdopen(fp.fileno(), mode, bufsize)
398 setattr(ui, fn, newfp)
399 setattr(ui, fn, newfp)
399 setattr(self, cn, newfp)
400 setattr(self, cn, newfp)
400
401
401 self.cresult.write(struct.pack('>i', len(clientfds)))
402 self.cresult.write(struct.pack('>i', len(clientfds)))
402
403
403 def _saveio(self):
404 def _saveio(self):
404 if self._oldios:
405 if self._oldios:
405 return False
406 return False
406 ui = self.ui
407 ui = self.ui
407 for cn, fn, _mode in _iochannels:
408 for cn, fn, _mode in _iochannels:
408 ch = getattr(self, cn)
409 ch = getattr(self, cn)
409 fp = getattr(ui, fn)
410 fp = getattr(ui, fn)
410 fd = os.dup(fp.fileno())
411 fd = os.dup(fp.fileno())
411 self._oldios.append((ch, fp, fd))
412 self._oldios.append((ch, fp, fd))
412 return True
413 return True
413
414
414 def _restoreio(self):
415 def _restoreio(self):
415 ui = self.ui
416 ui = self.ui
416 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
417 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
417 newfp = getattr(ui, fn)
418 newfp = getattr(ui, fn)
418 # close newfp while it's associated with client; otherwise it
419 # close newfp while it's associated with client; otherwise it
419 # would be closed when newfp is deleted
420 # would be closed when newfp is deleted
420 if newfp is not fp:
421 if newfp is not fp:
421 newfp.close()
422 newfp.close()
422 # restore original fd: fp is open again
423 # restore original fd: fp is open again
423 os.dup2(fd, fp.fileno())
424 os.dup2(fd, fp.fileno())
424 os.close(fd)
425 os.close(fd)
425 setattr(self, cn, ch)
426 setattr(self, cn, ch)
426 setattr(ui, fn, fp)
427 setattr(ui, fn, fp)
427 del self._oldios[:]
428 del self._oldios[:]
428
429
429 def validate(self):
430 def validate(self):
430 """Reload the config and check if the server is up to date
431 """Reload the config and check if the server is up to date
431
432
432 Read a list of '\0' separated arguments.
433 Read a list of '\0' separated arguments.
433 Write a non-empty list of '\0' separated instruction strings or '\0'
434 Write a non-empty list of '\0' separated instruction strings or '\0'
434 if the list is empty.
435 if the list is empty.
435 An instruction string could be either:
436 An instruction string could be either:
436 - "unlink $path", the client should unlink the path to stop the
437 - "unlink $path", the client should unlink the path to stop the
437 outdated server.
438 outdated server.
438 - "redirect $path", the client should attempt to connect to $path
439 - "redirect $path", the client should attempt to connect to $path
439 first. If it does not work, start a new server. It implies
440 first. If it does not work, start a new server. It implies
440 "reconnect".
441 "reconnect".
441 - "exit $n", the client should exit directly with code n.
442 - "exit $n", the client should exit directly with code n.
442 This may happen if we cannot parse the config.
443 This may happen if we cannot parse the config.
443 - "reconnect", the client should close the connection and
444 - "reconnect", the client should close the connection and
444 reconnect.
445 reconnect.
445 If neither "reconnect" nor "redirect" is included in the instruction
446 If neither "reconnect" nor "redirect" is included in the instruction
446 list, the client can continue with this server after completing all
447 list, the client can continue with this server after completing all
447 the instructions.
448 the instructions.
448 """
449 """
449 args = self._readlist()
450 args = self._readlist()
450 try:
451 try:
451 self.ui, lui = _loadnewui(self.ui, args)
452 self.ui, lui = _loadnewui(self.ui, args)
452 except error.ParseError as inst:
453 except error.ParseError as inst:
453 dispatch._formatparse(self.ui.warn, inst)
454 dispatch._formatparse(self.ui.warn, inst)
454 self.ui.flush()
455 self.ui.flush()
455 self.cresult.write('exit 255')
456 self.cresult.write('exit 255')
456 return
457 return
457 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
458 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
458 insts = []
459 insts = []
459 if newhash.mtimehash != self.hashstate.mtimehash:
460 if newhash.mtimehash != self.hashstate.mtimehash:
460 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
461 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
461 insts.append('unlink %s' % addr)
462 insts.append('unlink %s' % addr)
462 # mtimehash is empty if one or more extensions fail to load.
463 # mtimehash is empty if one or more extensions fail to load.
463 # to be compatible with hg, still serve the client this time.
464 # to be compatible with hg, still serve the client this time.
464 if self.hashstate.mtimehash:
465 if self.hashstate.mtimehash:
465 insts.append('reconnect')
466 insts.append('reconnect')
466 if newhash.confighash != self.hashstate.confighash:
467 if newhash.confighash != self.hashstate.confighash:
467 addr = _hashaddress(self.baseaddress, newhash.confighash)
468 addr = _hashaddress(self.baseaddress, newhash.confighash)
468 insts.append('redirect %s' % addr)
469 insts.append('redirect %s' % addr)
469 _log('validate: %s\n' % insts)
470 _log('validate: %s\n' % insts)
470 self.cresult.write('\0'.join(insts) or '\0')
471 self.cresult.write('\0'.join(insts) or '\0')
471
472
472 def chdir(self):
473 def chdir(self):
473 """Change current directory
474 """Change current directory
474
475
475 Note that the behavior of --cwd option is bit different from this.
476 Note that the behavior of --cwd option is bit different from this.
476 It does not affect --config parameter.
477 It does not affect --config parameter.
477 """
478 """
478 path = self._readstr()
479 path = self._readstr()
479 if not path:
480 if not path:
480 return
481 return
481 _log('chdir to %r\n' % path)
482 _log('chdir to %r\n' % path)
482 os.chdir(path)
483 os.chdir(path)
483
484
484 def setumask(self):
485 def setumask(self):
485 """Change umask"""
486 """Change umask"""
486 mask = struct.unpack('>I', self._read(4))[0]
487 mask = struct.unpack('>I', self._read(4))[0]
487 _log('setumask %r\n' % mask)
488 _log('setumask %r\n' % mask)
488 os.umask(mask)
489 os.umask(mask)
489
490
490 def getpager(self):
491 def getpager(self):
491 """Read cmdargs and write pager command to r-channel if enabled
492 """Read cmdargs and write pager command to r-channel if enabled
492
493
493 If pager isn't enabled, this writes '\0' because channeledoutput
494 If pager isn't enabled, this writes '\0' because channeledoutput
494 does not allow to write empty data.
495 does not allow to write empty data.
495 """
496 """
496 args = self._readlist()
497 args = self._readlist()
497 try:
498 try:
498 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
499 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
499 args)
500 args)
500 except (error.Abort, error.AmbiguousCommand, error.CommandError,
501 except (error.Abort, error.AmbiguousCommand, error.CommandError,
501 error.UnknownCommand):
502 error.UnknownCommand):
502 cmd = None
503 cmd = None
503 options = {}
504 options = {}
504 if not cmd or 'pager' not in options:
505 if not cmd or 'pager' not in options:
505 self.cresult.write('\0')
506 self.cresult.write('\0')
506 return
507 return
507
508
508 pagercmd = _setuppagercmd(self.ui, options, cmd)
509 pagercmd = _setuppagercmd(self.ui, options, cmd)
509 if pagercmd:
510 if pagercmd:
510 self.cresult.write(pagercmd)
511 self.cresult.write(pagercmd)
511 else:
512 else:
512 self.cresult.write('\0')
513 self.cresult.write('\0')
513
514
514 def setenv(self):
515 def setenv(self):
515 """Clear and update os.environ
516 """Clear and update os.environ
516
517
517 Note that not all variables can make an effect on the running process.
518 Note that not all variables can make an effect on the running process.
518 """
519 """
519 l = self._readlist()
520 l = self._readlist()
520 try:
521 try:
521 newenv = dict(s.split('=', 1) for s in l)
522 newenv = dict(s.split('=', 1) for s in l)
522 except ValueError:
523 except ValueError:
523 raise ValueError('unexpected value in setenv request')
524 raise ValueError('unexpected value in setenv request')
524 _log('setenv: %r\n' % sorted(newenv.keys()))
525 _log('setenv: %r\n' % sorted(newenv.keys()))
525 os.environ.clear()
526 os.environ.clear()
526 os.environ.update(newenv)
527 os.environ.update(newenv)
527 _clearenvaliases(commands.table)
528 _clearenvaliases(commands.table)
528
529
529 capabilities = commandserver.server.capabilities.copy()
530 capabilities = commandserver.server.capabilities.copy()
530 capabilities.update({'attachio': attachio,
531 capabilities.update({'attachio': attachio,
531 'chdir': chdir,
532 'chdir': chdir,
532 'getpager': getpager,
533 'getpager': getpager,
533 'setenv': setenv,
534 'setenv': setenv,
534 'setumask': setumask})
535 'setumask': setumask})
535
536
536 # copied from mercurial/commandserver.py
537 # copied from mercurial/commandserver.py
537 class _requesthandler(SocketServer.StreamRequestHandler):
538 class _requesthandler(SocketServer.StreamRequestHandler):
538 def handle(self):
539 def handle(self):
539 # use a different process group from the master process, making this
540 # use a different process group from the master process, making this
540 # process pass kernel "is_current_pgrp_orphaned" check so signals like
541 # process pass kernel "is_current_pgrp_orphaned" check so signals like
541 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
542 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
542 os.setpgid(0, 0)
543 os.setpgid(0, 0)
544 # change random state otherwise forked request handlers would have a
545 # same state inherited from parent.
546 random.seed()
543 ui = self.server.ui
547 ui = self.server.ui
544 repo = self.server.repo
548 repo = self.server.repo
545 sv = None
549 sv = None
546 try:
550 try:
547 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
551 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
548 self.server.hashstate, self.server.baseaddress)
552 self.server.hashstate, self.server.baseaddress)
549 try:
553 try:
550 sv.serve()
554 sv.serve()
551 # handle exceptions that may be raised by command server. most of
555 # handle exceptions that may be raised by command server. most of
552 # known exceptions are caught by dispatch.
556 # known exceptions are caught by dispatch.
553 except error.Abort as inst:
557 except error.Abort as inst:
554 ui.warn(_('abort: %s\n') % inst)
558 ui.warn(_('abort: %s\n') % inst)
555 except IOError as inst:
559 except IOError as inst:
556 if inst.errno != errno.EPIPE:
560 if inst.errno != errno.EPIPE:
557 raise
561 raise
558 except KeyboardInterrupt:
562 except KeyboardInterrupt:
559 pass
563 pass
560 finally:
564 finally:
561 sv.cleanup()
565 sv.cleanup()
562 except: # re-raises
566 except: # re-raises
563 # also write traceback to error channel. otherwise client cannot
567 # also write traceback to error channel. otherwise client cannot
564 # see it because it is written to server's stderr by default.
568 # see it because it is written to server's stderr by default.
565 if sv:
569 if sv:
566 cerr = sv.cerr
570 cerr = sv.cerr
567 else:
571 else:
568 cerr = commandserver.channeledoutput(self.wfile, 'e')
572 cerr = commandserver.channeledoutput(self.wfile, 'e')
569 traceback.print_exc(file=cerr)
573 traceback.print_exc(file=cerr)
570 raise
574 raise
571 finally:
575 finally:
572 # trigger __del__ since ForkingMixIn uses os._exit
576 # trigger __del__ since ForkingMixIn uses os._exit
573 gc.collect()
577 gc.collect()
574
578
575 def _tempaddress(address):
579 def _tempaddress(address):
576 return '%s.%d.tmp' % (address, os.getpid())
580 return '%s.%d.tmp' % (address, os.getpid())
577
581
578 def _hashaddress(address, hashstr):
582 def _hashaddress(address, hashstr):
579 return '%s-%s' % (address, hashstr)
583 return '%s-%s' % (address, hashstr)
580
584
581 class AutoExitMixIn: # use old-style to comply with SocketServer design
585 class AutoExitMixIn: # use old-style to comply with SocketServer design
582 lastactive = time.time()
586 lastactive = time.time()
583 idletimeout = 3600 # default 1 hour
587 idletimeout = 3600 # default 1 hour
584
588
585 def startautoexitthread(self):
589 def startautoexitthread(self):
586 # note: the auto-exit check here is cheap enough to not use a thread,
590 # note: the auto-exit check here is cheap enough to not use a thread,
587 # be done in serve_forever. however SocketServer is hook-unfriendly,
591 # be done in serve_forever. however SocketServer is hook-unfriendly,
588 # you simply cannot hook serve_forever without copying a lot of code.
592 # you simply cannot hook serve_forever without copying a lot of code.
589 # besides, serve_forever's docstring suggests using thread.
593 # besides, serve_forever's docstring suggests using thread.
590 thread = threading.Thread(target=self._autoexitloop)
594 thread = threading.Thread(target=self._autoexitloop)
591 thread.daemon = True
595 thread.daemon = True
592 thread.start()
596 thread.start()
593
597
594 def _autoexitloop(self, interval=1):
598 def _autoexitloop(self, interval=1):
595 while True:
599 while True:
596 time.sleep(interval)
600 time.sleep(interval)
597 if not self.issocketowner():
601 if not self.issocketowner():
598 _log('%s is not owned, exiting.\n' % self.server_address)
602 _log('%s is not owned, exiting.\n' % self.server_address)
599 break
603 break
600 if time.time() - self.lastactive > self.idletimeout:
604 if time.time() - self.lastactive > self.idletimeout:
601 _log('being idle too long. exiting.\n')
605 _log('being idle too long. exiting.\n')
602 break
606 break
603 self.shutdown()
607 self.shutdown()
604
608
605 def process_request(self, request, address):
609 def process_request(self, request, address):
606 self.lastactive = time.time()
610 self.lastactive = time.time()
607 return SocketServer.ForkingMixIn.process_request(
611 return SocketServer.ForkingMixIn.process_request(
608 self, request, address)
612 self, request, address)
609
613
610 def server_bind(self):
614 def server_bind(self):
611 # use a unique temp address so we can stat the file and do ownership
615 # use a unique temp address so we can stat the file and do ownership
612 # check later
616 # check later
613 tempaddress = _tempaddress(self.server_address)
617 tempaddress = _tempaddress(self.server_address)
614 # use relative path instead of full path at bind() if possible, since
618 # use relative path instead of full path at bind() if possible, since
615 # AF_UNIX path has very small length limit (107 chars) on common
619 # AF_UNIX path has very small length limit (107 chars) on common
616 # platforms (see sys/un.h)
620 # platforms (see sys/un.h)
617 dirname, basename = os.path.split(tempaddress)
621 dirname, basename = os.path.split(tempaddress)
618 bakwdfd = None
622 bakwdfd = None
619 if dirname:
623 if dirname:
620 bakwdfd = os.open('.', os.O_DIRECTORY)
624 bakwdfd = os.open('.', os.O_DIRECTORY)
621 os.chdir(dirname)
625 os.chdir(dirname)
622 self.socket.bind(basename)
626 self.socket.bind(basename)
623 self._socketstat = os.stat(basename)
627 self._socketstat = os.stat(basename)
624 # rename will replace the old socket file if exists atomically. the
628 # rename will replace the old socket file if exists atomically. the
625 # old server will detect ownership change and exit.
629 # old server will detect ownership change and exit.
626 util.rename(basename, self.server_address)
630 util.rename(basename, self.server_address)
627 if bakwdfd:
631 if bakwdfd:
628 os.fchdir(bakwdfd)
632 os.fchdir(bakwdfd)
629 os.close(bakwdfd)
633 os.close(bakwdfd)
630
634
631 def issocketowner(self):
635 def issocketowner(self):
632 try:
636 try:
633 stat = os.stat(self.server_address)
637 stat = os.stat(self.server_address)
634 return (stat.st_ino == self._socketstat.st_ino and
638 return (stat.st_ino == self._socketstat.st_ino and
635 stat.st_mtime == self._socketstat.st_mtime)
639 stat.st_mtime == self._socketstat.st_mtime)
636 except OSError:
640 except OSError:
637 return False
641 return False
638
642
639 def unlinksocketfile(self):
643 def unlinksocketfile(self):
640 if not self.issocketowner():
644 if not self.issocketowner():
641 return
645 return
642 # it is possible to have a race condition here that we may
646 # it is possible to have a race condition here that we may
643 # remove another server's socket file. but that's okay
647 # remove another server's socket file. but that's okay
644 # since that server will detect and exit automatically and
648 # since that server will detect and exit automatically and
645 # the client will start a new server on demand.
649 # the client will start a new server on demand.
646 try:
650 try:
647 os.unlink(self.server_address)
651 os.unlink(self.server_address)
648 except OSError as exc:
652 except OSError as exc:
649 if exc.errno != errno.ENOENT:
653 if exc.errno != errno.ENOENT:
650 raise
654 raise
651
655
652 class chgunixservice(commandserver.unixservice):
656 class chgunixservice(commandserver.unixservice):
653 def init(self):
657 def init(self):
654 if self.repo:
658 if self.repo:
655 # one chgserver can serve multiple repos. drop repo infomation
659 # one chgserver can serve multiple repos. drop repo infomation
656 self.ui.setconfig('bundle', 'mainreporoot', '', 'repo')
660 self.ui.setconfig('bundle', 'mainreporoot', '', 'repo')
657 self.repo = None
661 self.repo = None
658 self._inithashstate()
662 self._inithashstate()
659 self._checkextensions()
663 self._checkextensions()
660 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
664 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
661 SocketServer.UnixStreamServer):
665 SocketServer.UnixStreamServer):
662 ui = self.ui
666 ui = self.ui
663 repo = self.repo
667 repo = self.repo
664 hashstate = self.hashstate
668 hashstate = self.hashstate
665 baseaddress = self.baseaddress
669 baseaddress = self.baseaddress
666 self.server = cls(self.address, _requesthandler)
670 self.server = cls(self.address, _requesthandler)
667 self.server.idletimeout = self.ui.configint(
671 self.server.idletimeout = self.ui.configint(
668 'chgserver', 'idletimeout', self.server.idletimeout)
672 'chgserver', 'idletimeout', self.server.idletimeout)
669 self.server.startautoexitthread()
673 self.server.startautoexitthread()
670 self._createsymlink()
674 self._createsymlink()
671
675
672 def _inithashstate(self):
676 def _inithashstate(self):
673 self.baseaddress = self.address
677 self.baseaddress = self.address
674 if self.ui.configbool('chgserver', 'skiphash', False):
678 if self.ui.configbool('chgserver', 'skiphash', False):
675 self.hashstate = None
679 self.hashstate = None
676 return
680 return
677 self.hashstate = hashstate.fromui(self.ui)
681 self.hashstate = hashstate.fromui(self.ui)
678 self.address = _hashaddress(self.address, self.hashstate.confighash)
682 self.address = _hashaddress(self.address, self.hashstate.confighash)
679
683
680 def _checkextensions(self):
684 def _checkextensions(self):
681 if not self.hashstate:
685 if not self.hashstate:
682 return
686 return
683 if extensions.notloaded():
687 if extensions.notloaded():
684 # one or more extensions failed to load. mtimehash becomes
688 # one or more extensions failed to load. mtimehash becomes
685 # meaningless because we do not know the paths of those extensions.
689 # meaningless because we do not know the paths of those extensions.
686 # set mtimehash to an illegal hash value to invalidate the server.
690 # set mtimehash to an illegal hash value to invalidate the server.
687 self.hashstate.mtimehash = ''
691 self.hashstate.mtimehash = ''
688
692
689 def _createsymlink(self):
693 def _createsymlink(self):
690 if self.baseaddress == self.address:
694 if self.baseaddress == self.address:
691 return
695 return
692 tempaddress = _tempaddress(self.baseaddress)
696 tempaddress = _tempaddress(self.baseaddress)
693 os.symlink(os.path.basename(self.address), tempaddress)
697 os.symlink(os.path.basename(self.address), tempaddress)
694 util.rename(tempaddress, self.baseaddress)
698 util.rename(tempaddress, self.baseaddress)
695
699
696 def run(self):
700 def run(self):
697 try:
701 try:
698 self.server.serve_forever()
702 self.server.serve_forever()
699 finally:
703 finally:
700 self.server.unlinksocketfile()
704 self.server.unlinksocketfile()
701
705
702 def uisetup(ui):
706 def uisetup(ui):
703 commandserver._servicemap['chgunix'] = chgunixservice
707 commandserver._servicemap['chgunix'] = chgunixservice
704
708
705 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
709 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
706 # start another chg. drop it to avoid possible side effects.
710 # start another chg. drop it to avoid possible side effects.
707 if 'CHGINTERNALMARK' in os.environ:
711 if 'CHGINTERNALMARK' in os.environ:
708 del os.environ['CHGINTERNALMARK']
712 del os.environ['CHGINTERNALMARK']
General Comments 0
You need to be logged in to leave comments. Login now