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