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