• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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