##// END OF EJS Templates
chgserver: remove Python 2 file descriptor logic...
Yuya Nishihara -
r49806:cf99c4af default
parent child Browse files
Show More
@@ -1,755 +1,749
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 encoding.environ.items()
137 for k, v in encoding.environ.items()
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:
199 class hashstate:
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:
295 class channeledsystem:
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 environ.items())
323 args.extend(b'%s=%s' % (k, v) for k, v in environ.items())
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 else:
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 if newfp is not fp:
418 if newfp is not fp:
419 setattr(ui, fn, newfp)
419 setattr(ui, fn, newfp)
420 setattr(self, cn, newfp)
420 setattr(self, cn, newfp)
421
421
422 self._ioattached = True
422 self._ioattached = True
423 self.cresult.write(struct.pack(b'>i', len(clientfds)))
423 self.cresult.write(struct.pack(b'>i', len(clientfds)))
424
424
425 def _saveio(self):
425 def _saveio(self):
426 if self._oldios:
426 if self._oldios:
427 return
427 return
428 ui = self.ui
428 ui = self.ui
429 for cn, fn, _mode in _iochannels:
429 for cn, fn, _mode in _iochannels:
430 ch = getattr(self, cn)
430 ch = getattr(self, cn)
431 fp = getattr(ui, fn)
431 fp = getattr(ui, fn)
432 fd = os.dup(fp.fileno())
432 fd = os.dup(fp.fileno())
433 self._oldios.append((ch, fp, fd))
433 self._oldios.append((ch, fp, fd))
434
434
435 def _restoreio(self):
435 def _restoreio(self):
436 if not self._oldios:
436 if not self._oldios:
437 return
437 return
438 nullfd = os.open(os.devnull, os.O_WRONLY)
438 nullfd = os.open(os.devnull, os.O_WRONLY)
439 ui = self.ui
439 ui = self.ui
440 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):
441 newfp = getattr(ui, fn)
442 # On Python 3, newfp is just a wrapper around fp even if newfp is
443 # not fp, so deleting newfp is safe.
444 if newfp is not fp:
445 newfp.close()
446 # restore original fd: fp is open again
447 try:
441 try:
448 if newfp is fp and 'w' in mode:
442 if 'w' in mode:
449 # Discard buffered data which couldn't be flushed because
443 # Discard buffered data which couldn't be flushed because
450 # of EPIPE. The data should belong to the current session
444 # of EPIPE. The data should belong to the current session
451 # and should never persist.
445 # and should never persist.
452 os.dup2(nullfd, fp.fileno())
446 os.dup2(nullfd, fp.fileno())
453 fp.flush()
447 fp.flush()
454 os.dup2(fd, fp.fileno())
448 os.dup2(fd, fp.fileno())
455 except OSError as err:
449 except OSError as err:
456 # According to issue6330, running chg on heavy loaded systems
450 # According to issue6330, running chg on heavy loaded systems
457 # can lead to EBUSY. [man dup2] indicates that, on Linux,
451 # can lead to EBUSY. [man dup2] indicates that, on Linux,
458 # EBUSY comes from a race condition between open() and dup2().
452 # EBUSY comes from a race condition between open() and dup2().
459 # However it's not clear why open() race occurred for
453 # However it's not clear why open() race occurred for
460 # newfd=stdin/out/err.
454 # newfd=stdin/out/err.
461 self.ui.log(
455 self.ui.log(
462 b'chgserver',
456 b'chgserver',
463 b'got %s while duplicating %s\n',
457 b'got %s while duplicating %s\n',
464 stringutil.forcebytestr(err),
458 stringutil.forcebytestr(err),
465 fn,
459 fn,
466 )
460 )
467 os.close(fd)
461 os.close(fd)
468 setattr(self, cn, ch)
462 setattr(self, cn, ch)
469 setattr(ui, fn, fp)
463 setattr(ui, fn, fp)
470 os.close(nullfd)
464 os.close(nullfd)
471 del self._oldios[:]
465 del self._oldios[:]
472
466
473 def validate(self):
467 def validate(self):
474 """Reload the config and check if the server is up to date
468 """Reload the config and check if the server is up to date
475
469
476 Read a list of '\0' separated arguments.
470 Read a list of '\0' separated arguments.
477 Write a non-empty list of '\0' separated instruction strings or '\0'
471 Write a non-empty list of '\0' separated instruction strings or '\0'
478 if the list is empty.
472 if the list is empty.
479 An instruction string could be either:
473 An instruction string could be either:
480 - "unlink $path", the client should unlink the path to stop the
474 - "unlink $path", the client should unlink the path to stop the
481 outdated server.
475 outdated server.
482 - "redirect $path", the client should attempt to connect to $path
476 - "redirect $path", the client should attempt to connect to $path
483 first. If it does not work, start a new server. It implies
477 first. If it does not work, start a new server. It implies
484 "reconnect".
478 "reconnect".
485 - "exit $n", the client should exit directly with code n.
479 - "exit $n", the client should exit directly with code n.
486 This may happen if we cannot parse the config.
480 This may happen if we cannot parse the config.
487 - "reconnect", the client should close the connection and
481 - "reconnect", the client should close the connection and
488 reconnect.
482 reconnect.
489 If neither "reconnect" nor "redirect" is included in the instruction
483 If neither "reconnect" nor "redirect" is included in the instruction
490 list, the client can continue with this server after completing all
484 list, the client can continue with this server after completing all
491 the instructions.
485 the instructions.
492 """
486 """
493 args = self._readlist()
487 args = self._readlist()
494 errorraised = False
488 errorraised = False
495 detailed_exit_code = 255
489 detailed_exit_code = 255
496 try:
490 try:
497 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
491 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
498 except error.RepoError as inst:
492 except error.RepoError as inst:
499 # RepoError can be raised while trying to read shared source
493 # RepoError can be raised while trying to read shared source
500 # configuration
494 # configuration
501 self.ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
495 self.ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
502 if inst.hint:
496 if inst.hint:
503 self.ui.error(_(b"(%s)\n") % inst.hint)
497 self.ui.error(_(b"(%s)\n") % inst.hint)
504 errorraised = True
498 errorraised = True
505 except error.Error as inst:
499 except error.Error as inst:
506 if inst.detailed_exit_code is not None:
500 if inst.detailed_exit_code is not None:
507 detailed_exit_code = inst.detailed_exit_code
501 detailed_exit_code = inst.detailed_exit_code
508 self.ui.error(inst.format())
502 self.ui.error(inst.format())
509 errorraised = True
503 errorraised = True
510
504
511 if errorraised:
505 if errorraised:
512 self.ui.flush()
506 self.ui.flush()
513 exit_code = 255
507 exit_code = 255
514 if self.ui.configbool(b'ui', b'detailed-exit-code'):
508 if self.ui.configbool(b'ui', b'detailed-exit-code'):
515 exit_code = detailed_exit_code
509 exit_code = detailed_exit_code
516 self.cresult.write(b'exit %d' % exit_code)
510 self.cresult.write(b'exit %d' % exit_code)
517 return
511 return
518 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
512 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
519 insts = []
513 insts = []
520 if newhash.mtimehash != self.hashstate.mtimehash:
514 if newhash.mtimehash != self.hashstate.mtimehash:
521 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
515 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
522 insts.append(b'unlink %s' % addr)
516 insts.append(b'unlink %s' % addr)
523 # mtimehash is empty if one or more extensions fail to load.
517 # mtimehash is empty if one or more extensions fail to load.
524 # to be compatible with hg, still serve the client this time.
518 # to be compatible with hg, still serve the client this time.
525 if self.hashstate.mtimehash:
519 if self.hashstate.mtimehash:
526 insts.append(b'reconnect')
520 insts.append(b'reconnect')
527 if newhash.confighash != self.hashstate.confighash:
521 if newhash.confighash != self.hashstate.confighash:
528 addr = _hashaddress(self.baseaddress, newhash.confighash)
522 addr = _hashaddress(self.baseaddress, newhash.confighash)
529 insts.append(b'redirect %s' % addr)
523 insts.append(b'redirect %s' % addr)
530 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
524 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
531 self.cresult.write(b'\0'.join(insts) or b'\0')
525 self.cresult.write(b'\0'.join(insts) or b'\0')
532
526
533 def chdir(self):
527 def chdir(self):
534 """Change current directory
528 """Change current directory
535
529
536 Note that the behavior of --cwd option is bit different from this.
530 Note that the behavior of --cwd option is bit different from this.
537 It does not affect --config parameter.
531 It does not affect --config parameter.
538 """
532 """
539 path = self._readstr()
533 path = self._readstr()
540 if not path:
534 if not path:
541 return
535 return
542 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
536 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
543 os.chdir(path)
537 os.chdir(path)
544
538
545 def setumask(self):
539 def setumask(self):
546 """Change umask (DEPRECATED)"""
540 """Change umask (DEPRECATED)"""
547 # BUG: this does not follow the message frame structure, but kept for
541 # BUG: this does not follow the message frame structure, but kept for
548 # backward compatibility with old chg clients for some time
542 # backward compatibility with old chg clients for some time
549 self._setumask(self._read(4))
543 self._setumask(self._read(4))
550
544
551 def setumask2(self):
545 def setumask2(self):
552 """Change umask"""
546 """Change umask"""
553 data = self._readstr()
547 data = self._readstr()
554 if len(data) != 4:
548 if len(data) != 4:
555 raise ValueError(b'invalid mask length in setumask2 request')
549 raise ValueError(b'invalid mask length in setumask2 request')
556 self._setumask(data)
550 self._setumask(data)
557
551
558 def _setumask(self, data):
552 def _setumask(self, data):
559 mask = struct.unpack(b'>I', data)[0]
553 mask = struct.unpack(b'>I', data)[0]
560 self.ui.log(b'chgserver', b'setumask %r\n', mask)
554 self.ui.log(b'chgserver', b'setumask %r\n', mask)
561 util.setumask(mask)
555 util.setumask(mask)
562
556
563 def runcommand(self):
557 def runcommand(self):
564 # pager may be attached within the runcommand session, which should
558 # pager may be attached within the runcommand session, which should
565 # be detached at the end of the session. otherwise the pager wouldn't
559 # be detached at the end of the session. otherwise the pager wouldn't
566 # receive EOF.
560 # receive EOF.
567 globaloldios = self._oldios
561 globaloldios = self._oldios
568 self._oldios = []
562 self._oldios = []
569 try:
563 try:
570 return super(chgcmdserver, self).runcommand()
564 return super(chgcmdserver, self).runcommand()
571 finally:
565 finally:
572 self._restoreio()
566 self._restoreio()
573 self._oldios = globaloldios
567 self._oldios = globaloldios
574
568
575 def setenv(self):
569 def setenv(self):
576 """Clear and update os.environ
570 """Clear and update os.environ
577
571
578 Note that not all variables can make an effect on the running process.
572 Note that not all variables can make an effect on the running process.
579 """
573 """
580 l = self._readlist()
574 l = self._readlist()
581 try:
575 try:
582 newenv = dict(s.split(b'=', 1) for s in l)
576 newenv = dict(s.split(b'=', 1) for s in l)
583 except ValueError:
577 except ValueError:
584 raise ValueError(b'unexpected value in setenv request')
578 raise ValueError(b'unexpected value in setenv request')
585 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
579 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
586
580
587 encoding.environ.clear()
581 encoding.environ.clear()
588 encoding.environ.update(newenv)
582 encoding.environ.update(newenv)
589
583
590 capabilities = commandserver.server.capabilities.copy()
584 capabilities = commandserver.server.capabilities.copy()
591 capabilities.update(
585 capabilities.update(
592 {
586 {
593 b'attachio': attachio,
587 b'attachio': attachio,
594 b'chdir': chdir,
588 b'chdir': chdir,
595 b'runcommand': runcommand,
589 b'runcommand': runcommand,
596 b'setenv': setenv,
590 b'setenv': setenv,
597 b'setumask': setumask,
591 b'setumask': setumask,
598 b'setumask2': setumask2,
592 b'setumask2': setumask2,
599 }
593 }
600 )
594 )
601
595
602 if util.safehasattr(procutil, b'setprocname'):
596 if util.safehasattr(procutil, b'setprocname'):
603
597
604 def setprocname(self):
598 def setprocname(self):
605 """Change process title"""
599 """Change process title"""
606 name = self._readstr()
600 name = self._readstr()
607 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
601 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
608 procutil.setprocname(name)
602 procutil.setprocname(name)
609
603
610 capabilities[b'setprocname'] = setprocname
604 capabilities[b'setprocname'] = setprocname
611
605
612
606
613 def _tempaddress(address):
607 def _tempaddress(address):
614 return b'%s.%d.tmp' % (address, os.getpid())
608 return b'%s.%d.tmp' % (address, os.getpid())
615
609
616
610
617 def _hashaddress(address, hashstr):
611 def _hashaddress(address, hashstr):
618 # if the basename of address contains '.', use only the left part. this
612 # if the basename of address contains '.', use only the left part. this
619 # makes it possible for the client to pass 'server.tmp$PID' and follow by
613 # makes it possible for the client to pass 'server.tmp$PID' and follow by
620 # an atomic rename to avoid locking when spawning new servers.
614 # an atomic rename to avoid locking when spawning new servers.
621 dirname, basename = os.path.split(address)
615 dirname, basename = os.path.split(address)
622 basename = basename.split(b'.', 1)[0]
616 basename = basename.split(b'.', 1)[0]
623 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
617 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
624
618
625
619
626 class chgunixservicehandler:
620 class chgunixservicehandler:
627 """Set of operations for chg services"""
621 """Set of operations for chg services"""
628
622
629 pollinterval = 1 # [sec]
623 pollinterval = 1 # [sec]
630
624
631 def __init__(self, ui):
625 def __init__(self, ui):
632 self.ui = ui
626 self.ui = ui
633
627
634 # TODO: use PEP 526 syntax (`_hashstate: hashstate` at the class level)
628 # TODO: use PEP 526 syntax (`_hashstate: hashstate` at the class level)
635 # when 3.5 support is dropped.
629 # when 3.5 support is dropped.
636 self._hashstate = None # type: hashstate
630 self._hashstate = None # type: hashstate
637 self._baseaddress = None # type: bytes
631 self._baseaddress = None # type: bytes
638 self._realaddress = None # type: bytes
632 self._realaddress = None # type: bytes
639
633
640 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
634 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
641 self._lastactive = time.time()
635 self._lastactive = time.time()
642
636
643 def bindsocket(self, sock, address):
637 def bindsocket(self, sock, address):
644 self._inithashstate(address)
638 self._inithashstate(address)
645 self._checkextensions()
639 self._checkextensions()
646 self._bind(sock)
640 self._bind(sock)
647 self._createsymlink()
641 self._createsymlink()
648 # no "listening at" message should be printed to simulate hg behavior
642 # no "listening at" message should be printed to simulate hg behavior
649
643
650 def _inithashstate(self, address):
644 def _inithashstate(self, address):
651 self._baseaddress = address
645 self._baseaddress = address
652 if self.ui.configbool(b'chgserver', b'skiphash'):
646 if self.ui.configbool(b'chgserver', b'skiphash'):
653 self._hashstate = None
647 self._hashstate = None
654 self._realaddress = address
648 self._realaddress = address
655 return
649 return
656 self._hashstate = hashstate.fromui(self.ui)
650 self._hashstate = hashstate.fromui(self.ui)
657 self._realaddress = _hashaddress(address, self._hashstate.confighash)
651 self._realaddress = _hashaddress(address, self._hashstate.confighash)
658
652
659 def _checkextensions(self):
653 def _checkextensions(self):
660 if not self._hashstate:
654 if not self._hashstate:
661 return
655 return
662 if extensions.notloaded():
656 if extensions.notloaded():
663 # one or more extensions failed to load. mtimehash becomes
657 # one or more extensions failed to load. mtimehash becomes
664 # meaningless because we do not know the paths of those extensions.
658 # meaningless because we do not know the paths of those extensions.
665 # set mtimehash to an illegal hash value to invalidate the server.
659 # set mtimehash to an illegal hash value to invalidate the server.
666 self._hashstate.mtimehash = b''
660 self._hashstate.mtimehash = b''
667
661
668 def _bind(self, sock):
662 def _bind(self, sock):
669 # use a unique temp address so we can stat the file and do ownership
663 # use a unique temp address so we can stat the file and do ownership
670 # check later
664 # check later
671 tempaddress = _tempaddress(self._realaddress)
665 tempaddress = _tempaddress(self._realaddress)
672 util.bindunixsocket(sock, tempaddress)
666 util.bindunixsocket(sock, tempaddress)
673 self._socketstat = os.stat(tempaddress)
667 self._socketstat = os.stat(tempaddress)
674 sock.listen(socket.SOMAXCONN)
668 sock.listen(socket.SOMAXCONN)
675 # rename will replace the old socket file if exists atomically. the
669 # rename will replace the old socket file if exists atomically. the
676 # old server will detect ownership change and exit.
670 # old server will detect ownership change and exit.
677 util.rename(tempaddress, self._realaddress)
671 util.rename(tempaddress, self._realaddress)
678
672
679 def _createsymlink(self):
673 def _createsymlink(self):
680 if self._baseaddress == self._realaddress:
674 if self._baseaddress == self._realaddress:
681 return
675 return
682 tempaddress = _tempaddress(self._baseaddress)
676 tempaddress = _tempaddress(self._baseaddress)
683 os.symlink(os.path.basename(self._realaddress), tempaddress)
677 os.symlink(os.path.basename(self._realaddress), tempaddress)
684 util.rename(tempaddress, self._baseaddress)
678 util.rename(tempaddress, self._baseaddress)
685
679
686 def _issocketowner(self):
680 def _issocketowner(self):
687 try:
681 try:
688 st = os.stat(self._realaddress)
682 st = os.stat(self._realaddress)
689 return (
683 return (
690 st.st_ino == self._socketstat.st_ino
684 st.st_ino == self._socketstat.st_ino
691 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
685 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
692 )
686 )
693 except OSError:
687 except OSError:
694 return False
688 return False
695
689
696 def unlinksocket(self, address):
690 def unlinksocket(self, address):
697 if not self._issocketowner():
691 if not self._issocketowner():
698 return
692 return
699 # it is possible to have a race condition here that we may
693 # it is possible to have a race condition here that we may
700 # remove another server's socket file. but that's okay
694 # remove another server's socket file. but that's okay
701 # since that server will detect and exit automatically and
695 # since that server will detect and exit automatically and
702 # the client will start a new server on demand.
696 # the client will start a new server on demand.
703 util.tryunlink(self._realaddress)
697 util.tryunlink(self._realaddress)
704
698
705 def shouldexit(self):
699 def shouldexit(self):
706 if not self._issocketowner():
700 if not self._issocketowner():
707 self.ui.log(
701 self.ui.log(
708 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
702 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
709 )
703 )
710 return True
704 return True
711 if time.time() - self._lastactive > self._idletimeout:
705 if time.time() - self._lastactive > self._idletimeout:
712 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
706 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
713 return True
707 return True
714 return False
708 return False
715
709
716 def newconnection(self):
710 def newconnection(self):
717 self._lastactive = time.time()
711 self._lastactive = time.time()
718
712
719 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
713 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
720 return chgcmdserver(
714 return chgcmdserver(
721 self.ui,
715 self.ui,
722 repo,
716 repo,
723 fin,
717 fin,
724 fout,
718 fout,
725 conn,
719 conn,
726 prereposetups,
720 prereposetups,
727 self._hashstate,
721 self._hashstate,
728 self._baseaddress,
722 self._baseaddress,
729 )
723 )
730
724
731
725
732 def chgunixservice(ui, repo, opts):
726 def chgunixservice(ui, repo, opts):
733 # CHGINTERNALMARK is set by chg client. It is an indication of things are
727 # CHGINTERNALMARK is set by chg client. It is an indication of things are
734 # started by chg so other code can do things accordingly, like disabling
728 # started by chg so other code can do things accordingly, like disabling
735 # demandimport or detecting chg client started by chg client. When executed
729 # demandimport or detecting chg client started by chg client. When executed
736 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
730 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
737 # environ cleaner.
731 # environ cleaner.
738 if b'CHGINTERNALMARK' in encoding.environ:
732 if b'CHGINTERNALMARK' in encoding.environ:
739 del encoding.environ[b'CHGINTERNALMARK']
733 del encoding.environ[b'CHGINTERNALMARK']
740 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
734 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
741 # it thinks the current value is "C". This breaks the hash computation and
735 # it thinks the current value is "C". This breaks the hash computation and
742 # causes chg to restart loop.
736 # causes chg to restart loop.
743 if b'CHGORIG_LC_CTYPE' in encoding.environ:
737 if b'CHGORIG_LC_CTYPE' in encoding.environ:
744 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
738 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
745 del encoding.environ[b'CHGORIG_LC_CTYPE']
739 del encoding.environ[b'CHGORIG_LC_CTYPE']
746 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
740 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
747 if b'LC_CTYPE' in encoding.environ:
741 if b'LC_CTYPE' in encoding.environ:
748 del encoding.environ[b'LC_CTYPE']
742 del encoding.environ[b'LC_CTYPE']
749 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
743 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
750
744
751 if repo:
745 if repo:
752 # one chgserver can serve multiple repos. drop repo information
746 # one chgserver can serve multiple repos. drop repo information
753 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
747 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
754 h = chgunixservicehandler(ui)
748 h = chgunixservicehandler(ui)
755 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
749 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
General Comments 0
You need to be logged in to leave comments. Login now