Skip to content

Commit e6bdd99

Browse files
authored
Merge pull request #107 from Integration-Automation/dev
Dev
2 parents 1006a91 + 42d53e6 commit e6bdd99

24 files changed

+1480
-10
lines changed

automation_ide/automation_editor_ui/connect_gui/__init__.py

Whitespace-only changes.

automation_ide/automation_editor_ui/connect_gui/ssh/__init__.py

Whitespace-only changes.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import os
2+
import re
3+
4+
import paramiko
5+
from PySide6.QtCore import QThread, Signal
6+
from PySide6.QtWidgets import (
7+
QWidget, QLineEdit, QPushButton,
8+
QPlainTextEdit, QHBoxLayout, QVBoxLayout,
9+
QMessageBox
10+
)
11+
from je_editor import language_wrapper
12+
13+
from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_login_widget import LoginWidget
14+
15+
ANSI_ESCAPE_PATTERN = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
16+
17+
18+
class SSHReaderThread(QThread):
19+
data_received = Signal(bytes)
20+
closed = Signal(str)
21+
22+
def __init__(self, chan: paramiko.Channel, parent=None):
23+
super().__init__(parent)
24+
self.chan = chan
25+
self._running = True
26+
self.word_dict = language_wrapper.language_word_dict
27+
28+
def run(self):
29+
try:
30+
while self._running:
31+
if self.chan.recv_ready():
32+
data = self.chan.recv(4096)
33+
if data:
34+
self.data_received.emit(data)
35+
36+
if self.chan.recv_stderr_ready():
37+
err = self.chan.recv_stderr(4096)
38+
if err:
39+
self.data_received.emit(err)
40+
41+
if self.chan.closed or self.chan.exit_status_ready():
42+
break
43+
44+
self.msleep(10)
45+
except Exception as e:
46+
self.closed.emit(
47+
f"{self.word_dict.get('ssh_command_widget_error_message_reader_failed')} {e}")
48+
finally:
49+
self.closed.emit(
50+
self.word_dict.get("ssh_command_widget_log_message_reader_closed"))
51+
52+
def stop(self):
53+
self._running = False
54+
55+
56+
class SSHCommandWidget(QWidget):
57+
def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: bool = True):
58+
super().__init__()
59+
self.word_dict = language_wrapper.language_word_dict
60+
self.setWindowTitle(
61+
self.word_dict.get("ssh_command_widget_window_title_ssh_command_widget"))
62+
63+
self.add_login_widget = add_login_widget
64+
65+
# SSH 相關物件
66+
self.ssh_client: paramiko.SSHClient | None = None
67+
self.shell_channel: paramiko.Channel | None = None
68+
self.reader_thread: SSHReaderThread | None = None
69+
70+
if self.add_login_widget:
71+
# 使用獨立的登入介面
72+
self.login_widget = LoginWidget()
73+
else:
74+
if external_login_widget is None:
75+
external_login_widget = LoginWidget()
76+
self.login_widget = external_login_widget
77+
78+
# 其他 UI 控制元件
79+
self.terminal = QPlainTextEdit()
80+
self.terminal.setReadOnly(True)
81+
self.command_input_edit = QLineEdit()
82+
self.command_send_button = QPushButton(
83+
self.word_dict.get("ssh_command_widget_button_label_send_command"))
84+
85+
self._setup_ui()
86+
self._bind_events()
87+
88+
def _setup_ui(self):
89+
self.terminal.setReadOnly(True)
90+
self.terminal.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
91+
self.command_input_edit.setPlaceholderText(
92+
self.word_dict.get("ssh_command_widget_input_placeholder_command_line")
93+
)
94+
95+
terminal_panel = QVBoxLayout()
96+
terminal_panel.addWidget(self.terminal)
97+
98+
command_input_bar = QHBoxLayout()
99+
command_input_bar.addWidget(self.command_input_edit)
100+
command_input_bar.addWidget(self.command_send_button)
101+
102+
main_widget = QVBoxLayout()
103+
main_widget.addWidget(self.login_widget) # 插入登入介面
104+
main_widget.addLayout(terminal_panel)
105+
main_widget.addLayout(command_input_bar)
106+
107+
self.setLayout(main_widget)
108+
109+
def _bind_events(self):
110+
# 綁定 LoginWidget 的按鈕
111+
self.login_widget.connect_btn.clicked.connect(self.connect_ssh)
112+
self.login_widget.disconnect_btn.clicked.connect(self.disconnect_ssh)
113+
114+
# 綁定其他按鈕
115+
self.command_send_button.clicked.connect(self.send_command)
116+
self.command_input_edit.returnPressed.connect(self.send_command)
117+
118+
def append_text(self, text: str):
119+
self.terminal.appendPlainText(text)
120+
121+
def connect_ssh(self):
122+
host = self.login_widget.host_edit.text().strip()
123+
port = self.login_widget.port_spin.value()
124+
user = self.login_widget.user_edit.text().strip()
125+
use_key = self.login_widget.use_key_check.isChecked()
126+
key_path = self.login_widget.key_edit.text().strip()
127+
password = self.login_widget.pass_edit.text()
128+
129+
if not host or not user:
130+
QMessageBox.warning(
131+
self,
132+
self.word_dict.get("ssh_command_widget_dialog_title_input_error"),
133+
self.word_dict.get(
134+
"ssh_command_widget_dialog_message_input_error_host_user_required"))
135+
return
136+
137+
try:
138+
self.ssh_client = paramiko.SSHClient()
139+
self.ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
140+
141+
if use_key:
142+
if not os.path.exists(key_path):
143+
QMessageBox.warning(
144+
self,
145+
self.word_dict.get("ssh_command_widget_dialog_title_key_error"),
146+
self.word_dict.get("ssh_command_widget_dialog_message_key_file_not_exist"))
147+
return
148+
try:
149+
pkey = None
150+
for KeyType in (paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey):
151+
try:
152+
pkey = KeyType.from_private_key_file(key_path, password if password else None)
153+
break
154+
except Exception as error:
155+
print(error)
156+
continue
157+
if pkey is None:
158+
raise ValueError(
159+
self.word_dict.get(
160+
"ssh_command_widget_error_message_unsupported_private_key"
161+
))
162+
self.ssh_client.connect(hostname=host, port=port, username=user, pkey=pkey, timeout=10)
163+
except Exception as e:
164+
raise RuntimeError(
165+
f"{self.word_dict.get('ssh_command_widget_error_message_key_auth_failed')} {e}")
166+
else:
167+
self.ssh_client.connect(
168+
hostname=host, port=port, username=user, password=password, timeout=10
169+
)
170+
171+
self.shell_channel = self.ssh_client.invoke_shell(term='xterm', width=120, height=32)
172+
self.shell_channel.settimeout(0.0)
173+
self.reader_thread = SSHReaderThread(self.shell_channel)
174+
self.reader_thread.data_received.connect(self._on_data)
175+
self.reader_thread.closed.connect(self._on_closed)
176+
self.reader_thread.start()
177+
self.login_widget.status_label.setText(
178+
self.word_dict.get("ssh_command_widget_dialog_title_not_connected"))
179+
self.append_text(f"{self.word_dict.get('ssh_command_widget_log_message_connected')}"
180+
f" {host}:{port} as {user}\n")
181+
except Exception as e:
182+
self.login_widget.status_label.setText(
183+
self.word_dict.get('ssh_command_widget_status_label_disconnected'))
184+
self.append_text(f"{self.word_dict.get('ssh_command_widget_log_message_error')} {e}\n")
185+
self._cleanup()
186+
187+
def _on_data(self, data: bytes):
188+
try:
189+
text = data.decode("utf-8", errors="replace")
190+
clean_text = ANSI_ESCAPE_PATTERN.sub('', text)
191+
self.append_text(clean_text)
192+
except Exception as error:
193+
self.append_text(f"{self.word_dict.get('ssh_command_widget_error_message_decode_failed')}"
194+
f" {error}\n")
195+
196+
def _on_closed(self, msg: str):
197+
self.append_text(f"\n{self.word_dict.get('ssh_command_widget_log_message_channel_closed')}"
198+
f" {msg}\n")
199+
self.login_widget.status_label.setText(self.word_dict.get(
200+
'ssh_command_widget_status_label_disconnected'
201+
))
202+
203+
def send_command(self):
204+
cmd = self.command_input_edit.text()
205+
if not cmd:
206+
return
207+
if self.shell_channel and not self.shell_channel.closed:
208+
try:
209+
self.shell_channel.send(cmd + "\n")
210+
self.command_input_edit.clear()
211+
except Exception as e:
212+
self.append_text(f"{self.word_dict.get('ssh_command_widget_error_message_send_failed')} {e}\n")
213+
else:
214+
QMessageBox.information(
215+
self,
216+
self.word_dict.get('ssh_command_widget_dialog_title_not_connected'),
217+
self.word_dict.get('ssh_command_widget_dialog_message_not_connected_shell'))
218+
219+
def disconnect_ssh(self):
220+
self.append_text(f"{self.word_dict.get('ssh_command_widget_log_message_disconnect_in_progress')} \n")
221+
self._cleanup()
222+
self.login_widget.status_label.setText(
223+
self.word_dict.get('ssh_command_widget_status_label_disconnected'))
224+
225+
def _cleanup(self):
226+
try:
227+
if self.reader_thread:
228+
self.reader_thread.stop()
229+
self.reader_thread.wait(1000)
230+
except Exception as error:
231+
print(error)
232+
self.reader_thread = None
233+
234+
try:
235+
if self.shell_channel and not self.shell_channel.closed:
236+
self.shell_channel.close()
237+
except Exception as error:
238+
print(error)
239+
self.shell_channel = None
240+
241+
try:
242+
if self.ssh_client:
243+
self.ssh_client.close()
244+
except Exception as error:
245+
print(error)
246+
self.ssh_client = None

0 commit comments

Comments
 (0)