##// END OF EJS Templates
chgserver: store csystem separately...
Jun Wu -
r30643:dfb19aed default
parent child Browse files
Show More
@@ -1,640 +1,641
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 super(chgcmdserver, self).__init__(
331 super(chgcmdserver, self).__init__(
331 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
332 _newchgui(ui, self._csystem), repo, fin, fout)
332 self.clientsock = sock
333 self.clientsock = sock
333 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
334 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
334 self.hashstate = hashstate
335 self.hashstate = hashstate
335 self.baseaddress = baseaddress
336 self.baseaddress = baseaddress
336 if hashstate is not None:
337 if hashstate is not None:
337 self.capabilities = self.capabilities.copy()
338 self.capabilities = self.capabilities.copy()
338 self.capabilities['validate'] = chgcmdserver.validate
339 self.capabilities['validate'] = chgcmdserver.validate
339
340
340 def cleanup(self):
341 def cleanup(self):
341 super(chgcmdserver, self).cleanup()
342 super(chgcmdserver, self).cleanup()
342 # dispatch._runcatch() does not flush outputs if exception is not
343 # dispatch._runcatch() does not flush outputs if exception is not
343 # handled by dispatch._dispatch()
344 # handled by dispatch._dispatch()
344 self.ui.flush()
345 self.ui.flush()
345 self._restoreio()
346 self._restoreio()
346
347
347 def attachio(self):
348 def attachio(self):
348 """Attach to client's stdio passed via unix domain socket; all
349 """Attach to client's stdio passed via unix domain socket; all
349 channels except cresult will no longer be used
350 channels except cresult will no longer be used
350 """
351 """
351 # tell client to sendmsg() with 1-byte payload, which makes it
352 # tell client to sendmsg() with 1-byte payload, which makes it
352 # distinctive from "attachio\n" command consumed by client.read()
353 # distinctive from "attachio\n" command consumed by client.read()
353 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
354 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
354 clientfds = osutil.recvfds(self.clientsock.fileno())
355 clientfds = osutil.recvfds(self.clientsock.fileno())
355 _log('received fds: %r\n' % clientfds)
356 _log('received fds: %r\n' % clientfds)
356
357
357 ui = self.ui
358 ui = self.ui
358 ui.flush()
359 ui.flush()
359 first = self._saveio()
360 first = self._saveio()
360 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
361 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
361 assert fd > 0
362 assert fd > 0
362 fp = getattr(ui, fn)
363 fp = getattr(ui, fn)
363 os.dup2(fd, fp.fileno())
364 os.dup2(fd, fp.fileno())
364 os.close(fd)
365 os.close(fd)
365 if not first:
366 if not first:
366 continue
367 continue
367 # reset buffering mode when client is first attached. as we want
368 # reset buffering mode when client is first attached. as we want
368 # to see output immediately on pager, the mode stays unchanged
369 # to see output immediately on pager, the mode stays unchanged
369 # when client re-attached. ferr is unchanged because it should
370 # when client re-attached. ferr is unchanged because it should
370 # be unbuffered no matter if it is a tty or not.
371 # be unbuffered no matter if it is a tty or not.
371 if fn == 'ferr':
372 if fn == 'ferr':
372 newfp = fp
373 newfp = fp
373 else:
374 else:
374 # make it line buffered explicitly because the default is
375 # make it line buffered explicitly because the default is
375 # decided on first write(), where fout could be a pager.
376 # decided on first write(), where fout could be a pager.
376 if fp.isatty():
377 if fp.isatty():
377 bufsize = 1 # line buffered
378 bufsize = 1 # line buffered
378 else:
379 else:
379 bufsize = -1 # system default
380 bufsize = -1 # system default
380 newfp = os.fdopen(fp.fileno(), mode, bufsize)
381 newfp = os.fdopen(fp.fileno(), mode, bufsize)
381 setattr(ui, fn, newfp)
382 setattr(ui, fn, newfp)
382 setattr(self, cn, newfp)
383 setattr(self, cn, newfp)
383
384
384 self.cresult.write(struct.pack('>i', len(clientfds)))
385 self.cresult.write(struct.pack('>i', len(clientfds)))
385
386
386 def _saveio(self):
387 def _saveio(self):
387 if self._oldios:
388 if self._oldios:
388 return False
389 return False
389 ui = self.ui
390 ui = self.ui
390 for cn, fn, _mode in _iochannels:
391 for cn, fn, _mode in _iochannels:
391 ch = getattr(self, cn)
392 ch = getattr(self, cn)
392 fp = getattr(ui, fn)
393 fp = getattr(ui, fn)
393 fd = os.dup(fp.fileno())
394 fd = os.dup(fp.fileno())
394 self._oldios.append((ch, fp, fd))
395 self._oldios.append((ch, fp, fd))
395 return True
396 return True
396
397
397 def _restoreio(self):
398 def _restoreio(self):
398 ui = self.ui
399 ui = self.ui
399 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):
400 newfp = getattr(ui, fn)
401 newfp = getattr(ui, fn)
401 # close newfp while it's associated with client; otherwise it
402 # close newfp while it's associated with client; otherwise it
402 # would be closed when newfp is deleted
403 # would be closed when newfp is deleted
403 if newfp is not fp:
404 if newfp is not fp:
404 newfp.close()
405 newfp.close()
405 # restore original fd: fp is open again
406 # restore original fd: fp is open again
406 os.dup2(fd, fp.fileno())
407 os.dup2(fd, fp.fileno())
407 os.close(fd)
408 os.close(fd)
408 setattr(self, cn, ch)
409 setattr(self, cn, ch)
409 setattr(ui, fn, fp)
410 setattr(ui, fn, fp)
410 del self._oldios[:]
411 del self._oldios[:]
411
412
412 def validate(self):
413 def validate(self):
413 """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
414
415
415 Read a list of '\0' separated arguments.
416 Read a list of '\0' separated arguments.
416 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'
417 if the list is empty.
418 if the list is empty.
418 An instruction string could be either:
419 An instruction string could be either:
419 - "unlink $path", the client should unlink the path to stop the
420 - "unlink $path", the client should unlink the path to stop the
420 outdated server.
421 outdated server.
421 - "redirect $path", the client should attempt to connect to $path
422 - "redirect $path", the client should attempt to connect to $path
422 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
423 "reconnect".
424 "reconnect".
424 - "exit $n", the client should exit directly with code n.
425 - "exit $n", the client should exit directly with code n.
425 This may happen if we cannot parse the config.
426 This may happen if we cannot parse the config.
426 - "reconnect", the client should close the connection and
427 - "reconnect", the client should close the connection and
427 reconnect.
428 reconnect.
428 If neither "reconnect" nor "redirect" is included in the instruction
429 If neither "reconnect" nor "redirect" is included in the instruction
429 list, the client can continue with this server after completing all
430 list, the client can continue with this server after completing all
430 the instructions.
431 the instructions.
431 """
432 """
432 from . import dispatch # avoid cycle
433 from . import dispatch # avoid cycle
433
434
434 args = self._readlist()
435 args = self._readlist()
435 try:
436 try:
436 self.ui, lui = _loadnewui(self.ui, args)
437 self.ui, lui = _loadnewui(self.ui, args)
437 except error.ParseError as inst:
438 except error.ParseError as inst:
438 dispatch._formatparse(self.ui.warn, inst)
439 dispatch._formatparse(self.ui.warn, inst)
439 self.ui.flush()
440 self.ui.flush()
440 self.cresult.write('exit 255')
441 self.cresult.write('exit 255')
441 return
442 return
442 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
443 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
443 insts = []
444 insts = []
444 if newhash.mtimehash != self.hashstate.mtimehash:
445 if newhash.mtimehash != self.hashstate.mtimehash:
445 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
446 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
446 insts.append('unlink %s' % addr)
447 insts.append('unlink %s' % addr)
447 # mtimehash is empty if one or more extensions fail to load.
448 # mtimehash is empty if one or more extensions fail to load.
448 # to be compatible with hg, still serve the client this time.
449 # to be compatible with hg, still serve the client this time.
449 if self.hashstate.mtimehash:
450 if self.hashstate.mtimehash:
450 insts.append('reconnect')
451 insts.append('reconnect')
451 if newhash.confighash != self.hashstate.confighash:
452 if newhash.confighash != self.hashstate.confighash:
452 addr = _hashaddress(self.baseaddress, newhash.confighash)
453 addr = _hashaddress(self.baseaddress, newhash.confighash)
453 insts.append('redirect %s' % addr)
454 insts.append('redirect %s' % addr)
454 _log('validate: %s\n' % insts)
455 _log('validate: %s\n' % insts)
455 self.cresult.write('\0'.join(insts) or '\0')
456 self.cresult.write('\0'.join(insts) or '\0')
456
457
457 def chdir(self):
458 def chdir(self):
458 """Change current directory
459 """Change current directory
459
460
460 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.
461 It does not affect --config parameter.
462 It does not affect --config parameter.
462 """
463 """
463 path = self._readstr()
464 path = self._readstr()
464 if not path:
465 if not path:
465 return
466 return
466 _log('chdir to %r\n' % path)
467 _log('chdir to %r\n' % path)
467 os.chdir(path)
468 os.chdir(path)
468
469
469 def setumask(self):
470 def setumask(self):
470 """Change umask"""
471 """Change umask"""
471 mask = struct.unpack('>I', self._read(4))[0]
472 mask = struct.unpack('>I', self._read(4))[0]
472 _log('setumask %r\n' % mask)
473 _log('setumask %r\n' % mask)
473 os.umask(mask)
474 os.umask(mask)
474
475
475 def getpager(self):
476 def getpager(self):
476 """Read cmdargs and write pager command to r-channel if enabled
477 """Read cmdargs and write pager command to r-channel if enabled
477
478
478 If pager isn't enabled, this writes '\0' because channeledoutput
479 If pager isn't enabled, this writes '\0' because channeledoutput
479 does not allow to write empty data.
480 does not allow to write empty data.
480 """
481 """
481 from . import dispatch # avoid cycle
482 from . import dispatch # avoid cycle
482
483
483 args = self._readlist()
484 args = self._readlist()
484 try:
485 try:
485 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
486 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
486 args)
487 args)
487 except (error.Abort, error.AmbiguousCommand, error.CommandError,
488 except (error.Abort, error.AmbiguousCommand, error.CommandError,
488 error.UnknownCommand):
489 error.UnknownCommand):
489 cmd = None
490 cmd = None
490 options = {}
491 options = {}
491 if not cmd or 'pager' not in options:
492 if not cmd or 'pager' not in options:
492 self.cresult.write('\0')
493 self.cresult.write('\0')
493 return
494 return
494
495
495 pagercmd = _setuppagercmd(self.ui, options, cmd)
496 pagercmd = _setuppagercmd(self.ui, options, cmd)
496 if pagercmd:
497 if pagercmd:
497 # 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
498 # we can exit if the pipe to the pager is closed
499 # we can exit if the pipe to the pager is closed
499 if util.safehasattr(signal, 'SIGPIPE') and \
500 if util.safehasattr(signal, 'SIGPIPE') and \
500 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
501 signal.getsignal(signal.SIGPIPE) == signal.SIG_IGN:
501 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
502 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
502 self.cresult.write(pagercmd)
503 self.cresult.write(pagercmd)
503 else:
504 else:
504 self.cresult.write('\0')
505 self.cresult.write('\0')
505
506
506 def setenv(self):
507 def setenv(self):
507 """Clear and update os.environ
508 """Clear and update os.environ
508
509
509 Note that not all variables can make an effect on the running process.
510 Note that not all variables can make an effect on the running process.
510 """
511 """
511 l = self._readlist()
512 l = self._readlist()
512 try:
513 try:
513 newenv = dict(s.split('=', 1) for s in l)
514 newenv = dict(s.split('=', 1) for s in l)
514 except ValueError:
515 except ValueError:
515 raise ValueError('unexpected value in setenv request')
516 raise ValueError('unexpected value in setenv request')
516 _log('setenv: %r\n' % sorted(newenv.keys()))
517 _log('setenv: %r\n' % sorted(newenv.keys()))
517 encoding.environ.clear()
518 encoding.environ.clear()
518 encoding.environ.update(newenv)
519 encoding.environ.update(newenv)
519
520
520 capabilities = commandserver.server.capabilities.copy()
521 capabilities = commandserver.server.capabilities.copy()
521 capabilities.update({'attachio': attachio,
522 capabilities.update({'attachio': attachio,
522 'chdir': chdir,
523 'chdir': chdir,
523 'getpager': getpager,
524 'getpager': getpager,
524 'setenv': setenv,
525 'setenv': setenv,
525 'setumask': setumask})
526 'setumask': setumask})
526
527
527 def _tempaddress(address):
528 def _tempaddress(address):
528 return '%s.%d.tmp' % (address, os.getpid())
529 return '%s.%d.tmp' % (address, os.getpid())
529
530
530 def _hashaddress(address, hashstr):
531 def _hashaddress(address, hashstr):
531 # if the basename of address contains '.', use only the left part. this
532 # if the basename of address contains '.', use only the left part. this
532 # makes it possible for the client to pass 'server.tmp$PID' and follow by
533 # makes it possible for the client to pass 'server.tmp$PID' and follow by
533 # an atomic rename to avoid locking when spawning new servers.
534 # an atomic rename to avoid locking when spawning new servers.
534 dirname, basename = os.path.split(address)
535 dirname, basename = os.path.split(address)
535 basename = basename.split('.', 1)[0]
536 basename = basename.split('.', 1)[0]
536 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
537 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
537
538
538 class chgunixservicehandler(object):
539 class chgunixservicehandler(object):
539 """Set of operations for chg services"""
540 """Set of operations for chg services"""
540
541
541 pollinterval = 1 # [sec]
542 pollinterval = 1 # [sec]
542
543
543 def __init__(self, ui):
544 def __init__(self, ui):
544 self.ui = ui
545 self.ui = ui
545 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
546 self._idletimeout = ui.configint('chgserver', 'idletimeout', 3600)
546 self._lastactive = time.time()
547 self._lastactive = time.time()
547
548
548 def bindsocket(self, sock, address):
549 def bindsocket(self, sock, address):
549 self._inithashstate(address)
550 self._inithashstate(address)
550 self._checkextensions()
551 self._checkextensions()
551 self._bind(sock)
552 self._bind(sock)
552 self._createsymlink()
553 self._createsymlink()
553
554
554 def _inithashstate(self, address):
555 def _inithashstate(self, address):
555 self._baseaddress = address
556 self._baseaddress = address
556 if self.ui.configbool('chgserver', 'skiphash', False):
557 if self.ui.configbool('chgserver', 'skiphash', False):
557 self._hashstate = None
558 self._hashstate = None
558 self._realaddress = address
559 self._realaddress = address
559 return
560 return
560 self._hashstate = hashstate.fromui(self.ui)
561 self._hashstate = hashstate.fromui(self.ui)
561 self._realaddress = _hashaddress(address, self._hashstate.confighash)
562 self._realaddress = _hashaddress(address, self._hashstate.confighash)
562
563
563 def _checkextensions(self):
564 def _checkextensions(self):
564 if not self._hashstate:
565 if not self._hashstate:
565 return
566 return
566 if extensions.notloaded():
567 if extensions.notloaded():
567 # one or more extensions failed to load. mtimehash becomes
568 # one or more extensions failed to load. mtimehash becomes
568 # meaningless because we do not know the paths of those extensions.
569 # meaningless because we do not know the paths of those extensions.
569 # set mtimehash to an illegal hash value to invalidate the server.
570 # set mtimehash to an illegal hash value to invalidate the server.
570 self._hashstate.mtimehash = ''
571 self._hashstate.mtimehash = ''
571
572
572 def _bind(self, sock):
573 def _bind(self, sock):
573 # use a unique temp address so we can stat the file and do ownership
574 # use a unique temp address so we can stat the file and do ownership
574 # check later
575 # check later
575 tempaddress = _tempaddress(self._realaddress)
576 tempaddress = _tempaddress(self._realaddress)
576 util.bindunixsocket(sock, tempaddress)
577 util.bindunixsocket(sock, tempaddress)
577 self._socketstat = os.stat(tempaddress)
578 self._socketstat = os.stat(tempaddress)
578 # rename will replace the old socket file if exists atomically. the
579 # rename will replace the old socket file if exists atomically. the
579 # old server will detect ownership change and exit.
580 # old server will detect ownership change and exit.
580 util.rename(tempaddress, self._realaddress)
581 util.rename(tempaddress, self._realaddress)
581
582
582 def _createsymlink(self):
583 def _createsymlink(self):
583 if self._baseaddress == self._realaddress:
584 if self._baseaddress == self._realaddress:
584 return
585 return
585 tempaddress = _tempaddress(self._baseaddress)
586 tempaddress = _tempaddress(self._baseaddress)
586 os.symlink(os.path.basename(self._realaddress), tempaddress)
587 os.symlink(os.path.basename(self._realaddress), tempaddress)
587 util.rename(tempaddress, self._baseaddress)
588 util.rename(tempaddress, self._baseaddress)
588
589
589 def _issocketowner(self):
590 def _issocketowner(self):
590 try:
591 try:
591 stat = os.stat(self._realaddress)
592 stat = os.stat(self._realaddress)
592 return (stat.st_ino == self._socketstat.st_ino and
593 return (stat.st_ino == self._socketstat.st_ino and
593 stat.st_mtime == self._socketstat.st_mtime)
594 stat.st_mtime == self._socketstat.st_mtime)
594 except OSError:
595 except OSError:
595 return False
596 return False
596
597
597 def unlinksocket(self, address):
598 def unlinksocket(self, address):
598 if not self._issocketowner():
599 if not self._issocketowner():
599 return
600 return
600 # it is possible to have a race condition here that we may
601 # it is possible to have a race condition here that we may
601 # remove another server's socket file. but that's okay
602 # remove another server's socket file. but that's okay
602 # since that server will detect and exit automatically and
603 # since that server will detect and exit automatically and
603 # the client will start a new server on demand.
604 # the client will start a new server on demand.
604 try:
605 try:
605 os.unlink(self._realaddress)
606 os.unlink(self._realaddress)
606 except OSError as exc:
607 except OSError as exc:
607 if exc.errno != errno.ENOENT:
608 if exc.errno != errno.ENOENT:
608 raise
609 raise
609
610
610 def printbanner(self, address):
611 def printbanner(self, address):
611 # no "listening at" message should be printed to simulate hg behavior
612 # no "listening at" message should be printed to simulate hg behavior
612 pass
613 pass
613
614
614 def shouldexit(self):
615 def shouldexit(self):
615 if not self._issocketowner():
616 if not self._issocketowner():
616 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
617 self.ui.debug('%s is not owned, exiting.\n' % self._realaddress)
617 return True
618 return True
618 if time.time() - self._lastactive > self._idletimeout:
619 if time.time() - self._lastactive > self._idletimeout:
619 self.ui.debug('being idle too long. exiting.\n')
620 self.ui.debug('being idle too long. exiting.\n')
620 return True
621 return True
621 return False
622 return False
622
623
623 def newconnection(self):
624 def newconnection(self):
624 self._lastactive = time.time()
625 self._lastactive = time.time()
625
626
626 def createcmdserver(self, repo, conn, fin, fout):
627 def createcmdserver(self, repo, conn, fin, fout):
627 return chgcmdserver(self.ui, repo, fin, fout, conn,
628 return chgcmdserver(self.ui, repo, fin, fout, conn,
628 self._hashstate, self._baseaddress)
629 self._hashstate, self._baseaddress)
629
630
630 def chgunixservice(ui, repo, opts):
631 def chgunixservice(ui, repo, opts):
631 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
632 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
632 # start another chg. drop it to avoid possible side effects.
633 # start another chg. drop it to avoid possible side effects.
633 if 'CHGINTERNALMARK' in encoding.environ:
634 if 'CHGINTERNALMARK' in encoding.environ:
634 del encoding.environ['CHGINTERNALMARK']
635 del encoding.environ['CHGINTERNALMARK']
635
636
636 if repo:
637 if repo:
637 # one chgserver can serve multiple repos. drop repo information
638 # one chgserver can serve multiple repos. drop repo information
638 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
639 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
639 h = chgunixservicehandler(ui)
640 h = chgunixservicehandler(ui)
640 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
641 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now