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