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