python SSH 终端连接
·
# -*- coding: utf-8 -*-
"""
SSH 终端工具 - 单页面版(exec_command 模式)
整改优化点:
1. 子线程异步执行命令,解决UI卡死未响应
2. 命令互斥锁:同一时间仅允许一条命令运行
3. 前端拦截 cls/clear 本地清屏,解决清屏无效问题
4. 端口数字校验、完善异常捕获,避免程序崩溃
5. ini自动保存上次连接IP/端口/用户名,启动自动回填
6. 新增手动清屏按钮
7. 连接中禁止重复点击连接按钮
8. 解码逻辑容错增强
"""
import sys
import os
import configparser
import paramiko
from PyQt5 import QtCore, QtGui, QtWidgets
class SSHClient:
"""SSH 客户端封装:建立连接 + 执行命令 + 断开连接"""
def __init__(self):
self.ssh = None # paramiko.SSHClient 实例
self.connected = False # 连接状态标志
def connect(self, host, port, username, password, timeout=10):
"""建立 SSH 连接"""
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(
hostname=host,
port=port,
username=username,
password=password,
timeout=timeout
)
self.connected = True
return True
def execute(self, cmd, timeout=15):
"""执行单条命令,返回 (stdout_bytes, stderr_bytes)"""
if not self.connected or not self.ssh:
return None, "未连接"
stdin, stdout, stderr = self.ssh.exec_command(cmd, timeout=timeout)
out = stdout.read()
err = stderr.read()
return out, err
def disconnect(self):
"""断开 SSH 连接"""
if self.ssh:
try:
self.ssh.close()
except Exception:
pass
self.connected = False
def _decode(self, data):
"""字节数据解码:优先尝试 gbk/gb2312,失败用 utf-8 兜底"""
if not data:
return ""
for enc in ['gbk', 'gb2312', 'utf-8']:
try:
return data.decode(enc)
except Exception:
continue
return data.decode('utf-8', errors='replace')
# 命令异步执行线程
class CmdRunThread(QtCore.QThread):
result_signal = QtCore.pyqtSignal(str, str, str) # cmd, stdout, stderr
error_signal = QtCore.pyqtSignal(str)
def __init__(self, ssh_client, cmd):
super().__init__()
self.ssh_client = ssh_client
self.cmd = cmd
def run(self):
try:
out_bytes, err_bytes = self.ssh_client.execute(self.cmd)
out = self.ssh_client._decode(out_bytes)
err = self.ssh_client._decode(err_bytes)
self.result_signal.emit(self.cmd, out, err)
except Exception as e:
self.error_signal.emit(str(e))
class SSHTerminalWidget(QtWidgets.QWidget):
"""SSH 终端主窗口:单页面集成登录 + 终端 + 命令输入"""
def __init__(self):
super().__init__()
self.setWindowTitle("SSH 终端")
self.resize(950, 700)
self.client = SSHClient() # SSH 客户端实例
self.is_cmd_running = False # 命令忙锁:防止并发命令
self.is_connecting = False # 连接中锁:防止重复点连接
self.config_path = "ssh_terminal_config.ini"
self.cfg = configparser.ConfigParser()
# 深色主题样式
self.setStyleSheet("""
QWidget {
background-color: #0F111A;
color: #00E5FF;
font-family: 'Consolas', 'Courier New', monospace;
}
QTextEdit {
background-color: #161925;
color: #ADB5BD;
border: 1px solid #232733;
border-radius: 4px;
padding: 8px;
font-size: 12px;
}
QLineEdit {
background-color: #1A1C2C;
border: 1px solid #3E445E;
color: white;
border-radius: 4px;
padding: 6px;
font-size: 12px;
}
QPushButton {
background-color: #3D5AFE;
color: white;
border-radius: 4px;
padding: 6px 16px;
font-weight: bold;
}
QPushButton:hover { background-color: #536DFE; }
QPushButton:disabled { background-color: #555; color: #888; }
QLabel { color: #00E5FF; font-weight: bold; }
""")
self._setup_ui()
self.load_last_connect_config()
def _setup_ui(self):
"""构建界面:登录栏 + 终端输出区 + 命令输入区"""
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(15, 15, 15, 15)
layout.setSpacing(10)
# ========== 登录区域(顶部)==========
login_frame = QtWidgets.QFrame()
login_frame.setStyleSheet("""
QFrame {
background-color: rgba(30, 34, 48, 0.6);
border: 1px solid rgba(0, 229, 255, 0.2);
border-radius: 8px;
}
""")
login_layout = QtWidgets.QGridLayout(login_frame)
login_layout.setHorizontalSpacing(10)
login_layout.setVerticalSpacing(8)
login_layout.setContentsMargins(15, 12, 15, 12)
# 主机地址
login_layout.addWidget(QtWidgets.QLabel("主机地址:"), 0, 0)
self.host_input = QtWidgets.QLineEdit()
self.host_input.setMaximumWidth(180)
login_layout.addWidget(self.host_input, 0, 1)
# 端口
login_layout.addWidget(QtWidgets.QLabel("端口:"), 0, 2)
self.port_input = QtWidgets.QLineEdit("22")
self.port_input.setMaximumWidth(60)
login_layout.addWidget(self.port_input, 0, 3)
# 用户名
login_layout.addWidget(QtWidgets.QLabel("用户名:"), 0, 4)
self.user_input = QtWidgets.QLineEdit()
self.user_input.setMaximumWidth(150)
login_layout.addWidget(self.user_input, 0, 5)
# 密码
login_layout.addWidget(QtWidgets.QLabel("密码:"), 0, 6)
self.pwd_input = QtWidgets.QLineEdit()
self.pwd_input.setEchoMode(QtWidgets.QLineEdit.Password) # 密码掩码
self.pwd_input.setMaximumWidth(150)
login_layout.addWidget(self.pwd_input, 0, 7)
# 连接按钮
self.btn_connect = QtWidgets.QPushButton("连接")
self.btn_connect.setMinimumWidth(80)
self.btn_connect.clicked.connect(self._do_connect)
login_layout.addWidget(self.btn_connect, 0, 8)
# 断开按钮
self.btn_disconnect = QtWidgets.QPushButton("断开")
self.btn_disconnect.setMinimumWidth(80)
self.btn_disconnect.clicked.connect(self._disconnect)
self.btn_disconnect.setEnabled(False)
login_layout.addWidget(self.btn_disconnect, 0, 9)
# 清屏按钮
self.btn_clear = QtWidgets.QPushButton("清屏")
self.btn_clear.setMinimumWidth(70)
self.btn_clear.clicked.connect(lambda: self.output_display.clear())
login_layout.addWidget(self.btn_clear, 0, 10)
# 状态标签
self.status_label = QtWidgets.QLabel("未连接")
self.status_label.setStyleSheet("color: #FF5252;")
login_layout.addWidget(self.status_label, 0, 11)
login_layout.setColumnStretch(12, 1) # 右侧伸缩占位
layout.addWidget(login_frame)
# ========== 终端输出区域(中间,自适应高度)==========
self.output_display = QtWidgets.QTextEdit()
self.output_display.setReadOnly(True)
self.output_display.setLineWrapMode(QtWidgets.QTextEdit.NoWrap) # 不换行
font = QtGui.QFont("Consolas", 10)
font.setStyleHint(QtGui.QFont.Monospace)
self.output_display.setFont(font)
layout.addWidget(self.output_display, 1) # stretch=1 占满剩余空间
# ========== 命令输入区域(底部)==========
bottom_layout = QtWidgets.QHBoxLayout()
self.cmd_label = QtWidgets.QLabel("命令:")
self.cmd_input = QtWidgets.QLineEdit()
self.cmd_input.setPlaceholderText("输入命令后按 Enter 发送...")
self.cmd_input.returnPressed.connect(self._send_command) # Enter 发送
self.cmd_input.setEnabled(False) # 未连接时禁用
self.btn_send = QtWidgets.QPushButton("发送")
self.btn_send.clicked.connect(self._send_command)
self.btn_send.setEnabled(False)
bottom_layout.addWidget(self.cmd_label)
bottom_layout.addWidget(self.cmd_input, 1)
bottom_layout.addWidget(self.btn_send)
layout.addLayout(bottom_layout)
def load_last_connect_config(self):
"""读取上次连接配置,自动回填IP/端口/用户名,不保存密码"""
if not os.path.exists(self.config_path):
return
try:
self.cfg.read(self.config_path, encoding="utf-8")
if "LastConnect" in self.cfg.sections():
self.host_input.setText(self.cfg.get("LastConnect", "host", fallback=""))
self.port_input.setText(self.cfg.get("LastConnect", "port", fallback="22"))
self.user_input.setText(self.cfg.get("LastConnect", "user", fallback=""))
except Exception as e:
print(f"读取配置失败: {e}")
def save_connect_config(self, host, port, user):
"""连接成功保存配置"""
try:
if "LastConnect" not in self.cfg.sections():
self.cfg.add_section("LastConnect")
self.cfg.set("LastConnect", "host", host)
self.cfg.set("LastConnect", "port", str(port))
self.cfg.set("LastConnect", "user", user)
with open(self.config_path, "w", encoding="utf-8") as f:
self.cfg.write(f)
except Exception as e:
print(f"保存配置失败: {e}")
def _do_connect(self):
"""点击连接按钮:验证输入 -> 建立 SSH 连接 -> 更新界面状态"""
# 正在连接中,禁止重复点击
if self.is_connecting:
self._append_output("[提示] 正在建立连接,请稍等...\n")
return
host = self.host_input.text().strip()
port_str = self.port_input.text().strip()
username = self.user_input.text().strip()
password = self.pwd_input.text()
# 输入非空校验
if not host or not username or not password:
self.status_label.setText("请填写完整信息")
self.status_label.setStyleSheet("color: #FF5252;")
return
# 端口数字校验
try:
port = int(port_str)
if not (1 <= port <= 65535):
self.status_label.setText("端口范围1~65535")
self.status_label.setStyleSheet("color: #FF5252;")
return
except ValueError:
self.status_label.setText("端口必须为数字")
self.status_label.setStyleSheet("color: #FF5252;")
return
# 禁用登录栏,防止重复点击
self.is_connecting = True
self._set_login_enabled(False)
self.output_display.clear()
self._append_output(f"正在连接 {host}:{port} ...\n")
try:
self.client.connect(host, port, username, password)
# 连接成功,保存配置
self.save_connect_config(host, port, username)
self.status_label.setText("已连接 | 连接成功")
self.status_label.setStyleSheet("color: #69F0AE;")
self.btn_connect.setEnabled(False)
self.btn_disconnect.setEnabled(True)
self.cmd_input.setEnabled(True)
self.btn_send.setEnabled(True)
self.cmd_input.setFocus()
self._append_output("连接成功!\n\n")
except Exception as e:
# 连接失败,恢复登录栏
self.status_label.setText(f"连接失败: {str(e)}")
self.status_label.setStyleSheet("color: #FF5252;")
self._set_login_enabled(True)
self._append_output(f"\n[连接失败] {str(e)}\n")
finally:
self.is_connecting = False
def _send_command(self):
"""点击发送或按 Enter:执行命令并显示结果"""
cmd = self.cmd_input.text().strip()
if not cmd:
return
# 命令忙锁判断
if self.is_cmd_running:
self._append_output("[提示] 上一条命令尚未执行完成,请等待...\n")
return
# 本地拦截清屏命令,不发给SSH
if cmd.lower() in ("cls", "clear"):
self.output_display.clear()
self.cmd_input.clear()
return
# 在终端显示 "> 命令" 提示
self._append_output(f"> {cmd}\n")
self.cmd_input.clear()
self.is_cmd_running = True
# 子线程异步执行,避免UI阻塞
self.cmd_thread = CmdRunThread(self.client, cmd)
self.cmd_thread.result_signal.connect(self._on_cmd_finish)
self.cmd_thread.error_signal.connect(self._on_cmd_error)
self.cmd_thread.finished.connect(lambda: setattr(self, "is_cmd_running", False))
self.cmd_thread.start()
def _on_cmd_finish(self, cmd, out, err):
"""命令正常执行完成回调"""
if out:
self._append_output(out)
if err:
self._append_output(f"[错误] {err}\n")
if not out and not err:
self._append_output("(命令执行完成,无输出)\n")
def _on_cmd_error(self, err_msg):
"""命令异常回调"""
self._append_output(f"[执行错误] {err_msg}\n")
def _append_output(self, text):
"""追加文本到终端末尾,并强制滚动到底部(解决顶部留白问题)"""
self.output_display.moveCursor(QtGui.QTextCursor.End)
self.output_display.insertPlainText(text)
scrollbar = self.output_display.verticalScrollBar()
scrollbar.setValue(scrollbar.maximum())
def _set_login_enabled(self, enabled):
"""设置登录区域控件启用/禁用状态"""
self.host_input.setEnabled(enabled)
self.port_input.setEnabled(enabled)
self.user_input.setEnabled(enabled)
self.pwd_input.setEnabled(enabled)
self.btn_connect.setEnabled(enabled)
def _disconnect(self):
"""点击断开按钮:断开连接并恢复界面"""
self.client.disconnect()
self.is_cmd_running = False
self.status_label.setText("未连接")
self.status_label.setStyleSheet("color: #FF5252;")
self._set_login_enabled(True)
self.btn_disconnect.setEnabled(False)
self.cmd_input.setEnabled(False)
self.btn_send.setEnabled(False)
self._append_output("\n[已断开连接]\n")
def closeEvent(self, event):
"""窗口关闭时自动断开 SSH 连接"""
self.client.disconnect()
event.accept()
if __name__ == "__main__":
# 屏蔽Qt图片png警告
os.environ["QT_LOGGING_RULES"] = "qt.image.png=false"
app = QtWidgets.QApplication(sys.argv)
font = QtGui.QFont("Microsoft YaHei", 9)
app.setFont(font)
window = SSHTerminalWidget()
window.show()
sys.exit(app.exec_())
更多推荐


所有评论(0)