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