##// END OF EJS Templates
commandserver: handle IOError related to flushing of streams...
Pulkit Goyal -
r46702:ac9de799 default
parent child Browse files
Show More
@@ -1,771 +1,782 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
21
22 selectors.BaseSelector
22 selectors.BaseSelector
23 except ImportError:
23 except ImportError:
24 from .thirdparty import selectors2 as selectors
24 from .thirdparty import selectors2 as selectors
25
25
26 from .i18n import _
26 from .i18n import _
27 from .pycompat import getattr
27 from .pycompat import getattr
28 from . import (
28 from . import (
29 encoding,
29 encoding,
30 error,
30 error,
31 loggingutil,
31 loggingutil,
32 pycompat,
32 pycompat,
33 repocache,
33 repocache,
34 util,
34 util,
35 vfs as vfsmod,
35 vfs as vfsmod,
36 )
36 )
37 from .utils import (
37 from .utils import (
38 cborutil,
38 cborutil,
39 procutil,
39 procutil,
40 )
40 )
41
41
42
42
43 class channeledoutput(object):
43 class channeledoutput(object):
44 """
44 """
45 Write data to out in the following format:
45 Write data to out in the following format:
46
46
47 data length (unsigned int),
47 data length (unsigned int),
48 data
48 data
49 """
49 """
50
50
51 def __init__(self, out, channel):
51 def __init__(self, out, channel):
52 self.out = out
52 self.out = out
53 self.channel = channel
53 self.channel = channel
54
54
55 @property
55 @property
56 def name(self):
56 def name(self):
57 return b'<%c-channel>' % self.channel
57 return b'<%c-channel>' % self.channel
58
58
59 def write(self, data):
59 def write(self, data):
60 if not data:
60 if not data:
61 return
61 return
62 # single write() to guarantee the same atomicity as the underlying file
62 # single write() to guarantee the same atomicity as the underlying file
63 self.out.write(struct.pack(b'>cI', self.channel, len(data)) + data)
63 self.out.write(struct.pack(b'>cI', self.channel, len(data)) + data)
64 self.out.flush()
64 self.out.flush()
65
65
66 def __getattr__(self, attr):
66 def __getattr__(self, attr):
67 if attr in ('isatty', 'fileno', 'tell', 'seek'):
67 if attr in ('isatty', 'fileno', 'tell', 'seek'):
68 raise AttributeError(attr)
68 raise AttributeError(attr)
69 return getattr(self.out, attr)
69 return getattr(self.out, attr)
70
70
71
71
72 class channeledmessage(object):
72 class channeledmessage(object):
73 """
73 """
74 Write encoded message and metadata to out in the following format:
74 Write encoded message and metadata to out in the following format:
75
75
76 data length (unsigned int),
76 data length (unsigned int),
77 encoded message and metadata, as a flat key-value dict.
77 encoded message and metadata, as a flat key-value dict.
78
78
79 Each message should have 'type' attribute. Messages of unknown type
79 Each message should have 'type' attribute. Messages of unknown type
80 should be ignored.
80 should be ignored.
81 """
81 """
82
82
83 # teach ui that write() can take **opts
83 # teach ui that write() can take **opts
84 structured = True
84 structured = True
85
85
86 def __init__(self, out, channel, encodename, encodefn):
86 def __init__(self, out, channel, encodename, encodefn):
87 self._cout = channeledoutput(out, channel)
87 self._cout = channeledoutput(out, channel)
88 self.encoding = encodename
88 self.encoding = encodename
89 self._encodefn = encodefn
89 self._encodefn = encodefn
90
90
91 def write(self, data, **opts):
91 def write(self, data, **opts):
92 opts = pycompat.byteskwargs(opts)
92 opts = pycompat.byteskwargs(opts)
93 if data is not None:
93 if data is not None:
94 opts[b'data'] = data
94 opts[b'data'] = data
95 self._cout.write(self._encodefn(opts))
95 self._cout.write(self._encodefn(opts))
96
96
97 def __getattr__(self, attr):
97 def __getattr__(self, attr):
98 return getattr(self._cout, attr)
98 return getattr(self._cout, attr)
99
99
100
100
101 class channeledinput(object):
101 class channeledinput(object):
102 """
102 """
103 Read data from in_.
103 Read data from in_.
104
104
105 Requests for input are written to out in the following format:
105 Requests for input are written to out in the following format:
106 channel identifier - 'I' for plain input, 'L' line based (1 byte)
106 channel identifier - 'I' for plain input, 'L' line based (1 byte)
107 how many bytes to send at most (unsigned int),
107 how many bytes to send at most (unsigned int),
108
108
109 The client replies with:
109 The client replies with:
110 data length (unsigned int), 0 meaning EOF
110 data length (unsigned int), 0 meaning EOF
111 data
111 data
112 """
112 """
113
113
114 maxchunksize = 4 * 1024
114 maxchunksize = 4 * 1024
115
115
116 def __init__(self, in_, out, channel):
116 def __init__(self, in_, out, channel):
117 self.in_ = in_
117 self.in_ = in_
118 self.out = out
118 self.out = out
119 self.channel = channel
119 self.channel = channel
120
120
121 @property
121 @property
122 def name(self):
122 def name(self):
123 return b'<%c-channel>' % self.channel
123 return b'<%c-channel>' % self.channel
124
124
125 def read(self, size=-1):
125 def read(self, size=-1):
126 if size < 0:
126 if size < 0:
127 # if we need to consume all the clients input, ask for 4k chunks
127 # if we need to consume all the clients input, ask for 4k chunks
128 # so the pipe doesn't fill up risking a deadlock
128 # so the pipe doesn't fill up risking a deadlock
129 size = self.maxchunksize
129 size = self.maxchunksize
130 s = self._read(size, self.channel)
130 s = self._read(size, self.channel)
131 buf = s
131 buf = s
132 while s:
132 while s:
133 s = self._read(size, self.channel)
133 s = self._read(size, self.channel)
134 buf += s
134 buf += s
135
135
136 return buf
136 return buf
137 else:
137 else:
138 return self._read(size, self.channel)
138 return self._read(size, self.channel)
139
139
140 def _read(self, size, channel):
140 def _read(self, size, channel):
141 if not size:
141 if not size:
142 return b''
142 return b''
143 assert size > 0
143 assert size > 0
144
144
145 # tell the client we need at most size bytes
145 # tell the client we need at most size bytes
146 self.out.write(struct.pack(b'>cI', channel, size))
146 self.out.write(struct.pack(b'>cI', channel, size))
147 self.out.flush()
147 self.out.flush()
148
148
149 length = self.in_.read(4)
149 length = self.in_.read(4)
150 length = struct.unpack(b'>I', length)[0]
150 length = struct.unpack(b'>I', length)[0]
151 if not length:
151 if not length:
152 return b''
152 return b''
153 else:
153 else:
154 return self.in_.read(length)
154 return self.in_.read(length)
155
155
156 def readline(self, size=-1):
156 def readline(self, size=-1):
157 if size < 0:
157 if size < 0:
158 size = self.maxchunksize
158 size = self.maxchunksize
159 s = self._read(size, b'L')
159 s = self._read(size, b'L')
160 buf = s
160 buf = s
161 # keep asking for more until there's either no more or
161 # keep asking for more until there's either no more or
162 # we got a full line
162 # we got a full line
163 while s and not s.endswith(b'\n'):
163 while s and not s.endswith(b'\n'):
164 s = self._read(size, b'L')
164 s = self._read(size, b'L')
165 buf += s
165 buf += s
166
166
167 return buf
167 return buf
168 else:
168 else:
169 return self._read(size, b'L')
169 return self._read(size, b'L')
170
170
171 def __iter__(self):
171 def __iter__(self):
172 return self
172 return self
173
173
174 def next(self):
174 def next(self):
175 l = self.readline()
175 l = self.readline()
176 if not l:
176 if not l:
177 raise StopIteration
177 raise StopIteration
178 return l
178 return l
179
179
180 __next__ = next
180 __next__ = next
181
181
182 def __getattr__(self, attr):
182 def __getattr__(self, attr):
183 if attr in ('isatty', 'fileno', 'tell', 'seek'):
183 if attr in ('isatty', 'fileno', 'tell', 'seek'):
184 raise AttributeError(attr)
184 raise AttributeError(attr)
185 return getattr(self.in_, attr)
185 return getattr(self.in_, attr)
186
186
187
187
188 _messageencoders = {
188 _messageencoders = {
189 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
189 b'cbor': lambda v: b''.join(cborutil.streamencode(v)),
190 }
190 }
191
191
192
192
193 def _selectmessageencoder(ui):
193 def _selectmessageencoder(ui):
194 encnames = ui.configlist(b'cmdserver', b'message-encodings')
194 encnames = ui.configlist(b'cmdserver', b'message-encodings')
195 for n in encnames:
195 for n in encnames:
196 f = _messageencoders.get(n)
196 f = _messageencoders.get(n)
197 if f:
197 if f:
198 return n, f
198 return n, f
199 raise error.Abort(
199 raise error.Abort(
200 b'no supported message encodings: %s' % b' '.join(encnames)
200 b'no supported message encodings: %s' % b' '.join(encnames)
201 )
201 )
202
202
203
203
204 class server(object):
204 class server(object):
205 """
205 """
206 Listens for commands on fin, runs them and writes the output on a channel
206 Listens for commands on fin, runs them and writes the output on a channel
207 based stream to fout.
207 based stream to fout.
208 """
208 """
209
209
210 def __init__(self, ui, repo, fin, fout, prereposetups=None):
210 def __init__(self, ui, repo, fin, fout, prereposetups=None):
211 self.cwd = encoding.getcwd()
211 self.cwd = encoding.getcwd()
212
212
213 if repo:
213 if repo:
214 # the ui here is really the repo ui so take its baseui so we don't
214 # the ui here is really the repo ui so take its baseui so we don't
215 # end up with its local configuration
215 # end up with its local configuration
216 self.ui = repo.baseui
216 self.ui = repo.baseui
217 self.repo = repo
217 self.repo = repo
218 self.repoui = repo.ui
218 self.repoui = repo.ui
219 else:
219 else:
220 self.ui = ui
220 self.ui = ui
221 self.repo = self.repoui = None
221 self.repo = self.repoui = None
222 self._prereposetups = prereposetups
222 self._prereposetups = prereposetups
223
223
224 self.cdebug = channeledoutput(fout, b'd')
224 self.cdebug = channeledoutput(fout, b'd')
225 self.cerr = channeledoutput(fout, b'e')
225 self.cerr = channeledoutput(fout, b'e')
226 self.cout = channeledoutput(fout, b'o')
226 self.cout = channeledoutput(fout, b'o')
227 self.cin = channeledinput(fin, fout, b'I')
227 self.cin = channeledinput(fin, fout, b'I')
228 self.cresult = channeledoutput(fout, b'r')
228 self.cresult = channeledoutput(fout, b'r')
229
229
230 if self.ui.config(b'cmdserver', b'log') == b'-':
230 if self.ui.config(b'cmdserver', b'log') == b'-':
231 # switch log stream of server's ui to the 'd' (debug) channel
231 # switch log stream of server's ui to the 'd' (debug) channel
232 # (don't touch repo.ui as its lifetime is longer than the server)
232 # (don't touch repo.ui as its lifetime is longer than the server)
233 self.ui = self.ui.copy()
233 self.ui = self.ui.copy()
234 setuplogging(self.ui, repo=None, fp=self.cdebug)
234 setuplogging(self.ui, repo=None, fp=self.cdebug)
235
235
236 self.cmsg = None
236 self.cmsg = None
237 if ui.config(b'ui', b'message-output') == b'channel':
237 if ui.config(b'ui', b'message-output') == b'channel':
238 encname, encfn = _selectmessageencoder(ui)
238 encname, encfn = _selectmessageencoder(ui)
239 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
239 self.cmsg = channeledmessage(fout, b'm', encname, encfn)
240
240
241 self.client = fin
241 self.client = fin
242
242
243 # If shutdown-on-interrupt is off, the default SIGINT handler is
243 # If shutdown-on-interrupt is off, the default SIGINT handler is
244 # removed so that client-server communication wouldn't be interrupted.
244 # removed so that client-server communication wouldn't be interrupted.
245 # For example, 'runcommand' handler will issue three short read()s.
245 # For example, 'runcommand' handler will issue three short read()s.
246 # If one of the first two read()s were interrupted, the communication
246 # If one of the first two read()s were interrupted, the communication
247 # channel would be left at dirty state and the subsequent request
247 # channel would be left at dirty state and the subsequent request
248 # wouldn't be parsed. So catching KeyboardInterrupt isn't enough.
248 # wouldn't be parsed. So catching KeyboardInterrupt isn't enough.
249 self._shutdown_on_interrupt = ui.configbool(
249 self._shutdown_on_interrupt = ui.configbool(
250 b'cmdserver', b'shutdown-on-interrupt'
250 b'cmdserver', b'shutdown-on-interrupt'
251 )
251 )
252 self._old_inthandler = None
252 self._old_inthandler = None
253 if not self._shutdown_on_interrupt:
253 if not self._shutdown_on_interrupt:
254 self._old_inthandler = signal.signal(signal.SIGINT, signal.SIG_IGN)
254 self._old_inthandler = signal.signal(signal.SIGINT, signal.SIG_IGN)
255
255
256 def cleanup(self):
256 def cleanup(self):
257 """release and restore resources taken during server session"""
257 """release and restore resources taken during server session"""
258 if not self._shutdown_on_interrupt:
258 if not self._shutdown_on_interrupt:
259 signal.signal(signal.SIGINT, self._old_inthandler)
259 signal.signal(signal.SIGINT, self._old_inthandler)
260
260
261 def _read(self, size):
261 def _read(self, size):
262 if not size:
262 if not size:
263 return b''
263 return b''
264
264
265 data = self.client.read(size)
265 data = self.client.read(size)
266
266
267 # is the other end closed?
267 # is the other end closed?
268 if not data:
268 if not data:
269 raise EOFError
269 raise EOFError
270
270
271 return data
271 return data
272
272
273 def _readstr(self):
273 def _readstr(self):
274 """read a string from the channel
274 """read a string from the channel
275
275
276 format:
276 format:
277 data length (uint32), data
277 data length (uint32), data
278 """
278 """
279 length = struct.unpack(b'>I', self._read(4))[0]
279 length = struct.unpack(b'>I', self._read(4))[0]
280 if not length:
280 if not length:
281 return b''
281 return b''
282 return self._read(length)
282 return self._read(length)
283
283
284 def _readlist(self):
284 def _readlist(self):
285 """read a list of NULL separated strings from the channel"""
285 """read a list of NULL separated strings from the channel"""
286 s = self._readstr()
286 s = self._readstr()
287 if s:
287 if s:
288 return s.split(b'\0')
288 return s.split(b'\0')
289 else:
289 else:
290 return []
290 return []
291
291
292 def _dispatchcommand(self, req):
292 def _dispatchcommand(self, req):
293 from . import dispatch # avoid cycle
293 from . import dispatch # avoid cycle
294
294
295 if self._shutdown_on_interrupt:
295 if self._shutdown_on_interrupt:
296 # no need to restore SIGINT handler as it is unmodified.
296 # no need to restore SIGINT handler as it is unmodified.
297 return dispatch.dispatch(req)
297 return dispatch.dispatch(req)
298
298
299 try:
299 try:
300 signal.signal(signal.SIGINT, self._old_inthandler)
300 signal.signal(signal.SIGINT, self._old_inthandler)
301 return dispatch.dispatch(req)
301 return dispatch.dispatch(req)
302 except error.SignalInterrupt:
302 except error.SignalInterrupt:
303 # propagate SIGBREAK, SIGHUP, or SIGTERM.
303 # propagate SIGBREAK, SIGHUP, or SIGTERM.
304 raise
304 raise
305 except KeyboardInterrupt:
305 except KeyboardInterrupt:
306 # SIGINT may be received out of the try-except block of dispatch(),
306 # SIGINT may be received out of the try-except block of dispatch(),
307 # so catch it as last ditch. Another KeyboardInterrupt may be
307 # so catch it as last ditch. Another KeyboardInterrupt may be
308 # raised while handling exceptions here, but there's no way to
308 # raised while handling exceptions here, but there's no way to
309 # avoid that except for doing everything in C.
309 # avoid that except for doing everything in C.
310 pass
310 pass
311 finally:
311 finally:
312 signal.signal(signal.SIGINT, signal.SIG_IGN)
312 signal.signal(signal.SIGINT, signal.SIG_IGN)
313 # On KeyboardInterrupt, print error message and exit *after* SIGINT
313 # On KeyboardInterrupt, print error message and exit *after* SIGINT
314 # handler removed.
314 # handler removed.
315 req.ui.error(_(b'interrupted!\n'))
315 req.ui.error(_(b'interrupted!\n'))
316 return -1
316 return -1
317
317
318 def runcommand(self):
318 def runcommand(self):
319 """reads a list of \0 terminated arguments, executes
319 """reads a list of \0 terminated arguments, executes
320 and writes the return code to the result channel"""
320 and writes the return code to the result channel"""
321 from . import dispatch # avoid cycle
321 from . import dispatch # avoid cycle
322
322
323 args = self._readlist()
323 args = self._readlist()
324
324
325 # copy the uis so changes (e.g. --config or --verbose) don't
325 # copy the uis so changes (e.g. --config or --verbose) don't
326 # persist between requests
326 # persist between requests
327 copiedui = self.ui.copy()
327 copiedui = self.ui.copy()
328 uis = [copiedui]
328 uis = [copiedui]
329 if self.repo:
329 if self.repo:
330 self.repo.baseui = copiedui
330 self.repo.baseui = copiedui
331 # clone ui without using ui.copy because this is protected
331 # clone ui without using ui.copy because this is protected
332 repoui = self.repoui.__class__(self.repoui)
332 repoui = self.repoui.__class__(self.repoui)
333 repoui.copy = copiedui.copy # redo copy protection
333 repoui.copy = copiedui.copy # redo copy protection
334 uis.append(repoui)
334 uis.append(repoui)
335 self.repo.ui = self.repo.dirstate._ui = repoui
335 self.repo.ui = self.repo.dirstate._ui = repoui
336 self.repo.invalidateall()
336 self.repo.invalidateall()
337
337
338 for ui in uis:
338 for ui in uis:
339 ui.resetstate()
339 ui.resetstate()
340 # any kind of interaction must use server channels, but chg may
340 # any kind of interaction must use server channels, but chg may
341 # replace channels by fully functional tty files. so nontty is
341 # replace channels by fully functional tty files. so nontty is
342 # enforced only if cin is a channel.
342 # enforced only if cin is a channel.
343 if not util.safehasattr(self.cin, b'fileno'):
343 if not util.safehasattr(self.cin, b'fileno'):
344 ui.setconfig(b'ui', b'nontty', b'true', b'commandserver')
344 ui.setconfig(b'ui', b'nontty', b'true', b'commandserver')
345
345
346 req = dispatch.request(
346 req = dispatch.request(
347 args[:],
347 args[:],
348 copiedui,
348 copiedui,
349 self.repo,
349 self.repo,
350 self.cin,
350 self.cin,
351 self.cout,
351 self.cout,
352 self.cerr,
352 self.cerr,
353 self.cmsg,
353 self.cmsg,
354 prereposetups=self._prereposetups,
354 prereposetups=self._prereposetups,
355 )
355 )
356
356
357 try:
357 try:
358 ret = self._dispatchcommand(req) & 255
358 err = None
359 try:
360 status = self._dispatchcommand(req)
361 except error.StdioError as e:
362 status = -1
363 err = e
364
365 retval = dispatch.closestdio(req.ui, err)
366 if retval:
367 status = retval
368
369 ret = status & 255
359 # If shutdown-on-interrupt is off, it's important to write the
370 # If shutdown-on-interrupt is off, it's important to write the
360 # result code *after* SIGINT handler removed. If the result code
371 # result code *after* SIGINT handler removed. If the result code
361 # were lost, the client wouldn't be able to continue processing.
372 # were lost, the client wouldn't be able to continue processing.
362 self.cresult.write(struct.pack(b'>i', int(ret)))
373 self.cresult.write(struct.pack(b'>i', int(ret)))
363 finally:
374 finally:
364 # restore old cwd
375 # restore old cwd
365 if b'--cwd' in args:
376 if b'--cwd' in args:
366 os.chdir(self.cwd)
377 os.chdir(self.cwd)
367
378
368 def getencoding(self):
379 def getencoding(self):
369 """ writes the current encoding to the result channel """
380 """ writes the current encoding to the result channel """
370 self.cresult.write(encoding.encoding)
381 self.cresult.write(encoding.encoding)
371
382
372 def serveone(self):
383 def serveone(self):
373 cmd = self.client.readline()[:-1]
384 cmd = self.client.readline()[:-1]
374 if cmd:
385 if cmd:
375 handler = self.capabilities.get(cmd)
386 handler = self.capabilities.get(cmd)
376 if handler:
387 if handler:
377 handler(self)
388 handler(self)
378 else:
389 else:
379 # clients are expected to check what commands are supported by
390 # clients are expected to check what commands are supported by
380 # looking at the servers capabilities
391 # looking at the servers capabilities
381 raise error.Abort(_(b'unknown command %s') % cmd)
392 raise error.Abort(_(b'unknown command %s') % cmd)
382
393
383 return cmd != b''
394 return cmd != b''
384
395
385 capabilities = {b'runcommand': runcommand, b'getencoding': getencoding}
396 capabilities = {b'runcommand': runcommand, b'getencoding': getencoding}
386
397
387 def serve(self):
398 def serve(self):
388 hellomsg = b'capabilities: ' + b' '.join(sorted(self.capabilities))
399 hellomsg = b'capabilities: ' + b' '.join(sorted(self.capabilities))
389 hellomsg += b'\n'
400 hellomsg += b'\n'
390 hellomsg += b'encoding: ' + encoding.encoding
401 hellomsg += b'encoding: ' + encoding.encoding
391 hellomsg += b'\n'
402 hellomsg += b'\n'
392 if self.cmsg:
403 if self.cmsg:
393 hellomsg += b'message-encoding: %s\n' % self.cmsg.encoding
404 hellomsg += b'message-encoding: %s\n' % self.cmsg.encoding
394 hellomsg += b'pid: %d' % procutil.getpid()
405 hellomsg += b'pid: %d' % procutil.getpid()
395 if util.safehasattr(os, b'getpgid'):
406 if util.safehasattr(os, b'getpgid'):
396 hellomsg += b'\n'
407 hellomsg += b'\n'
397 hellomsg += b'pgid: %d' % os.getpgid(0)
408 hellomsg += b'pgid: %d' % os.getpgid(0)
398
409
399 # write the hello msg in -one- chunk
410 # write the hello msg in -one- chunk
400 self.cout.write(hellomsg)
411 self.cout.write(hellomsg)
401
412
402 try:
413 try:
403 while self.serveone():
414 while self.serveone():
404 pass
415 pass
405 except EOFError:
416 except EOFError:
406 # we'll get here if the client disconnected while we were reading
417 # we'll get here if the client disconnected while we were reading
407 # its request
418 # its request
408 return 1
419 return 1
409
420
410 return 0
421 return 0
411
422
412
423
413 def setuplogging(ui, repo=None, fp=None):
424 def setuplogging(ui, repo=None, fp=None):
414 """Set up server logging facility
425 """Set up server logging facility
415
426
416 If cmdserver.log is '-', log messages will be sent to the given fp.
427 If cmdserver.log is '-', log messages will be sent to the given fp.
417 It should be the 'd' channel while a client is connected, and otherwise
428 It should be the 'd' channel while a client is connected, and otherwise
418 is the stderr of the server process.
429 is the stderr of the server process.
419 """
430 """
420 # developer config: cmdserver.log
431 # developer config: cmdserver.log
421 logpath = ui.config(b'cmdserver', b'log')
432 logpath = ui.config(b'cmdserver', b'log')
422 if not logpath:
433 if not logpath:
423 return
434 return
424 # developer config: cmdserver.track-log
435 # developer config: cmdserver.track-log
425 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
436 tracked = set(ui.configlist(b'cmdserver', b'track-log'))
426
437
427 if logpath == b'-' and fp:
438 if logpath == b'-' and fp:
428 logger = loggingutil.fileobjectlogger(fp, tracked)
439 logger = loggingutil.fileobjectlogger(fp, tracked)
429 elif logpath == b'-':
440 elif logpath == b'-':
430 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
441 logger = loggingutil.fileobjectlogger(ui.ferr, tracked)
431 else:
442 else:
432 logpath = os.path.abspath(util.expandpath(logpath))
443 logpath = os.path.abspath(util.expandpath(logpath))
433 # developer config: cmdserver.max-log-files
444 # developer config: cmdserver.max-log-files
434 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
445 maxfiles = ui.configint(b'cmdserver', b'max-log-files')
435 # developer config: cmdserver.max-log-size
446 # developer config: cmdserver.max-log-size
436 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
447 maxsize = ui.configbytes(b'cmdserver', b'max-log-size')
437 vfs = vfsmod.vfs(os.path.dirname(logpath))
448 vfs = vfsmod.vfs(os.path.dirname(logpath))
438 logger = loggingutil.filelogger(
449 logger = loggingutil.filelogger(
439 vfs,
450 vfs,
440 os.path.basename(logpath),
451 os.path.basename(logpath),
441 tracked,
452 tracked,
442 maxfiles=maxfiles,
453 maxfiles=maxfiles,
443 maxsize=maxsize,
454 maxsize=maxsize,
444 )
455 )
445
456
446 targetuis = {ui}
457 targetuis = {ui}
447 if repo:
458 if repo:
448 targetuis.add(repo.baseui)
459 targetuis.add(repo.baseui)
449 targetuis.add(repo.ui)
460 targetuis.add(repo.ui)
450 for u in targetuis:
461 for u in targetuis:
451 u.setlogger(b'cmdserver', logger)
462 u.setlogger(b'cmdserver', logger)
452
463
453
464
454 class pipeservice(object):
465 class pipeservice(object):
455 def __init__(self, ui, repo, opts):
466 def __init__(self, ui, repo, opts):
456 self.ui = ui
467 self.ui = ui
457 self.repo = repo
468 self.repo = repo
458
469
459 def init(self):
470 def init(self):
460 pass
471 pass
461
472
462 def run(self):
473 def run(self):
463 ui = self.ui
474 ui = self.ui
464 # redirect stdio to null device so that broken extensions or in-process
475 # redirect stdio to null device so that broken extensions or in-process
465 # hooks will never cause corruption of channel protocol.
476 # hooks will never cause corruption of channel protocol.
466 with ui.protectedfinout() as (fin, fout):
477 with ui.protectedfinout() as (fin, fout):
467 sv = server(ui, self.repo, fin, fout)
478 sv = server(ui, self.repo, fin, fout)
468 try:
479 try:
469 return sv.serve()
480 return sv.serve()
470 finally:
481 finally:
471 sv.cleanup()
482 sv.cleanup()
472
483
473
484
474 def _initworkerprocess():
485 def _initworkerprocess():
475 # use a different process group from the master process, in order to:
486 # use a different process group from the master process, in order to:
476 # 1. make the current process group no longer "orphaned" (because the
487 # 1. make the current process group no longer "orphaned" (because the
477 # parent of this process is in a different process group while
488 # parent of this process is in a different process group while
478 # remains in a same session)
489 # remains in a same session)
479 # according to POSIX 2.2.2.52, orphaned process group will ignore
490 # according to POSIX 2.2.2.52, orphaned process group will ignore
480 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
491 # terminal-generated stop signals like SIGTSTP (Ctrl+Z), which will
481 # cause trouble for things like ncurses.
492 # cause trouble for things like ncurses.
482 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
493 # 2. the client can use kill(-pgid, sig) to simulate terminal-generated
483 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
494 # SIGINT (Ctrl+C) and process-exit-generated SIGHUP. our child
484 # processes like ssh will be killed properly, without affecting
495 # processes like ssh will be killed properly, without affecting
485 # unrelated processes.
496 # unrelated processes.
486 os.setpgid(0, 0)
497 os.setpgid(0, 0)
487 # change random state otherwise forked request handlers would have a
498 # change random state otherwise forked request handlers would have a
488 # same state inherited from parent.
499 # same state inherited from parent.
489 random.seed()
500 random.seed()
490
501
491
502
492 def _serverequest(ui, repo, conn, createcmdserver, prereposetups):
503 def _serverequest(ui, repo, conn, createcmdserver, prereposetups):
493 fin = conn.makefile('rb')
504 fin = conn.makefile('rb')
494 fout = conn.makefile('wb')
505 fout = conn.makefile('wb')
495 sv = None
506 sv = None
496 try:
507 try:
497 sv = createcmdserver(repo, conn, fin, fout, prereposetups)
508 sv = createcmdserver(repo, conn, fin, fout, prereposetups)
498 try:
509 try:
499 sv.serve()
510 sv.serve()
500 # handle exceptions that may be raised by command server. most of
511 # handle exceptions that may be raised by command server. most of
501 # known exceptions are caught by dispatch.
512 # known exceptions are caught by dispatch.
502 except error.Abort as inst:
513 except error.Abort as inst:
503 ui.error(_(b'abort: %s\n') % inst.message)
514 ui.error(_(b'abort: %s\n') % inst.message)
504 except IOError as inst:
515 except IOError as inst:
505 if inst.errno != errno.EPIPE:
516 if inst.errno != errno.EPIPE:
506 raise
517 raise
507 except KeyboardInterrupt:
518 except KeyboardInterrupt:
508 pass
519 pass
509 finally:
520 finally:
510 sv.cleanup()
521 sv.cleanup()
511 except: # re-raises
522 except: # re-raises
512 # also write traceback to error channel. otherwise client cannot
523 # also write traceback to error channel. otherwise client cannot
513 # see it because it is written to server's stderr by default.
524 # see it because it is written to server's stderr by default.
514 if sv:
525 if sv:
515 cerr = sv.cerr
526 cerr = sv.cerr
516 else:
527 else:
517 cerr = channeledoutput(fout, b'e')
528 cerr = channeledoutput(fout, b'e')
518 cerr.write(encoding.strtolocal(traceback.format_exc()))
529 cerr.write(encoding.strtolocal(traceback.format_exc()))
519 raise
530 raise
520 finally:
531 finally:
521 fin.close()
532 fin.close()
522 try:
533 try:
523 fout.close() # implicit flush() may cause another EPIPE
534 fout.close() # implicit flush() may cause another EPIPE
524 except IOError as inst:
535 except IOError as inst:
525 if inst.errno != errno.EPIPE:
536 if inst.errno != errno.EPIPE:
526 raise
537 raise
527
538
528
539
529 class unixservicehandler(object):
540 class unixservicehandler(object):
530 """Set of pluggable operations for unix-mode services
541 """Set of pluggable operations for unix-mode services
531
542
532 Almost all methods except for createcmdserver() are called in the main
543 Almost all methods except for createcmdserver() are called in the main
533 process. You can't pass mutable resource back from createcmdserver().
544 process. You can't pass mutable resource back from createcmdserver().
534 """
545 """
535
546
536 pollinterval = None
547 pollinterval = None
537
548
538 def __init__(self, ui):
549 def __init__(self, ui):
539 self.ui = ui
550 self.ui = ui
540
551
541 def bindsocket(self, sock, address):
552 def bindsocket(self, sock, address):
542 util.bindunixsocket(sock, address)
553 util.bindunixsocket(sock, address)
543 sock.listen(socket.SOMAXCONN)
554 sock.listen(socket.SOMAXCONN)
544 self.ui.status(_(b'listening at %s\n') % address)
555 self.ui.status(_(b'listening at %s\n') % address)
545 self.ui.flush() # avoid buffering of status message
556 self.ui.flush() # avoid buffering of status message
546
557
547 def unlinksocket(self, address):
558 def unlinksocket(self, address):
548 os.unlink(address)
559 os.unlink(address)
549
560
550 def shouldexit(self):
561 def shouldexit(self):
551 """True if server should shut down; checked per pollinterval"""
562 """True if server should shut down; checked per pollinterval"""
552 return False
563 return False
553
564
554 def newconnection(self):
565 def newconnection(self):
555 """Called when main process notices new connection"""
566 """Called when main process notices new connection"""
556
567
557 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
568 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
558 """Create new command server instance; called in the process that
569 """Create new command server instance; called in the process that
559 serves for the current connection"""
570 serves for the current connection"""
560 return server(self.ui, repo, fin, fout, prereposetups)
571 return server(self.ui, repo, fin, fout, prereposetups)
561
572
562
573
563 class unixforkingservice(object):
574 class unixforkingservice(object):
564 """
575 """
565 Listens on unix domain socket and forks server per connection
576 Listens on unix domain socket and forks server per connection
566 """
577 """
567
578
568 def __init__(self, ui, repo, opts, handler=None):
579 def __init__(self, ui, repo, opts, handler=None):
569 self.ui = ui
580 self.ui = ui
570 self.repo = repo
581 self.repo = repo
571 self.address = opts[b'address']
582 self.address = opts[b'address']
572 if not util.safehasattr(socket, b'AF_UNIX'):
583 if not util.safehasattr(socket, b'AF_UNIX'):
573 raise error.Abort(_(b'unsupported platform'))
584 raise error.Abort(_(b'unsupported platform'))
574 if not self.address:
585 if not self.address:
575 raise error.Abort(_(b'no socket path specified with --address'))
586 raise error.Abort(_(b'no socket path specified with --address'))
576 self._servicehandler = handler or unixservicehandler(ui)
587 self._servicehandler = handler or unixservicehandler(ui)
577 self._sock = None
588 self._sock = None
578 self._mainipc = None
589 self._mainipc = None
579 self._workeripc = None
590 self._workeripc = None
580 self._oldsigchldhandler = None
591 self._oldsigchldhandler = None
581 self._workerpids = set() # updated by signal handler; do not iterate
592 self._workerpids = set() # updated by signal handler; do not iterate
582 self._socketunlinked = None
593 self._socketunlinked = None
583 # experimental config: cmdserver.max-repo-cache
594 # experimental config: cmdserver.max-repo-cache
584 maxlen = ui.configint(b'cmdserver', b'max-repo-cache')
595 maxlen = ui.configint(b'cmdserver', b'max-repo-cache')
585 if maxlen < 0:
596 if maxlen < 0:
586 raise error.Abort(_(b'negative max-repo-cache size not allowed'))
597 raise error.Abort(_(b'negative max-repo-cache size not allowed'))
587 self._repoloader = repocache.repoloader(ui, maxlen)
598 self._repoloader = repocache.repoloader(ui, maxlen)
588 # attempt to avoid crash in CoreFoundation when using chg after fix in
599 # attempt to avoid crash in CoreFoundation when using chg after fix in
589 # a89381e04c58
600 # a89381e04c58
590 if pycompat.isdarwin:
601 if pycompat.isdarwin:
591 procutil.gui()
602 procutil.gui()
592
603
593 def init(self):
604 def init(self):
594 self._sock = socket.socket(socket.AF_UNIX)
605 self._sock = socket.socket(socket.AF_UNIX)
595 # IPC channel from many workers to one main process; this is actually
606 # IPC channel from many workers to one main process; this is actually
596 # a uni-directional pipe, but is backed by a DGRAM socket so each
607 # a uni-directional pipe, but is backed by a DGRAM socket so each
597 # message can be easily separated.
608 # message can be easily separated.
598 o = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
609 o = socket.socketpair(socket.AF_UNIX, socket.SOCK_DGRAM)
599 self._mainipc, self._workeripc = o
610 self._mainipc, self._workeripc = o
600 self._servicehandler.bindsocket(self._sock, self.address)
611 self._servicehandler.bindsocket(self._sock, self.address)
601 if util.safehasattr(procutil, b'unblocksignal'):
612 if util.safehasattr(procutil, b'unblocksignal'):
602 procutil.unblocksignal(signal.SIGCHLD)
613 procutil.unblocksignal(signal.SIGCHLD)
603 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
614 o = signal.signal(signal.SIGCHLD, self._sigchldhandler)
604 self._oldsigchldhandler = o
615 self._oldsigchldhandler = o
605 self._socketunlinked = False
616 self._socketunlinked = False
606 self._repoloader.start()
617 self._repoloader.start()
607
618
608 def _unlinksocket(self):
619 def _unlinksocket(self):
609 if not self._socketunlinked:
620 if not self._socketunlinked:
610 self._servicehandler.unlinksocket(self.address)
621 self._servicehandler.unlinksocket(self.address)
611 self._socketunlinked = True
622 self._socketunlinked = True
612
623
613 def _cleanup(self):
624 def _cleanup(self):
614 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
625 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
615 self._sock.close()
626 self._sock.close()
616 self._mainipc.close()
627 self._mainipc.close()
617 self._workeripc.close()
628 self._workeripc.close()
618 self._unlinksocket()
629 self._unlinksocket()
619 self._repoloader.stop()
630 self._repoloader.stop()
620 # don't kill child processes as they have active clients, just wait
631 # don't kill child processes as they have active clients, just wait
621 self._reapworkers(0)
632 self._reapworkers(0)
622
633
623 def run(self):
634 def run(self):
624 try:
635 try:
625 self._mainloop()
636 self._mainloop()
626 finally:
637 finally:
627 self._cleanup()
638 self._cleanup()
628
639
629 def _mainloop(self):
640 def _mainloop(self):
630 exiting = False
641 exiting = False
631 h = self._servicehandler
642 h = self._servicehandler
632 selector = selectors.DefaultSelector()
643 selector = selectors.DefaultSelector()
633 selector.register(
644 selector.register(
634 self._sock, selectors.EVENT_READ, self._acceptnewconnection
645 self._sock, selectors.EVENT_READ, self._acceptnewconnection
635 )
646 )
636 selector.register(
647 selector.register(
637 self._mainipc, selectors.EVENT_READ, self._handlemainipc
648 self._mainipc, selectors.EVENT_READ, self._handlemainipc
638 )
649 )
639 while True:
650 while True:
640 if not exiting and h.shouldexit():
651 if not exiting and h.shouldexit():
641 # clients can no longer connect() to the domain socket, so
652 # clients can no longer connect() to the domain socket, so
642 # we stop queuing new requests.
653 # we stop queuing new requests.
643 # for requests that are queued (connect()-ed, but haven't been
654 # for requests that are queued (connect()-ed, but haven't been
644 # accept()-ed), handle them before exit. otherwise, clients
655 # accept()-ed), handle them before exit. otherwise, clients
645 # waiting for recv() will receive ECONNRESET.
656 # waiting for recv() will receive ECONNRESET.
646 self._unlinksocket()
657 self._unlinksocket()
647 exiting = True
658 exiting = True
648 try:
659 try:
649 events = selector.select(timeout=h.pollinterval)
660 events = selector.select(timeout=h.pollinterval)
650 except OSError as inst:
661 except OSError as inst:
651 # selectors2 raises ETIMEDOUT if timeout exceeded while
662 # selectors2 raises ETIMEDOUT if timeout exceeded while
652 # handling signal interrupt. That's probably wrong, but
663 # handling signal interrupt. That's probably wrong, but
653 # we can easily get around it.
664 # we can easily get around it.
654 if inst.errno != errno.ETIMEDOUT:
665 if inst.errno != errno.ETIMEDOUT:
655 raise
666 raise
656 events = []
667 events = []
657 if not events:
668 if not events:
658 # only exit if we completed all queued requests
669 # only exit if we completed all queued requests
659 if exiting:
670 if exiting:
660 break
671 break
661 continue
672 continue
662 for key, _mask in events:
673 for key, _mask in events:
663 key.data(key.fileobj, selector)
674 key.data(key.fileobj, selector)
664 selector.close()
675 selector.close()
665
676
666 def _acceptnewconnection(self, sock, selector):
677 def _acceptnewconnection(self, sock, selector):
667 h = self._servicehandler
678 h = self._servicehandler
668 try:
679 try:
669 conn, _addr = sock.accept()
680 conn, _addr = sock.accept()
670 except socket.error as inst:
681 except socket.error as inst:
671 if inst.args[0] == errno.EINTR:
682 if inst.args[0] == errno.EINTR:
672 return
683 return
673 raise
684 raise
674
685
675 # Future improvement: On Python 3.7, maybe gc.freeze() can be used
686 # Future improvement: On Python 3.7, maybe gc.freeze() can be used
676 # to prevent COW memory from being touched by GC.
687 # to prevent COW memory from being touched by GC.
677 # https://instagram-engineering.com/
688 # https://instagram-engineering.com/
678 # copy-on-write-friendly-python-garbage-collection-ad6ed5233ddf
689 # copy-on-write-friendly-python-garbage-collection-ad6ed5233ddf
679 pid = os.fork()
690 pid = os.fork()
680 if pid:
691 if pid:
681 try:
692 try:
682 self.ui.log(
693 self.ui.log(
683 b'cmdserver', b'forked worker process (pid=%d)\n', pid
694 b'cmdserver', b'forked worker process (pid=%d)\n', pid
684 )
695 )
685 self._workerpids.add(pid)
696 self._workerpids.add(pid)
686 h.newconnection()
697 h.newconnection()
687 finally:
698 finally:
688 conn.close() # release handle in parent process
699 conn.close() # release handle in parent process
689 else:
700 else:
690 try:
701 try:
691 selector.close()
702 selector.close()
692 sock.close()
703 sock.close()
693 self._mainipc.close()
704 self._mainipc.close()
694 self._runworker(conn)
705 self._runworker(conn)
695 conn.close()
706 conn.close()
696 self._workeripc.close()
707 self._workeripc.close()
697 os._exit(0)
708 os._exit(0)
698 except: # never return, hence no re-raises
709 except: # never return, hence no re-raises
699 try:
710 try:
700 self.ui.traceback(force=True)
711 self.ui.traceback(force=True)
701 finally:
712 finally:
702 os._exit(255)
713 os._exit(255)
703
714
704 def _handlemainipc(self, sock, selector):
715 def _handlemainipc(self, sock, selector):
705 """Process messages sent from a worker"""
716 """Process messages sent from a worker"""
706 try:
717 try:
707 path = sock.recv(32768) # large enough to receive path
718 path = sock.recv(32768) # large enough to receive path
708 except socket.error as inst:
719 except socket.error as inst:
709 if inst.args[0] == errno.EINTR:
720 if inst.args[0] == errno.EINTR:
710 return
721 return
711 raise
722 raise
712 self._repoloader.load(path)
723 self._repoloader.load(path)
713
724
714 def _sigchldhandler(self, signal, frame):
725 def _sigchldhandler(self, signal, frame):
715 self._reapworkers(os.WNOHANG)
726 self._reapworkers(os.WNOHANG)
716
727
717 def _reapworkers(self, options):
728 def _reapworkers(self, options):
718 while self._workerpids:
729 while self._workerpids:
719 try:
730 try:
720 pid, _status = os.waitpid(-1, options)
731 pid, _status = os.waitpid(-1, options)
721 except OSError as inst:
732 except OSError as inst:
722 if inst.errno == errno.EINTR:
733 if inst.errno == errno.EINTR:
723 continue
734 continue
724 if inst.errno != errno.ECHILD:
735 if inst.errno != errno.ECHILD:
725 raise
736 raise
726 # no child processes at all (reaped by other waitpid()?)
737 # no child processes at all (reaped by other waitpid()?)
727 self._workerpids.clear()
738 self._workerpids.clear()
728 return
739 return
729 if pid == 0:
740 if pid == 0:
730 # no waitable child processes
741 # no waitable child processes
731 return
742 return
732 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
743 self.ui.log(b'cmdserver', b'worker process exited (pid=%d)\n', pid)
733 self._workerpids.discard(pid)
744 self._workerpids.discard(pid)
734
745
735 def _runworker(self, conn):
746 def _runworker(self, conn):
736 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
747 signal.signal(signal.SIGCHLD, self._oldsigchldhandler)
737 _initworkerprocess()
748 _initworkerprocess()
738 h = self._servicehandler
749 h = self._servicehandler
739 try:
750 try:
740 _serverequest(
751 _serverequest(
741 self.ui,
752 self.ui,
742 self.repo,
753 self.repo,
743 conn,
754 conn,
744 h.createcmdserver,
755 h.createcmdserver,
745 prereposetups=[self._reposetup],
756 prereposetups=[self._reposetup],
746 )
757 )
747 finally:
758 finally:
748 gc.collect() # trigger __del__ since worker process uses os._exit
759 gc.collect() # trigger __del__ since worker process uses os._exit
749
760
750 def _reposetup(self, ui, repo):
761 def _reposetup(self, ui, repo):
751 if not repo.local():
762 if not repo.local():
752 return
763 return
753
764
754 class unixcmdserverrepo(repo.__class__):
765 class unixcmdserverrepo(repo.__class__):
755 def close(self):
766 def close(self):
756 super(unixcmdserverrepo, self).close()
767 super(unixcmdserverrepo, self).close()
757 try:
768 try:
758 self._cmdserveripc.send(self.root)
769 self._cmdserveripc.send(self.root)
759 except socket.error:
770 except socket.error:
760 self.ui.log(
771 self.ui.log(
761 b'cmdserver', b'failed to send repo root to master\n'
772 b'cmdserver', b'failed to send repo root to master\n'
762 )
773 )
763
774
764 repo.__class__ = unixcmdserverrepo
775 repo.__class__ = unixcmdserverrepo
765 repo._cmdserveripc = self._workeripc
776 repo._cmdserveripc = self._workeripc
766
777
767 cachedrepo = self._repoloader.get(repo.root)
778 cachedrepo = self._repoloader.get(repo.root)
768 if cachedrepo is None:
779 if cachedrepo is None:
769 return
780 return
770 repo.ui.log(b'repocache', b'repo from cache: %s\n', repo.root)
781 repo.ui.log(b'repocache', b'repo from cache: %s\n', repo.root)
771 repocache.copycache(cachedrepo, repo)
782 repocache.copycache(cachedrepo, repo)
@@ -1,1346 +1,1354 b''
1 # dispatch.py - command dispatching for mercurial
1 # dispatch.py - command dispatching for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 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, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import errno
10 import errno
11 import getopt
11 import getopt
12 import io
12 import io
13 import os
13 import os
14 import pdb
14 import pdb
15 import re
15 import re
16 import signal
16 import signal
17 import sys
17 import sys
18 import traceback
18 import traceback
19
19
20
20
21 from .i18n import _
21 from .i18n import _
22 from .pycompat import getattr
22 from .pycompat import getattr
23
23
24 from hgdemandimport import tracing
24 from hgdemandimport import tracing
25
25
26 from . import (
26 from . import (
27 cmdutil,
27 cmdutil,
28 color,
28 color,
29 commands,
29 commands,
30 demandimport,
30 demandimport,
31 encoding,
31 encoding,
32 error,
32 error,
33 extensions,
33 extensions,
34 fancyopts,
34 fancyopts,
35 help,
35 help,
36 hg,
36 hg,
37 hook,
37 hook,
38 localrepo,
38 localrepo,
39 profiling,
39 profiling,
40 pycompat,
40 pycompat,
41 rcutil,
41 rcutil,
42 registrar,
42 registrar,
43 requirements as requirementsmod,
43 requirements as requirementsmod,
44 scmutil,
44 scmutil,
45 ui as uimod,
45 ui as uimod,
46 util,
46 util,
47 vfs,
47 vfs,
48 )
48 )
49
49
50 from .utils import (
50 from .utils import (
51 procutil,
51 procutil,
52 stringutil,
52 stringutil,
53 )
53 )
54
54
55
55
56 class request(object):
56 class request(object):
57 def __init__(
57 def __init__(
58 self,
58 self,
59 args,
59 args,
60 ui=None,
60 ui=None,
61 repo=None,
61 repo=None,
62 fin=None,
62 fin=None,
63 fout=None,
63 fout=None,
64 ferr=None,
64 ferr=None,
65 fmsg=None,
65 fmsg=None,
66 prereposetups=None,
66 prereposetups=None,
67 ):
67 ):
68 self.args = args
68 self.args = args
69 self.ui = ui
69 self.ui = ui
70 self.repo = repo
70 self.repo = repo
71
71
72 # input/output/error streams
72 # input/output/error streams
73 self.fin = fin
73 self.fin = fin
74 self.fout = fout
74 self.fout = fout
75 self.ferr = ferr
75 self.ferr = ferr
76 # separate stream for status/error messages
76 # separate stream for status/error messages
77 self.fmsg = fmsg
77 self.fmsg = fmsg
78
78
79 # remember options pre-parsed by _earlyparseopts()
79 # remember options pre-parsed by _earlyparseopts()
80 self.earlyoptions = {}
80 self.earlyoptions = {}
81
81
82 # reposetups which run before extensions, useful for chg to pre-fill
82 # reposetups which run before extensions, useful for chg to pre-fill
83 # low-level repo state (for example, changelog) before extensions.
83 # low-level repo state (for example, changelog) before extensions.
84 self.prereposetups = prereposetups or []
84 self.prereposetups = prereposetups or []
85
85
86 # store the parsed and canonical command
86 # store the parsed and canonical command
87 self.canonical_command = None
87 self.canonical_command = None
88
88
89 def _runexithandlers(self):
89 def _runexithandlers(self):
90 exc = None
90 exc = None
91 handlers = self.ui._exithandlers
91 handlers = self.ui._exithandlers
92 try:
92 try:
93 while handlers:
93 while handlers:
94 func, args, kwargs = handlers.pop()
94 func, args, kwargs = handlers.pop()
95 try:
95 try:
96 func(*args, **kwargs)
96 func(*args, **kwargs)
97 except: # re-raises below
97 except: # re-raises below
98 if exc is None:
98 if exc is None:
99 exc = sys.exc_info()[1]
99 exc = sys.exc_info()[1]
100 self.ui.warnnoi18n(b'error in exit handlers:\n')
100 self.ui.warnnoi18n(b'error in exit handlers:\n')
101 self.ui.traceback(force=True)
101 self.ui.traceback(force=True)
102 finally:
102 finally:
103 if exc is not None:
103 if exc is not None:
104 raise exc
104 raise exc
105
105
106
106
107 def closestdio(ui, err):
108 status = None
109 # In all cases we try to flush stdio streams.
110 if util.safehasattr(ui, b'fout'):
111 assert ui is not None # help pytype
112 assert ui.fout is not None # help pytype
113 try:
114 ui.fout.flush()
115 except IOError as e:
116 err = e
117 status = -1
118
119 if util.safehasattr(ui, b'ferr'):
120 assert ui is not None # help pytype
121 assert ui.ferr is not None # help pytype
122 try:
123 if err is not None and err.errno != errno.EPIPE:
124 ui.ferr.write(
125 b'abort: %s\n' % encoding.strtolocal(err.strerror)
126 )
127 ui.ferr.flush()
128 # There's not much we can do about an I/O error here. So (possibly)
129 # change the status code and move on.
130 except IOError:
131 status = -1
132
133 return status
134
135
107 def run():
136 def run():
108 """run the command in sys.argv"""
137 """run the command in sys.argv"""
109 try:
138 try:
110 initstdio()
139 initstdio()
111 with tracing.log('parse args into request'):
140 with tracing.log('parse args into request'):
112 req = request(pycompat.sysargv[1:])
141 req = request(pycompat.sysargv[1:])
113 err = None
142 err = None
114 try:
143 try:
115 status = dispatch(req)
144 status = dispatch(req)
116 except error.StdioError as e:
145 except error.StdioError as e:
117 err = e
146 err = e
118 status = -1
147 status = -1
119
148
120 # In all cases we try to flush stdio streams.
149 ret = closestdio(req.ui, err)
121 if util.safehasattr(req.ui, b'fout'):
150 if ret:
122 assert req.ui is not None # help pytype
151 status = ret
123 assert req.ui.fout is not None # help pytype
124 try:
125 req.ui.fout.flush()
126 except IOError as e:
127 err = e
128 status = -1
129
130 if util.safehasattr(req.ui, b'ferr'):
131 assert req.ui is not None # help pytype
132 assert req.ui.ferr is not None # help pytype
133 try:
134 if err is not None and err.errno != errno.EPIPE:
135 req.ui.ferr.write(
136 b'abort: %s\n' % encoding.strtolocal(err.strerror)
137 )
138 req.ui.ferr.flush()
139 # There's not much we can do about an I/O error here. So (possibly)
140 # change the status code and move on.
141 except IOError:
142 status = -1
143
144 _silencestdio()
152 _silencestdio()
145 except KeyboardInterrupt:
153 except KeyboardInterrupt:
146 # Catch early/late KeyboardInterrupt as last ditch. Here nothing will
154 # Catch early/late KeyboardInterrupt as last ditch. Here nothing will
147 # be printed to console to avoid another IOError/KeyboardInterrupt.
155 # be printed to console to avoid another IOError/KeyboardInterrupt.
148 status = -1
156 status = -1
149 sys.exit(status & 255)
157 sys.exit(status & 255)
150
158
151
159
152 if pycompat.ispy3:
160 if pycompat.ispy3:
153
161
154 def initstdio():
162 def initstdio():
155 # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
163 # stdio streams on Python 3 are io.TextIOWrapper instances proxying another
156 # buffer. These streams will normalize \n to \r\n by default. Mercurial's
164 # buffer. These streams will normalize \n to \r\n by default. Mercurial's
157 # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
165 # preferred mechanism for writing output (ui.write()) uses io.BufferedWriter
158 # instances, which write to the underlying stdio file descriptor in binary
166 # instances, which write to the underlying stdio file descriptor in binary
159 # mode. ui.write() uses \n for line endings and no line ending normalization
167 # mode. ui.write() uses \n for line endings and no line ending normalization
160 # is attempted through this interface. This "just works," even if the system
168 # is attempted through this interface. This "just works," even if the system
161 # preferred line ending is not \n.
169 # preferred line ending is not \n.
162 #
170 #
163 # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
171 # But some parts of Mercurial (e.g. hooks) can still send data to sys.stdout
164 # and sys.stderr. They will inherit the line ending normalization settings,
172 # and sys.stderr. They will inherit the line ending normalization settings,
165 # potentially causing e.g. \r\n to be emitted. Since emitting \n should
173 # potentially causing e.g. \r\n to be emitted. Since emitting \n should
166 # "just work," here we change the sys.* streams to disable line ending
174 # "just work," here we change the sys.* streams to disable line ending
167 # normalization, ensuring compatibility with our ui type.
175 # normalization, ensuring compatibility with our ui type.
168
176
169 # write_through is new in Python 3.7.
177 # write_through is new in Python 3.7.
170 kwargs = {
178 kwargs = {
171 "newline": "\n",
179 "newline": "\n",
172 "line_buffering": sys.stdout.line_buffering,
180 "line_buffering": sys.stdout.line_buffering,
173 }
181 }
174 if util.safehasattr(sys.stdout, "write_through"):
182 if util.safehasattr(sys.stdout, "write_through"):
175 kwargs["write_through"] = sys.stdout.write_through
183 kwargs["write_through"] = sys.stdout.write_through
176 sys.stdout = io.TextIOWrapper(
184 sys.stdout = io.TextIOWrapper(
177 sys.stdout.buffer, sys.stdout.encoding, sys.stdout.errors, **kwargs
185 sys.stdout.buffer, sys.stdout.encoding, sys.stdout.errors, **kwargs
178 )
186 )
179
187
180 kwargs = {
188 kwargs = {
181 "newline": "\n",
189 "newline": "\n",
182 "line_buffering": sys.stderr.line_buffering,
190 "line_buffering": sys.stderr.line_buffering,
183 }
191 }
184 if util.safehasattr(sys.stderr, "write_through"):
192 if util.safehasattr(sys.stderr, "write_through"):
185 kwargs["write_through"] = sys.stderr.write_through
193 kwargs["write_through"] = sys.stderr.write_through
186 sys.stderr = io.TextIOWrapper(
194 sys.stderr = io.TextIOWrapper(
187 sys.stderr.buffer, sys.stderr.encoding, sys.stderr.errors, **kwargs
195 sys.stderr.buffer, sys.stderr.encoding, sys.stderr.errors, **kwargs
188 )
196 )
189
197
190 if sys.stdin is not None:
198 if sys.stdin is not None:
191 # No write_through on read-only stream.
199 # No write_through on read-only stream.
192 sys.stdin = io.TextIOWrapper(
200 sys.stdin = io.TextIOWrapper(
193 sys.stdin.buffer,
201 sys.stdin.buffer,
194 sys.stdin.encoding,
202 sys.stdin.encoding,
195 sys.stdin.errors,
203 sys.stdin.errors,
196 # None is universal newlines mode.
204 # None is universal newlines mode.
197 newline=None,
205 newline=None,
198 line_buffering=sys.stdin.line_buffering,
206 line_buffering=sys.stdin.line_buffering,
199 )
207 )
200
208
201 def _silencestdio():
209 def _silencestdio():
202 for fp in (sys.stdout, sys.stderr):
210 for fp in (sys.stdout, sys.stderr):
203 # Check if the file is okay
211 # Check if the file is okay
204 try:
212 try:
205 fp.flush()
213 fp.flush()
206 continue
214 continue
207 except IOError:
215 except IOError:
208 pass
216 pass
209 # Otherwise mark it as closed to silence "Exception ignored in"
217 # Otherwise mark it as closed to silence "Exception ignored in"
210 # message emitted by the interpreter finalizer. Be careful to
218 # message emitted by the interpreter finalizer. Be careful to
211 # not close procutil.stdout, which may be a fdopen-ed file object
219 # not close procutil.stdout, which may be a fdopen-ed file object
212 # and its close() actually closes the underlying file descriptor.
220 # and its close() actually closes the underlying file descriptor.
213 try:
221 try:
214 fp.close()
222 fp.close()
215 except IOError:
223 except IOError:
216 pass
224 pass
217
225
218
226
219 else:
227 else:
220
228
221 def initstdio():
229 def initstdio():
222 for fp in (sys.stdin, sys.stdout, sys.stderr):
230 for fp in (sys.stdin, sys.stdout, sys.stderr):
223 procutil.setbinary(fp)
231 procutil.setbinary(fp)
224
232
225 def _silencestdio():
233 def _silencestdio():
226 pass
234 pass
227
235
228
236
229 def _formatargs(args):
237 def _formatargs(args):
230 return b' '.join(procutil.shellquote(a) for a in args)
238 return b' '.join(procutil.shellquote(a) for a in args)
231
239
232
240
233 def dispatch(req):
241 def dispatch(req):
234 """run the command specified in req.args; returns an integer status code"""
242 """run the command specified in req.args; returns an integer status code"""
235 with tracing.log('dispatch.dispatch'):
243 with tracing.log('dispatch.dispatch'):
236 if req.ferr:
244 if req.ferr:
237 ferr = req.ferr
245 ferr = req.ferr
238 elif req.ui:
246 elif req.ui:
239 ferr = req.ui.ferr
247 ferr = req.ui.ferr
240 else:
248 else:
241 ferr = procutil.stderr
249 ferr = procutil.stderr
242
250
243 try:
251 try:
244 if not req.ui:
252 if not req.ui:
245 req.ui = uimod.ui.load()
253 req.ui = uimod.ui.load()
246 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
254 req.earlyoptions.update(_earlyparseopts(req.ui, req.args))
247 if req.earlyoptions[b'traceback']:
255 if req.earlyoptions[b'traceback']:
248 req.ui.setconfig(b'ui', b'traceback', b'on', b'--traceback')
256 req.ui.setconfig(b'ui', b'traceback', b'on', b'--traceback')
249
257
250 # set ui streams from the request
258 # set ui streams from the request
251 if req.fin:
259 if req.fin:
252 req.ui.fin = req.fin
260 req.ui.fin = req.fin
253 if req.fout:
261 if req.fout:
254 req.ui.fout = req.fout
262 req.ui.fout = req.fout
255 if req.ferr:
263 if req.ferr:
256 req.ui.ferr = req.ferr
264 req.ui.ferr = req.ferr
257 if req.fmsg:
265 if req.fmsg:
258 req.ui.fmsg = req.fmsg
266 req.ui.fmsg = req.fmsg
259 except error.Abort as inst:
267 except error.Abort as inst:
260 ferr.write(inst.format())
268 ferr.write(inst.format())
261 return -1
269 return -1
262
270
263 msg = _formatargs(req.args)
271 msg = _formatargs(req.args)
264 starttime = util.timer()
272 starttime = util.timer()
265 ret = 1 # default of Python exit code on unhandled exception
273 ret = 1 # default of Python exit code on unhandled exception
266 try:
274 try:
267 ret = _runcatch(req) or 0
275 ret = _runcatch(req) or 0
268 except error.ProgrammingError as inst:
276 except error.ProgrammingError as inst:
269 req.ui.error(_(b'** ProgrammingError: %s\n') % inst)
277 req.ui.error(_(b'** ProgrammingError: %s\n') % inst)
270 if inst.hint:
278 if inst.hint:
271 req.ui.error(_(b'** (%s)\n') % inst.hint)
279 req.ui.error(_(b'** (%s)\n') % inst.hint)
272 raise
280 raise
273 except KeyboardInterrupt as inst:
281 except KeyboardInterrupt as inst:
274 try:
282 try:
275 if isinstance(inst, error.SignalInterrupt):
283 if isinstance(inst, error.SignalInterrupt):
276 msg = _(b"killed!\n")
284 msg = _(b"killed!\n")
277 else:
285 else:
278 msg = _(b"interrupted!\n")
286 msg = _(b"interrupted!\n")
279 req.ui.error(msg)
287 req.ui.error(msg)
280 except error.SignalInterrupt:
288 except error.SignalInterrupt:
281 # maybe pager would quit without consuming all the output, and
289 # maybe pager would quit without consuming all the output, and
282 # SIGPIPE was raised. we cannot print anything in this case.
290 # SIGPIPE was raised. we cannot print anything in this case.
283 pass
291 pass
284 except IOError as inst:
292 except IOError as inst:
285 if inst.errno != errno.EPIPE:
293 if inst.errno != errno.EPIPE:
286 raise
294 raise
287 ret = -1
295 ret = -1
288 finally:
296 finally:
289 duration = util.timer() - starttime
297 duration = util.timer() - starttime
290 req.ui.flush() # record blocked times
298 req.ui.flush() # record blocked times
291 if req.ui.logblockedtimes:
299 if req.ui.logblockedtimes:
292 req.ui._blockedtimes[b'command_duration'] = duration * 1000
300 req.ui._blockedtimes[b'command_duration'] = duration * 1000
293 req.ui.log(
301 req.ui.log(
294 b'uiblocked',
302 b'uiblocked',
295 b'ui blocked ms\n',
303 b'ui blocked ms\n',
296 **pycompat.strkwargs(req.ui._blockedtimes)
304 **pycompat.strkwargs(req.ui._blockedtimes)
297 )
305 )
298 return_code = ret & 255
306 return_code = ret & 255
299 req.ui.log(
307 req.ui.log(
300 b"commandfinish",
308 b"commandfinish",
301 b"%s exited %d after %0.2f seconds\n",
309 b"%s exited %d after %0.2f seconds\n",
302 msg,
310 msg,
303 return_code,
311 return_code,
304 duration,
312 duration,
305 return_code=return_code,
313 return_code=return_code,
306 duration=duration,
314 duration=duration,
307 canonical_command=req.canonical_command,
315 canonical_command=req.canonical_command,
308 )
316 )
309 try:
317 try:
310 req._runexithandlers()
318 req._runexithandlers()
311 except: # exiting, so no re-raises
319 except: # exiting, so no re-raises
312 ret = ret or -1
320 ret = ret or -1
313 # do flush again since ui.log() and exit handlers may write to ui
321 # do flush again since ui.log() and exit handlers may write to ui
314 req.ui.flush()
322 req.ui.flush()
315 return ret
323 return ret
316
324
317
325
318 def _runcatch(req):
326 def _runcatch(req):
319 with tracing.log('dispatch._runcatch'):
327 with tracing.log('dispatch._runcatch'):
320
328
321 def catchterm(*args):
329 def catchterm(*args):
322 raise error.SignalInterrupt
330 raise error.SignalInterrupt
323
331
324 ui = req.ui
332 ui = req.ui
325 try:
333 try:
326 for name in b'SIGBREAK', b'SIGHUP', b'SIGTERM':
334 for name in b'SIGBREAK', b'SIGHUP', b'SIGTERM':
327 num = getattr(signal, name, None)
335 num = getattr(signal, name, None)
328 if num:
336 if num:
329 signal.signal(num, catchterm)
337 signal.signal(num, catchterm)
330 except ValueError:
338 except ValueError:
331 pass # happens if called in a thread
339 pass # happens if called in a thread
332
340
333 def _runcatchfunc():
341 def _runcatchfunc():
334 realcmd = None
342 realcmd = None
335 try:
343 try:
336 cmdargs = fancyopts.fancyopts(
344 cmdargs = fancyopts.fancyopts(
337 req.args[:], commands.globalopts, {}
345 req.args[:], commands.globalopts, {}
338 )
346 )
339 cmd = cmdargs[0]
347 cmd = cmdargs[0]
340 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
348 aliases, entry = cmdutil.findcmd(cmd, commands.table, False)
341 realcmd = aliases[0]
349 realcmd = aliases[0]
342 except (
350 except (
343 error.UnknownCommand,
351 error.UnknownCommand,
344 error.AmbiguousCommand,
352 error.AmbiguousCommand,
345 IndexError,
353 IndexError,
346 getopt.GetoptError,
354 getopt.GetoptError,
347 ):
355 ):
348 # Don't handle this here. We know the command is
356 # Don't handle this here. We know the command is
349 # invalid, but all we're worried about for now is that
357 # invalid, but all we're worried about for now is that
350 # it's not a command that server operators expect to
358 # it's not a command that server operators expect to
351 # be safe to offer to users in a sandbox.
359 # be safe to offer to users in a sandbox.
352 pass
360 pass
353 if realcmd == b'serve' and b'--stdio' in cmdargs:
361 if realcmd == b'serve' and b'--stdio' in cmdargs:
354 # We want to constrain 'hg serve --stdio' instances pretty
362 # We want to constrain 'hg serve --stdio' instances pretty
355 # closely, as many shared-ssh access tools want to grant
363 # closely, as many shared-ssh access tools want to grant
356 # access to run *only* 'hg -R $repo serve --stdio'. We
364 # access to run *only* 'hg -R $repo serve --stdio'. We
357 # restrict to exactly that set of arguments, and prohibit
365 # restrict to exactly that set of arguments, and prohibit
358 # any repo name that starts with '--' to prevent
366 # any repo name that starts with '--' to prevent
359 # shenanigans wherein a user does something like pass
367 # shenanigans wherein a user does something like pass
360 # --debugger or --config=ui.debugger=1 as a repo
368 # --debugger or --config=ui.debugger=1 as a repo
361 # name. This used to actually run the debugger.
369 # name. This used to actually run the debugger.
362 if (
370 if (
363 len(req.args) != 4
371 len(req.args) != 4
364 or req.args[0] != b'-R'
372 or req.args[0] != b'-R'
365 or req.args[1].startswith(b'--')
373 or req.args[1].startswith(b'--')
366 or req.args[2] != b'serve'
374 or req.args[2] != b'serve'
367 or req.args[3] != b'--stdio'
375 or req.args[3] != b'--stdio'
368 ):
376 ):
369 raise error.Abort(
377 raise error.Abort(
370 _(b'potentially unsafe serve --stdio invocation: %s')
378 _(b'potentially unsafe serve --stdio invocation: %s')
371 % (stringutil.pprint(req.args),)
379 % (stringutil.pprint(req.args),)
372 )
380 )
373
381
374 try:
382 try:
375 debugger = b'pdb'
383 debugger = b'pdb'
376 debugtrace = {b'pdb': pdb.set_trace}
384 debugtrace = {b'pdb': pdb.set_trace}
377 debugmortem = {b'pdb': pdb.post_mortem}
385 debugmortem = {b'pdb': pdb.post_mortem}
378
386
379 # read --config before doing anything else
387 # read --config before doing anything else
380 # (e.g. to change trust settings for reading .hg/hgrc)
388 # (e.g. to change trust settings for reading .hg/hgrc)
381 cfgs = _parseconfig(req.ui, req.earlyoptions[b'config'])
389 cfgs = _parseconfig(req.ui, req.earlyoptions[b'config'])
382
390
383 if req.repo:
391 if req.repo:
384 # copy configs that were passed on the cmdline (--config) to
392 # copy configs that were passed on the cmdline (--config) to
385 # the repo ui
393 # the repo ui
386 for sec, name, val in cfgs:
394 for sec, name, val in cfgs:
387 req.repo.ui.setconfig(
395 req.repo.ui.setconfig(
388 sec, name, val, source=b'--config'
396 sec, name, val, source=b'--config'
389 )
397 )
390
398
391 # developer config: ui.debugger
399 # developer config: ui.debugger
392 debugger = ui.config(b"ui", b"debugger")
400 debugger = ui.config(b"ui", b"debugger")
393 debugmod = pdb
401 debugmod = pdb
394 if not debugger or ui.plain():
402 if not debugger or ui.plain():
395 # if we are in HGPLAIN mode, then disable custom debugging
403 # if we are in HGPLAIN mode, then disable custom debugging
396 debugger = b'pdb'
404 debugger = b'pdb'
397 elif req.earlyoptions[b'debugger']:
405 elif req.earlyoptions[b'debugger']:
398 # This import can be slow for fancy debuggers, so only
406 # This import can be slow for fancy debuggers, so only
399 # do it when absolutely necessary, i.e. when actual
407 # do it when absolutely necessary, i.e. when actual
400 # debugging has been requested
408 # debugging has been requested
401 with demandimport.deactivated():
409 with demandimport.deactivated():
402 try:
410 try:
403 debugmod = __import__(debugger)
411 debugmod = __import__(debugger)
404 except ImportError:
412 except ImportError:
405 pass # Leave debugmod = pdb
413 pass # Leave debugmod = pdb
406
414
407 debugtrace[debugger] = debugmod.set_trace
415 debugtrace[debugger] = debugmod.set_trace
408 debugmortem[debugger] = debugmod.post_mortem
416 debugmortem[debugger] = debugmod.post_mortem
409
417
410 # enter the debugger before command execution
418 # enter the debugger before command execution
411 if req.earlyoptions[b'debugger']:
419 if req.earlyoptions[b'debugger']:
412 ui.warn(
420 ui.warn(
413 _(
421 _(
414 b"entering debugger - "
422 b"entering debugger - "
415 b"type c to continue starting hg or h for help\n"
423 b"type c to continue starting hg or h for help\n"
416 )
424 )
417 )
425 )
418
426
419 if (
427 if (
420 debugger != b'pdb'
428 debugger != b'pdb'
421 and debugtrace[debugger] == debugtrace[b'pdb']
429 and debugtrace[debugger] == debugtrace[b'pdb']
422 ):
430 ):
423 ui.warn(
431 ui.warn(
424 _(
432 _(
425 b"%s debugger specified "
433 b"%s debugger specified "
426 b"but its module was not found\n"
434 b"but its module was not found\n"
427 )
435 )
428 % debugger
436 % debugger
429 )
437 )
430 with demandimport.deactivated():
438 with demandimport.deactivated():
431 debugtrace[debugger]()
439 debugtrace[debugger]()
432 try:
440 try:
433 return _dispatch(req)
441 return _dispatch(req)
434 finally:
442 finally:
435 ui.flush()
443 ui.flush()
436 except: # re-raises
444 except: # re-raises
437 # enter the debugger when we hit an exception
445 # enter the debugger when we hit an exception
438 if req.earlyoptions[b'debugger']:
446 if req.earlyoptions[b'debugger']:
439 traceback.print_exc()
447 traceback.print_exc()
440 debugmortem[debugger](sys.exc_info()[2])
448 debugmortem[debugger](sys.exc_info()[2])
441 raise
449 raise
442
450
443 return _callcatch(ui, _runcatchfunc)
451 return _callcatch(ui, _runcatchfunc)
444
452
445
453
446 def _callcatch(ui, func):
454 def _callcatch(ui, func):
447 """like scmutil.callcatch but handles more high-level exceptions about
455 """like scmutil.callcatch but handles more high-level exceptions about
448 config parsing and commands. besides, use handlecommandexception to handle
456 config parsing and commands. besides, use handlecommandexception to handle
449 uncaught exceptions.
457 uncaught exceptions.
450 """
458 """
451 try:
459 try:
452 return scmutil.callcatch(ui, func)
460 return scmutil.callcatch(ui, func)
453 except error.AmbiguousCommand as inst:
461 except error.AmbiguousCommand as inst:
454 ui.warn(
462 ui.warn(
455 _(b"hg: command '%s' is ambiguous:\n %s\n")
463 _(b"hg: command '%s' is ambiguous:\n %s\n")
456 % (inst.prefix, b" ".join(inst.matches))
464 % (inst.prefix, b" ".join(inst.matches))
457 )
465 )
458 except error.CommandError as inst:
466 except error.CommandError as inst:
459 if inst.command:
467 if inst.command:
460 ui.pager(b'help')
468 ui.pager(b'help')
461 msgbytes = pycompat.bytestr(inst.message)
469 msgbytes = pycompat.bytestr(inst.message)
462 ui.warn(_(b"hg %s: %s\n") % (inst.command, msgbytes))
470 ui.warn(_(b"hg %s: %s\n") % (inst.command, msgbytes))
463 commands.help_(ui, inst.command, full=False, command=True)
471 commands.help_(ui, inst.command, full=False, command=True)
464 else:
472 else:
465 ui.warn(_(b"hg: %s\n") % inst.message)
473 ui.warn(_(b"hg: %s\n") % inst.message)
466 ui.warn(_(b"(use 'hg help -v' for a list of global options)\n"))
474 ui.warn(_(b"(use 'hg help -v' for a list of global options)\n"))
467 except error.UnknownCommand as inst:
475 except error.UnknownCommand as inst:
468 nocmdmsg = _(b"hg: unknown command '%s'\n") % inst.command
476 nocmdmsg = _(b"hg: unknown command '%s'\n") % inst.command
469 try:
477 try:
470 # check if the command is in a disabled extension
478 # check if the command is in a disabled extension
471 # (but don't check for extensions themselves)
479 # (but don't check for extensions themselves)
472 formatted = help.formattedhelp(
480 formatted = help.formattedhelp(
473 ui, commands, inst.command, unknowncmd=True
481 ui, commands, inst.command, unknowncmd=True
474 )
482 )
475 ui.warn(nocmdmsg)
483 ui.warn(nocmdmsg)
476 ui.write(formatted)
484 ui.write(formatted)
477 except (error.UnknownCommand, error.Abort):
485 except (error.UnknownCommand, error.Abort):
478 suggested = False
486 suggested = False
479 if inst.all_commands:
487 if inst.all_commands:
480 sim = error.getsimilar(inst.all_commands, inst.command)
488 sim = error.getsimilar(inst.all_commands, inst.command)
481 if sim:
489 if sim:
482 ui.warn(nocmdmsg)
490 ui.warn(nocmdmsg)
483 ui.warn(b"(%s)\n" % error.similarity_hint(sim))
491 ui.warn(b"(%s)\n" % error.similarity_hint(sim))
484 suggested = True
492 suggested = True
485 if not suggested:
493 if not suggested:
486 ui.warn(nocmdmsg)
494 ui.warn(nocmdmsg)
487 ui.warn(_(b"(use 'hg help' for a list of commands)\n"))
495 ui.warn(_(b"(use 'hg help' for a list of commands)\n"))
488 except IOError:
496 except IOError:
489 raise
497 raise
490 except KeyboardInterrupt:
498 except KeyboardInterrupt:
491 raise
499 raise
492 except: # probably re-raises
500 except: # probably re-raises
493 if not handlecommandexception(ui):
501 if not handlecommandexception(ui):
494 raise
502 raise
495
503
496 return -1
504 return -1
497
505
498
506
499 def aliasargs(fn, givenargs):
507 def aliasargs(fn, givenargs):
500 args = []
508 args = []
501 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
509 # only care about alias 'args', ignore 'args' set by extensions.wrapfunction
502 if not util.safehasattr(fn, b'_origfunc'):
510 if not util.safehasattr(fn, b'_origfunc'):
503 args = getattr(fn, 'args', args)
511 args = getattr(fn, 'args', args)
504 if args:
512 if args:
505 cmd = b' '.join(map(procutil.shellquote, args))
513 cmd = b' '.join(map(procutil.shellquote, args))
506
514
507 nums = []
515 nums = []
508
516
509 def replacer(m):
517 def replacer(m):
510 num = int(m.group(1)) - 1
518 num = int(m.group(1)) - 1
511 nums.append(num)
519 nums.append(num)
512 if num < len(givenargs):
520 if num < len(givenargs):
513 return givenargs[num]
521 return givenargs[num]
514 raise error.InputError(_(b'too few arguments for command alias'))
522 raise error.InputError(_(b'too few arguments for command alias'))
515
523
516 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
524 cmd = re.sub(br'\$(\d+|\$)', replacer, cmd)
517 givenargs = [x for i, x in enumerate(givenargs) if i not in nums]
525 givenargs = [x for i, x in enumerate(givenargs) if i not in nums]
518 args = pycompat.shlexsplit(cmd)
526 args = pycompat.shlexsplit(cmd)
519 return args + givenargs
527 return args + givenargs
520
528
521
529
522 def aliasinterpolate(name, args, cmd):
530 def aliasinterpolate(name, args, cmd):
523 """interpolate args into cmd for shell aliases
531 """interpolate args into cmd for shell aliases
524
532
525 This also handles $0, $@ and "$@".
533 This also handles $0, $@ and "$@".
526 """
534 """
527 # util.interpolate can't deal with "$@" (with quotes) because it's only
535 # util.interpolate can't deal with "$@" (with quotes) because it's only
528 # built to match prefix + patterns.
536 # built to match prefix + patterns.
529 replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)}
537 replacemap = {b'$%d' % (i + 1): arg for i, arg in enumerate(args)}
530 replacemap[b'$0'] = name
538 replacemap[b'$0'] = name
531 replacemap[b'$$'] = b'$'
539 replacemap[b'$$'] = b'$'
532 replacemap[b'$@'] = b' '.join(args)
540 replacemap[b'$@'] = b' '.join(args)
533 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
541 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
534 # parameters, separated out into words. Emulate the same behavior here by
542 # parameters, separated out into words. Emulate the same behavior here by
535 # quoting the arguments individually. POSIX shells will then typically
543 # quoting the arguments individually. POSIX shells will then typically
536 # tokenize each argument into exactly one word.
544 # tokenize each argument into exactly one word.
537 replacemap[b'"$@"'] = b' '.join(procutil.shellquote(arg) for arg in args)
545 replacemap[b'"$@"'] = b' '.join(procutil.shellquote(arg) for arg in args)
538 # escape '\$' for regex
546 # escape '\$' for regex
539 regex = b'|'.join(replacemap.keys()).replace(b'$', br'\$')
547 regex = b'|'.join(replacemap.keys()).replace(b'$', br'\$')
540 r = re.compile(regex)
548 r = re.compile(regex)
541 return r.sub(lambda x: replacemap[x.group()], cmd)
549 return r.sub(lambda x: replacemap[x.group()], cmd)
542
550
543
551
544 class cmdalias(object):
552 class cmdalias(object):
545 def __init__(self, ui, name, definition, cmdtable, source):
553 def __init__(self, ui, name, definition, cmdtable, source):
546 self.name = self.cmd = name
554 self.name = self.cmd = name
547 self.cmdname = b''
555 self.cmdname = b''
548 self.definition = definition
556 self.definition = definition
549 self.fn = None
557 self.fn = None
550 self.givenargs = []
558 self.givenargs = []
551 self.opts = []
559 self.opts = []
552 self.help = b''
560 self.help = b''
553 self.badalias = None
561 self.badalias = None
554 self.unknowncmd = False
562 self.unknowncmd = False
555 self.source = source
563 self.source = source
556
564
557 try:
565 try:
558 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
566 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
559 for alias, e in pycompat.iteritems(cmdtable):
567 for alias, e in pycompat.iteritems(cmdtable):
560 if e is entry:
568 if e is entry:
561 self.cmd = alias
569 self.cmd = alias
562 break
570 break
563 self.shadows = True
571 self.shadows = True
564 except error.UnknownCommand:
572 except error.UnknownCommand:
565 self.shadows = False
573 self.shadows = False
566
574
567 if not self.definition:
575 if not self.definition:
568 self.badalias = _(b"no definition for alias '%s'") % self.name
576 self.badalias = _(b"no definition for alias '%s'") % self.name
569 return
577 return
570
578
571 if self.definition.startswith(b'!'):
579 if self.definition.startswith(b'!'):
572 shdef = self.definition[1:]
580 shdef = self.definition[1:]
573 self.shell = True
581 self.shell = True
574
582
575 def fn(ui, *args):
583 def fn(ui, *args):
576 env = {b'HG_ARGS': b' '.join((self.name,) + args)}
584 env = {b'HG_ARGS': b' '.join((self.name,) + args)}
577
585
578 def _checkvar(m):
586 def _checkvar(m):
579 if m.groups()[0] == b'$':
587 if m.groups()[0] == b'$':
580 return m.group()
588 return m.group()
581 elif int(m.groups()[0]) <= len(args):
589 elif int(m.groups()[0]) <= len(args):
582 return m.group()
590 return m.group()
583 else:
591 else:
584 ui.debug(
592 ui.debug(
585 b"No argument found for substitution "
593 b"No argument found for substitution "
586 b"of %i variable in alias '%s' definition.\n"
594 b"of %i variable in alias '%s' definition.\n"
587 % (int(m.groups()[0]), self.name)
595 % (int(m.groups()[0]), self.name)
588 )
596 )
589 return b''
597 return b''
590
598
591 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
599 cmd = re.sub(br'\$(\d+|\$)', _checkvar, shdef)
592 cmd = aliasinterpolate(self.name, args, cmd)
600 cmd = aliasinterpolate(self.name, args, cmd)
593 return ui.system(
601 return ui.system(
594 cmd, environ=env, blockedtag=b'alias_%s' % self.name
602 cmd, environ=env, blockedtag=b'alias_%s' % self.name
595 )
603 )
596
604
597 self.fn = fn
605 self.fn = fn
598 self.alias = True
606 self.alias = True
599 self._populatehelp(ui, name, shdef, self.fn)
607 self._populatehelp(ui, name, shdef, self.fn)
600 return
608 return
601
609
602 try:
610 try:
603 args = pycompat.shlexsplit(self.definition)
611 args = pycompat.shlexsplit(self.definition)
604 except ValueError as inst:
612 except ValueError as inst:
605 self.badalias = _(b"error in definition for alias '%s': %s") % (
613 self.badalias = _(b"error in definition for alias '%s': %s") % (
606 self.name,
614 self.name,
607 stringutil.forcebytestr(inst),
615 stringutil.forcebytestr(inst),
608 )
616 )
609 return
617 return
610 earlyopts, args = _earlysplitopts(args)
618 earlyopts, args = _earlysplitopts(args)
611 if earlyopts:
619 if earlyopts:
612 self.badalias = _(
620 self.badalias = _(
613 b"error in definition for alias '%s': %s may "
621 b"error in definition for alias '%s': %s may "
614 b"only be given on the command line"
622 b"only be given on the command line"
615 ) % (self.name, b'/'.join(pycompat.ziplist(*earlyopts)[0]))
623 ) % (self.name, b'/'.join(pycompat.ziplist(*earlyopts)[0]))
616 return
624 return
617 self.cmdname = cmd = args.pop(0)
625 self.cmdname = cmd = args.pop(0)
618 self.givenargs = args
626 self.givenargs = args
619
627
620 try:
628 try:
621 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
629 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
622 if len(tableentry) > 2:
630 if len(tableentry) > 2:
623 self.fn, self.opts, cmdhelp = tableentry
631 self.fn, self.opts, cmdhelp = tableentry
624 else:
632 else:
625 self.fn, self.opts = tableentry
633 self.fn, self.opts = tableentry
626 cmdhelp = None
634 cmdhelp = None
627
635
628 self.alias = True
636 self.alias = True
629 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
637 self._populatehelp(ui, name, cmd, self.fn, cmdhelp)
630
638
631 except error.UnknownCommand:
639 except error.UnknownCommand:
632 self.badalias = _(
640 self.badalias = _(
633 b"alias '%s' resolves to unknown command '%s'"
641 b"alias '%s' resolves to unknown command '%s'"
634 ) % (
642 ) % (
635 self.name,
643 self.name,
636 cmd,
644 cmd,
637 )
645 )
638 self.unknowncmd = True
646 self.unknowncmd = True
639 except error.AmbiguousCommand:
647 except error.AmbiguousCommand:
640 self.badalias = _(
648 self.badalias = _(
641 b"alias '%s' resolves to ambiguous command '%s'"
649 b"alias '%s' resolves to ambiguous command '%s'"
642 ) % (
650 ) % (
643 self.name,
651 self.name,
644 cmd,
652 cmd,
645 )
653 )
646
654
647 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
655 def _populatehelp(self, ui, name, cmd, fn, defaulthelp=None):
648 # confine strings to be passed to i18n.gettext()
656 # confine strings to be passed to i18n.gettext()
649 cfg = {}
657 cfg = {}
650 for k in (b'doc', b'help', b'category'):
658 for k in (b'doc', b'help', b'category'):
651 v = ui.config(b'alias', b'%s:%s' % (name, k), None)
659 v = ui.config(b'alias', b'%s:%s' % (name, k), None)
652 if v is None:
660 if v is None:
653 continue
661 continue
654 if not encoding.isasciistr(v):
662 if not encoding.isasciistr(v):
655 self.badalias = _(
663 self.badalias = _(
656 b"non-ASCII character in alias definition '%s:%s'"
664 b"non-ASCII character in alias definition '%s:%s'"
657 ) % (name, k)
665 ) % (name, k)
658 return
666 return
659 cfg[k] = v
667 cfg[k] = v
660
668
661 self.help = cfg.get(b'help', defaulthelp or b'')
669 self.help = cfg.get(b'help', defaulthelp or b'')
662 if self.help and self.help.startswith(b"hg " + cmd):
670 if self.help and self.help.startswith(b"hg " + cmd):
663 # drop prefix in old-style help lines so hg shows the alias
671 # drop prefix in old-style help lines so hg shows the alias
664 self.help = self.help[4 + len(cmd) :]
672 self.help = self.help[4 + len(cmd) :]
665
673
666 self.owndoc = b'doc' in cfg
674 self.owndoc = b'doc' in cfg
667 doc = cfg.get(b'doc', pycompat.getdoc(fn))
675 doc = cfg.get(b'doc', pycompat.getdoc(fn))
668 if doc is not None:
676 if doc is not None:
669 doc = pycompat.sysstr(doc)
677 doc = pycompat.sysstr(doc)
670 self.__doc__ = doc
678 self.__doc__ = doc
671
679
672 self.helpcategory = cfg.get(
680 self.helpcategory = cfg.get(
673 b'category', registrar.command.CATEGORY_NONE
681 b'category', registrar.command.CATEGORY_NONE
674 )
682 )
675
683
676 @property
684 @property
677 def args(self):
685 def args(self):
678 args = pycompat.maplist(util.expandpath, self.givenargs)
686 args = pycompat.maplist(util.expandpath, self.givenargs)
679 return aliasargs(self.fn, args)
687 return aliasargs(self.fn, args)
680
688
681 def __getattr__(self, name):
689 def __getattr__(self, name):
682 adefaults = {
690 adefaults = {
683 'norepo': True,
691 'norepo': True,
684 'intents': set(),
692 'intents': set(),
685 'optionalrepo': False,
693 'optionalrepo': False,
686 'inferrepo': False,
694 'inferrepo': False,
687 }
695 }
688 if name not in adefaults:
696 if name not in adefaults:
689 raise AttributeError(name)
697 raise AttributeError(name)
690 if self.badalias or util.safehasattr(self, b'shell'):
698 if self.badalias or util.safehasattr(self, b'shell'):
691 return adefaults[name]
699 return adefaults[name]
692 return getattr(self.fn, name)
700 return getattr(self.fn, name)
693
701
694 def __call__(self, ui, *args, **opts):
702 def __call__(self, ui, *args, **opts):
695 if self.badalias:
703 if self.badalias:
696 hint = None
704 hint = None
697 if self.unknowncmd:
705 if self.unknowncmd:
698 try:
706 try:
699 # check if the command is in a disabled extension
707 # check if the command is in a disabled extension
700 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
708 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
701 hint = _(b"'%s' is provided by '%s' extension") % (cmd, ext)
709 hint = _(b"'%s' is provided by '%s' extension") % (cmd, ext)
702 except error.UnknownCommand:
710 except error.UnknownCommand:
703 pass
711 pass
704 raise error.ConfigError(self.badalias, hint=hint)
712 raise error.ConfigError(self.badalias, hint=hint)
705 if self.shadows:
713 if self.shadows:
706 ui.debug(
714 ui.debug(
707 b"alias '%s' shadows command '%s'\n" % (self.name, self.cmdname)
715 b"alias '%s' shadows command '%s'\n" % (self.name, self.cmdname)
708 )
716 )
709
717
710 ui.log(
718 ui.log(
711 b'commandalias',
719 b'commandalias',
712 b"alias '%s' expands to '%s'\n",
720 b"alias '%s' expands to '%s'\n",
713 self.name,
721 self.name,
714 self.definition,
722 self.definition,
715 )
723 )
716 if util.safehasattr(self, b'shell'):
724 if util.safehasattr(self, b'shell'):
717 return self.fn(ui, *args, **opts)
725 return self.fn(ui, *args, **opts)
718 else:
726 else:
719 try:
727 try:
720 return util.checksignature(self.fn)(ui, *args, **opts)
728 return util.checksignature(self.fn)(ui, *args, **opts)
721 except error.SignatureError:
729 except error.SignatureError:
722 args = b' '.join([self.cmdname] + self.args)
730 args = b' '.join([self.cmdname] + self.args)
723 ui.debug(b"alias '%s' expands to '%s'\n" % (self.name, args))
731 ui.debug(b"alias '%s' expands to '%s'\n" % (self.name, args))
724 raise
732 raise
725
733
726
734
727 class lazyaliasentry(object):
735 class lazyaliasentry(object):
728 """like a typical command entry (func, opts, help), but is lazy"""
736 """like a typical command entry (func, opts, help), but is lazy"""
729
737
730 def __init__(self, ui, name, definition, cmdtable, source):
738 def __init__(self, ui, name, definition, cmdtable, source):
731 self.ui = ui
739 self.ui = ui
732 self.name = name
740 self.name = name
733 self.definition = definition
741 self.definition = definition
734 self.cmdtable = cmdtable.copy()
742 self.cmdtable = cmdtable.copy()
735 self.source = source
743 self.source = source
736 self.alias = True
744 self.alias = True
737
745
738 @util.propertycache
746 @util.propertycache
739 def _aliasdef(self):
747 def _aliasdef(self):
740 return cmdalias(
748 return cmdalias(
741 self.ui, self.name, self.definition, self.cmdtable, self.source
749 self.ui, self.name, self.definition, self.cmdtable, self.source
742 )
750 )
743
751
744 def __getitem__(self, n):
752 def __getitem__(self, n):
745 aliasdef = self._aliasdef
753 aliasdef = self._aliasdef
746 if n == 0:
754 if n == 0:
747 return aliasdef
755 return aliasdef
748 elif n == 1:
756 elif n == 1:
749 return aliasdef.opts
757 return aliasdef.opts
750 elif n == 2:
758 elif n == 2:
751 return aliasdef.help
759 return aliasdef.help
752 else:
760 else:
753 raise IndexError
761 raise IndexError
754
762
755 def __iter__(self):
763 def __iter__(self):
756 for i in range(3):
764 for i in range(3):
757 yield self[i]
765 yield self[i]
758
766
759 def __len__(self):
767 def __len__(self):
760 return 3
768 return 3
761
769
762
770
763 def addaliases(ui, cmdtable):
771 def addaliases(ui, cmdtable):
764 # aliases are processed after extensions have been loaded, so they
772 # aliases are processed after extensions have been loaded, so they
765 # may use extension commands. Aliases can also use other alias definitions,
773 # may use extension commands. Aliases can also use other alias definitions,
766 # but only if they have been defined prior to the current definition.
774 # but only if they have been defined prior to the current definition.
767 for alias, definition in ui.configitems(b'alias', ignoresub=True):
775 for alias, definition in ui.configitems(b'alias', ignoresub=True):
768 try:
776 try:
769 if cmdtable[alias].definition == definition:
777 if cmdtable[alias].definition == definition:
770 continue
778 continue
771 except (KeyError, AttributeError):
779 except (KeyError, AttributeError):
772 # definition might not exist or it might not be a cmdalias
780 # definition might not exist or it might not be a cmdalias
773 pass
781 pass
774
782
775 source = ui.configsource(b'alias', alias)
783 source = ui.configsource(b'alias', alias)
776 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
784 entry = lazyaliasentry(ui, alias, definition, cmdtable, source)
777 cmdtable[alias] = entry
785 cmdtable[alias] = entry
778
786
779
787
780 def _parse(ui, args):
788 def _parse(ui, args):
781 options = {}
789 options = {}
782 cmdoptions = {}
790 cmdoptions = {}
783
791
784 try:
792 try:
785 args = fancyopts.fancyopts(args, commands.globalopts, options)
793 args = fancyopts.fancyopts(args, commands.globalopts, options)
786 except getopt.GetoptError as inst:
794 except getopt.GetoptError as inst:
787 raise error.CommandError(None, stringutil.forcebytestr(inst))
795 raise error.CommandError(None, stringutil.forcebytestr(inst))
788
796
789 if args:
797 if args:
790 cmd, args = args[0], args[1:]
798 cmd, args = args[0], args[1:]
791 aliases, entry = cmdutil.findcmd(
799 aliases, entry = cmdutil.findcmd(
792 cmd, commands.table, ui.configbool(b"ui", b"strict")
800 cmd, commands.table, ui.configbool(b"ui", b"strict")
793 )
801 )
794 cmd = aliases[0]
802 cmd = aliases[0]
795 args = aliasargs(entry[0], args)
803 args = aliasargs(entry[0], args)
796 defaults = ui.config(b"defaults", cmd)
804 defaults = ui.config(b"defaults", cmd)
797 if defaults:
805 if defaults:
798 args = (
806 args = (
799 pycompat.maplist(util.expandpath, pycompat.shlexsplit(defaults))
807 pycompat.maplist(util.expandpath, pycompat.shlexsplit(defaults))
800 + args
808 + args
801 )
809 )
802 c = list(entry[1])
810 c = list(entry[1])
803 else:
811 else:
804 cmd = None
812 cmd = None
805 c = []
813 c = []
806
814
807 # combine global options into local
815 # combine global options into local
808 for o in commands.globalopts:
816 for o in commands.globalopts:
809 c.append((o[0], o[1], options[o[1]], o[3]))
817 c.append((o[0], o[1], options[o[1]], o[3]))
810
818
811 try:
819 try:
812 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
820 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
813 except getopt.GetoptError as inst:
821 except getopt.GetoptError as inst:
814 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
822 raise error.CommandError(cmd, stringutil.forcebytestr(inst))
815
823
816 # separate global options back out
824 # separate global options back out
817 for o in commands.globalopts:
825 for o in commands.globalopts:
818 n = o[1]
826 n = o[1]
819 options[n] = cmdoptions[n]
827 options[n] = cmdoptions[n]
820 del cmdoptions[n]
828 del cmdoptions[n]
821
829
822 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
830 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
823
831
824
832
825 def _parseconfig(ui, config):
833 def _parseconfig(ui, config):
826 """parse the --config options from the command line"""
834 """parse the --config options from the command line"""
827 configs = []
835 configs = []
828
836
829 for cfg in config:
837 for cfg in config:
830 try:
838 try:
831 name, value = [cfgelem.strip() for cfgelem in cfg.split(b'=', 1)]
839 name, value = [cfgelem.strip() for cfgelem in cfg.split(b'=', 1)]
832 section, name = name.split(b'.', 1)
840 section, name = name.split(b'.', 1)
833 if not section or not name:
841 if not section or not name:
834 raise IndexError
842 raise IndexError
835 ui.setconfig(section, name, value, b'--config')
843 ui.setconfig(section, name, value, b'--config')
836 configs.append((section, name, value))
844 configs.append((section, name, value))
837 except (IndexError, ValueError):
845 except (IndexError, ValueError):
838 raise error.Abort(
846 raise error.Abort(
839 _(
847 _(
840 b'malformed --config option: %r '
848 b'malformed --config option: %r '
841 b'(use --config section.name=value)'
849 b'(use --config section.name=value)'
842 )
850 )
843 % pycompat.bytestr(cfg)
851 % pycompat.bytestr(cfg)
844 )
852 )
845
853
846 return configs
854 return configs
847
855
848
856
849 def _earlyparseopts(ui, args):
857 def _earlyparseopts(ui, args):
850 options = {}
858 options = {}
851 fancyopts.fancyopts(
859 fancyopts.fancyopts(
852 args,
860 args,
853 commands.globalopts,
861 commands.globalopts,
854 options,
862 options,
855 gnu=not ui.plain(b'strictflags'),
863 gnu=not ui.plain(b'strictflags'),
856 early=True,
864 early=True,
857 optaliases={b'repository': [b'repo']},
865 optaliases={b'repository': [b'repo']},
858 )
866 )
859 return options
867 return options
860
868
861
869
862 def _earlysplitopts(args):
870 def _earlysplitopts(args):
863 """Split args into a list of possible early options and remainder args"""
871 """Split args into a list of possible early options and remainder args"""
864 shortoptions = b'R:'
872 shortoptions = b'R:'
865 # TODO: perhaps 'debugger' should be included
873 # TODO: perhaps 'debugger' should be included
866 longoptions = [b'cwd=', b'repository=', b'repo=', b'config=']
874 longoptions = [b'cwd=', b'repository=', b'repo=', b'config=']
867 return fancyopts.earlygetopt(
875 return fancyopts.earlygetopt(
868 args, shortoptions, longoptions, gnu=True, keepsep=True
876 args, shortoptions, longoptions, gnu=True, keepsep=True
869 )
877 )
870
878
871
879
872 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
880 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
873 # run pre-hook, and abort if it fails
881 # run pre-hook, and abort if it fails
874 hook.hook(
882 hook.hook(
875 lui,
883 lui,
876 repo,
884 repo,
877 b"pre-%s" % cmd,
885 b"pre-%s" % cmd,
878 True,
886 True,
879 args=b" ".join(fullargs),
887 args=b" ".join(fullargs),
880 pats=cmdpats,
888 pats=cmdpats,
881 opts=cmdoptions,
889 opts=cmdoptions,
882 )
890 )
883 try:
891 try:
884 ret = _runcommand(ui, options, cmd, d)
892 ret = _runcommand(ui, options, cmd, d)
885 # run post-hook, passing command result
893 # run post-hook, passing command result
886 hook.hook(
894 hook.hook(
887 lui,
895 lui,
888 repo,
896 repo,
889 b"post-%s" % cmd,
897 b"post-%s" % cmd,
890 False,
898 False,
891 args=b" ".join(fullargs),
899 args=b" ".join(fullargs),
892 result=ret,
900 result=ret,
893 pats=cmdpats,
901 pats=cmdpats,
894 opts=cmdoptions,
902 opts=cmdoptions,
895 )
903 )
896 except Exception:
904 except Exception:
897 # run failure hook and re-raise
905 # run failure hook and re-raise
898 hook.hook(
906 hook.hook(
899 lui,
907 lui,
900 repo,
908 repo,
901 b"fail-%s" % cmd,
909 b"fail-%s" % cmd,
902 False,
910 False,
903 args=b" ".join(fullargs),
911 args=b" ".join(fullargs),
904 pats=cmdpats,
912 pats=cmdpats,
905 opts=cmdoptions,
913 opts=cmdoptions,
906 )
914 )
907 raise
915 raise
908 return ret
916 return ret
909
917
910
918
911 def _readsharedsourceconfig(ui, path):
919 def _readsharedsourceconfig(ui, path):
912 """if the current repository is shared one, this tries to read
920 """if the current repository is shared one, this tries to read
913 .hg/hgrc of shared source if we are in share-safe mode
921 .hg/hgrc of shared source if we are in share-safe mode
914
922
915 Config read is loaded into the ui object passed
923 Config read is loaded into the ui object passed
916
924
917 This should be called before reading .hg/hgrc or the main repo
925 This should be called before reading .hg/hgrc or the main repo
918 as that overrides config set in shared source"""
926 as that overrides config set in shared source"""
919 try:
927 try:
920 with open(os.path.join(path, b".hg", b"requires"), "rb") as fp:
928 with open(os.path.join(path, b".hg", b"requires"), "rb") as fp:
921 requirements = set(fp.read().splitlines())
929 requirements = set(fp.read().splitlines())
922 if not (
930 if not (
923 requirementsmod.SHARESAFE_REQUIREMENT in requirements
931 requirementsmod.SHARESAFE_REQUIREMENT in requirements
924 and requirementsmod.SHARED_REQUIREMENT in requirements
932 and requirementsmod.SHARED_REQUIREMENT in requirements
925 ):
933 ):
926 return
934 return
927 hgvfs = vfs.vfs(os.path.join(path, b".hg"))
935 hgvfs = vfs.vfs(os.path.join(path, b".hg"))
928 sharedvfs = localrepo._getsharedvfs(hgvfs, requirements)
936 sharedvfs = localrepo._getsharedvfs(hgvfs, requirements)
929 root = sharedvfs.base
937 root = sharedvfs.base
930 ui.readconfig(sharedvfs.join(b"hgrc"), root)
938 ui.readconfig(sharedvfs.join(b"hgrc"), root)
931 except IOError:
939 except IOError:
932 pass
940 pass
933
941
934
942
935 def _getlocal(ui, rpath, wd=None):
943 def _getlocal(ui, rpath, wd=None):
936 """Return (path, local ui object) for the given target path.
944 """Return (path, local ui object) for the given target path.
937
945
938 Takes paths in [cwd]/.hg/hgrc into account."
946 Takes paths in [cwd]/.hg/hgrc into account."
939 """
947 """
940 if wd is None:
948 if wd is None:
941 try:
949 try:
942 wd = encoding.getcwd()
950 wd = encoding.getcwd()
943 except OSError as e:
951 except OSError as e:
944 raise error.Abort(
952 raise error.Abort(
945 _(b"error getting current working directory: %s")
953 _(b"error getting current working directory: %s")
946 % encoding.strtolocal(e.strerror)
954 % encoding.strtolocal(e.strerror)
947 )
955 )
948
956
949 path = cmdutil.findrepo(wd) or b""
957 path = cmdutil.findrepo(wd) or b""
950 if not path:
958 if not path:
951 lui = ui
959 lui = ui
952 else:
960 else:
953 lui = ui.copy()
961 lui = ui.copy()
954 if rcutil.use_repo_hgrc():
962 if rcutil.use_repo_hgrc():
955 _readsharedsourceconfig(lui, path)
963 _readsharedsourceconfig(lui, path)
956 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
964 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
957 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
965 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
958
966
959 if rpath:
967 if rpath:
960 path = lui.expandpath(rpath)
968 path = lui.expandpath(rpath)
961 lui = ui.copy()
969 lui = ui.copy()
962 if rcutil.use_repo_hgrc():
970 if rcutil.use_repo_hgrc():
963 _readsharedsourceconfig(lui, path)
971 _readsharedsourceconfig(lui, path)
964 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
972 lui.readconfig(os.path.join(path, b".hg", b"hgrc"), path)
965 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
973 lui.readconfig(os.path.join(path, b".hg", b"hgrc-not-shared"), path)
966
974
967 return path, lui
975 return path, lui
968
976
969
977
970 def _checkshellalias(lui, ui, args):
978 def _checkshellalias(lui, ui, args):
971 """Return the function to run the shell alias, if it is required"""
979 """Return the function to run the shell alias, if it is required"""
972 options = {}
980 options = {}
973
981
974 try:
982 try:
975 args = fancyopts.fancyopts(args, commands.globalopts, options)
983 args = fancyopts.fancyopts(args, commands.globalopts, options)
976 except getopt.GetoptError:
984 except getopt.GetoptError:
977 return
985 return
978
986
979 if not args:
987 if not args:
980 return
988 return
981
989
982 cmdtable = commands.table
990 cmdtable = commands.table
983
991
984 cmd = args[0]
992 cmd = args[0]
985 try:
993 try:
986 strict = ui.configbool(b"ui", b"strict")
994 strict = ui.configbool(b"ui", b"strict")
987 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
995 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
988 except (error.AmbiguousCommand, error.UnknownCommand):
996 except (error.AmbiguousCommand, error.UnknownCommand):
989 return
997 return
990
998
991 cmd = aliases[0]
999 cmd = aliases[0]
992 fn = entry[0]
1000 fn = entry[0]
993
1001
994 if cmd and util.safehasattr(fn, b'shell'):
1002 if cmd and util.safehasattr(fn, b'shell'):
995 # shell alias shouldn't receive early options which are consumed by hg
1003 # shell alias shouldn't receive early options which are consumed by hg
996 _earlyopts, args = _earlysplitopts(args)
1004 _earlyopts, args = _earlysplitopts(args)
997 d = lambda: fn(ui, *args[1:])
1005 d = lambda: fn(ui, *args[1:])
998 return lambda: runcommand(
1006 return lambda: runcommand(
999 lui, None, cmd, args[:1], ui, options, d, [], {}
1007 lui, None, cmd, args[:1], ui, options, d, [], {}
1000 )
1008 )
1001
1009
1002
1010
1003 def _dispatch(req):
1011 def _dispatch(req):
1004 args = req.args
1012 args = req.args
1005 ui = req.ui
1013 ui = req.ui
1006
1014
1007 # check for cwd
1015 # check for cwd
1008 cwd = req.earlyoptions[b'cwd']
1016 cwd = req.earlyoptions[b'cwd']
1009 if cwd:
1017 if cwd:
1010 os.chdir(cwd)
1018 os.chdir(cwd)
1011
1019
1012 rpath = req.earlyoptions[b'repository']
1020 rpath = req.earlyoptions[b'repository']
1013 path, lui = _getlocal(ui, rpath)
1021 path, lui = _getlocal(ui, rpath)
1014
1022
1015 uis = {ui, lui}
1023 uis = {ui, lui}
1016
1024
1017 if req.repo:
1025 if req.repo:
1018 uis.add(req.repo.ui)
1026 uis.add(req.repo.ui)
1019
1027
1020 if (
1028 if (
1021 req.earlyoptions[b'verbose']
1029 req.earlyoptions[b'verbose']
1022 or req.earlyoptions[b'debug']
1030 or req.earlyoptions[b'debug']
1023 or req.earlyoptions[b'quiet']
1031 or req.earlyoptions[b'quiet']
1024 ):
1032 ):
1025 for opt in (b'verbose', b'debug', b'quiet'):
1033 for opt in (b'verbose', b'debug', b'quiet'):
1026 val = pycompat.bytestr(bool(req.earlyoptions[opt]))
1034 val = pycompat.bytestr(bool(req.earlyoptions[opt]))
1027 for ui_ in uis:
1035 for ui_ in uis:
1028 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1036 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1029
1037
1030 if req.earlyoptions[b'profile']:
1038 if req.earlyoptions[b'profile']:
1031 for ui_ in uis:
1039 for ui_ in uis:
1032 ui_.setconfig(b'profiling', b'enabled', b'true', b'--profile')
1040 ui_.setconfig(b'profiling', b'enabled', b'true', b'--profile')
1033
1041
1034 profile = lui.configbool(b'profiling', b'enabled')
1042 profile = lui.configbool(b'profiling', b'enabled')
1035 with profiling.profile(lui, enabled=profile) as profiler:
1043 with profiling.profile(lui, enabled=profile) as profiler:
1036 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
1044 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
1037 # reposetup
1045 # reposetup
1038 extensions.loadall(lui)
1046 extensions.loadall(lui)
1039 # Propagate any changes to lui.__class__ by extensions
1047 # Propagate any changes to lui.__class__ by extensions
1040 ui.__class__ = lui.__class__
1048 ui.__class__ = lui.__class__
1041
1049
1042 # (uisetup and extsetup are handled in extensions.loadall)
1050 # (uisetup and extsetup are handled in extensions.loadall)
1043
1051
1044 # (reposetup is handled in hg.repository)
1052 # (reposetup is handled in hg.repository)
1045
1053
1046 addaliases(lui, commands.table)
1054 addaliases(lui, commands.table)
1047
1055
1048 # All aliases and commands are completely defined, now.
1056 # All aliases and commands are completely defined, now.
1049 # Check abbreviation/ambiguity of shell alias.
1057 # Check abbreviation/ambiguity of shell alias.
1050 shellaliasfn = _checkshellalias(lui, ui, args)
1058 shellaliasfn = _checkshellalias(lui, ui, args)
1051 if shellaliasfn:
1059 if shellaliasfn:
1052 # no additional configs will be set, set up the ui instances
1060 # no additional configs will be set, set up the ui instances
1053 for ui_ in uis:
1061 for ui_ in uis:
1054 extensions.populateui(ui_)
1062 extensions.populateui(ui_)
1055 return shellaliasfn()
1063 return shellaliasfn()
1056
1064
1057 # check for fallback encoding
1065 # check for fallback encoding
1058 fallback = lui.config(b'ui', b'fallbackencoding')
1066 fallback = lui.config(b'ui', b'fallbackencoding')
1059 if fallback:
1067 if fallback:
1060 encoding.fallbackencoding = fallback
1068 encoding.fallbackencoding = fallback
1061
1069
1062 fullargs = args
1070 fullargs = args
1063 cmd, func, args, options, cmdoptions = _parse(lui, args)
1071 cmd, func, args, options, cmdoptions = _parse(lui, args)
1064
1072
1065 # store the canonical command name in request object for later access
1073 # store the canonical command name in request object for later access
1066 req.canonical_command = cmd
1074 req.canonical_command = cmd
1067
1075
1068 if options[b"config"] != req.earlyoptions[b"config"]:
1076 if options[b"config"] != req.earlyoptions[b"config"]:
1069 raise error.InputError(_(b"option --config may not be abbreviated"))
1077 raise error.InputError(_(b"option --config may not be abbreviated"))
1070 if options[b"cwd"] != req.earlyoptions[b"cwd"]:
1078 if options[b"cwd"] != req.earlyoptions[b"cwd"]:
1071 raise error.InputError(_(b"option --cwd may not be abbreviated"))
1079 raise error.InputError(_(b"option --cwd may not be abbreviated"))
1072 if options[b"repository"] != req.earlyoptions[b"repository"]:
1080 if options[b"repository"] != req.earlyoptions[b"repository"]:
1073 raise error.InputError(
1081 raise error.InputError(
1074 _(
1082 _(
1075 b"option -R has to be separated from other options (e.g. not "
1083 b"option -R has to be separated from other options (e.g. not "
1076 b"-qR) and --repository may only be abbreviated as --repo"
1084 b"-qR) and --repository may only be abbreviated as --repo"
1077 )
1085 )
1078 )
1086 )
1079 if options[b"debugger"] != req.earlyoptions[b"debugger"]:
1087 if options[b"debugger"] != req.earlyoptions[b"debugger"]:
1080 raise error.InputError(
1088 raise error.InputError(
1081 _(b"option --debugger may not be abbreviated")
1089 _(b"option --debugger may not be abbreviated")
1082 )
1090 )
1083 # don't validate --profile/--traceback, which can be enabled from now
1091 # don't validate --profile/--traceback, which can be enabled from now
1084
1092
1085 if options[b"encoding"]:
1093 if options[b"encoding"]:
1086 encoding.encoding = options[b"encoding"]
1094 encoding.encoding = options[b"encoding"]
1087 if options[b"encodingmode"]:
1095 if options[b"encodingmode"]:
1088 encoding.encodingmode = options[b"encodingmode"]
1096 encoding.encodingmode = options[b"encodingmode"]
1089 if options[b"time"]:
1097 if options[b"time"]:
1090
1098
1091 def get_times():
1099 def get_times():
1092 t = os.times()
1100 t = os.times()
1093 if t[4] == 0.0:
1101 if t[4] == 0.0:
1094 # Windows leaves this as zero, so use time.perf_counter()
1102 # Windows leaves this as zero, so use time.perf_counter()
1095 t = (t[0], t[1], t[2], t[3], util.timer())
1103 t = (t[0], t[1], t[2], t[3], util.timer())
1096 return t
1104 return t
1097
1105
1098 s = get_times()
1106 s = get_times()
1099
1107
1100 def print_time():
1108 def print_time():
1101 t = get_times()
1109 t = get_times()
1102 ui.warn(
1110 ui.warn(
1103 _(b"time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n")
1111 _(b"time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n")
1104 % (
1112 % (
1105 t[4] - s[4],
1113 t[4] - s[4],
1106 t[0] - s[0],
1114 t[0] - s[0],
1107 t[2] - s[2],
1115 t[2] - s[2],
1108 t[1] - s[1],
1116 t[1] - s[1],
1109 t[3] - s[3],
1117 t[3] - s[3],
1110 )
1118 )
1111 )
1119 )
1112
1120
1113 ui.atexit(print_time)
1121 ui.atexit(print_time)
1114 if options[b"profile"]:
1122 if options[b"profile"]:
1115 profiler.start()
1123 profiler.start()
1116
1124
1117 # if abbreviated version of this were used, take them in account, now
1125 # if abbreviated version of this were used, take them in account, now
1118 if options[b'verbose'] or options[b'debug'] or options[b'quiet']:
1126 if options[b'verbose'] or options[b'debug'] or options[b'quiet']:
1119 for opt in (b'verbose', b'debug', b'quiet'):
1127 for opt in (b'verbose', b'debug', b'quiet'):
1120 if options[opt] == req.earlyoptions[opt]:
1128 if options[opt] == req.earlyoptions[opt]:
1121 continue
1129 continue
1122 val = pycompat.bytestr(bool(options[opt]))
1130 val = pycompat.bytestr(bool(options[opt]))
1123 for ui_ in uis:
1131 for ui_ in uis:
1124 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1132 ui_.setconfig(b'ui', opt, val, b'--' + opt)
1125
1133
1126 if options[b'traceback']:
1134 if options[b'traceback']:
1127 for ui_ in uis:
1135 for ui_ in uis:
1128 ui_.setconfig(b'ui', b'traceback', b'on', b'--traceback')
1136 ui_.setconfig(b'ui', b'traceback', b'on', b'--traceback')
1129
1137
1130 if options[b'noninteractive']:
1138 if options[b'noninteractive']:
1131 for ui_ in uis:
1139 for ui_ in uis:
1132 ui_.setconfig(b'ui', b'interactive', b'off', b'-y')
1140 ui_.setconfig(b'ui', b'interactive', b'off', b'-y')
1133
1141
1134 if cmdoptions.get(b'insecure', False):
1142 if cmdoptions.get(b'insecure', False):
1135 for ui_ in uis:
1143 for ui_ in uis:
1136 ui_.insecureconnections = True
1144 ui_.insecureconnections = True
1137
1145
1138 # setup color handling before pager, because setting up pager
1146 # setup color handling before pager, because setting up pager
1139 # might cause incorrect console information
1147 # might cause incorrect console information
1140 coloropt = options[b'color']
1148 coloropt = options[b'color']
1141 for ui_ in uis:
1149 for ui_ in uis:
1142 if coloropt:
1150 if coloropt:
1143 ui_.setconfig(b'ui', b'color', coloropt, b'--color')
1151 ui_.setconfig(b'ui', b'color', coloropt, b'--color')
1144 color.setup(ui_)
1152 color.setup(ui_)
1145
1153
1146 if stringutil.parsebool(options[b'pager']):
1154 if stringutil.parsebool(options[b'pager']):
1147 # ui.pager() expects 'internal-always-' prefix in this case
1155 # ui.pager() expects 'internal-always-' prefix in this case
1148 ui.pager(b'internal-always-' + cmd)
1156 ui.pager(b'internal-always-' + cmd)
1149 elif options[b'pager'] != b'auto':
1157 elif options[b'pager'] != b'auto':
1150 for ui_ in uis:
1158 for ui_ in uis:
1151 ui_.disablepager()
1159 ui_.disablepager()
1152
1160
1153 # configs are fully loaded, set up the ui instances
1161 # configs are fully loaded, set up the ui instances
1154 for ui_ in uis:
1162 for ui_ in uis:
1155 extensions.populateui(ui_)
1163 extensions.populateui(ui_)
1156
1164
1157 if options[b'version']:
1165 if options[b'version']:
1158 return commands.version_(ui)
1166 return commands.version_(ui)
1159 if options[b'help']:
1167 if options[b'help']:
1160 return commands.help_(ui, cmd, command=cmd is not None)
1168 return commands.help_(ui, cmd, command=cmd is not None)
1161 elif not cmd:
1169 elif not cmd:
1162 return commands.help_(ui, b'shortlist')
1170 return commands.help_(ui, b'shortlist')
1163
1171
1164 repo = None
1172 repo = None
1165 cmdpats = args[:]
1173 cmdpats = args[:]
1166 assert func is not None # help out pytype
1174 assert func is not None # help out pytype
1167 if not func.norepo:
1175 if not func.norepo:
1168 # use the repo from the request only if we don't have -R
1176 # use the repo from the request only if we don't have -R
1169 if not rpath and not cwd:
1177 if not rpath and not cwd:
1170 repo = req.repo
1178 repo = req.repo
1171
1179
1172 if repo:
1180 if repo:
1173 # set the descriptors of the repo ui to those of ui
1181 # set the descriptors of the repo ui to those of ui
1174 repo.ui.fin = ui.fin
1182 repo.ui.fin = ui.fin
1175 repo.ui.fout = ui.fout
1183 repo.ui.fout = ui.fout
1176 repo.ui.ferr = ui.ferr
1184 repo.ui.ferr = ui.ferr
1177 repo.ui.fmsg = ui.fmsg
1185 repo.ui.fmsg = ui.fmsg
1178 else:
1186 else:
1179 try:
1187 try:
1180 repo = hg.repository(
1188 repo = hg.repository(
1181 ui,
1189 ui,
1182 path=path,
1190 path=path,
1183 presetupfuncs=req.prereposetups,
1191 presetupfuncs=req.prereposetups,
1184 intents=func.intents,
1192 intents=func.intents,
1185 )
1193 )
1186 if not repo.local():
1194 if not repo.local():
1187 raise error.InputError(
1195 raise error.InputError(
1188 _(b"repository '%s' is not local") % path
1196 _(b"repository '%s' is not local") % path
1189 )
1197 )
1190 repo.ui.setconfig(
1198 repo.ui.setconfig(
1191 b"bundle", b"mainreporoot", repo.root, b'repo'
1199 b"bundle", b"mainreporoot", repo.root, b'repo'
1192 )
1200 )
1193 except error.RequirementError:
1201 except error.RequirementError:
1194 raise
1202 raise
1195 except error.RepoError:
1203 except error.RepoError:
1196 if rpath: # invalid -R path
1204 if rpath: # invalid -R path
1197 raise
1205 raise
1198 if not func.optionalrepo:
1206 if not func.optionalrepo:
1199 if func.inferrepo and args and not path:
1207 if func.inferrepo and args and not path:
1200 # try to infer -R from command args
1208 # try to infer -R from command args
1201 repos = pycompat.maplist(cmdutil.findrepo, args)
1209 repos = pycompat.maplist(cmdutil.findrepo, args)
1202 guess = repos[0]
1210 guess = repos[0]
1203 if guess and repos.count(guess) == len(repos):
1211 if guess and repos.count(guess) == len(repos):
1204 req.args = [b'--repository', guess] + fullargs
1212 req.args = [b'--repository', guess] + fullargs
1205 req.earlyoptions[b'repository'] = guess
1213 req.earlyoptions[b'repository'] = guess
1206 return _dispatch(req)
1214 return _dispatch(req)
1207 if not path:
1215 if not path:
1208 raise error.InputError(
1216 raise error.InputError(
1209 _(
1217 _(
1210 b"no repository found in"
1218 b"no repository found in"
1211 b" '%s' (.hg not found)"
1219 b" '%s' (.hg not found)"
1212 )
1220 )
1213 % encoding.getcwd()
1221 % encoding.getcwd()
1214 )
1222 )
1215 raise
1223 raise
1216 if repo:
1224 if repo:
1217 ui = repo.ui
1225 ui = repo.ui
1218 if options[b'hidden']:
1226 if options[b'hidden']:
1219 repo = repo.unfiltered()
1227 repo = repo.unfiltered()
1220 args.insert(0, repo)
1228 args.insert(0, repo)
1221 elif rpath:
1229 elif rpath:
1222 ui.warn(_(b"warning: --repository ignored\n"))
1230 ui.warn(_(b"warning: --repository ignored\n"))
1223
1231
1224 msg = _formatargs(fullargs)
1232 msg = _formatargs(fullargs)
1225 ui.log(b"command", b'%s\n', msg)
1233 ui.log(b"command", b'%s\n', msg)
1226 strcmdopt = pycompat.strkwargs(cmdoptions)
1234 strcmdopt = pycompat.strkwargs(cmdoptions)
1227 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
1235 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
1228 try:
1236 try:
1229 return runcommand(
1237 return runcommand(
1230 lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions
1238 lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions
1231 )
1239 )
1232 finally:
1240 finally:
1233 if repo and repo != req.repo:
1241 if repo and repo != req.repo:
1234 repo.close()
1242 repo.close()
1235
1243
1236
1244
1237 def _runcommand(ui, options, cmd, cmdfunc):
1245 def _runcommand(ui, options, cmd, cmdfunc):
1238 """Run a command function, possibly with profiling enabled."""
1246 """Run a command function, possibly with profiling enabled."""
1239 try:
1247 try:
1240 with tracing.log("Running %s command" % cmd):
1248 with tracing.log("Running %s command" % cmd):
1241 return cmdfunc()
1249 return cmdfunc()
1242 except error.SignatureError:
1250 except error.SignatureError:
1243 raise error.CommandError(cmd, _(b'invalid arguments'))
1251 raise error.CommandError(cmd, _(b'invalid arguments'))
1244
1252
1245
1253
1246 def _exceptionwarning(ui):
1254 def _exceptionwarning(ui):
1247 """Produce a warning message for the current active exception"""
1255 """Produce a warning message for the current active exception"""
1248
1256
1249 # For compatibility checking, we discard the portion of the hg
1257 # For compatibility checking, we discard the portion of the hg
1250 # version after the + on the assumption that if a "normal
1258 # version after the + on the assumption that if a "normal
1251 # user" is running a build with a + in it the packager
1259 # user" is running a build with a + in it the packager
1252 # probably built from fairly close to a tag and anyone with a
1260 # probably built from fairly close to a tag and anyone with a
1253 # 'make local' copy of hg (where the version number can be out
1261 # 'make local' copy of hg (where the version number can be out
1254 # of date) will be clueful enough to notice the implausible
1262 # of date) will be clueful enough to notice the implausible
1255 # version number and try updating.
1263 # version number and try updating.
1256 ct = util.versiontuple(n=2)
1264 ct = util.versiontuple(n=2)
1257 worst = None, ct, b'', b''
1265 worst = None, ct, b'', b''
1258 if ui.config(b'ui', b'supportcontact') is None:
1266 if ui.config(b'ui', b'supportcontact') is None:
1259 for name, mod in extensions.extensions():
1267 for name, mod in extensions.extensions():
1260 # 'testedwith' should be bytes, but not all extensions are ported
1268 # 'testedwith' should be bytes, but not all extensions are ported
1261 # to py3 and we don't want UnicodeException because of that.
1269 # to py3 and we don't want UnicodeException because of that.
1262 testedwith = stringutil.forcebytestr(
1270 testedwith = stringutil.forcebytestr(
1263 getattr(mod, 'testedwith', b'')
1271 getattr(mod, 'testedwith', b'')
1264 )
1272 )
1265 version = extensions.moduleversion(mod)
1273 version = extensions.moduleversion(mod)
1266 report = getattr(mod, 'buglink', _(b'the extension author.'))
1274 report = getattr(mod, 'buglink', _(b'the extension author.'))
1267 if not testedwith.strip():
1275 if not testedwith.strip():
1268 # We found an untested extension. It's likely the culprit.
1276 # We found an untested extension. It's likely the culprit.
1269 worst = name, b'unknown', report, version
1277 worst = name, b'unknown', report, version
1270 break
1278 break
1271
1279
1272 # Never blame on extensions bundled with Mercurial.
1280 # Never blame on extensions bundled with Mercurial.
1273 if extensions.ismoduleinternal(mod):
1281 if extensions.ismoduleinternal(mod):
1274 continue
1282 continue
1275
1283
1276 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1284 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
1277 if ct in tested:
1285 if ct in tested:
1278 continue
1286 continue
1279
1287
1280 lower = [t for t in tested if t < ct]
1288 lower = [t for t in tested if t < ct]
1281 nearest = max(lower or tested)
1289 nearest = max(lower or tested)
1282 if worst[0] is None or nearest < worst[1]:
1290 if worst[0] is None or nearest < worst[1]:
1283 worst = name, nearest, report, version
1291 worst = name, nearest, report, version
1284 if worst[0] is not None:
1292 if worst[0] is not None:
1285 name, testedwith, report, version = worst
1293 name, testedwith, report, version = worst
1286 if not isinstance(testedwith, (bytes, str)):
1294 if not isinstance(testedwith, (bytes, str)):
1287 testedwith = b'.'.join(
1295 testedwith = b'.'.join(
1288 [stringutil.forcebytestr(c) for c in testedwith]
1296 [stringutil.forcebytestr(c) for c in testedwith]
1289 )
1297 )
1290 extver = version or _(b"(version N/A)")
1298 extver = version or _(b"(version N/A)")
1291 warning = _(
1299 warning = _(
1292 b'** Unknown exception encountered with '
1300 b'** Unknown exception encountered with '
1293 b'possibly-broken third-party extension "%s" %s\n'
1301 b'possibly-broken third-party extension "%s" %s\n'
1294 b'** which supports versions %s of Mercurial.\n'
1302 b'** which supports versions %s of Mercurial.\n'
1295 b'** Please disable "%s" and try your action again.\n'
1303 b'** Please disable "%s" and try your action again.\n'
1296 b'** If that fixes the bug please report it to %s\n'
1304 b'** If that fixes the bug please report it to %s\n'
1297 ) % (name, extver, testedwith, name, stringutil.forcebytestr(report))
1305 ) % (name, extver, testedwith, name, stringutil.forcebytestr(report))
1298 else:
1306 else:
1299 bugtracker = ui.config(b'ui', b'supportcontact')
1307 bugtracker = ui.config(b'ui', b'supportcontact')
1300 if bugtracker is None:
1308 if bugtracker is None:
1301 bugtracker = _(b"https://mercurial-scm.org/wiki/BugTracker")
1309 bugtracker = _(b"https://mercurial-scm.org/wiki/BugTracker")
1302 warning = (
1310 warning = (
1303 _(
1311 _(
1304 b"** unknown exception encountered, "
1312 b"** unknown exception encountered, "
1305 b"please report by visiting\n** "
1313 b"please report by visiting\n** "
1306 )
1314 )
1307 + bugtracker
1315 + bugtracker
1308 + b'\n'
1316 + b'\n'
1309 )
1317 )
1310 sysversion = pycompat.sysbytes(sys.version).replace(b'\n', b'')
1318 sysversion = pycompat.sysbytes(sys.version).replace(b'\n', b'')
1311
1319
1312 def ext_with_ver(x):
1320 def ext_with_ver(x):
1313 ext = x[0]
1321 ext = x[0]
1314 ver = extensions.moduleversion(x[1])
1322 ver = extensions.moduleversion(x[1])
1315 if ver:
1323 if ver:
1316 ext += b' ' + ver
1324 ext += b' ' + ver
1317 return ext
1325 return ext
1318
1326
1319 warning += (
1327 warning += (
1320 (_(b"** Python %s\n") % sysversion)
1328 (_(b"** Python %s\n") % sysversion)
1321 + (_(b"** Mercurial Distributed SCM (version %s)\n") % util.version())
1329 + (_(b"** Mercurial Distributed SCM (version %s)\n") % util.version())
1322 + (
1330 + (
1323 _(b"** Extensions loaded: %s\n")
1331 _(b"** Extensions loaded: %s\n")
1324 % b", ".join(
1332 % b", ".join(
1325 [ext_with_ver(x) for x in sorted(extensions.extensions())]
1333 [ext_with_ver(x) for x in sorted(extensions.extensions())]
1326 )
1334 )
1327 )
1335 )
1328 )
1336 )
1329 return warning
1337 return warning
1330
1338
1331
1339
1332 def handlecommandexception(ui):
1340 def handlecommandexception(ui):
1333 """Produce a warning message for broken commands
1341 """Produce a warning message for broken commands
1334
1342
1335 Called when handling an exception; the exception is reraised if
1343 Called when handling an exception; the exception is reraised if
1336 this function returns False, ignored otherwise.
1344 this function returns False, ignored otherwise.
1337 """
1345 """
1338 warning = _exceptionwarning(ui)
1346 warning = _exceptionwarning(ui)
1339 ui.log(
1347 ui.log(
1340 b"commandexception",
1348 b"commandexception",
1341 b"%s\n%s\n",
1349 b"%s\n%s\n",
1342 warning,
1350 warning,
1343 pycompat.sysbytes(traceback.format_exc()),
1351 pycompat.sysbytes(traceback.format_exc()),
1344 )
1352 )
1345 ui.warn(warning)
1353 ui.warn(warning)
1346 return False # re-raise the exception
1354 return False # re-raise the exception
General Comments 0
You need to be logged in to leave comments. Login now