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