def _forward_output_until_active_prompt( self, output_consumer: Callable[[str, str], None], stream_name="stdout"): INCREMENTAL_OUTPUT_BLOCK_CLOSERS = re.compile(b"|".join( map(re.escape, [LF, NORMAL_PROMPT]))) pending = b"" while True: # There may be an input submission waiting # and we can't progress without resolving it first self._check_for_side_commands() # Prefer whole lines, but allow also incremental output to single line new_data = self._connection.soft_read_until( INCREMENTAL_OUTPUT_BLOCK_CLOSERS, timeout=0.05) if not new_data: continue pending += new_data if pending.endswith(LF): output_consumer(self._decode(pending), stream_name) pending = b"" elif pending.endswith(NORMAL_PROMPT): out = pending[:-len(NORMAL_PROMPT)] output_consumer(self._decode(out), stream_name) return NORMAL_PROMPT elif ends_overlap(pending, NORMAL_PROMPT): # Maybe we have a prefix of the prompt and the rest is still coming? follow_up = self._connection.soft_read(1, timeout=0.1) if not follow_up: # most likely not a Python prompt, let's forget about it output_consumer(self._decode(pending), stream_name) pending = b"" else: # Let's withhold this for now pending += follow_up else: # No prompt in sight. # Output and keep working. output_consumer(self._decode(pending), stream_name) pending = b""
def _forward_output_until_active_prompt( self, output_consumer: Callable[[str, str], None], stream_name="stdout"): """Meant for incrementally forwarding stdout from user statements, scripts and soft-reboots. Also used for forwarding side-effect output from expression evaluations and for capturing help("modules") output. In these cases it is expected to arrive to an EOT. Also used for initial prompt searching or for recovering from a protocol error. In this case it must work until active normal prompt or first raw prompt. The code may have been submitted in any of the REPL modes or automatically via (soft-)reset. NB! The processing may end in normal mode even if the command was started in raw mode (eg. when user presses reset during processing in some devices)! The processing may also end in FIRST_RAW_REPL, when it was started in normal REPL and Ctrl+A was issued during processing (ie. before Ctrl+C in this example): 6 7 8 9 10 Traceback (most recent call last): File "main.py", line 5, in <module> KeyboardInterrupt: MicroPython v1.11-624-g210d05328 on 2019-12-09; ESP32 module with ESP32 Type "help()" for more information. >>> raw REPL; CTRL-B to exit > (Preceding output does not contain EOT) Note that this Ctrl+A may have been issued even before Thonny connected to the device. Note that interrupt does not affect the structure of the output -- it is presented just like any other exception. The method returns EOT, RAW_PROMPT or NORMAL_PROMPT, depending on which terminator ended the processing. The terminating EOT may be either the first EOT from normal raw-REPL output or the starting EOT from Thonny expression (or, in principle, even the second raw-REPL EOT or terminating Thonny expression EOT) -- the caller will do the interpretation. Because ot the special role of EOT and NORMAL_PROMT, we assume user code will not output these. If it does, processing may break. It may succceed if the propmt is followed by something (quickly enough) -- that's why we look for *active* prompt, ie. prompt without following text. TODO: Experiment with this! Output produced by background threads (eg. in WiPy ESP32) cause even more difficulties, because it becomes impossible to say whether we are at prompt and output is from another thread or the main thread is still running. For now I'm ignoring these problems and assume all output comes from the main thread. """ INCREMENTAL_OUTPUT_BLOCK_CLOSERS = re.compile(b"|".join( map(re.escape, [NORMAL_PROMPT, LF]))) pending = b"" while True: # There may be an input submission waiting # and we can't progress without resolving it first self._check_for_side_commands() # Prefer whole lines, but allow also incremental output to single line new_data = self._connection.soft_read_until( INCREMENTAL_OUTPUT_BLOCK_CLOSERS, timeout=0.05) if TRACEBACK_MARKER in new_data: stream_name = "stderr" if not new_data: # In case we are still waiting for the first bits after connecting ... # TODO: this suggestion should be implemented in Shell if (self._connection.num_bytes_received == 0 and not self._interrupt_suggestion_given and time.time() - self._connection.startup_time > 2.5): self._show_error( "\n" + "Device is busy or does not respond. Your options:\n\n" + " - wait until it completes current work;\n" + " - use Ctrl+C to interrupt current work;\n" + " - use Stop/Restart to interrupt more and enter REPL.\n" ) self._interrupt_suggestion_given = True if not pending: # nothing to parse continue pending += new_data if pending.endswith(LF): output_consumer(self._decode(pending), stream_name) pending = b"" elif pending.endswith(NORMAL_PROMPT): # This looks like prompt. # Make sure it is not followed by anything. follow_up = self._connection.soft_read(1, timeout=0.01) if follow_up: # Nope, the prompt is not active. # (Actually it may be that a background thread has produced this follow up, # but this would be too hard to consider.) # Don't output yet, because the follow up may turn into another prompt # and they can be captured all together. self._connection.unread(follow_up) # read propmt must remain in pending else: # let's hope it is an active prompt terminator = NORMAL_PROMPT # Strip all trailing prompts out = pending while True: if out.endswith(NORMAL_PROMPT): out = out[:-len(NORMAL_PROMPT)] else: break output_consumer(self._decode(out), stream_name) return terminator elif ends_overlap(pending, NORMAL_PROMPT): # Maybe we have a prefix of the prompt and the rest is still coming? # (it's OK to wait a bit, as the user output usually ends with a newline, ie not # with a prompt prefix) follow_up = self._connection.soft_read(1, timeout=0.3) if not follow_up: # most likely not a Python prompt, let's forget about it output_consumer(self._decode(pending), stream_name) pending = b"" else: # Let's try the possible prefix again in the next iteration # (I'm unreading otherwise the read_until won't see the whole prompt # and needs to wait for the timeout) n = ends_overlap(pending, NORMAL_PROMPT) try_again = pending[-n:] pending = pending[:-n] self._connection.unread(try_again + follow_up) else: # No EOT or prompt in sight. # Output and keep working. output_consumer(self._decode(pending), stream_name) pending = b""