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