##// END OF EJS Templates
commandserver: unindent superfluous "if True" blocks
Yuya Nishihara -
r29585:6ed452d0 default
parent child Browse files
Show More
@@ -1,644 +1,643
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.address = address
550 self._inithashstate()
550 self._inithashstate()
551 self._checkextensions()
551 self._checkextensions()
552 self._bind(sock)
552 self._bind(sock)
553 self._createsymlink()
553 self._createsymlink()
554
554
555 def _inithashstate(self):
555 def _inithashstate(self):
556 self.baseaddress = self.address
556 self.baseaddress = self.address
557 if self.ui.configbool('chgserver', 'skiphash', False):
557 if self.ui.configbool('chgserver', 'skiphash', False):
558 self.hashstate = None
558 self.hashstate = None
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(self.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 _createsymlink(self):
572 def _createsymlink(self):
573 if self.baseaddress == self.address:
573 if self.baseaddress == self.address:
574 return
574 return
575 tempaddress = _tempaddress(self.baseaddress)
575 tempaddress = _tempaddress(self.baseaddress)
576 os.symlink(os.path.basename(self.address), tempaddress)
576 os.symlink(os.path.basename(self.address), tempaddress)
577 util.rename(tempaddress, self.baseaddress)
577 util.rename(tempaddress, self.baseaddress)
578
578
579 def printbanner(self, address):
579 def printbanner(self, address):
580 # no "listening at" message should be printed to simulate hg behavior
580 # no "listening at" message should be printed to simulate hg behavior
581 pass
581 pass
582
582
583 def shouldexit(self):
583 def shouldexit(self):
584 if True: # TODO: unindent
584 if not self.issocketowner():
585 if not self.issocketowner():
585 _log('%s is not owned, exiting.\n' % self.address)
586 _log('%s is not owned, exiting.\n' % self.address)
586 return True
587 return True
587 if time.time() - self.lastactive > self.idletimeout:
588 if time.time() - self.lastactive > self.idletimeout:
588 _log('being idle too long. exiting.\n')
589 _log('being idle too long. exiting.\n')
589 return True
590 return True
591 return False
590 return False
592
591
593 def newconnection(self):
592 def newconnection(self):
594 self.lastactive = time.time()
593 self.lastactive = time.time()
595
594
596 def _bind(self, sock):
595 def _bind(self, sock):
597 # use a unique temp address so we can stat the file and do ownership
596 # use a unique temp address so we can stat the file and do ownership
598 # check later
597 # check later
599 tempaddress = _tempaddress(self.address)
598 tempaddress = _tempaddress(self.address)
600 util.bindunixsocket(sock, tempaddress)
599 util.bindunixsocket(sock, tempaddress)
601 self._socketstat = os.stat(tempaddress)
600 self._socketstat = os.stat(tempaddress)
602 # rename will replace the old socket file if exists atomically. the
601 # rename will replace the old socket file if exists atomically. the
603 # old server will detect ownership change and exit.
602 # old server will detect ownership change and exit.
604 util.rename(tempaddress, self.address)
603 util.rename(tempaddress, self.address)
605
604
606 def issocketowner(self):
605 def issocketowner(self):
607 try:
606 try:
608 stat = os.stat(self.address)
607 stat = os.stat(self.address)
609 return (stat.st_ino == self._socketstat.st_ino and
608 return (stat.st_ino == self._socketstat.st_ino and
610 stat.st_mtime == self._socketstat.st_mtime)
609 stat.st_mtime == self._socketstat.st_mtime)
611 except OSError:
610 except OSError:
612 return False
611 return False
613
612
614 def unlinksocket(self, address):
613 def unlinksocket(self, address):
615 if not self.issocketowner():
614 if not self.issocketowner():
616 return
615 return
617 # it is possible to have a race condition here that we may
616 # it is possible to have a race condition here that we may
618 # remove another server's socket file. but that's okay
617 # remove another server's socket file. but that's okay
619 # since that server will detect and exit automatically and
618 # since that server will detect and exit automatically and
620 # the client will start a new server on demand.
619 # the client will start a new server on demand.
621 try:
620 try:
622 os.unlink(self.address)
621 os.unlink(self.address)
623 except OSError as exc:
622 except OSError as exc:
624 if exc.errno != errno.ENOENT:
623 if exc.errno != errno.ENOENT:
625 raise
624 raise
626
625
627 def createcmdserver(self, repo, conn, fin, fout):
626 def createcmdserver(self, repo, conn, fin, fout):
628 return chgcmdserver(self.ui, repo, fin, fout, conn,
627 return chgcmdserver(self.ui, repo, fin, fout, conn,
629 self.hashstate, self.baseaddress)
628 self.hashstate, self.baseaddress)
630
629
631 def chgunixservice(ui, repo, opts):
630 def chgunixservice(ui, repo, opts):
632 if repo:
631 if repo:
633 # one chgserver can serve multiple repos. drop repo infomation
632 # one chgserver can serve multiple repos. drop repo infomation
634 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
633 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
635 h = chgunixservicehandler(ui)
634 h = chgunixservicehandler(ui)
636 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
635 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
637
636
638 def uisetup(ui):
637 def uisetup(ui):
639 commandserver._servicemap['chgunix'] = chgunixservice
638 commandserver._servicemap['chgunix'] = chgunixservice
640
639
641 # 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
642 # start another chg. drop it to avoid possible side effects.
641 # start another chg. drop it to avoid possible side effects.
643 if 'CHGINTERNALMARK' in os.environ:
642 if 'CHGINTERNALMARK' in os.environ:
644 del os.environ['CHGINTERNALMARK']
643 del os.environ['CHGINTERNALMARK']
@@ -1,534 +1,533
1 # commandserver.py - communicate with Mercurial's API over a pipe
1 # commandserver.py - communicate with Mercurial's API over a pipe
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import gc
11 import gc
12 import os
12 import os
13 import random
13 import random
14 import select
14 import select
15 import signal
15 import signal
16 import socket
16 import socket
17 import struct
17 import struct
18 import sys
18 import sys
19 import traceback
19 import traceback
20
20
21 from .i18n import _
21 from .i18n import _
22 from . import (
22 from . import (
23 encoding,
23 encoding,
24 error,
24 error,
25 util,
25 util,
26 )
26 )
27
27
28 logfile = None
28 logfile = None
29
29
30 def log(*args):
30 def log(*args):
31 if not logfile:
31 if not logfile:
32 return
32 return
33
33
34 for a in args:
34 for a in args:
35 logfile.write(str(a))
35 logfile.write(str(a))
36
36
37 logfile.flush()
37 logfile.flush()
38
38
39 class channeledoutput(object):
39 class channeledoutput(object):
40 """
40 """
41 Write data to out in the following format:
41 Write data to out in the following format:
42
42
43 data length (unsigned int),
43 data length (unsigned int),
44 data
44 data
45 """
45 """
46 def __init__(self, out, channel):
46 def __init__(self, out, channel):
47 self.out = out
47 self.out = out
48 self.channel = channel
48 self.channel = channel
49
49
50 @property
50 @property
51 def name(self):
51 def name(self):
52 return '<%c-channel>' % self.channel
52 return '<%c-channel>' % self.channel
53
53
54 def write(self, data):
54 def write(self, data):
55 if not data:
55 if not data:
56 return
56 return
57 self.out.write(struct.pack('>cI', self.channel, len(data)))
57 self.out.write(struct.pack('>cI', self.channel, len(data)))
58 self.out.write(data)
58 self.out.write(data)
59 self.out.flush()
59 self.out.flush()
60
60
61 def __getattr__(self, attr):
61 def __getattr__(self, attr):
62 if attr in ('isatty', 'fileno', 'tell', 'seek'):
62 if attr in ('isatty', 'fileno', 'tell', 'seek'):
63 raise AttributeError(attr)
63 raise AttributeError(attr)
64 return getattr(self.out, attr)
64 return getattr(self.out, attr)
65
65
66 class channeledinput(object):
66 class channeledinput(object):
67 """
67 """
68 Read data from in_.
68 Read data from in_.
69
69
70 Requests for input are written to out in the following format:
70 Requests for input are written to out in the following format:
71 channel identifier - 'I' for plain input, 'L' line based (1 byte)
71 channel identifier - 'I' for plain input, 'L' line based (1 byte)
72 how many bytes to send at most (unsigned int),
72 how many bytes to send at most (unsigned int),
73
73
74 The client replies with:
74 The client replies with:
75 data length (unsigned int), 0 meaning EOF
75 data length (unsigned int), 0 meaning EOF
76 data
76 data
77 """
77 """
78
78
79 maxchunksize = 4 * 1024
79 maxchunksize = 4 * 1024
80
80
81 def __init__(self, in_, out, channel):
81 def __init__(self, in_, out, channel):
82 self.in_ = in_
82 self.in_ = in_
83 self.out = out
83 self.out = out
84 self.channel = channel
84 self.channel = channel
85
85
86 @property
86 @property
87 def name(self):
87 def name(self):
88 return '<%c-channel>' % self.channel
88 return '<%c-channel>' % self.channel
89
89
90 def read(self, size=-1):
90 def read(self, size=-1):
91 if size < 0:
91 if size < 0:
92 # if we need to consume all the clients input, ask for 4k chunks
92 # if we need to consume all the clients input, ask for 4k chunks
93 # so the pipe doesn't fill up risking a deadlock
93 # so the pipe doesn't fill up risking a deadlock
94 size = self.maxchunksize
94 size = self.maxchunksize
95 s = self._read(size, self.channel)
95 s = self._read(size, self.channel)
96 buf = s
96 buf = s
97 while s:
97 while s:
98 s = self._read(size, self.channel)
98 s = self._read(size, self.channel)
99 buf += s
99 buf += s
100
100
101 return buf
101 return buf
102 else:
102 else:
103 return self._read(size, self.channel)
103 return self._read(size, self.channel)
104
104
105 def _read(self, size, channel):
105 def _read(self, size, channel):
106 if not size:
106 if not size:
107 return ''
107 return ''
108 assert size > 0
108 assert size > 0
109
109
110 # tell the client we need at most size bytes
110 # tell the client we need at most size bytes
111 self.out.write(struct.pack('>cI', channel, size))
111 self.out.write(struct.pack('>cI', channel, size))
112 self.out.flush()
112 self.out.flush()
113
113
114 length = self.in_.read(4)
114 length = self.in_.read(4)
115 length = struct.unpack('>I', length)[0]
115 length = struct.unpack('>I', length)[0]
116 if not length:
116 if not length:
117 return ''
117 return ''
118 else:
118 else:
119 return self.in_.read(length)
119 return self.in_.read(length)
120
120
121 def readline(self, size=-1):
121 def readline(self, size=-1):
122 if size < 0:
122 if size < 0:
123 size = self.maxchunksize
123 size = self.maxchunksize
124 s = self._read(size, 'L')
124 s = self._read(size, 'L')
125 buf = s
125 buf = s
126 # keep asking for more until there's either no more or
126 # keep asking for more until there's either no more or
127 # we got a full line
127 # we got a full line
128 while s and s[-1] != '\n':
128 while s and s[-1] != '\n':
129 s = self._read(size, 'L')
129 s = self._read(size, 'L')
130 buf += s
130 buf += s
131
131
132 return buf
132 return buf
133 else:
133 else:
134 return self._read(size, 'L')
134 return self._read(size, 'L')
135
135
136 def __iter__(self):
136 def __iter__(self):
137 return self
137 return self
138
138
139 def next(self):
139 def next(self):
140 l = self.readline()
140 l = self.readline()
141 if not l:
141 if not l:
142 raise StopIteration
142 raise StopIteration
143 return l
143 return l
144
144
145 def __getattr__(self, attr):
145 def __getattr__(self, attr):
146 if attr in ('isatty', 'fileno', 'tell', 'seek'):
146 if attr in ('isatty', 'fileno', 'tell', 'seek'):
147 raise AttributeError(attr)
147 raise AttributeError(attr)
148 return getattr(self.in_, attr)
148 return getattr(self.in_, attr)
149
149
150 class server(object):
150 class server(object):
151 """
151 """
152 Listens for commands on fin, runs them and writes the output on a channel
152 Listens for commands on fin, runs them and writes the output on a channel
153 based stream to fout.
153 based stream to fout.
154 """
154 """
155 def __init__(self, ui, repo, fin, fout):
155 def __init__(self, ui, repo, fin, fout):
156 self.cwd = os.getcwd()
156 self.cwd = os.getcwd()
157
157
158 # developer config: cmdserver.log
158 # developer config: cmdserver.log
159 logpath = ui.config("cmdserver", "log", None)
159 logpath = ui.config("cmdserver", "log", None)
160 if logpath:
160 if logpath:
161 global logfile
161 global logfile
162 if logpath == '-':
162 if logpath == '-':
163 # write log on a special 'd' (debug) channel
163 # write log on a special 'd' (debug) channel
164 logfile = channeledoutput(fout, 'd')
164 logfile = channeledoutput(fout, 'd')
165 else:
165 else:
166 logfile = open(logpath, 'a')
166 logfile = open(logpath, 'a')
167
167
168 if repo:
168 if repo:
169 # the ui here is really the repo ui so take its baseui so we don't
169 # the ui here is really the repo ui so take its baseui so we don't
170 # end up with its local configuration
170 # end up with its local configuration
171 self.ui = repo.baseui
171 self.ui = repo.baseui
172 self.repo = repo
172 self.repo = repo
173 self.repoui = repo.ui
173 self.repoui = repo.ui
174 else:
174 else:
175 self.ui = ui
175 self.ui = ui
176 self.repo = self.repoui = None
176 self.repo = self.repoui = None
177
177
178 self.cerr = channeledoutput(fout, 'e')
178 self.cerr = channeledoutput(fout, 'e')
179 self.cout = channeledoutput(fout, 'o')
179 self.cout = channeledoutput(fout, 'o')
180 self.cin = channeledinput(fin, fout, 'I')
180 self.cin = channeledinput(fin, fout, 'I')
181 self.cresult = channeledoutput(fout, 'r')
181 self.cresult = channeledoutput(fout, 'r')
182
182
183 self.client = fin
183 self.client = fin
184
184
185 def cleanup(self):
185 def cleanup(self):
186 """release and restore resources taken during server session"""
186 """release and restore resources taken during server session"""
187 pass
187 pass
188
188
189 def _read(self, size):
189 def _read(self, size):
190 if not size:
190 if not size:
191 return ''
191 return ''
192
192
193 data = self.client.read(size)
193 data = self.client.read(size)
194
194
195 # is the other end closed?
195 # is the other end closed?
196 if not data:
196 if not data:
197 raise EOFError
197 raise EOFError
198
198
199 return data
199 return data
200
200
201 def _readstr(self):
201 def _readstr(self):
202 """read a string from the channel
202 """read a string from the channel
203
203
204 format:
204 format:
205 data length (uint32), data
205 data length (uint32), data
206 """
206 """
207 length = struct.unpack('>I', self._read(4))[0]
207 length = struct.unpack('>I', self._read(4))[0]
208 if not length:
208 if not length:
209 return ''
209 return ''
210 return self._read(length)
210 return self._read(length)
211
211
212 def _readlist(self):
212 def _readlist(self):
213 """read a list of NULL separated strings from the channel"""
213 """read a list of NULL separated strings from the channel"""
214 s = self._readstr()
214 s = self._readstr()
215 if s:
215 if s:
216 return s.split('\0')
216 return s.split('\0')
217 else:
217 else:
218 return []
218 return []
219
219
220 def runcommand(self):
220 def runcommand(self):
221 """ reads a list of \0 terminated arguments, executes
221 """ reads a list of \0 terminated arguments, executes
222 and writes the return code to the result channel """
222 and writes the return code to the result channel """
223 from . import dispatch # avoid cycle
223 from . import dispatch # avoid cycle
224
224
225 args = self._readlist()
225 args = self._readlist()
226
226
227 # copy the uis so changes (e.g. --config or --verbose) don't
227 # copy the uis so changes (e.g. --config or --verbose) don't
228 # persist between requests
228 # persist between requests
229 copiedui = self.ui.copy()
229 copiedui = self.ui.copy()
230 uis = [copiedui]
230 uis = [copiedui]
231 if self.repo:
231 if self.repo:
232 self.repo.baseui = copiedui
232 self.repo.baseui = copiedui
233 # clone ui without using ui.copy because this is protected
233 # clone ui without using ui.copy because this is protected
234 repoui = self.repoui.__class__(self.repoui)
234 repoui = self.repoui.__class__(self.repoui)
235 repoui.copy = copiedui.copy # redo copy protection
235 repoui.copy = copiedui.copy # redo copy protection
236 uis.append(repoui)
236 uis.append(repoui)
237 self.repo.ui = self.repo.dirstate._ui = repoui
237 self.repo.ui = self.repo.dirstate._ui = repoui
238 self.repo.invalidateall()
238 self.repo.invalidateall()
239
239
240 for ui in uis:
240 for ui in uis:
241 ui.resetstate()
241 ui.resetstate()
242 # any kind of interaction must use server channels, but chg may
242 # any kind of interaction must use server channels, but chg may
243 # replace channels by fully functional tty files. so nontty is
243 # replace channels by fully functional tty files. so nontty is
244 # enforced only if cin is a channel.
244 # enforced only if cin is a channel.
245 if not util.safehasattr(self.cin, 'fileno'):
245 if not util.safehasattr(self.cin, 'fileno'):
246 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
246 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
247
247
248 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
248 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
249 self.cout, self.cerr)
249 self.cout, self.cerr)
250
250
251 ret = (dispatch.dispatch(req) or 0) & 255 # might return None
251 ret = (dispatch.dispatch(req) or 0) & 255 # might return None
252
252
253 # restore old cwd
253 # restore old cwd
254 if '--cwd' in args:
254 if '--cwd' in args:
255 os.chdir(self.cwd)
255 os.chdir(self.cwd)
256
256
257 self.cresult.write(struct.pack('>i', int(ret)))
257 self.cresult.write(struct.pack('>i', int(ret)))
258
258
259 def getencoding(self):
259 def getencoding(self):
260 """ writes the current encoding to the result channel """
260 """ writes the current encoding to the result channel """
261 self.cresult.write(encoding.encoding)
261 self.cresult.write(encoding.encoding)
262
262
263 def serveone(self):
263 def serveone(self):
264 cmd = self.client.readline()[:-1]
264 cmd = self.client.readline()[:-1]
265 if cmd:
265 if cmd:
266 handler = self.capabilities.get(cmd)
266 handler = self.capabilities.get(cmd)
267 if handler:
267 if handler:
268 handler(self)
268 handler(self)
269 else:
269 else:
270 # clients are expected to check what commands are supported by
270 # clients are expected to check what commands are supported by
271 # looking at the servers capabilities
271 # looking at the servers capabilities
272 raise error.Abort(_('unknown command %s') % cmd)
272 raise error.Abort(_('unknown command %s') % cmd)
273
273
274 return cmd != ''
274 return cmd != ''
275
275
276 capabilities = {'runcommand' : runcommand,
276 capabilities = {'runcommand' : runcommand,
277 'getencoding' : getencoding}
277 'getencoding' : getencoding}
278
278
279 def serve(self):
279 def serve(self):
280 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
280 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
281 hellomsg += '\n'
281 hellomsg += '\n'
282 hellomsg += 'encoding: ' + encoding.encoding
282 hellomsg += 'encoding: ' + encoding.encoding
283 hellomsg += '\n'
283 hellomsg += '\n'
284 hellomsg += 'pid: %d' % util.getpid()
284 hellomsg += 'pid: %d' % util.getpid()
285 if util.safehasattr(os, 'getpgid'):
285 if util.safehasattr(os, 'getpgid'):
286 hellomsg += '\n'
286 hellomsg += '\n'
287 hellomsg += 'pgid: %d' % os.getpgid(0)
287 hellomsg += 'pgid: %d' % os.getpgid(0)
288
288
289 # write the hello msg in -one- chunk
289 # write the hello msg in -one- chunk
290 self.cout.write(hellomsg)
290 self.cout.write(hellomsg)
291
291
292 try:
292 try:
293 while self.serveone():
293 while self.serveone():
294 pass
294 pass
295 except EOFError:
295 except EOFError:
296 # we'll get here if the client disconnected while we were reading
296 # we'll get here if the client disconnected while we were reading
297 # its request
297 # its request
298 return 1
298 return 1
299
299
300 return 0
300 return 0
301
301
302 def _protectio(ui):
302 def _protectio(ui):
303 """ duplicates streams and redirect original to null if ui uses stdio """
303 """ duplicates streams and redirect original to null if ui uses stdio """
304 ui.flush()
304 ui.flush()
305 newfiles = []
305 newfiles = []
306 nullfd = os.open(os.devnull, os.O_RDWR)
306 nullfd = os.open(os.devnull, os.O_RDWR)
307 for f, sysf, mode in [(ui.fin, sys.stdin, 'rb'),
307 for f, sysf, mode in [(ui.fin, sys.stdin, 'rb'),
308 (ui.fout, sys.stdout, 'wb')]:
308 (ui.fout, sys.stdout, 'wb')]:
309 if f is sysf:
309 if f is sysf:
310 newfd = os.dup(f.fileno())
310 newfd = os.dup(f.fileno())
311 os.dup2(nullfd, f.fileno())
311 os.dup2(nullfd, f.fileno())
312 f = os.fdopen(newfd, mode)
312 f = os.fdopen(newfd, mode)
313 newfiles.append(f)
313 newfiles.append(f)
314 os.close(nullfd)
314 os.close(nullfd)
315 return tuple(newfiles)
315 return tuple(newfiles)
316
316
317 def _restoreio(ui, fin, fout):
317 def _restoreio(ui, fin, fout):
318 """ restores streams from duplicated ones """
318 """ restores streams from duplicated ones """
319 ui.flush()
319 ui.flush()
320 for f, uif in [(fin, ui.fin), (fout, ui.fout)]:
320 for f, uif in [(fin, ui.fin), (fout, ui.fout)]:
321 if f is not uif:
321 if f is not uif:
322 os.dup2(f.fileno(), uif.fileno())
322 os.dup2(f.fileno(), uif.fileno())
323 f.close()
323 f.close()
324
324
325 class pipeservice(object):
325 class pipeservice(object):
326 def __init__(self, ui, repo, opts):
326 def __init__(self, ui, repo, opts):
327 self.ui = ui
327 self.ui = ui
328 self.repo = repo
328 self.repo = repo
329
329
330 def init(self):
330 def init(self):
331 pass
331 pass
332
332
333 def run(self):
333 def run(self):
334 ui = self.ui
334 ui = self.ui
335 # redirect stdio to null device so that broken extensions or in-process
335 # redirect stdio to null device so that broken extensions or in-process
336 # hooks will never cause corruption of channel protocol.
336 # hooks will never cause corruption of channel protocol.
337 fin, fout = _protectio(ui)
337 fin, fout = _protectio(ui)
338 try:
338 try:
339 sv = server(ui, self.repo, fin, fout)
339 sv = server(ui, self.repo, fin, fout)
340 return sv.serve()
340 return sv.serve()
341 finally:
341 finally:
342 sv.cleanup()
342 sv.cleanup()
343 _restoreio(ui, fin, fout)
343 _restoreio(ui, fin, fout)
344
344
345 def _serverequest(ui, repo, conn, createcmdserver):
345 def _serverequest(ui, repo, conn, createcmdserver):
346 if True: # TODO: unindent
346 # use a different process group from the master process, making this
347 # use a different process group from the master process, making this
347 # process pass kernel "is_current_pgrp_orphaned" check so signals like
348 # process pass kernel "is_current_pgrp_orphaned" check so signals like
348 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
349 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
349 os.setpgid(0, 0)
350 os.setpgid(0, 0)
350 # change random state otherwise forked request handlers would have a
351 # change random state otherwise forked request handlers would have a
351 # same state inherited from parent.
352 # same state inherited from parent.
352 random.seed()
353 random.seed()
354
353
355 fin = conn.makefile('rb')
354 fin = conn.makefile('rb')
356 fout = conn.makefile('wb')
355 fout = conn.makefile('wb')
357 sv = None
356 sv = None
357 try:
358 sv = createcmdserver(repo, conn, fin, fout)
358 try:
359 try:
359 sv = createcmdserver(repo, conn, fin, fout)
360 sv.serve()
360 try:
361 # handle exceptions that may be raised by command server. most of
361 sv.serve()
362 # known exceptions are caught by dispatch.
362 # handle exceptions that may be raised by command server. most of
363 except error.Abort as inst:
363 # known exceptions are caught by dispatch.
364 ui.warn(_('abort: %s\n') % inst)
364 except error.Abort as inst:
365 except IOError as inst:
365 ui.warn(_('abort: %s\n') % inst)
366 if inst.errno != errno.EPIPE:
366 except IOError as inst:
367 raise
367 if inst.errno != errno.EPIPE:
368 except KeyboardInterrupt:
368 raise
369 pass
369 except KeyboardInterrupt:
370 pass
371 finally:
372 sv.cleanup()
373 except: # re-raises
374 # also write traceback to error channel. otherwise client cannot
375 # see it because it is written to server's stderr by default.
376 if sv:
377 cerr = sv.cerr
378 else:
379 cerr = channeledoutput(fout, 'e')
380 traceback.print_exc(file=cerr)
381 raise
382 finally:
370 finally:
383 fin.close()
371 sv.cleanup()
384 try:
372 except: # re-raises
385 fout.close() # implicit flush() may cause another EPIPE
373 # also write traceback to error channel. otherwise client cannot
386 except IOError as inst:
374 # see it because it is written to server's stderr by default.
387 if inst.errno != errno.EPIPE:
375 if sv:
388 raise
376 cerr = sv.cerr
389 # trigger __del__ since ForkingMixIn uses os._exit
377 else:
390 gc.collect()
378 cerr = channeledoutput(fout, 'e')
379 traceback.print_exc(file=cerr)
380 raise
381 finally:
382 fin.close()
383 try:
384 fout.close() # implicit flush() may cause another EPIPE
385 except IOError as inst:
386 if inst.errno != errno.EPIPE:
387 raise
388 # trigger __del__ since ForkingMixIn uses os._exit
389 gc.collect()
391
390
392 class unixservicehandler(object):
391 class unixservicehandler(object):
393 """Set of pluggable operations for unix-mode services
392 """Set of pluggable operations for unix-mode services
394
393
395 Almost all methods except for createcmdserver() are called in the main
394 Almost all methods except for createcmdserver() are called in the main
396 process. You can't pass mutable resource back from createcmdserver().
395 process. You can't pass mutable resource back from createcmdserver().
397 """
396 """
398
397
399 pollinterval = None
398 pollinterval = None
400
399
401 def __init__(self, ui):
400 def __init__(self, ui):
402 self.ui = ui
401 self.ui = ui
403
402
404 def bindsocket(self, sock, address):
403 def bindsocket(self, sock, address):
405 util.bindunixsocket(sock, address)
404 util.bindunixsocket(sock, address)
406
405
407 def unlinksocket(self, address):
406 def unlinksocket(self, address):
408 os.unlink(address)
407 os.unlink(address)
409
408
410 def printbanner(self, address):
409 def printbanner(self, address):
411 self.ui.status(_('listening at %s\n') % address)
410 self.ui.status(_('listening at %s\n') % address)
412 self.ui.flush() # avoid buffering of status message
411 self.ui.flush() # avoid buffering of status message
413
412
414 def shouldexit(self):
413 def shouldexit(self):
415 """True if server should shut down; checked per pollinterval"""
414 """True if server should shut down; checked per pollinterval"""
416 return False
415 return False
417
416
418 def newconnection(self):
417 def newconnection(self):
419 """Called when main process notices new connection"""
418 """Called when main process notices new connection"""
420 pass
419 pass
421
420
422 def createcmdserver(self, repo, conn, fin, fout):
421 def createcmdserver(self, repo, conn, fin, fout):
423 """Create new command server instance; called in the process that
422 """Create new command server instance; called in the process that
424 serves for the current connection"""
423 serves for the current connection"""
425 return server(self.ui, repo, fin, fout)
424 return server(self.ui, repo, fin, fout)
426
425
427 class unixforkingservice(object):
426 class unixforkingservice(object):
428 """
427 """
429 Listens on unix domain socket and forks server per connection
428 Listens on unix domain socket and forks server per connection
430 """
429 """
431
430
432 def __init__(self, ui, repo, opts, handler=None):
431 def __init__(self, ui, repo, opts, handler=None):
433 self.ui = ui
432 self.ui = ui
434 self.repo = repo
433 self.repo = repo
435 self.address = opts['address']
434 self.address = opts['address']
436 if not util.safehasattr(socket, 'AF_UNIX'):
435 if not util.safehasattr(socket, 'AF_UNIX'):
437 raise error.Abort(_('unsupported platform'))
436 raise error.Abort(_('unsupported platform'))
438 if not self.address:
437 if not self.address:
439 raise error.Abort(_('no socket path specified with --address'))
438 raise error.Abort(_('no socket path specified with --address'))
440 self._servicehandler = handler or unixservicehandler(ui)
439 self._servicehandler = handler or unixservicehandler(ui)
441 self._sock = None
440 self._sock = None
442 self._oldsigchldhandler = None
441 self._oldsigchldhandler = None
443 self._workerpids = set() # updated by signal handler; do not iterate
442 self._workerpids = set() # updated by signal handler; do not iterate
444
443
445 def init(self):
444 def init(self):
446 self._sock = socket.socket(socket.AF_UNIX)
445 self._sock = socket.socket(socket.AF_UNIX)
447 self._servicehandler.bindsocket(self._sock, self.address)
446 self._servicehandler.bindsocket(self._sock, self.address)
448 self._sock.listen(5)
447 self._sock.listen(5)
449 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
448 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
450 self._oldsigchldhandler = o
449 self._oldsigchldhandler = o
451 self._servicehandler.printbanner(self.address)
450 self._servicehandler.printbanner(self.address)
452
451
453 def _cleanup(self):
452 def _cleanup(self):
454 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
453 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
455 self._sock.close()
454 self._sock.close()
456 self._servicehandler.unlinksocket(self.address)
455 self._servicehandler.unlinksocket(self.address)
457 # don't kill child processes as they have active clients, just wait
456 # don't kill child processes as they have active clients, just wait
458 self._reapworkers(0)
457 self._reapworkers(0)
459
458
460 def run(self):
459 def run(self):
461 try:
460 try:
462 self._mainloop()
461 self._mainloop()
463 finally:
462 finally:
464 self._cleanup()
463 self._cleanup()
465
464
466 def _mainloop(self):
465 def _mainloop(self):
467 h = self._servicehandler
466 h = self._servicehandler
468 while not h.shouldexit():
467 while not h.shouldexit():
469 try:
468 try:
470 ready = select.select([self._sock], [], [], h.pollinterval)[0]
469 ready = select.select([self._sock], [], [], h.pollinterval)[0]
471 if not ready:
470 if not ready:
472 continue
471 continue
473 conn, _addr = self._sock.accept()
472 conn, _addr = self._sock.accept()
474 except (select.error, socket.error) as inst:
473 except (select.error, socket.error) as inst:
475 if inst.args[0] == errno.EINTR:
474 if inst.args[0] == errno.EINTR:
476 continue
475 continue
477 raise
476 raise
478
477
479 pid = os.fork()
478 pid = os.fork()
480 if pid:
479 if pid:
481 try:
480 try:
482 self.ui.debug('forked worker process (pid=%d)\n' % pid)
481 self.ui.debug('forked worker process (pid=%d)\n' % pid)
483 self._workerpids.add(pid)
482 self._workerpids.add(pid)
484 h.newconnection()
483 h.newconnection()
485 finally:
484 finally:
486 conn.close() # release handle in parent process
485 conn.close() # release handle in parent process
487 else:
486 else:
488 try:
487 try:
489 self._serveworker(conn)
488 self._serveworker(conn)
490 conn.close()
489 conn.close()
491 os._exit(0)
490 os._exit(0)
492 except: # never return, hence no re-raises
491 except: # never return, hence no re-raises
493 try:
492 try:
494 self.ui.traceback(force=True)
493 self.ui.traceback(force=True)
495 finally:
494 finally:
496 os._exit(255)
495 os._exit(255)
497
496
498 def _sigchldhandler(self, signal, frame):
497 def _sigchldhandler(self, signal, frame):
499 self._reapworkers(os.WNOHANG)
498 self._reapworkers(os.WNOHANG)
500
499
501 def _reapworkers(self, options):
500 def _reapworkers(self, options):
502 while self._workerpids:
501 while self._workerpids:
503 try:
502 try:
504 pid, _status = os.waitpid(-1, options)
503 pid, _status = os.waitpid(-1, options)
505 except OSError as inst:
504 except OSError as inst:
506 if inst.errno == errno.EINTR:
505 if inst.errno == errno.EINTR:
507 continue
506 continue
508 if inst.errno != errno.ECHILD:
507 if inst.errno != errno.ECHILD:
509 raise
508 raise
510 # no child processes at all (reaped by other waitpid()?)
509 # no child processes at all (reaped by other waitpid()?)
511 self._workerpids.clear()
510 self._workerpids.clear()
512 return
511 return
513 if pid == 0:
512 if pid == 0:
514 # no waitable child processes
513 # no waitable child processes
515 return
514 return
516 self.ui.debug('worker process exited (pid=%d)\n' % pid)
515 self.ui.debug('worker process exited (pid=%d)\n' % pid)
517 self._workerpids.discard(pid)
516 self._workerpids.discard(pid)
518
517
519 def _serveworker(self, conn):
518 def _serveworker(self, conn):
520 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
519 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
521 h = self._servicehandler
520 h = self._servicehandler
522 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
521 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
523
522
524 _servicemap = {
523 _servicemap = {
525 'pipe': pipeservice,
524 'pipe': pipeservice,
526 'unix': unixforkingservice,
525 'unix': unixforkingservice,
527 }
526 }
528
527
529 def createservice(ui, repo, opts):
528 def createservice(ui, repo, opts):
530 mode = opts['cmdserver']
529 mode = opts['cmdserver']
531 try:
530 try:
532 return _servicemap[mode](ui, repo, opts)
531 return _servicemap[mode](ui, repo, opts)
533 except KeyError:
532 except KeyError:
534 raise error.Abort(_('unknown mode %s') % mode)
533 raise error.Abort(_('unknown mode %s') % mode)
General Comments 0
You need to be logged in to leave comments. Login now