##// END OF EJS Templates
chgserver: import background server extension from cHg...
Yuya Nishihara -
r27792:98095733 default
parent child Browse files
Show More
@@ -0,0 +1,390 b''
1 # chgserver.py - command server extension for cHg
2 #
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 #
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.
7
8 """command server extension for cHg (EXPERIMENTAL)
9
10 'S' channel (read/write)
11 propagate ui.system() request to client
12
13 'attachio' command
14 attach client's stdio passed by sendmsg()
15
16 'chdir' command
17 change current directory
18
19 'getpager' command
20 checks if pager is enabled and which pager should be executed
21
22 'setenv' command
23 replace os.environ completely
24
25 'SIGHUP' signal
26 reload configuration files
27 """
28
29 from __future__ import absolute_import
30
31 import SocketServer
32 import errno
33 import os
34 import re
35 import signal
36 import struct
37 import traceback
38
39 from mercurial.i18n import _
40
41 from mercurial import (
42 cmdutil,
43 commands,
44 commandserver,
45 dispatch,
46 error,
47 osutil,
48 util,
49 )
50
51 _log = commandserver.log
52
53 # copied from hgext/pager.py:uisetup()
54 def _setuppagercmd(ui, options, cmd):
55 if not ui.formatted():
56 return
57
58 p = ui.config("pager", "pager", os.environ.get("PAGER"))
59 usepager = False
60 always = util.parsebool(options['pager'])
61 auto = options['pager'] == 'auto'
62
63 if not p:
64 pass
65 elif always:
66 usepager = True
67 elif not auto:
68 usepager = False
69 else:
70 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
71 attend = ui.configlist('pager', 'attend', attended)
72 ignore = ui.configlist('pager', 'ignore')
73 cmds, _ = cmdutil.findcmd(cmd, commands.table)
74
75 for cmd in cmds:
76 var = 'attend-%s' % cmd
77 if ui.config('pager', var):
78 usepager = ui.configbool('pager', var)
79 break
80 if (cmd in attend or
81 (cmd not in ignore and not attend)):
82 usepager = True
83 break
84
85 if usepager:
86 ui.setconfig('ui', 'formatted', ui.formatted(), 'pager')
87 ui.setconfig('ui', 'interactive', False, 'pager')
88 return p
89
90 _envvarre = re.compile(r'\$[a-zA-Z_]+')
91
92 def _clearenvaliases(cmdtable):
93 """Remove stale command aliases referencing env vars; variable expansion
94 is done at dispatch.addaliases()"""
95 for name, tab in cmdtable.items():
96 cmddef = tab[0]
97 if (isinstance(cmddef, dispatch.cmdalias) and
98 not cmddef.definition.startswith('!') and # shell alias
99 _envvarre.search(cmddef.definition)):
100 del cmdtable[name]
101
102 def _newchgui(srcui, csystem):
103 class chgui(srcui.__class__):
104 def __init__(self, src=None):
105 super(chgui, self).__init__(src)
106 if src:
107 self._csystem = getattr(src, '_csystem', csystem)
108 else:
109 self._csystem = csystem
110
111 def system(self, cmd, environ=None, cwd=None, onerr=None,
112 errprefix=None):
113 # copied from mercurial/util.py:system()
114 self.flush()
115 def py2shell(val):
116 if val is None or val is False:
117 return '0'
118 if val is True:
119 return '1'
120 return str(val)
121 env = os.environ.copy()
122 if environ:
123 env.update((k, py2shell(v)) for k, v in environ.iteritems())
124 env['HG'] = util.hgexecutable()
125 rc = self._csystem(cmd, env, cwd)
126 if rc and onerr:
127 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
128 util.explainexit(rc)[0])
129 if errprefix:
130 errmsg = '%s: %s' % (errprefix, errmsg)
131 raise onerr(errmsg)
132 return rc
133
134 return chgui(srcui)
135
136 def _renewui(srcui):
137 newui = srcui.__class__()
138 for a in ['fin', 'fout', 'ferr', 'environ']:
139 setattr(newui, a, getattr(srcui, a))
140 if util.safehasattr(srcui, '_csystem'):
141 newui._csystem = srcui._csystem
142 # stolen from tortoisehg.util.copydynamicconfig()
143 for section, name, value in srcui.walkconfig():
144 source = srcui.configsource(section, name)
145 if ':' in source:
146 # path:line
147 continue
148 if source == 'none':
149 # ui.configsource returns 'none' by default
150 source = ''
151 newui.setconfig(section, name, value, source)
152 return newui
153
154 class channeledsystem(object):
155 """Propagate ui.system() request in the following format:
156
157 payload length (unsigned int),
158 cmd, '\0',
159 cwd, '\0',
160 envkey, '=', val, '\0',
161 ...
162 envkey, '=', val
163
164 and waits:
165
166 exitcode length (unsigned int),
167 exitcode (int)
168 """
169 def __init__(self, in_, out, channel):
170 self.in_ = in_
171 self.out = out
172 self.channel = channel
173
174 def __call__(self, cmd, environ, cwd):
175 args = [util.quotecommand(cmd), cwd or '.']
176 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
177 data = '\0'.join(args)
178 self.out.write(struct.pack('>cI', self.channel, len(data)))
179 self.out.write(data)
180 self.out.flush()
181
182 length = self.in_.read(4)
183 length, = struct.unpack('>I', length)
184 if length != 4:
185 raise error.Abort(_('invalid response'))
186 rc, = struct.unpack('>i', self.in_.read(4))
187 return rc
188
189 _iochannels = [
190 # server.ch, ui.fp, mode
191 ('cin', 'fin', 'rb'),
192 ('cout', 'fout', 'wb'),
193 ('cerr', 'ferr', 'wb'),
194 ]
195
196 class chgcmdserver(commandserver.server):
197 def __init__(self, ui, repo, fin, fout, sock):
198 super(chgcmdserver, self).__init__(
199 _newchgui(ui, channeledsystem(fin, fout, 'S')), repo, fin, fout)
200 self.clientsock = sock
201 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
202
203 def cleanup(self):
204 # dispatch._runcatch() does not flush outputs if exception is not
205 # handled by dispatch._dispatch()
206 self.ui.flush()
207 self._restoreio()
208
209 def attachio(self):
210 """Attach to client's stdio passed via unix domain socket; all
211 channels except cresult will no longer be used
212 """
213 # tell client to sendmsg() with 1-byte payload, which makes it
214 # distinctive from "attachio\n" command consumed by client.read()
215 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
216 clientfds = osutil.recvfds(self.clientsock.fileno())
217 _log('received fds: %r\n' % clientfds)
218
219 ui = self.ui
220 ui.flush()
221 first = self._saveio()
222 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
223 assert fd > 0
224 fp = getattr(ui, fn)
225 os.dup2(fd, fp.fileno())
226 os.close(fd)
227 if not first:
228 continue
229 # reset buffering mode when client is first attached. as we want
230 # to see output immediately on pager, the mode stays unchanged
231 # when client re-attached. ferr is unchanged because it should
232 # be unbuffered no matter if it is a tty or not.
233 if fn == 'ferr':
234 newfp = fp
235 else:
236 # make it line buffered explicitly because the default is
237 # decided on first write(), where fout could be a pager.
238 if fp.isatty():
239 bufsize = 1 # line buffered
240 else:
241 bufsize = -1 # system default
242 newfp = os.fdopen(fp.fileno(), mode, bufsize)
243 setattr(ui, fn, newfp)
244 setattr(self, cn, newfp)
245
246 self.cresult.write(struct.pack('>i', len(clientfds)))
247
248 def _saveio(self):
249 if self._oldios:
250 return False
251 ui = self.ui
252 for cn, fn, _mode in _iochannels:
253 ch = getattr(self, cn)
254 fp = getattr(ui, fn)
255 fd = os.dup(fp.fileno())
256 self._oldios.append((ch, fp, fd))
257 return True
258
259 def _restoreio(self):
260 ui = self.ui
261 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
262 newfp = getattr(ui, fn)
263 # close newfp while it's associated with client; otherwise it
264 # would be closed when newfp is deleted
265 if newfp is not fp:
266 newfp.close()
267 # restore original fd: fp is open again
268 os.dup2(fd, fp.fileno())
269 os.close(fd)
270 setattr(self, cn, ch)
271 setattr(ui, fn, fp)
272 del self._oldios[:]
273
274 def chdir(self):
275 """Change current directory
276
277 Note that the behavior of --cwd option is bit different from this.
278 It does not affect --config parameter.
279 """
280 length = struct.unpack('>I', self._read(4))[0]
281 if not length:
282 return
283 path = self._read(length)
284 _log('chdir to %r\n' % path)
285 os.chdir(path)
286
287 def getpager(self):
288 """Read cmdargs and write pager command to r-channel if enabled
289
290 If pager isn't enabled, this writes '\0' because channeledoutput
291 does not allow to write empty data.
292 """
293 length = struct.unpack('>I', self._read(4))[0]
294 if not length:
295 args = []
296 else:
297 args = self._read(length).split('\0')
298 try:
299 cmd, _func, args, options, _cmdoptions = dispatch._parse(self.ui,
300 args)
301 except (error.Abort, error.AmbiguousCommand, error.CommandError,
302 error.UnknownCommand):
303 cmd = None
304 options = {}
305 if not cmd or 'pager' not in options:
306 self.cresult.write('\0')
307 return
308
309 pagercmd = _setuppagercmd(self.ui, options, cmd)
310 if pagercmd:
311 self.cresult.write(pagercmd)
312 else:
313 self.cresult.write('\0')
314
315 def setenv(self):
316 """Clear and update os.environ
317
318 Note that not all variables can make an effect on the running process.
319 """
320 length = struct.unpack('>I', self._read(4))[0]
321 if not length:
322 return
323 s = self._read(length)
324 try:
325 newenv = dict(l.split('=', 1) for l in s.split('\0'))
326 except ValueError:
327 raise ValueError('unexpected value in setenv request')
328
329 diffkeys = set(k for k in set(os.environ.keys() + newenv.keys())
330 if os.environ.get(k) != newenv.get(k))
331 _log('change env: %r\n' % sorted(diffkeys))
332
333 os.environ.clear()
334 os.environ.update(newenv)
335
336 if set(['HGPLAIN', 'HGPLAINEXCEPT']) & diffkeys:
337 # reload config so that ui.plain() takes effect
338 self.ui = _renewui(self.ui)
339
340 _clearenvaliases(commands.table)
341
342 capabilities = commandserver.server.capabilities.copy()
343 capabilities.update({'attachio': attachio,
344 'chdir': chdir,
345 'getpager': getpager,
346 'setenv': setenv})
347
348 # copied from mercurial/commandserver.py
349 class _requesthandler(SocketServer.StreamRequestHandler):
350 def handle(self):
351 ui = self.server.ui
352 repo = self.server.repo
353 sv = chgcmdserver(ui, repo, self.rfile, self.wfile, self.connection)
354 try:
355 try:
356 sv.serve()
357 # handle exceptions that may be raised by command server. most of
358 # known exceptions are caught by dispatch.
359 except error.Abort as inst:
360 ui.warn(_('abort: %s\n') % inst)
361 except IOError as inst:
362 if inst.errno != errno.EPIPE:
363 raise
364 except KeyboardInterrupt:
365 pass
366 finally:
367 sv.cleanup()
368 except: # re-raises
369 # also write traceback to error channel. otherwise client cannot
370 # see it because it is written to server's stderr by default.
371 traceback.print_exc(file=sv.cerr)
372 raise
373
374 class chgunixservice(commandserver.unixservice):
375 def init(self):
376 # drop options set for "hg serve --cmdserver" command
377 self.ui.setconfig('progress', 'assume-tty', None)
378 signal.signal(signal.SIGHUP, self._reloadconfig)
379 class cls(SocketServer.ForkingMixIn, SocketServer.UnixStreamServer):
380 ui = self.ui
381 repo = self.repo
382 self.server = cls(self.address, _requesthandler)
383 # avoid writing "listening at" message to stdout before attachio
384 # request, which calls setvbuf()
385
386 def _reloadconfig(self, signum, frame):
387 self.ui = self.server.ui = _renewui(self.ui)
388
389 def uisetup(ui):
390 commandserver._servicemap['chgunix'] = chgunixservice
General Comments 0
You need to be logged in to leave comments. Login now