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