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