import threading import tkinter as tk from tkinter import filedialog, messagebox, ttk from core.Function.dns_diag_fun import ALL_ADAPTERS, DEFAULT_DOMAINS, DEFAULT_RECORD_TYPES, DnsDiagnostic from core.ui.components import Page, action_bar, button, combo, field class DnsTab(Page): def __init__(self, parent, console): super().__init__(parent, "DNS 诊断", "对比本机 DNS 与常用 DNS 的解析结果、耗时和失败原因。") self.console = console self.body.rowconfigure(2, weight=1) status = self.section("实时状态", 0, columns=7) self.state = field(status, "状态", 1, 0, "等待", 10) self.current_dns = field(status, "当前 DNS", 1, 1, "-", 18, colspan=2) self.target_total = field(status, "目标数", 1, 3, "0", 8) self.progress = field(status, "进度", 1, 4, "0/0", 10) self.abnormal = field(status, "异常数", 1, 5, "0", 8) self.elapsed = field(status, "耗时", 1, 6, "0.0s", 10) for item in (self.state, self.current_dns, self.target_total, self.progress, self.abnormal, self.elapsed): item["entry"].configure(state="disabled") params = self.section("诊断参数", 1, columns=6) self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22) self.domains = field(params, "目标域名", 1, 1, DEFAULT_DOMAINS, 34, colspan=2) self.record_types = field(params, "记录类型", 1, 3, DEFAULT_RECORD_TYPES, 18) self.timeout = field(params, "超时 ms", 1, 4, "2000", 10) self.repeat_count = field(params, "重复次数", 1, 5, "1", 10) self.dns_servers = field(params, "DNS 服务器", 2, 0, "", 60, colspan=6) actions = action_bar(params, 3, 6) self.start_btn = button(actions, "开始诊断", self.start_diagnosis, "Primary.TButton") self.stop_btn = button(actions, "停止", self.stop_diagnosis, "Danger.TButton") self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton") self.auto_dns_btn = button(actions, "自动 DNS", self.fill_default_dns, "Secondary.TButton") self.repair_btn = button(actions, "修复异常", self.repair_dns, "Secondary.TButton") secondary_actions = action_bar(params, 4, 6) self.copy_btn = button(secondary_actions, "复制摘要", self.copy_summary, "Secondary.TButton") self.export_btn = button(secondary_actions, "导出 CSV", self.export_results, "Secondary.TButton") results = self.section("诊断结果", 2, columns=1) results.rowconfigure(1, weight=1) results.columnconfigure(0, weight=1) self.results_tree = ttk.Treeview( results, columns=("domain", "type", "server", "status", "elapsed", "values", "verdict"), show="headings", height=10, ) headings = { "domain": "域名", "type": "类型", "server": "DNS 服务器", "status": "状态", "elapsed": "耗时", "values": "解析结果", "verdict": "错误 / 判断", } widths = { "domain": 135, "type": 55, "server": 115, "status": 65, "elapsed": 78, "values": 250, "verdict": 130, } for column, title in headings.items(): self.results_tree.heading(column, text=title) self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False) self.results_tree.tag_configure("正常", foreground="#15803d") self.results_tree.tag_configure("无记录", foreground="#b7791f") self.results_tree.tag_configure("失败", foreground="#dc2626") self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) result_scroll = ttk.Scrollbar( results, orient="vertical", command=self.results_tree.yview, style="Modern.Vertical.TScrollbar", ) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18)) self.results_tree.configure(yscrollcommand=result_scroll.set) self.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result) self.after(350, self.load_adapters) def write(self, text, tag=None): self.console.write(text, tag) def clear(self): self.console.clear() for item in self.results_tree.get_children(): self.results_tree.delete(item) self.update_status( { "state": "等待", "current_dns": "-", "total": 0, "completed": 0, "abnormal": 0, "elapsed": 0, } ) def load_adapters(self): def worker(): try: values, default_dns = self.diagnostic.get_adapter_choices_and_default_dns() except Exception as exc: values = [ALL_ADAPTERS] default_dns = "" self.write(f"读取网卡 DNS 失败: {exc}\n", "warning") self.after(0, lambda: self.apply_adapters(values, default_dns)) threading.Thread(target=worker, daemon=True).start() def apply_adapters(self, values, default_dns): values = values or [ALL_ADAPTERS] values = list(dict.fromkeys(values)) self.adapter["combobox"]["values"] = values if self.adapter["var"].get() not in values: self.adapter["var"].set(ALL_ADAPTERS) if not self.dns_servers["var"].get() and default_dns: self.dns_servers["var"].set(default_dns) def fill_default_dns(self): try: value = self.diagnostic.default_dns_servers(self.adapter["var"].get()) if not value: messagebox.showinfo("提示", "未读取到本机 DNS,已保留常用 DNS 对比服务器") value = "223.5.5.5,114.114.114.114,8.8.8.8" self.dns_servers["var"].set(value) self.write(f"已生成 DNS 对比列表: {value}\n", "success") except Exception as exc: messagebox.showwarning("生成失败", str(exc)) def start_diagnosis(self): try: self.clear() self.start_btn.configure(state="disabled") self.repair_btn.configure(state="disabled") self.diagnostic.start_diagnosis(self.adapter["var"].get(), self.options()) except Exception as exc: self.start_btn.configure(state="normal") self.repair_btn.configure(state="normal") messagebox.showwarning("无法开始 DNS 诊断", str(exc)) def stop_diagnosis(self): try: self.diagnostic.stop_diagnosis() except Exception as exc: messagebox.showinfo("提示", str(exc)) def options(self): return { "domains": self.domains["var"].get(), "record_types": self.record_types["var"].get(), "dns_servers": self.dns_servers["var"].get(), "timeout_ms": self.timeout["var"].get(), "repeat_count": self.repeat_count["var"].get(), } def export_results(self): path = filedialog.asksaveasfilename( title="导出 DNS 诊断结果", defaultextension=".csv", filetypes=[("CSV 文件", "*.csv")], ) if not path: return try: self.diagnostic.export_results(path) self.write(f"已导出结果: {path}\n", "success") except Exception as exc: messagebox.showwarning("导出失败", str(exc)) def repair_dns(self): adapter = self.adapter["var"].get() or ALL_ADAPTERS if not messagebox.askyesno( "确认修复 DNS", f"将把“{adapter}”的 DNS 设置为 223.5.5.5 和 114.114.114.114,并刷新 DNS 缓存。\n\n此操作需要管理员权限,确定继续吗?", ): return self.repair_btn.configure(state="disabled") self.start_btn.configure(state="disabled") self.write("\n开始修复 DNS 异常...\n", "warning") def worker(): try: result = self.diagnostic.repair_abnormal_dns(adapter) self.after(0, lambda: self.on_repair_success(result)) except Exception as exc: self.after(0, lambda: self.on_repair_failed(exc)) threading.Thread(target=worker, daemon=True).start() def on_repair_success(self, result): names = "、".join(result.get("adapters", [])) servers = ",".join(result.get("servers", [])) self.write(f"DNS 修复完成: {names} -> {servers}\n", "success") self.dns_servers["var"].set(servers) self.load_adapters() self.start_btn.configure(state="normal") self.repair_btn.configure(state="normal") def on_repair_failed(self, exc): self.write(f"DNS 修复失败: {exc}\n", "error") self.start_btn.configure(state="normal") self.repair_btn.configure(state="normal") messagebox.showerror("DNS 修复失败", f"{exc}\n\n请确认程序已用管理员权限运行。") def copy_summary(self): text = self.diagnostic.copy_summary() if not text: messagebox.showinfo("提示", "还没有可复制的 DNS 诊断摘要") return self.clipboard_clear() self.clipboard_append(text) self.write("已复制 DNS 诊断摘要到剪贴板\n", "success") def update_status(self, stats): def apply(): total = stats.get("total", 0) completed = stats.get("completed", 0) values = [ (self.state, stats.get("state", "等待")), (self.current_dns, stats.get("current_dns") or "-"), (self.target_total, str(total)), (self.progress, f"{completed}/{total}" if total else "0/0"), (self.abnormal, str(stats.get("abnormal", 0))), (self.elapsed, f"{stats.get('elapsed', 0):.1f}s"), ] for item, value in values: item["entry"].configure(state="normal") item["var"].set(value) item["entry"].configure(state="disabled") self.after(0, apply) def add_result(self, row): def apply(): elapsed = row.get("elapsed_ms", 0) try: elapsed_text = f"{float(elapsed):.1f} ms" if float(elapsed) > 0 else "" except (TypeError, ValueError): elapsed_text = "" detail = row.get("error") or row.get("verdict", "") self.results_tree.insert( "", "end", values=( row.get("domain", ""), row.get("record_type", ""), row.get("dns_server", ""), row.get("status", ""), elapsed_text, row.get("values", ""), detail, ), tags=(row.get("status", ""),), ) self.after(0, apply) def on_task_done(self): self.after(0, lambda: (self.start_btn.configure(state="normal"), self.repair_btn.configure(state="normal")))