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