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