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