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