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