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