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