##// END OF EJS Templates
chgserver: discard buffered output before restoring fds (issue6207)...
Yuya Nishihara -
r45745:a17454a1 stable
parent child Browse files
Show More
@@ -1,731 +1,741 b''
1 # chgserver.py - command server extension for cHg
1 # chgserver.py - command server extension for cHg
2 #
2 #
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """command server extension for cHg
8 """command server extension for cHg
9
9
10 'S' channel (read/write)
10 'S' channel (read/write)
11 propagate ui.system() request to client
11 propagate ui.system() request to client
12
12
13 'attachio' command
13 'attachio' command
14 attach client's stdio passed by sendmsg()
14 attach client's stdio passed by sendmsg()
15
15
16 'chdir' command
16 'chdir' command
17 change current directory
17 change current directory
18
18
19 'setenv' command
19 'setenv' command
20 replace os.environ completely
20 replace os.environ completely
21
21
22 'setumask' command (DEPRECATED)
22 'setumask' command (DEPRECATED)
23 'setumask2' command
23 'setumask2' command
24 set umask
24 set umask
25
25
26 'validate' command
26 'validate' command
27 reload the config and check if the server is up to date
27 reload the config and check if the server is up to date
28
28
29 Config
29 Config
30 ------
30 ------
31
31
32 ::
32 ::
33
33
34 [chgserver]
34 [chgserver]
35 # how long (in seconds) should an idle chg server exit
35 # how long (in seconds) should an idle chg server exit
36 idletimeout = 3600
36 idletimeout = 3600
37
37
38 # whether to skip config or env change checks
38 # whether to skip config or env change checks
39 skiphash = False
39 skiphash = False
40 """
40 """
41
41
42 from __future__ import absolute_import
42 from __future__ import absolute_import
43
43
44 import inspect
44 import inspect
45 import os
45 import os
46 import re
46 import re
47 import socket
47 import socket
48 import stat
48 import stat
49 import struct
49 import struct
50 import time
50 import time
51
51
52 from .i18n import _
52 from .i18n import _
53 from .pycompat import (
53 from .pycompat import (
54 getattr,
54 getattr,
55 setattr,
55 setattr,
56 )
56 )
57
57
58 from . import (
58 from . import (
59 commandserver,
59 commandserver,
60 encoding,
60 encoding,
61 error,
61 error,
62 extensions,
62 extensions,
63 node,
63 node,
64 pycompat,
64 pycompat,
65 util,
65 util,
66 )
66 )
67
67
68 from .utils import (
68 from .utils import (
69 hashutil,
69 hashutil,
70 procutil,
70 procutil,
71 stringutil,
71 stringutil,
72 )
72 )
73
73
74
74
75 def _hashlist(items):
75 def _hashlist(items):
76 """return sha1 hexdigest for a list"""
76 """return sha1 hexdigest for a list"""
77 return node.hex(hashutil.sha1(stringutil.pprint(items)).digest())
77 return node.hex(hashutil.sha1(stringutil.pprint(items)).digest())
78
78
79
79
80 # sensitive config sections affecting confighash
80 # sensitive config sections affecting confighash
81 _configsections = [
81 _configsections = [
82 b'alias', # affects global state commands.table
82 b'alias', # affects global state commands.table
83 b'diff-tools', # affects whether gui or not in extdiff's uisetup
83 b'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 else:
412 else:
413 # make it line buffered explicitly because the default is
413 # make it line buffered explicitly because the default is
414 # decided on first write(), where fout could be a pager.
414 # decided on first write(), where fout could be a pager.
415 if fp.isatty():
415 if fp.isatty():
416 bufsize = 1 # line buffered
416 bufsize = 1 # line buffered
417 else:
417 else:
418 bufsize = -1 # system default
418 bufsize = -1 # system default
419 newfp = os.fdopen(fp.fileno(), mode, bufsize)
419 newfp = os.fdopen(fp.fileno(), mode, bufsize)
420 setattr(ui, fn, newfp)
420 setattr(ui, fn, newfp)
421 setattr(self, cn, newfp)
421 setattr(self, cn, newfp)
422
422
423 self._ioattached = True
423 self._ioattached = True
424 self.cresult.write(struct.pack(b'>i', len(clientfds)))
424 self.cresult.write(struct.pack(b'>i', len(clientfds)))
425
425
426 def _saveio(self):
426 def _saveio(self):
427 if self._oldios:
427 if self._oldios:
428 return
428 return
429 ui = self.ui
429 ui = self.ui
430 for cn, fn, _mode in _iochannels:
430 for cn, fn, _mode in _iochannels:
431 ch = getattr(self, cn)
431 ch = getattr(self, cn)
432 fp = getattr(ui, fn)
432 fp = getattr(ui, fn)
433 fd = os.dup(fp.fileno())
433 fd = os.dup(fp.fileno())
434 self._oldios.append((ch, fp, fd))
434 self._oldios.append((ch, fp, fd))
435
435
436 def _restoreio(self):
436 def _restoreio(self):
437 if not self._oldios:
438 return
439 nullfd = os.open(os.devnull, os.O_WRONLY)
437 ui = self.ui
440 ui = self.ui
438 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
441 for (ch, fp, fd), (cn, fn, mode) in zip(self._oldios, _iochannels):
439 newfp = getattr(ui, fn)
442 newfp = getattr(ui, fn)
440 # close newfp while it's associated with client; otherwise it
443 # close newfp while it's associated with client; otherwise it
441 # would be closed when newfp is deleted
444 # would be closed when newfp is deleted
442 if newfp is not fp:
445 if newfp is not fp:
443 newfp.close()
446 newfp.close()
444 # restore original fd: fp is open again
447 # restore original fd: fp is open again
445 try:
448 try:
449 if newfp is fp and 'w' in mode:
450 # Discard buffered data which couldn't be flushed because
451 # of EPIPE. The data should belong to the current session
452 # and should never persist.
453 os.dup2(nullfd, fp.fileno())
454 fp.flush()
446 os.dup2(fd, fp.fileno())
455 os.dup2(fd, fp.fileno())
447 except OSError as err:
456 except OSError as err:
448 # According to issue6330, running chg on heavy loaded systems
457 # According to issue6330, running chg on heavy loaded systems
449 # can lead to EBUSY. [man dup2] indicates that, on Linux,
458 # can lead to EBUSY. [man dup2] indicates that, on Linux,
450 # EBUSY comes from a race condition between open() and dup2().
459 # EBUSY comes from a race condition between open() and dup2().
451 # However it's not clear why open() race occurred for
460 # However it's not clear why open() race occurred for
452 # newfd=stdin/out/err.
461 # newfd=stdin/out/err.
453 self.ui.log(
462 self.ui.log(
454 b'chgserver',
463 b'chgserver',
455 b'got %s while duplicating %s\n',
464 b'got %s while duplicating %s\n',
456 stringutil.forcebytestr(err),
465 stringutil.forcebytestr(err),
457 fn,
466 fn,
458 )
467 )
459 os.close(fd)
468 os.close(fd)
460 setattr(self, cn, ch)
469 setattr(self, cn, ch)
461 setattr(ui, fn, fp)
470 setattr(ui, fn, fp)
471 os.close(nullfd)
462 del self._oldios[:]
472 del self._oldios[:]
463
473
464 def validate(self):
474 def validate(self):
465 """Reload the config and check if the server is up to date
475 """Reload the config and check if the server is up to date
466
476
467 Read a list of '\0' separated arguments.
477 Read a list of '\0' separated arguments.
468 Write a non-empty list of '\0' separated instruction strings or '\0'
478 Write a non-empty list of '\0' separated instruction strings or '\0'
469 if the list is empty.
479 if the list is empty.
470 An instruction string could be either:
480 An instruction string could be either:
471 - "unlink $path", the client should unlink the path to stop the
481 - "unlink $path", the client should unlink the path to stop the
472 outdated server.
482 outdated server.
473 - "redirect $path", the client should attempt to connect to $path
483 - "redirect $path", the client should attempt to connect to $path
474 first. If it does not work, start a new server. It implies
484 first. If it does not work, start a new server. It implies
475 "reconnect".
485 "reconnect".
476 - "exit $n", the client should exit directly with code n.
486 - "exit $n", the client should exit directly with code n.
477 This may happen if we cannot parse the config.
487 This may happen if we cannot parse the config.
478 - "reconnect", the client should close the connection and
488 - "reconnect", the client should close the connection and
479 reconnect.
489 reconnect.
480 If neither "reconnect" nor "redirect" is included in the instruction
490 If neither "reconnect" nor "redirect" is included in the instruction
481 list, the client can continue with this server after completing all
491 list, the client can continue with this server after completing all
482 the instructions.
492 the instructions.
483 """
493 """
484 from . import dispatch # avoid cycle
494 from . import dispatch # avoid cycle
485
495
486 args = self._readlist()
496 args = self._readlist()
487 try:
497 try:
488 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
498 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
489 except error.ParseError as inst:
499 except error.ParseError as inst:
490 dispatch._formatparse(self.ui.warn, inst)
500 dispatch._formatparse(self.ui.warn, inst)
491 self.ui.flush()
501 self.ui.flush()
492 self.cresult.write(b'exit 255')
502 self.cresult.write(b'exit 255')
493 return
503 return
494 except error.Abort as inst:
504 except error.Abort as inst:
495 self.ui.error(_(b"abort: %s\n") % inst)
505 self.ui.error(_(b"abort: %s\n") % inst)
496 if inst.hint:
506 if inst.hint:
497 self.ui.error(_(b"(%s)\n") % inst.hint)
507 self.ui.error(_(b"(%s)\n") % inst.hint)
498 self.ui.flush()
508 self.ui.flush()
499 self.cresult.write(b'exit 255')
509 self.cresult.write(b'exit 255')
500 return
510 return
501 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
511 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
502 insts = []
512 insts = []
503 if newhash.mtimehash != self.hashstate.mtimehash:
513 if newhash.mtimehash != self.hashstate.mtimehash:
504 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
514 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
505 insts.append(b'unlink %s' % addr)
515 insts.append(b'unlink %s' % addr)
506 # mtimehash is empty if one or more extensions fail to load.
516 # mtimehash is empty if one or more extensions fail to load.
507 # to be compatible with hg, still serve the client this time.
517 # to be compatible with hg, still serve the client this time.
508 if self.hashstate.mtimehash:
518 if self.hashstate.mtimehash:
509 insts.append(b'reconnect')
519 insts.append(b'reconnect')
510 if newhash.confighash != self.hashstate.confighash:
520 if newhash.confighash != self.hashstate.confighash:
511 addr = _hashaddress(self.baseaddress, newhash.confighash)
521 addr = _hashaddress(self.baseaddress, newhash.confighash)
512 insts.append(b'redirect %s' % addr)
522 insts.append(b'redirect %s' % addr)
513 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
523 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
514 self.cresult.write(b'\0'.join(insts) or b'\0')
524 self.cresult.write(b'\0'.join(insts) or b'\0')
515
525
516 def chdir(self):
526 def chdir(self):
517 """Change current directory
527 """Change current directory
518
528
519 Note that the behavior of --cwd option is bit different from this.
529 Note that the behavior of --cwd option is bit different from this.
520 It does not affect --config parameter.
530 It does not affect --config parameter.
521 """
531 """
522 path = self._readstr()
532 path = self._readstr()
523 if not path:
533 if not path:
524 return
534 return
525 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
535 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
526 os.chdir(path)
536 os.chdir(path)
527
537
528 def setumask(self):
538 def setumask(self):
529 """Change umask (DEPRECATED)"""
539 """Change umask (DEPRECATED)"""
530 # BUG: this does not follow the message frame structure, but kept for
540 # BUG: this does not follow the message frame structure, but kept for
531 # backward compatibility with old chg clients for some time
541 # backward compatibility with old chg clients for some time
532 self._setumask(self._read(4))
542 self._setumask(self._read(4))
533
543
534 def setumask2(self):
544 def setumask2(self):
535 """Change umask"""
545 """Change umask"""
536 data = self._readstr()
546 data = self._readstr()
537 if len(data) != 4:
547 if len(data) != 4:
538 raise ValueError(b'invalid mask length in setumask2 request')
548 raise ValueError(b'invalid mask length in setumask2 request')
539 self._setumask(data)
549 self._setumask(data)
540
550
541 def _setumask(self, data):
551 def _setumask(self, data):
542 mask = struct.unpack(b'>I', data)[0]
552 mask = struct.unpack(b'>I', data)[0]
543 self.ui.log(b'chgserver', b'setumask %r\n', mask)
553 self.ui.log(b'chgserver', b'setumask %r\n', mask)
544 util.setumask(mask)
554 util.setumask(mask)
545
555
546 def runcommand(self):
556 def runcommand(self):
547 # pager may be attached within the runcommand session, which should
557 # pager may be attached within the runcommand session, which should
548 # be detached at the end of the session. otherwise the pager wouldn't
558 # be detached at the end of the session. otherwise the pager wouldn't
549 # receive EOF.
559 # receive EOF.
550 globaloldios = self._oldios
560 globaloldios = self._oldios
551 self._oldios = []
561 self._oldios = []
552 try:
562 try:
553 return super(chgcmdserver, self).runcommand()
563 return super(chgcmdserver, self).runcommand()
554 finally:
564 finally:
555 self._restoreio()
565 self._restoreio()
556 self._oldios = globaloldios
566 self._oldios = globaloldios
557
567
558 def setenv(self):
568 def setenv(self):
559 """Clear and update os.environ
569 """Clear and update os.environ
560
570
561 Note that not all variables can make an effect on the running process.
571 Note that not all variables can make an effect on the running process.
562 """
572 """
563 l = self._readlist()
573 l = self._readlist()
564 try:
574 try:
565 newenv = dict(s.split(b'=', 1) for s in l)
575 newenv = dict(s.split(b'=', 1) for s in l)
566 except ValueError:
576 except ValueError:
567 raise ValueError(b'unexpected value in setenv request')
577 raise ValueError(b'unexpected value in setenv request')
568 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
578 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
569
579
570 encoding.environ.clear()
580 encoding.environ.clear()
571 encoding.environ.update(newenv)
581 encoding.environ.update(newenv)
572
582
573 capabilities = commandserver.server.capabilities.copy()
583 capabilities = commandserver.server.capabilities.copy()
574 capabilities.update(
584 capabilities.update(
575 {
585 {
576 b'attachio': attachio,
586 b'attachio': attachio,
577 b'chdir': chdir,
587 b'chdir': chdir,
578 b'runcommand': runcommand,
588 b'runcommand': runcommand,
579 b'setenv': setenv,
589 b'setenv': setenv,
580 b'setumask': setumask,
590 b'setumask': setumask,
581 b'setumask2': setumask2,
591 b'setumask2': setumask2,
582 }
592 }
583 )
593 )
584
594
585 if util.safehasattr(procutil, b'setprocname'):
595 if util.safehasattr(procutil, b'setprocname'):
586
596
587 def setprocname(self):
597 def setprocname(self):
588 """Change process title"""
598 """Change process title"""
589 name = self._readstr()
599 name = self._readstr()
590 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
600 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
591 procutil.setprocname(name)
601 procutil.setprocname(name)
592
602
593 capabilities[b'setprocname'] = setprocname
603 capabilities[b'setprocname'] = setprocname
594
604
595
605
596 def _tempaddress(address):
606 def _tempaddress(address):
597 return b'%s.%d.tmp' % (address, os.getpid())
607 return b'%s.%d.tmp' % (address, os.getpid())
598
608
599
609
600 def _hashaddress(address, hashstr):
610 def _hashaddress(address, hashstr):
601 # if the basename of address contains '.', use only the left part. this
611 # if the basename of address contains '.', use only the left part. this
602 # makes it possible for the client to pass 'server.tmp$PID' and follow by
612 # makes it possible for the client to pass 'server.tmp$PID' and follow by
603 # an atomic rename to avoid locking when spawning new servers.
613 # an atomic rename to avoid locking when spawning new servers.
604 dirname, basename = os.path.split(address)
614 dirname, basename = os.path.split(address)
605 basename = basename.split(b'.', 1)[0]
615 basename = basename.split(b'.', 1)[0]
606 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
616 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
607
617
608
618
609 class chgunixservicehandler(object):
619 class chgunixservicehandler(object):
610 """Set of operations for chg services"""
620 """Set of operations for chg services"""
611
621
612 pollinterval = 1 # [sec]
622 pollinterval = 1 # [sec]
613
623
614 def __init__(self, ui):
624 def __init__(self, ui):
615 self.ui = ui
625 self.ui = ui
616 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
626 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
617 self._lastactive = time.time()
627 self._lastactive = time.time()
618
628
619 def bindsocket(self, sock, address):
629 def bindsocket(self, sock, address):
620 self._inithashstate(address)
630 self._inithashstate(address)
621 self._checkextensions()
631 self._checkextensions()
622 self._bind(sock)
632 self._bind(sock)
623 self._createsymlink()
633 self._createsymlink()
624 # no "listening at" message should be printed to simulate hg behavior
634 # no "listening at" message should be printed to simulate hg behavior
625
635
626 def _inithashstate(self, address):
636 def _inithashstate(self, address):
627 self._baseaddress = address
637 self._baseaddress = address
628 if self.ui.configbool(b'chgserver', b'skiphash'):
638 if self.ui.configbool(b'chgserver', b'skiphash'):
629 self._hashstate = None
639 self._hashstate = None
630 self._realaddress = address
640 self._realaddress = address
631 return
641 return
632 self._hashstate = hashstate.fromui(self.ui)
642 self._hashstate = hashstate.fromui(self.ui)
633 self._realaddress = _hashaddress(address, self._hashstate.confighash)
643 self._realaddress = _hashaddress(address, self._hashstate.confighash)
634
644
635 def _checkextensions(self):
645 def _checkextensions(self):
636 if not self._hashstate:
646 if not self._hashstate:
637 return
647 return
638 if extensions.notloaded():
648 if extensions.notloaded():
639 # one or more extensions failed to load. mtimehash becomes
649 # one or more extensions failed to load. mtimehash becomes
640 # meaningless because we do not know the paths of those extensions.
650 # meaningless because we do not know the paths of those extensions.
641 # set mtimehash to an illegal hash value to invalidate the server.
651 # set mtimehash to an illegal hash value to invalidate the server.
642 self._hashstate.mtimehash = b''
652 self._hashstate.mtimehash = b''
643
653
644 def _bind(self, sock):
654 def _bind(self, sock):
645 # use a unique temp address so we can stat the file and do ownership
655 # use a unique temp address so we can stat the file and do ownership
646 # check later
656 # check later
647 tempaddress = _tempaddress(self._realaddress)
657 tempaddress = _tempaddress(self._realaddress)
648 util.bindunixsocket(sock, tempaddress)
658 util.bindunixsocket(sock, tempaddress)
649 self._socketstat = os.stat(tempaddress)
659 self._socketstat = os.stat(tempaddress)
650 sock.listen(socket.SOMAXCONN)
660 sock.listen(socket.SOMAXCONN)
651 # rename will replace the old socket file if exists atomically. the
661 # rename will replace the old socket file if exists atomically. the
652 # old server will detect ownership change and exit.
662 # old server will detect ownership change and exit.
653 util.rename(tempaddress, self._realaddress)
663 util.rename(tempaddress, self._realaddress)
654
664
655 def _createsymlink(self):
665 def _createsymlink(self):
656 if self._baseaddress == self._realaddress:
666 if self._baseaddress == self._realaddress:
657 return
667 return
658 tempaddress = _tempaddress(self._baseaddress)
668 tempaddress = _tempaddress(self._baseaddress)
659 os.symlink(os.path.basename(self._realaddress), tempaddress)
669 os.symlink(os.path.basename(self._realaddress), tempaddress)
660 util.rename(tempaddress, self._baseaddress)
670 util.rename(tempaddress, self._baseaddress)
661
671
662 def _issocketowner(self):
672 def _issocketowner(self):
663 try:
673 try:
664 st = os.stat(self._realaddress)
674 st = os.stat(self._realaddress)
665 return (
675 return (
666 st.st_ino == self._socketstat.st_ino
676 st.st_ino == self._socketstat.st_ino
667 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
677 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
668 )
678 )
669 except OSError:
679 except OSError:
670 return False
680 return False
671
681
672 def unlinksocket(self, address):
682 def unlinksocket(self, address):
673 if not self._issocketowner():
683 if not self._issocketowner():
674 return
684 return
675 # it is possible to have a race condition here that we may
685 # it is possible to have a race condition here that we may
676 # remove another server's socket file. but that's okay
686 # remove another server's socket file. but that's okay
677 # since that server will detect and exit automatically and
687 # since that server will detect and exit automatically and
678 # the client will start a new server on demand.
688 # the client will start a new server on demand.
679 util.tryunlink(self._realaddress)
689 util.tryunlink(self._realaddress)
680
690
681 def shouldexit(self):
691 def shouldexit(self):
682 if not self._issocketowner():
692 if not self._issocketowner():
683 self.ui.log(
693 self.ui.log(
684 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
694 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
685 )
695 )
686 return True
696 return True
687 if time.time() - self._lastactive > self._idletimeout:
697 if time.time() - self._lastactive > self._idletimeout:
688 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
698 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
689 return True
699 return True
690 return False
700 return False
691
701
692 def newconnection(self):
702 def newconnection(self):
693 self._lastactive = time.time()
703 self._lastactive = time.time()
694
704
695 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
705 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
696 return chgcmdserver(
706 return chgcmdserver(
697 self.ui,
707 self.ui,
698 repo,
708 repo,
699 fin,
709 fin,
700 fout,
710 fout,
701 conn,
711 conn,
702 prereposetups,
712 prereposetups,
703 self._hashstate,
713 self._hashstate,
704 self._baseaddress,
714 self._baseaddress,
705 )
715 )
706
716
707
717
708 def chgunixservice(ui, repo, opts):
718 def chgunixservice(ui, repo, opts):
709 # CHGINTERNALMARK is set by chg client. It is an indication of things are
719 # CHGINTERNALMARK is set by chg client. It is an indication of things are
710 # started by chg so other code can do things accordingly, like disabling
720 # started by chg so other code can do things accordingly, like disabling
711 # demandimport or detecting chg client started by chg client. When executed
721 # demandimport or detecting chg client started by chg client. When executed
712 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
722 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
713 # environ cleaner.
723 # environ cleaner.
714 if b'CHGINTERNALMARK' in encoding.environ:
724 if b'CHGINTERNALMARK' in encoding.environ:
715 del encoding.environ[b'CHGINTERNALMARK']
725 del encoding.environ[b'CHGINTERNALMARK']
716 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
726 # Python3.7+ "coerces" the LC_CTYPE environment variable to a UTF-8 one if
717 # it thinks the current value is "C". This breaks the hash computation and
727 # it thinks the current value is "C". This breaks the hash computation and
718 # causes chg to restart loop.
728 # causes chg to restart loop.
719 if b'CHGORIG_LC_CTYPE' in encoding.environ:
729 if b'CHGORIG_LC_CTYPE' in encoding.environ:
720 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
730 encoding.environ[b'LC_CTYPE'] = encoding.environ[b'CHGORIG_LC_CTYPE']
721 del encoding.environ[b'CHGORIG_LC_CTYPE']
731 del encoding.environ[b'CHGORIG_LC_CTYPE']
722 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
732 elif b'CHG_CLEAR_LC_CTYPE' in encoding.environ:
723 if b'LC_CTYPE' in encoding.environ:
733 if b'LC_CTYPE' in encoding.environ:
724 del encoding.environ[b'LC_CTYPE']
734 del encoding.environ[b'LC_CTYPE']
725 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
735 del encoding.environ[b'CHG_CLEAR_LC_CTYPE']
726
736
727 if repo:
737 if repo:
728 # one chgserver can serve multiple repos. drop repo information
738 # one chgserver can serve multiple repos. drop repo information
729 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
739 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
730 h = chgunixservicehandler(ui)
740 h = chgunixservicehandler(ui)
731 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
741 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,418 +1,461 b''
1 #require chg
1 #require chg
2
2
3 $ mkdir log
3 $ mkdir log
4 $ cp $HGRCPATH $HGRCPATH.unconfigured
4 $ cp $HGRCPATH $HGRCPATH.unconfigured
5 $ cat <<'EOF' >> $HGRCPATH
5 $ cat <<'EOF' >> $HGRCPATH
6 > [cmdserver]
6 > [cmdserver]
7 > log = $TESTTMP/log/server.log
7 > log = $TESTTMP/log/server.log
8 > max-log-files = 1
8 > max-log-files = 1
9 > max-log-size = 10 kB
9 > max-log-size = 10 kB
10 > EOF
10 > EOF
11 $ cp $HGRCPATH $HGRCPATH.orig
11 $ cp $HGRCPATH $HGRCPATH.orig
12
12
13 $ filterlog () {
13 $ filterlog () {
14 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
14 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!' \
15 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
15 > -e 's!\(setprocname\|received fds\|setenv\): .*!\1: ...!' \
16 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
16 > -e 's!\(confighash\|mtimehash\) = [0-9a-f]*!\1 = ...!g' \
17 > -e 's!\(in \)[0-9.]*s\b!\1 ...s!g' \
17 > -e 's!\(in \)[0-9.]*s\b!\1 ...s!g' \
18 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
18 > -e 's!\(pid\)=[0-9]*!\1=...!g' \
19 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
19 > -e 's!\(/server-\)[0-9a-f]*!\1...!g'
20 > }
20 > }
21
21
22 init repo
22 init repo
23
23
24 $ chg init foo
24 $ chg init foo
25 $ cd foo
25 $ cd foo
26
26
27 ill-formed config
27 ill-formed config
28
28
29 $ chg status
29 $ chg status
30 $ echo '=brokenconfig' >> $HGRCPATH
30 $ echo '=brokenconfig' >> $HGRCPATH
31 $ chg status
31 $ chg status
32 hg: parse error at * (glob)
32 hg: parse error at * (glob)
33 [255]
33 [255]
34
34
35 $ cp $HGRCPATH.orig $HGRCPATH
35 $ cp $HGRCPATH.orig $HGRCPATH
36
36
37 long socket path
37 long socket path
38
38
39 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
39 $ sockpath=$TESTTMP/this/path/should/be/longer/than/one-hundred-and-seven/characters/where/107/is/the/typical/size/limit/of/unix-domain-socket
40 $ mkdir -p $sockpath
40 $ mkdir -p $sockpath
41 $ bakchgsockname=$CHGSOCKNAME
41 $ bakchgsockname=$CHGSOCKNAME
42 $ CHGSOCKNAME=$sockpath/server
42 $ CHGSOCKNAME=$sockpath/server
43 $ export CHGSOCKNAME
43 $ export CHGSOCKNAME
44 $ chg root
44 $ chg root
45 $TESTTMP/foo
45 $TESTTMP/foo
46 $ rm -rf $sockpath
46 $ rm -rf $sockpath
47 $ CHGSOCKNAME=$bakchgsockname
47 $ CHGSOCKNAME=$bakchgsockname
48 $ export CHGSOCKNAME
48 $ export CHGSOCKNAME
49
49
50 $ cd ..
50 $ cd ..
51
51
52 editor
52 editor
53 ------
53 ------
54
54
55 $ cat >> pushbuffer.py <<EOF
55 $ cat >> pushbuffer.py <<EOF
56 > def reposetup(ui, repo):
56 > def reposetup(ui, repo):
57 > repo.ui.pushbuffer(subproc=True)
57 > repo.ui.pushbuffer(subproc=True)
58 > EOF
58 > EOF
59
59
60 $ chg init editor
60 $ chg init editor
61 $ cd editor
61 $ cd editor
62
62
63 by default, system() should be redirected to the client:
63 by default, system() should be redirected to the client:
64
64
65 $ touch foo
65 $ touch foo
66 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
66 $ CHGDEBUG= HGEDITOR=cat chg ci -Am channeled --edit 2>&1 \
67 > | egrep "HG:|run 'cat"
67 > | egrep "HG:|run 'cat"
68 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
68 chg: debug: * run 'cat "*"' at '$TESTTMP/editor' (glob)
69 HG: Enter commit message. Lines beginning with 'HG:' are removed.
69 HG: Enter commit message. Lines beginning with 'HG:' are removed.
70 HG: Leave message empty to abort commit.
70 HG: Leave message empty to abort commit.
71 HG: --
71 HG: --
72 HG: user: test
72 HG: user: test
73 HG: branch 'default'
73 HG: branch 'default'
74 HG: added foo
74 HG: added foo
75
75
76 but no redirection should be made if output is captured:
76 but no redirection should be made if output is captured:
77
77
78 $ touch bar
78 $ touch bar
79 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
79 $ CHGDEBUG= HGEDITOR=cat chg ci -Am bufferred --edit \
80 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
80 > --config extensions.pushbuffer="$TESTTMP/pushbuffer.py" 2>&1 \
81 > | egrep "HG:|run 'cat"
81 > | egrep "HG:|run 'cat"
82 [1]
82 [1]
83
83
84 check that commit commands succeeded:
84 check that commit commands succeeded:
85
85
86 $ hg log -T '{rev}:{desc}\n'
86 $ hg log -T '{rev}:{desc}\n'
87 1:bufferred
87 1:bufferred
88 0:channeled
88 0:channeled
89
89
90 $ cd ..
90 $ cd ..
91
91
92 pager
92 pager
93 -----
93 -----
94
94
95 $ cat >> fakepager.py <<EOF
95 $ cat >> fakepager.py <<EOF
96 > import sys
96 > import sys
97 > for line in sys.stdin:
97 > for line in sys.stdin:
98 > sys.stdout.write('paged! %r\n' % line)
98 > sys.stdout.write('paged! %r\n' % line)
99 > EOF
99 > EOF
100
100
101 enable pager extension globally, but spawns the master server with no tty:
101 enable pager extension globally, but spawns the master server with no tty:
102
102
103 $ chg init pager
103 $ chg init pager
104 $ cd pager
104 $ cd pager
105 $ cat >> $HGRCPATH <<EOF
105 $ cat >> $HGRCPATH <<EOF
106 > [extensions]
106 > [extensions]
107 > pager =
107 > pager =
108 > [pager]
108 > [pager]
109 > pager = "$PYTHON" $TESTTMP/fakepager.py
109 > pager = "$PYTHON" $TESTTMP/fakepager.py
110 > EOF
110 > EOF
111 $ chg version > /dev/null
111 $ chg version > /dev/null
112 $ touch foo
112 $ touch foo
113 $ chg ci -qAm foo
113 $ chg ci -qAm foo
114
114
115 pager should be enabled if the attached client has a tty:
115 pager should be enabled if the attached client has a tty:
116
116
117 $ chg log -l1 -q --config ui.formatted=True
117 $ chg log -l1 -q --config ui.formatted=True
118 paged! '0:1f7b0de80e11\n'
118 paged! '0:1f7b0de80e11\n'
119 $ chg log -l1 -q --config ui.formatted=False
119 $ chg log -l1 -q --config ui.formatted=False
120 0:1f7b0de80e11
120 0:1f7b0de80e11
121
121
122 chg waits for pager if runcommand raises
122 chg waits for pager if runcommand raises
123
123
124 $ cat > $TESTTMP/crash.py <<EOF
124 $ cat > $TESTTMP/crash.py <<EOF
125 > from mercurial import registrar
125 > from mercurial import registrar
126 > cmdtable = {}
126 > cmdtable = {}
127 > command = registrar.command(cmdtable)
127 > command = registrar.command(cmdtable)
128 > @command(b'crash')
128 > @command(b'crash')
129 > def pagercrash(ui, repo, *pats, **opts):
129 > def pagercrash(ui, repo, *pats, **opts):
130 > ui.write(b'going to crash\n')
130 > ui.write(b'going to crash\n')
131 > raise Exception('.')
131 > raise Exception('.')
132 > EOF
132 > EOF
133
133
134 $ cat > $TESTTMP/fakepager.py <<EOF
134 $ cat > $TESTTMP/fakepager.py <<EOF
135 > from __future__ import absolute_import
135 > from __future__ import absolute_import
136 > import sys
136 > import sys
137 > import time
137 > import time
138 > for line in iter(sys.stdin.readline, ''):
138 > for line in iter(sys.stdin.readline, ''):
139 > if 'crash' in line: # only interested in lines containing 'crash'
139 > if 'crash' in line: # only interested in lines containing 'crash'
140 > # if chg exits when pager is sleeping (incorrectly), the output
140 > # if chg exits when pager is sleeping (incorrectly), the output
141 > # will be captured by the next test case
141 > # will be captured by the next test case
142 > time.sleep(1)
142 > time.sleep(1)
143 > sys.stdout.write('crash-pager: %s' % line)
143 > sys.stdout.write('crash-pager: %s' % line)
144 > EOF
144 > EOF
145
145
146 $ cat >> .hg/hgrc <<EOF
146 $ cat >> .hg/hgrc <<EOF
147 > [extensions]
147 > [extensions]
148 > crash = $TESTTMP/crash.py
148 > crash = $TESTTMP/crash.py
149 > EOF
149 > EOF
150
150
151 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
151 $ chg crash --pager=on --config ui.formatted=True 2>/dev/null
152 crash-pager: going to crash
152 crash-pager: going to crash
153 [255]
153 [255]
154
154
155 no stdout data should be printed after pager quits, and the buffered data
156 should never persist (issue6207)
157
158 "killed!" may be printed if terminated by SIGPIPE, which isn't important
159 in this test.
160
161 $ cat > $TESTTMP/bulkwrite.py <<'EOF'
162 > import time
163 > from mercurial import error, registrar
164 > cmdtable = {}
165 > command = registrar.command(cmdtable)
166 > @command(b'bulkwrite')
167 > def bulkwrite(ui, repo, *pats, **opts):
168 > ui.write(b'going to write massive data\n')
169 > ui.flush()
170 > t = time.time()
171 > while time.time() - t < 2:
172 > ui.write(b'x' * 1023 + b'\n') # will be interrupted by SIGPIPE
173 > raise error.Abort(b"write() doesn't block")
174 > EOF
175
176 $ cat > $TESTTMP/fakepager.py <<'EOF'
177 > import sys
178 > import time
179 > sys.stdout.write('paged! %r\n' % sys.stdin.readline())
180 > time.sleep(1) # new data will be written
181 > EOF
182
183 $ cat >> .hg/hgrc <<EOF
184 > [extensions]
185 > bulkwrite = $TESTTMP/bulkwrite.py
186 > EOF
187
188 $ chg bulkwrite --pager=on --color no --config ui.formatted=True
189 paged! 'going to write massive data\n'
190 killed! (?)
191 [255]
192
193 $ chg bulkwrite --pager=on --color no --config ui.formatted=True
194 paged! 'going to write massive data\n'
195 killed! (?)
196 [255]
197
155 $ cd ..
198 $ cd ..
156
199
157 server lifecycle
200 server lifecycle
158 ----------------
201 ----------------
159
202
160 chg server should be restarted on code change, and old server will shut down
203 chg server should be restarted on code change, and old server will shut down
161 automatically. In this test, we use the following time parameters:
204 automatically. In this test, we use the following time parameters:
162
205
163 - "sleep 1" to make mtime different
206 - "sleep 1" to make mtime different
164 - "sleep 2" to notice mtime change (polling interval is 1 sec)
207 - "sleep 2" to notice mtime change (polling interval is 1 sec)
165
208
166 set up repository with an extension:
209 set up repository with an extension:
167
210
168 $ chg init extreload
211 $ chg init extreload
169 $ cd extreload
212 $ cd extreload
170 $ touch dummyext.py
213 $ touch dummyext.py
171 $ cat <<EOF >> .hg/hgrc
214 $ cat <<EOF >> .hg/hgrc
172 > [extensions]
215 > [extensions]
173 > dummyext = dummyext.py
216 > dummyext = dummyext.py
174 > EOF
217 > EOF
175
218
176 isolate socket directory for stable result:
219 isolate socket directory for stable result:
177
220
178 $ OLDCHGSOCKNAME=$CHGSOCKNAME
221 $ OLDCHGSOCKNAME=$CHGSOCKNAME
179 $ mkdir chgsock
222 $ mkdir chgsock
180 $ CHGSOCKNAME=`pwd`/chgsock/server
223 $ CHGSOCKNAME=`pwd`/chgsock/server
181
224
182 warm up server:
225 warm up server:
183
226
184 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
227 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
185 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
228 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
186
229
187 new server should be started if extension modified:
230 new server should be started if extension modified:
188
231
189 $ sleep 1
232 $ sleep 1
190 $ touch dummyext.py
233 $ touch dummyext.py
191 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
234 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
192 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
235 chg: debug: * instruction: unlink $TESTTMP/extreload/chgsock/server-* (glob)
193 chg: debug: * instruction: reconnect (glob)
236 chg: debug: * instruction: reconnect (glob)
194 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
237 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
195
238
196 old server will shut down, while new server should still be reachable:
239 old server will shut down, while new server should still be reachable:
197
240
198 $ sleep 2
241 $ sleep 2
199 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
242 $ CHGDEBUG= chg log 2>&1 | (egrep 'instruction|start' || true)
200
243
201 socket file should never be unlinked by old server:
244 socket file should never be unlinked by old server:
202 (simulates unowned socket by updating mtime, which makes sure server exits
245 (simulates unowned socket by updating mtime, which makes sure server exits
203 at polling cycle)
246 at polling cycle)
204
247
205 $ ls chgsock/server-*
248 $ ls chgsock/server-*
206 chgsock/server-* (glob)
249 chgsock/server-* (glob)
207 $ touch chgsock/server-*
250 $ touch chgsock/server-*
208 $ sleep 2
251 $ sleep 2
209 $ ls chgsock/server-*
252 $ ls chgsock/server-*
210 chgsock/server-* (glob)
253 chgsock/server-* (glob)
211
254
212 since no server is reachable from socket file, new server should be started:
255 since no server is reachable from socket file, new server should be started:
213 (this test makes sure that old server shut down automatically)
256 (this test makes sure that old server shut down automatically)
214
257
215 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
258 $ CHGDEBUG= chg log 2>&1 | egrep 'instruction|start'
216 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
259 chg: debug: * start cmdserver at $TESTTMP/extreload/chgsock/server.* (glob)
217
260
218 shut down servers and restore environment:
261 shut down servers and restore environment:
219
262
220 $ rm -R chgsock
263 $ rm -R chgsock
221 $ sleep 2
264 $ sleep 2
222 $ CHGSOCKNAME=$OLDCHGSOCKNAME
265 $ CHGSOCKNAME=$OLDCHGSOCKNAME
223 $ cd ..
266 $ cd ..
224
267
225 check that server events are recorded:
268 check that server events are recorded:
226
269
227 $ ls log
270 $ ls log
228 server.log
271 server.log
229 server.log.1
272 server.log.1
230
273
231 print only the last 10 lines, since we aren't sure how many records are
274 print only the last 10 lines, since we aren't sure how many records are
232 preserved (since setprocname isn't available on py3 and pure version,
275 preserved (since setprocname isn't available on py3 and pure version,
233 the 10th-most-recent line is different when using py3):
276 the 10th-most-recent line is different when using py3):
234
277
235 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
278 $ cat log/server.log.1 log/server.log | tail -10 | filterlog
236 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ... (no-setprocname !)
279 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ... (no-setprocname !)
237 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
280 YYYY/MM/DD HH:MM:SS (PID)> forked worker process (pid=...)
238 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ... (setprocname !)
281 YYYY/MM/DD HH:MM:SS (PID)> setprocname: ... (setprocname !)
239 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
282 YYYY/MM/DD HH:MM:SS (PID)> received fds: ...
240 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
283 YYYY/MM/DD HH:MM:SS (PID)> chdir to '$TESTTMP/extreload'
241 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
284 YYYY/MM/DD HH:MM:SS (PID)> setumask 18
242 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
285 YYYY/MM/DD HH:MM:SS (PID)> setenv: ...
243 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
286 YYYY/MM/DD HH:MM:SS (PID)> confighash = ... mtimehash = ...
244 YYYY/MM/DD HH:MM:SS (PID)> validate: []
287 YYYY/MM/DD HH:MM:SS (PID)> validate: []
245 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
288 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
246 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
289 YYYY/MM/DD HH:MM:SS (PID)> $TESTTMP/extreload/chgsock/server-... is not owned, exiting.
247
290
248 global data mutated by schems
291 global data mutated by schems
249 -----------------------------
292 -----------------------------
250
293
251 $ hg init schemes
294 $ hg init schemes
252 $ cd schemes
295 $ cd schemes
253
296
254 initial state
297 initial state
255
298
256 $ cat > .hg/hgrc <<'EOF'
299 $ cat > .hg/hgrc <<'EOF'
257 > [extensions]
300 > [extensions]
258 > schemes =
301 > schemes =
259 > [schemes]
302 > [schemes]
260 > foo = https://foo.example.org/
303 > foo = https://foo.example.org/
261 > EOF
304 > EOF
262 $ hg debugexpandscheme foo://expanded
305 $ hg debugexpandscheme foo://expanded
263 https://foo.example.org/expanded
306 https://foo.example.org/expanded
264 $ hg debugexpandscheme bar://unexpanded
307 $ hg debugexpandscheme bar://unexpanded
265 bar://unexpanded
308 bar://unexpanded
266
309
267 add bar
310 add bar
268
311
269 $ cat > .hg/hgrc <<'EOF'
312 $ cat > .hg/hgrc <<'EOF'
270 > [extensions]
313 > [extensions]
271 > schemes =
314 > schemes =
272 > [schemes]
315 > [schemes]
273 > foo = https://foo.example.org/
316 > foo = https://foo.example.org/
274 > bar = https://bar.example.org/
317 > bar = https://bar.example.org/
275 > EOF
318 > EOF
276 $ hg debugexpandscheme foo://expanded
319 $ hg debugexpandscheme foo://expanded
277 https://foo.example.org/expanded
320 https://foo.example.org/expanded
278 $ hg debugexpandscheme bar://expanded
321 $ hg debugexpandscheme bar://expanded
279 https://bar.example.org/expanded
322 https://bar.example.org/expanded
280
323
281 remove foo
324 remove foo
282
325
283 $ cat > .hg/hgrc <<'EOF'
326 $ cat > .hg/hgrc <<'EOF'
284 > [extensions]
327 > [extensions]
285 > schemes =
328 > schemes =
286 > [schemes]
329 > [schemes]
287 > bar = https://bar.example.org/
330 > bar = https://bar.example.org/
288 > EOF
331 > EOF
289 $ hg debugexpandscheme foo://unexpanded
332 $ hg debugexpandscheme foo://unexpanded
290 foo://unexpanded
333 foo://unexpanded
291 $ hg debugexpandscheme bar://expanded
334 $ hg debugexpandscheme bar://expanded
292 https://bar.example.org/expanded
335 https://bar.example.org/expanded
293
336
294 $ cd ..
337 $ cd ..
295
338
296 repository cache
339 repository cache
297 ----------------
340 ----------------
298
341
299 $ rm log/server.log*
342 $ rm log/server.log*
300 $ cp $HGRCPATH.unconfigured $HGRCPATH
343 $ cp $HGRCPATH.unconfigured $HGRCPATH
301 $ cat <<'EOF' >> $HGRCPATH
344 $ cat <<'EOF' >> $HGRCPATH
302 > [cmdserver]
345 > [cmdserver]
303 > log = $TESTTMP/log/server.log
346 > log = $TESTTMP/log/server.log
304 > max-repo-cache = 1
347 > max-repo-cache = 1
305 > track-log = command, repocache
348 > track-log = command, repocache
306 > EOF
349 > EOF
307
350
308 isolate socket directory for stable result:
351 isolate socket directory for stable result:
309
352
310 $ OLDCHGSOCKNAME=$CHGSOCKNAME
353 $ OLDCHGSOCKNAME=$CHGSOCKNAME
311 $ mkdir chgsock
354 $ mkdir chgsock
312 $ CHGSOCKNAME=`pwd`/chgsock/server
355 $ CHGSOCKNAME=`pwd`/chgsock/server
313
356
314 create empty repo and cache it:
357 create empty repo and cache it:
315
358
316 $ hg init cached
359 $ hg init cached
317 $ hg id -R cached
360 $ hg id -R cached
318 000000000000 tip
361 000000000000 tip
319 $ sleep 1
362 $ sleep 1
320
363
321 modify repo (and cache will be invalidated):
364 modify repo (and cache will be invalidated):
322
365
323 $ touch cached/a
366 $ touch cached/a
324 $ hg ci -R cached -Am 'add a'
367 $ hg ci -R cached -Am 'add a'
325 adding a
368 adding a
326 $ sleep 1
369 $ sleep 1
327
370
328 read cached repo:
371 read cached repo:
329
372
330 $ hg log -R cached
373 $ hg log -R cached
331 changeset: 0:ac82d8b1f7c4
374 changeset: 0:ac82d8b1f7c4
332 tag: tip
375 tag: tip
333 user: test
376 user: test
334 date: Thu Jan 01 00:00:00 1970 +0000
377 date: Thu Jan 01 00:00:00 1970 +0000
335 summary: add a
378 summary: add a
336
379
337 $ sleep 1
380 $ sleep 1
338
381
339 discard cached from LRU cache:
382 discard cached from LRU cache:
340
383
341 $ hg clone cached cached2
384 $ hg clone cached cached2
342 updating to branch default
385 updating to branch default
343 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
386 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
344 $ hg id -R cached2
387 $ hg id -R cached2
345 ac82d8b1f7c4 tip
388 ac82d8b1f7c4 tip
346 $ sleep 1
389 $ sleep 1
347
390
348 read uncached repo:
391 read uncached repo:
349
392
350 $ hg log -R cached
393 $ hg log -R cached
351 changeset: 0:ac82d8b1f7c4
394 changeset: 0:ac82d8b1f7c4
352 tag: tip
395 tag: tip
353 user: test
396 user: test
354 date: Thu Jan 01 00:00:00 1970 +0000
397 date: Thu Jan 01 00:00:00 1970 +0000
355 summary: add a
398 summary: add a
356
399
357 $ sleep 1
400 $ sleep 1
358
401
359 shut down servers and restore environment:
402 shut down servers and restore environment:
360
403
361 $ rm -R chgsock
404 $ rm -R chgsock
362 $ sleep 2
405 $ sleep 2
363 $ CHGSOCKNAME=$OLDCHGSOCKNAME
406 $ CHGSOCKNAME=$OLDCHGSOCKNAME
364
407
365 check server log:
408 check server log:
366
409
367 $ cat log/server.log | filterlog
410 $ cat log/server.log | filterlog
368 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
411 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
369 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
412 YYYY/MM/DD HH:MM:SS (PID)> worker process exited (pid=...)
370 YYYY/MM/DD HH:MM:SS (PID)> init cached
413 YYYY/MM/DD HH:MM:SS (PID)> init cached
371 YYYY/MM/DD HH:MM:SS (PID)> id -R cached
414 YYYY/MM/DD HH:MM:SS (PID)> id -R cached
372 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
415 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
373 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
416 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
374 YYYY/MM/DD HH:MM:SS (PID)> ci -R cached -Am 'add a'
417 YYYY/MM/DD HH:MM:SS (PID)> ci -R cached -Am 'add a'
375 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
418 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
376 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
419 YYYY/MM/DD HH:MM:SS (PID)> repo from cache: $TESTTMP/cached
377 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
420 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
378 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
421 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
379 YYYY/MM/DD HH:MM:SS (PID)> clone cached cached2
422 YYYY/MM/DD HH:MM:SS (PID)> clone cached cached2
380 YYYY/MM/DD HH:MM:SS (PID)> id -R cached2
423 YYYY/MM/DD HH:MM:SS (PID)> id -R cached2
381 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached2 (in ...s)
424 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached2 (in ...s)
382 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
425 YYYY/MM/DD HH:MM:SS (PID)> log -R cached
383 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
426 YYYY/MM/DD HH:MM:SS (PID)> loaded repo into cache: $TESTTMP/cached (in ...s)
384
427
385 Test that chg works (sets to the user's actual LC_CTYPE) even when python
428 Test that chg works (sets to the user's actual LC_CTYPE) even when python
386 "coerces" the locale (py3.7+)
429 "coerces" the locale (py3.7+)
387
430
388 $ cat > $TESTTMP/debugenv.py <<EOF
431 $ cat > $TESTTMP/debugenv.py <<EOF
389 > from mercurial import encoding
432 > from mercurial import encoding
390 > from mercurial import registrar
433 > from mercurial import registrar
391 > cmdtable = {}
434 > cmdtable = {}
392 > command = registrar.command(cmdtable)
435 > command = registrar.command(cmdtable)
393 > @command(b'debugenv', [], b'', norepo=True)
436 > @command(b'debugenv', [], b'', norepo=True)
394 > def debugenv(ui):
437 > def debugenv(ui):
395 > for k in [b'LC_ALL', b'LC_CTYPE', b'LANG']:
438 > for k in [b'LC_ALL', b'LC_CTYPE', b'LANG']:
396 > v = encoding.environ.get(k)
439 > v = encoding.environ.get(k)
397 > if v is not None:
440 > if v is not None:
398 > ui.write(b'%s=%s\n' % (k, encoding.environ[k]))
441 > ui.write(b'%s=%s\n' % (k, encoding.environ[k]))
399 > EOF
442 > EOF
400 (hg keeps python's modified LC_CTYPE, chg doesn't)
443 (hg keeps python's modified LC_CTYPE, chg doesn't)
401 $ (unset LC_ALL; unset LANG; LC_CTYPE= "$CHGHG" \
444 $ (unset LC_ALL; unset LANG; LC_CTYPE= "$CHGHG" \
402 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
445 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
403 LC_CTYPE=C.UTF-8 (py37 !)
446 LC_CTYPE=C.UTF-8 (py37 !)
404 LC_CTYPE= (no-py37 !)
447 LC_CTYPE= (no-py37 !)
405 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
448 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
406 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
449 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
407 LC_CTYPE=
450 LC_CTYPE=
408 $ (unset LC_ALL; unset LANG; LC_CTYPE=unsupported_value chg \
451 $ (unset LC_ALL; unset LANG; LC_CTYPE=unsupported_value chg \
409 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
452 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
410 LC_CTYPE=unsupported_value
453 LC_CTYPE=unsupported_value
411 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
454 $ (unset LC_ALL; unset LANG; LC_CTYPE= chg \
412 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
455 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv)
413 LC_CTYPE=
456 LC_CTYPE=
414 $ LANG= LC_ALL= LC_CTYPE= chg \
457 $ LANG= LC_ALL= LC_CTYPE= chg \
415 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv
458 > --config extensions.debugenv=$TESTTMP/debugenv.py debugenv
416 LC_ALL=
459 LC_ALL=
417 LC_CTYPE=
460 LC_CTYPE=
418 LANG=
461 LANG=
General Comments 0
You need to be logged in to leave comments. Login now