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