##// END OF EJS Templates
chgserver: resolve relative path before sending via system channel...
Jun Wu -
r28514:0747ef2c default
parent child Browse files
Show More
@@ -1,663 +1,663 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 inspect
45 import inspect
46 import os
46 import os
47 import re
47 import re
48 import struct
48 import struct
49 import sys
49 import sys
50 import threading
50 import threading
51 import time
51 import time
52 import traceback
52 import traceback
53
53
54 from mercurial.i18n import _
54 from mercurial.i18n import _
55
55
56 from mercurial import (
56 from mercurial import (
57 cmdutil,
57 cmdutil,
58 commands,
58 commands,
59 commandserver,
59 commandserver,
60 dispatch,
60 dispatch,
61 error,
61 error,
62 extensions,
62 extensions,
63 osutil,
63 osutil,
64 util,
64 util,
65 )
65 )
66
66
67 # Note for extension authors: ONLY specify testedwith = 'internal' for
67 # Note for extension authors: ONLY specify testedwith = 'internal' for
68 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
68 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
69 # be specifying the version(s) of Mercurial they are tested with, or
69 # be specifying the version(s) of Mercurial they are tested with, or
70 # leave the attribute unspecified.
70 # leave the attribute unspecified.
71 testedwith = 'internal'
71 testedwith = 'internal'
72
72
73 _log = commandserver.log
73 _log = commandserver.log
74
74
75 def _hashlist(items):
75 def _hashlist(items):
76 """return sha1 hexdigest for a list"""
76 """return sha1 hexdigest for a list"""
77 return util.sha1(str(items)).hexdigest()
77 return util.sha1(str(items)).hexdigest()
78
78
79 # sensitive config sections affecting confighash
79 # sensitive config sections affecting confighash
80 _configsections = [
80 _configsections = [
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 def trystat(path):
152 def trystat(path):
153 try:
153 try:
154 st = os.stat(path)
154 st = os.stat(path)
155 return (st.st_mtime, st.st_size)
155 return (st.st_mtime, st.st_size)
156 except OSError:
156 except OSError:
157 # could be ENOENT, EPERM etc. not fatal in any case
157 # could be ENOENT, EPERM etc. not fatal in any case
158 pass
158 pass
159 return _hashlist(map(trystat, paths))[:12]
159 return _hashlist(map(trystat, paths))[:12]
160
160
161 class hashstate(object):
161 class hashstate(object):
162 """a structure storing confighash, mtimehash, paths used for mtimehash"""
162 """a structure storing confighash, mtimehash, paths used for mtimehash"""
163 def __init__(self, confighash, mtimehash, mtimepaths):
163 def __init__(self, confighash, mtimehash, mtimepaths):
164 self.confighash = confighash
164 self.confighash = confighash
165 self.mtimehash = mtimehash
165 self.mtimehash = mtimehash
166 self.mtimepaths = mtimepaths
166 self.mtimepaths = mtimepaths
167
167
168 @staticmethod
168 @staticmethod
169 def fromui(ui, mtimepaths=None):
169 def fromui(ui, mtimepaths=None):
170 if mtimepaths is None:
170 if mtimepaths is None:
171 mtimepaths = _getmtimepaths(ui)
171 mtimepaths = _getmtimepaths(ui)
172 confighash = _confighash(ui)
172 confighash = _confighash(ui)
173 mtimehash = _mtimehash(mtimepaths)
173 mtimehash = _mtimehash(mtimepaths)
174 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
174 _log('confighash = %s mtimehash = %s\n' % (confighash, mtimehash))
175 return hashstate(confighash, mtimehash, mtimepaths)
175 return hashstate(confighash, mtimehash, mtimepaths)
176
176
177 # copied from hgext/pager.py:uisetup()
177 # copied from hgext/pager.py:uisetup()
178 def _setuppagercmd(ui, options, cmd):
178 def _setuppagercmd(ui, options, cmd):
179 if not ui.formatted():
179 if not ui.formatted():
180 return
180 return
181
181
182 p = ui.config("pager", "pager", os.environ.get("PAGER"))
182 p = ui.config("pager", "pager", os.environ.get("PAGER"))
183 usepager = False
183 usepager = False
184 always = util.parsebool(options['pager'])
184 always = util.parsebool(options['pager'])
185 auto = options['pager'] == 'auto'
185 auto = options['pager'] == 'auto'
186
186
187 if not p:
187 if not p:
188 pass
188 pass
189 elif always:
189 elif always:
190 usepager = True
190 usepager = True
191 elif not auto:
191 elif not auto:
192 usepager = False
192 usepager = False
193 else:
193 else:
194 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
194 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
195 attend = ui.configlist('pager', 'attend', attended)
195 attend = ui.configlist('pager', 'attend', attended)
196 ignore = ui.configlist('pager', 'ignore')
196 ignore = ui.configlist('pager', 'ignore')
197 cmds, _ = cmdutil.findcmd(cmd, commands.table)
197 cmds, _ = cmdutil.findcmd(cmd, commands.table)
198
198
199 for cmd in cmds:
199 for cmd in cmds:
200 var = 'attend-%s' % cmd
200 var = 'attend-%s' % cmd
201 if ui.config('pager', var):
201 if ui.config('pager', var):
202 usepager = ui.configbool('pager', var)
202 usepager = ui.configbool('pager', var)
203 break
203 break
204 if (cmd in attend or
204 if (cmd in attend or
205 (cmd not in ignore and not attend)):
205 (cmd not in ignore and not attend)):
206 usepager = True
206 usepager = True
207 break
207 break
208
208
209 if usepager:
209 if usepager:
210 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
210 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
211 ui.setconfig('ui', 'interactive', False, 'pager')
211 ui.setconfig('ui', 'interactive', False, 'pager')
212 return p
212 return p
213
213
214 _envvarre = re.compile(r'\$[a-zA-Z_]+')
214 _envvarre = re.compile(r'\$[a-zA-Z_]+')
215
215
216 def _clearenvaliases(cmdtable):
216 def _clearenvaliases(cmdtable):
217 """Remove stale command aliases referencing env vars; variable expansion
217 """Remove stale command aliases referencing env vars; variable expansion
218 is done at dispatch.addaliases()"""
218 is done at dispatch.addaliases()"""
219 for name, tab in cmdtable.items():
219 for name, tab in cmdtable.items():
220 cmddef = tab[0]
220 cmddef = tab[0]
221 if (isinstance(cmddef, dispatch.cmdalias) and
221 if (isinstance(cmddef, dispatch.cmdalias) and
222 not cmddef.definition.startswith('!') and # shell alias
222 not cmddef.definition.startswith('!') and # shell alias
223 _envvarre.search(cmddef.definition)):
223 _envvarre.search(cmddef.definition)):
224 del cmdtable[name]
224 del cmdtable[name]
225
225
226 def _newchgui(srcui, csystem):
226 def _newchgui(srcui, csystem):
227 class chgui(srcui.__class__):
227 class chgui(srcui.__class__):
228 def __init__(self, src=None):
228 def __init__(self, src=None):
229 super(chgui, self).__init__(src)
229 super(chgui, self).__init__(src)
230 if src:
230 if src:
231 self._csystem = getattr(src, '_csystem', csystem)
231 self._csystem = getattr(src, '_csystem', csystem)
232 else:
232 else:
233 self._csystem = csystem
233 self._csystem = csystem
234
234
235 def system(self, cmd, environ=None, cwd=None, onerr=None,
235 def system(self, cmd, environ=None, cwd=None, onerr=None,
236 errprefix=None):
236 errprefix=None):
237 # copied from mercurial/util.py:system()
237 # copied from mercurial/util.py:system()
238 self.flush()
238 self.flush()
239 def py2shell(val):
239 def py2shell(val):
240 if val is None or val is False:
240 if val is None or val is False:
241 return '0'
241 return '0'
242 if val is True:
242 if val is True:
243 return '1'
243 return '1'
244 return str(val)
244 return str(val)
245 env = os.environ.copy()
245 env = os.environ.copy()
246 if environ:
246 if environ:
247 env.update((k, py2shell(v)) for k, v in environ.iteritems())
247 env.update((k, py2shell(v)) for k, v in environ.iteritems())
248 env['HG'] = util.hgexecutable()
248 env['HG'] = util.hgexecutable()
249 rc = self._csystem(cmd, env, cwd)
249 rc = self._csystem(cmd, env, cwd)
250 if rc and onerr:
250 if rc and onerr:
251 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
251 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
252 util.explainexit(rc)[0])
252 util.explainexit(rc)[0])
253 if errprefix:
253 if errprefix:
254 errmsg = '%s: %s' % (errprefix, errmsg)
254 errmsg = '%s: %s' % (errprefix, errmsg)
255 raise onerr(errmsg)
255 raise onerr(errmsg)
256 return rc
256 return rc
257
257
258 return chgui(srcui)
258 return chgui(srcui)
259
259
260 def _renewui(srcui, args=None):
260 def _renewui(srcui, args=None):
261 if not args:
261 if not args:
262 args = []
262 args = []
263
263
264 newui = srcui.__class__()
264 newui = srcui.__class__()
265 for a in ['fin', 'fout', 'ferr', 'environ']:
265 for a in ['fin', 'fout', 'ferr', 'environ']:
266 setattr(newui, a, getattr(srcui, a))
266 setattr(newui, a, getattr(srcui, a))
267 if util.safehasattr(srcui, '_csystem'):
267 if util.safehasattr(srcui, '_csystem'):
268 newui._csystem = srcui._csystem
268 newui._csystem = srcui._csystem
269
269
270 # load wd and repo config, copied from dispatch.py
270 # load wd and repo config, copied from dispatch.py
271 cwds = dispatch._earlygetopt(['--cwd'], args)
271 cwds = dispatch._earlygetopt(['--cwd'], args)
272 cwd = cwds and os.path.realpath(cwds[-1]) or None
272 cwd = cwds and os.path.realpath(cwds[-1]) or None
273 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
273 rpath = dispatch._earlygetopt(["-R", "--repository", "--repo"], args)
274 path, newui = dispatch._getlocal(newui, rpath, wd=cwd)
274 path, newui = dispatch._getlocal(newui, rpath, wd=cwd)
275
275
276 # internal config: extensions.chgserver
276 # internal config: extensions.chgserver
277 # copy it. it can only be overrided from command line.
277 # copy it. it can only be overrided from command line.
278 newui.setconfig('extensions', 'chgserver',
278 newui.setconfig('extensions', 'chgserver',
279 srcui.config('extensions', 'chgserver'), '--config')
279 srcui.config('extensions', 'chgserver'), '--config')
280
280
281 # command line args
281 # command line args
282 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
282 dispatch._parseconfig(newui, dispatch._earlygetopt(['--config'], args))
283
283
284 # stolen from tortoisehg.util.copydynamicconfig()
284 # stolen from tortoisehg.util.copydynamicconfig()
285 for section, name, value in srcui.walkconfig():
285 for section, name, value in srcui.walkconfig():
286 source = srcui.configsource(section, name)
286 source = srcui.configsource(section, name)
287 if ':' in source or source == '--config':
287 if ':' in source or source == '--config':
288 # path:line or command line
288 # path:line or command line
289 continue
289 continue
290 if source == 'none':
290 if source == 'none':
291 # ui.configsource returns 'none' by default
291 # ui.configsource returns 'none' by default
292 source = ''
292 source = ''
293 newui.setconfig(section, name, value, source)
293 newui.setconfig(section, name, value, source)
294 return newui
294 return newui
295
295
296 class channeledsystem(object):
296 class channeledsystem(object):
297 """Propagate ui.system() request in the following format:
297 """Propagate ui.system() request in the following format:
298
298
299 payload length (unsigned int),
299 payload length (unsigned int),
300 cmd, '\0',
300 cmd, '\0',
301 cwd, '\0',
301 cwd, '\0',
302 envkey, '=', val, '\0',
302 envkey, '=', val, '\0',
303 ...
303 ...
304 envkey, '=', val
304 envkey, '=', val
305
305
306 and waits:
306 and waits:
307
307
308 exitcode length (unsigned int),
308 exitcode length (unsigned int),
309 exitcode (int)
309 exitcode (int)
310 """
310 """
311 def __init__(self, in_, out, channel):
311 def __init__(self, in_, out, channel):
312 self.in_ = in_
312 self.in_ = in_
313 self.out = out
313 self.out = out
314 self.channel = channel
314 self.channel = channel
315
315
316 def __call__(self, cmd, environ, cwd):
316 def __call__(self, cmd, environ, cwd):
317 args = [util.quotecommand(cmd), cwd or '.']
317 args = [util.quotecommand(cmd), os.path.abspath(cwd or '.')]
318 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
318 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
319 data = '\0'.join(args)
319 data = '\0'.join(args)
320 self.out.write(struct.pack('>cI', self.channel, len(data)))
320 self.out.write(struct.pack('>cI', self.channel, len(data)))
321 self.out.write(data)
321 self.out.write(data)
322 self.out.flush()
322 self.out.flush()
323
323
324 length = self.in_.read(4)
324 length = self.in_.read(4)
325 length, = struct.unpack('>I', length)
325 length, = struct.unpack('>I', length)
326 if length != 4:
326 if length != 4:
327 raise error.Abort(_('invalid response'))
327 raise error.Abort(_('invalid response'))
328 rc, = struct.unpack('>i', self.in_.read(4))
328 rc, = struct.unpack('>i', self.in_.read(4))
329 return rc
329 return rc
330
330
331 _iochannels = [
331 _iochannels = [
332 # server.ch, ui.fp, mode
332 # server.ch, ui.fp, mode
333 ('cin', 'fin', 'rb'),
333 ('cin', 'fin', 'rb'),
334 ('cout', 'fout', 'wb'),
334 ('cout', 'fout', 'wb'),
335 ('cerr', 'ferr', 'wb'),
335 ('cerr', 'ferr', 'wb'),
336 ]
336 ]
337
337
338 class chgcmdserver(commandserver.server):
338 class chgcmdserver(commandserver.server):
339 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
339 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
340 super(chgcmdserver, self).__init__(
340 super(chgcmdserver, self).__init__(
341 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
341 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
342 self.clientsock = sock
342 self.clientsock = sock
343 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
343 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
344 self.hashstate = hashstate
344 self.hashstate = hashstate
345 self.baseaddress = baseaddress
345 self.baseaddress = baseaddress
346 if hashstate is not None:
346 if hashstate is not None:
347 self.capabilities = self.capabilities.copy()
347 self.capabilities = self.capabilities.copy()
348 self.capabilities['validate'] = chgcmdserver.validate
348 self.capabilities['validate'] = chgcmdserver.validate
349
349
350 def cleanup(self):
350 def cleanup(self):
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 try to connect to another
430 - "redirect $path", the client should try to connect to another
431 server instead.
431 server instead.
432 """
432 """
433 args = self._readlist()
433 args = self._readlist()
434 self.ui = _renewui(self.ui, args)
434 self.ui = _renewui(self.ui, args)
435 newhash = hashstate.fromui(self.ui, self.hashstate.mtimepaths)
435 newhash = hashstate.fromui(self.ui, self.hashstate.mtimepaths)
436 insts = []
436 insts = []
437 if newhash.mtimehash != self.hashstate.mtimehash:
437 if newhash.mtimehash != self.hashstate.mtimehash:
438 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
438 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
439 insts.append('unlink %s' % addr)
439 insts.append('unlink %s' % addr)
440 if newhash.confighash != self.hashstate.confighash:
440 if newhash.confighash != self.hashstate.confighash:
441 addr = _hashaddress(self.baseaddress, newhash.confighash)
441 addr = _hashaddress(self.baseaddress, newhash.confighash)
442 insts.append('redirect %s' % addr)
442 insts.append('redirect %s' % addr)
443 _log('validate: %s\n' % insts)
443 _log('validate: %s\n' % insts)
444 self.cresult.write('\0'.join(insts) or '\0')
444 self.cresult.write('\0'.join(insts) or '\0')
445
445
446 def chdir(self):
446 def chdir(self):
447 """Change current directory
447 """Change current directory
448
448
449 Note that the behavior of --cwd option is bit different from this.
449 Note that the behavior of --cwd option is bit different from this.
450 It does not affect --config parameter.
450 It does not affect --config parameter.
451 """
451 """
452 path = self._readstr()
452 path = self._readstr()
453 if not path:
453 if not path:
454 return
454 return
455 _log('chdir to %r\n' % path)
455 _log('chdir to %r\n' % path)
456 os.chdir(path)
456 os.chdir(path)
457
457
458 def setumask(self):
458 def setumask(self):
459 """Change umask"""
459 """Change umask"""
460 mask = struct.unpack('>I', self._read(4))[0]
460 mask = struct.unpack('>I', self._read(4))[0]
461 _log('setumask %r\n' % mask)
461 _log('setumask %r\n' % mask)
462 os.umask(mask)
462 os.umask(mask)
463
463
464 def getpager(self):
464 def getpager(self):
465 """Read cmdargs and write pager command to r-channel if enabled
465 """Read cmdargs and write pager command to r-channel if enabled
466
466
467 If pager isn't enabled, this writes '\0' because channeledoutput
467 If pager isn't enabled, this writes '\0' because channeledoutput
468 does not allow to write empty data.
468 does not allow to write empty data.
469 """
469 """
470 args = self._readlist()
470 args = self._readlist()
471 try:
471 try:
472 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
472 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
473 args)
473 args)
474 except (error.Abort, error.AmbiguousCommand, error.CommandError,
474 except (error.Abort, error.AmbiguousCommand, error.CommandError,
475 error.UnknownCommand):
475 error.UnknownCommand):
476 cmd = None
476 cmd = None
477 options = {}
477 options = {}
478 if not cmd or 'pager' not in options:
478 if not cmd or 'pager' not in options:
479 self.cresult.write('\0')
479 self.cresult.write('\0')
480 return
480 return
481
481
482 pagercmd = _setuppagercmd(self.ui, options, cmd)
482 pagercmd = _setuppagercmd(self.ui, options, cmd)
483 if pagercmd:
483 if pagercmd:
484 self.cresult.write(pagercmd)
484 self.cresult.write(pagercmd)
485 else:
485 else:
486 self.cresult.write('\0')
486 self.cresult.write('\0')
487
487
488 def setenv(self):
488 def setenv(self):
489 """Clear and update os.environ
489 """Clear and update os.environ
490
490
491 Note that not all variables can make an effect on the running process.
491 Note that not all variables can make an effect on the running process.
492 """
492 """
493 l = self._readlist()
493 l = self._readlist()
494 try:
494 try:
495 newenv = dict(s.split('=', 1) for s in l)
495 newenv = dict(s.split('=', 1) for s in l)
496 except ValueError:
496 except ValueError:
497 raise ValueError('unexpected value in setenv request')
497 raise ValueError('unexpected value in setenv request')
498
498
499 diffkeys = set(k for k in set(os.environ.keys() + newenv.keys())
499 diffkeys = set(k for k in set(os.environ.keys() + newenv.keys())
500 if os.environ.get(k) != newenv.get(k))
500 if os.environ.get(k) != newenv.get(k))
501 _log('change env: %r\n' % sorted(diffkeys))
501 _log('change env: %r\n' % sorted(diffkeys))
502
502
503 os.environ.clear()
503 os.environ.clear()
504 os.environ.update(newenv)
504 os.environ.update(newenv)
505
505
506 if set(['HGPLAIN', 'HGPLAINEXCEPT']) & diffkeys:
506 if set(['HGPLAIN', 'HGPLAINEXCEPT']) & diffkeys:
507 # reload config so that ui.plain() takes effect
507 # reload config so that ui.plain() takes effect
508 self.ui = _renewui(self.ui)
508 self.ui = _renewui(self.ui)
509
509
510 _clearenvaliases(commands.table)
510 _clearenvaliases(commands.table)
511
511
512 capabilities = commandserver.server.capabilities.copy()
512 capabilities = commandserver.server.capabilities.copy()
513 capabilities.update({'attachio': attachio,
513 capabilities.update({'attachio': attachio,
514 'chdir': chdir,
514 'chdir': chdir,
515 'getpager': getpager,
515 'getpager': getpager,
516 'setenv': setenv,
516 'setenv': setenv,
517 'setumask': setumask})
517 'setumask': setumask})
518
518
519 # copied from mercurial/commandserver.py
519 # copied from mercurial/commandserver.py
520 class _requesthandler(SocketServer.StreamRequestHandler):
520 class _requesthandler(SocketServer.StreamRequestHandler):
521 def handle(self):
521 def handle(self):
522 # use a different process group from the master process, making this
522 # use a different process group from the master process, making this
523 # process pass kernel "is_current_pgrp_orphaned" check so signals like
523 # process pass kernel "is_current_pgrp_orphaned" check so signals like
524 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
524 # SIGTSTP, SIGTTIN, SIGTTOU are not ignored.
525 os.setpgid(0, 0)
525 os.setpgid(0, 0)
526 ui = self.server.ui
526 ui = self.server.ui
527 repo = self.server.repo
527 repo = self.server.repo
528 sv = None
528 sv = None
529 try:
529 try:
530 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
530 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection,
531 self.server.hashstate, self.server.baseaddress)
531 self.server.hashstate, self.server.baseaddress)
532 try:
532 try:
533 sv.serve()
533 sv.serve()
534 # handle exceptions that may be raised by command server. most of
534 # handle exceptions that may be raised by command server. most of
535 # known exceptions are caught by dispatch.
535 # known exceptions are caught by dispatch.
536 except error.Abort as inst:
536 except error.Abort as inst:
537 ui.warn(_('abort: %s\n') % inst)
537 ui.warn(_('abort: %s\n') % inst)
538 except IOError as inst:
538 except IOError as inst:
539 if inst.errno != errno.EPIPE:
539 if inst.errno != errno.EPIPE:
540 raise
540 raise
541 except KeyboardInterrupt:
541 except KeyboardInterrupt:
542 pass
542 pass
543 finally:
543 finally:
544 sv.cleanup()
544 sv.cleanup()
545 except: # re-raises
545 except: # re-raises
546 # also write traceback to error channel. otherwise client cannot
546 # also write traceback to error channel. otherwise client cannot
547 # see it because it is written to server's stderr by default.
547 # see it because it is written to server's stderr by default.
548 if sv:
548 if sv:
549 cerr = sv.cerr
549 cerr = sv.cerr
550 else:
550 else:
551 cerr = commandserver.channeledoutput(self.wfile, 'e')
551 cerr = commandserver.channeledoutput(self.wfile, 'e')
552 traceback.print_exc(file=cerr)
552 traceback.print_exc(file=cerr)
553 raise
553 raise
554
554
555 def _tempaddress(address):
555 def _tempaddress(address):
556 return '%s.%d.tmp' % (address, os.getpid())
556 return '%s.%d.tmp' % (address, os.getpid())
557
557
558 def _hashaddress(address, hashstr):
558 def _hashaddress(address, hashstr):
559 return '%s-%s' % (address, hashstr)
559 return '%s-%s' % (address, hashstr)
560
560
561 class AutoExitMixIn: # use old-style to comply with SocketServer design
561 class AutoExitMixIn: # use old-style to comply with SocketServer design
562 lastactive = time.time()
562 lastactive = time.time()
563 idletimeout = 3600 # default 1 hour
563 idletimeout = 3600 # default 1 hour
564
564
565 def startautoexitthread(self):
565 def startautoexitthread(self):
566 # note: the auto-exit check here is cheap enough to not use a thread,
566 # note: the auto-exit check here is cheap enough to not use a thread,
567 # be done in serve_forever. however SocketServer is hook-unfriendly,
567 # be done in serve_forever. however SocketServer is hook-unfriendly,
568 # you simply cannot hook serve_forever without copying a lot of code.
568 # you simply cannot hook serve_forever without copying a lot of code.
569 # besides, serve_forever's docstring suggests using thread.
569 # besides, serve_forever's docstring suggests using thread.
570 thread = threading.Thread(target=self._autoexitloop)
570 thread = threading.Thread(target=self._autoexitloop)
571 thread.daemon = True
571 thread.daemon = True
572 thread.start()
572 thread.start()
573
573
574 def _autoexitloop(self, interval=1):
574 def _autoexitloop(self, interval=1):
575 while True:
575 while True:
576 time.sleep(interval)
576 time.sleep(interval)
577 if not self.issocketowner():
577 if not self.issocketowner():
578 _log('%s is not owned, exiting.\n' % self.server_address)
578 _log('%s is not owned, exiting.\n' % self.server_address)
579 break
579 break
580 if time.time() - self.lastactive > self.idletimeout:
580 if time.time() - self.lastactive > self.idletimeout:
581 _log('being idle too long. exiting.\n')
581 _log('being idle too long. exiting.\n')
582 break
582 break
583 self.shutdown()
583 self.shutdown()
584
584
585 def process_request(self, request, address):
585 def process_request(self, request, address):
586 self.lastactive = time.time()
586 self.lastactive = time.time()
587 return SocketServer.ForkingMixIn.process_request(
587 return SocketServer.ForkingMixIn.process_request(
588 self, request, address)
588 self, request, address)
589
589
590 def server_bind(self):
590 def server_bind(self):
591 # use a unique temp address so we can stat the file and do ownership
591 # use a unique temp address so we can stat the file and do ownership
592 # check later
592 # check later
593 tempaddress = _tempaddress(self.server_address)
593 tempaddress = _tempaddress(self.server_address)
594 self.socket.bind(tempaddress)
594 self.socket.bind(tempaddress)
595 self._socketstat = os.stat(tempaddress)
595 self._socketstat = os.stat(tempaddress)
596 # rename will replace the old socket file if exists atomically. the
596 # rename will replace the old socket file if exists atomically. the
597 # old server will detect ownership change and exit.
597 # old server will detect ownership change and exit.
598 util.rename(tempaddress, self.server_address)
598 util.rename(tempaddress, self.server_address)
599
599
600 def issocketowner(self):
600 def issocketowner(self):
601 try:
601 try:
602 stat = os.stat(self.server_address)
602 stat = os.stat(self.server_address)
603 return (stat.st_ino == self._socketstat.st_ino and
603 return (stat.st_ino == self._socketstat.st_ino and
604 stat.st_mtime == self._socketstat.st_mtime)
604 stat.st_mtime == self._socketstat.st_mtime)
605 except OSError:
605 except OSError:
606 return False
606 return False
607
607
608 def unlinksocketfile(self):
608 def unlinksocketfile(self):
609 if not self.issocketowner():
609 if not self.issocketowner():
610 return
610 return
611 # it is possible to have a race condition here that we may
611 # it is possible to have a race condition here that we may
612 # remove another server's socket file. but that's okay
612 # remove another server's socket file. but that's okay
613 # since that server will detect and exit automatically and
613 # since that server will detect and exit automatically and
614 # the client will start a new server on demand.
614 # the client will start a new server on demand.
615 try:
615 try:
616 os.unlink(self.server_address)
616 os.unlink(self.server_address)
617 except OSError as exc:
617 except OSError as exc:
618 if exc.errno != errno.ENOENT:
618 if exc.errno != errno.ENOENT:
619 raise
619 raise
620
620
621 class chgunixservice(commandserver.unixservice):
621 class chgunixservice(commandserver.unixservice):
622 def init(self):
622 def init(self):
623 self._inithashstate()
623 self._inithashstate()
624 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
624 class cls(AutoExitMixIn, SocketServer.ForkingMixIn,
625 SocketServer.UnixStreamServer):
625 SocketServer.UnixStreamServer):
626 ui = self.ui
626 ui = self.ui
627 repo = self.repo
627 repo = self.repo
628 hashstate = self.hashstate
628 hashstate = self.hashstate
629 baseaddress = self.baseaddress
629 baseaddress = self.baseaddress
630 self.server = cls(self.address, _requesthandler)
630 self.server = cls(self.address, _requesthandler)
631 self.server.idletimeout = self.ui.configint(
631 self.server.idletimeout = self.ui.configint(
632 'chgserver', 'idletimeout', self.server.idletimeout)
632 'chgserver', 'idletimeout', self.server.idletimeout)
633 self.server.startautoexitthread()
633 self.server.startautoexitthread()
634 self._createsymlink()
634 self._createsymlink()
635
635
636 def _inithashstate(self):
636 def _inithashstate(self):
637 self.baseaddress = self.address
637 self.baseaddress = self.address
638 if self.ui.configbool('chgserver', 'skiphash', False):
638 if self.ui.configbool('chgserver', 'skiphash', False):
639 self.hashstate = None
639 self.hashstate = None
640 return
640 return
641 self.hashstate = hashstate.fromui(self.ui)
641 self.hashstate = hashstate.fromui(self.ui)
642 self.address = _hashaddress(self.address, self.hashstate.confighash)
642 self.address = _hashaddress(self.address, self.hashstate.confighash)
643
643
644 def _createsymlink(self):
644 def _createsymlink(self):
645 if self.baseaddress == self.address:
645 if self.baseaddress == self.address:
646 return
646 return
647 tempaddress = _tempaddress(self.baseaddress)
647 tempaddress = _tempaddress(self.baseaddress)
648 os.symlink(os.path.basename(self.address), tempaddress)
648 os.symlink(os.path.basename(self.address), tempaddress)
649 util.rename(tempaddress, self.baseaddress)
649 util.rename(tempaddress, self.baseaddress)
650
650
651 def run(self):
651 def run(self):
652 try:
652 try:
653 self.server.serve_forever()
653 self.server.serve_forever()
654 finally:
654 finally:
655 self.server.unlinksocketfile()
655 self.server.unlinksocketfile()
656
656
657 def uisetup(ui):
657 def uisetup(ui):
658 commandserver._servicemap['chgunix'] = chgunixservice
658 commandserver._servicemap['chgunix'] = chgunixservice
659
659
660 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
660 # CHGINTERNALMARK is temporarily set by chg client to detect if chg will
661 # start another chg. drop it to avoid possible side effects.
661 # start another chg. drop it to avoid possible side effects.
662 if 'CHGINTERNALMARK' in os.environ:
662 if 'CHGINTERNALMARK' in os.environ:
663 del os.environ['CHGINTERNALMARK']
663 del os.environ['CHGINTERNALMARK']
General Comments 0
You need to be logged in to leave comments. Login now