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