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