##// END OF EJS Templates
chgserver: override runcommand...
Jun Wu -
r30644:2bb8c53b default
parent child Browse files
Show More
@@ -1,641 +1,645
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 sys
50 import sys
51 import time
51 import time
52
52
53 from .i18n import _
53 from .i18n import _
54
54
55 from . import (
55 from . import (
56 cmdutil,
56 cmdutil,
57 commandserver,
57 commandserver,
58 encoding,
58 encoding,
59 error,
59 error,
60 extensions,
60 extensions,
61 osutil,
61 osutil,
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 = [sys.executable]
125 files = [sys.executable]
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()
234 # copied from mercurial/util.py:system()
235 self.flush()
235 self.flush()
236 def py2shell(val):
236 def py2shell(val):
237 if val is None or val is False:
237 if val is None or val is False:
238 return '0'
238 return '0'
239 if val is True:
239 if val is True:
240 return '1'
240 return '1'
241 return str(val)
241 return str(val)
242 env = encoding.environ.copy()
242 env = encoding.environ.copy()
243 if environ:
243 if environ:
244 env.update((k, py2shell(v)) for k, v in environ.iteritems())
244 env.update((k, py2shell(v)) for k, v in environ.iteritems())
245 env['HG'] = util.hgexecutable()
245 env['HG'] = util.hgexecutable()
246 rc = self._csystem(cmd, env, cwd)
246 rc = self._csystem(cmd, env, cwd)
247 if rc and onerr:
247 if rc and onerr:
248 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
248 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
249 util.explainexit(rc)[0])
249 util.explainexit(rc)[0])
250 if errprefix:
250 if errprefix:
251 errmsg = '%s: %s' % (errprefix, errmsg)
251 errmsg = '%s: %s' % (errprefix, errmsg)
252 raise onerr(errmsg)
252 raise onerr(errmsg)
253 return rc
253 return rc
254
254
255 return chgui(srcui)
255 return chgui(srcui)
256
256
257 def _loadnewui(srcui, args):
257 def _loadnewui(srcui, args):
258 from . import dispatch # avoid cycle
258 from . import dispatch # avoid cycle
259
259
260 newui = srcui.__class__.load()
260 newui = srcui.__class__.load()
261 for a in ['fin', 'fout', 'ferr', 'environ']:
261 for a in ['fin', 'fout', 'ferr', 'environ']:
262 setattr(newui, a, getattr(srcui, a))
262 setattr(newui, a, getattr(srcui, a))
263 if util.safehasattr(srcui, '_csystem'):
263 if util.safehasattr(srcui, '_csystem'):
264 newui._csystem = srcui._csystem
264 newui._csystem = srcui._csystem
265
265
266 # command line args
266 # command line args
267 args = args[:]
267 args = args[:]
268 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
268 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
269
269
270 # stolen from tortoisehg.util.copydynamicconfig()
270 # stolen from tortoisehg.util.copydynamicconfig()
271 for section, name, value in srcui.walkconfig():
271 for section, name, value in srcui.walkconfig():
272 source = srcui.configsource(section, name)
272 source = srcui.configsource(section, name)
273 if ':' in source or source == '--config':
273 if ':' in source or source == '--config':
274 # path:line or command line
274 # path:line or command line
275 continue
275 continue
276 newui.setconfig(section, name, value, source)
276 newui.setconfig(section, name, value, source)
277
277
278 # load wd and repo config, copied from dispatch.py
278 # load wd and repo config, copied from dispatch.py
279 cwds = dispatch._earlygetopt(['--cwd'], args)
279 cwds = dispatch._earlygetopt(['--cwd'], args)
280 cwd = cwds and os.path.realpath(cwds[-1]) or None
280 cwd = cwds and os.path.realpath(cwds[-1]) or None
281 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
281 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
282 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
282 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
283
283
284 return (newui, newlui)
284 return (newui, newlui)
285
285
286 class channeledsystem(object):
286 class channeledsystem(object):
287 """Propagate ui.system() request in the following format:
287 """Propagate ui.system() request in the following format:
288
288
289 payload length (unsigned int),
289 payload length (unsigned int),
290 cmd, '\0',
290 cmd, '\0',
291 cwd, '\0',
291 cwd, '\0',
292 envkey, '=', val, '\0',
292 envkey, '=', val, '\0',
293 ...
293 ...
294 envkey, '=', val
294 envkey, '=', val
295
295
296 and waits:
296 and waits:
297
297
298 exitcode length (unsigned int),
298 exitcode length (unsigned int),
299 exitcode (int)
299 exitcode (int)
300 """
300 """
301 def __init__(self, in_, out, channel):
301 def __init__(self, in_, out, channel):
302 self.in_ = in_
302 self.in_ = in_
303 self.out = out
303 self.out = out
304 self.channel = channel
304 self.channel = channel
305
305
306 def __call__(self, cmd, environ, cwd):
306 def __call__(self, cmd, environ, cwd):
307 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
307 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
308 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
308 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
309 data = '\0'.join(args)
309 data = '\0'.join(args)
310 self.out.write(struct.pack('>cI', self.channel, len(data)))
310 self.out.write(struct.pack('>cI', self.channel, len(data)))
311 self.out.write(data)
311 self.out.write(data)
312 self.out.flush()
312 self.out.flush()
313
313
314 length = self.in_.read(4)
314 length = self.in_.read(4)
315 length, = struct.unpack('>I', length)
315 length, = struct.unpack('>I', length)
316 if length != 4:
316 if length != 4:
317 raise error.Abort(_('invalid response'))
317 raise error.Abort(_('invalid response'))
318 rc, = struct.unpack('>i', self.in_.read(4))
318 rc, = struct.unpack('>i', self.in_.read(4))
319 return rc
319 return rc
320
320
321 _iochannels = [
321 _iochannels = [
322 # server.ch, ui.fp, mode
322 # server.ch, ui.fp, mode
323 ('cin', 'fin', 'rb'),
323 ('cin', 'fin', 'rb'),
324 ('cout', 'fout', 'wb'),
324 ('cout', 'fout', 'wb'),
325 ('cerr', 'ferr', 'wb'),
325 ('cerr', 'ferr', 'wb'),
326 ]
326 ]
327
327
328 class chgcmdserver(commandserver.server):
328 class chgcmdserver(commandserver.server):
329 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
329 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
330 self._csystem = channeledsystem(fin, fout, 'S')
330 self._csystem = channeledsystem(fin, fout, 'S')
331 super(chgcmdserver, self).__init__(
331 super(chgcmdserver, self).__init__(
332 _newchgui(ui, self._csystem), repo, fin, fout)
332 _newchgui(ui, self._csystem), repo, fin, fout)
333 self.clientsock = sock
333 self.clientsock = sock
334 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
334 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
335 self.hashstate = hashstate
335 self.hashstate = hashstate
336 self.baseaddress = baseaddress
336 self.baseaddress = baseaddress
337 if hashstate is not None:
337 if hashstate is not None:
338 self.capabilities = self.capabilities.copy()
338 self.capabilities = self.capabilities.copy()
339 self.capabilities['validate'] = chgcmdserver.validate
339 self.capabilities['validate'] = chgcmdserver.validate
340
340
341 def cleanup(self):
341 def cleanup(self):
342 super(chgcmdserver, self).cleanup()
342 super(chgcmdserver, self).cleanup()
343 # dispatch._runcatch() does not flush outputs if exception is not
343 # dispatch._runcatch() does not flush outputs if exception is not
344 # handled by dispatch._dispatch()
344 # handled by dispatch._dispatch()
345 self.ui.flush()
345 self.ui.flush()
346 self._restoreio()
346 self._restoreio()
347
347
348 def attachio(self):
348 def attachio(self):
349 """Attach to client's stdio passed via unix domain socket; all
349 """Attach to client's stdio passed via unix domain socket; all
350 channels except cresult will no longer be used
350 channels except cresult will no longer be used
351 """
351 """
352 # tell client to sendmsg() with 1-byte payload, which makes it
352 # tell client to sendmsg() with 1-byte payload, which makes it
353 # distinctive from "attachio\n" command consumed by client.read()
353 # distinctive from "attachio\n" command consumed by client.read()
354 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
354 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
355 clientfds = osutil.recvfds(self.clientsock.fileno())
355 clientfds = osutil.recvfds(self.clientsock.fileno())
356 _log('received fds: %r\n' % clientfds)
356 _log('received fds: %r\n' % clientfds)
357
357
358 ui = self.ui
358 ui = self.ui
359 ui.flush()
359 ui.flush()
360 first = self._saveio()
360 first = self._saveio()
361 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
361 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
362 assert fd > 0
362 assert fd > 0
363 fp = getattr(ui, fn)
363 fp = getattr(ui, fn)
364 os.dup2(fd, fp.fileno())
364 os.dup2(fd, fp.fileno())
365 os.close(fd)
365 os.close(fd)
366 if not first:
366 if not first:
367 continue
367 continue
368 # reset buffering mode when client is first attached. as we want
368 # reset buffering mode when client is first attached. as we want
369 # to see output immediately on pager, the mode stays unchanged
369 # to see output immediately on pager, the mode stays unchanged
370 # when client re-attached. ferr is unchanged because it should
370 # when client re-attached. ferr is unchanged because it should
371 # be unbuffered no matter if it is a tty or not.
371 # be unbuffered no matter if it is a tty or not.
372 if fn == 'ferr':
372 if fn == 'ferr':
373 newfp = fp
373 newfp = fp
374 else:
374 else:
375 # make it line buffered explicitly because the default is
375 # make it line buffered explicitly because the default is
376 # decided on first write(), where fout could be a pager.
376 # decided on first write(), where fout could be a pager.
377 if fp.isatty():
377 if fp.isatty():
378 bufsize = 1 # line buffered
378 bufsize = 1 # line buffered
379 else:
379 else:
380 bufsize = -1 # system default
380 bufsize = -1 # system default
381 newfp = os.fdopen(fp.fileno(), mode, bufsize)
381 newfp = os.fdopen(fp.fileno(), mode, bufsize)
382 setattr(ui, fn, newfp)
382 setattr(ui, fn, newfp)
383 setattr(self, cn, newfp)
383 setattr(self, cn, newfp)
384
384
385 self.cresult.write(struct.pack('>i', len(clientfds)))
385 self.cresult.write(struct.pack('>i', len(clientfds)))
386
386
387 def _saveio(self):
387 def _saveio(self):
388 if self._oldios:
388 if self._oldios:
389 return False
389 return False
390 ui = self.ui
390 ui = self.ui
391 for cn, fn, _mode in _iochannels:
391 for cn, fn, _mode in _iochannels:
392 ch = getattr(self, cn)
392 ch = getattr(self, cn)
393 fp = getattr(ui, fn)
393 fp = getattr(ui, fn)
394 fd = os.dup(fp.fileno())
394 fd = os.dup(fp.fileno())
395 self._oldios.append((ch, fp, fd))
395 self._oldios.append((ch, fp, fd))
396 return True
396 return True
397
397
398 def _restoreio(self):
398 def _restoreio(self):
399 ui = self.ui
399 ui = self.ui
400 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
400 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
401 newfp = getattr(ui, fn)
401 newfp = getattr(ui, fn)
402 # close newfp while it's associated with client; otherwise it
402 # close newfp while it's associated with client; otherwise it
403 # would be closed when newfp is deleted
403 # would be closed when newfp is deleted
404 if newfp is not fp:
404 if newfp is not fp:
405 newfp.close()
405 newfp.close()
406 # restore original fd: fp is open again
406 # restore original fd: fp is open again
407 os.dup2(fd, fp.fileno())
407 os.dup2(fd, fp.fileno())
408 os.close(fd)
408 os.close(fd)
409 setattr(self, cn, ch)
409 setattr(self, cn, ch)
410 setattr(ui, fn, fp)
410 setattr(ui, fn, fp)
411 del self._oldios[:]
411 del self._oldios[:]
412
412
413 def validate(self):
413 def validate(self):
414 """Reload the config and check if the server is up to date
414 """Reload the config and check if the server is up to date
415
415
416 Read a list of '\0' separated arguments.
416 Read a list of '\0' separated arguments.
417 Write a non-empty list of '\0' separated instruction strings or '\0'
417 Write a non-empty list of '\0' separated instruction strings or '\0'
418 if the list is empty.
418 if the list is empty.
419 An instruction string could be either:
419 An instruction string could be either:
420 - "unlink $path", the client should unlink the path to stop the
420 - "unlink $path", the client should unlink the path to stop the
421 outdated server.
421 outdated server.
422 - "redirect $path", the client should attempt to connect to $path
422 - "redirect $path", the client should attempt to connect to $path
423 first. If it does not work, start a new server. It implies
423 first. If it does not work, start a new server. It implies
424 "reconnect".
424 "reconnect".
425 - "exit $n", the client should exit directly with code n.
425 - "exit $n", the client should exit directly with code n.
426 This may happen if we cannot parse the config.
426 This may happen if we cannot parse the config.
427 - "reconnect", the client should close the connection and
427 - "reconnect", the client should close the connection and
428 reconnect.
428 reconnect.
429 If neither "reconnect" nor "redirect" is included in the instruction
429 If neither "reconnect" nor "redirect" is included in the instruction
430 list, the client can continue with this server after completing all
430 list, the client can continue with this server after completing all
431 the instructions.
431 the instructions.
432 """
432 """
433 from . import dispatch # avoid cycle
433 from . import dispatch # avoid cycle
434
434
435 args = self._readlist()
435 args = self._readlist()
436 try:
436 try:
437 self.ui, lui = _loadnewui(self.ui, args)
437 self.ui, lui = _loadnewui(self.ui, args)
438 except error.ParseError as inst:
438 except error.ParseError as inst:
439 dispatch._formatparse(self.ui.warn, inst)
439 dispatch._formatparse(self.ui.warn, inst)
440 self.ui.flush()
440 self.ui.flush()
441 self.cresult.write('exit 255')
441 self.cresult.write('exit 255')
442 return
442 return
443 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
443 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
444 insts = []
444 insts = []
445 if newhash.mtimehash != self.hashstate.mtimehash:
445 if newhash.mtimehash != self.hashstate.mtimehash:
446 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
446 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
447 insts.append('unlink %s' % addr)
447 insts.append('unlink %s' % addr)
448 # mtimehash is empty if one or more extensions fail to load.
448 # mtimehash is empty if one or more extensions fail to load.
449 # to be compatible with hg, still serve the client this time.
449 # to be compatible with hg, still serve the client this time.
450 if self.hashstate.mtimehash:
450 if self.hashstate.mtimehash:
451 insts.append('reconnect')
451 insts.append('reconnect')
452 if newhash.confighash != self.hashstate.confighash:
452 if newhash.confighash != self.hashstate.confighash:
453 addr = _hashaddress(self.baseaddress, newhash.confighash)
453 addr = _hashaddress(self.baseaddress, newhash.confighash)
454 insts.append('redirect %s' % addr)
454 insts.append('redirect %s' % addr)
455 _log('validate: %s\n' % insts)
455 _log('validate: %s\n' % insts)
456 self.cresult.write('\0'.join(insts) or '\0')
456 self.cresult.write('\0'.join(insts) or '\0')
457
457
458 def chdir(self):
458 def chdir(self):
459 """Change current directory
459 """Change current directory
460
460
461 Note that the behavior of --cwd option is bit different from this.
461 Note that the behavior of --cwd option is bit different from this.
462 It does not affect --config parameter.
462 It does not affect --config parameter.
463 """
463 """
464 path = self._readstr()
464 path = self._readstr()
465 if not path:
465 if not path:
466 return
466 return
467 _log('chdir to %r\n' % path)
467 _log('chdir to %r\n' % path)
468 os.chdir(path)
468 os.chdir(path)
469
469
470 def setumask(self):
470 def setumask(self):
471 """Change umask"""
471 """Change umask"""
472 mask = struct.unpack('>I', self._read(4))[0]
472 mask = struct.unpack('>I', self._read(4))[0]
473 _log('setumask %r\n' % mask)
473 _log('setumask %r\n' % mask)
474 os.umask(mask)
474 os.umask(mask)
475
475
476 def getpager(self):
476 def getpager(self):
477 """Read cmdargs and write pager command to r-channel if enabled
477 """Read cmdargs and write pager command to r-channel if enabled
478
478
479 If pager isn't enabled, this writes '\0' because channeledoutput
479 If pager isn't enabled, this writes '\0' because channeledoutput
480 does not allow to write empty data.
480 does not allow to write empty data.
481 """
481 """
482 from . import dispatch # avoid cycle
482 from . import dispatch # avoid cycle
483
483
484 args = self._readlist()
484 args = self._readlist()
485 try:
485 try:
486 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
486 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
487 args)
487 args)
488 except (error.Abort, error.AmbiguousCommand, error.CommandError,
488 except (error.Abort, error.AmbiguousCommand, error.CommandError,
489 error.UnknownCommand):
489 error.UnknownCommand):
490 cmd = None
490 cmd = None
491 options = {}
491 options = {}
492 if not cmd or 'pager' not in options:
492 if not cmd or 'pager' not in options:
493 self.cresult.write('\0')
493 self.cresult.write('\0')
494 return
494 return
495
495
496 pagercmd = _setuppagercmd(self.ui, options, cmd)
496 pagercmd = _setuppagercmd(self.ui, options, cmd)
497 if pagercmd:
497 if pagercmd:
498 # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
498 # Python's SIGPIPE is SIG_IGN by default. change to SIG_DFL so
499 # we can exit if the pipe to the pager is closed
499 # we can exit if the pipe to the pager is closed
500 if util.safehasattr(signal, 'SIGPIPE') and \
500 if util.safehasattr(signal, 'SIGPIPE') and \
501 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
501 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
502 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
502 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
503 self.cresult.write(pagercmd)
503 self.cresult.write(pagercmd)
504 else:
504 else:
505 self.cresult.write('\0')
505 self.cresult.write('\0')
506
506
507 def runcommand(self):
508 return super(chgcmdserver, self).runcommand()
509
507 def setenv(self):
510 def setenv(self):
508 """Clear and update os.environ
511 """Clear and update os.environ
509
512
510 Note that not all variables can make an effect on the running process.
513 Note that not all variables can make an effect on the running process.
511 """
514 """
512 l = self._readlist()
515 l = self._readlist()
513 try:
516 try:
514 newenv = dict(s.split('=', 1) for s in l)
517 newenv = dict(s.split('=', 1) for s in l)
515 except ValueError:
518 except ValueError:
516 raise ValueError('unexpected value in setenv request')
519 raise ValueError('unexpected value in setenv request')
517 _log('setenv: %r\n' % sorted(newenv.keys()))
520 _log('setenv: %r\n' % sorted(newenv.keys()))
518 encoding.environ.clear()
521 encoding.environ.clear()
519 encoding.environ.update(newenv)
522 encoding.environ.update(newenv)
520
523
521 capabilities = commandserver.server.capabilities.copy()
524 capabilities = commandserver.server.capabilities.copy()
522 capabilities.update({'attachio': attachio,
525 capabilities.update({'attachio': attachio,
523 'chdir': chdir,
526 'chdir': chdir,
524 'getpager': getpager,
527 'getpager': getpager,
528 'runcommand': runcommand,
525 'setenv': setenv,
529 'setenv': setenv,
526 'setumask': setumask})
530 'setumask': setumask})
527
531
528 def _tempaddress(address):
532 def _tempaddress(address):
529 return '%s.%d.tmp' % (address, os.getpid())
533 return '%s.%d.tmp' % (address, os.getpid())
530
534
531 def _hashaddress(address, hashstr):
535 def _hashaddress(address, hashstr):
532 # if the basename of address contains '.', use only the left part. this
536 # if the basename of address contains '.', use only the left part. this
533 # makes it possible for the client to pass 'server.tmp$PID' and follow by
537 # makes it possible for the client to pass 'server.tmp$PID' and follow by
534 # an atomic rename to avoid locking when spawning new servers.
538 # an atomic rename to avoid locking when spawning new servers.
535 dirname, basename = os.path.split(address)
539 dirname, basename = os.path.split(address)
536 basename = basename.split('.', 1)[0]
540 basename = basename.split('.', 1)[0]
537 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
541 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
538
542
539 class chgunixservicehandler(object):
543 class chgunixservicehandler(object):
540 """Set of operations for chg services"""
544 """Set of operations for chg services"""
541
545
542 pollinterval = 1 # [sec]
546 pollinterval = 1 # [sec]
543
547
544 def __init__(self, ui):
548 def __init__(self, ui):
545 self.ui = ui
549 self.ui = ui
546 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
550 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
547 self._lastactive = time.time()
551 self._lastactive = time.time()
548
552
549 def bindsocket(self, sock, address):
553 def bindsocket(self, sock, address):
550 self._inithashstate(address)
554 self._inithashstate(address)
551 self._checkextensions()
555 self._checkextensions()
552 self._bind(sock)
556 self._bind(sock)
553 self._createsymlink()
557 self._createsymlink()
554
558
555 def _inithashstate(self, address):
559 def _inithashstate(self, address):
556 self._baseaddress = address
560 self._baseaddress = address
557 if self.ui.configbool('chgserver', 'skiphash', False):
561 if self.ui.configbool('chgserver', 'skiphash', False):
558 self._hashstate = None
562 self._hashstate = None
559 self._realaddress = address
563 self._realaddress = address
560 return
564 return
561 self._hashstate = hashstate.fromui(self.ui)
565 self._hashstate = hashstate.fromui(self.ui)
562 self._realaddress = _hashaddress(address, self._hashstate.confighash)
566 self._realaddress = _hashaddress(address, self._hashstate.confighash)
563
567
564 def _checkextensions(self):
568 def _checkextensions(self):
565 if not self._hashstate:
569 if not self._hashstate:
566 return
570 return
567 if extensions.notloaded():
571 if extensions.notloaded():
568 # one or more extensions failed to load. mtimehash becomes
572 # one or more extensions failed to load. mtimehash becomes
569 # meaningless because we do not know the paths of those extensions.
573 # meaningless because we do not know the paths of those extensions.
570 # set mtimehash to an illegal hash value to invalidate the server.
574 # set mtimehash to an illegal hash value to invalidate the server.
571 self._hashstate.mtimehash = ''
575 self._hashstate.mtimehash = ''
572
576
573 def _bind(self, sock):
577 def _bind(self, sock):
574 # use a unique temp address so we can stat the file and do ownership
578 # use a unique temp address so we can stat the file and do ownership
575 # check later
579 # check later
576 tempaddress = _tempaddress(self._realaddress)
580 tempaddress = _tempaddress(self._realaddress)
577 util.bindunixsocket(sock, tempaddress)
581 util.bindunixsocket(sock, tempaddress)
578 self._socketstat = os.stat(tempaddress)
582 self._socketstat = os.stat(tempaddress)
579 # rename will replace the old socket file if exists atomically. the
583 # rename will replace the old socket file if exists atomically. the
580 # old server will detect ownership change and exit.
584 # old server will detect ownership change and exit.
581 util.rename(tempaddress, self._realaddress)
585 util.rename(tempaddress, self._realaddress)
582
586
583 def _createsymlink(self):
587 def _createsymlink(self):
584 if self._baseaddress == self._realaddress:
588 if self._baseaddress == self._realaddress:
585 return
589 return
586 tempaddress = _tempaddress(self._baseaddress)
590 tempaddress = _tempaddress(self._baseaddress)
587 os.symlink(os.path.basename(self._realaddress), tempaddress)
591 os.symlink(os.path.basename(self._realaddress), tempaddress)
588 util.rename(tempaddress, self._baseaddress)
592 util.rename(tempaddress, self._baseaddress)
589
593
590 def _issocketowner(self):
594 def _issocketowner(self):
591 try:
595 try:
592 stat = os.stat(self._realaddress)
596 stat = os.stat(self._realaddress)
593 return (stat.st_ino == self._socketstat.st_ino and
597 return (stat.st_ino == self._socketstat.st_ino and
594 stat.st_mtime == self._socketstat.st_mtime)
598 stat.st_mtime == self._socketstat.st_mtime)
595 except OSError:
599 except OSError:
596 return False
600 return False
597
601
598 def unlinksocket(self, address):
602 def unlinksocket(self, address):
599 if not self._issocketowner():
603 if not self._issocketowner():
600 return
604 return
601 # it is possible to have a race condition here that we may
605 # it is possible to have a race condition here that we may
602 # remove another server's socket file. but that's okay
606 # remove another server's socket file. but that's okay
603 # since that server will detect and exit automatically and
607 # since that server will detect and exit automatically and
604 # the client will start a new server on demand.
608 # the client will start a new server on demand.
605 try:
609 try:
606 os.unlink(self._realaddress)
610 os.unlink(self._realaddress)
607 except OSError as exc:
611 except OSError as exc:
608 if exc.errno != errno.ENOENT:
612 if exc.errno != errno.ENOENT:
609 raise
613 raise
610
614
611 def printbanner(self, address):
615 def printbanner(self, address):
612 # no "listening at" message should be printed to simulate hg behavior
616 # no "listening at" message should be printed to simulate hg behavior
613 pass
617 pass
614
618
615 def shouldexit(self):
619 def shouldexit(self):
616 if not self._issocketowner():
620 if not self._issocketowner():
617 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
621 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
618 return True
622 return True
619 if time.time() - self._lastactive > self._idletimeout:
623 if time.time() - self._lastactive > self._idletimeout:
620 self.ui.debug('being idle too long. exiting.\n')
624 self.ui.debug('being idle too long. exiting.\n')
621 return True
625 return True
622 return False
626 return False
623
627
624 def newconnection(self):
628 def newconnection(self):
625 self._lastactive = time.time()
629 self._lastactive = time.time()
626
630
627 def createcmdserver(self, repo, conn, fin, fout):
631 def createcmdserver(self, repo, conn, fin, fout):
628 return chgcmdserver(self.ui, repo, fin, fout, conn,
632 return chgcmdserver(self.ui, repo, fin, fout, conn,
629 self._hashstate, self._baseaddress)
633 self._hashstate, self._baseaddress)
630
634
631 def chgunixservice(ui, repo, opts):
635 def chgunixservice(ui, repo, opts):
632 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
636 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
633 # start another chg. drop it to avoid possible side effects.
637 # start another chg. drop it to avoid possible side effects.
634 if 'CHGINTERNALMARK' in encoding.environ:
638 if 'CHGINTERNALMARK' in encoding.environ:
635 del encoding.environ['CHGINTERNALMARK']
639 del encoding.environ['CHGINTERNALMARK']
636
640
637 if repo:
641 if repo:
638 # one chgserver can serve multiple repos. drop repo information
642 # one chgserver can serve multiple repos. drop repo information
639 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
643 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
640 h = chgunixservicehandler(ui)
644 h = chgunixservicehandler(ui)
641 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
645 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now