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