1import shutil
2from tkinter import messagebox
3
4from thonny import get_runner, get_shell, get_workbench
5from thonny.common import ImmediateCommand, ToplevelCommand
6from thonny.languages import tr
7from thonny.plugins.backend_config_page import BaseSshProxyConfigPage, get_ssh_password
8from thonny.running import SubprocessProxy
9
10
11class SshCPythonProxy(SubprocessProxy):
12    def __init__(self, clean):
13        self._host = get_workbench().get_option("ssh.host")
14        self._user = get_workbench().get_option("ssh.user")
15        self._remote_interpreter = get_workbench().get_option("ssh.executable")
16
17        super().__init__(clean)
18        self._send_msg(ToplevelCommand("get_environment_info"))
19
20    def _get_launcher_with_args(self):
21        return [
22            "-m",
23            "thonny.plugins.cpython_ssh",
24            repr(
25                {
26                    "host": self._host,
27                    "user": self._user,
28                    "password": get_ssh_password("ssh"),
29                    "interpreter": self._remote_interpreter,
30                    "cwd": self._get_initial_cwd(),
31                }
32            ),
33        ]
34
35    def _connect(self):
36        pass
37
38    def _get_initial_cwd(self):
39        return get_workbench().get_option("ssh.cwd")
40
41    def _publish_cwd(self, cwd):
42        return get_workbench().set_option("ssh.cwd", cwd)
43
44    def interrupt(self):
45        # Don't interrupt local process, but direct it to device
46        self._send_msg(ImmediateCommand("interrupt"))
47
48    def fetch_next_message(self):
49        msg = super().fetch_next_message()
50        if msg and "welcome_text" in msg:
51            assert hasattr(self, "_reported_executable")
52            msg["welcome_text"] += " (" + self._reported_executable + " on " + self._host + ")"
53        return msg
54
55    def supports_remote_files(self):
56        return self._proc is not None
57
58    def uses_local_filesystem(self):
59        return False
60
61    def ready_for_remote_file_operations(self):
62        return self._proc is not None and get_runner().is_waiting_toplevel_command()
63
64    def supports_remote_directories(self):
65        return self._cwd is not None and self._cwd != ""
66
67    def supports_trash(self):
68        return False
69
70    def is_connected(self):
71        return self._proc is not None
72
73    def _show_error(self, text):
74        get_shell().print_error("\n" + text + "\n")
75
76    def disconnect(self):
77        self.destroy()
78
79    def get_node_label(self):
80        return self._host
81
82    def get_exe_dirs(self):
83        return []
84
85    def destroy(self):
86        try:
87            self.send_command(ImmediateCommand("kill"))
88        except BrokenPipeError:
89            pass
90        except OSError:
91            pass
92        super().destroy()
93
94    def can_run_remote_files(self):
95        return True
96
97    def can_run_local_files(self):
98        return False
99
100    @classmethod
101    def should_show_in_switcher(cls):
102        # Show when the executable, user and host are configured
103        return (
104            get_workbench().get_option("ssh.host")
105            and get_workbench().get_option("ssh.user")
106            and get_workbench().get_option("ssh.executable")
107        )
108
109    @classmethod
110    def get_switcher_entries(cls):
111        if cls.should_show_in_switcher():
112            return [(cls.get_current_switcher_configuration(), cls.backend_description)]
113        else:
114            return []
115
116    def get_pip_gui_class(self):
117        from thonny.plugins import pip_gui
118
119        return pip_gui.CPythonBackendPipDialog
120
121    def has_custom_system_shell(self):
122        return True
123
124    def open_custom_system_shell(self):
125        if not shutil.which("ssh"):
126            messagebox.showerror(
127                "Command not found", "Command 'ssh' not found", master=get_workbench()
128            )
129            return
130
131        from thonny import terminal
132
133        userhost = "%s@%s" % (self._user, self._host)
134        terminal.run_in_terminal(
135            ["ssh", userhost], cwd=get_workbench().get_local_cwd(), keep_open=False, title=userhost
136        )
137
138
139class SshProxyConfigPage(BaseSshProxyConfigPage):
140    backend_name = None  # Will be overwritten on Workbench.add_backend
141
142    def __init__(self, master):
143        super().__init__(master, "ssh")
144
145
146def load_plugin():
147    get_workbench().set_default("ssh.host", "")
148    get_workbench().set_default("ssh.user", "")
149    get_workbench().set_default("ssh.auth_method", "password")
150    get_workbench().set_default("ssh.executable", "python3")
151    get_workbench().set_default("ssh.cwd", "~")
152    get_workbench().add_backend(
153        "SSHProxy", SshCPythonProxy, tr("Remote Python 3 (SSH)"), SshProxyConfigPage, sort_key="15"
154    )
155