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