python运维实战-ssh工具

1. 功能描述

1.1 系统具有的功能描述

(1)连接服务器:用户可以通过系统连接到远程服务器,系统支持多个服务器配置,并且可以方便地管理这些配置。

(2)执行命令:用户可以在连接成功后,在界面中输入命令,并执行在远程服务器上执行。执行结果会显示在界面中,包括标准输出和标准错误。

(3)上传和下载文件:用户可以通过系统上传本地文件到远程服务器,或者下载远程服务器上的文件到本地。

(4)服务器监控:系统可以监控远程服务器的 CPU 使用率、内存使用率和进程列表,实时显示在界面上。

(5)查看日志文件:用户可以通过系统查看远程服务器上的日志文件,并且可以选择下载到本地进行查看。

(6)Vim编辑文件:用户可以在界面上编辑远程服务器上的文件,编辑后可以保存到服务器上。

1.2 所用技术与模块描述

(1)JSON模块(json):用于存储和加载配置信息、与服务器交换数据等。

操作系统路径模块(os.path):主要用于处理文件路径,比如检查文件是否存在、获取文件或目录的详细信息等。

(2)正则表达式模块(re):Python的正则表达式模块,用于字符串匹配和替换。

(3)套接字模块(socket):提供了网络通信的底层接口,允许应用程序在网络上发送和接收数据。

(4)子进程模块(subprocess):允许从Python脚本中启动新的应用程序,连接到它们的输入/输出/错误管道,并获取它们的返回码。

 

(5)线程模块(threading):提供了对线程的基本支持,线程是轻量级的进程,允许在Python程序中并发执行代码。

时间模块(time):提供了与时间相关的各种功能,如获取当前时间、执行时间延迟等。

(6)Tkinter简单对话框模块(tkinter.simpledialog):Tkinter GUI库的一部分,提供了简单的对话框功能,如输入对话框、消息框等。

(7)Paramiko模块(paramiko):一个实现SSHv2协议的Python库,支持连接和操作SSH服务器。

(8)Watchdog事件模块(watchdog.events):watchdog库的一部分,提供了文件系统事件(如文件创建、修改、删除等)的接口。

(9)Watchdog观察者模块(watchdog.observers):watchdog库的一部分,提供了跨平台的文件系统事件观察者实现。

1.3 ssh暗影坤手概述

一个简单易用的SSH管理工具,允许用户通过图形化界面实现对虚拟机(VM)的基本操作。通过集成SSH协议,该工具能够提供远程登录、命令执行、文件传输等核心功能,为用户提供便捷、安全的虚拟机管理体验。

 

2. 系统功能实现

2.1 服务器信息管理的功能设计与实现

当前目录下创建一个server.json的文件

将所有服务器信息保存在json文件中,调用时借助函数加载文件,读取数据。

代码展示如下:

def load_server(self):with open('server.json', "r", encoding='utf-8') as fp:self.servers = json.load(fp)print(f"获取到的服务器信息:{self.servers}")def save_server(self):with open('server.json', "w", encoding='utf-8') as fp:fp.write(json.dumps(self.servers))

 

2.2 连接服务器的功能设计与实现

(1)读取json文件中的数据,将ip、端口、用户名等信息传到connect中连接服务器。

代码展示如下:

def connecting(self):self.connected = Noneself.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy)try:# 连接到服务器self.ssh.connect(hostname=self.server["ip"], port=self.server["port"], username=self.server["username"],password=self.server["password"])print(f"【success】连接到服务器【{self.server['ip']}】成功!!!")self.connected = Trueself.get_pwd()return Trueexcept Exception as e:print(f"【errors】连接到服务器【{self.server['ip']}】失败!!!")self.connected = Falsereturn False

(2)创建连接指定服务器函数,并且创建线程拥堵单独执行连接操作,不影响其他操作的执行,创建显示函数将连接成功或失败信息展示到界面,以便用户清楚服务器是否已经连接成功。

代码展示如下:

# 连接到指定的服务器
def connect(self, server):print("连接到指定的服务器")self.mssh.server = serverprint(f"服务器信息:{self.mssh.server}")socket.setdefaulttimeout(5)# 在单独的线程中执行连接操作thread_connect = threading.Thread(target=self.connect_thread, args=(server,))thread_connect.start()self.pop_win.destroy()def connect_thread(self, server):result = self.mssh.connecting()if result:self.is_connected = Trueself.update_connection_status(server, True)else:self.is_connected = Falseself.update_connection_status(server, False)
#服务器连接展示
def update_connection_status(self, server, connected):if connected:self.label_server.config(text=f"服务器{server['ip']}连接成功", foreground="green")else:self.label_server.config(text=f"服务器{server['ip']}连接失败", foreground="red")

2.3 添加服务器的功能设计与实现

(1)首先创建函数用于添加服务器的操作,然后输入服务器的IP地址,账户名和密码添加服务器,最后将服务器信息保存到json文件中。

代码展示如下:

def add_server(self, ip, port, username, password, description):self.servers.append({"description": description,"ip": ip,"port": port,"username": username,"password": password})self.save_server()

(2)创建新建函数,用于与用户交互的图形化界面,让用户输入服务器的IP地址,账户名和密码添加服务器信息。

代码展示如下:

# 新建连接
def new_connect_ui(self):self.pop_win = tk.Toplevel(self.root)# 设置标题和大小self.pop_win.title("新建连接")self.pop_win.geometry("300x200+600+250")frame = tk.Frame(self.pop_win)frame.grid()# IPip_label = tk.Label(frame, text="服务器IP地址:")ip_label.grid(row=0, column=0)self.ip_entry = tk.Entry(frame)self.ip_entry.grid(row=0, column=1)# Portport_label = tk.Label(frame, text="服务器端口号:")port_label.grid(row=1, column=0)self.port_entry = tk.Entry(frame)self.port_entry.grid(row=1, column=1)# Usernameusername_label = tk.Label(frame, text="用户名:")username_label.grid(row=2, column=0)self.username_entry = tk.Entry(frame)self.username_entry.grid(row=2, column=1)# Passwordpassword_label = tk.Label(frame, text="密码:")password_label.grid(row=3, column=0)self.password_entry = tk.Entry(frame, show="*")self.password_entry.grid(row=3, column=1)# Descriptiondescription_label = tk.Label(frame, text="备注信息:")description_label.grid(row=4, column=0)self.description_entry = tk.Entry(frame)self.description_entry.grid(row=4, column=1)# Buttonsubmit_button = tk.Button(frame, text="保存", command=self.save_server_info)submit_button.grid(row=5, columnspan=2)

(3)获取用户所输入的信息,将服务器信息保存到文件。

代码展示如下:

def save_server_info(self):ip = self.ip_entry.get()port = self.port_entry.get()username = self.username_entry.get()password = self.password_entry.get()description = self.description_entry.get()# 调用 add_server 函数,将获取到的信息添加到服务器列表中ssh.add_server(ip=ip, port=int(port), username=username, password=password, description=description)messagebox.showinfo("成功","服务器信息已保存!!")# print("服务器信息已保存")

 

2.4 删除服务器的功能设计与实现

创建删除函数,当用户点击确定时,调用remove移除服务器信息,并且返回删除成功提示信息。

代码展示如下:

def del_connect(self, server):print("删除指定的服务器")# 输出服务器信息print(f"服务器信息:{server}")# 确认是否删除服务器confirm = messagebox.askyesno("确认删除", f"确认删除服务器 {server['ip']} 吗?")if confirm:# 从服务器列表中删除指定的服务器if server in self.mssh.servers:self.mssh.servers.remove(server)self.pop_win.destroy()self.open_connect_ui()messagebox.showinfo("删除成功", f"服务器 {server['ip']} 已成功删除!")else:messagebox.showerror("错误", "服务器不存在,无法删除")# 保存更新后的服务器列表到配置文件中self.mssh.save_server()

 

2.5 修改服务器的功能设计与实现

(1)创建修改函数,用于与用户交互的图形化界面,让用户修改服务器的IP地址、账户名和密码等服务器信息。

代码展示如下:

def modify_server(self, server):print("修改服务器信息")self.mssh.server = server.copy()  # 添加这一行来复制服务器信息self.pop_win_modify = tk.Toplevel(self.root)frame = tk.Frame(self.pop_win_modify)frame.grid()# IPip_label = tk.Label(frame, text="服务器IP地址:")ip_label.grid(row=0, column=0)self.ip_entry = tk.Entry(frame, textvariable=tk.StringVar(value=server['ip']))self.ip_entry.grid(row=0, column=1)# Portport_label = tk.Label(frame, text="服务器端口号:")port_label.grid(row=1, column=0)self.port_entry = tk.Entry(frame, textvariable=tk.StringVar(value=str(server['port'])))self.port_entry.grid(row=1, column=1)# Usernameusername_label = tk.Label(frame, text="用户名:")username_label.grid(row=2, column=0)self.username_entry = tk.Entry(frame, textvariable=tk.StringVar(value=server['username']))self.username_entry.grid(row=2, column=1)# Passwordpassword_label = tk.Label(frame, text="密码:")password_label.grid(row=3, column=0)self.password_entry = tk.Entry(frame, show="*", textvariable=tk.StringVar(value=server['password']))self.password_entry.grid(row=3, column=1)# Descriptiondescription_label = tk.Label(frame, text="备注信息:")description_label.grid(row=4, column=0)self.description_entry = tk.Entry(frame, textvariable=tk.StringVar(value=server['description']))self.description_entry.grid(row=4, column=1)# Buttonsubmit_button = tk.Button(frame, text="保存", command=self.save_modified_server)submit_button.grid(row=5, columnspan=2)

(2)获取修改后的服务器信息,保存到json文件中。

代码展示如下:

