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