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