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