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