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