1""" idlelib.run 2 3Simplified, pyshell.ModifiedInterpreter spawns a subprocess with 4f'''{sys.executable} -c "__import__('idlelib.run').run.main()"''' 5'.run' is needed because __import__ returns idlelib, not idlelib.run. 6""" 7import contextlib 8import functools 9import io 10import linecache 11import queue 12import sys 13import textwrap 14import time 15import traceback 16import _thread as thread 17import threading 18import warnings 19 20import idlelib # testing 21from idlelib import autocomplete # AutoComplete, fetch_encodings 22from idlelib import calltip # Calltip 23from idlelib import debugger_r # start_debugger 24from idlelib import debugobj_r # remote_object_tree_item 25from idlelib import iomenu # encoding 26from idlelib import rpc # multiple objects 27from idlelib import stackviewer # StackTreeItem 28import __main__ 29 30import tkinter # Use tcl and, if startup fails, messagebox. 31if not hasattr(sys.modules['idlelib.run'], 'firstrun'): 32 # Undo modifications of tkinter by idlelib imports; see bpo-25507. 33 for mod in ('simpledialog', 'messagebox', 'font', 34 'dialog', 'filedialog', 'commondialog', 35 'ttk'): 36 delattr(tkinter, mod) 37 del sys.modules['tkinter.' + mod] 38 # Avoid AttributeError if run again; see bpo-37038. 39 sys.modules['idlelib.run'].firstrun = False 40 41LOCALHOST = '127.0.0.1' 42 43try: 44 eof = 'Ctrl-D (end-of-file)' 45 exit.eof = eof 46 quit.eof = eof 47except NameError: # In case subprocess started with -S (maybe in future). 48 pass 49 50 51def idle_formatwarning(message, category, filename, lineno, line=None): 52 """Format warnings the IDLE way.""" 53 54 s = "\nWarning (from warnings module):\n" 55 s += f' File \"{filename}\", line {lineno}\n' 56 if line is None: 57 line = linecache.getline(filename, lineno) 58 line = line.strip() 59 if line: 60 s += " %s\n" % line 61 s += f"{category.__name__}: {message}\n" 62 return s 63 64def idle_showwarning_subproc( 65 message, category, filename, lineno, file=None, line=None): 66 """Show Idle-format warning after replacing warnings.showwarning. 67 68 The only difference is the formatter called. 69 """ 70 if file is None: 71 file = sys.stderr 72 try: 73 file.write(idle_formatwarning( 74 message, category, filename, lineno, line)) 75 except OSError: 76 pass # the file (probably stderr) is invalid - this warning gets lost. 77 78_warnings_showwarning = None 79 80def capture_warnings(capture): 81 "Replace warning.showwarning with idle_showwarning_subproc, or reverse." 82 83 global _warnings_showwarning 84 if capture: 85 if _warnings_showwarning is None: 86 _warnings_showwarning = warnings.showwarning 87 warnings.showwarning = idle_showwarning_subproc 88 else: 89 if _warnings_showwarning is not None: 90 warnings.showwarning = _warnings_showwarning 91 _warnings_showwarning = None 92 93capture_warnings(True) 94 95if idlelib.testing: 96 # gh-121008: When testing IDLE, don't create a Tk object to avoid side 97 # effects such as installing a PyOS_InputHook hook. 98 def handle_tk_events(): 99 pass 100else: 101 tcl = tkinter.Tcl() 102 103 def handle_tk_events(tcl=tcl): 104 """Process any tk events that are ready to be dispatched if tkinter 105 has been imported, a tcl interpreter has been created and tk has been 106 loaded.""" 107 tcl.eval("update") 108 109# Thread shared globals: Establish a queue between a subthread (which handles 110# the socket) and the main thread (which runs user code), plus global 111# completion, exit and interruptible (the main thread) flags: 112 113exit_now = False 114quitting = False 115interruptible = False 116 117def main(del_exitfunc=False): 118 """Start the Python execution server in a subprocess 119 120 In the Python subprocess, RPCServer is instantiated with handlerclass 121 MyHandler, which inherits register/unregister methods from RPCHandler via 122 the mix-in class SocketIO. 123 124 When the RPCServer 'server' is instantiated, the TCPServer initialization 125 creates an instance of run.MyHandler and calls its handle() method. 126 handle() instantiates a run.Executive object, passing it a reference to the 127 MyHandler object. That reference is saved as attribute rpchandler of the 128 Executive instance. The Executive methods have access to the reference and 129 can pass it on to entities that they command 130 (e.g. debugger_r.Debugger.start_debugger()). The latter, in turn, can 131 call MyHandler(SocketIO) register/unregister methods via the reference to 132 register and unregister themselves. 133 134 """ 135 global exit_now 136 global quitting 137 global no_exitfunc 138 no_exitfunc = del_exitfunc 139 #time.sleep(15) # test subprocess not responding 140 try: 141 assert(len(sys.argv) > 1) 142 port = int(sys.argv[-1]) 143 except: 144 print("IDLE Subprocess: no IP port passed in sys.argv.", 145 file=sys.__stderr__) 146 return 147 148 capture_warnings(True) 149 sys.argv[:] = [""] 150 threading.Thread(target=manage_socket, 151 name='SockThread', 152 args=((LOCALHOST, port),), 153 daemon=True, 154 ).start() 155 156 while True: 157 try: 158 if exit_now: 159 try: 160 exit() 161 except KeyboardInterrupt: 162 # exiting but got an extra KBI? Try again! 163 continue 164 try: 165 request = rpc.request_queue.get(block=True, timeout=0.05) 166 except queue.Empty: 167 request = None 168 # Issue 32207: calling handle_tk_events here adds spurious 169 # queue.Empty traceback to event handling exceptions. 170 if request: 171 seq, (method, args, kwargs) = request 172 ret = method(*args, **kwargs) 173 rpc.response_queue.put((seq, ret)) 174 else: 175 handle_tk_events() 176 except KeyboardInterrupt: 177 if quitting: 178 exit_now = True 179 continue 180 except SystemExit: 181 capture_warnings(False) 182 raise 183 except: 184 type, value, tb = sys.exc_info() 185 try: 186 print_exception() 187 rpc.response_queue.put((seq, None)) 188 except: 189 # Link didn't work, print same exception to __stderr__ 190 traceback.print_exception(type, value, tb, file=sys.__stderr__) 191 exit() 192 else: 193 continue 194 195def manage_socket(address): 196 for i in range(3): 197 time.sleep(i) 198 try: 199 server = MyRPCServer(address, MyHandler) 200 break 201 except OSError as err: 202 print("IDLE Subprocess: OSError: " + err.args[1] + 203 ", retrying....", file=sys.__stderr__) 204 socket_error = err 205 else: 206 print("IDLE Subprocess: Connection to " 207 "IDLE GUI failed, exiting.", file=sys.__stderr__) 208 show_socket_error(socket_error, address) 209 global exit_now 210 exit_now = True 211 return 212 server.handle_request() # A single request only 213 214def show_socket_error(err, address): 215 "Display socket error from manage_socket." 216 import tkinter 217 from tkinter.messagebox import showerror 218 root = tkinter.Tk() 219 fix_scaling(root) 220 root.withdraw() 221 showerror( 222 "Subprocess Connection Error", 223 f"IDLE's subprocess can't connect to {address[0]}:{address[1]}.\n" 224 f"Fatal OSError #{err.errno}: {err.strerror}.\n" 225 "See the 'Startup failure' section of the IDLE doc, online at\n" 226 "https://docs.python.org/3/library/idle.html#startup-failure", 227 parent=root) 228 root.destroy() 229 230 231def get_message_lines(typ, exc, tb): 232 "Return line composing the exception message." 233 if typ in (AttributeError, NameError): 234 # 3.10+ hints are not directly accessible from python (#44026). 235 err = io.StringIO() 236 with contextlib.redirect_stderr(err): 237 sys.__excepthook__(typ, exc, tb) 238 return [err.getvalue().split("\n")[-2] + "\n"] 239 else: 240 return traceback.format_exception_only(typ, exc) 241 242 243def print_exception(): 244 import linecache 245 linecache.checkcache() 246 flush_stdout() 247 efile = sys.stderr 248 typ, val, tb = excinfo = sys.exc_info() 249 sys.last_type, sys.last_value, sys.last_traceback = excinfo 250 sys.last_exc = val 251 seen = set() 252 253 def print_exc(typ, exc, tb): 254 seen.add(id(exc)) 255 context = exc.__context__ 256 cause = exc.__cause__ 257 if cause is not None and id(cause) not in seen: 258 print_exc(type(cause), cause, cause.__traceback__) 259 print("\nThe above exception was the direct cause " 260 "of the following exception:\n", file=efile) 261 elif (context is not None and 262 not exc.__suppress_context__ and 263 id(context) not in seen): 264 print_exc(type(context), context, context.__traceback__) 265 print("\nDuring handling of the above exception, " 266 "another exception occurred:\n", file=efile) 267 if tb: 268 tbe = traceback.extract_tb(tb) 269 print('Traceback (most recent call last):', file=efile) 270 exclude = ("run.py", "rpc.py", "threading.py", "queue.py", 271 "debugger_r.py", "bdb.py") 272 cleanup_traceback(tbe, exclude) 273 traceback.print_list(tbe, file=efile) 274 lines = get_message_lines(typ, exc, tb) 275 for line in lines: 276 print(line, end='', file=efile) 277 278 print_exc(typ, val, tb) 279 280def cleanup_traceback(tb, exclude): 281 "Remove excluded traces from beginning/end of tb; get cached lines" 282 orig_tb = tb[:] 283 while tb: 284 for rpcfile in exclude: 285 if tb[0][0].count(rpcfile): 286 break # found an exclude, break for: and delete tb[0] 287 else: 288 break # no excludes, have left RPC code, break while: 289 del tb[0] 290 while tb: 291 for rpcfile in exclude: 292 if tb[-1][0].count(rpcfile): 293 break 294 else: 295 break 296 del tb[-1] 297 if len(tb) == 0: 298 # exception was in IDLE internals, don't prune! 299 tb[:] = orig_tb[:] 300 print("** IDLE Internal Exception: ", file=sys.stderr) 301 rpchandler = rpc.objecttable['exec'].rpchandler 302 for i in range(len(tb)): 303 fn, ln, nm, line = tb[i] 304 if nm == '?': 305 nm = "-toplevel-" 306 if not line and fn.startswith("<pyshell#"): 307 line = rpchandler.remotecall('linecache', 'getline', 308 (fn, ln), {}) 309 tb[i] = fn, ln, nm, line 310 311def flush_stdout(): 312 """XXX How to do this now?""" 313 314def exit(): 315 """Exit subprocess, possibly after first clearing exit functions. 316 317 If config-main.cfg/.def 'General' 'delete-exitfunc' is True, then any 318 functions registered with atexit will be removed before exiting. 319 (VPython support) 320 321 """ 322 if no_exitfunc: 323 import atexit 324 atexit._clear() 325 capture_warnings(False) 326 sys.exit(0) 327 328 329def fix_scaling(root): 330 """Scale fonts on HiDPI displays.""" 331 import tkinter.font 332 scaling = float(root.tk.call('tk', 'scaling')) 333 if scaling > 1.4: 334 for name in tkinter.font.names(root): 335 font = tkinter.font.Font(root=root, name=name, exists=True) 336 size = int(font['size']) 337 if size < 0: 338 font['size'] = round(-0.75*size) 339 340 341def fixdoc(fun, text): 342 tem = (fun.__doc__ + '\n\n') if fun.__doc__ is not None else '' 343 fun.__doc__ = tem + textwrap.fill(textwrap.dedent(text)) 344 345RECURSIONLIMIT_DELTA = 30 346 347def install_recursionlimit_wrappers(): 348 """Install wrappers to always add 30 to the recursion limit.""" 349 # see: bpo-26806 350 351 @functools.wraps(sys.setrecursionlimit) 352 def setrecursionlimit(*args, **kwargs): 353 # mimic the original sys.setrecursionlimit()'s input handling 354 if kwargs: 355 raise TypeError( 356 "setrecursionlimit() takes no keyword arguments") 357 try: 358 limit, = args 359 except ValueError: 360 raise TypeError(f"setrecursionlimit() takes exactly one " 361 f"argument ({len(args)} given)") 362 if not limit > 0: 363 raise ValueError( 364 "recursion limit must be greater or equal than 1") 365 366 return setrecursionlimit.__wrapped__(limit + RECURSIONLIMIT_DELTA) 367 368 fixdoc(setrecursionlimit, f"""\ 369 This IDLE wrapper adds {RECURSIONLIMIT_DELTA} to prevent possible 370 uninterruptible loops.""") 371 372 @functools.wraps(sys.getrecursionlimit) 373 def getrecursionlimit(): 374 return getrecursionlimit.__wrapped__() - RECURSIONLIMIT_DELTA 375 376 fixdoc(getrecursionlimit, f"""\ 377 This IDLE wrapper subtracts {RECURSIONLIMIT_DELTA} to compensate 378 for the {RECURSIONLIMIT_DELTA} IDLE adds when setting the limit.""") 379 380 # add the delta to the default recursion limit, to compensate 381 sys.setrecursionlimit(sys.getrecursionlimit() + RECURSIONLIMIT_DELTA) 382 383 sys.setrecursionlimit = setrecursionlimit 384 sys.getrecursionlimit = getrecursionlimit 385 386 387def uninstall_recursionlimit_wrappers(): 388 """Uninstall the recursion limit wrappers from the sys module. 389 390 IDLE only uses this for tests. Users can import run and call 391 this to remove the wrapping. 392 """ 393 if ( 394 getattr(sys.setrecursionlimit, '__wrapped__', None) and 395 getattr(sys.getrecursionlimit, '__wrapped__', None) 396 ): 397 sys.setrecursionlimit = sys.setrecursionlimit.__wrapped__ 398 sys.getrecursionlimit = sys.getrecursionlimit.__wrapped__ 399 sys.setrecursionlimit(sys.getrecursionlimit() - RECURSIONLIMIT_DELTA) 400 401 402class MyRPCServer(rpc.RPCServer): 403 404 def handle_error(self, request, client_address): 405 """Override RPCServer method for IDLE 406 407 Interrupt the MainThread and exit server if link is dropped. 408 409 """ 410 global quitting 411 try: 412 raise 413 except SystemExit: 414 raise 415 except EOFError: 416 global exit_now 417 exit_now = True 418 thread.interrupt_main() 419 except: 420 erf = sys.__stderr__ 421 print(textwrap.dedent(f""" 422 {'-'*40} 423 Unhandled exception in user code execution server!' 424 Thread: {threading.current_thread().name} 425 IDLE Client Address: {client_address} 426 Request: {request!r} 427 """), file=erf) 428 traceback.print_exc(limit=-20, file=erf) 429 print(textwrap.dedent(f""" 430 *** Unrecoverable, server exiting! 431 432 Users should never see this message; it is likely transient. 433 If this recurs, report this with a copy of the message 434 and an explanation of how to make it repeat. 435 {'-'*40}"""), file=erf) 436 quitting = True 437 thread.interrupt_main() 438 439 440# Pseudofiles for shell-remote communication (also used in pyshell) 441 442class StdioFile(io.TextIOBase): 443 444 def __init__(self, shell, tags, encoding='utf-8', errors='strict'): 445 self.shell = shell 446 # GH-78889: accessing unpickleable attributes freezes Shell. 447 # IDLE only needs methods; allow 'width' for possible use. 448 self.shell._RPCProxy__attributes = {'width': 1} 449 self.tags = tags 450 self._encoding = encoding 451 self._errors = errors 452 453 @property 454 def encoding(self): 455 return self._encoding 456 457 @property 458 def errors(self): 459 return self._errors 460 461 @property 462 def name(self): 463 return '<%s>' % self.tags 464 465 def isatty(self): 466 return True 467 468 469class StdOutputFile(StdioFile): 470 471 def writable(self): 472 return True 473 474 def write(self, s): 475 if self.closed: 476 raise ValueError("write to closed file") 477 s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors) 478 return self.shell.write(s, self.tags) 479 480 481class StdInputFile(StdioFile): 482 _line_buffer = '' 483 484 def readable(self): 485 return True 486 487 def read(self, size=-1): 488 if self.closed: 489 raise ValueError("read from closed file") 490 if size is None: 491 size = -1 492 elif not isinstance(size, int): 493 raise TypeError('must be int, not ' + type(size).__name__) 494 result = self._line_buffer 495 self._line_buffer = '' 496 if size < 0: 497 while line := self.shell.readline(): 498 result += line 499 else: 500 while len(result) < size: 501 line = self.shell.readline() 502 if not line: break 503 result += line 504 self._line_buffer = result[size:] 505 result = result[:size] 506 return result 507 508 def readline(self, size=-1): 509 if self.closed: 510 raise ValueError("read from closed file") 511 if size is None: 512 size = -1 513 elif not isinstance(size, int): 514 raise TypeError('must be int, not ' + type(size).__name__) 515 line = self._line_buffer or self.shell.readline() 516 if size < 0: 517 size = len(line) 518 eol = line.find('\n', 0, size) 519 if eol >= 0: 520 size = eol + 1 521 self._line_buffer = line[size:] 522 return line[:size] 523 524 def close(self): 525 self.shell.close() 526 527 528class MyHandler(rpc.RPCHandler): 529 530 def handle(self): 531 """Override base method""" 532 executive = Executive(self) 533 self.register("exec", executive) 534 self.console = self.get_remote_proxy("console") 535 sys.stdin = StdInputFile(self.console, "stdin", 536 iomenu.encoding, iomenu.errors) 537 sys.stdout = StdOutputFile(self.console, "stdout", 538 iomenu.encoding, iomenu.errors) 539 sys.stderr = StdOutputFile(self.console, "stderr", 540 iomenu.encoding, "backslashreplace") 541 542 sys.displayhook = rpc.displayhook 543 # page help() text to shell. 544 import pydoc # import must be done here to capture i/o binding 545 pydoc.pager = pydoc.plainpager 546 547 # Keep a reference to stdin so that it won't try to exit IDLE if 548 # sys.stdin gets changed from within IDLE's shell. See issue17838. 549 self._keep_stdin = sys.stdin 550 551 install_recursionlimit_wrappers() 552 553 self.interp = self.get_remote_proxy("interp") 554 rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05) 555 556 def exithook(self): 557 "override SocketIO method - wait for MainThread to shut us down" 558 time.sleep(10) 559 560 def EOFhook(self): 561 "Override SocketIO method - terminate wait on callback and exit thread" 562 global quitting 563 quitting = True 564 thread.interrupt_main() 565 566 def decode_interrupthook(self): 567 "interrupt awakened thread" 568 global quitting 569 quitting = True 570 thread.interrupt_main() 571 572 573class Executive: 574 575 def __init__(self, rpchandler): 576 self.rpchandler = rpchandler 577 if idlelib.testing is False: 578 self.locals = __main__.__dict__ 579 self.calltip = calltip.Calltip() 580 self.autocomplete = autocomplete.AutoComplete() 581 else: 582 self.locals = {} 583 584 def runcode(self, code): 585 global interruptible 586 try: 587 self.user_exc_info = None 588 interruptible = True 589 try: 590 exec(code, self.locals) 591 finally: 592 interruptible = False 593 except SystemExit as e: 594 if e.args: # SystemExit called with an argument. 595 ob = e.args[0] 596 if not isinstance(ob, (type(None), int)): 597 print('SystemExit: ' + str(ob), file=sys.stderr) 598 # Return to the interactive prompt. 599 except: 600 self.user_exc_info = sys.exc_info() # For testing, hook, viewer. 601 if quitting: 602 exit() 603 if sys.excepthook is sys.__excepthook__: 604 print_exception() 605 else: 606 try: 607 sys.excepthook(*self.user_exc_info) 608 except: 609 self.user_exc_info = sys.exc_info() # For testing. 610 print_exception() 611 jit = self.rpchandler.console.getvar("<<toggle-jit-stack-viewer>>") 612 if jit: 613 self.rpchandler.interp.open_remote_stack_viewer() 614 else: 615 flush_stdout() 616 617 def interrupt_the_server(self): 618 if interruptible: 619 thread.interrupt_main() 620 621 def start_the_debugger(self, gui_adap_oid): 622 return debugger_r.start_debugger(self.rpchandler, gui_adap_oid) 623 624 def stop_the_debugger(self, idb_adap_oid): 625 "Unregister the Idb Adapter. Link objects and Idb then subject to GC" 626 self.rpchandler.unregister(idb_adap_oid) 627 628 def get_the_calltip(self, name): 629 return self.calltip.fetch_tip(name) 630 631 def get_the_completion_list(self, what, mode): 632 return self.autocomplete.fetch_completions(what, mode) 633 634 def stackviewer(self, flist_oid=None): 635 if self.user_exc_info: 636 _, exc, tb = self.user_exc_info 637 else: 638 return None 639 flist = None 640 if flist_oid is not None: 641 flist = self.rpchandler.get_remote_proxy(flist_oid) 642 while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]: 643 tb = tb.tb_next 644 exc.__traceback__ = tb 645 item = stackviewer.StackTreeItem(exc, flist) 646 return debugobj_r.remote_object_tree_item(item) 647 648 649if __name__ == '__main__': 650 from unittest import main 651 main('idlelib.idle_test.test_run', verbosity=2) 652 653capture_warnings(False) # Make sure turned off; see bpo-18081. 654