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