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