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