Skip to content

Commit

Permalink
Add disconnect button, thonny#2328
Browse files Browse the repository at this point in the history
  • Loading branch information
aivarannamaa committed Aug 5, 2024
1 parent cdac496 commit c7030a4
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 20 deletions.
17 changes: 12 additions & 5 deletions thonny/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import thonny
from thonny import report_time
from thonny.common import ( # TODO: try to get rid of this
ALL_EXPLAINED_STATUS_CODE,
IGNORED_FILES_AND_DIRS,
PROCESS_ACK,
BackendEvent,
Expand Down Expand Up @@ -103,9 +104,15 @@ def handle_connection_error(self, error=None):
message = "Connection lost"
if error:
message += " -- " + str(error)
self._send_output(
"\n", "stderr"
) # in case we were at prompt or another line without newline
self._send_output("\n" + message + "\n", "stderr")
self._send_output("\n" + "Use Stop/Restart to reconnect." + "\n", "stderr")
sys.exit(1)
self._send_output(
"\n" + "Click ☐ at the bottom of the window or use Stop/Restart to reconnect." + "\n",
"stderr",
)
sys.exit(ALL_EXPLAINED_STATUS_CODE)

def _current_command_is_interrupted(self):
return getattr(self._current_command, "interrupted", False)
Expand Down Expand Up @@ -300,7 +307,7 @@ def _handle_normal_command(self, cmd: CommandToBackend) -> None:
except Exception as e:
logger.exception("Exception while handling %r", cmd.name)
self._report_internal_exception("Exception while handling %r" % cmd.name)
sys.exit(1)
sys.exit(ALL_EXPLAINED_STATUS_CODE)

if response is False:
# Command doesn't want to send any response
Expand Down Expand Up @@ -785,7 +792,7 @@ def _try_load_paramiko(self):
" Install it from 'Tools => Manage plug-ins' or via your system package manager.",
file=sys.stderr,
)
sys.exit(1)
sys.exit(ALL_EXPLAINED_STATUS_CODE)

def _connect(self):
from paramiko import SSHException
Expand All @@ -805,7 +812,7 @@ def _connect(self):
print("Re-check your host, authentication method, password or keys.", file=sys.stderr)
delete_stored_ssh_password()

sys.exit(1)
sys.exit(ALL_EXPLAINED_STATUS_CODE)

def _create_remote_process(self, cmd_items: List[str], cwd: str, env: Dict) -> RemoteProcess:
import shlex
Expand Down
1 change: 1 addition & 0 deletions thonny/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
OBJECT_LINK_END = "[/object_link_for_thonny]"
REMOTE_PATH_MARKER = " :: "
PROCESS_ACK = "OK"
ALL_EXPLAINED_STATUS_CODE = 193

