##// END OF EJS Templates
chgserver: use global ui instead of repo ui for dispatch.request.ui...
Jun Wu -
r28599:0e7a9297 default
parent child Browse files
Show More
@@ -1,705 +1,706 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 _renewui(srcui, args=None):
270 def _loadnewui(srcui, args=None):
271 if not args:
271 if not args:
272 args = []
272 args = []
273
273
274 newui = srcui.__class__()
274 newui = srcui.__class__()
275 for a in ['fin', 'fout', 'ferr', 'environ']:
275 for a in ['fin', 'fout', 'ferr', 'environ']:
276 setattr(newui, a, getattr(srcui, a))
276 setattr(newui, a, getattr(srcui, a))
277 if util.safehasattr(srcui, '_csystem'):
277 if util.safehasattr(srcui, '_csystem'):
278 newui._csystem = srcui._csystem
278 newui._csystem = srcui._csystem
279
279
280 # load wd and repo config, copied from dispatch.py
281 cwds = dispatch._earlygetopt(['--cwd'], args)
282 cwd = cwds and os.path.realpath(cwds[-1]) or None
283 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
284 path, newui = dispatch._getlocal(newui, rpath, wd=cwd)
285
286 # internal config: extensions.chgserver
280 # internal config: extensions.chgserver
287 # copy it. it can only be overrided from command line.
288 newui.setconfig('extensions', 'chgserver',
281 newui.setconfig('extensions', 'chgserver',
289 srcui.config('extensions', 'chgserver'), '--config')
282 srcui.config('extensions', 'chgserver'), '--config')
290
283
291 # command line args
284 # command line args
292 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
285 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
293
286
294 # stolen from tortoisehg.util.copydynamicconfig()
287 # stolen from tortoisehg.util.copydynamicconfig()
295 for section, name, value in srcui.walkconfig():
288 for section, name, value in srcui.walkconfig():
296 source = srcui.configsource(section, name)
289 source = srcui.configsource(section, name)
297 if ':' in source or source == '--config':
290 if ':' in source or source == '--config':
298 # path:line or command line
291 # path:line or command line
299 continue
292 continue
300 if source == 'none':
293 if source == 'none':
301 # ui.configsource returns 'none' by default
294 # ui.configsource returns 'none' by default
302 source = ''
295 source = ''
303 newui.setconfig(section, name, value, source)
296 newui.setconfig(section, name, value, source)
304 return newui
297
298 # load wd and repo config, copied from dispatch.py
299 args = args[:]
300 cwds = dispatch._earlygetopt(['--cwd'], args)
301 cwd = cwds and os.path.realpath(cwds[-1]) or None
302 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
303 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
304
305 return (newui, newlui)
305
306
306 class channeledsystem(object):
307 class channeledsystem(object):
307 """Propagate ui.system() request in the following format:
308 """Propagate ui.system() request in the following format:
308
309
309 payload length (unsigned int),
310 payload length (unsigned int),
310 cmd, '\0',
311 cmd, '\0',
311 cwd, '\0',
312 cwd, '\0',
312 envkey, '=', val, '\0',
313 envkey, '=', val, '\0',
313 ...
314 ...
314 envkey, '=', val
315 envkey, '=', val
315
316
316 and waits:
317 and waits:
317
318
318 exitcode length (unsigned int),
319 exitcode length (unsigned int),
319 exitcode (int)
320 exitcode (int)
320 """
321 """
321 def __init__(self, in_, out, channel):
322 def __init__(self, in_, out, channel):
322 self.in_ = in_
323 self.in_ = in_
323 self.out = out
324 self.out = out
324 self.channel = channel
325 self.channel = channel
325
326
326 def __call__(self, cmd, environ, cwd):
327 def __call__(self, cmd, environ, cwd):
327 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
328 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
328 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
329 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
329 data = '\0'.join(args)
330 data = '\0'.join(args)
330 self.out.write(struct.pack('>cI', self.channel, len(data)))
331 self.out.write(struct.pack('>cI', self.channel, len(data)))
331 self.out.write(data)
332 self.out.write(data)
332 self.out.flush()
333 self.out.flush()
333
334
334 length = self.in_.read(4)
335 length = self.in_.read(4)
335 length, = struct.unpack('>I', length)
336 length, = struct.unpack('>I', length)
336 if length != 4:
337 if length != 4:
337 raise error.Abort(_('invalid response'))
338 raise error.Abort(_('invalid response'))
338 rc, = struct.unpack('>i', self.in_.read(4))
339 rc, = struct.unpack('>i', self.in_.read(4))
339 return rc
340 return rc
340
341
341 _iochannels = [
342 _iochannels = [
342 # server.ch, ui.fp, mode
343 # server.ch, ui.fp, mode
343 ('cin', 'fin', 'rb'),
344 ('cin', 'fin', 'rb'),
344 ('cout', 'fout', 'wb'),
345 ('cout', 'fout', 'wb'),
345 ('cerr', 'ferr', 'wb'),
346 ('cerr', 'ferr', 'wb'),
346 ]
347 ]
347
348
348 class chgcmdserver(commandserver.server):
349 class chgcmdserver(commandserver.server):
349 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
350 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
350 super(chgcmdserver, self).__init__(
351 super(chgcmdserver, self).__init__(
351 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
352 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
352 self.clientsock = sock
353 self.clientsock = sock
353 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
354 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
354 self.hashstate = hashstate
355 self.hashstate = hashstate
355 self.baseaddress = baseaddress
356 self.baseaddress = baseaddress
356 if hashstate is not None:
357 if hashstate is not None:
357 self.capabilities = self.capabilities.copy()
358 self.capabilities = self.capabilities.copy()
358 self.capabilities['validate'] = chgcmdserver.validate
359 self.capabilities['validate'] = chgcmdserver.validate
359
360
360 def cleanup(self):
361 def cleanup(self):
361 # dispatch._runcatch() does not flush outputs if exception is not
362 # dispatch._runcatch() does not flush outputs if exception is not
362 # handled by dispatch._dispatch()
363 # handled by dispatch._dispatch()
363 self.ui.flush()
364 self.ui.flush()
364 self._restoreio()
365 self._restoreio()
365
366
366 def attachio(self):
367 def attachio(self):
367 """Attach to client's stdio passed via unix domain socket; all
368 """Attach to client's stdio passed via unix domain socket; all
368 channels except cresult will no longer be used
369 channels except cresult will no longer be used
369 """
370 """
370 # tell client to sendmsg() with 1-byte payload, which makes it
371 # tell client to sendmsg() with 1-byte payload, which makes it
371 # distinctive from "attachio\n" command consumed by client.read()
372 # distinctive from "attachio\n" command consumed by client.read()
372 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
373 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
373 clientfds = osutil.recvfds(self.clientsock.fileno())
374 clientfds = osutil.recvfds(self.clientsock.fileno())
374 _log('received fds: %r\n' % clientfds)
375 _log('received fds: %r\n' % clientfds)
375
376
376 ui = self.ui
377 ui = self.ui
377 ui.flush()
378 ui.flush()
378 first = self._saveio()
379 first = self._saveio()
379 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
380 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
380 assert fd > 0
381 assert fd > 0
381 fp = getattr(ui, fn)
382 fp = getattr(ui, fn)
382 os.dup2(fd, fp.fileno())
383 os.dup2(fd, fp.fileno())
383 os.close(fd)
384 os.close(fd)
384 if not first:
385 if not first:
385 continue
386 continue
386 # reset buffering mode when client is first attached. as we want
387 # reset buffering mode when client is first attached. as we want
387 # to see output immediately on pager, the mode stays unchanged
388 # to see output immediately on pager, the mode stays unchanged
388 # when client re-attached. ferr is unchanged because it should
389 # when client re-attached. ferr is unchanged because it should
389 # be unbuffered no matter if it is a tty or not.
390 # be unbuffered no matter if it is a tty or not.
390 if fn == 'ferr':
391 if fn == 'ferr':
391 newfp = fp
392 newfp = fp
392 else:
393 else:
393 # make it line buffered explicitly because the default is
394 # make it line buffered explicitly because the default is
394 # decided on first write(), where fout could be a pager.
395 # decided on first write(), where fout could be a pager.
395 if fp.isatty():
396 if fp.isatty():
396 bufsize = 1 # line buffered
397 bufsize = 1 # line buffered
397 else:
398 else:
398 bufsize = -1 # system default
399 bufsize = -1 # system default
399 newfp = os.fdopen(fp.fileno(), mode, bufsize)
400 newfp = os.fdopen(fp.fileno(), mode, bufsize)
400 setattr(ui, fn, newfp)
401 setattr(ui, fn, newfp)
401 setattr(self, cn, newfp)
402 setattr(self, cn, newfp)
402
403
403 self.cresult.write(struct.pack('>i', len(clientfds)))
404 self.cresult.write(struct.pack('>i', len(clientfds)))
404
405
405 def _saveio(self):
406 def _saveio(self):
406 if self._oldios:
407 if self._oldios:
407 return False
408 return False
408 ui = self.ui
409 ui = self.ui
409 for cn, fn, _mode in _iochannels:
410 for cn, fn, _mode in _iochannels:
410 ch = getattr(self, cn)
411 ch = getattr(self, cn)
411 fp = getattr(ui, fn)
412 fp = getattr(ui, fn)
412 fd = os.dup(fp.fileno())
413 fd = os.dup(fp.fileno())
413 self._oldios.append((ch, fp, fd))
414 self._oldios.append((ch, fp, fd))
414 return True
415 return True
415
416
416 def _restoreio(self):
417 def _restoreio(self):
417 ui = self.ui
418 ui = self.ui
418 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
419 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
419 newfp = getattr(ui, fn)
420 newfp = getattr(ui, fn)
420 # close newfp while it's associated with client; otherwise it
421 # close newfp while it's associated with client; otherwise it
421 # would be closed when newfp is deleted
422 # would be closed when newfp is deleted
422 if newfp is not fp:
423 if newfp is not fp:
423 newfp.close()
424 newfp.close()
424 # restore original fd: fp is open again
425 # restore original fd: fp is open again
425 os.dup2(fd, fp.fileno())
426 os.dup2(fd, fp.fileno())
426 os.close(fd)
427 os.close(fd)
427 setattr(self, cn, ch)
428 setattr(self, cn, ch)
428 setattr(ui, fn, fp)
429 setattr(ui, fn, fp)
429 del self._oldios[:]
430 del self._oldios[:]
430
431
431 def validate(self):
432 def validate(self):
432 """Reload the config and check if the server is up to date
433 """Reload the config and check if the server is up to date
433
434
434 Read a list of '\0' separated arguments.
435 Read a list of '\0' separated arguments.
435 Write a non-empty list of '\0' separated instruction strings or '\0'
436 Write a non-empty list of '\0' separated instruction strings or '\0'
436 if the list is empty.
437 if the list is empty.
437 An instruction string could be either:
438 An instruction string could be either:
438 - "unlink $path", the client should unlink the path to stop the
439 - "unlink $path", the client should unlink the path to stop the
439 outdated server.
440 outdated server.
440 - "redirect $path", the client should attempt to connect to $path
441 - "redirect $path", the client should attempt to connect to $path
441 first. If it does not work, start a new server. It implies
442 first. If it does not work, start a new server. It implies
442 "reconnect".
443 "reconnect".
443 - "exit $n", the client should exit directly with code n.
444 - "exit $n", the client should exit directly with code n.
444 This may happen if we cannot parse the config.
445 This may happen if we cannot parse the config.
445 - "reconnect", the client should close the connection and
446 - "reconnect", the client should close the connection and
446 reconnect.
447 reconnect.
447 If neither "reconnect" nor "redirect" is included in the instruction
448 If neither "reconnect" nor "redirect" is included in the instruction
448 list, the client can continue with this server after completing all
449 list, the client can continue with this server after completing all
449 the instructions.
450 the instructions.
450 """
451 """
451 args = self._readlist()
452 args = self._readlist()
452 try:
453 try:
453 self.ui = _renewui(self.ui, args)
454 self.ui, lui = _loadnewui(self.ui, args)
454 except error.ParseError as inst:
455 except error.ParseError as inst:
455 dispatch._formatparse(self.ui.warn, inst)
456 dispatch._formatparse(self.ui.warn, inst)
456 self.ui.flush()
457 self.ui.flush()
457 self.cresult.write('exit 255')
458 self.cresult.write('exit 255')
458 return
459 return
459 newhash = hashstate.fromui(self.ui, self.hashstate.mtimepaths)
460 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
460 insts = []
461 insts = []
461 if newhash.mtimehash != self.hashstate.mtimehash:
462 if newhash.mtimehash != self.hashstate.mtimehash:
462 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
463 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
463 insts.append('unlink %s' % addr)
464 insts.append('unlink %s' % addr)
464 # mtimehash is empty if one or more extensions fail to load.
465 # mtimehash is empty if one or more extensions fail to load.
465 # to be compatible with hg, still serve the client this time.
466 # to be compatible with hg, still serve the client this time.
466 if self.hashstate.mtimehash:
467 if self.hashstate.mtimehash:
467 insts.append('reconnect')
468 insts.append('reconnect')
468 if newhash.confighash != self.hashstate.confighash:
469 if newhash.confighash != self.hashstate.confighash:
469 addr = _hashaddress(self.baseaddress, newhash.confighash)
470 addr = _hashaddress(self.baseaddress, newhash.confighash)
470 insts.append('redirect %s' % addr)
471 insts.append('redirect %s' % addr)
471 _log('validate: %s\n' % insts)
472 _log('validate: %s\n' % insts)
472 self.cresult.write('\0'.join(insts) or '\0')
473 self.cresult.write('\0'.join(insts) or '\0')
473
474
474 def chdir(self):
475 def chdir(self):
475 """Change current directory
476 """Change current directory
476
477
477 Note that the behavior of --cwd option is bit different from this.
478 Note that the behavior of --cwd option is bit different from this.
478 It does not affect --config parameter.
479 It does not affect --config parameter.
479 """
480 """
480 path = self._readstr()
481 path = self._readstr()
481 if not path:
482 if not path:
482 return
483 return
483 _log('chdir to %r\n' % path)
484 _log('chdir to %r\n' % path)
484 os.chdir(path)
485 os.chdir(path)
485
486
486 def setumask(self):
487 def setumask(self):
487 """Change umask"""
488 """Change umask"""
488 mask = struct.unpack('>I', self._read(4))[0]
489 mask = struct.unpack('>I', self._read(4))[0]
489 _log('setumask %r\n' % mask)
490 _log('setumask %r\n' % mask)
490 os.umask(mask)
491 os.umask(mask)
491
492
492 def getpager(self):
493 def getpager(self):
493 """Read cmdargs and write pager command to r-channel if enabled
494 """Read cmdargs and write pager command to r-channel if enabled
494
495
495 If pager isn't enabled, this writes '\0' because channeledoutput
496 If pager isn't enabled, this writes '\0' because channeledoutput
496 does not allow to write empty data.
497 does not allow to write empty data.
497 """
498 """
498 args = self._readlist()
499 args = self._readlist()
499 try:
500 try:
500 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
501 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
501 args)
502 args)
502 except (error.Abort, error.AmbiguousCommand, error.CommandError,
503 except (error.Abort, error.AmbiguousCommand, error.CommandError,
503 error.UnknownCommand):
504 error.UnknownCommand):
504 cmd = None
505 cmd = None
505 options = {}
506 options = {}
506 if not cmd or 'pager' not in options:
507 if not cmd or 'pager' not in options:
507 self.cresult.write('\0')
508 self.cresult.write('\0')
508 return
509 return
509
510
510 pagercmd = _setuppagercmd(self.ui, options, cmd)
511 pagercmd = _setuppagercmd(self.ui, options, cmd)
511 if pagercmd:
512 if pagercmd:
512 self.cresult.write(pagercmd)
513 self.cresult.write(pagercmd)
513 else:
514 else:
514 self.cresult.write('\0')
515 self.cresult.write('\0')
515
516
516 def setenv(self):
517 def setenv(self):
517 """Clear and update os.environ
518 """Clear and update os.environ
518
519
519 Note that not all variables can make an effect on the running process.
520 Note that not all variables can make an effect on the running process.
520 """
521 """
521 l = self._readlist()
522 l = self._readlist()
522 try:
523 try:
523 newenv = dict(s.split('=', 1) for s in l)
524 newenv = dict(s.split('=', 1) for s in l)
524 except ValueError:
525 except ValueError:
525 raise ValueError('unexpected value in setenv request')
526 raise ValueError('unexpected value in setenv request')
526
527
527 diffkeys = set(k for k in set(os.environ.keys() + newenv.keys())
528 diffkeys = set(k for k in set(os.environ.keys() + newenv.keys())
528 if os.environ.get(k) != newenv.get(k))
529 if os.environ.get(k) != newenv.get(k))
529 _log('change env: %r\n' % sorted(diffkeys))
530 _log('change env: %r\n' % sorted(diffkeys))
530
531
531 os.environ.clear()
532 os.environ.clear()
532 os.environ.update(newenv)
533 os.environ.update(newenv)
533
534
534 if set(['HGPLAIN', 'HGPLAINEXCEPT']) & diffkeys:
535 if set(['HGPLAIN', 'HGPLAINEXCEPT']) & diffkeys:
535 # reload config so that ui.plain() takes effect
536 # reload config so that ui.plain() takes effect
536 self.ui = _renewui(self.ui)
537 self.ui, _lui = _loadnewui(self.ui)
537
538
538 _clearenvaliases(commands.table)
539 _clearenvaliases(commands.table)
539
540
540 capabilities = commandserver.server.capabilities.copy()
541 capabilities = commandserver.server.capabilities.copy()
541 capabilities.update({'attachio': attachio,
542 capabilities.update({'attachio': attachio,
542 'chdir': chdir,
543 'chdir': chdir,
543 'getpager': getpager,
544 'getpager': getpager,
544 'setenv': setenv,
545 'setenv': setenv,
545 'setumask': setumask})
546 'setumask': setumask})
546
547
547 # copied from mercurial/commandserver.py
548 # copied from mercurial/commandserver.py
548 class _requesthandler(SocketServer.StreamRequestHandler):
549 class _requesthandler(SocketServer.StreamRequestHandler):
549 def handle(self):
550 def handle(self):
550 # use a different process group from the master process, making this
551 # use a different process group from the master process, making this
551 # process pass kernel "is_current_pgrp_orphaned" check so signals like
552 # process pass kernel "is_current_pgrp_orphaned" check so signals like
552 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
553 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
553 os.setpgid(0, 0)
554 os.setpgid(0, 0)
554 ui = self.server.ui
555 ui = self.server.ui
555 repo = self.server.repo
556 repo = self.server.repo
556 sv = None
557 sv = None
557 try:
558 try:
558 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
559 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
559 self.server.hashstate, self.server.baseaddress)
560 self.server.hashstate, self.server.baseaddress)
560 try:
561 try:
561 sv.serve()
562 sv.serve()
562 # handle exceptions that may be raised by command server. most of
563 # handle exceptions that may be raised by command server. most of
563 # known exceptions are caught by dispatch.
564 # known exceptions are caught by dispatch.
564 except error.Abort as inst:
565 except error.Abort as inst:
565 ui.warn(_('abort: %s\n') % inst)
566 ui.warn(_('abort: %s\n') % inst)
566 except IOError as inst:
567 except IOError as inst:
567 if inst.errno != errno.EPIPE:
568 if inst.errno != errno.EPIPE:
568 raise
569 raise
569 except KeyboardInterrupt:
570 except KeyboardInterrupt:
570 pass
571 pass
571 finally:
572 finally:
572 sv.cleanup()
573 sv.cleanup()
573 except: # re-raises
574 except: # re-raises
574 # also write traceback to error channel. otherwise client cannot
575 # also write traceback to error channel. otherwise client cannot
575 # see it because it is written to server's stderr by default.
576 # see it because it is written to server's stderr by default.
576 if sv:
577 if sv:
577 cerr = sv.cerr
578 cerr = sv.cerr
578 else:
579 else:
579 cerr = commandserver.channeledoutput(self.wfile, 'e')
580 cerr = commandserver.channeledoutput(self.wfile, 'e')
580 traceback.print_exc(file=cerr)
581 traceback.print_exc(file=cerr)
581 raise
582 raise
582 finally:
583 finally:
583 # trigger __del__ since ForkingMixIn uses os._exit
584 # trigger __del__ since ForkingMixIn uses os._exit
584 gc.collect()
585 gc.collect()
585
586
586 def _tempaddress(address):
587 def _tempaddress(address):
587 return '%s.%d.tmp' % (address, os.getpid())
588 return '%s.%d.tmp' % (address, os.getpid())
588
589
589 def _hashaddress(address, hashstr):
590 def _hashaddress(address, hashstr):
590 return '%s-%s' % (address, hashstr)
591 return '%s-%s' % (address, hashstr)
591
592
592 class AutoExitMixIn: # use old-style to comply with SocketServer design
593 class AutoExitMixIn: # use old-style to comply with SocketServer design
593 lastactive = time.time()
594 lastactive = time.time()
594 idletimeout = 3600 # default 1 hour
595 idletimeout = 3600 # default 1 hour
595
596
596 def startautoexitthread(self):
597 def startautoexitthread(self):
597 # note: the auto-exit check here is cheap enough to not use a thread,
598 # note: the auto-exit check here is cheap enough to not use a thread,
598 # be done in serve_forever. however SocketServer is hook-unfriendly,
599 # be done in serve_forever. however SocketServer is hook-unfriendly,
599 # you simply cannot hook serve_forever without copying a lot of code.
600 # you simply cannot hook serve_forever without copying a lot of code.
600 # besides, serve_forever's docstring suggests using thread.
601 # besides, serve_forever's docstring suggests using thread.
601 thread = threading.Thread(target=self._autoexitloop)
602 thread = threading.Thread(target=self._autoexitloop)
602 thread.daemon = True
603 thread.daemon = True
603 thread.start()
604 thread.start()
604
605
605 def _autoexitloop(self, interval=1):
606 def _autoexitloop(self, interval=1):
606 while True:
607 while True:
607 time.sleep(interval)
608 time.sleep(interval)
608 if not self.issocketowner():
609 if not self.issocketowner():
609 _log('%s is not owned, exiting.\n' % self.server_address)
610 _log('%s is not owned, exiting.\n' % self.server_address)
610 break
611 break
611 if time.time() - self.lastactive > self.idletimeout:
612 if time.time() - self.lastactive > self.idletimeout:
612 _log('being idle too long. exiting.\n')
613 _log('being idle too long. exiting.\n')
613 break
614 break
614 self.shutdown()
615 self.shutdown()
615
616
616 def process_request(self, request, address):
617 def process_request(self, request, address):
617 self.lastactive = time.time()
618 self.lastactive = time.time()
618 return SocketServer.ForkingMixIn.process_request(
619 return SocketServer.ForkingMixIn.process_request(
619 self, request, address)
620 self, request, address)
620
621
621 def server_bind(self):
622 def server_bind(self):
622 # use a unique temp address so we can stat the file and do ownership
623 # use a unique temp address so we can stat the file and do ownership
623 # check later
624 # check later
624 tempaddress = _tempaddress(self.server_address)
625 tempaddress = _tempaddress(self.server_address)
625 self.socket.bind(tempaddress)
626 self.socket.bind(tempaddress)
626 self._socketstat = os.stat(tempaddress)
627 self._socketstat = os.stat(tempaddress)
627 # rename will replace the old socket file if exists atomically. the
628 # rename will replace the old socket file if exists atomically. the
628 # old server will detect ownership change and exit.
629 # old server will detect ownership change and exit.
629 util.rename(tempaddress, self.server_address)
630 util.rename(tempaddress, self.server_address)
630
631
631 def issocketowner(self):
632 def issocketowner(self):
632 try:
633 try:
633 stat = os.stat(self.server_address)
634 stat = os.stat(self.server_address)
634 return (stat.st_ino == self._socketstat.st_ino and
635 return (stat.st_ino == self._socketstat.st_ino and
635 stat.st_mtime == self._socketstat.st_mtime)
636 stat.st_mtime == self._socketstat.st_mtime)
636 except OSError:
637 except OSError:
637 return False
638 return False
638
639
639 def unlinksocketfile(self):
640 def unlinksocketfile(self):
640 if not self.issocketowner():
641 if not self.issocketowner():
641 return
642 return
642 # it is possible to have a race condition here that we may
643 # it is possible to have a race condition here that we may
643 # remove another server's socket file. but that's okay
644 # remove another server's socket file. but that's okay
644 # since that server will detect and exit automatically and
645 # since that server will detect and exit automatically and
645 # the client will start a new server on demand.
646 # the client will start a new server on demand.
646 try:
647 try:
647 os.unlink(self.server_address)
648 os.unlink(self.server_address)
648 except OSError as exc:
649 except OSError as exc:
649 if exc.errno != errno.ENOENT:
650 if exc.errno != errno.ENOENT:
650 raise
651 raise
651
652
652 class chgunixservice(commandserver.unixservice):
653 class chgunixservice(commandserver.unixservice):
653 def init(self):
654 def init(self):
654 self.repo = None
655 self.repo = None
655 self._inithashstate()
656 self._inithashstate()
656 self._checkextensions()
657 self._checkextensions()
657 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
658 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
658 SocketServer.UnixStreamServer):
659 SocketServer.UnixStreamServer):
659 ui = self.ui
660 ui = self.ui
660 repo = self.repo
661 repo = self.repo
661 hashstate = self.hashstate
662 hashstate = self.hashstate
662 baseaddress = self.baseaddress
663 baseaddress = self.baseaddress
663 self.server = cls(self.address, _requesthandler)
664 self.server = cls(self.address, _requesthandler)
664 self.server.idletimeout = self.ui.configint(
665 self.server.idletimeout = self.ui.configint(
665 'chgserver', 'idletimeout', self.server.idletimeout)
666 'chgserver', 'idletimeout', self.server.idletimeout)
666 self.server.startautoexitthread()
667 self.server.startautoexitthread()
667 self._createsymlink()
668 self._createsymlink()
668
669
669 def _inithashstate(self):
670 def _inithashstate(self):
670 self.baseaddress = self.address
671 self.baseaddress = self.address
671 if self.ui.configbool('chgserver', 'skiphash', False):
672 if self.ui.configbool('chgserver', 'skiphash', False):
672 self.hashstate = None
673 self.hashstate = None
673 return
674 return
674 self.hashstate = hashstate.fromui(self.ui)
675 self.hashstate = hashstate.fromui(self.ui)
675 self.address = _hashaddress(self.address, self.hashstate.confighash)
676 self.address = _hashaddress(self.address, self.hashstate.confighash)
676
677
677 def _checkextensions(self):
678 def _checkextensions(self):
678 if not self.hashstate:
679 if not self.hashstate:
679 return
680 return
680 if extensions.notloaded():
681 if extensions.notloaded():
681 # one or more extensions failed to load. mtimehash becomes
682 # one or more extensions failed to load. mtimehash becomes
682 # meaningless because we do not know the paths of those extensions.
683 # meaningless because we do not know the paths of those extensions.
683 # set mtimehash to an illegal hash value to invalidate the server.
684 # set mtimehash to an illegal hash value to invalidate the server.
684 self.hashstate.mtimehash = ''
685 self.hashstate.mtimehash = ''
685
686
686 def _createsymlink(self):
687 def _createsymlink(self):
687 if self.baseaddress == self.address:
688 if self.baseaddress == self.address:
688 return
689 return
689 tempaddress = _tempaddress(self.baseaddress)
690 tempaddress = _tempaddress(self.baseaddress)
690 os.symlink(os.path.basename(self.address), tempaddress)
691 os.symlink(os.path.basename(self.address), tempaddress)
691 util.rename(tempaddress, self.baseaddress)
692 util.rename(tempaddress, self.baseaddress)
692
693
693 def run(self):
694 def run(self):
694 try:
695 try:
695 self.server.serve_forever()
696 self.server.serve_forever()
696 finally:
697 finally:
697 self.server.unlinksocketfile()
698 self.server.unlinksocketfile()
698
699
699 def uisetup(ui):
700 def uisetup(ui):
700 commandserver._servicemap['chgunix'] = chgunixservice
701 commandserver._servicemap['chgunix'] = chgunixservice
701
702
702 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
703 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
703 # start another chg. drop it to avoid possible side effects.
704 # start another chg. drop it to avoid possible side effects.
704 if 'CHGINTERNALMARK' in os.environ:
705 if 'CHGINTERNALMARK' in os.environ:
705 del os.environ['CHGINTERNALMARK']
706 del os.environ['CHGINTERNALMARK']
General Comments 0
You need to be logged in to leave comments. Login now