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