diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index cddf1169..022a03c0 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -1,17 +1,14 @@ """For running Python code that could interrupt itself at any time in order to, for example, ask for a read on stdin, or a write on stdout -The CodeRunner spawns a greenlet to run code in, and that code can suspend its -own execution to ask the main greenlet to refresh the display or get +The CodeRunner spawns a thread to run code in, and that code can block +on a queue to ask the main (UI) thread to refresh the display or get information. - -Greenlets are basically threads that can explicitly switch control to each -other. You can replace the word "greenlet" with "thread" in these docs if that -makes more sense to you. """ import code -import greenlet +from six.moves import queue +import threading import logging import signal @@ -21,12 +18,12 @@ class SigintHappened: - """If this class is returned, a SIGINT happened while the main greenlet""" + """If this class is returned, a SIGINT happened while the main thread""" class SystemExitFromCodeRunner(SystemExit): """If this class is returned, a SystemExit happened while in the code - greenlet""" + thread""" class RequestFromCodeRunner: @@ -60,13 +57,14 @@ class CodeRunner: """Runs user code in an interpreter. Running code requests a refresh by calling - request_from_main_context(force_refresh=True), which - suspends execution of the code and switches back to the main greenlet + request_from_main_thread(force_refresh=True), which + suspends execution of the code by blocking on a queue + that the main thread was blocked on. After load_code() is called with the source code to be run, the run_code() method should be called to start running the code. The running code may request screen refreshes and user input - by calling request_from_main_context. + by calling request_from_main_thread. When this are called, the running source code cedes control, and the current run_code() method call returns. @@ -77,10 +75,10 @@ class CodeRunner: has been gathered, run_code() should be called again, passing in any requested user input. This continues until run_code returns Done. - The code greenlet is responsible for telling the main greenlet + The code thread is responsible for telling the main thread what it wants returned in the next run_code call - CodeRunner just passes whatever is passed in to run_code(for_code) to the - code greenlet + code thread. """ def __init__(self, interp=None, request_refresh=lambda: None): @@ -93,20 +91,20 @@ def __init__(self, interp=None, request_refresh=lambda: None): """ self.interp = interp or code.InteractiveInterpreter() self.source = None - self.main_context = greenlet.getcurrent() - self.code_context = None + self.code_thread = None + self.requests_from_code_thread = queue.Queue(maxsize=0) + self.responses_for_code_thread = queue.Queue() self.request_refresh = request_refresh # waiting for response from main thread self.code_is_waiting = False # sigint happened while in main thread - self.sigint_happened_in_main_context = False + self.sigint_happened_in_main_thread = False # TODO rename context to thread self.orig_sigint_handler = None @property def running(self): - """Returns greenlet if code has been loaded greenlet has been - started""" - return self.source and self.code_context + """Returns the running thread if code has been loaded and started.""" + return self.source and self.code_thread def load_code(self, source): """Prep code to be run""" @@ -114,43 +112,47 @@ def load_code(self, source): "you shouldn't load code when some is " "already running" ) self.source = source - self.code_context = None + self.code_thread = None def _unload_code(self): """Called when done running code""" self.source = None - self.code_context = None + self.code_thread = None self.code_is_waiting = False def run_code(self, for_code=None): """Returns Truthy values if code finishes, False otherwise - if for_code is provided, send that value to the code greenlet + if for_code is provided, send that value to the code thread if source code is complete, returns "done" if source code is incomplete, returns "unfinished" """ - if self.code_context is None: + if self.code_thread is None: assert self.source is not None - self.code_context = greenlet.greenlet(self._blocking_run_code) + self.code_thread = threading.Thread( + target=self._blocking_run_code, + name='codethread') + self.code_thread.daemon = True if is_main_thread(): self.orig_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - request = self.code_context.switch() + self.code_thread.start() else: assert self.code_is_waiting self.code_is_waiting = False if is_main_thread(): signal.signal(signal.SIGINT, self.sigint_handler) - if self.sigint_happened_in_main_context: - self.sigint_happened_in_main_context = False - request = self.code_context.switch(SigintHappened) + if self.sigint_happened_in_main_thread: + self.sigint_happened_in_main_thread = False + self.responses_for_code_thread.put(SigintHappened) else: - request = self.code_context.switch(for_code) + self.responses_for_code_thread.put(for_code) + request = self.requests_from_code_thread.get() logger.debug("request received from code was %r", request) if not isinstance(request, RequestFromCodeRunner): raise ValueError( - "Not a valid value from code greenlet: %r" % request + "Not a valid value from code thread: %r" % request ) if isinstance(request, (Wait, Refresh)): self.code_is_waiting = True @@ -170,7 +172,7 @@ def run_code(self, for_code=None): def sigint_handler(self, *args): """SIGINT handler to use while code is running or request being fulfilled""" - if greenlet.getcurrent() is self.code_context: + if threading.current_thread() is self.code_thread: logger.debug("sigint while running user code!") raise KeyboardInterrupt() else: @@ -178,24 +180,29 @@ def sigint_handler(self, *args): "sigint while fulfilling code request sigint handler " "running!" ) - self.sigint_happened_in_main_context = True + self.sigint_happened_in_main_thread = True def _blocking_run_code(self): try: unfinished = self.interp.runsource(self.source) except SystemExit as e: - return SystemExitRequest(*e.args) - return Unfinished() if unfinished else Done() + self.requests_from_code_thread.push(SystemExitRequest(*e.args)) + return + self.requests_from_code_thread.put(Unfinished() + if unfinished + else Done()) - def request_from_main_context(self, force_refresh=False): + def request_from_main_thread(self, force_refresh=False): """Return the argument passed in to .run_code(for_code) Nothing means calls to run_code must be... ??? """ if force_refresh: - value = self.main_context.switch(Refresh()) + self.requests_from_code_thread.put(Refresh()) + value = self.responses_for_code_thread.get() else: - value = self.main_context.switch(Wait()) + self.requests_from_code_thread.put(Wait()) + value = self.responses_for_code_thread.get() if value is SigintHappened: raise KeyboardInterrupt() return value @@ -216,7 +223,7 @@ def __init__(self, coderunner, on_write, real_fileobj): def write(self, s, *args, **kwargs): self.on_write(s, *args, **kwargs) - return self.coderunner.request_from_main_context(force_refresh=True) + return self.coderunner.request_from_main_thread(force_refresh=True) # Some applications which use curses require that sys.stdout # have a method called fileno. One example is pwntools. This diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 51fd28d3..ebed9f31 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -1,5 +1,5 @@ -import greenlet import time +from queue import Queue from curtsies import events from ..translations import _ @@ -43,8 +43,8 @@ def __init__( self.permanent_stack = [] if permanent_text: self.permanent_stack.append(permanent_text) - self.main_context = greenlet.getcurrent() - self.request_context = None + self.response_queue = Queue() + self.request_or_notify_queue = Queue() self.request_refresh = request_refresh self.schedule_refresh = schedule_refresh @@ -83,13 +83,14 @@ def process_event(self, e): assert self.in_prompt or self.in_confirm or self.waiting_for_refresh if isinstance(e, RefreshRequestEvent): self.waiting_for_refresh = False - self.request_context.switch() + self.request_or_notify_queue.put(None) + self.response_queue.get() elif isinstance(e, events.PasteEvent): for ee in e.events: # strip control seq self.add_normal_character(ee if len(ee) == 1 else ee[-1]) elif e == "" or isinstance(e, events.SigIntEvent): - self.request_context.switch(False) + self.request_queue.put(False) self.escape() elif e in edit_keys: self.cursor_offset_in_line, self._current_line = edit_keys[e]( @@ -102,12 +103,12 @@ def process_event(self, e): elif self.in_prompt and e in ("\n", "\r", "", "Ctrl-m>"): line = self._current_line self.escape() - self.request_context.switch(line) + self.response_queue.put(line) elif self.in_confirm: if e.lower() == _("y"): - self.request_context.switch(True) + self.request_queue.put(True) else: - self.request_context.switch(False) + self.request_queue.put(False) self.escape() else: # add normal character self.add_normal_character(e) @@ -126,6 +127,7 @@ def add_normal_character(self, e): def escape(self): """unfocus from statusbar, clear prompt state, wait for notify call""" + self.wait_for_request_or_notify() self.in_prompt = False self.in_confirm = False self.prompt = "" @@ -148,27 +150,34 @@ def current_line(self): def should_show_message(self): return bool(self.current_line) - # interaction interface - should be called from other greenlets + def wait_for_request_or_notify(self): + try: + r = self.request_or_notify_queue.get(True, 1) + except queue.Empty: + raise Exception('Main thread blocked because task thread not calling back') + return r + + # interaction interface - should be called from other threads def notify(self, msg, n=3, wait_for_keypress=False): - self.request_context = greenlet.getcurrent() self.message_time = n self.message(msg, schedule_refresh=wait_for_keypress) self.waiting_for_refresh = True self.request_refresh() - self.main_context.switch(msg) + self.request_or_notify_queue.push(msg) - # below really ought to be called from greenlets other than main because + ################################### + # below really ought to be called from threads other than main because # they block def confirm(self, q): """Expected to return True or False, given question prompt q""" - self.request_context = greenlet.getcurrent() self.prompt = q self.in_confirm = True - return self.main_context.switch(q) + self.request_or_notify_queue.put(q) + return self.response_queue.get() def file_prompt(self, s): """Expected to return a file name, given""" - self.request_context = greenlet.getcurrent() self.prompt = s self.in_prompt = True - return self.main_context.switch(s) + self.request_or_notify_queue.put(s) + return self.response_queue.get() diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 30fbbb14..a0409d5f 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +import threading import time import unicodedata from enum import Enum @@ -111,7 +112,7 @@ def process_event(self, e): self.cursor_offset, self.current_line ) elif isinstance(e, events.SigIntEvent): - self.coderunner.sigint_happened_in_main_context = True + self.coderunner.sigint_happened_in_main_thread = True self.has_focus = False self.current_line = "" self.cursor_offset = 0 @@ -163,7 +164,7 @@ def add_input_character(self, e): def readline(self): self.has_focus = True self.repl.send_to_stdin(self.current_line) - value = self.coderunner.request_from_main_context() + value = self.coderunner.request_from_main_thread() self.readline_results.append(value) return value @@ -773,15 +774,15 @@ def process_key_event(self, e): elif e in key_dispatch[self.config.redo_key]: # ctrl-g for redo self.redo() elif e in key_dispatch[self.config.save_key]: # ctrl-s for save - greenlet.greenlet(self.write2file).switch() + self.switch(self.write2file) elif e in key_dispatch[self.config.pastebin_key]: # F8 for pastebin - greenlet.greenlet(self.pastebin).switch() + self.switch(self.pastebin) elif e in key_dispatch[self.config.copy_clipboard_key]: - greenlet.greenlet(self.copy2clipboard).switch() + self.switch(self.copy2clipboard) elif e in key_dispatch[self.config.external_editor_key]: self.send_session_to_external_editor() elif e in key_dispatch[self.config.edit_config_key]: - greenlet.greenlet(self.edit_config).switch() + self.switch(self.edit_config) # TODO add PAD keys hack as in bpython.cli elif e in key_dispatch[self.config.edit_current_block_key]: self.send_current_block_to_external_editor() @@ -792,6 +793,14 @@ def process_key_event(self, e): else: self.add_normal_character(e) + def switch(self, task): + """Runs task in another thread""" + t = threading.Thread(target=task) + t.daemon = True + logging.debug('starting task thread') + t.start() + self.interact.wait_for_request_or_notify() + def get_last_word(self): previous_word = _last_word(self.rl_history.entry) @@ -1849,7 +1858,7 @@ def prompt_for_undo(): if n > 0: self.request_undo(n=n) - greenlet.greenlet(prompt_for_undo).switch() + self.switch(prompt_for_undo) def redo(self): if self.redo_stack: diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 84fb2da5..d28d450f 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -60,7 +60,7 @@ Next install your development copy of bpython and its dependencies: .. code-block:: bash - $ sudp apt install python3-greenlet python3-pygments python3-requests + $ sudp apt install python3-pygments python3-requests $ sudo apt install python3-watchdog python3-urwid $ sudo apt install python3-sphinx python3-pytest