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