IGNORED_FILES_AND_DIRS = [
"System Volume Information",
Expand Down
5 changes: 3 additions & 2 deletions thonny/plugins/micropython/bare_metal_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from thonny import report_time
from thonny.backend import UploadDownloadMixin, convert_newlines_if_has_shebang
from thonny.common import (
ALL_EXPLAINED_STATUS_CODE,
PROCESS_ACK,
BackendEvent,
EOFCommand,
Expand Down Expand Up @@ -1742,7 +1743,7 @@ def launch_bare_metal_backend(backend_class: Callable[..., BareMetalMicroPythonB
try:
if args["port"] is None:
print("\nPort not defined", file=sys.stderr)
sys.exit(1)
sys.exit(ALL_EXPLAINED_STATUS_CODE)
elif args["port"] == "webrepl":
connection = WebReplConnection(args["url"], args["password"])
else:
Expand All @@ -1763,7 +1764,7 @@ def launch_bare_metal_backend(backend_class: Callable[..., BareMetalMicroPythonB
msg = BackendEvent(event_type="ProgramOutput", stream_name="stderr", data=text)
sys.stdout.write(serialize_message(msg) + "\n")
sys.stdout.flush()
sys.exit(1)
sys.exit(ALL_EXPLAINED_STATUS_CODE)


if __name__ == "__main__":
Expand Down
6 changes: 6 additions & 0 deletions thonny/plugins/micropython/mp_front.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,9 @@ def _augment_dist_info(cls, dist_info: DistInfo) -> DistInfo:

return dist_info

def needs_disconnect_button(self):
return True


class BareMetalMicroPythonProxy(MicroPythonProxy):
def __init__(self, clean):
Expand Down Expand Up @@ -505,6 +508,9 @@ def _show_error(self, text):

def disconnect(self):
self.destroy()
self._show_error(
"\nDisconnected.\n\nClick ☐ at the bottom of the window or use Stop/Restart to reconnect."
)

def get_node_label(self):
if "CircuitPython" in self._welcome_text:
Expand Down
4 changes: 2 additions & 2 deletions thonny/plugins/micropython/os_mp_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import thonny
from thonny import report_time
from thonny.backend import SshMixin
from thonny.common import PROCESS_ACK, BackendEvent, serialize_message
from thonny.common import ALL_EXPLAINED_STATUS_CODE, PROCESS_ACK, BackendEvent, serialize_message
from thonny.plugins.micropython.bare_metal_backend import LF, NORMAL_PROMPT
from thonny.plugins.micropython.connection import MicroPythonConnection
from thonny.plugins.micropython.mp_back import (
Expand Down Expand Up @@ -75,7 +75,7 @@ def __init__(self, args):
msg = BackendEvent(event_type="ProgramOutput", stream_name="stderr", data=text)
sys.stdout.write(serialize_message(msg) + "\n")
sys.stdout.flush()
sys.exit(1)
sys.exit(ALL_EXPLAINED_STATUS_CODE)

MicroPythonBackend.__init__(self, None, args)

Expand Down
3 changes: 2 additions & 1 deletion thonny/plugins/micropython/subprocess_connection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import signal
import sys

from thonny.common import ALL_EXPLAINED_STATUS_CODE
from thonny.plugins.micropython.connection import MicroPythonConnection


Expand All @@ -15,7 +16,7 @@ def __init__(self, executable, args=[]):
"ERROR: This back-end requires a Python package named 'ptyprocess'.\n"
+ "Install it via system package manager or 'Tools => Manage plug-ins'."
)
sys.exit(1)
sys.exit(ALL_EXPLAINED_STATUS_CODE)

super().__init__()
cmd = [executable] + args
Expand Down
4 changes: 2 additions & 2 deletions thonny/plugins/micropython/webrepl_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from logging import DEBUG, getLogger
from queue import Queue

from ...common import execute_with_frontend_sys_path
from ...common import ALL_EXPLAINED_STATUS_CODE, execute_with_frontend_sys_path
from .connection import MicroPythonConnection

logger = getLogger(__name__)
Expand Down Expand Up @@ -47,7 +47,7 @@ def _try_load_websockets(self):
"Can't import `websockets`. You can install it via 'Tools => Manage plug-ins'.",
file=sys.stderr,
)
sys.exit(1)
sys.exit(ALL_EXPLAINED_STATUS_CODE)

def _wrap_ws_main(self):
import asyncio
Expand Down
36 changes: 32 additions & 4 deletions thonny/running.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
report_time,
)
from thonny.common import (
ALL_EXPLAINED_STATUS_CODE,
PROCESS_ACK,
BackendEvent,
CommandToBackend,
Expand Down Expand Up @@ -657,7 +658,8 @@ def disconnect(self):
proxy.disconnect()

def disconnect_enabled(self):
return hasattr(self.get_backend_proxy(), "disconnect")
proxy = self.get_backend_proxy()
return proxy is not None and proxy.needs_disconnect_button()

def ctrld(self):
proxy = self.get_backend_proxy()
Expand Down Expand Up @@ -749,8 +751,11 @@ def _pull_backend_messages(self):
self._send_thread_commands()
self._send_postponed_commands()

def _handle_backend_termination(self, returncode: int) -> None:
err = f"Process ended with exit code {returncode}."
def _handle_backend_termination(self, returncode: Optional[int]) -> None:
if returncode is None or returncode == ALL_EXPLAINED_STATUS_CODE:
err = ""
else:
err = f"Process ended with exit code {returncode}."

try:
faults_file = os.path.join(get_thonny_user_dir(), "backend_faults.log")
Expand Down Expand Up @@ -896,6 +901,12 @@ def using_venv(self) -> bool:
isinstance(self._proxy, (LocalCPythonProxy, SshCPythonProxy)) and self._proxy._in_venv
)

def is_connected(self):
if self._proxy is None:
return False
else:
return self._proxy.is_connected()


class BackendProxy(ABC):
"""Communicates with backend process.
Expand Down Expand Up @@ -949,7 +960,24 @@ def destroy(self, for_restart: bool = False):
...

@abstractmethod
def is_connected(self): ...
def is_connected(self) -> bool:
"""Returns True if the backend is operational.
Returns False if the connection is lost (or not created yet)
or the remote process has died (or not created yet).
"""
...

def disconnect(self):
"""
Means different things for different backends.
For local CPython it does nothing (???).
For BareMetalMicroPython it means simply closing the serial connection.
For SSH back-ends it means closing the SSH connection.
"""
pass

def needs_disconnect_button(self):
return False

@abstractmethod
def has_local_interpreter(self): ...
Expand Down
40 changes: 36 additions & 4 deletions thonny/workbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ def __init__(self, parsed_args: Dict[str, Any]) -> None:
self.bind("<FocusOut>", self._on_focus_out, True)
self.bind("<FocusIn>", self._on_focus_in, True)
self.bind("BackendRestart", self._on_backend_restart, True)
self.bind("BackendTerminated", self._on_backend_terminated, True)

self._publish_commands()
self.initializing = False
Expand Down Expand Up @@ -904,11 +905,38 @@ def _init_backend_switcher(self):
menu_conf = get_style_configuration("Menu")
self._backend_menu = tk.Menu(self._statusbar, tearoff=False, **menu_conf)

# Set up the button.
self._backend_button = CustomToolbutton(self._statusbar, text=get_menu_char())
# Set up the buttons
self._connection_button = CustomToolbutton(
self._statusbar, text="", command=self._toggle_connection
)
self._connection_button.grid(row=1, column=8, sticky="nes")

self._backend_button = CustomToolbutton(
self._statusbar, text=get_menu_char(), command=self._post_backend_menu
)

self._backend_button.grid(row=1, column=3, sticky="nes")
self._backend_button.configure(command=self._post_backend_menu)
self._backend_button.grid(row=1, column=9, sticky="nes")

def _update_connection_button(self):
if get_runner().is_connected():
# ▣☑☐🔲🔳☑️☹️🟢🔴🔵🟠🟡🟣🟤🟦🟧🟥🟨🟩🟪🟫🟬🟭🟮
self._connection_button.configure(text=" ☑ ")
should_be_visible = get_runner().disconnect_enabled()
else:
self._connection_button.configure(text=" ☐ ")
should_be_visible = True

if should_be_visible and not self._connection_button.winfo_ismapped():
self._connection_button.grid()
elif not should_be_visible and self._connection_button.winfo_ismapped():
self._connection_button.grid_remove()

def _toggle_connection(self):
if get_runner().is_connected():
get_runner().get_backend_proxy().disconnect()
else:
get_runner().restart_backend(clean=False, first=False, automatic=False)
self._update_connection_button()

def _post_backend_menu(self):
from thonny.plugins.micropython.uf2dialog import (
Expand Down Expand Up @@ -1016,6 +1044,10 @@ def _on_backend_restart(self, event):
self._backend_conf_variable.set(value=switcher_value)
self._last_active_backend_conf_variable_value = switcher_value
self._backend_button.configure(text=desc + " " + get_menu_char())
self._update_connection_button()

def _on_backend_terminated(self, event):
self._update_connection_button()

def _init_theming(self) -> None:
if self.get_option("view.ui_theme") == "Kind of Aqua":
Expand Down

0 comments on commit c7030a4

Please sign in to comment.