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