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