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