def save_modified_server(self):modified_server = {"ip": self.ip_entry.get(),"port": int(self.port_entry.get()),"username": self.username_entry.get(),"password": self.mssh.server["password"],  # 假设密码不变,保持原值"description": self.description_entry.get(),}index = self.mssh.servers.index(self.mssh.server)self.mssh.servers[index] = modified_servermessagebox.showinfo("成功", "服务器信息已成功修改!")self.pop_win_modify.destroy()self.open_connect_ui()self.mssh.save_server()

 

2.6 执行交互式命令的功能设计与实现

主要调用paramiko库的exec_command方法执行命令,同时获取标准输入、标准输出和标准错误。

(1)基本命令cd的实现

创建exec方法主要用于执行远程命令,并处理用户输入的路径,确保命令相对于当前路径执行。方法接收用户输入的命令,执行命令(若是"cd"命令则更新当前路径),获取命令执行结果,并返回执行结果。

代码展示如下:

def exec(self, command):if command.startswith("cd"):tmp_path = command.split(" ")[-1]if tmp_path.startswith("/"):# 绝对路径处理self.path = tmp_pathelse:# todo 如果用户输入的是一个相对路径呢?print("用户输入的是相对路径")if tmp_path.startswith(".."):self.path = self.path + "/" + tmp_pathelse:self.path = self.path + tmp_pathelse:# 根据当前的路径去执行指令command = f"cd {self.path} && {command}"print(f"->输入指令({self.path}):{command}")stdin, stdout, stderr = self.ssh.exec_command(command)result = stdout.read().decode("utf-8")print(f"<-返回:{result}")# 用户输入的路径里包含.. ,通过pwd刷新当前路径if command.find("..") > 0:self.get_pwd()return result

 

(2)创建函数用于获取当前路径

代码展示如下:

# 使用pwd更新当前路径
def get_pwd(self):stdin, stdout, stderr = self.ssh.exec_command(f"cd {self.path} && pwd")result = stdout.read().decode("utf-8")self.path = result.strip()

 

  1. 交互式命令实现

通过SSH与远程服务器进行交互,用户可以输入命令并发送到远程服务器,同时应用会读取并显示来自远程服务器的输出。所有的输入/输出处理都在单独的线程中进行,以保持GUI的响应性。

代码展示如下:

def _read_channel(self, channel,output_callback):"""在独立线程中读取通道输出,并调用回调函数处理"""while True:if channel.recv_ready():data = channel.recv(1024)output_callback(data)def handle_output(self, data):# 先解码数据decoded_data = data.decode("utf-8")# 去除颜色转义序列cleaned_data = re.sub(r'\x1B\[([0-?]*[ -/]*[@-~])', '', decoded_data)# 插入到文本框的末尾self.txt_result.insert(tk.END, cleaned_data)# 确保滚动条跟随到最后self.txt_result.see(tk.END)def okk(self,event):self.command.set('q')self.ok(self)def ok(self, event):command = self.command.get()if not self.mssh.connected:self.txt_result.insert(tk.END, "未连接到服务器!\n")return# 如果ssh_channel尚未创建,创建一个新的if self.mssh_channel is None:self.mssh_channel = self.mssh.ssh.invoke_shell()self.mssh_channel.set_combine_stderr(True)try:# 使用ssh_channel发送命令self.mssh_channel.send(f"{command}\n")# 创建线程监听输出并调用UI提供的回调函数t = threading.Thread(target=self.mssh._read_channel, args=(self.mssh_channel, self.handle_output))t.daemon = Truet.start()# 设置延时清空输入框self.root.after(2000, lambda: self.command.set(""))except Exception as e:self.txt_result.insert(tk.END, f"执行错误: {str(e)}\n")# 清空输入框self.command.set("")

2.7 vim编辑器功能设计与实现

利用watchdog模块监控文件系统的更改,如文件的创建、修改、移动或删除等,并在这些事件发生时执行自定义操作。

(1)定义watch_and_upload方法用于监视本地文件的修改并在远程服务器上进行相应的上传操作。

代码展示如下:

def watch_and_upload(self, local_file_path, remote_full_path):class FileModifiedHandler(FileSystemEventHandler):def __init__(self, ssh_handler, remote_full_path):self.ssh_handler = ssh_handlerself.remote_full_path = remote_full_pathdef on_modified(self, event):if event.is_directory or not event.src_path == local_file_path:return# messagebox.showinfo("文件被修改", f"文件被修改:{event.src_path}")try:self.ssh_handler.connecting()  # 重新连接SSH(如果需要)sftp = self.ssh_handler.ssh.open_sftp()sftp.put(local_file_path, self.remote_full_path)sftp.close()# messagebox.showinfo("文件上传成功", f"vim修改成功: {local_file_path} -> {self.remote_full_path}")except Exception as e:messagebox.showerror("文件上传失败", f"文件上传失败: {e}")event_handler = FileModifiedHandler(self, remote_full_path)observer = Observer()observer.schedule(event_handler, path=os.path.dirname(local_file_path), recursive=False)observer.start()try:# 等待用户操作或其他信号来停止监视# 这里使用time.sleep作为示例,但建议使用更好的机制,如事件或信号while True:time.sleep(1)except KeyboardInterrupt:observer.stop()observer.join()

(2)创建vim_edit 函数用于在vscode中编辑文件。首先提示用户输入要编辑的文件名,并获取远程文件的路径,用户选择本地目录,并构造本地文件的完整路径,连接到远程服务器,并创建 SFTP 客户端,检查远程文件是否存在,如果不存在则在远程服务器上创建空文件。下载文件到本地,并启动文件监视线程 watch_and_upload,然后调用子进程打开文件编辑器。

 

def vim_edit(self):remote_file = simpledialog.askstring("vim", "请输入要编辑的完整远程文件路径:")if remote_file is None:  # 用户点击“取消”或关闭对话框returnelif not remote_file:  # 用户输入了空字符串messagebox.showerror("错误", "请输入文件路径!")return# 检查用户是否输入了以'/'开头的绝对路径if not remote_file.startswith('/'):messagebox.showerror("错误", "请输入绝对路径,例如:/home/user/file.txt")return# 临时文件目录tmp = filedialog.askdirectory(title="选择下载到的本地目录")if not tmp:  # 用户取消了选择目录returnlocal_file_path = os.path.join(tmp, os.path.basename(remote_file))try:self.connecting()sftp = self.ssh.open_sftp()# 检查远程文件是否存在try:sftp.stat(remote_file)except FileNotFoundError:# 如果不存在,则在远程服务器上创建空文件with sftp.open(remote_file, 'wb') as f:pass  # 写入空内容或者模板内容# 下载文件到本地sftp.get(remote_file, local_file_path)sftp.close()# 启动文件监视线程watch_thread = threading.Thread(target=self.watch_and_upload, args=(local_file_path, remote_file))watch_thread.start()# 打开文件编辑器subprocess.run(['Code', local_file_path], shell=True)except Exception as e:messagebox.showerror("文件下载失败", f"文件下载失败:{e}")print(f"文件下载失败:{e}")

2.8 文件远程上传下载的功能设计与实现

(1)文件上传功能实现

首先检查是否已连接到服务器。如果未连接到服务器,则显示错误消息并返回。如果已连接到服务器,则调用filedialog.askopenfilename()方法请求用户选择要上传的文件。接下来,如果用户选择了文件,则提取文件名并创建一个SFTP客户端。然后,计算远程路径,并在控制台打印文件和远程路径信息。最后,使用SFTP客户端将本地文件上传到远程路径,并显示上传成功的消息框。如果用户未选择文件,则输出“请选择要上传的文件”。如果出现任何异常,则显示文件上传失败的错误消息。

代码展示如下:

# 上传文件
def upload_file(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returnelse:self.file_path = filedialog.askopenfilename()try:if self.file_path:# 获取文件名file_name = self.file_path.split("/")[-1]# 创建 SFTP 客户端sftp_client = self.mssh.ssh.open_sftp()remote_path = f"{self.mssh.path}/{file_name}"print(f"文件:{self.file_path},上传到:{remote_path}")# 上传文件sftp_client.put(self.file_path, remote_path)messagebox.showinfo("成功", "上传成功")sftp_client.close()else:print("请选择要上传的文件")except Exception as e:messagebox.showerror("错误", f"文件上传失败: {e}")#判断是否连接服务器def vim_edit(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returnself.mssh.vim_edit()

 

 

(2)文件下载功能实现

使用tk中的simpledialog.askstring 获取用户输入的要下载文件名,filedialog.askdirectory获取用户所选目录的完整路径,创建一个SFTP连接,并调用get方法来下载文件,调用完关闭SFTP连接,弹窗提示用户下载成功。

代码展示如下:

 

def download_file(self):#tk.simpledialog.askstring 的主要用途是获取用户输入的文本信息remote_file = tk.simpledialog.askstring("输入文件名", "请输入要下载的文件名:")if remote_file is None:  # 用户点击“取消”或关闭对话框returnelif not remote_file:  # 用户输入了空字符串messagebox.showerror("错误", "请输入文件名!")return#filedialog.askdirectory返回的是用户所选目录的完整路径,而不是文件名local_path = filedialog.askdirectory(title="选择下载到的本地目录")if local_path:try:sftp = self.ssh.open_sftp()local_file_path = os.path.join(local_path, remote_file)sftp.get(remote_file, local_file_path)sftp.close()messagebox.showinfo("下载成功", f"文件下载成功: {remote_file} -> {local_path}")except Exception as e:messagebox.showerror("下载失败", f"文件下载失败: {e}")#下载文件
def downloader(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returnself.mssh.download_file()

 

2.9 服务器监控功能设计与实现

通过SSH连接到远程服务器,并在界面上展示服务器的进程信息、系统资源利用情况和磁盘使用情况,同时处理连接状态的变化。

(1)进程展示的实现

创建get_process方法通过执行"ps -ef"命令获取进程信息并展示在界面上的表格中。如果连接失败或执行命令出错,会通过弹出窗口显示错误信息。 代码展示如下:

def get_process(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returnself.pop_win = tk.Toplevel(self.root)self.pop_win.title("进程")self.pop_win.geometry("800x600+600+250")table_frame = ttk.Frame(self.pop_win)table_frame.pack(padx=10, pady=10, fill="both", expand=True)server = self.mssh.serverip = server['ip']username = server['username']password = server['password']try:client = paramiko.SSHClient()client.set_missing_host_key_policy(paramiko.AutoAddPolicy())client.connect(ip, 22, username, password)stdin, stdout, stderr = client.exec_command("ps -ef")result = stdout.read().decode("utf-8").strip().split("\n")# 创建表格table = ttk.Treeview(table_frame, columns=("user", "pid", "cpu", "mem", "vsz", "rss", "tty", "stat", "start", "time", "command"), show="headings")table.heading("user", text="用户")table.heading("pid", text="进程ID")table.heading("cpu", text="CPU%")table.heading("mem", text="内存%")table.heading("vsz", text="虚拟内存")table.heading("rss", text="常驻内存")table.heading("tty", text="终端")table.heading("stat", text="状态")table.heading("start", text="启动时间")table.heading("time", text="CPU时间")table.heading("command", text="命令")table.pack(side="left", fill="both", expand=True)# 添加滚动条scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=table.yview)scrollbar.pack(side="right", fill="y")table.configure(yscrollcommand=scrollbar.set)# 填充表格数据for line in result[1:]:parts = line.split()if len(parts) >= 10:user, pid, cpu, mem, vsz, rss, tty, stat, start, time, *command = partscommand = " ".join(command)table.insert("", "end", values=(user, pid, cpu, mem, vsz, rss, tty, stat, start, time, command))# 调整列宽度table.column("#0", width=0, stretch="no")  # 隐藏第一列table.column("user", width=80, anchor="w")table.column("pid", width=60, anchor="e")table.column("cpu", width=60, anchor="e")table.column("mem", width=60, anchor="e")table.column("vsz", width=100, anchor="e")table.column("rss", width=100, anchor="e")table.column("tty", width=60, anchor="w")table.column("stat", width=60, anchor="w")table.column("start", width=120, anchor="w")table.column("time", width=80, anchor="e")table.column("command", width=300, anchor="w")# 自动调整行高def fixed_map(option):return [elm for elm in style.map("Treeview", query_opt=option) if elm[:2] != ("!disabled", "!selected")]style = ttk.Style()style.map("Treeview", foreground=fixed_map("foreground"), background=fixed_map("background"))except paramiko.AuthenticationException:messagebox.showerror("错误", "认证失败,请验证您的凭据")except paramiko.SSHException as sshException:messagebox.showerror("错误", f"无法建立SSH连接: {sshException}")except paramiko.BadHostKeyException as badHostKeyException:messagebox.showerror("错误", f"无法验证服务器的主机密钥: {badHostKeyException}")except Exception as e:messagebox.showerror("错误", f"发生错误: {e}")finally:if 'client' in locals() and client:client.close()  # 确保连接被关闭# 当窗口关闭时,确保没有遗留的引用或连接self.pop_win.protocol("WM_DELETE_WINDOW", self.on_closing)

(2)CPU、内存、网络等信息展示的实现

创建create_widgets方法制作界面上的标签、进度条和按钮。用于展示CPU、内存、网络等信息,并提供查看磁盘使用情况的按钮。使用update_info方法用于在界面上更新系统信息,包括CPU利用率、内存使用情况和网络速率。reset_system_monitoring方法重置系统监控状态,将CPU信息、内存信息和网络信息重置为初始状态。show_disk_usage展示磁盘使用情况,并创建一个新窗口展示磁盘信息。

代码展示如下:

def create_widgets(self, frame):self.cpu_label = ttk.Label(frame, text="CPU信息:")self.cpu_label.grid()self.cpu_progress = ttk.Progressbar(frame, orient='horizontal', length=200, mode='determinate')self.cpu_progress.grid(pady=(0, 10))self.mem_label = ttk.Label(frame, text="Memory Usage: 0 MB / 0 MB")self.mem_label.grid()self.mem_progress = ttk.Progressbar(frame, orient='horizontal', length=200, mode='determinate')self.mem_progress.grid(pady=(0, 5))self.net_label = ttk.Label(frame, text="网络信息:")self.net_label.grid(pady=5)self.disk_button = tk.Button(frame, text="查看磁盘使用情况", command=self.show_disk_usage)self.disk_button.grid(pady=(5, 5))def update_info(self, frame):self.create_widgets(frame)  # 创建标签# 定义进度条样式style = ttk.Style()style.theme_use("default")style.configure("green.Horizontal.TProgressbar", foreground='green', background='green')style.configure("yellow.Horizontal.TProgressbar", foreground='yellow', background='yellow')style.configure("red.Horizontal.TProgressbar", foreground='red', background='red')prev_connected = self.mssh.connected  # 记录上一次的连接状态while True:if "ip" in self.mssh.server:if self.mssh.connected == None:result_connect = (f"服务器{self.mssh.server['ip']}连接中", "red")self.reset_system_monitoring()  # 重置系统监控状态elif self.mssh.connected:result_connect = (f"服务器{self.mssh.server['ip']}连接成功", "green")# -----------------------------系统信息------------------------------------------------server = self.mssh.serverip = server['ip']username = server['username']password = server['password']self.client = paramiko.SSHClient()self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())self.client.connect(ip, 22, username, password)# 执行远程命令获取内存信息stdin, stdout, stderr = self.client.exec_command("free -m")result = stdout.read().decode('utf-8')# 获取系统内存信息lines = result.split('\n')mem_info = lines[1].split()total_mem = int(mem_info[1])used_mem = int(mem_info[2])mem_usage = used_mem / total_mem * 100# 获取网络速率stdin1, stdout1, stderr1 = self.client.exec_command("ip a")result = stdout1.read().decode("utf-8")# 使用正则表达式提取数据包和字节数信息matches = re.findall(r'ens160.*?RX packets (\d+)  bytes (\d+).*?TX packets (\d+)  bytes (\d+)',result, re.DOTALL)if matches:rx_packets, rx_bytes, tx_packets, tx_bytes = matches[0]else:rx_packets, rx_bytes, tx_packets, tx_bytes = 0, 0, 0, 0time.sleep(1)stdin2, stdout2, stderr3 = self.client.exec_command("ip a")result = stdout2.read().decode("utf-8")matches = re.findall(r'ens160.*?RX packets (\d+)  bytes (\d+).*?TX packets (\d+)  bytes (\d+)',result, re.DOTALL)if matches:rx_packets, new_rx_bytes, tx_packets, new_tx_bytes = matches[0]else:rx_packets, new_rx_bytes, tx_packets, new_tx_bytes = 0, 0, 0, 0# 计算速率upload_speed = ((int(new_tx_bytes) - int(tx_bytes)) / 1024)  # MB/s,download_speed = ((int(new_rx_bytes) - int(rx_bytes)) / 1024)# 获取cpu利用率stdin4, stdout4, stderr4 = self.client.exec_command("top -bn 1")result = stdout4.read().decode("utf-8")cpu = 0for line in result.split("\n"):if line.startswith("%Cpu(s)"):cpu_us = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[0])[0]cpu_sy = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[1])[0]cpu_ni = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[2])[0]cpu_id = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[3])[0]cpu_wa = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[4])[0]cpu_hi = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[5])[0]cpu_si = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[6])[0]cpu_st = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[7])[0]sum = float(cpu_st) + float(cpu_si) + float(cpu_hi) + float(cpu_wa) + float(cpu_id) + float(cpu_ni) + float(cpu_sy) + float(cpu_us)cpu = sum - float(cpu_id)# 设置进度条样式if cpu > 80:cpu_style = "red.Horizontal.TProgressbar"elif cpu > 50:cpu_style = "yellow.Horizontal.TProgressbar"else:cpu_style = "green.Horizontal.TProgressbar"if mem_usage > 80:mem_style = "red.Horizontal.TProgressbar"elif mem_usage > 50:mem_style = "yellow.Horizontal.TProgressbar"else:mem_style = "green.Horizontal.TProgressbar"# 更新标签self.cpu_label.config(text=f"CPU信息:{cpu:.2f}%")self.cpu_progress.config(style=cpu_style, value=cpu)self.mem_label.config(text=f"Memory Usage: {used_mem} MB / {total_mem} MB ({mem_usage:.2f}%)")self.mem_progress.config(style=mem_style, value=mem_usage)self.net_label.config(text=f"网络信息:↑{upload_speed:.2f}MB/s \t ↓{download_speed:.2f}MB/s")else:result_connect = (f"服务器{self.mssh.server['ip']}连接失败", "red")self.reset_system_monitoring()  # 重置系统监控状态else:result_connect = (f"服务器未选择", "black")self.reset_system_monitoring()  # 重置系统监控状态# 检查连接状态是否发生变化if self.mssh.connected != prev_connected:self.reset_system_monitoring()  # 重置系统监控状态prev_connected = self.mssh.connectedself.label_server.config(text=f"{result_connect[0]}", foreground=result_connect[1])time.sleep(1)def reset_system_monitoring(self):# 重置系统监控状态self.cpu_label.config(text="CPU信息:")self.cpu_progress.config(style="", value=0)self.mem_label.config(text="Memory Usage: 0 MB / 0 MB")self.mem_progress.config(style="", value=0)self.net_label.config(text="网络信息:")def show_disk_usage(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returntry:self.mssh.connecting()stdin, stdout, stderr = self.mssh.ssh.exec_command("df -h")disk_info = stdout.read().decode("utf-8").strip().split("\n")# 创建新窗口disk_window = tk.Toplevel(self.root)disk_window.title("磁盘使用情况")# 创建表格table_frame = ttk.Frame(disk_window)table_frame.pack(padx=10, pady=10, fill="both", expand=True)# 创建表格标题table = ttk.Treeview(table_frame, columns=("filesystem", "size", "used", "avail", "use%", "mounted_on"),show="headings")table.heading("filesystem", text="文件系统")table.heading("size", text="总大小")table.heading("used", text="已用空间")table.heading("avail", text="可用空间")table.heading("use%", text="已用百分比")table.heading("mounted_on", text="挂载点")table.pack(side="left", fill="both", expand=True)# 添加滚动条scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=table.yview)scrollbar.pack(side="right", fill="y")table.configure(yscrollcommand=scrollbar.set)# 填充表格数据for line in disk_info[1:]:parts = line.split()if len(parts) >= 6:filesystem, size, used, avail, use_percent, mounted_on = partstable.insert("", "end", values=(filesystem, size, used, avail, use_percent, mounted_on))# 调整列宽度table.column("#0", width=0, stretch="no")  # 隐藏第一列table.column("filesystem", width=100, anchor="w")table.column("size", width=80, anchor="e")table.column("used", width=80, anchor="e")table.column("avail", width=80, anchor="e")table.column("use%", width=80, anchor="e")table.column("mounted_on", width=200, anchor="w")# 自动调整行高def fixed_map(option):return [elm for elm in style.map("Treeview", query_opt=option) if elm[:2] != ("!disabled", "!selected")]style = ttk.Style()style.map("Treeview", foreground=fixed_map("foreground"), background=fixed_map("background"))except Exception as e:messagebox.showerror("错误", f"无法获取磁盘使用情况: {e}")

2.10 批量下载日志文件和查看日志功能设计与实现

(1)批量下载日志文件的实现

首先,检查是否已连接到远程服务器,然后设置远程目录为 /var/log。接下来,它通过SSH连接到服务器并打开SFTP客户端。然后,它检查远程目录是否存在,并获取远程目录中的文件列表。接着,它要求用户选择下载到的本地目录,并将选择的本地目录设置为类属性 local_log_dir。然后,它遍历远程目录中的文件,筛选以 .log 结尾的文件,并将这些文件下载到本地目录。最后,它关闭SFTP连接,并显示成功下载的文件数量。 代码展示如下:

 

def download_logs(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")return# 将远程目录设置为 /var/logremote_dir = "/var/log"try:self.mssh.connecting()sftp = self.mssh.ssh.open_sftp()# 检查远程目录是否存在try:sftp.stat(remote_dir)except IOError as e:messagebox.showerror("错误", f"远程目录 {remote_dir} 不存在")returnremote_files = sftp.listdir(remote_dir)local_dir = filedialog.askdirectory(title="选择下载到的本地目录")if local_dir:self.local_log_dir = local_direlse:returndownloaded_count = 0for filename in remote_files:# 检查文件是否以 ".log" 结尾if filename.endswith(".log"):remote_path = f"{remote_dir}/{filename}"local_path = f"{local_dir}/{filename}"sftp.get(remote_path, local_path)downloaded_count += 1print(f"下载成功: {remote_path} -> {local_path}")sftp.close()print(f"共下载了 {downloaded_count} 个文件")if downloaded_count > 0:messagebox.showinfo("成功", f"成功下载了 {downloaded_count} 个日志文件")except Exception as e:print(f"下载失败: {e}")messagebox.showerror("错误", f"下载日志文件失败: {e}")(2)查看日志功能的实现 先检查是否已连接到远程服务器并下载了日志文件。接着,它尝试从 local_log_dir 属性获取之前选择的本地目录,如果该属性不存在,则要求用户选择下载日志文件所在的目录。然后,它要求用户选择要查看的日志文件,并使用VSCode打开选定的日志文件。代码展示如下:def view_log(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接服务器并下载日志文件!")return# 从 download_logs 函数中获取之前选择的本地目录try:local_dir = self.local_log_direxcept AttributeError:local_dir = filedialog.askdirectory(title="选择下载日志文件所在的目录")if not local_dir:returnlog_file = filedialog.askopenfilename(initialdir=local_dir, title="选择要查看的日志文件",filetypes=(("Log Files", "*.log"),))if log_file:try:# 使用 VSCode 打开日志文件subprocess.run(['Code', log_file], shell=True)except Exception as e:messagebox.showerror("错误", f"无法打开文件: {e}")

 

 

2.11界面窗口的实现

创建一个具有菜单、标签、输入框和文本框的用户界面(UI),以及一个后台线程来更新系统信息。同时,通过不同的菜单项和按钮,用户可以执行连接、上传文件、下载文件、查看进程、扩展功能等操作。 代码展示如下:

def __init__(self, ssh):print("初始化")self.is_connected = Falseself.mssh = ssh# 主应用程序窗口self.root = tk.Tk()frame = ttk.Frame(self.root)self.root.geometry("800x550")# 设置窗口标题(可选)self.root.title("ssh 暗影坤手")# 使用 grid 布局self.root.grid_rowconfigure(0, weight=1)self.root.grid_columnconfigure(0, weight=1)# 布局,网格布局# 创建一个框架容器frame = ttk.Frame(self.root)frame.grid(sticky="nsew")# 添加菜单项   连接:新建连接  打开连接 操作:-- 帮助:--menu_top = tk.Menu(self.root)# 连接菜单menu_link = tk.Menu(menu_top)menu_top.add_cascade(label="连接", menu=menu_link)menu_link.add_command(label="新建连接", command=self.new_connect_ui)menu_link.add_command(label="打开连接", command=self.open_connect_ui)# 操作菜单menu_func = tk.Menu(menu_top)menu_top.add_cascade(label="操作", menu=menu_func)menu_func.add_command(label="上传文件", command=self.upload_file)menu_func.add_command(label="下载文件", command=self.downloader)menu_func.add_command(label="vim编辑文件", command=self.vim_edit)# 帮助菜单menu_help = tk.Menu(menu_top)menu_top.add_cascade(label="帮助", menu=menu_help)menu_help.add_command(label="查看进程", command=self.get_process)# menu_help.add_command(label="查看磁盘使用情况", command=self.view_disk_usage)# 扩展菜单menu_ext = tk.Menu(menu_top)menu_top.add_cascade(label="扩展", menu=menu_ext)menu_ext.add_command(label="批量下载日志文件", command=self.download_logs)menu_ext.add_command(label="查看日志文件", command=self.view_log)# 展示信息的labelself.label_server = ttk.Label(frame, text="服务器信息:")self.label_server.grid(row=0, column=0, sticky="w", padx=10, pady=5)# 输入指令框self.command = tk.StringVar()entry_command = ttk.Entry(frame, width=60, textvariable=self.command)entry_command.grid(row=1, column=0, sticky="ew", padx=10, pady=5)# 执行指令的按钮entry_command.bind("<Return>", self.ok)  # 当按下Enter键时执行命令# 展示结果self.txt_result = tk.Text(frame)self.txt_result.grid(row=2, column=0, sticky="nsew", padx=10, pady=5)self.root.config(menu=menu_top)# 文本框背景/前景颜色self.txt_result.config(bg="#262626", fg="#D9D9D9")self.root.config(menu=menu_top)# 导入 ttk 后style = ttk.Style()# 设置 ttk 主题style.theme_use("clam")# 定制按钮样式style.configure("TButton", foreground="white", background="#404040")style.map("TButton",foreground=[("active", " #CCCCCC"), ("disabled", "#666666")],background=[("active", "#404040"), ("disabled", "#262626")])# 让文本框自动扩展以填充剩余空间frame.grid_rowconfigure(2, weight=1)frame.grid_columnconfigure(0, weight=1)frame.grid_rowconfigure(3, weight=0)  # 信息监控面板不需要扩展# thread_info=threading.Thread(target=self.update_info)thread_info = threading.Thread(target=self.update_info, args=(frame,))thread_info.start()# 将主应用程序挂起self.root.protocol("WM_DELETE_WINDOW", self.on_closing_main)self.root.mainloop()

2.12 打包成可执行文件的实现(按需求来)

 PyInstaller将Python程序打包成独立的可执行文件,包括所有必要的依赖项。

在终端执行以下命令:

pip install pyinstaller

Pyinstaller -F -w -i 图标(.ico) -p D:\python\Lib\site-packages --onefile --add-data "server.json;./"  demotest.py

 

3. 系统完成效果

3.1 服务器信息结果展示

65f7cc8fe92e47c6b7656adc55486da2.png

 

3.2 连接服务器结果展示

6a830cf753d14aa8b1029c60dfcba59f.png

 

3.3 添加服务器信息结果展示

138186a1a9e044b2a4a3b6a39ad27ecc.png

07eb7678dae44b47b9432fb37081483d.png

 

3.4 删除服务器信息结果展示

c80e7774b33147118d67425770ec6096.png

7319d0d0aba24b63a08cf8e0c2692421.png

9ef7cfd698e84b8c8a67c78f9efe1f7b.png

 

3.5 修改服务器信息结果展示

26ddec0079804eee8099c6877082458d.png

019fcc0c4e6f4edd9c831a3f0788390b.png

 

3.6 执行交互式交互命令结果展示

59886604a93e43fc90a110d384432b89.png

 

3.7 vim编辑器结果展示

81bb7a35fa814f84bdc778a45fb273ac.png

defbcc46849b467780cb0070cd1ea5bb.png

02239e7b848a4fbd8be189e0f9884140.png

 

3.8 文件上传下载结果展示

(1)上传文件

841945507adc45c8b6979625cecc5e8b.png

e7c2f5d59a33480695d71dfd2ecd8d07.png

(2)下载文件

c01113059e0942dfb80447f9e4c8d1cf.png

7029850b283c4ec2be1ace73ad201c28.png

dfcbbd47486c4733ac966041e034ab61.png

 

3.9 服务器监控功能结果展示

(1)cpu、内存、网络信息展示

13d1ce9928dc44c9b9f37365e5ebd18a.png

(2)磁盘使用情况展示

20b730df478d46a58139ec91a3a5c453.png

(3)进程信息

5d4916b09f95449383a6e80bcd848dff.png

3.10 批量下载日志文件和查看日志文件结果展示

(1)批量下载文件展示

3a0f8ebb24e14fb7b3705c803a18739b.png

14bf906494ec4ec99caf3614e87623fe.png

(2)查看日志信息

80d692b7cea34daa91e114893a158fbb.png

 

3.11窗口界面结果展示

0cdb02bbdabe43369d038660361c632a.png

4. 最终代码

import json
import os.path
import re
import socket
import subprocess
import threading
import time
from tkinter import simpledialogimport paramikofrom watchdog.events import FileSystemEventHandler
from watchdog.observers import Observerclass MySsh:def __init__(self):# 实例化 ,ssh服务器的会话高级表现形式,把通道/传输类/sftp进行了封装self.ssh = paramiko.SSHClient()self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())self.path = "~"# 所有服务器的列表self.servers = []# 当前正在使用的服务器信息self.server = {}self.load_server()self.connected = []def load_server(self):with open('server.json', "r", encoding='utf-8') as fp:self.servers = json.load(fp)print(f"获取到的服务器信息:{self.servers}")def save_server(self):with open('server.json', "w", encoding='utf-8') as fp:fp.write(json.dumps(self.servers))def add_server(self, ip, port, username, password, description):self.servers.append({"description": description,"ip": ip,"port": port,"username": username,"password": password})self.save_server()# def find_server(self,ip):def connecting(self):self.connected = None# 将信任的主机自动添加到know_hosts文件(~/.ssh/)中self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy)try:# 连接到服务器self.ssh.connect(hostname=self.server["ip"], port=self.server["port"], username=self.server["username"],password=self.server["password"])# messagebox.showinfo("成功", f"连接到服务器【{self.server['ip']}】成功!")print(f"连接到服务器【{self.server['ip']}】成功!")self.connected = Trueself.get_pwd()return Trueexcept Exception as e:messagebox.showerror("错误", f"连接到服务器【{self.server['ip']}】失败!")self.connected = Falsereturn Falsedef exec(self, command):if command.startswith("cd"):tmp_path = command.split(" ")[-1]if tmp_path.startswith("/"):# 绝对路径处理self.path = tmp_pathelse:# todo 如果用户输入的是一个相对路径呢?print("用户输入的是相对路径")if tmp_path.startswith(".."):self.path = self.path + "/" + tmp_pathelse:self.path = self.path + tmp_pathelse:# 根据当前的路径去执行指令command = f"cd {self.path} && {command}"print(f"->输入指令({self.path}):{command}")stdin, stdout, stderr = self.ssh.exec_command(command)result = stdout.read().decode("utf-8")print(f"<-返回:{result}")# 用户输入的路径里包含.. ,通过pwd刷新当前路径if command.find("..") > 0:self.get_pwd()return resultdef _read_channel(self, channel, output_callback):"""在独立线程中读取通道输出,并调用回调函数处理"""while True:if channel.recv_ready():data = channel.recv(1024)output_callback(data)def download_file(self):remote_file = tk.simpledialog.askstring("输入文件名", "请输入要下载的文件名:")if remote_file is None:  # 用户点击“取消”或关闭对话框returnelif not remote_file:  # 用户输入了空字符串messagebox.showerror("错误", "请输入文件名!")returnlocal_path = filedialog.askdirectory(title="选择下载到的本地目录")if local_path:try:sftp = self.ssh.open_sftp()local_file_path = os.path.join(local_path, remote_file)sftp.get(remote_file, local_file_path)sftp.close()messagebox.showinfo("下载成功", f"文件下载成功: {remote_file} -> {local_path}")except Exception as e:messagebox.showerror("下载失败", f"文件下载失败: {e}")def watch_and_upload(self, local_file_path, remote_full_path):class FileModifiedHandler(FileSystemEventHandler):def __init__(self, ssh_handler, remote_full_path):self.ssh_handler = ssh_handlerself.remote_full_path = remote_full_pathdef on_modified(self, event):if event.is_directory or not event.src_path == local_file_path:return# messagebox.showinfo("文件被修改", f"文件被修改:{event.src_path}")try:# self.ssh_handler.connecting()  # 重新连接SSH(如果需要)sftp = self.ssh_handler.ssh.open_sftp()sftp.put(local_file_path, self.remote_full_path)sftp.close()messagebox.showinfo("文件上传成功", f"vim修改成功: {local_file_path} -> {self.remote_full_path}")except Exception as e:messagebox.showerror("文件上传失败", f"文件上传失败: {e}")event_handler = FileModifiedHandler(self, remote_full_path)observer = Observer()observer.schedule(event_handler, path=os.path.dirname(local_file_path), recursive=False)observer.start()try:# 等待用户操作或其他信号来停止监视# 这里使用time.sleep作为示例,但建议使用更好的机制,如事件或信号while True:time.sleep(1)except KeyboardInterrupt:observer.stop()observer.join()def vim_edit(self):remote_file = simpledialog.askstring("vim", "请输入要编辑的完整远程文件路径:")if remote_file is None:  # 用户点击“取消”或关闭对话框returnelif not remote_file:  # 用户输入了空字符串messagebox.showerror("错误", "请输入文件路径!")return# 检查用户是否输入了以'/'开头的绝对路径if not remote_file.startswith('/'):messagebox.showerror("错误", "请输入绝对路径,例如:/home/user/file.txt")return# 临时文件目录tmp = filedialog.askdirectory(title="选择下载到的本地目录")if not tmp:  # 用户取消了选择目录returnlocal_file_path = os.path.join(tmp, os.path.basename(remote_file))try:self.connecting()sftp = self.ssh.open_sftp()# 检查远程文件是否存在try:sftp.stat(remote_file)except FileNotFoundError:# 如果不存在,则在远程服务器上创建空文件with sftp.open(remote_file, 'wb') as f:pass  # 写入空内容或者模板内容# 下载文件到本地sftp.get(remote_file, local_file_path)sftp.close()# 启动文件监视线程watch_thread = threading.Thread(target=self.watch_and_upload, args=(local_file_path, remote_file))watch_thread.start()# 打开文件编辑器subprocess.run(['Code', local_file_path], shell=True)except Exception as e:messagebox.showerror("文件下载失败", f"文件下载失败:{e}")print(f"文件下载失败:{e}")# 使用pwd更新当前路径def get_pwd(self):stdin, stdout, stderr = self.ssh.exec_command(f"cd {self.path} && pwd")result = stdout.read().decode("utf-8")self.path = result.strip()import tkinter as tk
from tkinter import ttk, filedialog
from tkinter import messageboxclass MyUI:def __init__(self, ssh):print("初始化")self.is_connected = Falseself.mssh = sshself.mssh_channel = None# 主应用程序窗口self.root = tk.Tk()frame = ttk.Frame(self.root)self.root.geometry("800x550")# 设置窗口标题(可选)self.root.title("暗影坤手")# 使用 grid 布局self.root.grid_rowconfigure(0, weight=1)self.root.grid_columnconfigure(0, weight=1)# 布局,网格布局# 创建一个框架容器frame = ttk.Frame(self.root)frame.grid(sticky="nsew")# 添加菜单项   连接:新建连接  打开连接 操作:-- 帮助:--menu_top = tk.Menu(self.root)# 连接菜单menu_link = tk.Menu(menu_top)menu_top.add_cascade(label="连接", menu=menu_link)menu_link.add_command(label="新建连接", command=self.new_connect_ui)menu_link.add_command(label="打开连接", command=self.open_connect_ui)# 操作菜单menu_func = tk.Menu(menu_top)menu_top.add_cascade(label="操作", menu=menu_func)menu_func.add_command(label="上传文件", command=self.upload_file)menu_func.add_command(label="下载文件", command=self.downloader)menu_func.add_command(label="vim编辑文件", command=self.vim_edit)# 帮助菜单menu_help = tk.Menu(menu_top)menu_top.add_cascade(label="帮助", menu=menu_help)menu_help.add_command(label="查看进程", command=self.get_process)# menu_help.add_command(label="查看磁盘使用情况", command=self.view_disk_usage)# 扩展菜单menu_ext = tk.Menu(menu_top)menu_top.add_cascade(label="扩展", menu=menu_ext)menu_ext.add_command(label="批量下载日志文件", command=self.download_logs)menu_ext.add_command(label="查看日志文件", command=self.view_log)# 展示信息的labelself.label_server = ttk.Label(frame, text="服务器信息:")self.label_server.grid(row=0, column=0, sticky="w", padx=10, pady=5)# 输入指令框self.command = tk.StringVar()entry_command = ttk.Entry(frame, width=60, textvariable=self.command)entry_command.grid(row=1, column=0, sticky="ew", padx=10, pady=5)# 执行指令的按钮entry_command.bind("<Return>", self.ok)  # 当按下Enter键时执行命令entry_command.bind("<Control-c>", self.okk)# 展示结果self.txt_result = tk.Text(frame)self.txt_result.grid(row=2, column=0, sticky="nsew", padx=10, pady=5)self.root.config(menu=menu_top)# 文本框背景/前景颜色self.txt_result.config(bg="#262626", fg="#D9D9D9")self.root.config(menu=menu_top)# 导入 ttk 后style = ttk.Style()# 设置 ttk 主题style.theme_use("clam")# 定制按钮样式style.configure("TButton", foreground="white", background="#404040")style.map("TButton",foreground=[("active", " #CCCCCC"), ("disabled", "#666666")],background=[("active", "#404040"), ("disabled", "#262626")])# 让文本框自动扩展以填充剩余空间frame.grid_rowconfigure(2, weight=1)frame.grid_columnconfigure(0, weight=1)frame.grid_rowconfigure(3, weight=0)  # 信息监控面板不需要扩展# thread_info=threading.Thread(target=self.update_info)thread_info = threading.Thread(target=self.update_info, args=(frame,))thread_info.start()# 将主应用程序挂起self.root.protocol("WM_DELETE_WINDOW", self.on_closing_main)self.root.mainloop()def okk(self, event):self.command.set('q')self.ok(self)def handle_output(self, data):# 先解码数据decoded_data = data.decode("utf-8")# 去除颜色转义序列cleaned_data = re.sub(r'\x1B\[([0-?]*[ -/]*[@-~])', '', decoded_data)# 插入到文本框的末尾self.txt_result.insert(tk.END, cleaned_data)# 确保滚动条跟随到最后self.txt_result.see(tk.END)# ------------------------------------------帮助---------------------------------------------------def get_process(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returnself.pop_win = tk.Toplevel(self.root)self.pop_win.title("进程")self.pop_win.geometry("800x600+600+250")table_frame = ttk.Frame(self.pop_win)table_frame.pack(padx=10, pady=10, fill="both", expand=True)server = self.mssh.serverip = server['ip']username = server['username']password = server['password']try:client = paramiko.SSHClient()client.set_missing_host_key_policy(paramiko.AutoAddPolicy())client.connect(ip, 22, username, password)stdin, stdout, stderr = client.exec_command("ps -ef")result = stdout.read().decode("utf-8").strip().split("\n")# 创建表格table = ttk.Treeview(table_frame, columns=("user", "pid", "cpu", "mem", "vsz", "rss", "tty", "stat", "start", "time", "command"), show="headings")table.heading("user", text="用户")table.heading("pid", text="进程ID")table.heading("cpu", text="CPU%")table.heading("mem", text="内存%")table.heading("vsz", text="虚拟内存")table.heading("rss", text="常驻内存")table.heading("tty", text="终端")table.heading("stat", text="状态")table.heading("start", text="启动时间")table.heading("time", text="CPU时间")table.heading("command", text="命令")table.pack(side="left", fill="both", expand=True)# 添加滚动条scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=table.yview)scrollbar.pack(side="right", fill="y")table.configure(yscrollcommand=scrollbar.set)# 填充表格数据for line in result[1:]:parts = line.split()if len(parts) >= 10:user, pid, cpu, mem, vsz, rss, tty, stat, start, time, *command = partscommand = " ".join(command)table.insert("", "end", values=(user, pid, cpu, mem, vsz, rss, tty, stat, start, time, command))# 调整列宽度table.column("#0", width=0, stretch="no")  # 隐藏第一列table.column("user", width=80, anchor="w")table.column("pid", width=60, anchor="e")table.column("cpu", width=60, anchor="e")table.column("mem", width=60, anchor="e")table.column("vsz", width=100, anchor="e")table.column("rss", width=100, anchor="e")table.column("tty", width=60, anchor="w")table.column("stat", width=60, anchor="w")table.column("start", width=120, anchor="w")table.column("time", width=80, anchor="e")table.column("command", width=300, anchor="w")# 自动调整行高def fixed_map(option):return [elm for elm in style.map("Treeview", query_opt=option) if elm[:2] != ("!disabled", "!selected")]style = ttk.Style()style.map("Treeview", foreground=fixed_map("foreground"), background=fixed_map("background"))except paramiko.AuthenticationException:messagebox.showerror("错误", "认证失败,请验证您的凭据")except paramiko.SSHException as sshException:messagebox.showerror("错误", f"无法建立SSH连接: {sshException}")except paramiko.BadHostKeyException as badHostKeyException:messagebox.showerror("错误", f"无法验证服务器的主机密钥: {badHostKeyException}")except Exception as e:messagebox.showerror("错误", f"发生错误: {e}")finally:if 'client' in locals() and client:client.close()  # 确保连接被关闭# 当窗口关闭时,确保没有遗留的引用或连接self.pop_win.protocol("WM_DELETE_WINDOW", self.on_closing)def create_widgets(self, frame):self.cpu_label = ttk.Label(frame, text="CPU信息:")self.cpu_label.grid()self.cpu_progress = ttk.Progressbar(frame, orient='horizontal', length=200, mode='determinate')self.cpu_progress.grid(pady=(0, 10))self.mem_label = ttk.Label(frame, text="Memory Usage: 0 MB / 0 MB")self.mem_label.grid()self.mem_progress = ttk.Progressbar(frame, orient='horizontal', length=200, mode='determinate')self.mem_progress.grid(pady=(0, 5))self.net_label = ttk.Label(frame, text="网络信息:")self.net_label.grid(pady=5)self.disk_button = tk.Button(frame, text="查看磁盘使用情况", command=self.show_disk_usage)self.disk_button.grid(pady=(5, 5))def update_info(self, frame):self.create_widgets(frame)  # 创建标签# 定义进度条样式style = ttk.Style()style.theme_use("default")style.configure("green.Horizontal.TProgressbar", foreground='green', background='green')style.configure("yellow.Horizontal.TProgressbar", foreground='yellow', background='yellow')style.configure("red.Horizontal.TProgressbar", foreground='red', background='red')prev_connected = self.mssh.connected  # 记录上一次的连接状态while True:if "ip" in self.mssh.server:if self.mssh.connected == None:result_connect = (f"服务器{self.mssh.server['ip']}连接中", "red")self.reset_system_monitoring()  # 重置系统监控状态elif self.mssh.connected:result_connect = (f"服务器{self.mssh.server['ip']}连接成功", "green")# -----------------------------系统信息------------------------------------------------server = self.mssh.serverip = server['ip']username = server['username']password = server['password']self.client = paramiko.SSHClient()self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())self.client.connect(ip, 22, username, password)# 执行远程命令获取内存信息#是Paramiko库中SSHClient对象的一个方法,可用于在远程服务器上执行命令。# 它接受一个字符串参数作为要执行的命令,并返回输入、输出和错误流作为结果。stdin, stdout, stderr = self.client.exec_command("free -m")result = stdout.read().decode('utf-8')# 获取系统内存信息lines = result.split('\n')mem_info = lines[1].split()total_mem = int(mem_info[1])used_mem = int(mem_info[2])mem_usage = used_mem / total_mem * 100# 获取网络速率stdin1, stdout1, stderr1 = self.client.exec_command("ifconfig")result = stdout1.read().decode("utf-8")# 使用正则表达式提取数据包和字节数信息matches = re.findall(r'ens160.*?RX packets (\d+)  bytes (\d+).*?TX packets (\d+)  bytes (\d+)',result, re.DOTALL)if matches:rx_packets, rx_bytes, tx_packets, tx_bytes = matches[0]else:rx_packets, rx_bytes, tx_packets, tx_bytes = 0, 0, 0, 0time.sleep(1)stdin2, stdout2, stderr3 = self.client.exec_command("ifconfig")result = stdout2.read().decode("utf-8")matches = re.findall(r'ens160.*?RX packets (\d+)  bytes (\d+).*?TX packets (\d+)  bytes (\d+)',result, re.DOTALL)if matches:rx_packets, new_rx_bytes, tx_packets, new_tx_bytes = matches[0]else:rx_packets, new_rx_bytes, tx_packets, new_tx_bytes = 0, 0, 0, 0# 计算速率upload_speed = ((int(new_tx_bytes) - int(tx_bytes)) / 1024)  # MB/s,download_speed = ((int(new_rx_bytes) - int(rx_bytes)) / 1024)# 获取cpu利用率stdin4, stdout4, stderr4 = self.client.exec_command("top -bn 1")result = stdout4.read().decode("utf-8")cpu = 0for line in result.split("\n"):if line.startswith("%Cpu(s)"):cpu_us = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[0])[0]cpu_sy = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[1])[0]cpu_ni = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[2])[0]cpu_id = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[3])[0]cpu_wa = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[4])[0]cpu_hi = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[5])[0]cpu_si = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[6])[0]cpu_st = re.findall(r'\d+\.\d+', line.split(":")[1].split(",")[7])[0]sum = float(cpu_st) + float(cpu_si) + float(cpu_hi) + float(cpu_wa) + float(cpu_id) + float(cpu_ni) + float(cpu_sy) + float(cpu_us)cpu = sum - float(cpu_id)# 设置进度条样式if cpu > 80:cpu_style = "red.Horizontal.TProgressbar"elif cpu > 50:cpu_style = "yellow.Horizontal.TProgressbar"else:cpu_style = "green.Horizontal.TProgressbar"if mem_usage > 80:mem_style = "red.Horizontal.TProgressbar"elif mem_usage > 50:mem_style = "yellow.Horizontal.TProgressbar"else:mem_style = "green.Horizontal.TProgressbar"# 更新标签self.cpu_label.config(text=f"CPU信息:{cpu:.2f}%")self.cpu_progress.config(style=cpu_style, value=cpu)self.mem_label.config(text=f"Memory Usage: {used_mem} MB / {total_mem} MB ({mem_usage:.2f}%)")self.mem_progress.config(style=mem_style, value=mem_usage)self.net_label.config(text=f"网络信息:↑{upload_speed:.2f}MB/s \t ↓{download_speed:.2f}MB/s")else:result_connect = (f"服务器{self.mssh.server['ip']}连接失败", "red")self.reset_system_monitoring()  # 重置系统监控状态else:result_connect = (f"服务器未选择", "black")self.reset_system_monitoring()  # 重置系统监控状态# 检查连接状态是否发生变化if self.mssh.connected != prev_connected:self.reset_system_monitoring()  # 重置系统监控状态prev_connected = self.mssh.connectedself.label_server.config(text=f"{result_connect[0]}", foreground=result_connect[1])time.sleep(1)def reset_system_monitoring(self):# 重置系统监控状态self.cpu_label.config(text="CPU信息:")self.cpu_progress.config(style="", value=0)self.mem_label.config(text="Memory Usage: 0 MB / 0 MB")self.mem_progress.config(style="", value=0)self.net_label.config(text="网络信息:")def show_disk_usage(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returntry:self.mssh.connecting()stdin, stdout, stderr = self.mssh.ssh.exec_command("df -h")disk_info = stdout.read().decode("utf-8").strip().split("\n")# 创建新窗口disk_window = tk.Toplevel(self.root)disk_window.title("磁盘使用情况")# 创建表格table_frame = ttk.Frame(disk_window)table_frame.pack(padx=10, pady=10, fill="both", expand=True)# 创建表格标题table = ttk.Treeview(table_frame, columns=("filesystem", "size", "used", "avail", "use%", "mounted_on"),show="headings")table.heading("filesystem", text="文件系统")table.heading("size", text="总大小")table.heading("used", text="已用空间")table.heading("avail", text="可用空间")table.heading("use%", text="已用百分比")table.heading("mounted_on", text="挂载点")table.pack(side="left", fill="both", expand=True)# 添加滚动条scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=table.yview)scrollbar.pack(side="right", fill="y")table.configure(yscrollcommand=scrollbar.set)# 填充表格数据for line in disk_info[1:]:parts = line.split()if len(parts) >= 6:filesystem, size, used, avail, use_percent, mounted_on = partstable.insert("", "end", values=(filesystem, size, used, avail, use_percent, mounted_on))# 调整列宽度table.column("#0", width=0, stretch="no")  # 隐藏第一列table.column("filesystem", width=100, anchor="w")table.column("size", width=80, anchor="e")table.column("used", width=80, anchor="e")table.column("avail", width=80, anchor="e")table.column("use%", width=80, anchor="e")table.column("mounted_on", width=200, anchor="w")# 自动调整行高def fixed_map(option):return [elm for elm in style.map("Treeview", query_opt=option) if elm[:2] != ("!disabled", "!selected")]style = ttk.Style()style.map("Treeview", foreground=fixed_map("foreground"), background=fixed_map("background"))except Exception as e:messagebox.showerror("错误", f"无法获取磁盘使用情况: {e}")def ok(self, event):command = self.command.get()if not self.mssh.connected:self.txt_result.insert(tk.END, "未连接到服务器!\n")return# 如果ssh_channel尚未创建,创建一个新的if self.mssh_channel is None:self.mssh_channel = self.mssh.ssh.invoke_shell()self.mssh_channel.set_combine_stderr(True)try:# 使用ssh_channel发送命令self.mssh_channel.send(f"{command}\n")# 创建线程监听输出并调用UI提供的回调函数t = threading.Thread(target=self.mssh._read_channel, args=(self.mssh_channel, self.handle_output))t.daemon = Truet.start()# 设置延时清空输入框self.root.after(2000, lambda: self.command.set(""))except Exception as e:self.txt_result.insert(tk.END, f"执行错误: {str(e)}\n")# 清空输入框self.command.set("")# --------------------------------------------连接-----------------------------------------------# 新建连接def new_connect_ui(self):self.pop_win = tk.Toplevel(self.root)# 设置标题和大小self.pop_win.title("新建连接")self.pop_win.geometry("300x200+600+250")frame = tk.Frame(self.pop_win)frame.grid()# IPip_label = tk.Label(frame, text="服务器IP地址:")ip_label.grid(row=0, column=0)self.ip_entry = tk.Entry(frame)self.ip_entry.grid(row=0, column=1)# Portport_label = tk.Label(frame, text="服务器端口号:")port_label.grid(row=1, column=0)self.port_entry = tk.Entry(frame)self.port_entry.grid(row=1, column=1)# Usernameusername_label = tk.Label(frame, text="用户名:")username_label.grid(row=2, column=0)self.username_entry = tk.Entry(frame)self.username_entry.grid(row=2, column=1)# Passwordpassword_label = tk.Label(frame, text="密码:")password_label.grid(row=3, column=0)self.password_entry = tk.Entry(frame, show="*")self.password_entry.grid(row=3, column=1)# Descriptiondescription_label = tk.Label(frame, text="备注信息:")description_label.grid(row=4, column=0)self.description_entry = tk.Entry(frame)self.description_entry.grid(row=4, column=1)# Buttonsubmit_button = tk.Button(frame, text="保存", command=self.save_server_info)submit_button.grid(row=5, columnspan=2)# 保存新建连接的服务器信息def save_server_info(self):if self.ip_entry.get() == "" or self.port_entry.get() == "" or self.username_entry.get() == "" or self.password_entry.get() == "":messagebox.showerror("错误", "请填写完整的服务器信息!")returnelse:ip = self.ip_entry.get()port = self.port_entry.get()username = self.username_entry.get()password = self.password_entry.get()description = self.description_entry.get()# 这里应该调用你的 add_server 函数,将获取到的信息添加到服务器列表中ssh.add_server(ip=ip, port=int(port), username=username, password=password, description=description)messagebox.showinfo('成功', "服务器信息已保存")#  打开连接def open_connect_ui(self):print("打开连接!")self.pop_win = tk.Toplevel(self.root)# 设置标题和大小self.pop_win.title("打开连接")self.pop_win.geometry("+480+230")frame = tk.Frame(self.pop_win)frame.grid()# 初始化索引变量 index 为 0,用于控制部件的位置index = 0# 遍历服务器列表 self.mssh.servers 中的每个服务器信息for item in self.mssh.servers:# 删除列表信息中的password(这里假设item是一个字典)#用于遍历 item.items() 中的键值对,并使用它们创建一个新的字典,# 创建一个新的字典其中排除了原始字典item中键为'password'的键值对。item_without_password = {k: v for k, v in item.items() if k != 'password'}# 显示不包含密码的服务器信息# 创建一个标签,显示当前服务器的信息,并将其放置在连接界面中的指定位置# padx=5 和 pady=5 是用来设置周围的水平和垂直填充# 在部件的左右/上下两侧各增加 5 个像素的水平填充。tk.Label(self.pop_win, text=str(item_without_password)).grid(row=index, column=0, padx=5, pady=5)# 添加连接、删除、修改按钮...# lambda 表达式用于创建一个匿名函数# command=lambda item=item# 将当前循环中的 item 值作为参数传递给 lambda 表达式,这样在按钮被点击时# lambda 表达式将会调用相应的方法,并将当前循环中的 item 值作为参数传递给这个方法# 使用 lambda item=item 的形式可以确保每个按钮的回调函数都引用了当前循环中的正确值tk.Button(self.pop_win, text="连接", command=lambda item=item: self.connect(item)).grid(row=index, column=2)tk.Button(self.pop_win, text='删除', command=lambda item=item: self.del_connect(item)).grid(row=index,column=4)tk.Button(self.pop_win, text='修改', command=lambda item=item: self.modify_server(item)).grid(row=index,column=6)# 更新索引变量 index,以便将下一个服务器的部件放置在下一行index = index + 1# 连接到指定的服务器def connect(self, server):print("连接到指定的服务器")# 将 self.mssh 对象的 server 属性设置为传入的 server 参数,以便记录当前连接的服务器信息。self.mssh.server = serverprint(f"服务器信息:{self.mssh.server}")#如果在连接到远程服务器时遇到超时,系统将等待 5 秒,然后会引发socket.timeout异常。socket.setdefaulttimeout(5)# 在单独的线程中执行连接操# 在单独的线程中执行连接操作# 创建一个新的线程 thread_connect,目标是调用 self.connect_thread 方法,并传入 server 参数。# 然后启动该线程,以便在单独的线程中执行连接操作,以避免阻塞主线程。thread_connect = threading.Thread(target=self.connect_thread, args=(server,))thread_connect.start()# 销毁打开连接窗口,以便在连接操作进行时,不再显示连接界面self.pop_win.destroy()def connect_thread(self, server):# 调用 self.mssh 对象的 connecting 方法,该方法用于执行连接操作,并将连接结果存储在 result 变量中result = self.mssh.connecting()if result:# 将对象的 is_connected 属性设置为 True,表示连接状态为已连接self.is_connected = True# 调用 update_connection_status 方法,将连接状态更新为已连接,并传入当前连接的服务器和连接状态self.update_connection_status(server, True)else:self.is_connected = Falseself.update_connection_status(server, False)def update_connection_status(self, server, connected):if connected:# 如果连接成功,通过 self.label_server 的 config 方法更新标签的文本内容为连接成功的消息# 使用了格式化字符串来显示服务器的 IP 地址,并设置文本颜色为绿色self.label_server.config(text=f"服务器{server['ip']}连接成功", foreground="green")else:self.label_server.config(text=f"服务器{server['ip']}连接失败", foreground="red")def del_connect(self, server):print("删除指定的服务器")# 输出服务器信息# 输出要删除的服务器信息,包括服务器的详细信息。print(f"服务器信息:{server}")# 确认是否删除服务器confirm = messagebox.askyesno("确认删除", f"确认删除服务器 {server['ip']} 吗?")if confirm:# 从服务器列表中删除指定的服务器# # 检查要删除的服务器是否存在于服务器列表中。if server in self.mssh.servers:self.mssh.servers.remove(server)self.pop_win.destroy()self.open_connect_ui()messagebox.showinfo("删除成功", f"服务器 {server['ip']} 已成功删除!")self.mssh.save_server()else:messagebox.showerror("错误", "服务器不存在,无法删除")# 保存更新后的服务器列表到配置文件中def modify_server(self, server):self.pop_win.destroy()print("修改服务器信息")# 将传入的服务器信息进行复制并存储在 self.mssh.server 中。self.mssh.server = server.copy()  # 添加这一行来复制服务器信息self.pop_win_modify = tk.Toplevel(self.root)frame = tk.Frame(self.pop_win_modify)frame.grid()# IPip_label = tk.Label(frame, text="服务器IP地址:")ip_label.grid(row=0, column=0)# 创建了一个文本框(Entry),并将其父级设置为 frame 框架。此文本框用于接收用户输入的 IP 地址。# textvariable 参数是用来关联文本框的值的变量,# 这里使用了 tk.StringVar 类来创建一个与文本框关联的字符串变量,并将该变量的初始值设置为 server['ip'],即当前服务器的IP地址self.ip_entry = tk.Entry(frame, textvariable=tk.StringVar(value=server['ip']))self.ip_entry.grid(row=0, column=1)# Portport_label = tk.Label(frame, text="服务器端口号:")port_label.grid(row=1, column=0)self.port_entry = tk.Entry(frame, textvariable=tk.StringVar(value=str(server['port'])))self.port_entry.grid(row=1, column=1)# Usernameusername_label = tk.Label(frame, text="用户名:")username_label.grid(row=2, column=0)# 创建了一个文本框(Entry),并将其父级设置为 frame 框架。此文本框用于接收用户输入的 username。# textvariable 参数是用来关联文本框的值的变量,# 这里使用了 tk.StringVar 类来创建一个与文本框关联的字符串变量,并将该变量的初始值设置为 server['username'],即当前服务器的用户名self.username_entry = tk.Entry(frame, textvariable=tk.StringVar(value=server['username']))self.username_entry.grid(row=2, column=1)# Passwordpassword_label = tk.Label(frame, text="密码:")password_label.grid(row=3, column=0)self.password_entry = tk.Entry(frame, show="*", textvariable=tk.StringVar(value=server['password']))self.password_entry.grid(row=3, column=1)# Descriptiondescription_label = tk.Label(frame, text="备注信息:")description_label.grid(row=4, column=0)# 创建了一个文本框(Entry),并将其父级设置为 frame 框架。此文本框用于接收用户输入的 备注信息。# textvariable 参数是用来关联文本框的值的变量,# 这里使用了 tk.StringVar 类来创建一个与文本框关联的字符串变量,并将该变量的初始值设置为 server['description'],即当前服务器的备注信息self.description_entry = tk.Entry(frame, textvariable=tk.StringVar(value=server['description']))self.description_entry.grid(row=4, column=1)# Buttonsubmit_button = tk.Button(frame, text="保存", command=self.save_modified_server)submit_button.grid(row=5, columnspan=2)# 保存修改后的服务器信息def save_modified_server(self):if self.ip_entry.get() == "" or self.port_entry.get() == "" or self.username_entry.get() == "" or self.password_entry.get() == "":messagebox.showerror("错误", "请填写完整的服务器信息!")returnelse:modified_server = {"ip": self.ip_entry.get(),"port": int(self.port_entry.get()),"username": self.username_entry.get(),"password": self.password_entry.get(),  # 假设密码不变,保持原值"description": self.description_entry.get(),}# 找到了待修改服务器在服务器列表中的索引# 使用了 index 方法来查找 self.mssh.server 在 self.mssh.servers 中的位置。index = self.mssh.servers.index(self.mssh.server)# 将修改后的服务器信息替换掉原来的服务器信息,以完成对服务器信息的更新。self.mssh.servers[index] = modified_server# 弹出一个消息框,显示成功修改服务器信息的提示。messagebox.showinfo("成功", "服务器信息已成功修改!")# 关闭了修改服务器信息的窗口self.pop_win_modify.destroy()# 调用了 open_connect_ui 方法,打开连接用户界面,让用户可以选择连接到修改后的服务器。self.open_connect_ui()# 调用了 save_server 方法,用于保存修改后的服务器信息到文件中,以便下次程序运行时能够读取到最新的服务器信息。self.mssh.save_server()##################################  操作  ################################################## 上传文件def upload_file(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returnelse:self.file_path = filedialog.askopenfilename()try:if self.file_path:# 获取文件名file_name = self.file_path.split("/")[-1]# 创建 SFTP 客户端sftp_client = self.mssh.ssh.open_sftp()remote_path = f"{self.mssh.path}/{file_name}"print(f"文件:{self.file_path},上传到:{remote_path}")# 上传文件sftp_client.put(self.file_path, remote_path)messagebox.showinfo("成功", "上传成功")sftp_client.close()else:print("请选择要上传的文件")except Exception as e:messagebox.showerror("错误", f"文件上传失败: {e}")# #下载文件def downloader(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returnself.mssh.download_file()def vim_edit(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")returnself.mssh.vim_edit()def download_logs(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接到服务器!")return# 将远程目录设置为 /var/logremote_dir = "/var/log"try:self.mssh.connecting()sftp = self.mssh.ssh.open_sftp()# 检查远程目录是否存在try:sftp.stat(remote_dir)except IOError as e:messagebox.showerror("错误", f"远程目录 {remote_dir} 不存在")returnremote_files = sftp.listdir(remote_dir)local_dir = filedialog.askdirectory(title="选择下载到的本地目录")if local_dir:self.local_log_dir = local_direlse:returndownloaded_count = 0for filename in remote_files:# 检查文件是否以 ".log" 结尾if filename.endswith(".log"):remote_path = f"{remote_dir}/{filename}"local_path = f"{local_dir}/{filename}"sftp.get(remote_path, local_path)downloaded_count += 1print(f"下载成功: {remote_path} -> {local_path}")sftp.close()print(f"共下载了 {downloaded_count} 个文件")if downloaded_count > 0:messagebox.showinfo("成功", f"成功下载了 {downloaded_count} 个日志文件")except Exception as e:print(f"下载失败: {e}")messagebox.showerror("错误", f"下载日志文件失败: {e}")def view_log(self):if not self.mssh.connected:messagebox.showerror("错误", "请先连接服务器并下载日志文件!")return# 从 download_logs 函数中获取之前选择的本地目录try:local_dir = self.local_log_direxcept AttributeError:local_dir = filedialog.askdirectory(title="选择下载日志文件所在的目录")if not local_dir:returnlog_file = filedialog.askopenfilename(initialdir=local_dir, title="选择要查看的日志文件",filetypes=(("Log Files", "*.log"),))if log_file:try:# 使用 VSCode 打开日志文件subprocess.run(['Code', log_file], shell=True)except Exception as e:messagebox.showerror("错误", f"无法打开文件: {e}")def on_closing_main(self):if messagebox.askokcancel("Quit", "确认退出吗?"):self.root.destroy()def on_closing(self):if messagebox.askokcancel("Quit", "确认退出吗?"):self.pop_win.destroy()if __name__ == '__main__':ssh = MySsh()ui = MyUI(ssh)

5. 总结与分析

        使用 Python 的 Tkinter 模块编写 SSH 工具需要综合考虑界面设计、SSH 连接的实现、用户体验和安全等方面。通过 Tkinter 提供的丰富小部件,可以创建直观且易于操作的界面,让用户输入主机名、用户名、密码等信息,并提供连接按钮来触发 SSH 连接。同时,使用 Paramiko 或其他类似的库处理 SSH 连接和命令执行的细节,考虑使用异步处理以避免界面冻结,同时要注意处理连接错误和认证失败等异常情况,可以使用多线程来解决。在设计中需确保安全处理用户凭据和敏感信息,避免在界面中明文显示密码。最终,在测试和调试过程中不断完善功能,确保工具的稳定性和可靠性。通过充分利用 Python 和 Tkinter 的功能,可以创建一个功能强大且易于使用的 SSH 工具。

 

注:此项目是大学期间与搭子 霍格沃茨的纸飞鹤-CSDN博客 共同完成的,现在分享给大家参考。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/873869.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

RabbitMQ发布确认及交换机类型

RabbitMQ发布确认机制详解 一、引言 在消息队列&#xff08;MQ&#xff09;技术中&#xff0c;RabbitMQ因其稳定性、可靠性和易用性而受到广泛欢迎。为了确保消息的可靠传递&#xff0c;RabbitMQ提供了一系列高级特性&#xff0c;其中发布确认&#xff08;Publisher Confirms&…

配置kali 的apt命令在线安装包的源为国内源

目录 一、安装VMware Tools 二、配置apt国内源 一、安装VMware Tools 点击安装 VMware Tools 后&#xff0c;会加载一个虚拟光驱&#xff0c;里面包含 VMware Tools 的安装包 鼠标右键单击 VMware Tools 的安装包&#xff0c;点击复制到 点击 主目录&#xff0c;再点击选择…

C语言 ——— 写一个函数,调整 整型数组 中 奇数偶数的顺序

目录 题目要求 代码实现 题目要求 创建一个整型数组 自定义函数实现&#xff1a;调整该数组中数字的顺序&#xff0c;使得数组中所有的奇数位于数组的前半部分&#xff0c;数组中所有的偶数位于数组的后半部分 举例&#xff1a; 输入的整型数组为&#xff1a;[234,24,45,…

TCP协议的相关特性

目录 首先先了解TCP和UDP协议的特点 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09; UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09; TCP的六个标志位 确认应答 超时重传 连接管理(三次握手,四次挥…

Python爬虫(基本流程)

1. 确定目标和范围 明确需求&#xff1a;确定你需要从哪些网站抓取哪些数据。合法性&#xff1a;检查目标网站的robots.txt文件&#xff0c;了解哪些内容可以被抓取。数据范围&#xff1a;确定爬取数据的起始和结束点&#xff0c;比如时间范围、页面数量等。 2. 选择合适的工…

科研绘图系列:R语言微生物堆积图(stacked barplot)

介绍 堆叠条形图是一种数据可视化图表,它通过将每个条形分割成多个部分来展示不同类别的数值。每个条形代表一个总体数据,而条形内的每个部分则代表该总体数据中不同子类别的数值。这种图表特别适合展示整体与部分的关系,以及各部分在整体中的比例。 特点: 多部分条形:每…

框架设计MVP

重点&#xff1a; 1.presenter里面包含view和model 2.和MVC不同的是View和Model之间不进行交互 3.view包含presenter,view通过presenter反向处理逻辑。 链接&#xff1a; MVP简单实现_安卓mvp presenter-CSDN博客 Model.h #pragma once //Model数据处理器 class Model { …

动漫风格动漫404网站维护HTML源码

源码介绍 动漫风格动漫404网站维护HTML源码&#xff0c;源码由HTMLCSSJS组成&#xff0c;记事本打开源码文件可以进行内容文字之类的修改&#xff0c;双击html文件可以本地运行效果&#xff0c;也可以上传到服务器里面 效果预览 源码下载 动漫风格动漫404网站维护HTML源码

GSAP动画魔法:解锁网页设计的无限创意与动感

GSAP&#xff08;GreenSock Animation Platform&#xff09;是一个强大的JavaScript动画库&#xff0c;用于在Web应用程序中创建高性能和复杂的动画效果&#xff0c;其提供了一套丰富的API和工具&#xff0c;使开发者能够轻松地创建流畅、交互式的动画&#xff0c;涵盖了从简单…

Shader入门精要总结(一)渲染流水线

渲染流水线的最终目的在于生成或者说是渲染一张二维纹理&#xff0c;即我们在电脑屏幕上看到的所有效果。 流水线主要分成三个阶段&#xff1a;应用阶段&#xff0c;几何阶段&#xff0c;光栅化阶段。&#xff08;概念&#xff09; 一.概述 1. 应用阶段 这个阶段由应用主导&am…

Debian Linux下rclone挂载谷歌云盘碰到的坑

可能是明月好久没有使用境外服务器挂载境外的云盘缘故吧,今天一个代维客户需要他的Linux服务器挂载谷歌云盘好进行云备份,本来是个很简单的事儿,没想到在rclone连接谷歌云盘的时候卡壳了,可是把明月给难为坏了,搜索到的简体中文教程倒是很多,但没有一个提到这个“坑”,最…

秋招突击——7/20——科大讯飞笔试相关——提前{图像叠加、世界杯积分问题、直角三角形个数}——正式{持续输出中位数,二次幂和三次幂的序列表示}

文章目录 引言提前图像叠加世界杯积分问题个人实现参考实现 直角三角形个数个人实现 正式等待时间期望值中位数输出个人实现 二次幂和三次幂序列个人实现 总结 引言 昨天用了牛客网做了一下作业帮的笔试&#xff0c;做的一塌糊涂&#xff0c;但是提醒我要练习使用牛客网的环境…

基于Matlab的数据可视化

基于Matlab的数据可视化 一、二维图形的绘制&#xff08;一&#xff09;基本图形函数&#xff08;1&#xff09;plot函数&#xff08;2&#xff09;fplot函数&#xff08;3&#xff09;其他坐标系的二维曲线 &#xff08;二&#xff09;图形属性设置&#xff08;1&#xff09;线…

Linux网络:应用层协议HTTP(一)

一、什么是HTTP协议 虽然我们说, 应用层协议是我们程序猿自己定的. 但实际上, 已经有大佬们定义了一些现成的, 又非常好用的应用层协议, 供我们直接参考使用. HTTP(超文本传输协议)就是其中之一。 在互联网世界中&#xff0c;HTTP&#xff08;HyperText Transfer Protocol&…

C#中的栈和队列

什么是栈 栈和队列是非常重要的两种数据结构,在软件设计中应用很多。栈和队列也是线性结构,线性表、栈和队列这三种数据结构的数据元素以及数据元素间的逻辑关系完全相同,差别是线性表的操作不受限制,而栈和队列的操作受到眼制。栈的操作只能在表的一端进行,队列的插入操作在表…

安卓篇-unidbg初步测试

前言:目前app的进度是c层的md5魔改 已经做好,又套了一层java层的加密。刚好可以再熟悉一下unidbg 最新版的app ,作为demo测试一下。 分析先看下app 的反编译的结果,很明显,源码很清晰 ,之前的混淆方式一点用没有。 然后跳过去 /* loaded from: classes4.dex */ public c…

FPGA实验5:4位加法计数器

实验目的及要求 掌握时钟信号、进程和BUFFER端口的运用&#xff1b;了解计数器的设计、仿真和硬件测试&#xff0c;进一步熟悉VHDL语句、语法及应用等。 实验原理 运用Quartus II 集成环境下的VHDL文本设计方法设计4位加法计数器&#xff0c;进行波形仿真和分析、引脚分配…

4 Java的基本程序设计结构(基本语法3)- 字符串、面向对象(进阶)

文章目录 前言一、java中的 究竟比较的是什么&#xff1f;二、字符串1 String&#xff08;1&#xff09;String的两种构造方式&#xff08;2&#xff09;字符数组转字符串、字节数组转字符串&#xff08;3&#xff09;字符串的 拼接&#xff08;4&#xff09;字符串的内存结构…

【Android面试八股文】荣耀面试算法题:输入一个N阶方阵(0<N<10),输出此方阵顺时针旋转M(0<=M<=10000)次后的方阵

文章目录 1. 算法题:输入一个N阶方阵(0<N<10),输出此方阵顺时针旋转M(0<=M<=10000)次后的方阵1.1 题目描述1.2 算法实现1.2.1 步骤说明:1.2.2 算法实现1.2.3 代码实现:1.2.4 程序说明:1.2.5 示例详细讲解如何将一个矩阵顺时针旋转90度1. 算法题:输入一个N阶方…

Spring -- 三层架构

T04BF &#x1f44b;专栏: 算法|JAVA|MySQL|C语言 &#x1faf5; 今天你敲代码了吗 应用分层 介绍 在阿里的开发手册里,关于工程结构部分,定义的常见工程的应用分层结构 那么什么是应用分层呢? 应用分层是一种软件开发设计思想,他将应用程序分层N个层次.这N个层次分别负责各…