##// END OF EJS Templates
commandserver: pass around option to hook repo instance creation...
Yuya Nishihara -
r40911:e7110f44 default
parent child Browse files
Show More
@@ -1,637 +1,638 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 hashlib
44 import hashlib
45 import inspect
45 import inspect
46 import os
46 import os
47 import re
47 import re
48 import socket
48 import socket
49 import stat
49 import stat
50 import struct
50 import struct
51 import time
51 import time
52
52
53 from .i18n import _
53 from .i18n import _
54
54
55 from . import (
55 from . import (
56 commandserver,
56 commandserver,
57 encoding,
57 encoding,
58 error,
58 error,
59 extensions,
59 extensions,
60 node,
60 node,
61 pycompat,
61 pycompat,
62 util,
62 util,
63 )
63 )
64
64
65 from .utils import (
65 from .utils import (
66 procutil,
66 procutil,
67 )
67 )
68
68
69 def _hashlist(items):
69 def _hashlist(items):
70 """return sha1 hexdigest for a list"""
70 """return sha1 hexdigest for a list"""
71 return node.hex(hashlib.sha1(str(items)).digest())
71 return node.hex(hashlib.sha1(str(items)).digest())
72
72
73 # sensitive config sections affecting confighash
73 # sensitive config sections affecting confighash
74 _configsections = [
74 _configsections = [
75 'alias', # affects global state commands.table
75 'alias', # affects global state commands.table
76 'eol', # uses setconfig('eol', ...)
76 'eol', # uses setconfig('eol', ...)
77 'extdiff', # uisetup will register new commands
77 'extdiff', # uisetup will register new commands
78 'extensions',
78 'extensions',
79 ]
79 ]
80
80
81 _configsectionitems = [
81 _configsectionitems = [
82 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
82 ('commands', 'show.aliasprefix'), # show.py reads it in extsetup
83 ]
83 ]
84
84
85 # sensitive environment variables affecting confighash
85 # sensitive environment variables affecting confighash
86 _envre = re.compile(r'''\A(?:
86 _envre = re.compile(r'''\A(?:
87 CHGHG
87 CHGHG
88 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
88 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
89 |HG(?:ENCODING|PLAIN).*
89 |HG(?:ENCODING|PLAIN).*
90 |LANG(?:UAGE)?
90 |LANG(?:UAGE)?
91 |LC_.*
91 |LC_.*
92 |LD_.*
92 |LD_.*
93 |PATH
93 |PATH
94 |PYTHON.*
94 |PYTHON.*
95 |TERM(?:INFO)?
95 |TERM(?:INFO)?
96 |TZ
96 |TZ
97 )\Z''', re.X)
97 )\Z''', re.X)
98
98
99 def _confighash(ui):
99 def _confighash(ui):
100 """return a quick hash for detecting config/env changes
100 """return a quick hash for detecting config/env changes
101
101
102 confighash is the hash of sensitive config items and environment variables.
102 confighash is the hash of sensitive config items and environment variables.
103
103
104 for chgserver, it is designed that once confighash changes, the server is
104 for chgserver, it is designed that once confighash changes, the server is
105 not qualified to serve its client and should redirect the client to a new
105 not qualified to serve its client and should redirect the client to a new
106 server. different from mtimehash, confighash change will not mark the
106 server. different from mtimehash, confighash change will not mark the
107 server outdated and exit since the user can have different configs at the
107 server outdated and exit since the user can have different configs at the
108 same time.
108 same time.
109 """
109 """
110 sectionitems = []
110 sectionitems = []
111 for section in _configsections:
111 for section in _configsections:
112 sectionitems.append(ui.configitems(section))
112 sectionitems.append(ui.configitems(section))
113 for section, item in _configsectionitems:
113 for section, item in _configsectionitems:
114 sectionitems.append(ui.config(section, item))
114 sectionitems.append(ui.config(section, item))
115 sectionhash = _hashlist(sectionitems)
115 sectionhash = _hashlist(sectionitems)
116 # If $CHGHG is set, the change to $HG should not trigger a new chg server
116 # If $CHGHG is set, the change to $HG should not trigger a new chg server
117 if 'CHGHG' in encoding.environ:
117 if 'CHGHG' in encoding.environ:
118 ignored = {'HG'}
118 ignored = {'HG'}
119 else:
119 else:
120 ignored = set()
120 ignored = set()
121 envitems = [(k, v) for k, v in encoding.environ.iteritems()
121 envitems = [(k, v) for k, v in encoding.environ.iteritems()
122 if _envre.match(k) and k not in ignored]
122 if _envre.match(k) and k not in ignored]
123 envhash = _hashlist(sorted(envitems))
123 envhash = _hashlist(sorted(envitems))
124 return sectionhash[:6] + envhash[:6]
124 return sectionhash[:6] + envhash[:6]
125
125
126 def _getmtimepaths(ui):
126 def _getmtimepaths(ui):
127 """get a list of paths that should be checked to detect change
127 """get a list of paths that should be checked to detect change
128
128
129 The list will include:
129 The list will include:
130 - extensions (will not cover all files for complex extensions)
130 - extensions (will not cover all files for complex extensions)
131 - mercurial/__version__.py
131 - mercurial/__version__.py
132 - python binary
132 - python binary
133 """
133 """
134 modules = [m for n, m in extensions.extensions(ui)]
134 modules = [m for n, m in extensions.extensions(ui)]
135 try:
135 try:
136 from . import __version__
136 from . import __version__
137 modules.append(__version__)
137 modules.append(__version__)
138 except ImportError:
138 except ImportError:
139 pass
139 pass
140 files = [pycompat.sysexecutable]
140 files = [pycompat.sysexecutable]
141 for m in modules:
141 for m in modules:
142 try:
142 try:
143 files.append(inspect.getabsfile(m))
143 files.append(inspect.getabsfile(m))
144 except TypeError:
144 except TypeError:
145 pass
145 pass
146 return sorted(set(files))
146 return sorted(set(files))
147
147
148 def _mtimehash(paths):
148 def _mtimehash(paths):
149 """return a quick hash for detecting file changes
149 """return a quick hash for detecting file changes
150
150
151 mtimehash calls stat on given paths and calculate a hash based on size and
151 mtimehash calls stat on given paths and calculate a hash based on size and
152 mtime of each file. mtimehash does not read file content because reading is
152 mtime of each file. mtimehash does not read file content because reading is
153 expensive. therefore it's not 100% reliable for detecting content changes.
153 expensive. therefore it's not 100% reliable for detecting content changes.
154 it's possible to return different hashes for same file contents.
154 it's possible to return different hashes for same file contents.
155 it's also possible to return a same hash for different file contents for
155 it's also possible to return a same hash for different file contents for
156 some carefully crafted situation.
156 some carefully crafted situation.
157
157
158 for chgserver, it is designed that once mtimehash changes, the server is
158 for chgserver, it is designed that once mtimehash changes, the server is
159 considered outdated immediately and should no longer provide service.
159 considered outdated immediately and should no longer provide service.
160
160
161 mtimehash is not included in confighash because we only know the paths of
161 mtimehash is not included in confighash because we only know the paths of
162 extensions after importing them (there is imp.find_module but that faces
162 extensions after importing them (there is imp.find_module but that faces
163 race conditions). We need to calculate confighash without importing.
163 race conditions). We need to calculate confighash without importing.
164 """
164 """
165 def trystat(path):
165 def trystat(path):
166 try:
166 try:
167 st = os.stat(path)
167 st = os.stat(path)
168 return (st[stat.ST_MTIME], st.st_size)
168 return (st[stat.ST_MTIME], st.st_size)
169 except OSError:
169 except OSError:
170 # could be ENOENT, EPERM etc. not fatal in any case
170 # could be ENOENT, EPERM etc. not fatal in any case
171 pass
171 pass
172 return _hashlist(map(trystat, paths))[:12]
172 return _hashlist(map(trystat, paths))[:12]
173
173
174 class hashstate(object):
174 class hashstate(object):
175 """a structure storing confighash, mtimehash, paths used for mtimehash"""
175 """a structure storing confighash, mtimehash, paths used for mtimehash"""
176 def __init__(self, confighash, mtimehash, mtimepaths):
176 def __init__(self, confighash, mtimehash, mtimepaths):
177 self.confighash = confighash
177 self.confighash = confighash
178 self.mtimehash = mtimehash
178 self.mtimehash = mtimehash
179 self.mtimepaths = mtimepaths
179 self.mtimepaths = mtimepaths
180
180
181 @staticmethod
181 @staticmethod
182 def fromui(ui, mtimepaths=None):
182 def fromui(ui, mtimepaths=None):
183 if mtimepaths is None:
183 if mtimepaths is None:
184 mtimepaths = _getmtimepaths(ui)
184 mtimepaths = _getmtimepaths(ui)
185 confighash = _confighash(ui)
185 confighash = _confighash(ui)
186 mtimehash = _mtimehash(mtimepaths)
186 mtimehash = _mtimehash(mtimepaths)
187 ui.log('cmdserver', 'confighash = %s mtimehash = %s\n',
187 ui.log('cmdserver', 'confighash = %s mtimehash = %s\n',
188 confighash, mtimehash)
188 confighash, mtimehash)
189 return hashstate(confighash, mtimehash, mtimepaths)
189 return hashstate(confighash, mtimehash, mtimepaths)
190
190
191 def _newchgui(srcui, csystem, attachio):
191 def _newchgui(srcui, csystem, attachio):
192 class chgui(srcui.__class__):
192 class chgui(srcui.__class__):
193 def __init__(self, src=None):
193 def __init__(self, src=None):
194 super(chgui, self).__init__(src)
194 super(chgui, self).__init__(src)
195 if src:
195 if src:
196 self._csystem = getattr(src, '_csystem', csystem)
196 self._csystem = getattr(src, '_csystem', csystem)
197 else:
197 else:
198 self._csystem = csystem
198 self._csystem = csystem
199
199
200 def _runsystem(self, cmd, environ, cwd, out):
200 def _runsystem(self, cmd, environ, cwd, out):
201 # fallback to the original system method if
201 # fallback to the original system method if
202 # a. the output stream is not stdout (e.g. stderr, cStringIO),
202 # a. the output stream is not stdout (e.g. stderr, cStringIO),
203 # b. or stdout is redirected by protectstdio(),
203 # b. or stdout is redirected by protectstdio(),
204 # because the chg client is not aware of these situations and
204 # because the chg client is not aware of these situations and
205 # will behave differently (i.e. write to stdout).
205 # will behave differently (i.e. write to stdout).
206 if (out is not self.fout
206 if (out is not self.fout
207 or not util.safehasattr(self.fout, 'fileno')
207 or not util.safehasattr(self.fout, 'fileno')
208 or self.fout.fileno() != procutil.stdout.fileno()
208 or self.fout.fileno() != procutil.stdout.fileno()
209 or self._finoutredirected):
209 or self._finoutredirected):
210 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
210 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
211 self.flush()
211 self.flush()
212 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
212 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
213
213
214 def _runpager(self, cmd, env=None):
214 def _runpager(self, cmd, env=None):
215 self._csystem(cmd, procutil.shellenviron(env), type='pager',
215 self._csystem(cmd, procutil.shellenviron(env), type='pager',
216 cmdtable={'attachio': attachio})
216 cmdtable={'attachio': attachio})
217 return True
217 return True
218
218
219 return chgui(srcui)
219 return chgui(srcui)
220
220
221 def _loadnewui(srcui, args, cdebug):
221 def _loadnewui(srcui, args, cdebug):
222 from . import dispatch # avoid cycle
222 from . import dispatch # avoid cycle
223
223
224 newui = srcui.__class__.load()
224 newui = srcui.__class__.load()
225 for a in ['fin', 'fout', 'ferr', 'environ']:
225 for a in ['fin', 'fout', 'ferr', 'environ']:
226 setattr(newui, a, getattr(srcui, a))
226 setattr(newui, a, getattr(srcui, a))
227 if util.safehasattr(srcui, '_csystem'):
227 if util.safehasattr(srcui, '_csystem'):
228 newui._csystem = srcui._csystem
228 newui._csystem = srcui._csystem
229
229
230 # command line args
230 # command line args
231 options = dispatch._earlyparseopts(newui, args)
231 options = dispatch._earlyparseopts(newui, args)
232 dispatch._parseconfig(newui, options['config'])
232 dispatch._parseconfig(newui, options['config'])
233
233
234 # stolen from tortoisehg.util.copydynamicconfig()
234 # stolen from tortoisehg.util.copydynamicconfig()
235 for section, name, value in srcui.walkconfig():
235 for section, name, value in srcui.walkconfig():
236 source = srcui.configsource(section, name)
236 source = srcui.configsource(section, name)
237 if ':' in source or source == '--config' or source.startswith('$'):
237 if ':' in source or source == '--config' or source.startswith('$'):
238 # path:line or command line, or environ
238 # path:line or command line, or environ
239 continue
239 continue
240 newui.setconfig(section, name, value, source)
240 newui.setconfig(section, name, value, source)
241
241
242 # load wd and repo config, copied from dispatch.py
242 # load wd and repo config, copied from dispatch.py
243 cwd = options['cwd']
243 cwd = options['cwd']
244 cwd = cwd and os.path.realpath(cwd) or None
244 cwd = cwd and os.path.realpath(cwd) or None
245 rpath = options['repository']
245 rpath = options['repository']
246 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
246 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
247
247
248 extensions.populateui(newui)
248 extensions.populateui(newui)
249 commandserver.setuplogging(newui, fp=cdebug)
249 commandserver.setuplogging(newui, fp=cdebug)
250 if newui is not newlui:
250 if newui is not newlui:
251 extensions.populateui(newlui)
251 extensions.populateui(newlui)
252 commandserver.setuplogging(newlui, fp=cdebug)
252 commandserver.setuplogging(newlui, fp=cdebug)
253
253
254 return (newui, newlui)
254 return (newui, newlui)
255
255
256 class channeledsystem(object):
256 class channeledsystem(object):
257 """Propagate ui.system() request in the following format:
257 """Propagate ui.system() request in the following format:
258
258
259 payload length (unsigned int),
259 payload length (unsigned int),
260 type, '\0',
260 type, '\0',
261 cmd, '\0',
261 cmd, '\0',
262 cwd, '\0',
262 cwd, '\0',
263 envkey, '=', val, '\0',
263 envkey, '=', val, '\0',
264 ...
264 ...
265 envkey, '=', val
265 envkey, '=', val
266
266
267 if type == 'system', waits for:
267 if type == 'system', waits for:
268
268
269 exitcode length (unsigned int),
269 exitcode length (unsigned int),
270 exitcode (int)
270 exitcode (int)
271
271
272 if type == 'pager', repetitively waits for a command name ending with '\n'
272 if type == 'pager', repetitively waits for a command name ending with '\n'
273 and executes it defined by cmdtable, or exits the loop if the command name
273 and executes it defined by cmdtable, or exits the loop if the command name
274 is empty.
274 is empty.
275 """
275 """
276 def __init__(self, in_, out, channel):
276 def __init__(self, in_, out, channel):
277 self.in_ = in_
277 self.in_ = in_
278 self.out = out
278 self.out = out
279 self.channel = channel
279 self.channel = channel
280
280
281 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
281 def __call__(self, cmd, environ, cwd=None, type='system', cmdtable=None):
282 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or '.')]
282 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or '.')]
283 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
283 args.extend('%s=%s' % (k, v) for k, v in environ.iteritems())
284 data = '\0'.join(args)
284 data = '\0'.join(args)
285 self.out.write(struct.pack('>cI', self.channel, len(data)))
285 self.out.write(struct.pack('>cI', self.channel, len(data)))
286 self.out.write(data)
286 self.out.write(data)
287 self.out.flush()
287 self.out.flush()
288
288
289 if type == 'system':
289 if type == 'system':
290 length = self.in_.read(4)
290 length = self.in_.read(4)
291 length, = struct.unpack('>I', length)
291 length, = struct.unpack('>I', length)
292 if length != 4:
292 if length != 4:
293 raise error.Abort(_('invalid response'))
293 raise error.Abort(_('invalid response'))
294 rc, = struct.unpack('>i', self.in_.read(4))
294 rc, = struct.unpack('>i', self.in_.read(4))
295 return rc
295 return rc
296 elif type == 'pager':
296 elif type == 'pager':
297 while True:
297 while True:
298 cmd = self.in_.readline()[:-1]
298 cmd = self.in_.readline()[:-1]
299 if not cmd:
299 if not cmd:
300 break
300 break
301 if cmdtable and cmd in cmdtable:
301 if cmdtable and cmd in cmdtable:
302 cmdtable[cmd]()
302 cmdtable[cmd]()
303 else:
303 else:
304 raise error.Abort(_('unexpected command: %s') % cmd)
304 raise error.Abort(_('unexpected command: %s') % cmd)
305 else:
305 else:
306 raise error.ProgrammingError('invalid S channel type: %s' % type)
306 raise error.ProgrammingError('invalid S channel type: %s' % type)
307
307
308 _iochannels = [
308 _iochannels = [
309 # server.ch, ui.fp, mode
309 # server.ch, ui.fp, mode
310 ('cin', 'fin', r'rb'),
310 ('cin', 'fin', r'rb'),
311 ('cout', 'fout', r'wb'),
311 ('cout', 'fout', r'wb'),
312 ('cerr', 'ferr', r'wb'),
312 ('cerr', 'ferr', r'wb'),
313 ]
313 ]
314
314
315 class chgcmdserver(commandserver.server):
315 class chgcmdserver(commandserver.server):
316 def __init__(self, ui, repo, fin, fout, sock, hashstate, baseaddress):
316 def __init__(self, ui, repo, fin, fout, sock, prereposetups,
317 hashstate, baseaddress):
317 super(chgcmdserver, self).__init__(
318 super(chgcmdserver, self).__init__(
318 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
319 _newchgui(ui, channeledsystem(fin, fout, 'S'), self.attachio),
319 repo, fin, fout)
320 repo, fin, fout, prereposetups)
320 self.clientsock = sock
321 self.clientsock = sock
321 self._ioattached = False
322 self._ioattached = False
322 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
323 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
323 self.hashstate = hashstate
324 self.hashstate = hashstate
324 self.baseaddress = baseaddress
325 self.baseaddress = baseaddress
325 if hashstate is not None:
326 if hashstate is not None:
326 self.capabilities = self.capabilities.copy()
327 self.capabilities = self.capabilities.copy()
327 self.capabilities['validate'] = chgcmdserver.validate
328 self.capabilities['validate'] = chgcmdserver.validate
328
329
329 def cleanup(self):
330 def cleanup(self):
330 super(chgcmdserver, self).cleanup()
331 super(chgcmdserver, self).cleanup()
331 # dispatch._runcatch() does not flush outputs if exception is not
332 # dispatch._runcatch() does not flush outputs if exception is not
332 # handled by dispatch._dispatch()
333 # handled by dispatch._dispatch()
333 self.ui.flush()
334 self.ui.flush()
334 self._restoreio()
335 self._restoreio()
335 self._ioattached = False
336 self._ioattached = False
336
337
337 def attachio(self):
338 def attachio(self):
338 """Attach to client's stdio passed via unix domain socket; all
339 """Attach to client's stdio passed via unix domain socket; all
339 channels except cresult will no longer be used
340 channels except cresult will no longer be used
340 """
341 """
341 # tell client to sendmsg() with 1-byte payload, which makes it
342 # tell client to sendmsg() with 1-byte payload, which makes it
342 # distinctive from "attachio\n" command consumed by client.read()
343 # distinctive from "attachio\n" command consumed by client.read()
343 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
344 self.clientsock.sendall(struct.pack('>cI', 'I', 1))
344 clientfds = util.recvfds(self.clientsock.fileno())
345 clientfds = util.recvfds(self.clientsock.fileno())
345 self.ui.log('chgserver', 'received fds: %r\n', clientfds)
346 self.ui.log('chgserver', 'received fds: %r\n', clientfds)
346
347
347 ui = self.ui
348 ui = self.ui
348 ui.flush()
349 ui.flush()
349 self._saveio()
350 self._saveio()
350 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
351 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
351 assert fd > 0
352 assert fd > 0
352 fp = getattr(ui, fn)
353 fp = getattr(ui, fn)
353 os.dup2(fd, fp.fileno())
354 os.dup2(fd, fp.fileno())
354 os.close(fd)
355 os.close(fd)
355 if self._ioattached:
356 if self._ioattached:
356 continue
357 continue
357 # reset buffering mode when client is first attached. as we want
358 # reset buffering mode when client is first attached. as we want
358 # to see output immediately on pager, the mode stays unchanged
359 # to see output immediately on pager, the mode stays unchanged
359 # when client re-attached. ferr is unchanged because it should
360 # when client re-attached. ferr is unchanged because it should
360 # be unbuffered no matter if it is a tty or not.
361 # be unbuffered no matter if it is a tty or not.
361 if fn == 'ferr':
362 if fn == 'ferr':
362 newfp = fp
363 newfp = fp
363 else:
364 else:
364 # make it line buffered explicitly because the default is
365 # make it line buffered explicitly because the default is
365 # decided on first write(), where fout could be a pager.
366 # decided on first write(), where fout could be a pager.
366 if fp.isatty():
367 if fp.isatty():
367 bufsize = 1 # line buffered
368 bufsize = 1 # line buffered
368 else:
369 else:
369 bufsize = -1 # system default
370 bufsize = -1 # system default
370 newfp = os.fdopen(fp.fileno(), mode, bufsize)
371 newfp = os.fdopen(fp.fileno(), mode, bufsize)
371 setattr(ui, fn, newfp)
372 setattr(ui, fn, newfp)
372 setattr(self, cn, newfp)
373 setattr(self, cn, newfp)
373
374
374 self._ioattached = True
375 self._ioattached = True
375 self.cresult.write(struct.pack('>i', len(clientfds)))
376 self.cresult.write(struct.pack('>i', len(clientfds)))
376
377
377 def _saveio(self):
378 def _saveio(self):
378 if self._oldios:
379 if self._oldios:
379 return
380 return
380 ui = self.ui
381 ui = self.ui
381 for cn, fn, _mode in _iochannels:
382 for cn, fn, _mode in _iochannels:
382 ch = getattr(self, cn)
383 ch = getattr(self, cn)
383 fp = getattr(ui, fn)
384 fp = getattr(ui, fn)
384 fd = os.dup(fp.fileno())
385 fd = os.dup(fp.fileno())
385 self._oldios.append((ch, fp, fd))
386 self._oldios.append((ch, fp, fd))
386
387
387 def _restoreio(self):
388 def _restoreio(self):
388 ui = self.ui
389 ui = self.ui
389 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
390 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
390 newfp = getattr(ui, fn)
391 newfp = getattr(ui, fn)
391 # close newfp while it's associated with client; otherwise it
392 # close newfp while it's associated with client; otherwise it
392 # would be closed when newfp is deleted
393 # would be closed when newfp is deleted
393 if newfp is not fp:
394 if newfp is not fp:
394 newfp.close()
395 newfp.close()
395 # restore original fd: fp is open again
396 # restore original fd: fp is open again
396 os.dup2(fd, fp.fileno())
397 os.dup2(fd, fp.fileno())
397 os.close(fd)
398 os.close(fd)
398 setattr(self, cn, ch)
399 setattr(self, cn, ch)
399 setattr(ui, fn, fp)
400 setattr(ui, fn, fp)
400 del self._oldios[:]
401 del self._oldios[:]
401
402
402 def validate(self):
403 def validate(self):
403 """Reload the config and check if the server is up to date
404 """Reload the config and check if the server is up to date
404
405
405 Read a list of '\0' separated arguments.
406 Read a list of '\0' separated arguments.
406 Write a non-empty list of '\0' separated instruction strings or '\0'
407 Write a non-empty list of '\0' separated instruction strings or '\0'
407 if the list is empty.
408 if the list is empty.
408 An instruction string could be either:
409 An instruction string could be either:
409 - "unlink $path", the client should unlink the path to stop the
410 - "unlink $path", the client should unlink the path to stop the
410 outdated server.
411 outdated server.
411 - "redirect $path", the client should attempt to connect to $path
412 - "redirect $path", the client should attempt to connect to $path
412 first. If it does not work, start a new server. It implies
413 first. If it does not work, start a new server. It implies
413 "reconnect".
414 "reconnect".
414 - "exit $n", the client should exit directly with code n.
415 - "exit $n", the client should exit directly with code n.
415 This may happen if we cannot parse the config.
416 This may happen if we cannot parse the config.
416 - "reconnect", the client should close the connection and
417 - "reconnect", the client should close the connection and
417 reconnect.
418 reconnect.
418 If neither "reconnect" nor "redirect" is included in the instruction
419 If neither "reconnect" nor "redirect" is included in the instruction
419 list, the client can continue with this server after completing all
420 list, the client can continue with this server after completing all
420 the instructions.
421 the instructions.
421 """
422 """
422 from . import dispatch # avoid cycle
423 from . import dispatch # avoid cycle
423
424
424 args = self._readlist()
425 args = self._readlist()
425 try:
426 try:
426 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
427 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
427 except error.ParseError as inst:
428 except error.ParseError as inst:
428 dispatch._formatparse(self.ui.warn, inst)
429 dispatch._formatparse(self.ui.warn, inst)
429 self.ui.flush()
430 self.ui.flush()
430 self.cresult.write('exit 255')
431 self.cresult.write('exit 255')
431 return
432 return
432 except error.Abort as inst:
433 except error.Abort as inst:
433 self.ui.error(_("abort: %s\n") % inst)
434 self.ui.error(_("abort: %s\n") % inst)
434 if inst.hint:
435 if inst.hint:
435 self.ui.error(_("(%s)\n") % inst.hint)
436 self.ui.error(_("(%s)\n") % inst.hint)
436 self.ui.flush()
437 self.ui.flush()
437 self.cresult.write('exit 255')
438 self.cresult.write('exit 255')
438 return
439 return
439 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
440 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
440 insts = []
441 insts = []
441 if newhash.mtimehash != self.hashstate.mtimehash:
442 if newhash.mtimehash != self.hashstate.mtimehash:
442 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
443 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
443 insts.append('unlink %s' % addr)
444 insts.append('unlink %s' % addr)
444 # mtimehash is empty if one or more extensions fail to load.
445 # mtimehash is empty if one or more extensions fail to load.
445 # to be compatible with hg, still serve the client this time.
446 # to be compatible with hg, still serve the client this time.
446 if self.hashstate.mtimehash:
447 if self.hashstate.mtimehash:
447 insts.append('reconnect')
448 insts.append('reconnect')
448 if newhash.confighash != self.hashstate.confighash:
449 if newhash.confighash != self.hashstate.confighash:
449 addr = _hashaddress(self.baseaddress, newhash.confighash)
450 addr = _hashaddress(self.baseaddress, newhash.confighash)
450 insts.append('redirect %s' % addr)
451 insts.append('redirect %s' % addr)
451 self.ui.log('chgserver', 'validate: %s\n', insts)
452 self.ui.log('chgserver', 'validate: %s\n', insts)
452 self.cresult.write('\0'.join(insts) or '\0')
453 self.cresult.write('\0'.join(insts) or '\0')
453
454
454 def chdir(self):
455 def chdir(self):
455 """Change current directory
456 """Change current directory
456
457
457 Note that the behavior of --cwd option is bit different from this.
458 Note that the behavior of --cwd option is bit different from this.
458 It does not affect --config parameter.
459 It does not affect --config parameter.
459 """
460 """
460 path = self._readstr()
461 path = self._readstr()
461 if not path:
462 if not path:
462 return
463 return
463 self.ui.log('chgserver', 'chdir to %r\n', path)
464 self.ui.log('chgserver', 'chdir to %r\n', path)
464 os.chdir(path)
465 os.chdir(path)
465
466
466 def setumask(self):
467 def setumask(self):
467 """Change umask (DEPRECATED)"""
468 """Change umask (DEPRECATED)"""
468 # BUG: this does not follow the message frame structure, but kept for
469 # BUG: this does not follow the message frame structure, but kept for
469 # backward compatibility with old chg clients for some time
470 # backward compatibility with old chg clients for some time
470 self._setumask(self._read(4))
471 self._setumask(self._read(4))
471
472
472 def setumask2(self):
473 def setumask2(self):
473 """Change umask"""
474 """Change umask"""
474 data = self._readstr()
475 data = self._readstr()
475 if len(data) != 4:
476 if len(data) != 4:
476 raise ValueError('invalid mask length in setumask2 request')
477 raise ValueError('invalid mask length in setumask2 request')
477 self._setumask(data)
478 self._setumask(data)
478
479
479 def _setumask(self, data):
480 def _setumask(self, data):
480 mask = struct.unpack('>I', data)[0]
481 mask = struct.unpack('>I', data)[0]
481 self.ui.log('chgserver', 'setumask %r\n', mask)
482 self.ui.log('chgserver', 'setumask %r\n', mask)
482 os.umask(mask)
483 os.umask(mask)
483
484
484 def runcommand(self):
485 def runcommand(self):
485 # pager may be attached within the runcommand session, which should
486 # pager may be attached within the runcommand session, which should
486 # be detached at the end of the session. otherwise the pager wouldn't
487 # be detached at the end of the session. otherwise the pager wouldn't
487 # receive EOF.
488 # receive EOF.
488 globaloldios = self._oldios
489 globaloldios = self._oldios
489 self._oldios = []
490 self._oldios = []
490 try:
491 try:
491 return super(chgcmdserver, self).runcommand()
492 return super(chgcmdserver, self).runcommand()
492 finally:
493 finally:
493 self._restoreio()
494 self._restoreio()
494 self._oldios = globaloldios
495 self._oldios = globaloldios
495
496
496 def setenv(self):
497 def setenv(self):
497 """Clear and update os.environ
498 """Clear and update os.environ
498
499
499 Note that not all variables can make an effect on the running process.
500 Note that not all variables can make an effect on the running process.
500 """
501 """
501 l = self._readlist()
502 l = self._readlist()
502 try:
503 try:
503 newenv = dict(s.split('=', 1) for s in l)
504 newenv = dict(s.split('=', 1) for s in l)
504 except ValueError:
505 except ValueError:
505 raise ValueError('unexpected value in setenv request')
506 raise ValueError('unexpected value in setenv request')
506 self.ui.log('chgserver', 'setenv: %r\n', sorted(newenv.keys()))
507 self.ui.log('chgserver', 'setenv: %r\n', sorted(newenv.keys()))
507 encoding.environ.clear()
508 encoding.environ.clear()
508 encoding.environ.update(newenv)
509 encoding.environ.update(newenv)
509
510
510 capabilities = commandserver.server.capabilities.copy()
511 capabilities = commandserver.server.capabilities.copy()
511 capabilities.update({'attachio': attachio,
512 capabilities.update({'attachio': attachio,
512 'chdir': chdir,
513 'chdir': chdir,
513 'runcommand': runcommand,
514 'runcommand': runcommand,
514 'setenv': setenv,
515 'setenv': setenv,
515 'setumask': setumask,
516 'setumask': setumask,
516 'setumask2': setumask2})
517 'setumask2': setumask2})
517
518
518 if util.safehasattr(procutil, 'setprocname'):
519 if util.safehasattr(procutil, 'setprocname'):
519 def setprocname(self):
520 def setprocname(self):
520 """Change process title"""
521 """Change process title"""
521 name = self._readstr()
522 name = self._readstr()
522 self.ui.log('chgserver', 'setprocname: %r\n', name)
523 self.ui.log('chgserver', 'setprocname: %r\n', name)
523 procutil.setprocname(name)
524 procutil.setprocname(name)
524 capabilities['setprocname'] = setprocname
525 capabilities['setprocname'] = setprocname
525
526
526 def _tempaddress(address):
527 def _tempaddress(address):
527 return '%s.%d.tmp' % (address, os.getpid())
528 return '%s.%d.tmp' % (address, os.getpid())
528
529
529 def _hashaddress(address, hashstr):
530 def _hashaddress(address, hashstr):
530 # if the basename of address contains '.', use only the left part. this
531 # if the basename of address contains '.', use only the left part. this
531 # makes it possible for the client to pass 'server.tmp$PID' and follow by
532 # makes it possible for the client to pass 'server.tmp$PID' and follow by
532 # an atomic rename to avoid locking when spawning new servers.
533 # an atomic rename to avoid locking when spawning new servers.
533 dirname, basename = os.path.split(address)
534 dirname, basename = os.path.split(address)
534 basename = basename.split('.', 1)[0]
535 basename = basename.split('.', 1)[0]
535 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
536 return '%s-%s' % (os.path.join(dirname, basename), hashstr)
536
537
537 class chgunixservicehandler(object):
538 class chgunixservicehandler(object):
538 """Set of operations for chg services"""
539 """Set of operations for chg services"""
539
540
540 pollinterval = 1 # [sec]
541 pollinterval = 1 # [sec]
541
542
542 def __init__(self, ui):
543 def __init__(self, ui):
543 self.ui = ui
544 self.ui = ui
544 self._idletimeout = ui.configint('chgserver', 'idletimeout')
545 self._idletimeout = ui.configint('chgserver', 'idletimeout')
545 self._lastactive = time.time()
546 self._lastactive = time.time()
546
547
547 def bindsocket(self, sock, address):
548 def bindsocket(self, sock, address):
548 self._inithashstate(address)
549 self._inithashstate(address)
549 self._checkextensions()
550 self._checkextensions()
550 self._bind(sock)
551 self._bind(sock)
551 self._createsymlink()
552 self._createsymlink()
552 # no "listening at" message should be printed to simulate hg behavior
553 # no "listening at" message should be printed to simulate hg behavior
553
554
554 def _inithashstate(self, address):
555 def _inithashstate(self, address):
555 self._baseaddress = address
556 self._baseaddress = address
556 if self.ui.configbool('chgserver', 'skiphash'):
557 if self.ui.configbool('chgserver', 'skiphash'):
557 self._hashstate = None
558 self._hashstate = None
558 self._realaddress = address
559 self._realaddress = address
559 return
560 return
560 self._hashstate = hashstate.fromui(self.ui)
561 self._hashstate = hashstate.fromui(self.ui)
561 self._realaddress = _hashaddress(address, self._hashstate.confighash)
562 self._realaddress = _hashaddress(address, self._hashstate.confighash)
562
563
563 def _checkextensions(self):
564 def _checkextensions(self):
564 if not self._hashstate:
565 if not self._hashstate:
565 return
566 return
566 if extensions.notloaded():
567 if extensions.notloaded():
567 # one or more extensions failed to load. mtimehash becomes
568 # one or more extensions failed to load. mtimehash becomes
568 # meaningless because we do not know the paths of those extensions.
569 # meaningless because we do not know the paths of those extensions.
569 # set mtimehash to an illegal hash value to invalidate the server.
570 # set mtimehash to an illegal hash value to invalidate the server.
570 self._hashstate.mtimehash = ''
571 self._hashstate.mtimehash = ''
571
572
572 def _bind(self, sock):
573 def _bind(self, sock):
573 # use a unique temp address so we can stat the file and do ownership
574 # use a unique temp address so we can stat the file and do ownership
574 # check later
575 # check later
575 tempaddress = _tempaddress(self._realaddress)
576 tempaddress = _tempaddress(self._realaddress)
576 util.bindunixsocket(sock, tempaddress)
577 util.bindunixsocket(sock, tempaddress)
577 self._socketstat = os.stat(tempaddress)
578 self._socketstat = os.stat(tempaddress)
578 sock.listen(socket.SOMAXCONN)
579 sock.listen(socket.SOMAXCONN)
579 # rename will replace the old socket file if exists atomically. the
580 # rename will replace the old socket file if exists atomically. the
580 # old server will detect ownership change and exit.
581 # old server will detect ownership change and exit.
581 util.rename(tempaddress, self._realaddress)
582 util.rename(tempaddress, self._realaddress)
582
583
583 def _createsymlink(self):
584 def _createsymlink(self):
584 if self._baseaddress == self._realaddress:
585 if self._baseaddress == self._realaddress:
585 return
586 return
586 tempaddress = _tempaddress(self._baseaddress)
587 tempaddress = _tempaddress(self._baseaddress)
587 os.symlink(os.path.basename(self._realaddress), tempaddress)
588 os.symlink(os.path.basename(self._realaddress), tempaddress)
588 util.rename(tempaddress, self._baseaddress)
589 util.rename(tempaddress, self._baseaddress)
589
590
590 def _issocketowner(self):
591 def _issocketowner(self):
591 try:
592 try:
592 st = os.stat(self._realaddress)
593 st = os.stat(self._realaddress)
593 return (st.st_ino == self._socketstat.st_ino and
594 return (st.st_ino == self._socketstat.st_ino and
594 st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
595 st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME])
595 except OSError:
596 except OSError:
596 return False
597 return False
597
598
598 def unlinksocket(self, address):
599 def unlinksocket(self, address):
599 if not self._issocketowner():
600 if not self._issocketowner():
600 return
601 return
601 # it is possible to have a race condition here that we may
602 # it is possible to have a race condition here that we may
602 # remove another server's socket file. but that's okay
603 # remove another server's socket file. but that's okay
603 # since that server will detect and exit automatically and
604 # since that server will detect and exit automatically and
604 # the client will start a new server on demand.
605 # the client will start a new server on demand.
605 util.tryunlink(self._realaddress)
606 util.tryunlink(self._realaddress)
606
607
607 def shouldexit(self):
608 def shouldexit(self):
608 if not self._issocketowner():
609 if not self._issocketowner():
609 self.ui.log(b'chgserver', b'%s is not owned, exiting.\n',
610 self.ui.log(b'chgserver', b'%s is not owned, exiting.\n',
610 self._realaddress)
611 self._realaddress)
611 return True
612 return True
612 if time.time() - self._lastactive > self._idletimeout:
613 if time.time() - self._lastactive > self._idletimeout:
613 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
614 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
614 return True
615 return True
615 return False
616 return False
616
617
617 def newconnection(self):
618 def newconnection(self):
618 self._lastactive = time.time()
619 self._lastactive = time.time()
619
620
620 def createcmdserver(self, repo, conn, fin, fout):
621 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
621 return chgcmdserver(self.ui, repo, fin, fout, conn,
622 return chgcmdserver(self.ui, repo, fin, fout, conn, prereposetups,
622 self._hashstate, self._baseaddress)
623 self._hashstate, self._baseaddress)
623
624
624 def chgunixservice(ui, repo, opts):
625 def chgunixservice(ui, repo, opts):
625 # CHGINTERNALMARK is set by chg client. It is an indication of things are
626 # CHGINTERNALMARK is set by chg client. It is an indication of things are
626 # started by chg so other code can do things accordingly, like disabling
627 # started by chg so other code can do things accordingly, like disabling
627 # demandimport or detecting chg client started by chg client. When executed
628 # demandimport or detecting chg client started by chg client. When executed
628 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
629 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
629 # environ cleaner.
630 # environ cleaner.
630 if 'CHGINTERNALMARK' in encoding.environ:
631 if 'CHGINTERNALMARK' in encoding.environ:
631 del encoding.environ['CHGINTERNALMARK']
632 del encoding.environ['CHGINTERNALMARK']
632
633
633 if repo:
634 if repo:
634 # one chgserver can serve multiple repos. drop repo information
635 # one chgserver can serve multiple repos. drop repo information
635 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
636 ui.setconfig('bundle', 'mainreporoot', '', 'repo')
636 h = chgunixservicehandler(ui)
637 h = chgunixservicehandler(ui)
637 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
638 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,624 +1,627 b''
1 # commandserver.py - communicate with Mercurial's API over a pipe
1 # commandserver.py - communicate with Mercurial's API over a pipe
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright Matt Mackall <mpm@selenic.com>
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import gc
11 import gc
12 import os
12 import os
13 import random
13 import random
14 import signal
14 import signal
15 import socket
15 import socket
16 import struct
16 import struct
17 import traceback
17 import traceback
18
18
19 try:
19 try:
20 import selectors
20 import selectors
21 selectors.BaseSelector
21 selectors.BaseSelector
22 except ImportError:
22 except ImportError:
23 from .thirdparty import selectors2 as selectors
23 from .thirdparty import selectors2 as selectors
24
24
25 from .i18n import _
25 from .i18n import _
26 from . import (
26 from . import (
27 encoding,
27 encoding,
28 error,
28 error,
29 loggingutil,
29 loggingutil,
30 pycompat,
30 pycompat,
31 util,
31 util,
32 vfs as vfsmod,
32 vfs as vfsmod,
33 )
33 )
34 from .utils import (
34 from .utils import (
35 cborutil,
35 cborutil,
36 procutil,
36 procutil,
37 )
37 )
38
38
39 class channeledoutput(object):
39 class channeledoutput(object):
40 """
40 """
41 Write data to out in the following format:
41 Write data to out in the following format:
42
42
43 data length (unsigned int),
43 data length (unsigned int),
44 data
44 data
45 """
45 """
46 def __init__(self, out, channel):
46 def __init__(self, out, channel):
47 self.out = out
47 self.out = out
48 self.channel = channel
48 self.channel = channel
49
49
50 @property
50 @property
51 def name(self):
51 def name(self):
52 return '<%c-channel>' % self.channel
52 return '<%c-channel>' % self.channel
53
53
54 def write(self, data):
54 def write(self, data):
55 if not data:
55 if not data:
56 return
56 return
57 # single write() to guarantee the same atomicity as the underlying file
57 # single write() to guarantee the same atomicity as the underlying file
58 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
58 self.out.write(struct.pack('>cI', self.channel, len(data)) + data)
59 self.out.flush()
59 self.out.flush()
60
60
61 def __getattr__(self, attr):
61 def __getattr__(self, attr):
62 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
62 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
63 raise AttributeError(attr)
63 raise AttributeError(attr)
64 return getattr(self.out, attr)
64 return getattr(self.out, attr)
65
65
66 class channeledmessage(object):
66 class channeledmessage(object):
67 """
67 """
68 Write encoded message and metadata to out in the following format:
68 Write encoded message and metadata to out in the following format:
69
69
70 data length (unsigned int),
70 data length (unsigned int),
71 encoded message and metadata, as a flat key-value dict.
71 encoded message and metadata, as a flat key-value dict.
72
72
73 Each message should have 'type' attribute. Messages of unknown type
73 Each message should have 'type' attribute. Messages of unknown type
74 should be ignored.
74 should be ignored.
75 """
75 """
76
76
77 # teach ui that write() can take **opts
77 # teach ui that write() can take **opts
78 structured = True
78 structured = True
79
79
80 def __init__(self, out, channel, encodename, encodefn):
80 def __init__(self, out, channel, encodename, encodefn):
81 self._cout = channeledoutput(out, channel)
81 self._cout = channeledoutput(out, channel)
82 self.encoding = encodename
82 self.encoding = encodename
83 self._encodefn = encodefn
83 self._encodefn = encodefn
84
84
85 def write(self, data, **opts):
85 def write(self, data, **opts):
86 opts = pycompat.byteskwargs(opts)
86 opts = pycompat.byteskwargs(opts)
87 if data is not None:
87 if data is not None:
88 opts[b'data'] = data
88 opts[b'data'] = data
89 self._cout.write(self._encodefn(opts))
89 self._cout.write(self._encodefn(opts))
90
90
91 def __getattr__(self, attr):
91 def __getattr__(self, attr):
92 return getattr(self._cout, attr)
92 return getattr(self._cout, attr)
93
93
94 class channeledinput(object):
94 class channeledinput(object):
95 """
95 """
96 Read data from in_.
96 Read data from in_.
97
97
98 Requests for input are written to out in the following format:
98 Requests for input are written to out in the following format:
99 channel identifier - 'I' for plain input, 'L' line based (1 byte)
99 channel identifier - 'I' for plain input, 'L' line based (1 byte)
100 how many bytes to send at most (unsigned int),
100 how many bytes to send at most (unsigned int),
101
101
102 The client replies with:
102 The client replies with:
103 data length (unsigned int), 0 meaning EOF
103 data length (unsigned int), 0 meaning EOF
104 data
104 data
105 """
105 """
106
106
107 maxchunksize = 4 * 1024
107 maxchunksize = 4 * 1024
108
108
109 def __init__(self, in_, out, channel):
109 def __init__(self, in_, out, channel):
110 self.in_ = in_
110 self.in_ = in_
111 self.out = out
111 self.out = out
112 self.channel = channel
112 self.channel = channel
113
113
114 @property
114 @property
115 def name(self):
115 def name(self):
116 return '<%c-channel>' % self.channel
116 return '<%c-channel>' % self.channel
117
117
118 def read(self, size=-1):
118 def read(self, size=-1):
119 if size < 0:
119 if size < 0:
120 # if we need to consume all the clients input, ask for 4k chunks
120 # if we need to consume all the clients input, ask for 4k chunks
121 # so the pipe doesn't fill up risking a deadlock
121 # so the pipe doesn't fill up risking a deadlock
122 size = self.maxchunksize
122 size = self.maxchunksize
123 s = self._read(size, self.channel)
123 s = self._read(size, self.channel)
124 buf = s
124 buf = s
125 while s:
125 while s:
126 s = self._read(size, self.channel)
126 s = self._read(size, self.channel)
127 buf += s
127 buf += s
128
128
129 return buf
129 return buf
130 else:
130 else:
131 return self._read(size, self.channel)
131 return self._read(size, self.channel)
132
132
133 def _read(self, size, channel):
133 def _read(self, size, channel):
134 if not size:
134 if not size:
135 return ''
135 return ''
136 assert size > 0
136 assert size > 0
137
137
138 # tell the client we need at most size bytes
138 # tell the client we need at most size bytes
139 self.out.write(struct.pack('>cI', channel, size))
139 self.out.write(struct.pack('>cI', channel, size))
140 self.out.flush()
140 self.out.flush()
141
141
142 length = self.in_.read(4)
142 length = self.in_.read(4)
143 length = struct.unpack('>I', length)[0]
143 length = struct.unpack('>I', length)[0]
144 if not length:
144 if not length:
145 return ''
145 return ''
146 else:
146 else:
147 return self.in_.read(length)
147 return self.in_.read(length)
148
148
149 def readline(self, size=-1):
149 def readline(self, size=-1):
150 if size < 0:
150 if size < 0:
151 size = self.maxchunksize
151 size = self.maxchunksize
152 s = self._read(size, 'L')
152 s = self._read(size, 'L')
153 buf = s
153 buf = s
154 # keep asking for more until there's either no more or
154 # keep asking for more until there's either no more or
155 # we got a full line
155 # we got a full line
156 while s and s[-1] != '\n':
156 while s and s[-1] != '\n':
157 s = self._read(size, 'L')
157 s = self._read(size, 'L')
158 buf += s
158 buf += s
159
159
160 return buf
160 return buf
161 else:
161 else:
162 return self._read(size, 'L')
162 return self._read(size, 'L')
163
163
164 def __iter__(self):
164 def __iter__(self):
165 return self
165 return self
166
166
167 def next(self):
167 def next(self):
168 l = self.readline()
168 l = self.readline()
169 if not l:
169 if not l:
170 raise StopIteration
170 raise StopIteration
171 return l
171 return l
172
172
173 __next__ = next
173 __next__ = next
174
174
175 def __getattr__(self, attr):
175 def __getattr__(self, attr):
176 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
176 if attr in (r'isatty', r'fileno', r'tell', r'seek'):
177 raise AttributeError(attr)
177 raise AttributeError(attr)
178 return getattr(self.in_, attr)
178 return getattr(self.in_, attr)
179
179
180 _messageencoders = {
180 _messageencoders = {
181 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
181 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
182 }
182 }
183
183
184 def _selectmessageencoder(ui):
184 def _selectmessageencoder(ui):
185 # experimental config: cmdserver.message-encodings
185 # experimental config: cmdserver.message-encodings
186 encnames = ui.configlist(b'cmdserver', b'message-encodings')
186 encnames = ui.configlist(b'cmdserver', b'message-encodings')
187 for n in encnames:
187 for n in encnames:
188 f = _messageencoders.get(n)
188 f = _messageencoders.get(n)
189 if f:
189 if f:
190 return n, f
190 return n, f
191 raise error.Abort(b'no supported message encodings: %s'
191 raise error.Abort(b'no supported message encodings: %s'
192 % b' '.join(encnames))
192 % b' '.join(encnames))
193
193
194 class server(object):
194 class server(object):
195 """
195 """
196 Listens for commands on fin, runs them and writes the output on a channel
196 Listens for commands on fin, runs them and writes the output on a channel
197 based stream to fout.
197 based stream to fout.
198 """
198 """
199 def __init__(self, ui, repo, fin, fout):
199 def __init__(self, ui, repo, fin, fout, prereposetups=None):
200 self.cwd = encoding.getcwd()
200 self.cwd = encoding.getcwd()
201
201
202 if repo:
202 if repo:
203 # the ui here is really the repo ui so take its baseui so we don't
203 # the ui here is really the repo ui so take its baseui so we don't
204 # end up with its local configuration
204 # end up with its local configuration
205 self.ui = repo.baseui
205 self.ui = repo.baseui
206 self.repo = repo
206 self.repo = repo
207 self.repoui = repo.ui
207 self.repoui = repo.ui
208 else:
208 else:
209 self.ui = ui
209 self.ui = ui
210 self.repo = self.repoui = None
210 self.repo = self.repoui = None
211 self._prereposetups = prereposetups
211
212
212 self.cdebug = channeledoutput(fout, 'd')
213 self.cdebug = channeledoutput(fout, 'd')
213 self.cerr = channeledoutput(fout, 'e')
214 self.cerr = channeledoutput(fout, 'e')
214 self.cout = channeledoutput(fout, 'o')
215 self.cout = channeledoutput(fout, 'o')
215 self.cin = channeledinput(fin, fout, 'I')
216 self.cin = channeledinput(fin, fout, 'I')
216 self.cresult = channeledoutput(fout, 'r')
217 self.cresult = channeledoutput(fout, 'r')
217
218
218 if self.ui.config(b'cmdserver', b'log') == b'-':
219 if self.ui.config(b'cmdserver', b'log') == b'-':
219 # switch log stream of server's ui to the 'd' (debug) channel
220 # switch log stream of server's ui to the 'd' (debug) channel
220 # (don't touch repo.ui as its lifetime is longer than the server)
221 # (don't touch repo.ui as its lifetime is longer than the server)
221 self.ui = self.ui.copy()
222 self.ui = self.ui.copy()
222 setuplogging(self.ui, repo=None, fp=self.cdebug)
223 setuplogging(self.ui, repo=None, fp=self.cdebug)
223
224
224 # TODO: add this to help/config.txt when stabilized
225 # TODO: add this to help/config.txt when stabilized
225 # ``channel``
226 # ``channel``
226 # Use separate channel for structured output. (Command-server only)
227 # Use separate channel for structured output. (Command-server only)
227 self.cmsg = None
228 self.cmsg = None
228 if ui.config(b'ui', b'message-output') == b'channel':
229 if ui.config(b'ui', b'message-output') == b'channel':
229 encname, encfn = _selectmessageencoder(ui)
230 encname, encfn = _selectmessageencoder(ui)
230 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
231 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
231
232
232 self.client = fin
233 self.client = fin
233
234
234 def cleanup(self):
235 def cleanup(self):
235 """release and restore resources taken during server session"""
236 """release and restore resources taken during server session"""
236
237
237 def _read(self, size):
238 def _read(self, size):
238 if not size:
239 if not size:
239 return ''
240 return ''
240
241
241 data = self.client.read(size)
242 data = self.client.read(size)
242
243
243 # is the other end closed?
244 # is the other end closed?
244 if not data:
245 if not data:
245 raise EOFError
246 raise EOFError
246
247
247 return data
248 return data
248
249
249 def _readstr(self):
250 def _readstr(self):
250 """read a string from the channel
251 """read a string from the channel
251
252
252 format:
253 format:
253 data length (uint32), data
254 data length (uint32), data
254 """
255 """
255 length = struct.unpack('>I', self._read(4))[0]
256 length = struct.unpack('>I', self._read(4))[0]
256 if not length:
257 if not length:
257 return ''
258 return ''
258 return self._read(length)
259 return self._read(length)
259
260
260 def _readlist(self):
261 def _readlist(self):
261 """read a list of NULL separated strings from the channel"""
262 """read a list of NULL separated strings from the channel"""
262 s = self._readstr()
263 s = self._readstr()
263 if s:
264 if s:
264 return s.split('\0')
265 return s.split('\0')
265 else:
266 else:
266 return []
267 return []
267
268
268 def runcommand(self):
269 def runcommand(self):
269 """ reads a list of \0 terminated arguments, executes
270 """ reads a list of \0 terminated arguments, executes
270 and writes the return code to the result channel """
271 and writes the return code to the result channel """
271 from . import dispatch # avoid cycle
272 from . import dispatch # avoid cycle
272
273
273 args = self._readlist()
274 args = self._readlist()
274
275
275 # copy the uis so changes (e.g. --config or --verbose) don't
276 # copy the uis so changes (e.g. --config or --verbose) don't
276 # persist between requests
277 # persist between requests
277 copiedui = self.ui.copy()
278 copiedui = self.ui.copy()
278 uis = [copiedui]
279 uis = [copiedui]
279 if self.repo:
280 if self.repo:
280 self.repo.baseui = copiedui
281 self.repo.baseui = copiedui
281 # clone ui without using ui.copy because this is protected
282 # clone ui without using ui.copy because this is protected
282 repoui = self.repoui.__class__(self.repoui)
283 repoui = self.repoui.__class__(self.repoui)
283 repoui.copy = copiedui.copy # redo copy protection
284 repoui.copy = copiedui.copy # redo copy protection
284 uis.append(repoui)
285 uis.append(repoui)
285 self.repo.ui = self.repo.dirstate._ui = repoui
286 self.repo.ui = self.repo.dirstate._ui = repoui
286 self.repo.invalidateall()
287 self.repo.invalidateall()
287
288
288 for ui in uis:
289 for ui in uis:
289 ui.resetstate()
290 ui.resetstate()
290 # any kind of interaction must use server channels, but chg may
291 # any kind of interaction must use server channels, but chg may
291 # replace channels by fully functional tty files. so nontty is
292 # replace channels by fully functional tty files. so nontty is
292 # enforced only if cin is a channel.
293 # enforced only if cin is a channel.
293 if not util.safehasattr(self.cin, 'fileno'):
294 if not util.safehasattr(self.cin, 'fileno'):
294 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
295 ui.setconfig('ui', 'nontty', 'true', 'commandserver')
295
296
296 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
297 req = dispatch.request(args[:], copiedui, self.repo, self.cin,
297 self.cout, self.cerr, self.cmsg)
298 self.cout, self.cerr, self.cmsg,
299 prereposetups=self._prereposetups)
298
300
299 try:
301 try:
300 ret = dispatch.dispatch(req) & 255
302 ret = dispatch.dispatch(req) & 255
301 self.cresult.write(struct.pack('>i', int(ret)))
303 self.cresult.write(struct.pack('>i', int(ret)))
302 finally:
304 finally:
303 # restore old cwd
305 # restore old cwd
304 if '--cwd' in args:
306 if '--cwd' in args:
305 os.chdir(self.cwd)
307 os.chdir(self.cwd)
306
308
307 def getencoding(self):
309 def getencoding(self):
308 """ writes the current encoding to the result channel """
310 """ writes the current encoding to the result channel """
309 self.cresult.write(encoding.encoding)
311 self.cresult.write(encoding.encoding)
310
312
311 def serveone(self):
313 def serveone(self):
312 cmd = self.client.readline()[:-1]
314 cmd = self.client.readline()[:-1]
313 if cmd:
315 if cmd:
314 handler = self.capabilities.get(cmd)
316 handler = self.capabilities.get(cmd)
315 if handler:
317 if handler:
316 handler(self)
318 handler(self)
317 else:
319 else:
318 # clients are expected to check what commands are supported by
320 # clients are expected to check what commands are supported by
319 # looking at the servers capabilities
321 # looking at the servers capabilities
320 raise error.Abort(_('unknown command %s') % cmd)
322 raise error.Abort(_('unknown command %s') % cmd)
321
323
322 return cmd != ''
324 return cmd != ''
323
325
324 capabilities = {'runcommand': runcommand,
326 capabilities = {'runcommand': runcommand,
325 'getencoding': getencoding}
327 'getencoding': getencoding}
326
328
327 def serve(self):
329 def serve(self):
328 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
330 hellomsg = 'capabilities: ' + ' '.join(sorted(self.capabilities))
329 hellomsg += '\n'
331 hellomsg += '\n'
330 hellomsg += 'encoding: ' + encoding.encoding
332 hellomsg += 'encoding: ' + encoding.encoding
331 hellomsg += '\n'
333 hellomsg += '\n'
332 if self.cmsg:
334 if self.cmsg:
333 hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
335 hellomsg += 'message-encoding: %s\n' % self.cmsg.encoding
334 hellomsg += 'pid: %d' % procutil.getpid()
336 hellomsg += 'pid: %d' % procutil.getpid()
335 if util.safehasattr(os, 'getpgid'):
337 if util.safehasattr(os, 'getpgid'):
336 hellomsg += '\n'
338 hellomsg += '\n'
337 hellomsg += 'pgid: %d' % os.getpgid(0)
339 hellomsg += 'pgid: %d' % os.getpgid(0)
338
340
339 # write the hello msg in -one- chunk
341 # write the hello msg in -one- chunk
340 self.cout.write(hellomsg)
342 self.cout.write(hellomsg)
341
343
342 try:
344 try:
343 while self.serveone():
345 while self.serveone():
344 pass
346 pass
345 except EOFError:
347 except EOFError:
346 # we'll get here if the client disconnected while we were reading
348 # we'll get here if the client disconnected while we were reading
347 # its request
349 # its request
348 return 1
350 return 1
349
351
350 return 0
352 return 0
351
353
352 def setuplogging(ui, repo=None, fp=None):
354 def setuplogging(ui, repo=None, fp=None):
353 """Set up server logging facility
355 """Set up server logging facility
354
356
355 If cmdserver.log is '-', log messages will be sent to the given fp.
357 If cmdserver.log is '-', log messages will be sent to the given fp.
356 It should be the 'd' channel while a client is connected, and otherwise
358 It should be the 'd' channel while a client is connected, and otherwise
357 is the stderr of the server process.
359 is the stderr of the server process.
358 """
360 """
359 # developer config: cmdserver.log
361 # developer config: cmdserver.log
360 logpath = ui.config(b'cmdserver', b'log')
362 logpath = ui.config(b'cmdserver', b'log')
361 if not logpath:
363 if not logpath:
362 return
364 return
363 # developer config: cmdserver.track-log
365 # developer config: cmdserver.track-log
364 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
366 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
365
367
366 if logpath == b'-' and fp:
368 if logpath == b'-' and fp:
367 logger = loggingutil.fileobjectlogger(fp, tracked)
369 logger = loggingutil.fileobjectlogger(fp, tracked)
368 elif logpath == b'-':
370 elif logpath == b'-':
369 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
371 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
370 else:
372 else:
371 logpath = os.path.abspath(util.expandpath(logpath))
373 logpath = os.path.abspath(util.expandpath(logpath))
372 # developer config: cmdserver.max-log-files
374 # developer config: cmdserver.max-log-files
373 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
375 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
374 # developer config: cmdserver.max-log-size
376 # developer config: cmdserver.max-log-size
375 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
377 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
376 vfs = vfsmod.vfs(os.path.dirname(logpath))
378 vfs = vfsmod.vfs(os.path.dirname(logpath))
377 logger = loggingutil.filelogger(vfs, os.path.basename(logpath), tracked,
379 logger = loggingutil.filelogger(vfs, os.path.basename(logpath), tracked,
378 maxfiles=maxfiles, maxsize=maxsize)
380 maxfiles=maxfiles, maxsize=maxsize)
379
381
380 targetuis = {ui}
382 targetuis = {ui}
381 if repo:
383 if repo:
382 targetuis.add(repo.baseui)
384 targetuis.add(repo.baseui)
383 targetuis.add(repo.ui)
385 targetuis.add(repo.ui)
384 for u in targetuis:
386 for u in targetuis:
385 u.setlogger(b'cmdserver', logger)
387 u.setlogger(b'cmdserver', logger)
386
388
387 class pipeservice(object):
389 class pipeservice(object):
388 def __init__(self, ui, repo, opts):
390 def __init__(self, ui, repo, opts):
389 self.ui = ui
391 self.ui = ui
390 self.repo = repo
392 self.repo = repo
391
393
392 def init(self):
394 def init(self):
393 pass
395 pass
394
396
395 def run(self):
397 def run(self):
396 ui = self.ui
398 ui = self.ui
397 # redirect stdio to null device so that broken extensions or in-process
399 # redirect stdio to null device so that broken extensions or in-process
398 # hooks will never cause corruption of channel protocol.
400 # hooks will never cause corruption of channel protocol.
399 with procutil.protectedstdio(ui.fin, ui.fout) as (fin, fout):
401 with procutil.protectedstdio(ui.fin, ui.fout) as (fin, fout):
400 sv = server(ui, self.repo, fin, fout)
402 sv = server(ui, self.repo, fin, fout)
401 try:
403 try:
402 return sv.serve()
404 return sv.serve()
403 finally:
405 finally:
404 sv.cleanup()
406 sv.cleanup()
405
407
406 def _initworkerprocess():
408 def _initworkerprocess():
407 # use a different process group from the master process, in order to:
409 # use a different process group from the master process, in order to:
408 # 1. make the current process group no longer "orphaned" (because the
410 # 1. make the current process group no longer "orphaned" (because the
409 # parent of this process is in a different process group while
411 # parent of this process is in a different process group while
410 # remains in a same session)
412 # remains in a same session)
411 # according to POSIX 2.2.2.52, orphaned process group will ignore
413 # according to POSIX 2.2.2.52, orphaned process group will ignore
412 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
414 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
413 # cause trouble for things like ncurses.
415 # cause trouble for things like ncurses.
414 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
416 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
415 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
417 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
416 # processes like ssh will be killed properly, without affecting
418 # processes like ssh will be killed properly, without affecting
417 # unrelated processes.
419 # unrelated processes.
418 os.setpgid(0, 0)
420 os.setpgid(0, 0)
419 # change random state otherwise forked request handlers would have a
421 # change random state otherwise forked request handlers would have a
420 # same state inherited from parent.
422 # same state inherited from parent.
421 random.seed()
423 random.seed()
422
424
423 def _serverequest(ui, repo, conn, createcmdserver):
425 def _serverequest(ui, repo, conn, createcmdserver, prereposetups):
424 fin = conn.makefile(r'rb')
426 fin = conn.makefile(r'rb')
425 fout = conn.makefile(r'wb')
427 fout = conn.makefile(r'wb')
426 sv = None
428 sv = None
427 try:
429 try:
428 sv = createcmdserver(repo, conn, fin, fout)
430 sv = createcmdserver(repo, conn, fin, fout, prereposetups)
429 try:
431 try:
430 sv.serve()
432 sv.serve()
431 # handle exceptions that may be raised by command server. most of
433 # handle exceptions that may be raised by command server. most of
432 # known exceptions are caught by dispatch.
434 # known exceptions are caught by dispatch.
433 except error.Abort as inst:
435 except error.Abort as inst:
434 ui.error(_('abort: %s\n') % inst)
436 ui.error(_('abort: %s\n') % inst)
435 except IOError as inst:
437 except IOError as inst:
436 if inst.errno != errno.EPIPE:
438 if inst.errno != errno.EPIPE:
437 raise
439 raise
438 except KeyboardInterrupt:
440 except KeyboardInterrupt:
439 pass
441 pass
440 finally:
442 finally:
441 sv.cleanup()
443 sv.cleanup()
442 except: # re-raises
444 except: # re-raises
443 # also write traceback to error channel. otherwise client cannot
445 # also write traceback to error channel. otherwise client cannot
444 # see it because it is written to server's stderr by default.
446 # see it because it is written to server's stderr by default.
445 if sv:
447 if sv:
446 cerr = sv.cerr
448 cerr = sv.cerr
447 else:
449 else:
448 cerr = channeledoutput(fout, 'e')
450 cerr = channeledoutput(fout, 'e')
449 cerr.write(encoding.strtolocal(traceback.format_exc()))
451 cerr.write(encoding.strtolocal(traceback.format_exc()))
450 raise
452 raise
451 finally:
453 finally:
452 fin.close()
454 fin.close()
453 try:
455 try:
454 fout.close() # implicit flush() may cause another EPIPE
456 fout.close() # implicit flush() may cause another EPIPE
455 except IOError as inst:
457 except IOError as inst:
456 if inst.errno != errno.EPIPE:
458 if inst.errno != errno.EPIPE:
457 raise
459 raise
458
460
459 class unixservicehandler(object):
461 class unixservicehandler(object):
460 """Set of pluggable operations for unix-mode services
462 """Set of pluggable operations for unix-mode services
461
463
462 Almost all methods except for createcmdserver() are called in the main
464 Almost all methods except for createcmdserver() are called in the main
463 process. You can't pass mutable resource back from createcmdserver().
465 process. You can't pass mutable resource back from createcmdserver().
464 """
466 """
465
467
466 pollinterval = None
468 pollinterval = None
467
469
468 def __init__(self, ui):
470 def __init__(self, ui):
469 self.ui = ui
471 self.ui = ui
470
472
471 def bindsocket(self, sock, address):
473 def bindsocket(self, sock, address):
472 util.bindunixsocket(sock, address)
474 util.bindunixsocket(sock, address)
473 sock.listen(socket.SOMAXCONN)
475 sock.listen(socket.SOMAXCONN)
474 self.ui.status(_('listening at %s\n') % address)
476 self.ui.status(_('listening at %s\n') % address)
475 self.ui.flush() # avoid buffering of status message
477 self.ui.flush() # avoid buffering of status message
476
478
477 def unlinksocket(self, address):
479 def unlinksocket(self, address):
478 os.unlink(address)
480 os.unlink(address)
479
481
480 def shouldexit(self):
482 def shouldexit(self):
481 """True if server should shut down; checked per pollinterval"""
483 """True if server should shut down; checked per pollinterval"""
482 return False
484 return False
483
485
484 def newconnection(self):
486 def newconnection(self):
485 """Called when main process notices new connection"""
487 """Called when main process notices new connection"""
486
488
487 def createcmdserver(self, repo, conn, fin, fout):
489 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
488 """Create new command server instance; called in the process that
490 """Create new command server instance; called in the process that
489 serves for the current connection"""
491 serves for the current connection"""
490 return server(self.ui, repo, fin, fout)
492 return server(self.ui, repo, fin, fout, prereposetups)
491
493
492 class unixforkingservice(object):
494 class unixforkingservice(object):
493 """
495 """
494 Listens on unix domain socket and forks server per connection
496 Listens on unix domain socket and forks server per connection
495 """
497 """
496
498
497 def __init__(self, ui, repo, opts, handler=None):
499 def __init__(self, ui, repo, opts, handler=None):
498 self.ui = ui
500 self.ui = ui
499 self.repo = repo
501 self.repo = repo
500 self.address = opts['address']
502 self.address = opts['address']
501 if not util.safehasattr(socket, 'AF_UNIX'):
503 if not util.safehasattr(socket, 'AF_UNIX'):
502 raise error.Abort(_('unsupported platform'))
504 raise error.Abort(_('unsupported platform'))
503 if not self.address:
505 if not self.address:
504 raise error.Abort(_('no socket path specified with --address'))
506 raise error.Abort(_('no socket path specified with --address'))
505 self._servicehandler = handler or unixservicehandler(ui)
507 self._servicehandler = handler or unixservicehandler(ui)
506 self._sock = None
508 self._sock = None
507 self._oldsigchldhandler = None
509 self._oldsigchldhandler = None
508 self._workerpids = set() # updated by signal handler; do not iterate
510 self._workerpids = set() # updated by signal handler; do not iterate
509 self._socketunlinked = None
511 self._socketunlinked = None
510
512
511 def init(self):
513 def init(self):
512 self._sock = socket.socket(socket.AF_UNIX)
514 self._sock = socket.socket(socket.AF_UNIX)
513 self._servicehandler.bindsocket(self._sock, self.address)
515 self._servicehandler.bindsocket(self._sock, self.address)
514 if util.safehasattr(procutil, 'unblocksignal'):
516 if util.safehasattr(procutil, 'unblocksignal'):
515 procutil.unblocksignal(signal.SIGCHLD)
517 procutil.unblocksignal(signal.SIGCHLD)
516 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
518 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
517 self._oldsigchldhandler = o
519 self._oldsigchldhandler = o
518 self._socketunlinked = False
520 self._socketunlinked = False
519
521
520 def _unlinksocket(self):
522 def _unlinksocket(self):
521 if not self._socketunlinked:
523 if not self._socketunlinked:
522 self._servicehandler.unlinksocket(self.address)
524 self._servicehandler.unlinksocket(self.address)
523 self._socketunlinked = True
525 self._socketunlinked = True
524
526
525 def _cleanup(self):
527 def _cleanup(self):
526 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
528 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
527 self._sock.close()
529 self._sock.close()
528 self._unlinksocket()
530 self._unlinksocket()
529 # don't kill child processes as they have active clients, just wait
531 # don't kill child processes as they have active clients, just wait
530 self._reapworkers(0)
532 self._reapworkers(0)
531
533
532 def run(self):
534 def run(self):
533 try:
535 try:
534 self._mainloop()
536 self._mainloop()
535 finally:
537 finally:
536 self._cleanup()
538 self._cleanup()
537
539
538 def _mainloop(self):
540 def _mainloop(self):
539 exiting = False
541 exiting = False
540 h = self._servicehandler
542 h = self._servicehandler
541 selector = selectors.DefaultSelector()
543 selector = selectors.DefaultSelector()
542 selector.register(self._sock, selectors.EVENT_READ)
544 selector.register(self._sock, selectors.EVENT_READ)
543 while True:
545 while True:
544 if not exiting and h.shouldexit():
546 if not exiting and h.shouldexit():
545 # clients can no longer connect() to the domain socket, so
547 # clients can no longer connect() to the domain socket, so
546 # we stop queuing new requests.
548 # we stop queuing new requests.
547 # for requests that are queued (connect()-ed, but haven't been
549 # for requests that are queued (connect()-ed, but haven't been
548 # accept()-ed), handle them before exit. otherwise, clients
550 # accept()-ed), handle them before exit. otherwise, clients
549 # waiting for recv() will receive ECONNRESET.
551 # waiting for recv() will receive ECONNRESET.
550 self._unlinksocket()
552 self._unlinksocket()
551 exiting = True
553 exiting = True
552 try:
554 try:
553 ready = selector.select(timeout=h.pollinterval)
555 ready = selector.select(timeout=h.pollinterval)
554 except OSError as inst:
556 except OSError as inst:
555 # selectors2 raises ETIMEDOUT if timeout exceeded while
557 # selectors2 raises ETIMEDOUT if timeout exceeded while
556 # handling signal interrupt. That's probably wrong, but
558 # handling signal interrupt. That's probably wrong, but
557 # we can easily get around it.
559 # we can easily get around it.
558 if inst.errno != errno.ETIMEDOUT:
560 if inst.errno != errno.ETIMEDOUT:
559 raise
561 raise
560 ready = []
562 ready = []
561 if not ready:
563 if not ready:
562 # only exit if we completed all queued requests
564 # only exit if we completed all queued requests
563 if exiting:
565 if exiting:
564 break
566 break
565 continue
567 continue
566 try:
568 try:
567 conn, _addr = self._sock.accept()
569 conn, _addr = self._sock.accept()
568 except socket.error as inst:
570 except socket.error as inst:
569 if inst.args[0] == errno.EINTR:
571 if inst.args[0] == errno.EINTR:
570 continue
572 continue
571 raise
573 raise
572
574
573 pid = os.fork()
575 pid = os.fork()
574 if pid:
576 if pid:
575 try:
577 try:
576 self.ui.log(b'cmdserver',
578 self.ui.log(b'cmdserver',
577 b'forked worker process (pid=%d)\n', pid)
579 b'forked worker process (pid=%d)\n', pid)
578 self._workerpids.add(pid)
580 self._workerpids.add(pid)
579 h.newconnection()
581 h.newconnection()
580 finally:
582 finally:
581 conn.close() # release handle in parent process
583 conn.close() # release handle in parent process
582 else:
584 else:
583 try:
585 try:
584 selector.close()
586 selector.close()
585 self._sock.close()
587 self._sock.close()
586 self._runworker(conn)
588 self._runworker(conn)
587 conn.close()
589 conn.close()
588 os._exit(0)
590 os._exit(0)
589 except: # never return, hence no re-raises
591 except: # never return, hence no re-raises
590 try:
592 try:
591 self.ui.traceback(force=True)
593 self.ui.traceback(force=True)
592 finally:
594 finally:
593 os._exit(255)
595 os._exit(255)
594 selector.close()
596 selector.close()
595
597
596 def _sigchldhandler(self, signal, frame):
598 def _sigchldhandler(self, signal, frame):
597 self._reapworkers(os.WNOHANG)
599 self._reapworkers(os.WNOHANG)
598
600
599 def _reapworkers(self, options):
601 def _reapworkers(self, options):
600 while self._workerpids:
602 while self._workerpids:
601 try:
603 try:
602 pid, _status = os.waitpid(-1, options)
604 pid, _status = os.waitpid(-1, options)
603 except OSError as inst:
605 except OSError as inst:
604 if inst.errno == errno.EINTR:
606 if inst.errno == errno.EINTR:
605 continue
607 continue
606 if inst.errno != errno.ECHILD:
608 if inst.errno != errno.ECHILD:
607 raise
609 raise
608 # no child processes at all (reaped by other waitpid()?)
610 # no child processes at all (reaped by other waitpid()?)
609 self._workerpids.clear()
611 self._workerpids.clear()
610 return
612 return
611 if pid == 0:
613 if pid == 0:
612 # no waitable child processes
614 # no waitable child processes
613 return
615 return
614 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
616 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
615 self._workerpids.discard(pid)
617 self._workerpids.discard(pid)
616
618
617 def _runworker(self, conn):
619 def _runworker(self, conn):
618 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
620 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
619 _initworkerprocess()
621 _initworkerprocess()
620 h = self._servicehandler
622 h = self._servicehandler
621 try:
623 try:
622 _serverequest(self.ui, self.repo, conn, h.createcmdserver)
624 _serverequest(self.ui, self.repo, conn, h.createcmdserver,
625 prereposetups=None) # TODO: pass in hook functions
623 finally:
626 finally:
624 gc.collect() # trigger __del__ since worker process uses os._exit
627 gc.collect() # trigger __del__ since worker process uses os._exit
@@ -1,1105 +1,1105 b''
1 #if windows
1 #if windows
2 $ PYTHONPATH="$TESTDIR/../contrib;$PYTHONPATH"
2 $ PYTHONPATH="$TESTDIR/../contrib;$PYTHONPATH"
3 #else
3 #else
4 $ PYTHONPATH="$TESTDIR/../contrib:$PYTHONPATH"
4 $ PYTHONPATH="$TESTDIR/../contrib:$PYTHONPATH"
5 #endif
5 #endif
6 $ export PYTHONPATH
6 $ export PYTHONPATH
7
7
8 typical client does not want echo-back messages, so test without it:
8 typical client does not want echo-back messages, so test without it:
9
9
10 $ grep -v '^promptecho ' < $HGRCPATH >> $HGRCPATH.new
10 $ grep -v '^promptecho ' < $HGRCPATH >> $HGRCPATH.new
11 $ mv $HGRCPATH.new $HGRCPATH
11 $ mv $HGRCPATH.new $HGRCPATH
12
12
13 $ hg init repo
13 $ hg init repo
14 $ cd repo
14 $ cd repo
15
15
16 >>> from __future__ import absolute_import
16 >>> from __future__ import absolute_import
17 >>> import os
17 >>> import os
18 >>> import sys
18 >>> import sys
19 >>> from hgclient import bprint, check, readchannel, runcommand
19 >>> from hgclient import bprint, check, readchannel, runcommand
20 >>> @check
20 >>> @check
21 ... def hellomessage(server):
21 ... def hellomessage(server):
22 ... ch, data = readchannel(server)
22 ... ch, data = readchannel(server)
23 ... bprint(b'%c, %r' % (ch, data))
23 ... bprint(b'%c, %r' % (ch, data))
24 ... # run an arbitrary command to make sure the next thing the server
24 ... # run an arbitrary command to make sure the next thing the server
25 ... # sends isn't part of the hello message
25 ... # sends isn't part of the hello message
26 ... runcommand(server, [b'id'])
26 ... runcommand(server, [b'id'])
27 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
27 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
28 *** runcommand id
28 *** runcommand id
29 000000000000 tip
29 000000000000 tip
30
30
31 >>> from hgclient import check
31 >>> from hgclient import check
32 >>> @check
32 >>> @check
33 ... def unknowncommand(server):
33 ... def unknowncommand(server):
34 ... server.stdin.write(b'unknowncommand\n')
34 ... server.stdin.write(b'unknowncommand\n')
35 abort: unknown command unknowncommand
35 abort: unknown command unknowncommand
36
36
37 >>> from hgclient import check, readchannel, runcommand
37 >>> from hgclient import check, readchannel, runcommand
38 >>> @check
38 >>> @check
39 ... def checkruncommand(server):
39 ... def checkruncommand(server):
40 ... # hello block
40 ... # hello block
41 ... readchannel(server)
41 ... readchannel(server)
42 ...
42 ...
43 ... # no args
43 ... # no args
44 ... runcommand(server, [])
44 ... runcommand(server, [])
45 ...
45 ...
46 ... # global options
46 ... # global options
47 ... runcommand(server, [b'id', b'--quiet'])
47 ... runcommand(server, [b'id', b'--quiet'])
48 ...
48 ...
49 ... # make sure global options don't stick through requests
49 ... # make sure global options don't stick through requests
50 ... runcommand(server, [b'id'])
50 ... runcommand(server, [b'id'])
51 ...
51 ...
52 ... # --config
52 ... # --config
53 ... runcommand(server, [b'id', b'--config', b'ui.quiet=True'])
53 ... runcommand(server, [b'id', b'--config', b'ui.quiet=True'])
54 ...
54 ...
55 ... # make sure --config doesn't stick
55 ... # make sure --config doesn't stick
56 ... runcommand(server, [b'id'])
56 ... runcommand(server, [b'id'])
57 ...
57 ...
58 ... # negative return code should be masked
58 ... # negative return code should be masked
59 ... runcommand(server, [b'id', b'-runknown'])
59 ... runcommand(server, [b'id', b'-runknown'])
60 *** runcommand
60 *** runcommand
61 Mercurial Distributed SCM
61 Mercurial Distributed SCM
62
62
63 basic commands:
63 basic commands:
64
64
65 add add the specified files on the next commit
65 add add the specified files on the next commit
66 annotate show changeset information by line for each file
66 annotate show changeset information by line for each file
67 clone make a copy of an existing repository
67 clone make a copy of an existing repository
68 commit commit the specified files or all outstanding changes
68 commit commit the specified files or all outstanding changes
69 diff diff repository (or selected files)
69 diff diff repository (or selected files)
70 export dump the header and diffs for one or more changesets
70 export dump the header and diffs for one or more changesets
71 forget forget the specified files on the next commit
71 forget forget the specified files on the next commit
72 init create a new repository in the given directory
72 init create a new repository in the given directory
73 log show revision history of entire repository or files
73 log show revision history of entire repository or files
74 merge merge another revision into working directory
74 merge merge another revision into working directory
75 pull pull changes from the specified source
75 pull pull changes from the specified source
76 push push changes to the specified destination
76 push push changes to the specified destination
77 remove remove the specified files on the next commit
77 remove remove the specified files on the next commit
78 serve start stand-alone webserver
78 serve start stand-alone webserver
79 status show changed files in the working directory
79 status show changed files in the working directory
80 summary summarize working directory state
80 summary summarize working directory state
81 update update working directory (or switch revisions)
81 update update working directory (or switch revisions)
82
82
83 (use 'hg help' for the full list of commands or 'hg -v' for details)
83 (use 'hg help' for the full list of commands or 'hg -v' for details)
84 *** runcommand id --quiet
84 *** runcommand id --quiet
85 000000000000
85 000000000000
86 *** runcommand id
86 *** runcommand id
87 000000000000 tip
87 000000000000 tip
88 *** runcommand id --config ui.quiet=True
88 *** runcommand id --config ui.quiet=True
89 000000000000
89 000000000000
90 *** runcommand id
90 *** runcommand id
91 000000000000 tip
91 000000000000 tip
92 *** runcommand id -runknown
92 *** runcommand id -runknown
93 abort: unknown revision 'unknown'!
93 abort: unknown revision 'unknown'!
94 [255]
94 [255]
95
95
96 >>> from hgclient import bprint, check, readchannel
96 >>> from hgclient import bprint, check, readchannel
97 >>> @check
97 >>> @check
98 ... def inputeof(server):
98 ... def inputeof(server):
99 ... readchannel(server)
99 ... readchannel(server)
100 ... server.stdin.write(b'runcommand\n')
100 ... server.stdin.write(b'runcommand\n')
101 ... # close stdin while server is waiting for input
101 ... # close stdin while server is waiting for input
102 ... server.stdin.close()
102 ... server.stdin.close()
103 ...
103 ...
104 ... # server exits with 1 if the pipe closed while reading the command
104 ... # server exits with 1 if the pipe closed while reading the command
105 ... bprint(b'server exit code =', b'%d' % server.wait())
105 ... bprint(b'server exit code =', b'%d' % server.wait())
106 server exit code = 1
106 server exit code = 1
107
107
108 >>> from hgclient import check, readchannel, runcommand, stringio
108 >>> from hgclient import check, readchannel, runcommand, stringio
109 >>> @check
109 >>> @check
110 ... def serverinput(server):
110 ... def serverinput(server):
111 ... readchannel(server)
111 ... readchannel(server)
112 ...
112 ...
113 ... patch = b"""
113 ... patch = b"""
114 ... # HG changeset patch
114 ... # HG changeset patch
115 ... # User test
115 ... # User test
116 ... # Date 0 0
116 ... # Date 0 0
117 ... # Node ID c103a3dec114d882c98382d684d8af798d09d857
117 ... # Node ID c103a3dec114d882c98382d684d8af798d09d857
118 ... # Parent 0000000000000000000000000000000000000000
118 ... # Parent 0000000000000000000000000000000000000000
119 ... 1
119 ... 1
120 ...
120 ...
121 ... diff -r 000000000000 -r c103a3dec114 a
121 ... diff -r 000000000000 -r c103a3dec114 a
122 ... --- /dev/null Thu Jan 01 00:00:00 1970 +0000
122 ... --- /dev/null Thu Jan 01 00:00:00 1970 +0000
123 ... +++ b/a Thu Jan 01 00:00:00 1970 +0000
123 ... +++ b/a Thu Jan 01 00:00:00 1970 +0000
124 ... @@ -0,0 +1,1 @@
124 ... @@ -0,0 +1,1 @@
125 ... +1
125 ... +1
126 ... """
126 ... """
127 ...
127 ...
128 ... runcommand(server, [b'import', b'-'], input=stringio(patch))
128 ... runcommand(server, [b'import', b'-'], input=stringio(patch))
129 ... runcommand(server, [b'log'])
129 ... runcommand(server, [b'log'])
130 *** runcommand import -
130 *** runcommand import -
131 applying patch from stdin
131 applying patch from stdin
132 *** runcommand log
132 *** runcommand log
133 changeset: 0:eff892de26ec
133 changeset: 0:eff892de26ec
134 tag: tip
134 tag: tip
135 user: test
135 user: test
136 date: Thu Jan 01 00:00:00 1970 +0000
136 date: Thu Jan 01 00:00:00 1970 +0000
137 summary: 1
137 summary: 1
138
138
139
139
140 check strict parsing of early options:
140 check strict parsing of early options:
141
141
142 >>> import os
142 >>> import os
143 >>> from hgclient import check, readchannel, runcommand
143 >>> from hgclient import check, readchannel, runcommand
144 >>> os.environ['HGPLAIN'] = '+strictflags'
144 >>> os.environ['HGPLAIN'] = '+strictflags'
145 >>> @check
145 >>> @check
146 ... def cwd(server):
146 ... def cwd(server):
147 ... readchannel(server)
147 ... readchannel(server)
148 ... runcommand(server, [b'log', b'-b', b'--config=alias.log=!echo pwned',
148 ... runcommand(server, [b'log', b'-b', b'--config=alias.log=!echo pwned',
149 ... b'default'])
149 ... b'default'])
150 *** runcommand log -b --config=alias.log=!echo pwned default
150 *** runcommand log -b --config=alias.log=!echo pwned default
151 abort: unknown revision '--config=alias.log=!echo pwned'!
151 abort: unknown revision '--config=alias.log=!echo pwned'!
152 [255]
152 [255]
153
153
154 check that "histedit --commands=-" can read rules from the input channel:
154 check that "histedit --commands=-" can read rules from the input channel:
155
155
156 >>> from hgclient import check, readchannel, runcommand, stringio
156 >>> from hgclient import check, readchannel, runcommand, stringio
157 >>> @check
157 >>> @check
158 ... def serverinput(server):
158 ... def serverinput(server):
159 ... readchannel(server)
159 ... readchannel(server)
160 ... rules = b'pick eff892de26ec\n'
160 ... rules = b'pick eff892de26ec\n'
161 ... runcommand(server, [b'histedit', b'0', b'--commands=-',
161 ... runcommand(server, [b'histedit', b'0', b'--commands=-',
162 ... b'--config', b'extensions.histedit='],
162 ... b'--config', b'extensions.histedit='],
163 ... input=stringio(rules))
163 ... input=stringio(rules))
164 *** runcommand histedit 0 --commands=- --config extensions.histedit=
164 *** runcommand histedit 0 --commands=- --config extensions.histedit=
165
165
166 check that --cwd doesn't persist between requests:
166 check that --cwd doesn't persist between requests:
167
167
168 $ mkdir foo
168 $ mkdir foo
169 $ touch foo/bar
169 $ touch foo/bar
170 >>> from hgclient import check, readchannel, runcommand
170 >>> from hgclient import check, readchannel, runcommand
171 >>> @check
171 >>> @check
172 ... def cwd(server):
172 ... def cwd(server):
173 ... readchannel(server)
173 ... readchannel(server)
174 ... runcommand(server, [b'--cwd', b'foo', b'st', b'bar'])
174 ... runcommand(server, [b'--cwd', b'foo', b'st', b'bar'])
175 ... runcommand(server, [b'st', b'foo/bar'])
175 ... runcommand(server, [b'st', b'foo/bar'])
176 *** runcommand --cwd foo st bar
176 *** runcommand --cwd foo st bar
177 ? bar
177 ? bar
178 *** runcommand st foo/bar
178 *** runcommand st foo/bar
179 ? foo/bar
179 ? foo/bar
180
180
181 $ rm foo/bar
181 $ rm foo/bar
182
182
183
183
184 check that local configs for the cached repo aren't inherited when -R is used:
184 check that local configs for the cached repo aren't inherited when -R is used:
185
185
186 $ cat <<EOF >> .hg/hgrc
186 $ cat <<EOF >> .hg/hgrc
187 > [ui]
187 > [ui]
188 > foo = bar
188 > foo = bar
189 > EOF
189 > EOF
190
190
191 #if no-extraextensions
191 #if no-extraextensions
192
192
193 >>> from hgclient import check, readchannel, runcommand, sep
193 >>> from hgclient import check, readchannel, runcommand, sep
194 >>> @check
194 >>> @check
195 ... def localhgrc(server):
195 ... def localhgrc(server):
196 ... readchannel(server)
196 ... readchannel(server)
197 ...
197 ...
198 ... # the cached repo local hgrc contains ui.foo=bar, so showconfig should
198 ... # the cached repo local hgrc contains ui.foo=bar, so showconfig should
199 ... # show it
199 ... # show it
200 ... runcommand(server, [b'showconfig'], outfilter=sep)
200 ... runcommand(server, [b'showconfig'], outfilter=sep)
201 ...
201 ...
202 ... # but not for this repo
202 ... # but not for this repo
203 ... runcommand(server, [b'init', b'foo'])
203 ... runcommand(server, [b'init', b'foo'])
204 ... runcommand(server, [b'-R', b'foo', b'showconfig', b'ui', b'defaults'])
204 ... runcommand(server, [b'-R', b'foo', b'showconfig', b'ui', b'defaults'])
205 *** runcommand showconfig
205 *** runcommand showconfig
206 bundle.mainreporoot=$TESTTMP/repo
206 bundle.mainreporoot=$TESTTMP/repo
207 devel.all-warnings=true
207 devel.all-warnings=true
208 devel.default-date=0 0
208 devel.default-date=0 0
209 extensions.fsmonitor= (fsmonitor !)
209 extensions.fsmonitor= (fsmonitor !)
210 largefiles.usercache=$TESTTMP/.cache/largefiles
210 largefiles.usercache=$TESTTMP/.cache/largefiles
211 lfs.usercache=$TESTTMP/.cache/lfs
211 lfs.usercache=$TESTTMP/.cache/lfs
212 ui.slash=True
212 ui.slash=True
213 ui.interactive=False
213 ui.interactive=False
214 ui.merge=internal:merge
214 ui.merge=internal:merge
215 ui.mergemarkers=detailed
215 ui.mergemarkers=detailed
216 ui.foo=bar
216 ui.foo=bar
217 ui.nontty=true
217 ui.nontty=true
218 web.address=localhost
218 web.address=localhost
219 web\.ipv6=(?:True|False) (re)
219 web\.ipv6=(?:True|False) (re)
220 web.server-header=testing stub value
220 web.server-header=testing stub value
221 *** runcommand init foo
221 *** runcommand init foo
222 *** runcommand -R foo showconfig ui defaults
222 *** runcommand -R foo showconfig ui defaults
223 ui.slash=True
223 ui.slash=True
224 ui.interactive=False
224 ui.interactive=False
225 ui.merge=internal:merge
225 ui.merge=internal:merge
226 ui.mergemarkers=detailed
226 ui.mergemarkers=detailed
227 ui.nontty=true
227 ui.nontty=true
228 #endif
228 #endif
229
229
230 $ rm -R foo
230 $ rm -R foo
231
231
232 #if windows
232 #if windows
233 $ PYTHONPATH="$TESTTMP/repo;$PYTHONPATH"
233 $ PYTHONPATH="$TESTTMP/repo;$PYTHONPATH"
234 #else
234 #else
235 $ PYTHONPATH="$TESTTMP/repo:$PYTHONPATH"
235 $ PYTHONPATH="$TESTTMP/repo:$PYTHONPATH"
236 #endif
236 #endif
237
237
238 $ cat <<EOF > hook.py
238 $ cat <<EOF > hook.py
239 > import sys
239 > import sys
240 > from hgclient import bprint
240 > from hgclient import bprint
241 > def hook(**args):
241 > def hook(**args):
242 > bprint(b'hook talking')
242 > bprint(b'hook talking')
243 > bprint(b'now try to read something: %r' % sys.stdin.read())
243 > bprint(b'now try to read something: %r' % sys.stdin.read())
244 > EOF
244 > EOF
245
245
246 >>> from hgclient import check, readchannel, runcommand, stringio
246 >>> from hgclient import check, readchannel, runcommand, stringio
247 >>> @check
247 >>> @check
248 ... def hookoutput(server):
248 ... def hookoutput(server):
249 ... readchannel(server)
249 ... readchannel(server)
250 ... runcommand(server, [b'--config',
250 ... runcommand(server, [b'--config',
251 ... b'hooks.pre-identify=python:hook.hook',
251 ... b'hooks.pre-identify=python:hook.hook',
252 ... b'id'],
252 ... b'id'],
253 ... input=stringio(b'some input'))
253 ... input=stringio(b'some input'))
254 *** runcommand --config hooks.pre-identify=python:hook.hook id
254 *** runcommand --config hooks.pre-identify=python:hook.hook id
255 eff892de26ec tip
255 eff892de26ec tip
256 hook talking
256 hook talking
257 now try to read something: ''
257 now try to read something: ''
258
258
259 Clean hook cached version
259 Clean hook cached version
260 $ rm hook.py*
260 $ rm hook.py*
261 $ rm -Rf __pycache__
261 $ rm -Rf __pycache__
262
262
263 $ echo a >> a
263 $ echo a >> a
264 >>> import os
264 >>> import os
265 >>> from hgclient import check, readchannel, runcommand
265 >>> from hgclient import check, readchannel, runcommand
266 >>> @check
266 >>> @check
267 ... def outsidechanges(server):
267 ... def outsidechanges(server):
268 ... readchannel(server)
268 ... readchannel(server)
269 ... runcommand(server, [b'status'])
269 ... runcommand(server, [b'status'])
270 ... os.system('hg ci -Am2')
270 ... os.system('hg ci -Am2')
271 ... runcommand(server, [b'tip'])
271 ... runcommand(server, [b'tip'])
272 ... runcommand(server, [b'status'])
272 ... runcommand(server, [b'status'])
273 *** runcommand status
273 *** runcommand status
274 M a
274 M a
275 *** runcommand tip
275 *** runcommand tip
276 changeset: 1:d3a0a68be6de
276 changeset: 1:d3a0a68be6de
277 tag: tip
277 tag: tip
278 user: test
278 user: test
279 date: Thu Jan 01 00:00:00 1970 +0000
279 date: Thu Jan 01 00:00:00 1970 +0000
280 summary: 2
280 summary: 2
281
281
282 *** runcommand status
282 *** runcommand status
283
283
284 >>> import os
284 >>> import os
285 >>> from hgclient import bprint, check, readchannel, runcommand
285 >>> from hgclient import bprint, check, readchannel, runcommand
286 >>> @check
286 >>> @check
287 ... def bookmarks(server):
287 ... def bookmarks(server):
288 ... readchannel(server)
288 ... readchannel(server)
289 ... runcommand(server, [b'bookmarks'])
289 ... runcommand(server, [b'bookmarks'])
290 ...
290 ...
291 ... # changes .hg/bookmarks
291 ... # changes .hg/bookmarks
292 ... os.system('hg bookmark -i bm1')
292 ... os.system('hg bookmark -i bm1')
293 ... os.system('hg bookmark -i bm2')
293 ... os.system('hg bookmark -i bm2')
294 ... runcommand(server, [b'bookmarks'])
294 ... runcommand(server, [b'bookmarks'])
295 ...
295 ...
296 ... # changes .hg/bookmarks.current
296 ... # changes .hg/bookmarks.current
297 ... os.system('hg upd bm1 -q')
297 ... os.system('hg upd bm1 -q')
298 ... runcommand(server, [b'bookmarks'])
298 ... runcommand(server, [b'bookmarks'])
299 ...
299 ...
300 ... runcommand(server, [b'bookmarks', b'bm3'])
300 ... runcommand(server, [b'bookmarks', b'bm3'])
301 ... f = open('a', 'ab')
301 ... f = open('a', 'ab')
302 ... f.write(b'a\n') and None
302 ... f.write(b'a\n') and None
303 ... f.close()
303 ... f.close()
304 ... runcommand(server, [b'commit', b'-Amm'])
304 ... runcommand(server, [b'commit', b'-Amm'])
305 ... runcommand(server, [b'bookmarks'])
305 ... runcommand(server, [b'bookmarks'])
306 ... bprint(b'')
306 ... bprint(b'')
307 *** runcommand bookmarks
307 *** runcommand bookmarks
308 no bookmarks set
308 no bookmarks set
309 *** runcommand bookmarks
309 *** runcommand bookmarks
310 bm1 1:d3a0a68be6de
310 bm1 1:d3a0a68be6de
311 bm2 1:d3a0a68be6de
311 bm2 1:d3a0a68be6de
312 *** runcommand bookmarks
312 *** runcommand bookmarks
313 * bm1 1:d3a0a68be6de
313 * bm1 1:d3a0a68be6de
314 bm2 1:d3a0a68be6de
314 bm2 1:d3a0a68be6de
315 *** runcommand bookmarks bm3
315 *** runcommand bookmarks bm3
316 *** runcommand commit -Amm
316 *** runcommand commit -Amm
317 *** runcommand bookmarks
317 *** runcommand bookmarks
318 bm1 1:d3a0a68be6de
318 bm1 1:d3a0a68be6de
319 bm2 1:d3a0a68be6de
319 bm2 1:d3a0a68be6de
320 * bm3 2:aef17e88f5f0
320 * bm3 2:aef17e88f5f0
321
321
322
322
323 >>> import os
323 >>> import os
324 >>> from hgclient import check, readchannel, runcommand
324 >>> from hgclient import check, readchannel, runcommand
325 >>> @check
325 >>> @check
326 ... def tagscache(server):
326 ... def tagscache(server):
327 ... readchannel(server)
327 ... readchannel(server)
328 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
328 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
329 ... os.system('hg tag -r 0 foo')
329 ... os.system('hg tag -r 0 foo')
330 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
330 ... runcommand(server, [b'id', b'-t', b'-r', b'0'])
331 *** runcommand id -t -r 0
331 *** runcommand id -t -r 0
332
332
333 *** runcommand id -t -r 0
333 *** runcommand id -t -r 0
334 foo
334 foo
335
335
336 >>> import os
336 >>> import os
337 >>> from hgclient import check, readchannel, runcommand
337 >>> from hgclient import check, readchannel, runcommand
338 >>> @check
338 >>> @check
339 ... def setphase(server):
339 ... def setphase(server):
340 ... readchannel(server)
340 ... readchannel(server)
341 ... runcommand(server, [b'phase', b'-r', b'.'])
341 ... runcommand(server, [b'phase', b'-r', b'.'])
342 ... os.system('hg phase -r . -p')
342 ... os.system('hg phase -r . -p')
343 ... runcommand(server, [b'phase', b'-r', b'.'])
343 ... runcommand(server, [b'phase', b'-r', b'.'])
344 *** runcommand phase -r .
344 *** runcommand phase -r .
345 3: draft
345 3: draft
346 *** runcommand phase -r .
346 *** runcommand phase -r .
347 3: public
347 3: public
348
348
349 $ echo a >> a
349 $ echo a >> a
350 >>> from hgclient import bprint, check, readchannel, runcommand
350 >>> from hgclient import bprint, check, readchannel, runcommand
351 >>> @check
351 >>> @check
352 ... def rollback(server):
352 ... def rollback(server):
353 ... readchannel(server)
353 ... readchannel(server)
354 ... runcommand(server, [b'phase', b'-r', b'.', b'-p'])
354 ... runcommand(server, [b'phase', b'-r', b'.', b'-p'])
355 ... runcommand(server, [b'commit', b'-Am.'])
355 ... runcommand(server, [b'commit', b'-Am.'])
356 ... runcommand(server, [b'rollback'])
356 ... runcommand(server, [b'rollback'])
357 ... runcommand(server, [b'phase', b'-r', b'.'])
357 ... runcommand(server, [b'phase', b'-r', b'.'])
358 ... bprint(b'')
358 ... bprint(b'')
359 *** runcommand phase -r . -p
359 *** runcommand phase -r . -p
360 no phases changed
360 no phases changed
361 *** runcommand commit -Am.
361 *** runcommand commit -Am.
362 *** runcommand rollback
362 *** runcommand rollback
363 repository tip rolled back to revision 3 (undo commit)
363 repository tip rolled back to revision 3 (undo commit)
364 working directory now based on revision 3
364 working directory now based on revision 3
365 *** runcommand phase -r .
365 *** runcommand phase -r .
366 3: public
366 3: public
367
367
368
368
369 >>> import os
369 >>> import os
370 >>> from hgclient import check, readchannel, runcommand
370 >>> from hgclient import check, readchannel, runcommand
371 >>> @check
371 >>> @check
372 ... def branch(server):
372 ... def branch(server):
373 ... readchannel(server)
373 ... readchannel(server)
374 ... runcommand(server, [b'branch'])
374 ... runcommand(server, [b'branch'])
375 ... os.system('hg branch foo')
375 ... os.system('hg branch foo')
376 ... runcommand(server, [b'branch'])
376 ... runcommand(server, [b'branch'])
377 ... os.system('hg branch default')
377 ... os.system('hg branch default')
378 *** runcommand branch
378 *** runcommand branch
379 default
379 default
380 marked working directory as branch foo
380 marked working directory as branch foo
381 (branches are permanent and global, did you want a bookmark?)
381 (branches are permanent and global, did you want a bookmark?)
382 *** runcommand branch
382 *** runcommand branch
383 foo
383 foo
384 marked working directory as branch default
384 marked working directory as branch default
385 (branches are permanent and global, did you want a bookmark?)
385 (branches are permanent and global, did you want a bookmark?)
386
386
387 $ touch .hgignore
387 $ touch .hgignore
388 >>> import os
388 >>> import os
389 >>> from hgclient import bprint, check, readchannel, runcommand
389 >>> from hgclient import bprint, check, readchannel, runcommand
390 >>> @check
390 >>> @check
391 ... def hgignore(server):
391 ... def hgignore(server):
392 ... readchannel(server)
392 ... readchannel(server)
393 ... runcommand(server, [b'commit', b'-Am.'])
393 ... runcommand(server, [b'commit', b'-Am.'])
394 ... f = open('ignored-file', 'ab')
394 ... f = open('ignored-file', 'ab')
395 ... f.write(b'') and None
395 ... f.write(b'') and None
396 ... f.close()
396 ... f.close()
397 ... f = open('.hgignore', 'ab')
397 ... f = open('.hgignore', 'ab')
398 ... f.write(b'ignored-file')
398 ... f.write(b'ignored-file')
399 ... f.close()
399 ... f.close()
400 ... runcommand(server, [b'status', b'-i', b'-u'])
400 ... runcommand(server, [b'status', b'-i', b'-u'])
401 ... bprint(b'')
401 ... bprint(b'')
402 *** runcommand commit -Am.
402 *** runcommand commit -Am.
403 adding .hgignore
403 adding .hgignore
404 *** runcommand status -i -u
404 *** runcommand status -i -u
405 I ignored-file
405 I ignored-file
406
406
407
407
408 cache of non-public revisions should be invalidated on repository change
408 cache of non-public revisions should be invalidated on repository change
409 (issue4855):
409 (issue4855):
410
410
411 >>> import os
411 >>> import os
412 >>> from hgclient import bprint, check, readchannel, runcommand
412 >>> from hgclient import bprint, check, readchannel, runcommand
413 >>> @check
413 >>> @check
414 ... def phasesetscacheaftercommit(server):
414 ... def phasesetscacheaftercommit(server):
415 ... readchannel(server)
415 ... readchannel(server)
416 ... # load _phasecache._phaserevs and _phasesets
416 ... # load _phasecache._phaserevs and _phasesets
417 ... runcommand(server, [b'log', b'-qr', b'draft()'])
417 ... runcommand(server, [b'log', b'-qr', b'draft()'])
418 ... # create draft commits by another process
418 ... # create draft commits by another process
419 ... for i in range(5, 7):
419 ... for i in range(5, 7):
420 ... f = open('a', 'ab')
420 ... f = open('a', 'ab')
421 ... f.seek(0, os.SEEK_END)
421 ... f.seek(0, os.SEEK_END)
422 ... f.write(b'a\n') and None
422 ... f.write(b'a\n') and None
423 ... f.close()
423 ... f.close()
424 ... os.system('hg commit -Aqm%d' % i)
424 ... os.system('hg commit -Aqm%d' % i)
425 ... # new commits should be listed as draft revisions
425 ... # new commits should be listed as draft revisions
426 ... runcommand(server, [b'log', b'-qr', b'draft()'])
426 ... runcommand(server, [b'log', b'-qr', b'draft()'])
427 ... bprint(b'')
427 ... bprint(b'')
428 *** runcommand log -qr draft()
428 *** runcommand log -qr draft()
429 4:7966c8e3734d
429 4:7966c8e3734d
430 *** runcommand log -qr draft()
430 *** runcommand log -qr draft()
431 4:7966c8e3734d
431 4:7966c8e3734d
432 5:41f6602d1c4f
432 5:41f6602d1c4f
433 6:10501e202c35
433 6:10501e202c35
434
434
435
435
436 >>> import os
436 >>> import os
437 >>> from hgclient import bprint, check, readchannel, runcommand
437 >>> from hgclient import bprint, check, readchannel, runcommand
438 >>> @check
438 >>> @check
439 ... def phasesetscacheafterstrip(server):
439 ... def phasesetscacheafterstrip(server):
440 ... readchannel(server)
440 ... readchannel(server)
441 ... # load _phasecache._phaserevs and _phasesets
441 ... # load _phasecache._phaserevs and _phasesets
442 ... runcommand(server, [b'log', b'-qr', b'draft()'])
442 ... runcommand(server, [b'log', b'-qr', b'draft()'])
443 ... # strip cached revisions by another process
443 ... # strip cached revisions by another process
444 ... os.system('hg --config extensions.strip= strip -q 5')
444 ... os.system('hg --config extensions.strip= strip -q 5')
445 ... # shouldn't abort by "unknown revision '6'"
445 ... # shouldn't abort by "unknown revision '6'"
446 ... runcommand(server, [b'log', b'-qr', b'draft()'])
446 ... runcommand(server, [b'log', b'-qr', b'draft()'])
447 ... bprint(b'')
447 ... bprint(b'')
448 *** runcommand log -qr draft()
448 *** runcommand log -qr draft()
449 4:7966c8e3734d
449 4:7966c8e3734d
450 5:41f6602d1c4f
450 5:41f6602d1c4f
451 6:10501e202c35
451 6:10501e202c35
452 *** runcommand log -qr draft()
452 *** runcommand log -qr draft()
453 4:7966c8e3734d
453 4:7966c8e3734d
454
454
455
455
456 cache of phase roots should be invalidated on strip (issue3827):
456 cache of phase roots should be invalidated on strip (issue3827):
457
457
458 >>> import os
458 >>> import os
459 >>> from hgclient import check, readchannel, runcommand, sep
459 >>> from hgclient import check, readchannel, runcommand, sep
460 >>> @check
460 >>> @check
461 ... def phasecacheafterstrip(server):
461 ... def phasecacheafterstrip(server):
462 ... readchannel(server)
462 ... readchannel(server)
463 ...
463 ...
464 ... # create new head, 5:731265503d86
464 ... # create new head, 5:731265503d86
465 ... runcommand(server, [b'update', b'-C', b'0'])
465 ... runcommand(server, [b'update', b'-C', b'0'])
466 ... f = open('a', 'ab')
466 ... f = open('a', 'ab')
467 ... f.write(b'a\n') and None
467 ... f.write(b'a\n') and None
468 ... f.close()
468 ... f.close()
469 ... runcommand(server, [b'commit', b'-Am.', b'a'])
469 ... runcommand(server, [b'commit', b'-Am.', b'a'])
470 ... runcommand(server, [b'log', b'-Gq'])
470 ... runcommand(server, [b'log', b'-Gq'])
471 ...
471 ...
472 ... # make it public; draft marker moves to 4:7966c8e3734d
472 ... # make it public; draft marker moves to 4:7966c8e3734d
473 ... runcommand(server, [b'phase', b'-p', b'.'])
473 ... runcommand(server, [b'phase', b'-p', b'.'])
474 ... # load _phasecache.phaseroots
474 ... # load _phasecache.phaseroots
475 ... runcommand(server, [b'phase', b'.'], outfilter=sep)
475 ... runcommand(server, [b'phase', b'.'], outfilter=sep)
476 ...
476 ...
477 ... # strip 1::4 outside server
477 ... # strip 1::4 outside server
478 ... os.system('hg -q --config extensions.mq= strip 1')
478 ... os.system('hg -q --config extensions.mq= strip 1')
479 ...
479 ...
480 ... # shouldn't raise "7966c8e3734d: no node!"
480 ... # shouldn't raise "7966c8e3734d: no node!"
481 ... runcommand(server, [b'branches'])
481 ... runcommand(server, [b'branches'])
482 *** runcommand update -C 0
482 *** runcommand update -C 0
483 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
483 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
484 (leaving bookmark bm3)
484 (leaving bookmark bm3)
485 *** runcommand commit -Am. a
485 *** runcommand commit -Am. a
486 created new head
486 created new head
487 *** runcommand log -Gq
487 *** runcommand log -Gq
488 @ 5:731265503d86
488 @ 5:731265503d86
489 |
489 |
490 | o 4:7966c8e3734d
490 | o 4:7966c8e3734d
491 | |
491 | |
492 | o 3:b9b85890c400
492 | o 3:b9b85890c400
493 | |
493 | |
494 | o 2:aef17e88f5f0
494 | o 2:aef17e88f5f0
495 | |
495 | |
496 | o 1:d3a0a68be6de
496 | o 1:d3a0a68be6de
497 |/
497 |/
498 o 0:eff892de26ec
498 o 0:eff892de26ec
499
499
500 *** runcommand phase -p .
500 *** runcommand phase -p .
501 *** runcommand phase .
501 *** runcommand phase .
502 5: public
502 5: public
503 *** runcommand branches
503 *** runcommand branches
504 default 1:731265503d86
504 default 1:731265503d86
505
505
506 in-memory cache must be reloaded if transaction is aborted. otherwise
506 in-memory cache must be reloaded if transaction is aborted. otherwise
507 changelog and manifest would have invalid node:
507 changelog and manifest would have invalid node:
508
508
509 $ echo a >> a
509 $ echo a >> a
510 >>> from hgclient import check, readchannel, runcommand
510 >>> from hgclient import check, readchannel, runcommand
511 >>> @check
511 >>> @check
512 ... def txabort(server):
512 ... def txabort(server):
513 ... readchannel(server)
513 ... readchannel(server)
514 ... runcommand(server, [b'commit', b'--config', b'hooks.pretxncommit=false',
514 ... runcommand(server, [b'commit', b'--config', b'hooks.pretxncommit=false',
515 ... b'-mfoo'])
515 ... b'-mfoo'])
516 ... runcommand(server, [b'verify'])
516 ... runcommand(server, [b'verify'])
517 *** runcommand commit --config hooks.pretxncommit=false -mfoo
517 *** runcommand commit --config hooks.pretxncommit=false -mfoo
518 transaction abort!
518 transaction abort!
519 rollback completed
519 rollback completed
520 abort: pretxncommit hook exited with status 1
520 abort: pretxncommit hook exited with status 1
521 [255]
521 [255]
522 *** runcommand verify
522 *** runcommand verify
523 checking changesets
523 checking changesets
524 checking manifests
524 checking manifests
525 crosschecking files in changesets and manifests
525 crosschecking files in changesets and manifests
526 checking files
526 checking files
527 checked 2 changesets with 2 changes to 1 files
527 checked 2 changesets with 2 changes to 1 files
528 $ hg revert --no-backup -aq
528 $ hg revert --no-backup -aq
529
529
530 $ cat >> .hg/hgrc << EOF
530 $ cat >> .hg/hgrc << EOF
531 > [experimental]
531 > [experimental]
532 > evolution.createmarkers=True
532 > evolution.createmarkers=True
533 > EOF
533 > EOF
534
534
535 >>> import os
535 >>> import os
536 >>> from hgclient import check, readchannel, runcommand
536 >>> from hgclient import check, readchannel, runcommand
537 >>> @check
537 >>> @check
538 ... def obsolete(server):
538 ... def obsolete(server):
539 ... readchannel(server)
539 ... readchannel(server)
540 ...
540 ...
541 ... runcommand(server, [b'up', b'null'])
541 ... runcommand(server, [b'up', b'null'])
542 ... runcommand(server, [b'phase', b'-df', b'tip'])
542 ... runcommand(server, [b'phase', b'-df', b'tip'])
543 ... cmd = 'hg debugobsolete `hg log -r tip --template {node}`'
543 ... cmd = 'hg debugobsolete `hg log -r tip --template {node}`'
544 ... if os.name == 'nt':
544 ... if os.name == 'nt':
545 ... cmd = 'sh -c "%s"' % cmd # run in sh, not cmd.exe
545 ... cmd = 'sh -c "%s"' % cmd # run in sh, not cmd.exe
546 ... os.system(cmd)
546 ... os.system(cmd)
547 ... runcommand(server, [b'log', b'--hidden'])
547 ... runcommand(server, [b'log', b'--hidden'])
548 ... runcommand(server, [b'log'])
548 ... runcommand(server, [b'log'])
549 *** runcommand up null
549 *** runcommand up null
550 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
550 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
551 *** runcommand phase -df tip
551 *** runcommand phase -df tip
552 obsoleted 1 changesets
552 obsoleted 1 changesets
553 *** runcommand log --hidden
553 *** runcommand log --hidden
554 changeset: 1:731265503d86
554 changeset: 1:731265503d86
555 tag: tip
555 tag: tip
556 user: test
556 user: test
557 date: Thu Jan 01 00:00:00 1970 +0000
557 date: Thu Jan 01 00:00:00 1970 +0000
558 obsolete: pruned
558 obsolete: pruned
559 summary: .
559 summary: .
560
560
561 changeset: 0:eff892de26ec
561 changeset: 0:eff892de26ec
562 bookmark: bm1
562 bookmark: bm1
563 bookmark: bm2
563 bookmark: bm2
564 bookmark: bm3
564 bookmark: bm3
565 user: test
565 user: test
566 date: Thu Jan 01 00:00:00 1970 +0000
566 date: Thu Jan 01 00:00:00 1970 +0000
567 summary: 1
567 summary: 1
568
568
569 *** runcommand log
569 *** runcommand log
570 changeset: 0:eff892de26ec
570 changeset: 0:eff892de26ec
571 bookmark: bm1
571 bookmark: bm1
572 bookmark: bm2
572 bookmark: bm2
573 bookmark: bm3
573 bookmark: bm3
574 tag: tip
574 tag: tip
575 user: test
575 user: test
576 date: Thu Jan 01 00:00:00 1970 +0000
576 date: Thu Jan 01 00:00:00 1970 +0000
577 summary: 1
577 summary: 1
578
578
579
579
580 $ cat <<EOF >> .hg/hgrc
580 $ cat <<EOF >> .hg/hgrc
581 > [extensions]
581 > [extensions]
582 > mq =
582 > mq =
583 > EOF
583 > EOF
584
584
585 >>> import os
585 >>> import os
586 >>> from hgclient import check, readchannel, runcommand
586 >>> from hgclient import check, readchannel, runcommand
587 >>> @check
587 >>> @check
588 ... def mqoutsidechanges(server):
588 ... def mqoutsidechanges(server):
589 ... readchannel(server)
589 ... readchannel(server)
590 ...
590 ...
591 ... # load repo.mq
591 ... # load repo.mq
592 ... runcommand(server, [b'qapplied'])
592 ... runcommand(server, [b'qapplied'])
593 ... os.system('hg qnew 0.diff')
593 ... os.system('hg qnew 0.diff')
594 ... # repo.mq should be invalidated
594 ... # repo.mq should be invalidated
595 ... runcommand(server, [b'qapplied'])
595 ... runcommand(server, [b'qapplied'])
596 ...
596 ...
597 ... runcommand(server, [b'qpop', b'--all'])
597 ... runcommand(server, [b'qpop', b'--all'])
598 ... os.system('hg qqueue --create foo')
598 ... os.system('hg qqueue --create foo')
599 ... # repo.mq should be recreated to point to new queue
599 ... # repo.mq should be recreated to point to new queue
600 ... runcommand(server, [b'qqueue', b'--active'])
600 ... runcommand(server, [b'qqueue', b'--active'])
601 *** runcommand qapplied
601 *** runcommand qapplied
602 *** runcommand qapplied
602 *** runcommand qapplied
603 0.diff
603 0.diff
604 *** runcommand qpop --all
604 *** runcommand qpop --all
605 popping 0.diff
605 popping 0.diff
606 patch queue now empty
606 patch queue now empty
607 *** runcommand qqueue --active
607 *** runcommand qqueue --active
608 foo
608 foo
609
609
610 $ cat <<'EOF' > ../dbgui.py
610 $ cat <<'EOF' > ../dbgui.py
611 > import os
611 > import os
612 > import sys
612 > import sys
613 > from mercurial import commands, registrar
613 > from mercurial import commands, registrar
614 > cmdtable = {}
614 > cmdtable = {}
615 > command = registrar.command(cmdtable)
615 > command = registrar.command(cmdtable)
616 > @command(b"debuggetpass", norepo=True)
616 > @command(b"debuggetpass", norepo=True)
617 > def debuggetpass(ui):
617 > def debuggetpass(ui):
618 > ui.write(b"%s\n" % ui.getpass())
618 > ui.write(b"%s\n" % ui.getpass())
619 > @command(b"debugprompt", norepo=True)
619 > @command(b"debugprompt", norepo=True)
620 > def debugprompt(ui):
620 > def debugprompt(ui):
621 > ui.write(b"%s\n" % ui.prompt(b"prompt:"))
621 > ui.write(b"%s\n" % ui.prompt(b"prompt:"))
622 > @command(b"debugpromptchoice", norepo=True)
622 > @command(b"debugpromptchoice", norepo=True)
623 > def debugpromptchoice(ui):
623 > def debugpromptchoice(ui):
624 > msg = b"promptchoice (y/n)? $$ &Yes $$ &No"
624 > msg = b"promptchoice (y/n)? $$ &Yes $$ &No"
625 > ui.write(b"%d\n" % ui.promptchoice(msg))
625 > ui.write(b"%d\n" % ui.promptchoice(msg))
626 > @command(b"debugreadstdin", norepo=True)
626 > @command(b"debugreadstdin", norepo=True)
627 > def debugreadstdin(ui):
627 > def debugreadstdin(ui):
628 > ui.write(b"read: %r\n" % sys.stdin.read(1))
628 > ui.write(b"read: %r\n" % sys.stdin.read(1))
629 > @command(b"debugwritestdout", norepo=True)
629 > @command(b"debugwritestdout", norepo=True)
630 > def debugwritestdout(ui):
630 > def debugwritestdout(ui):
631 > os.write(1, b"low-level stdout fd and\n")
631 > os.write(1, b"low-level stdout fd and\n")
632 > sys.stdout.write("stdout should be redirected to stderr\n")
632 > sys.stdout.write("stdout should be redirected to stderr\n")
633 > sys.stdout.flush()
633 > sys.stdout.flush()
634 > EOF
634 > EOF
635 $ cat <<EOF >> .hg/hgrc
635 $ cat <<EOF >> .hg/hgrc
636 > [extensions]
636 > [extensions]
637 > dbgui = ../dbgui.py
637 > dbgui = ../dbgui.py
638 > EOF
638 > EOF
639
639
640 >>> from hgclient import check, readchannel, runcommand, stringio
640 >>> from hgclient import check, readchannel, runcommand, stringio
641 >>> @check
641 >>> @check
642 ... def getpass(server):
642 ... def getpass(server):
643 ... readchannel(server)
643 ... readchannel(server)
644 ... runcommand(server, [b'debuggetpass', b'--config',
644 ... runcommand(server, [b'debuggetpass', b'--config',
645 ... b'ui.interactive=True'],
645 ... b'ui.interactive=True'],
646 ... input=stringio(b'1234\n'))
646 ... input=stringio(b'1234\n'))
647 ... runcommand(server, [b'debuggetpass', b'--config',
647 ... runcommand(server, [b'debuggetpass', b'--config',
648 ... b'ui.interactive=True'],
648 ... b'ui.interactive=True'],
649 ... input=stringio(b'\n'))
649 ... input=stringio(b'\n'))
650 ... runcommand(server, [b'debuggetpass', b'--config',
650 ... runcommand(server, [b'debuggetpass', b'--config',
651 ... b'ui.interactive=True'],
651 ... b'ui.interactive=True'],
652 ... input=stringio(b''))
652 ... input=stringio(b''))
653 ... runcommand(server, [b'debugprompt', b'--config',
653 ... runcommand(server, [b'debugprompt', b'--config',
654 ... b'ui.interactive=True'],
654 ... b'ui.interactive=True'],
655 ... input=stringio(b'5678\n'))
655 ... input=stringio(b'5678\n'))
656 ... runcommand(server, [b'debugreadstdin'])
656 ... runcommand(server, [b'debugreadstdin'])
657 ... runcommand(server, [b'debugwritestdout'])
657 ... runcommand(server, [b'debugwritestdout'])
658 *** runcommand debuggetpass --config ui.interactive=True
658 *** runcommand debuggetpass --config ui.interactive=True
659 password: 1234
659 password: 1234
660 *** runcommand debuggetpass --config ui.interactive=True
660 *** runcommand debuggetpass --config ui.interactive=True
661 password:
661 password:
662 *** runcommand debuggetpass --config ui.interactive=True
662 *** runcommand debuggetpass --config ui.interactive=True
663 password: abort: response expected
663 password: abort: response expected
664 [255]
664 [255]
665 *** runcommand debugprompt --config ui.interactive=True
665 *** runcommand debugprompt --config ui.interactive=True
666 prompt: 5678
666 prompt: 5678
667 *** runcommand debugreadstdin
667 *** runcommand debugreadstdin
668 read: ''
668 read: ''
669 *** runcommand debugwritestdout
669 *** runcommand debugwritestdout
670 low-level stdout fd and
670 low-level stdout fd and
671 stdout should be redirected to stderr
671 stdout should be redirected to stderr
672
672
673
673
674 run commandserver in commandserver, which is silly but should work:
674 run commandserver in commandserver, which is silly but should work:
675
675
676 >>> from hgclient import bprint, check, readchannel, runcommand, stringio
676 >>> from hgclient import bprint, check, readchannel, runcommand, stringio
677 >>> @check
677 >>> @check
678 ... def nested(server):
678 ... def nested(server):
679 ... bprint(b'%c, %r' % readchannel(server))
679 ... bprint(b'%c, %r' % readchannel(server))
680 ... class nestedserver(object):
680 ... class nestedserver(object):
681 ... stdin = stringio(b'getencoding\n')
681 ... stdin = stringio(b'getencoding\n')
682 ... stdout = stringio()
682 ... stdout = stringio()
683 ... runcommand(server, [b'serve', b'--cmdserver', b'pipe'],
683 ... runcommand(server, [b'serve', b'--cmdserver', b'pipe'],
684 ... output=nestedserver.stdout, input=nestedserver.stdin)
684 ... output=nestedserver.stdout, input=nestedserver.stdin)
685 ... nestedserver.stdout.seek(0)
685 ... nestedserver.stdout.seek(0)
686 ... bprint(b'%c, %r' % readchannel(nestedserver)) # hello
686 ... bprint(b'%c, %r' % readchannel(nestedserver)) # hello
687 ... bprint(b'%c, %r' % readchannel(nestedserver)) # getencoding
687 ... bprint(b'%c, %r' % readchannel(nestedserver)) # getencoding
688 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
688 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
689 *** runcommand serve --cmdserver pipe
689 *** runcommand serve --cmdserver pipe
690 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
690 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
691 r, '*' (glob)
691 r, '*' (glob)
692
692
693
693
694 start without repository:
694 start without repository:
695
695
696 $ cd ..
696 $ cd ..
697
697
698 >>> from hgclient import bprint, check, readchannel, runcommand
698 >>> from hgclient import bprint, check, readchannel, runcommand
699 >>> @check
699 >>> @check
700 ... def hellomessage(server):
700 ... def hellomessage(server):
701 ... ch, data = readchannel(server)
701 ... ch, data = readchannel(server)
702 ... bprint(b'%c, %r' % (ch, data))
702 ... bprint(b'%c, %r' % (ch, data))
703 ... # run an arbitrary command to make sure the next thing the server
703 ... # run an arbitrary command to make sure the next thing the server
704 ... # sends isn't part of the hello message
704 ... # sends isn't part of the hello message
705 ... runcommand(server, [b'id'])
705 ... runcommand(server, [b'id'])
706 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
706 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
707 *** runcommand id
707 *** runcommand id
708 abort: there is no Mercurial repository here (.hg not found)
708 abort: there is no Mercurial repository here (.hg not found)
709 [255]
709 [255]
710
710
711 >>> from hgclient import check, readchannel, runcommand
711 >>> from hgclient import check, readchannel, runcommand
712 >>> @check
712 >>> @check
713 ... def startwithoutrepo(server):
713 ... def startwithoutrepo(server):
714 ... readchannel(server)
714 ... readchannel(server)
715 ... runcommand(server, [b'init', b'repo2'])
715 ... runcommand(server, [b'init', b'repo2'])
716 ... runcommand(server, [b'id', b'-R', b'repo2'])
716 ... runcommand(server, [b'id', b'-R', b'repo2'])
717 *** runcommand init repo2
717 *** runcommand init repo2
718 *** runcommand id -R repo2
718 *** runcommand id -R repo2
719 000000000000 tip
719 000000000000 tip
720
720
721
721
722 don't fall back to cwd if invalid -R path is specified (issue4805):
722 don't fall back to cwd if invalid -R path is specified (issue4805):
723
723
724 $ cd repo
724 $ cd repo
725 $ hg serve --cmdserver pipe -R ../nonexistent
725 $ hg serve --cmdserver pipe -R ../nonexistent
726 abort: repository ../nonexistent not found!
726 abort: repository ../nonexistent not found!
727 [255]
727 [255]
728 $ cd ..
728 $ cd ..
729
729
730
730
731 structured message channel:
731 structured message channel:
732
732
733 $ cat <<'EOF' >> repo2/.hg/hgrc
733 $ cat <<'EOF' >> repo2/.hg/hgrc
734 > [ui]
734 > [ui]
735 > # server --config should precede repository option
735 > # server --config should precede repository option
736 > message-output = stdio
736 > message-output = stdio
737 > EOF
737 > EOF
738
738
739 >>> from hgclient import bprint, checkwith, readchannel, runcommand
739 >>> from hgclient import bprint, checkwith, readchannel, runcommand
740 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
740 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
741 ... b'--config', b'cmdserver.message-encodings=foo cbor'])
741 ... b'--config', b'cmdserver.message-encodings=foo cbor'])
742 ... def verify(server):
742 ... def verify(server):
743 ... _ch, data = readchannel(server)
743 ... _ch, data = readchannel(server)
744 ... bprint(data)
744 ... bprint(data)
745 ... runcommand(server, [b'-R', b'repo2', b'verify'])
745 ... runcommand(server, [b'-R', b'repo2', b'verify'])
746 capabilities: getencoding runcommand
746 capabilities: getencoding runcommand
747 encoding: ascii
747 encoding: ascii
748 message-encoding: cbor
748 message-encoding: cbor
749 pid: * (glob)
749 pid: * (glob)
750 pgid: * (glob) (no-windows !)
750 pgid: * (glob) (no-windows !)
751 *** runcommand -R repo2 verify
751 *** runcommand -R repo2 verify
752 message: '\xa2DdataTchecking changesets\nDtypeFstatus'
752 message: '\xa2DdataTchecking changesets\nDtypeFstatus'
753 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
753 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
754 message: '\xa2DdataSchecking manifests\nDtypeFstatus'
754 message: '\xa2DdataSchecking manifests\nDtypeFstatus'
755 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
755 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
756 message: '\xa2DdataX0crosschecking files in changesets and manifests\nDtypeFstatus'
756 message: '\xa2DdataX0crosschecking files in changesets and manifests\nDtypeFstatus'
757 message: '\xa6Ditem@Cpos\xf6EtopicMcrosscheckingEtotal\xf6DtypeHprogressDunit@'
757 message: '\xa6Ditem@Cpos\xf6EtopicMcrosscheckingEtotal\xf6DtypeHprogressDunit@'
758 message: '\xa2DdataOchecking files\nDtypeFstatus'
758 message: '\xa2DdataOchecking files\nDtypeFstatus'
759 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
759 message: '\xa6Ditem@Cpos\xf6EtopicHcheckingEtotal\xf6DtypeHprogressDunit@'
760 message: '\xa2DdataX/checked 0 changesets with 0 changes to 0 files\nDtypeFstatus'
760 message: '\xa2DdataX/checked 0 changesets with 0 changes to 0 files\nDtypeFstatus'
761
761
762 >>> from hgclient import checkwith, readchannel, runcommand, stringio
762 >>> from hgclient import checkwith, readchannel, runcommand, stringio
763 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
763 >>> @checkwith(extraargs=[b'--config', b'ui.message-output=channel',
764 ... b'--config', b'cmdserver.message-encodings=cbor',
764 ... b'--config', b'cmdserver.message-encodings=cbor',
765 ... b'--config', b'extensions.dbgui=dbgui.py'])
765 ... b'--config', b'extensions.dbgui=dbgui.py'])
766 ... def prompt(server):
766 ... def prompt(server):
767 ... readchannel(server)
767 ... readchannel(server)
768 ... interactive = [b'--config', b'ui.interactive=True']
768 ... interactive = [b'--config', b'ui.interactive=True']
769 ... runcommand(server, [b'debuggetpass'] + interactive,
769 ... runcommand(server, [b'debuggetpass'] + interactive,
770 ... input=stringio(b'1234\n'))
770 ... input=stringio(b'1234\n'))
771 ... runcommand(server, [b'debugprompt'] + interactive,
771 ... runcommand(server, [b'debugprompt'] + interactive,
772 ... input=stringio(b'5678\n'))
772 ... input=stringio(b'5678\n'))
773 ... runcommand(server, [b'debugpromptchoice'] + interactive,
773 ... runcommand(server, [b'debugpromptchoice'] + interactive,
774 ... input=stringio(b'n\n'))
774 ... input=stringio(b'n\n'))
775 *** runcommand debuggetpass --config ui.interactive=True
775 *** runcommand debuggetpass --config ui.interactive=True
776 message: '\xa3DdataJpassword: Hpassword\xf5DtypeFprompt'
776 message: '\xa3DdataJpassword: Hpassword\xf5DtypeFprompt'
777 1234
777 1234
778 *** runcommand debugprompt --config ui.interactive=True
778 *** runcommand debugprompt --config ui.interactive=True
779 message: '\xa3DdataGprompt:GdefaultAyDtypeFprompt'
779 message: '\xa3DdataGprompt:GdefaultAyDtypeFprompt'
780 5678
780 5678
781 *** runcommand debugpromptchoice --config ui.interactive=True
781 *** runcommand debugpromptchoice --config ui.interactive=True
782 message: '\xa4Gchoices\x82\x82AyCYes\x82AnBNoDdataTpromptchoice (y/n)? GdefaultAyDtypeFprompt'
782 message: '\xa4Gchoices\x82\x82AyCYes\x82AnBNoDdataTpromptchoice (y/n)? GdefaultAyDtypeFprompt'
783 1
783 1
784
784
785 bad message encoding:
785 bad message encoding:
786
786
787 $ hg serve --cmdserver pipe --config ui.message-output=channel
787 $ hg serve --cmdserver pipe --config ui.message-output=channel
788 abort: no supported message encodings:
788 abort: no supported message encodings:
789 [255]
789 [255]
790 $ hg serve --cmdserver pipe --config ui.message-output=channel \
790 $ hg serve --cmdserver pipe --config ui.message-output=channel \
791 > --config cmdserver.message-encodings='foo bar'
791 > --config cmdserver.message-encodings='foo bar'
792 abort: no supported message encodings: foo bar
792 abort: no supported message encodings: foo bar
793 [255]
793 [255]
794
794
795 unix domain socket:
795 unix domain socket:
796
796
797 $ cd repo
797 $ cd repo
798 $ hg update -q
798 $ hg update -q
799
799
800 #if unix-socket unix-permissions
800 #if unix-socket unix-permissions
801
801
802 >>> from hgclient import bprint, check, readchannel, runcommand, stringio, unixserver
802 >>> from hgclient import bprint, check, readchannel, runcommand, stringio, unixserver
803 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
803 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
804 >>> def hellomessage(conn):
804 >>> def hellomessage(conn):
805 ... ch, data = readchannel(conn)
805 ... ch, data = readchannel(conn)
806 ... bprint(b'%c, %r' % (ch, data))
806 ... bprint(b'%c, %r' % (ch, data))
807 ... runcommand(conn, [b'id'])
807 ... runcommand(conn, [b'id'])
808 >>> check(hellomessage, server.connect)
808 >>> check(hellomessage, server.connect)
809 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
809 o, 'capabilities: getencoding runcommand\nencoding: *\npid: *' (glob)
810 *** runcommand id
810 *** runcommand id
811 eff892de26ec tip bm1/bm2/bm3
811 eff892de26ec tip bm1/bm2/bm3
812 >>> def unknowncommand(conn):
812 >>> def unknowncommand(conn):
813 ... readchannel(conn)
813 ... readchannel(conn)
814 ... conn.stdin.write(b'unknowncommand\n')
814 ... conn.stdin.write(b'unknowncommand\n')
815 >>> check(unknowncommand, server.connect) # error sent to server.log
815 >>> check(unknowncommand, server.connect) # error sent to server.log
816 >>> def serverinput(conn):
816 >>> def serverinput(conn):
817 ... readchannel(conn)
817 ... readchannel(conn)
818 ... patch = b"""
818 ... patch = b"""
819 ... # HG changeset patch
819 ... # HG changeset patch
820 ... # User test
820 ... # User test
821 ... # Date 0 0
821 ... # Date 0 0
822 ... 2
822 ... 2
823 ...
823 ...
824 ... diff -r eff892de26ec -r 1ed24be7e7a0 a
824 ... diff -r eff892de26ec -r 1ed24be7e7a0 a
825 ... --- a/a
825 ... --- a/a
826 ... +++ b/a
826 ... +++ b/a
827 ... @@ -1,1 +1,2 @@
827 ... @@ -1,1 +1,2 @@
828 ... 1
828 ... 1
829 ... +2
829 ... +2
830 ... """
830 ... """
831 ... runcommand(conn, [b'import', b'-'], input=stringio(patch))
831 ... runcommand(conn, [b'import', b'-'], input=stringio(patch))
832 ... runcommand(conn, [b'log', b'-rtip', b'-q'])
832 ... runcommand(conn, [b'log', b'-rtip', b'-q'])
833 >>> check(serverinput, server.connect)
833 >>> check(serverinput, server.connect)
834 *** runcommand import -
834 *** runcommand import -
835 applying patch from stdin
835 applying patch from stdin
836 *** runcommand log -rtip -q
836 *** runcommand log -rtip -q
837 2:1ed24be7e7a0
837 2:1ed24be7e7a0
838 >>> server.shutdown()
838 >>> server.shutdown()
839
839
840 $ cat .hg/server.log
840 $ cat .hg/server.log
841 listening at .hg/server.sock
841 listening at .hg/server.sock
842 abort: unknown command unknowncommand
842 abort: unknown command unknowncommand
843 killed!
843 killed!
844 $ rm .hg/server.log
844 $ rm .hg/server.log
845
845
846 if server crashed before hello, traceback will be sent to 'e' channel as
846 if server crashed before hello, traceback will be sent to 'e' channel as
847 last ditch:
847 last ditch:
848
848
849 $ cat <<'EOF' > ../earlycrasher.py
849 $ cat <<'EOF' > ../earlycrasher.py
850 > from mercurial import commandserver, extensions
850 > from mercurial import commandserver, extensions
851 > def _serverequest(orig, ui, repo, conn, createcmdserver):
851 > def _serverequest(orig, ui, repo, conn, createcmdserver, prereposetups):
852 > def createcmdserver(*args, **kwargs):
852 > def createcmdserver(*args, **kwargs):
853 > raise Exception('crash')
853 > raise Exception('crash')
854 > return orig(ui, repo, conn, createcmdserver)
854 > return orig(ui, repo, conn, createcmdserver, prereposetups)
855 > def extsetup(ui):
855 > def extsetup(ui):
856 > extensions.wrapfunction(commandserver, b'_serverequest', _serverequest)
856 > extensions.wrapfunction(commandserver, b'_serverequest', _serverequest)
857 > EOF
857 > EOF
858 $ cat <<EOF >> .hg/hgrc
858 $ cat <<EOF >> .hg/hgrc
859 > [extensions]
859 > [extensions]
860 > earlycrasher = ../earlycrasher.py
860 > earlycrasher = ../earlycrasher.py
861 > EOF
861 > EOF
862 >>> from hgclient import bprint, check, readchannel, unixserver
862 >>> from hgclient import bprint, check, readchannel, unixserver
863 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
863 >>> server = unixserver(b'.hg/server.sock', b'.hg/server.log')
864 >>> def earlycrash(conn):
864 >>> def earlycrash(conn):
865 ... while True:
865 ... while True:
866 ... try:
866 ... try:
867 ... ch, data = readchannel(conn)
867 ... ch, data = readchannel(conn)
868 ... for l in data.splitlines(True):
868 ... for l in data.splitlines(True):
869 ... if not l.startswith(b' '):
869 ... if not l.startswith(b' '):
870 ... bprint(b'%c, %r' % (ch, l))
870 ... bprint(b'%c, %r' % (ch, l))
871 ... except EOFError:
871 ... except EOFError:
872 ... break
872 ... break
873 >>> check(earlycrash, server.connect)
873 >>> check(earlycrash, server.connect)
874 e, 'Traceback (most recent call last):\n'
874 e, 'Traceback (most recent call last):\n'
875 e, 'Exception: crash\n'
875 e, 'Exception: crash\n'
876 >>> server.shutdown()
876 >>> server.shutdown()
877
877
878 $ cat .hg/server.log | grep -v '^ '
878 $ cat .hg/server.log | grep -v '^ '
879 listening at .hg/server.sock
879 listening at .hg/server.sock
880 Traceback (most recent call last):
880 Traceback (most recent call last):
881 Exception: crash
881 Exception: crash
882 killed!
882 killed!
883 #endif
883 #endif
884 #if no-unix-socket
884 #if no-unix-socket
885
885
886 $ hg serve --cmdserver unix -a .hg/server.sock
886 $ hg serve --cmdserver unix -a .hg/server.sock
887 abort: unsupported platform
887 abort: unsupported platform
888 [255]
888 [255]
889
889
890 #endif
890 #endif
891
891
892 $ cd ..
892 $ cd ..
893
893
894 Test that accessing to invalid changelog cache is avoided at
894 Test that accessing to invalid changelog cache is avoided at
895 subsequent operations even if repo object is reused even after failure
895 subsequent operations even if repo object is reused even after failure
896 of transaction (see 0a7610758c42 also)
896 of transaction (see 0a7610758c42 also)
897
897
898 "hg log" after failure of transaction is needed to detect invalid
898 "hg log" after failure of transaction is needed to detect invalid
899 cache in repoview: this can't detect by "hg verify" only.
899 cache in repoview: this can't detect by "hg verify" only.
900
900
901 Combination of "finalization" and "empty-ness of changelog" (2 x 2 =
901 Combination of "finalization" and "empty-ness of changelog" (2 x 2 =
902 4) are tested, because '00changelog.i' are differently changed in each
902 4) are tested, because '00changelog.i' are differently changed in each
903 cases.
903 cases.
904
904
905 $ cat > $TESTTMP/failafterfinalize.py <<EOF
905 $ cat > $TESTTMP/failafterfinalize.py <<EOF
906 > # extension to abort transaction after finalization forcibly
906 > # extension to abort transaction after finalization forcibly
907 > from mercurial import commands, error, extensions, lock as lockmod
907 > from mercurial import commands, error, extensions, lock as lockmod
908 > from mercurial import registrar
908 > from mercurial import registrar
909 > cmdtable = {}
909 > cmdtable = {}
910 > command = registrar.command(cmdtable)
910 > command = registrar.command(cmdtable)
911 > configtable = {}
911 > configtable = {}
912 > configitem = registrar.configitem(configtable)
912 > configitem = registrar.configitem(configtable)
913 > configitem(b'failafterfinalize', b'fail',
913 > configitem(b'failafterfinalize', b'fail',
914 > default=None,
914 > default=None,
915 > )
915 > )
916 > def fail(tr):
916 > def fail(tr):
917 > raise error.Abort(b'fail after finalization')
917 > raise error.Abort(b'fail after finalization')
918 > def reposetup(ui, repo):
918 > def reposetup(ui, repo):
919 > class failrepo(repo.__class__):
919 > class failrepo(repo.__class__):
920 > def commitctx(self, ctx, error=False):
920 > def commitctx(self, ctx, error=False):
921 > if self.ui.configbool(b'failafterfinalize', b'fail'):
921 > if self.ui.configbool(b'failafterfinalize', b'fail'):
922 > # 'sorted()' by ASCII code on category names causes
922 > # 'sorted()' by ASCII code on category names causes
923 > # invoking 'fail' after finalization of changelog
923 > # invoking 'fail' after finalization of changelog
924 > # using "'cl-%i' % id(self)" as category name
924 > # using "'cl-%i' % id(self)" as category name
925 > self.currenttransaction().addfinalize(b'zzzzzzzz', fail)
925 > self.currenttransaction().addfinalize(b'zzzzzzzz', fail)
926 > return super(failrepo, self).commitctx(ctx, error)
926 > return super(failrepo, self).commitctx(ctx, error)
927 > repo.__class__ = failrepo
927 > repo.__class__ = failrepo
928 > EOF
928 > EOF
929
929
930 $ hg init repo3
930 $ hg init repo3
931 $ cd repo3
931 $ cd repo3
932
932
933 $ cat <<EOF >> $HGRCPATH
933 $ cat <<EOF >> $HGRCPATH
934 > [ui]
934 > [ui]
935 > logtemplate = {rev} {desc|firstline} ({files})\n
935 > logtemplate = {rev} {desc|firstline} ({files})\n
936 >
936 >
937 > [extensions]
937 > [extensions]
938 > failafterfinalize = $TESTTMP/failafterfinalize.py
938 > failafterfinalize = $TESTTMP/failafterfinalize.py
939 > EOF
939 > EOF
940
940
941 - test failure with "empty changelog"
941 - test failure with "empty changelog"
942
942
943 $ echo foo > foo
943 $ echo foo > foo
944 $ hg add foo
944 $ hg add foo
945
945
946 (failure before finalization)
946 (failure before finalization)
947
947
948 >>> from hgclient import check, readchannel, runcommand
948 >>> from hgclient import check, readchannel, runcommand
949 >>> @check
949 >>> @check
950 ... def abort(server):
950 ... def abort(server):
951 ... readchannel(server)
951 ... readchannel(server)
952 ... runcommand(server, [b'commit',
952 ... runcommand(server, [b'commit',
953 ... b'--config', b'hooks.pretxncommit=false',
953 ... b'--config', b'hooks.pretxncommit=false',
954 ... b'-mfoo'])
954 ... b'-mfoo'])
955 ... runcommand(server, [b'log'])
955 ... runcommand(server, [b'log'])
956 ... runcommand(server, [b'verify', b'-q'])
956 ... runcommand(server, [b'verify', b'-q'])
957 *** runcommand commit --config hooks.pretxncommit=false -mfoo
957 *** runcommand commit --config hooks.pretxncommit=false -mfoo
958 transaction abort!
958 transaction abort!
959 rollback completed
959 rollback completed
960 abort: pretxncommit hook exited with status 1
960 abort: pretxncommit hook exited with status 1
961 [255]
961 [255]
962 *** runcommand log
962 *** runcommand log
963 *** runcommand verify -q
963 *** runcommand verify -q
964
964
965 (failure after finalization)
965 (failure after finalization)
966
966
967 >>> from hgclient import check, readchannel, runcommand
967 >>> from hgclient import check, readchannel, runcommand
968 >>> @check
968 >>> @check
969 ... def abort(server):
969 ... def abort(server):
970 ... readchannel(server)
970 ... readchannel(server)
971 ... runcommand(server, [b'commit',
971 ... runcommand(server, [b'commit',
972 ... b'--config', b'failafterfinalize.fail=true',
972 ... b'--config', b'failafterfinalize.fail=true',
973 ... b'-mfoo'])
973 ... b'-mfoo'])
974 ... runcommand(server, [b'log'])
974 ... runcommand(server, [b'log'])
975 ... runcommand(server, [b'verify', b'-q'])
975 ... runcommand(server, [b'verify', b'-q'])
976 *** runcommand commit --config failafterfinalize.fail=true -mfoo
976 *** runcommand commit --config failafterfinalize.fail=true -mfoo
977 transaction abort!
977 transaction abort!
978 rollback completed
978 rollback completed
979 abort: fail after finalization
979 abort: fail after finalization
980 [255]
980 [255]
981 *** runcommand log
981 *** runcommand log
982 *** runcommand verify -q
982 *** runcommand verify -q
983
983
984 - test failure with "not-empty changelog"
984 - test failure with "not-empty changelog"
985
985
986 $ echo bar > bar
986 $ echo bar > bar
987 $ hg add bar
987 $ hg add bar
988 $ hg commit -mbar bar
988 $ hg commit -mbar bar
989
989
990 (failure before finalization)
990 (failure before finalization)
991
991
992 >>> from hgclient import check, readchannel, runcommand
992 >>> from hgclient import check, readchannel, runcommand
993 >>> @check
993 >>> @check
994 ... def abort(server):
994 ... def abort(server):
995 ... readchannel(server)
995 ... readchannel(server)
996 ... runcommand(server, [b'commit',
996 ... runcommand(server, [b'commit',
997 ... b'--config', b'hooks.pretxncommit=false',
997 ... b'--config', b'hooks.pretxncommit=false',
998 ... b'-mfoo', b'foo'])
998 ... b'-mfoo', b'foo'])
999 ... runcommand(server, [b'log'])
999 ... runcommand(server, [b'log'])
1000 ... runcommand(server, [b'verify', b'-q'])
1000 ... runcommand(server, [b'verify', b'-q'])
1001 *** runcommand commit --config hooks.pretxncommit=false -mfoo foo
1001 *** runcommand commit --config hooks.pretxncommit=false -mfoo foo
1002 transaction abort!
1002 transaction abort!
1003 rollback completed
1003 rollback completed
1004 abort: pretxncommit hook exited with status 1
1004 abort: pretxncommit hook exited with status 1
1005 [255]
1005 [255]
1006 *** runcommand log
1006 *** runcommand log
1007 0 bar (bar)
1007 0 bar (bar)
1008 *** runcommand verify -q
1008 *** runcommand verify -q
1009
1009
1010 (failure after finalization)
1010 (failure after finalization)
1011
1011
1012 >>> from hgclient import check, readchannel, runcommand
1012 >>> from hgclient import check, readchannel, runcommand
1013 >>> @check
1013 >>> @check
1014 ... def abort(server):
1014 ... def abort(server):
1015 ... readchannel(server)
1015 ... readchannel(server)
1016 ... runcommand(server, [b'commit',
1016 ... runcommand(server, [b'commit',
1017 ... b'--config', b'failafterfinalize.fail=true',
1017 ... b'--config', b'failafterfinalize.fail=true',
1018 ... b'-mfoo', b'foo'])
1018 ... b'-mfoo', b'foo'])
1019 ... runcommand(server, [b'log'])
1019 ... runcommand(server, [b'log'])
1020 ... runcommand(server, [b'verify', b'-q'])
1020 ... runcommand(server, [b'verify', b'-q'])
1021 *** runcommand commit --config failafterfinalize.fail=true -mfoo foo
1021 *** runcommand commit --config failafterfinalize.fail=true -mfoo foo
1022 transaction abort!
1022 transaction abort!
1023 rollback completed
1023 rollback completed
1024 abort: fail after finalization
1024 abort: fail after finalization
1025 [255]
1025 [255]
1026 *** runcommand log
1026 *** runcommand log
1027 0 bar (bar)
1027 0 bar (bar)
1028 *** runcommand verify -q
1028 *** runcommand verify -q
1029
1029
1030 $ cd ..
1030 $ cd ..
1031
1031
1032 Test symlink traversal over cached audited paths:
1032 Test symlink traversal over cached audited paths:
1033 -------------------------------------------------
1033 -------------------------------------------------
1034
1034
1035 #if symlink
1035 #if symlink
1036
1036
1037 set up symlink hell
1037 set up symlink hell
1038
1038
1039 $ mkdir merge-symlink-out
1039 $ mkdir merge-symlink-out
1040 $ hg init merge-symlink
1040 $ hg init merge-symlink
1041 $ cd merge-symlink
1041 $ cd merge-symlink
1042 $ touch base
1042 $ touch base
1043 $ hg commit -qAm base
1043 $ hg commit -qAm base
1044 $ ln -s ../merge-symlink-out a
1044 $ ln -s ../merge-symlink-out a
1045 $ hg commit -qAm 'symlink a -> ../merge-symlink-out'
1045 $ hg commit -qAm 'symlink a -> ../merge-symlink-out'
1046 $ hg up -q 0
1046 $ hg up -q 0
1047 $ mkdir a
1047 $ mkdir a
1048 $ touch a/poisoned
1048 $ touch a/poisoned
1049 $ hg commit -qAm 'file a/poisoned'
1049 $ hg commit -qAm 'file a/poisoned'
1050 $ hg log -G -T '{rev}: {desc}\n'
1050 $ hg log -G -T '{rev}: {desc}\n'
1051 @ 2: file a/poisoned
1051 @ 2: file a/poisoned
1052 |
1052 |
1053 | o 1: symlink a -> ../merge-symlink-out
1053 | o 1: symlink a -> ../merge-symlink-out
1054 |/
1054 |/
1055 o 0: base
1055 o 0: base
1056
1056
1057
1057
1058 try trivial merge after update: cache of audited paths should be discarded,
1058 try trivial merge after update: cache of audited paths should be discarded,
1059 and the merge should fail (issue5628)
1059 and the merge should fail (issue5628)
1060
1060
1061 $ hg up -q null
1061 $ hg up -q null
1062 >>> from hgclient import check, readchannel, runcommand
1062 >>> from hgclient import check, readchannel, runcommand
1063 >>> @check
1063 >>> @check
1064 ... def merge(server):
1064 ... def merge(server):
1065 ... readchannel(server)
1065 ... readchannel(server)
1066 ... # audit a/poisoned as a good path
1066 ... # audit a/poisoned as a good path
1067 ... runcommand(server, [b'up', b'-qC', b'2'])
1067 ... runcommand(server, [b'up', b'-qC', b'2'])
1068 ... runcommand(server, [b'up', b'-qC', b'1'])
1068 ... runcommand(server, [b'up', b'-qC', b'1'])
1069 ... # here a is a symlink, so a/poisoned is bad
1069 ... # here a is a symlink, so a/poisoned is bad
1070 ... runcommand(server, [b'merge', b'2'])
1070 ... runcommand(server, [b'merge', b'2'])
1071 *** runcommand up -qC 2
1071 *** runcommand up -qC 2
1072 *** runcommand up -qC 1
1072 *** runcommand up -qC 1
1073 *** runcommand merge 2
1073 *** runcommand merge 2
1074 abort: path 'a/poisoned' traverses symbolic link 'a'
1074 abort: path 'a/poisoned' traverses symbolic link 'a'
1075 [255]
1075 [255]
1076 $ ls ../merge-symlink-out
1076 $ ls ../merge-symlink-out
1077
1077
1078 cache of repo.auditor should be discarded, so matcher would never traverse
1078 cache of repo.auditor should be discarded, so matcher would never traverse
1079 symlinks:
1079 symlinks:
1080
1080
1081 $ hg up -qC 0
1081 $ hg up -qC 0
1082 $ touch ../merge-symlink-out/poisoned
1082 $ touch ../merge-symlink-out/poisoned
1083 >>> from hgclient import check, readchannel, runcommand
1083 >>> from hgclient import check, readchannel, runcommand
1084 >>> @check
1084 >>> @check
1085 ... def files(server):
1085 ... def files(server):
1086 ... readchannel(server)
1086 ... readchannel(server)
1087 ... runcommand(server, [b'up', b'-qC', b'2'])
1087 ... runcommand(server, [b'up', b'-qC', b'2'])
1088 ... # audit a/poisoned as a good path
1088 ... # audit a/poisoned as a good path
1089 ... runcommand(server, [b'files', b'a/poisoned'])
1089 ... runcommand(server, [b'files', b'a/poisoned'])
1090 ... runcommand(server, [b'up', b'-qC', b'0'])
1090 ... runcommand(server, [b'up', b'-qC', b'0'])
1091 ... runcommand(server, [b'up', b'-qC', b'1'])
1091 ... runcommand(server, [b'up', b'-qC', b'1'])
1092 ... # here 'a' is a symlink, so a/poisoned should be warned
1092 ... # here 'a' is a symlink, so a/poisoned should be warned
1093 ... runcommand(server, [b'files', b'a/poisoned'])
1093 ... runcommand(server, [b'files', b'a/poisoned'])
1094 *** runcommand up -qC 2
1094 *** runcommand up -qC 2
1095 *** runcommand files a/poisoned
1095 *** runcommand files a/poisoned
1096 a/poisoned
1096 a/poisoned
1097 *** runcommand up -qC 0
1097 *** runcommand up -qC 0
1098 *** runcommand up -qC 1
1098 *** runcommand up -qC 1
1099 *** runcommand files a/poisoned
1099 *** runcommand files a/poisoned
1100 abort: path 'a/poisoned' traverses symbolic link 'a'
1100 abort: path 'a/poisoned' traverses symbolic link 'a'
1101 [255]
1101 [255]
1102
1102
1103 $ cd ..
1103 $ cd ..
1104
1104
1105 #endif
1105 #endif
General Comments 0
You need to be logged in to leave comments. Login now