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