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