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