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('\n' + '-'*40, file=erf)
391            print('Unhandled server exception!', file=erf)
392            print('Thread: %s' % threading.current_thread().name, file=erf)
393            print('Client Address: ', client_address, file=erf)
394            print('Request: ', repr(request), file=erf)
395            traceback.print_exc(file=erf)
396            print('\n*** Unrecoverable, server exiting!', file=erf)
397            print('-'*40, file=erf)
398            quitting = True
399            thread.interrupt_main()
400
401
402# Pseudofiles for shell-remote communication (also used in pyshell)
403
404class StdioFile(io.TextIOBase):
405
406    def __init__(self, shell, tags, encoding='utf-8', errors='strict'):
407        self.shell = shell
408        self.tags = tags
409        self._encoding = encoding
410        self._errors = errors
411
412    @property
413    def encoding(self):
414        return self._encoding
415
416    @property
417    def errors(self):
418        return self._errors
419
420    @property
421    def name(self):
422        return '<%s>' % self.tags
423
424    def isatty(self):
425        return True
426
427
428class StdOutputFile(StdioFile):
429
430    def writable(self):
431        return True
432
433    def write(self, s):
434        if self.closed:
435            raise ValueError("write to closed file")
436        s = str.encode(s, self.encoding, self.errors).decode(self.encoding, self.errors)
437        return self.shell.write(s, self.tags)
438
439
440class StdInputFile(StdioFile):
441    _line_buffer = ''
442
443    def readable(self):
444        return True
445
446    def read(self, size=-1):
447        if self.closed:
448            raise ValueError("read from closed file")
449        if size is None:
450            size = -1
451        elif not isinstance(size, int):
452            raise TypeError('must be int, not ' + type(size).__name__)
453        result = self._line_buffer
454        self._line_buffer = ''
455        if size < 0:
456            while True:
457                line = self.shell.readline()
458                if not line: break
459                result += line
460        else:
461            while len(result) < size:
462                line = self.shell.readline()
463                if not line: break
464                result += line
465            self._line_buffer = result[size:]
466            result = result[:size]
467        return result
468
469    def readline(self, size=-1):
470        if self.closed:
471            raise ValueError("read from closed file")
472        if size is None:
473            size = -1
474        elif not isinstance(size, int):
475            raise TypeError('must be int, not ' + type(size).__name__)
476        line = self._line_buffer or self.shell.readline()
477        if size < 0:
478            size = len(line)
479        eol = line.find('\n', 0, size)
480        if eol >= 0:
481            size = eol + 1
482        self._line_buffer = line[size:]
483        return line[:size]
484
485    def close(self):
486        self.shell.close()
487
488
489class MyHandler(rpc.RPCHandler):
490
491    def handle(self):
492        """Override base method"""
493        executive = Executive(self)
494        self.register("exec", executive)
495        self.console = self.get_remote_proxy("console")
496        sys.stdin = StdInputFile(self.console, "stdin",
497                                 iomenu.encoding, iomenu.errors)
498        sys.stdout = StdOutputFile(self.console, "stdout",
499                                   iomenu.encoding, iomenu.errors)
500        sys.stderr = StdOutputFile(self.console, "stderr",
501                                   iomenu.encoding, "backslashreplace")
502
503        sys.displayhook = rpc.displayhook
504        # page help() text to shell.
505        import pydoc # import must be done here to capture i/o binding
506        pydoc.pager = pydoc.plainpager
507
508        # Keep a reference to stdin so that it won't try to exit IDLE if
509        # sys.stdin gets changed from within IDLE's shell. See issue17838.
510        self._keep_stdin = sys.stdin
511
512        install_recursionlimit_wrappers()
513
514        self.interp = self.get_remote_proxy("interp")
515        rpc.RPCHandler.getresponse(self, myseq=None, wait=0.05)
516
517    def exithook(self):
518        "override SocketIO method - wait for MainThread to shut us down"
519        time.sleep(10)
520
521    def EOFhook(self):
522        "Override SocketIO method - terminate wait on callback and exit thread"
523        global quitting
524        quitting = True
525        thread.interrupt_main()
526
527    def decode_interrupthook(self):
528        "interrupt awakened thread"
529        global quitting
530        quitting = True
531        thread.interrupt_main()
532
533
534class Executive(object):
535
536    def __init__(self, rpchandler):
537        self.rpchandler = rpchandler
538        self.locals = __main__.__dict__
539        self.calltip = calltip.Calltip()
540        self.autocomplete = autocomplete.AutoComplete()
541
542    def runcode(self, code):
543        global interruptable
544        try:
545            self.usr_exc_info = None
546            interruptable = True
547            try:
548                exec(code, self.locals)
549            finally:
550                interruptable = False
551        except SystemExit as e:
552            if e.args:  # SystemExit called with an argument.
553                ob = e.args[0]
554                if not isinstance(ob, (type(None), int)):
555                    print('SystemExit: ' + str(ob), file=sys.stderr)
556            # Return to the interactive prompt.
557        except:
558            self.usr_exc_info = sys.exc_info()
559            if quitting:
560                exit()
561            print_exception()
562            jit = self.rpchandler.console.getvar("<<toggle-jit-stack-viewer>>")
563            if jit:
564                self.rpchandler.interp.open_remote_stack_viewer()
565        else:
566            flush_stdout()
567
568    def interrupt_the_server(self):
569        if interruptable:
570            thread.interrupt_main()
571
572    def start_the_debugger(self, gui_adap_oid):
573        return debugger_r.start_debugger(self.rpchandler, gui_adap_oid)
574
575    def stop_the_debugger(self, idb_adap_oid):
576        "Unregister the Idb Adapter.  Link objects and Idb then subject to GC"
577        self.rpchandler.unregister(idb_adap_oid)
578
579    def get_the_calltip(self, name):
580        return self.calltip.fetch_tip(name)
581
582    def get_the_completion_list(self, what, mode):
583        return self.autocomplete.fetch_completions(what, mode)
584
585    def stackviewer(self, flist_oid=None):
586        if self.usr_exc_info:
587            typ, val, tb = self.usr_exc_info
588        else:
589            return None
590        flist = None
591        if flist_oid is not None:
592            flist = self.rpchandler.get_remote_proxy(flist_oid)
593        while tb and tb.tb_frame.f_globals["__name__"] in ["rpc", "run"]:
594            tb = tb.tb_next
595        sys.last_type = typ
596        sys.last_value = val
597        item = stackviewer.StackTreeItem(flist, tb)
598        return debugobj_r.remote_object_tree_item(item)
599
600
601if __name__ == '__main__':
602    from unittest import main
603    main('idlelib.idle_test.test_run', verbosity=2)
604
605capture_warnings(False)  # Make sure turned off; see bpo-18081.
606