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