This commit is contained in:
2026-06-08 13:39:59 +08:00
parent 2d67146487
commit e9ef41f184
17 changed files with 723 additions and 266 deletions
+61 -39
View File
@@ -2,8 +2,8 @@ import concurrent.futures
import csv import csv
import ipaddress import ipaddress
import json import json
import unicodedata
import re import re
import socket
import threading import threading
import time import time
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
@@ -75,7 +75,6 @@ class DeviceDiscoveryOptions:
class DeviceInfo: class DeviceInfo:
ip: str ip: str
mac: str mac: str
hostname: str
vendor: str vendor: str
adapter: str adapter: str
interface_index: str interface_index: str
@@ -112,6 +111,12 @@ class DeviceDiscovery:
adapters = self._active_adapters(self.network.get_network_info()) adapters = self._active_adapters(self.network.get_network_info())
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters] return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
def get_adapter_choices_and_default_range(self) -> tuple[list[str], str, str]:
adapters = self._active_adapters(self.network.get_network_info())
choices = [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
default_adapter = preferred_discovery_adapter(adapters)
return choices, adapter_to_safe_range(default_adapter) if default_adapter else "", default_adapter.get("name", "") if default_adapter else ""
def default_scan_range(self, adapter_name: str = ALL_ADAPTERS) -> str: def default_scan_range(self, adapter_name: str = ALL_ADAPTERS) -> str:
adapters = self._select_adapters(adapter_name) adapters = self._select_adapters(adapter_name)
if not adapters: if not adapters:
@@ -136,7 +141,7 @@ class DeviceDiscovery:
self.last_summary = "" self.last_summary = ""
self.output( self.output(
f"开始局域网设备发现: {adapter_name or ALL_ADAPTERS}" f"开始局域网设备发现: {adapter_name or ALL_ADAPTERS}"
f"目标 {len(targets)} 个,并发 {discovery_options.workers},超时 {discovery_options.timeout_ms}ms\n", f"目标 {len(targets)} 个,并发 {min(discovery_options.workers, 24, max(1, len(targets)))},超时 {discovery_options.timeout_ms}ms\n",
"muted", "muted",
) )
self.status(self._status("扫描中", adapter_name or ALL_ADAPTERS, describe_targets(targets), len(targets), 0, 0, 0)) self.status(self._status("扫描中", adapter_name or ALL_ADAPTERS, describe_targets(targets), len(targets), 0, 0, 0))
@@ -196,13 +201,14 @@ class DeviceDiscovery:
def scan_targets(self, targets: list[str], options: DeviceDiscoveryOptions, started: float) -> dict[str, dict]: def scan_targets(self, targets: list[str], options: DeviceDiscoveryOptions, started: float) -> dict[str, dict]:
results = {} results = {}
completed = 0 completed = 0
workers = min(options.workers, 24, max(1, len(targets)))
def task(ip: str) -> dict: def task(ip: str) -> dict:
if self.stop_event.is_set(): if self.stop_event.is_set():
return {"ip": ip, "ok": False, "rtt": 0.0} return {"ip": ip, "ok": False, "rtt": 0.0}
return ping_once(ip, options.timeout_ms) return ping_once(ip, options.timeout_ms)
with concurrent.futures.ThreadPoolExecutor(max_workers=options.workers) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
futures = {executor.submit(task, ip): ip for ip in targets} futures = {executor.submit(task, ip): ip for ip in targets}
for future in concurrent.futures.as_completed(futures): for future in concurrent.futures.as_completed(futures):
completed += 1 completed += 1
@@ -241,7 +247,6 @@ class DeviceDiscovery:
devices[local_ip] = DeviceInfo( devices[local_ip] = DeviceInfo(
ip=local_ip, ip=local_ip,
mac=normalize_mac(adapter.get("mac", "")), mac=normalize_mac(adapter.get("mac", "")),
hostname=resolve_hostname(local_ip),
vendor=vendor_name(adapter.get("mac", "")), vendor=vendor_name(adapter.get("mac", "")),
adapter=adapter.get("name", ""), adapter=adapter.get("name", ""),
interface_index=str(adapter.get("interface_index", "")), interface_index=str(adapter.get("interface_index", "")),
@@ -270,7 +275,6 @@ class DeviceDiscovery:
devices[ip] = DeviceInfo( devices[ip] = DeviceInfo(
ip=ip, ip=ip,
mac=mac, mac=mac,
hostname=resolve_hostname(ip),
vendor=vendor_name(mac), vendor=vendor_name(mac),
adapter=adapter.get("name", ""), adapter=adapter.get("name", ""),
interface_index=str(adapter.get("interface_index", "")), interface_index=str(adapter.get("interface_index", "")),
@@ -285,20 +289,15 @@ class DeviceDiscovery:
adapter = adapter_for_ip(ip, adapters) adapter = adapter_for_ip(ip, adapters)
if not adapter: if not adapter:
continue continue
# Ping succeeded but no valid ARP MAC was visible; keep it out of
# the asset list unless it is a known local/gateway address.
if ip not in gateways and ip not in local_ips:
continue
devices[ip] = DeviceInfo( devices[ip] = DeviceInfo(
ip=ip, ip=ip,
mac="", mac="",
hostname=resolve_hostname(ip),
vendor="未知", vendor="未知",
adapter=adapter.get("name", ""), adapter=adapter.get("name", ""),
interface_index=str(adapter.get("interface_index", "")), interface_index=str(adapter.get("interface_index", "")),
latency_ms=ping.get("rtt", 0.0), latency_ms=ping.get("rtt", 0.0),
method="在线", method="在线",
note="网关" if ip in gateways else "本机", note="网关" if ip in gateways else "本机" if ip in local_ips else "未读取到 MAC",
) )
return list(devices.values()) return list(devices.values())
@@ -346,7 +345,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
def build_scan_targets(self, adapters: list[dict], options: DeviceDiscoveryOptions) -> list[str]: def build_scan_targets(self, adapters: list[dict], options: DeviceDiscoveryOptions) -> list[str]:
if options.scan_range: if options.scan_range:
return parse_target_range(options.scan_range, options.max_hosts) return filter_targets_for_adapters(parse_target_range(options.scan_range, options.max_hosts), adapters)
targets = [] targets = []
for adapter in adapters: for adapter in adapters:
targets.extend(parse_target_range(adapter_to_safe_range(adapter), options.max_hosts)) targets.extend(parse_target_range(adapter_to_safe_range(adapter), options.max_hosts))
@@ -361,7 +360,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
def export_results(self, path: str) -> None: def export_results(self, path: str) -> None:
if not self.last_results: if not self.last_results:
raise RuntimeError("还没有可导出的设备发现结果") raise RuntimeError("还没有可导出的设备发现结果")
fields = ["ip", "mac", "hostname", "vendor", "adapter", "interface_index", "latency_ms", "method", "note", "checked_at"] fields = ["ip", "mac", "vendor", "adapter", "interface_index", "latency_ms", "method", "note", "checked_at"]
with open(path, "w", newline="", encoding="utf-8-sig") as file: with open(path, "w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=fields) writer = csv.DictWriter(file, fieldnames=fields)
writer.writeheader() writer.writeheader()
@@ -372,7 +371,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
if not self.last_results: if not self.last_results:
return self.last_summary return self.last_summary
return "\n".join( return "\n".join(
f"{row.get('ip', '')}\t{row.get('mac', '')}\t{row.get('hostname', '')}\t{row.get('vendor', '')}\t{row.get('method', '')}" f"{row.get('ip', '')}\t{row.get('mac', '')}\t{row.get('vendor', '')}\t{row.get('method', '')}"
for row in sorted(self.last_results, key=lambda item: ip_sort_key(item.get("ip", ""))) for row in sorted(self.last_results, key=lambda item: ip_sort_key(item.get("ip", "")))
) )
@@ -389,7 +388,8 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
adapters = self._active_adapters(self.network.get_network_info()) adapters = self._active_adapters(self.network.get_network_info())
if not adapter_name or adapter_name == ALL_ADAPTERS: if not adapter_name or adapter_name == ALL_ADAPTERS:
return adapters return adapters
return [adapter for adapter in adapters if adapter.get("name") == adapter_name] selected = normalize_adapter_name(adapter_name)
return [adapter for adapter in adapters if normalize_adapter_name(adapter.get("name", "")) == selected]
def _active_adapters(self, adapters: list[dict]) -> list[dict]: def _active_adapters(self, adapters: list[dict]) -> list[dict]:
active = [] active = []
@@ -397,7 +397,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
if not adapter.get("ipv4"): if not adapter.get("ipv4"):
continue continue
status = str(adapter.get("status", "")).lower() status = str(adapter.get("status", "")).lower()
if "disconnect" in status or "断开" in status: if any(value in status for value in ("disconnect", "disabled", "not present", "断开", "禁用")):
continue continue
active.append(adapter) active.append(adapter)
return active return active
@@ -419,7 +419,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
def ping_once(ip: str, timeout_ms: int) -> dict: def ping_once(ip: str, timeout_ms: int) -> dict:
try: try:
result = run_hidden(["ping", ip, "-n", "1", "-w", str(timeout_ms)], timeout=max(2, timeout_ms / 1000 + 2)) result = run_hidden(["ping", ip, "-n", "1", "-w", str(timeout_ms)], timeout=max(1.2, timeout_ms / 1000 + 0.8))
output = result.stdout output = result.stdout
if re.search(r"\bTTL=", output, re.IGNORECASE): if re.search(r"\bTTL=", output, re.IGNORECASE):
match = re.search(r"(?:time|时间)[=<]?\s*(\d+(?:\.\d+)?)\s*(?:ms|毫秒)", output, re.IGNORECASE) match = re.search(r"(?:time|时间)[=<]?\s*(\d+(?:\.\d+)?)\s*(?:ms|毫秒)", output, re.IGNORECASE)
@@ -480,6 +480,49 @@ def adapter_to_safe_range(adapter: dict) -> str:
return ".".join(parts[:3]) + ".0/24" if len(parts) == 4 else "" return ".".join(parts[:3]) + ".0/24" if len(parts) == 4 else ""
def preferred_discovery_adapter(adapters: list[dict]) -> Optional[dict]:
if not adapters:
return None
for adapter in adapters:
if adapter.get("gateway"):
return adapter
for adapter in adapters:
prefix = adapter.get("prefix_length")
try:
if prefix not in ("", None) and int(prefix) <= 24:
return adapter
except (TypeError, ValueError):
continue
return adapters[0]
def filter_targets_for_adapters(targets: list[str], adapters: list[dict]) -> list[str]:
return [target for target in targets if target_in_adapter_network(target, adapters)]
def target_in_adapter_network(target: str, adapters: list[dict]) -> bool:
try:
address = ipaddress.ip_address(target)
except ValueError:
return False
for adapter in adapters:
local_ip = adapter.get("ipv4", "")
if not local_ip:
continue
prefix = adapter.get("prefix_length") or netmask_to_prefix(adapter.get("netmask", "")) or 24
try:
if address in ipaddress.ip_network(f"{local_ip}/{prefix}", strict=False):
return True
except Exception:
continue
return False
def normalize_adapter_name(value: str) -> str:
normalized = unicodedata.normalize("NFKC", str(value or ""))
return re.sub(r"[\s\u200b-\u200d\ufeff]+", " ", normalized).strip().casefold()
def netmask_to_prefix(netmask: str) -> Optional[int]: def netmask_to_prefix(netmask: str) -> Optional[int]:
if not netmask: if not netmask:
return None return None
@@ -499,27 +542,6 @@ def normalize_neighbor(item: dict) -> dict:
} }
def resolve_hostname(ip: str) -> str:
try:
old_timeout = socket.getdefaulttimeout()
socket.setdefaulttimeout(0.8)
try:
return socket.gethostbyaddr(ip)[0]
finally:
socket.setdefaulttimeout(old_timeout)
except Exception:
pass
try:
result = run_hidden(["nbtstat", "-A", ip], timeout=2)
for line in result.stdout.splitlines():
match = re.match(r"\s*([^\s<]+)\s+<00>\s+UNIQUE", line, re.IGNORECASE)
if match:
return match.group(1).strip()
except Exception:
pass
return ""
def vendor_name(mac: str) -> str: def vendor_name(mac: str) -> str:
normalized = normalize_mac(mac) normalized = normalize_mac(mac)
if len(normalized) < 8: if len(normalized) < 8:
+37 -4
View File
@@ -68,7 +68,7 @@ class DnsDiagnostic:
self.done = done or (lambda: None) self.done = done or (lambda: None)
self.status = status or (lambda _stats: None) self.status = status or (lambda _stats: None)
self.result = result or (lambda _row: None) self.result = result or (lambda _row: None)
self.network = NetworkManager(lambda _text, _tag=None: None) self.network = NetworkManager(output)
self.stop_event = threading.Event() self.stop_event = threading.Event()
self.worker = None self.worker = None
self.last_results: list[dict] = [] self.last_results: list[dict] = []
@@ -76,14 +76,35 @@ class DnsDiagnostic:
self.local_dns_servers: set[str] = set() self.local_dns_servers: set[str] = set()
def get_adapter_choices(self) -> list[str]: def get_adapter_choices(self) -> list[str]:
adapters = self._active_adapters(self.network.get_network_info()) adapters = self.network.get_network_info()
return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters] return [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
def get_adapter_choices_and_default_dns(self) -> tuple[list[str], str]:
adapters = self.network.get_network_info()
choices = [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
servers = collect_adapter_dns(self._active_adapters(adapters))
default_dns = ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
return choices, default_dns
def default_dns_servers(self, adapter_name: str = ALL_ADAPTERS) -> str: def default_dns_servers(self, adapter_name: str = ALL_ADAPTERS) -> str:
adapters = self._select_adapters(adapter_name) adapters = self._select_dns_source_adapters(adapter_name)
servers = collect_adapter_dns(adapters) servers = collect_adapter_dns(adapters)
return ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS))) return ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
def repair_abnormal_dns(self, adapter_name: str = ALL_ADAPTERS) -> dict:
if self.is_running():
raise RuntimeError("DNS 诊断正在运行,请先停止当前任务")
adapters = self._select_repair_adapters(adapter_name)
if not adapters:
raise ValueError("没有找到可修复的活动网卡")
servers = PUBLIC_DNS_SERVERS[:2]
for adapter in adapters:
self.network.set_dns_servers(adapter["name"], servers)
self.network.flush_dns_cache()
return {"adapters": [adapter["name"] for adapter in adapters], "servers": servers}
def start_diagnosis(self, adapter_name: str = ALL_ADAPTERS, options: Optional[dict] = None) -> None: def start_diagnosis(self, adapter_name: str = ALL_ADAPTERS, options: Optional[dict] = None) -> None:
if self.is_running(): if self.is_running():
raise RuntimeError("DNS 诊断正在运行,请先停止当前任务") raise RuntimeError("DNS 诊断正在运行,请先停止当前任务")
@@ -302,13 +323,25 @@ class DnsDiagnostic:
return adapters return adapters
return [adapter for adapter in adapters if adapter.get("name") == adapter_name] return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
def _select_dns_source_adapters(self, adapter_name: str) -> list[dict]:
adapters = self.network.get_network_info()
if not adapter_name or adapter_name == ALL_ADAPTERS:
return self._active_adapters(adapters)
return [adapter for adapter in adapters if adapter.get("name") == adapter_name]
def _select_repair_adapters(self, adapter_name: str) -> list[dict]:
adapters = self._select_adapters(adapter_name)
if adapter_name and adapter_name != ALL_ADAPTERS:
return adapters
return [adapter for adapter in adapters if adapter.get("gateway") or adapter.get("dns1") or adapter.get("dns2")]
def _active_adapters(self, adapters: list[dict]) -> list[dict]: def _active_adapters(self, adapters: list[dict]) -> list[dict]:
active = [] active = []
for adapter in adapters: for adapter in adapters:
if not adapter.get("ipv4"): if not adapter.get("ipv4"):
continue continue
status = str(adapter.get("status", "")).lower() status = str(adapter.get("status", "")).lower()
if "disconnect" in status or "断开" in status: if any(value in status for value in ("disconnect", "disabled", "not present", "断开", "禁用")):
continue continue
active.append(adapter) active.append(adapter)
return active return active
+1 -1
View File
@@ -487,7 +487,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
if not adapter.get("ipv4"): if not adapter.get("ipv4"):
continue continue
status = str(adapter.get("status", "")).lower() status = str(adapter.get("status", "")).lower()
if "disconnect" in status or "断开" in status: if any(value in status for value in ("disconnect", "disabled", "not present", "断开", "禁用")):
continue continue
active.append(adapter) active.append(adapter)
return active return active
+1 -1
View File
@@ -353,7 +353,7 @@ Get-NetNeighbor -AddressFamily IPv4 | Select-Object ifIndex,IPAddress,LinkLayerA
if not adapter.get("ipv4"): if not adapter.get("ipv4"):
continue continue
status = str(adapter.get("status", "")).lower() status = str(adapter.get("status", "")).lower()
if "disconnect" in status or "断开" in status: if any(value in status for value in ("disconnect", "disabled", "not present", "断开", "禁用")):
continue continue
active.append(adapter) active.append(adapter)
return active return active
+92 -17
View File
@@ -24,33 +24,55 @@ class NetworkManager:
def get_network_info(self) -> list[dict]: def get_network_info(self) -> list[dict]:
script = r""" script = r"""
$ErrorActionPreference = "SilentlyContinue" $ErrorActionPreference = "SilentlyContinue"
$items = Get-NetIPConfiguration | ForEach-Object { $ipconfigs = @{}
$alias = $_.InterfaceAlias Get-NetIPConfiguration | ForEach-Object { $ipconfigs[[string]$_.InterfaceIndex] = $_ }
$index = $_.InterfaceIndex
$adapter = Get-NetAdapter -InterfaceAlias $alias -ErrorAction SilentlyContinue $ipifs = @{}
$ipif = Get-NetIPInterface -InterfaceAlias $alias -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1 Get-NetIPInterface -AddressFamily IPv4 | ForEach-Object {
$cim = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" -ErrorAction SilentlyContinue | Where-Object { $_.InterfaceIndex -eq $index } | Select-Object -First 1 $key = [string]$_.InterfaceIndex
if (-not $ipifs.ContainsKey($key)) { $ipifs[$key] = $_ }
}
$cims = @{}
Get-CimInstance Win32_NetworkAdapterConfiguration | ForEach-Object {
if ($_.InterfaceIndex -ne $null) { $cims[[string]$_.InterfaceIndex] = $_ }
}
$dnsMap = @{}
Get-DnsClientServerAddress -AddressFamily IPv4 | ForEach-Object {
$dnsMap[[string]$_.InterfaceIndex] = @($_.ServerAddresses)
}
$items = Get-NetAdapter | Sort-Object Name | ForEach-Object {
$adapter = $_
$alias = $adapter.Name
$index = $adapter.ifIndex
$key = [string]$index
$ipconfig = $ipconfigs[$key]
$ipif = $ipifs[$key]
$cim = $cims[$key]
$dns = @($dnsMap[$key])
[PSCustomObject]@{ [PSCustomObject]@{
name = $alias name = $alias
description = $_.InterfaceDescription description = $adapter.InterfaceDescription
mac = if ($adapter) { $adapter.MacAddress } else { "" } mac = $adapter.MacAddress
status = if ($adapter) { [string]$adapter.Status } else { "" } status = [string]$adapter.Status
link_speed = if ($adapter) { [string]$adapter.LinkSpeed } else { "" } link_speed = [string]$adapter.LinkSpeed
interface_index = $index interface_index = $index
ipv4 = @($_.IPv4Address | Select-Object -ExpandProperty IPAddress)[0] ipv4 = @($ipconfig.IPv4Address | Select-Object -ExpandProperty IPAddress)[0]
ipv6 = @($_.IPv6Address | Select-Object -ExpandProperty IPAddress) ipv6 = @($ipconfig.IPv6Address | Select-Object -ExpandProperty IPAddress)
prefix_length = @($_.IPv4Address | Select-Object -ExpandProperty PrefixLength)[0] prefix_length = @($ipconfig.IPv4Address | Select-Object -ExpandProperty PrefixLength)[0]
gateway = @($_.IPv4DefaultGateway | Select-Object -ExpandProperty NextHop)[0] gateway = @($ipconfig.IPv4DefaultGateway | Select-Object -ExpandProperty NextHop)[0]
dns = @($_.DNSServer.ServerAddresses) dns = $dns
dhcp_server = if ($cim) { $cim.DHCPServer } else { "" } dhcp_server = if ($cim) { $cim.DHCPServer } else { "" }
dhcp_lease_obtained = if ($cim) { [string]$cim.DHCPLeaseObtained } else { "" } dhcp_lease_obtained = if ($cim) { [string]$cim.DHCPLeaseObtained } else { "" }
dhcp_lease_expires = if ($cim) { [string]$cim.DHCPLeaseExpires } else { "" } dhcp_lease_expires = if ($cim) { [string]$cim.DHCPLeaseExpires } else { "" }
dhcp_enabled = if ($ipif) { [string]$ipif.Dhcp -eq "Enabled" } else { $false } dhcp_enabled = if ($ipif) { [string]$ipif.Dhcp -eq "Enabled" } elseif ($cim) { [bool]$cim.DHCPEnabled } else { $false }
} }
} }
$items | ConvertTo-Json -Depth 5 -Compress $items | ConvertTo-Json -Depth 5 -Compress
""" """
result = run_hidden(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], timeout=15) result = run_hidden(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], timeout=30)
if result.returncode != 0: if result.returncode != 0:
raise RuntimeError(result.stdout.strip() or "PowerShell 获取网卡信息失败") raise RuntimeError(result.stdout.strip() or "PowerShell 获取网卡信息失败")
@@ -60,6 +82,8 @@ $items | ConvertTo-Json -Depth 5 -Compress
return self._get_network_info_from_ipconfig() return self._get_network_info_from_ipconfig()
data = json.loads(output[json_start:]) data = json.loads(output[json_start:])
if data is None:
return []
if isinstance(data, dict): if isinstance(data, dict):
data = [data] data = [data]
@@ -263,6 +287,54 @@ $items | ConvertTo-Json -Depth 5 -Compress
self.output("静态 IPv4 配置已应用\n", "success") self.output("静态 IPv4 配置已应用\n", "success")
def set_dns_servers(self, name: str, dns_servers: list[str]) -> None:
adapter_name = name.strip()
if not adapter_name:
raise ValueError("请选择网卡")
servers = [validate_ip(server) for server in dns_servers if str(server).strip()]
if not servers:
raise ValueError("请输入至少一个 DNS 服务器")
self._run_netsh(
[
"interface",
"ip",
"set",
"dnsservers",
f"name={adapter_name}",
"source=static",
f"address={servers[0]}",
"index=1",
]
)
for index, server in enumerate(servers[1:], start=2):
self._run_netsh(["interface", "ip", "add", "dnsservers", f"name={adapter_name}", f"address={server}", f"index={index}"])
self.output(f"已设置 DNS: {adapter_name} -> {', '.join(servers)}\n", "success")
def flush_dns_cache(self) -> None:
result = run_hidden(["ipconfig", "/flushdns"], timeout=10)
if result.returncode != 0:
raise RuntimeError(result.stdout.strip() or "刷新 DNS 缓存失败")
self.output("已刷新 DNS 缓存\n", "success")
def set_adapter_enabled(self, name: str, enabled: bool) -> None:
adapter_name = name.strip()
if not adapter_name:
raise ValueError("请选择网卡")
action = "启用" if enabled else "禁用"
self.output(f"准备{action}网卡: {adapter_name}\n", "warning")
command = "Enable-NetAdapter" if enabled else "Disable-NetAdapter"
script = f"""
$ErrorActionPreference = "Stop"
{command} -Name {self._ps_quote(adapter_name)} -Confirm:$false
"""
result = run_hidden(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], timeout=20)
if result.returncode != 0:
raise RuntimeError(result.stdout.strip() or f"{action}网卡失败,请确认已用管理员权限运行")
self.output(f"网卡已{action}: {adapter_name}\n", "success")
def load_profiles(self) -> dict: def load_profiles(self) -> dict:
if not os.path.exists(self.profiles_file): if not os.path.exists(self.profiles_file):
return {} return {}
@@ -301,3 +373,6 @@ $items | ConvertTo-Json -Depth 5 -Compress
if result.returncode != 0: if result.returncode != 0:
message = (result.stdout + result.stderr).strip() message = (result.stdout + result.stderr).strip()
raise RuntimeError(message or "netsh 命令执行失败,请确认已用管理员权限运行") raise RuntimeError(message or "netsh 命令执行失败,请确认已用管理员权限运行")
def _ps_quote(self, value: str) -> str:
return "'" + value.replace("'", "''") + "'"
+98 -32
View File
@@ -1,36 +1,72 @@
import tkinter as tk import tkinter as tk
from tkinter import scrolledtext, ttk from tkinter import ttk
from typing import Callable, Optional from typing import Callable, Optional
from core.ui.theme import COLORS, FONT_MONO, FONT_SECTION, FONT_SMALL from core.ui.theme import COLORS, FONT_MONO, FONT_SMALL
BUTTON_ICONS = {
"应用": "",
"开始": "",
"批量": "",
"测试": "",
"停止": "",
"刷新": "",
"重新": "",
"自动": "",
"启用": "",
"禁用": "",
"修复": "",
"复制": "",
"导出": "",
"导入": "",
"保存": "",
"删除": "×",
}
class Console: class Console:
def __init__(self, parent, height: int = 16): def __init__(self, parent, height: int = 16):
self.widget = scrolledtext.ScrolledText( self.frame = tk.Frame(parent, bg=COLORS["console_bg"], bd=0, highlightthickness=0)
parent, self.frame.rowconfigure(0, weight=1)
self.frame.columnconfigure(0, weight=1)
self.widget = tk.Text(
self.frame,
height=height, height=height,
wrap="word", wrap="word",
bg=COLORS["console_bg"], bg=COLORS["console_bg"],
fg=COLORS["console_fg"], fg=COLORS["console_fg"],
insertbackground=COLORS["console_fg"], insertbackground=COLORS["console_fg"],
selectbackground="#2b4a6f", selectbackground="#17385c",
relief="flat", relief="flat",
borderwidth=0, borderwidth=0,
font=FONT_MONO, font=FONT_MONO,
padx=14, padx=16,
pady=12, pady=14,
) )
self.widget.grid(row=0, column=0, sticky="nsew")
scrollbar = ttk.Scrollbar(
self.frame,
orient="vertical",
command=self.widget.yview,
style="Modern.Vertical.TScrollbar",
)
scrollbar.grid(row=0, column=1, sticky="ns")
self.widget.configure(yscrollcommand=scrollbar.set)
self.widget.tag_config("muted", foreground=COLORS["console_muted"]) self.widget.tag_config("muted", foreground=COLORS["console_muted"])
self.widget.tag_config("accent", foreground=COLORS["console_accent"])
self.widget.tag_config("success", foreground="#86efac") self.widget.tag_config("success", foreground="#86efac")
self.widget.tag_config("warning", foreground="#fde68a") self.widget.tag_config("warning", foreground="#fde68a")
self.widget.tag_config("error", foreground="#fca5a5") self.widget.tag_config("error", foreground="#fca5a5")
def grid(self, **kwargs): def grid(self, **kwargs):
self.widget.grid(**kwargs) self.frame.grid(**kwargs)
def pack(self, **kwargs): def pack(self, **kwargs):
self.widget.pack(**kwargs) self.frame.pack(**kwargs)
def clear(self) -> None: def clear(self) -> None:
self.widget.delete("1.0", tk.END) self.widget.delete("1.0", tk.END)
@@ -45,30 +81,35 @@ class Console:
class Page(ttk.Frame): class Page(ttk.Frame):
def __init__(self, parent, title: str, subtitle: str): def __init__(self, parent, title: str, subtitle: str):
super().__init__(parent, style="Panel.TFrame") super().__init__(parent, style="Page.TFrame")
self.columnconfigure(0, weight=1) self.columnconfigure(0, weight=1)
self.rowconfigure(1, weight=1) self.rowconfigure(1, weight=1)
header = ttk.Frame(self, style="Panel.TFrame") header = ttk.Frame(self, style="Page.TFrame")
header.grid(row=0, column=0, sticky="ew", padx=28, pady=(24, 8)) header.grid(row=0, column=0, sticky="ew", padx=28, pady=(28, 12))
header.columnconfigure(0, weight=1) header.columnconfigure(0, weight=1)
ttk.Label(header, text=title, style="Title.TLabel").grid(row=0, column=0, sticky="w") ttk.Label(header, text=title, style="Title.TLabel").grid(row=0, column=0, sticky="w")
ttk.Label(header, text=subtitle, style="Muted.TLabel").grid(row=1, column=0, sticky="w", pady=(4, 0)) ttk.Label(header, text=subtitle, style="PageMuted.TLabel").grid(row=1, column=0, sticky="w", pady=(8, 0))
body_shell = ttk.Frame(self, style="Panel.TFrame") body_shell = ttk.Frame(self, style="Page.TFrame")
body_shell.grid(row=1, column=0, sticky="nsew", padx=28, pady=(4, 20)) body_shell.grid(row=1, column=0, sticky="nsew", padx=28, pady=(0, 22))
body_shell.columnconfigure(0, weight=1) body_shell.columnconfigure(0, weight=1)
body_shell.rowconfigure(0, weight=1) body_shell.rowconfigure(0, weight=1)
self._canvas = tk.Canvas(body_shell, bg=COLORS["panel"], highlightthickness=0, bd=0) self._canvas = tk.Canvas(body_shell, bg=COLORS["bg"], highlightthickness=0, bd=0)
self._canvas.grid(row=0, column=0, sticky="nsew") self._canvas.grid(row=0, column=0, sticky="nsew")
scrollbar = ttk.Scrollbar(body_shell, orient="vertical", command=self._canvas.yview) self._scrollbar = ttk.Scrollbar(
scrollbar.grid(row=0, column=1, sticky="ns", padx=(8, 0)) body_shell,
self._canvas.configure(yscrollcommand=scrollbar.set) orient="vertical",
command=self._canvas.yview,
style="Modern.Vertical.TScrollbar",
)
self._scrollbar.grid(row=0, column=1, sticky="ns", padx=(8, 0))
self._canvas.configure(yscrollcommand=self._sync_scrollbar)
self.body = ttk.Frame(self._canvas, style="Panel.TFrame") self.body = ttk.Frame(self._canvas, style="Page.TFrame")
self.body.columnconfigure(0, weight=1) self.body.columnconfigure(0, weight=1)
self._body_window = self._canvas.create_window((0, 0), window=self.body, anchor="nw") self._body_window = self._canvas.create_window((0, 0), window=self.body, anchor="nw")
@@ -77,14 +118,22 @@ class Page(ttk.Frame):
self._bind_mousewheel(self._canvas) self._bind_mousewheel(self._canvas)
def section(self, title: str, row: int, columns: int = 4): def section(self, title: str, row: int, columns: int = 4):
frame = tk.Frame(self.body, bg=COLORS["panel"], highlightbackground=COLORS["border"], highlightthickness=1) frame = tk.Frame(
frame.grid(row=row, column=0, sticky="ew", pady=(0, 14)) self.body,
bg=COLORS["panel"],
highlightbackground=COLORS["border"],
highlightcolor=COLORS["border"],
highlightthickness=1,
bd=0,
)
frame.grid(row=row, column=0, sticky="ew", pady=(0, 16))
for col in range(columns): for col in range(columns):
frame.columnconfigure(col, weight=1) frame.columnconfigure(col, weight=1)
ttk.Label(frame, text=title, style="Section.TLabel").grid( title_bar = ttk.Frame(frame, style="Panel.TFrame")
row=0, column=0, columnspan=columns, sticky="w", padx=18, pady=(14, 8) title_bar.grid(row=0, column=0, columnspan=columns, sticky="ew", padx=22, pady=(18, 10))
) ttk.Label(title_bar, text="", style="Section.TLabel", foreground=COLORS["primary"]).pack(side="left", padx=(0, 10))
ttk.Label(title_bar, text=title, style="Section.TLabel").pack(side="left")
return frame return frame
def _update_scroll_region(self, _event=None) -> None: def _update_scroll_region(self, _event=None) -> None:
@@ -93,6 +142,13 @@ class Page(ttk.Frame):
def _resize_body(self, event) -> None: def _resize_body(self, event) -> None:
self._canvas.itemconfigure(self._body_window, width=event.width) self._canvas.itemconfigure(self._body_window, width=event.width)
def _sync_scrollbar(self, first: str, last: str) -> None:
self._scrollbar.set(first, last)
if float(first) <= 0 and float(last) >= 1:
self._scrollbar.grid_remove()
else:
self._scrollbar.grid()
def _bind_mousewheel(self, widget) -> None: def _bind_mousewheel(self, widget) -> None:
widget.bind("<Enter>", lambda _event: widget.bind_all("<MouseWheel>", self._on_mousewheel)) widget.bind("<Enter>", lambda _event: widget.bind_all("<MouseWheel>", self._on_mousewheel))
widget.bind("<Leave>", lambda _event: widget.unbind_all("<MouseWheel>")) widget.bind("<Leave>", lambda _event: widget.unbind_all("<MouseWheel>"))
@@ -105,10 +161,10 @@ class Page(ttk.Frame):
def field(parent, label: str, row: int, column: int, value: str = "", width: int = 24, colspan: int = 1): def field(parent, label: str, row: int, column: int, value: str = "", width: int = 24, colspan: int = 1):
frame = ttk.Frame(parent, style="Panel.TFrame") frame = ttk.Frame(parent, style="Panel.TFrame")
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=18, pady=(4, 14)) frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=22, pady=(4, 16))
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5)) ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7))
var = tk.StringVar(value=value) var = tk.StringVar(value=value)
entry = ttk.Entry(frame, textvariable=var, width=width) entry = ttk.Entry(frame, textvariable=var, width=width)
entry.grid(row=1, column=0, sticky="ew") entry.grid(row=1, column=0, sticky="ew")
@@ -118,10 +174,10 @@ def field(parent, label: str, row: int, column: int, value: str = "", width: int
def combo(parent, label: str, row: int, column: int, values=None, value: str = "", width: int = 24, colspan: int = 1): def combo(parent, label: str, row: int, column: int, values=None, value: str = "", width: int = 24, colspan: int = 1):
values = values or [] values = values or []
frame = ttk.Frame(parent, style="Panel.TFrame") frame = ttk.Frame(parent, style="Panel.TFrame")
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=18, pady=(4, 14)) frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=22, pady=(4, 16))
frame.columnconfigure(0, weight=1) frame.columnconfigure(0, weight=1)
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 5)) ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7))
var = tk.StringVar(value=value) var = tk.StringVar(value=value)
control = ttk.Combobox(frame, textvariable=var, values=values, width=width, state="readonly") control = ttk.Combobox(frame, textvariable=var, values=values, width=width, state="readonly")
control.grid(row=1, column=0, sticky="ew") control.grid(row=1, column=0, sticky="ew")
@@ -130,15 +186,25 @@ def combo(parent, label: str, row: int, column: int, values=None, value: str = "
def action_bar(parent, row: int, columnspan: int = 4): def action_bar(parent, row: int, columnspan: int = 4):
frame = ttk.Frame(parent, style="Panel.TFrame") frame = ttk.Frame(parent, style="Panel.TFrame")
frame.grid(row=row, column=0, columnspan=columnspan, sticky="ew", padx=18, pady=(0, 16)) frame.grid(row=row, column=0, columnspan=columnspan, sticky="ew", padx=22, pady=(0, 18))
return frame return frame
def button(parent, text: str, command: Callable, style: str = "Secondary.TButton"): def button(parent, text: str, command: Callable, style: str = "Secondary.TButton"):
btn = ttk.Button(parent, text=text, command=command, style=style) btn = ttk.Button(parent, text=_button_label(text), command=command, style=style)
btn.pack(side="left", padx=(0, 10)) btn.pack(side="left", padx=(0, 12))
return btn return btn
def _button_label(text: str) -> str:
stripped = text.strip()
if not stripped:
return text
for keyword, icon in BUTTON_ICONS.items():
if stripped.startswith(keyword):
return f"{icon} {stripped}"
return stripped
def set_entry_state(item, enabled: bool) -> None: def set_entry_state(item, enabled: bool) -> None:
item["entry"].configure(state="normal" if enabled else "disabled") item["entry"].configure(state="normal" if enabled else "disabled")
+52 -37
View File
@@ -3,13 +3,14 @@ import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from core.Function.device_discovery_fun import ALL_ADAPTERS, DeviceDiscovery from core.Function.device_discovery_fun import ALL_ADAPTERS, DeviceDiscovery
from core.ui.components import Console, Page, action_bar, button, combo, field from core.ui.components import Page, action_bar, button, combo, field
class DeviceDiscoveryTab(Page): class DeviceDiscoveryTab(Page):
def __init__(self, parent): def __init__(self, parent, console):
super().__init__(parent, "设备发现", "发现局域网在线与 ARP 可见设备,整理 IP、MAC、主机名、厂商与来源网卡。") super().__init__(parent, "设备发现", "发现局域网在线与 ARP 可见设备,整理 IP、MAC、厂商与来源网卡。")
self.body.rowconfigure(3, weight=1) self.console = console
self.body.rowconfigure(2, weight=1)
status = self.section("实时状态", 0, columns=6) status = self.section("实时状态", 0, columns=6)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
@@ -23,31 +24,33 @@ class DeviceDiscoveryTab(Page):
params = self.section("发现参数", 1, columns=6) params = self.section("发现参数", 1, columns=6)
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22) self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 22)
self.adapter["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.fill_default_range(silent=True))
self.scan_range = field(params, "扫描范围", 1, 1, "", 34, colspan=2) self.scan_range = field(params, "扫描范围", 1, 1, "", 34, colspan=2)
self.workers = field(params, "并发数", 1, 3, "64", 10) self.workers = field(params, "并发数", 1, 3, "24", 10)
self.timeout = field(params, "超时 ms", 1, 4, "500", 10) self.timeout = field(params, "超时 ms", 1, 4, "500", 10)
self.max_hosts = field(params, "最大地址数", 1, 5, "254", 10) self.max_hosts = field(params, "最大地址数", 1, 5, "254", 10)
actions = action_bar(params, 2, 6) primary_actions = action_bar(params, 2, 6)
self.start_btn = button(actions, "开始发现", self.start_discovery, "Primary.TButton") self.start_btn = button(primary_actions, "开始发现", self.start_discovery, "Primary.TButton")
self.stop_btn = button(actions, "停止", self.stop_discovery, "Danger.TButton") self.stop_btn = button(primary_actions, "停止", self.stop_discovery, "Danger.TButton")
self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.TButton") self.refresh_btn = button(primary_actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
self.auto_range_btn = button(actions, "自动范围", self.fill_default_range, "Secondary.TButton") self.auto_range_btn = button(primary_actions, "自动范围", self.fill_default_range, "Secondary.TButton")
self.copy_btn = button(actions, "复制清单", self.copy_inventory, "Secondary.TButton") self.stop_btn.configure(state="disabled")
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton") secondary_actions = action_bar(params, 3, 6)
self.copy_btn = button(secondary_actions, "复制清单", self.copy_inventory, "Secondary.TButton")
self.export_btn = button(secondary_actions, "导出CSV", self.export_results, "Secondary.TButton")
results = self.section("发现结果", 2, columns=1) results = self.section("发现结果", 2, columns=1)
results.rowconfigure(1, weight=1) results.rowconfigure(1, weight=1)
results.columnconfigure(0, weight=1) results.columnconfigure(0, weight=1)
self.results_tree = ttk.Treeview( self.results_tree = ttk.Treeview(
results, results,
columns=("ip", "mac", "hostname", "vendor", "adapter", "latency", "method", "note"), columns=("ip", "mac", "vendor", "adapter", "latency", "method", "note"),
show="headings", show="headings",
height=10, height=10,
) )
headings = { headings = {
"ip": "IP", "ip": "IP",
"mac": "MAC", "mac": "MAC",
"hostname": "主机名",
"vendor": "厂商", "vendor": "厂商",
"adapter": "来源网卡", "adapter": "来源网卡",
"latency": "延迟", "latency": "延迟",
@@ -55,14 +58,13 @@ class DeviceDiscoveryTab(Page):
"note": "备注", "note": "备注",
} }
widths = { widths = {
"ip": 130, "ip": 112,
"mac": 150, "mac": 132,
"hostname": 180, "vendor": 120,
"vendor": 130,
"adapter": 150, "adapter": 150,
"latency": 85, "latency": 72,
"method": 90, "method": 80,
"note": 130, "note": 140,
} }
for column, title in headings.items(): for column, title in headings.items():
self.results_tree.heading(column, text=title) self.results_tree.heading(column, text=title)
@@ -73,13 +75,9 @@ class DeviceDiscoveryTab(Page):
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) 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) result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview)
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview)
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18))
output = self.section("发现控制台", 3, columns=1) self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
output.rowconfigure(1, weight=1)
output.columnconfigure(0, weight=1)
self.console = Console(output, height=12)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.discovery = DeviceDiscovery(self.write, self.on_task_done, self.update_status, self.add_result) self.discovery = DeviceDiscovery(self.write, self.on_task_done, self.update_status, self.add_result)
self.after(350, self.load_adapters) self.after(350, self.load_adapters)
@@ -106,48 +104,58 @@ class DeviceDiscoveryTab(Page):
def load_adapters(self): def load_adapters(self):
def worker(): def worker():
try: try:
values = self.discovery.get_adapter_choices() values, default_range, default_adapter = self.discovery.get_adapter_choices_and_default_range()
default_range = self.discovery.default_scan_range(ALL_ADAPTERS)
except Exception as exc: except Exception as exc:
values = [ALL_ADAPTERS] values = [ALL_ADAPTERS]
default_range = "" default_range = ""
default_adapter = ""
self.write(f"读取网卡失败: {exc}\n", "warning") self.write(f"读取网卡失败: {exc}\n", "warning")
self.after(0, lambda: self.apply_adapters(values, default_range)) self.after(0, lambda: self.apply_adapters(values, default_range, default_adapter))
threading.Thread(target=worker, daemon=True).start() threading.Thread(target=worker, daemon=True).start()
def apply_adapters(self, values, default_range): def apply_adapters(self, values, default_range, default_adapter=""):
values = values or [ALL_ADAPTERS] values = values or [ALL_ADAPTERS]
values = list(dict.fromkeys(values)) values = list(dict.fromkeys(values))
self.adapter["combobox"]["values"] = values self.adapter["combobox"]["values"] = values
if self.adapter["var"].get() not in values: if self.adapter["var"].get() not in values or self.adapter["var"].get() == ALL_ADAPTERS:
self.adapter["var"].set(values[1] if len(values) > 1 else values[0]) self.adapter["var"].set(default_adapter if default_adapter in values else values[1] if len(values) > 1 else values[0])
if not self.scan_range["var"].get() and default_range: if not self.scan_range["var"].get() and default_range:
self.scan_range["var"].set(default_range) self.scan_range["var"].set(default_range)
def fill_default_range(self): def fill_default_range(self, silent=False):
try: try:
value = self.discovery.default_scan_range(self.adapter["var"].get()) value = self.discovery.default_scan_range(self.adapter["var"].get())
if not value: if not value:
if not silent:
messagebox.showinfo("提示", "未能根据当前网卡生成扫描范围") messagebox.showinfo("提示", "未能根据当前网卡生成扫描范围")
return return
self.scan_range["var"].set(value) self.scan_range["var"].set(value)
if not silent:
self.write(f"已生成安全扫描范围: {value}\n", "success") self.write(f"已生成安全扫描范围: {value}\n", "success")
except Exception as exc: except Exception as exc:
if not silent:
messagebox.showwarning("生成失败", str(exc)) messagebox.showwarning("生成失败", str(exc))
def start_discovery(self): def start_discovery(self):
try: try:
self.clear() self.clear()
self.start_btn.configure(state="disabled") self.start_btn.configure(state="disabled")
self.stop_btn.configure(state="normal")
self.refresh_btn.configure(state="disabled")
self.auto_range_btn.configure(state="disabled")
self.discovery.start_discovery(self.adapter["var"].get(), self.options()) self.discovery.start_discovery(self.adapter["var"].get(), self.options())
except Exception as exc: except Exception as exc:
self.start_btn.configure(state="normal") self.start_btn.configure(state="normal")
self.stop_btn.configure(state="disabled")
self.refresh_btn.configure(state="normal")
self.auto_range_btn.configure(state="normal")
messagebox.showwarning("无法开始设备发现", str(exc)) messagebox.showwarning("无法开始设备发现", str(exc))
def stop_discovery(self): def stop_discovery(self):
try: try:
self.discovery.stop_discovery() self.discovery.stop_discovery()
self.stop_btn.configure(state="disabled")
except Exception as exc: except Exception as exc:
messagebox.showinfo("提示", str(exc)) messagebox.showinfo("提示", str(exc))
@@ -217,7 +225,6 @@ class DeviceDiscoveryTab(Page):
values=( values=(
row.get("ip", ""), row.get("ip", ""),
row.get("mac", ""), row.get("mac", ""),
row.get("hostname", ""),
row.get("vendor", ""), row.get("vendor", ""),
row.get("adapter", ""), row.get("adapter", ""),
latency_text, latency_text,
@@ -230,4 +237,12 @@ class DeviceDiscoveryTab(Page):
self.after(0, apply) self.after(0, apply)
def on_task_done(self): def on_task_done(self):
self.after(0, lambda: self.start_btn.configure(state="normal")) self.after(
0,
lambda: (
self.start_btn.configure(state="normal"),
self.stop_btn.configure(state="disabled"),
self.refresh_btn.configure(state="normal"),
self.auto_range_btn.configure(state="normal"),
),
)
+56 -21
View File
@@ -3,13 +3,14 @@ import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from core.Function.dns_diag_fun import ALL_ADAPTERS, DEFAULT_DOMAINS, DEFAULT_RECORD_TYPES, DnsDiagnostic from core.Function.dns_diag_fun import ALL_ADAPTERS, DEFAULT_DOMAINS, DEFAULT_RECORD_TYPES, DnsDiagnostic
from core.ui.components import Console, Page, action_bar, button, combo, field from core.ui.components import Page, action_bar, button, combo, field
class DnsTab(Page): class DnsTab(Page):
def __init__(self, parent): def __init__(self, parent, console):
super().__init__(parent, "DNS 诊断", "对比本机 DNS 与常用 DNS 的解析结果、耗时和失败原因。") super().__init__(parent, "DNS 诊断", "对比本机 DNS 与常用 DNS 的解析结果、耗时和失败原因。")
self.body.rowconfigure(3, weight=1) self.console = console
self.body.rowconfigure(2, weight=1)
status = self.section("实时状态", 0, columns=6) status = self.section("实时状态", 0, columns=6)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
@@ -33,6 +34,7 @@ class DnsTab(Page):
self.stop_btn = button(actions, "停止", self.stop_diagnosis, "Danger.TButton") self.stop_btn = button(actions, "停止", self.stop_diagnosis, "Danger.TButton")
self.refresh_btn = button(actions, "刷新网卡", self.load_adapters, "Secondary.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.auto_dns_btn = button(actions, "自动 DNS", self.fill_default_dns, "Secondary.TButton")
self.repair_btn = button(actions, "修复异常", self.repair_dns, "Secondary.TButton")
self.copy_btn = button(actions, "复制摘要", self.copy_summary, "Secondary.TButton") self.copy_btn = button(actions, "复制摘要", self.copy_summary, "Secondary.TButton")
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton") self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
@@ -55,13 +57,13 @@ class DnsTab(Page):
"verdict": "错误 / 判断", "verdict": "错误 / 判断",
} }
widths = { widths = {
"domain": 160, "domain": 130,
"type": 70, "type": 56,
"server": 130, "server": 116,
"status": 80, "status": 64,
"elapsed": 85, "elapsed": 76,
"values": 300, "values": 220,
"verdict": 360, "verdict": 260,
} }
for column, title in headings.items(): for column, title in headings.items():
self.results_tree.heading(column, text=title) self.results_tree.heading(column, text=title)
@@ -72,13 +74,9 @@ class DnsTab(Page):
self.results_tree.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18)) 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) result_scroll = ttk.Scrollbar(results, orient="vertical", command=self.results_tree.yview)
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview)
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18))
output = self.section("诊断控制台", 3, columns=1) self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
output.rowconfigure(1, weight=1)
output.columnconfigure(0, weight=1)
self.console = Console(output, height=12)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result) self.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result)
self.after(350, self.load_adapters) self.after(350, self.load_adapters)
@@ -104,8 +102,7 @@ class DnsTab(Page):
def load_adapters(self): def load_adapters(self):
def worker(): def worker():
try: try:
values = self.diagnostic.get_adapter_choices() values, default_dns = self.diagnostic.get_adapter_choices_and_default_dns()
default_dns = self.diagnostic.default_dns_servers(values[1] if len(values) > 1 else ALL_ADAPTERS)
except Exception as exc: except Exception as exc:
values = [ALL_ADAPTERS] values = [ALL_ADAPTERS]
default_dns = "" default_dns = ""
@@ -119,7 +116,7 @@ class DnsTab(Page):
values = list(dict.fromkeys(values)) values = list(dict.fromkeys(values))
self.adapter["combobox"]["values"] = values self.adapter["combobox"]["values"] = values
if self.adapter["var"].get() not in values: if self.adapter["var"].get() not in values:
self.adapter["var"].set(values[1] if len(values) > 1 else values[0]) self.adapter["var"].set(ALL_ADAPTERS)
if not self.dns_servers["var"].get() and default_dns: if not self.dns_servers["var"].get() and default_dns:
self.dns_servers["var"].set(default_dns) self.dns_servers["var"].set(default_dns)
@@ -138,9 +135,11 @@ class DnsTab(Page):
try: try:
self.clear() self.clear()
self.start_btn.configure(state="disabled") self.start_btn.configure(state="disabled")
self.repair_btn.configure(state="disabled")
self.diagnostic.start_diagnosis(self.adapter["var"].get(), self.options()) self.diagnostic.start_diagnosis(self.adapter["var"].get(), self.options())
except Exception as exc: except Exception as exc:
self.start_btn.configure(state="normal") self.start_btn.configure(state="normal")
self.repair_btn.configure(state="normal")
messagebox.showwarning("无法开始 DNS 诊断", str(exc)) messagebox.showwarning("无法开始 DNS 诊断", str(exc))
def stop_diagnosis(self): def stop_diagnosis(self):
@@ -172,6 +171,42 @@ class DnsTab(Page):
except Exception as exc: except Exception as exc:
messagebox.showwarning("导出失败", str(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): def copy_summary(self):
text = self.diagnostic.copy_summary() text = self.diagnostic.copy_summary()
if not text: if not text:
@@ -226,4 +261,4 @@ class DnsTab(Page):
self.after(0, apply) self.after(0, apply)
def on_task_done(self): def on_task_done(self):
self.after(0, lambda: self.start_btn.configure(state="normal")) self.after(0, lambda: (self.start_btn.configure(state="normal"), self.repair_btn.configure(state="normal")))
+4 -9
View File
@@ -3,13 +3,14 @@ import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from core.Function.ip_conflict_fun import ALL_ADAPTERS, MODE_BOTH, MODE_LOCAL, MODE_SUBNET, IpConflictDetector from core.Function.ip_conflict_fun import ALL_ADAPTERS, MODE_BOTH, MODE_LOCAL, MODE_SUBNET, IpConflictDetector
from core.ui.components import Console, Page, action_bar, button, combo, field from core.ui.components import Page, action_bar, button, combo, field
class IpConflictTab(Page): class IpConflictTab(Page):
def __init__(self, parent): def __init__(self, parent, console):
super().__init__(parent, "IP 冲突", "检测本机 IP 是否被占用,并安全扫描网段内 IP/MAC 异常。") super().__init__(parent, "IP 冲突", "检测本机 IP 是否被占用,并安全扫描网段内 IP/MAC 异常。")
self.body.rowconfigure(3, weight=1) self.console = console
self.body.rowconfigure(2, weight=1)
status = self.section("实时状态", 0, columns=6) status = self.section("实时状态", 0, columns=6)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
@@ -74,12 +75,6 @@ class IpConflictTab(Page):
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) self.results_tree.configure(yscrollcommand=result_scroll.set)
output = self.section("诊断控制台", 3, columns=1)
output.rowconfigure(1, weight=1)
output.columnconfigure(0, weight=1)
self.console = Console(output, height=12)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.detector = IpConflictDetector(self.write, self.on_task_done, self.update_status, self.add_result) self.detector = IpConflictDetector(self.write, self.on_task_done, self.update_status, self.add_result)
self.after(350, self.load_adapters) self.after(350, self.load_adapters)
+4 -9
View File
@@ -3,13 +3,14 @@ import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from core.Function.loop_fun import ALL_ADAPTERS, LoopDetector from core.Function.loop_fun import ALL_ADAPTERS, LoopDetector
from core.ui.components import Console, Page, action_bar, button, combo, field from core.ui.components import Page, action_bar, button, combo, field
class LoopTab(Page): class LoopTab(Page):
def __init__(self, parent): def __init__(self, parent, console):
super().__init__(parent, "环网检测", "基于本机证据判断疑似二层环路、广播风暴和网关抖动风险。") super().__init__(parent, "环网检测", "基于本机证据判断疑似二层环路、广播风暴和网关抖动风险。")
self.body.rowconfigure(3, weight=1) self.console = console
self.body.rowconfigure(2, weight=1)
status = self.section("实时状态", 0, columns=6) status = self.section("实时状态", 0, columns=6)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
@@ -79,12 +80,6 @@ class LoopTab(Page):
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) self.results_tree.configure(yscrollcommand=result_scroll.set)
output = self.section("诊断控制台", 3, columns=1)
output.rowconfigure(1, weight=1)
output.columnconfigure(0, weight=1)
self.console = Console(output, height=12)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.detector = LoopDetector(self.write, self.on_task_done, self.update_status, self.add_result) self.detector = LoopDetector(self.write, self.on_task_done, self.update_status, self.add_result)
self.after(350, self.load_adapters) self.after(350, self.load_adapters)
+65 -9
View File
@@ -2,13 +2,14 @@ import threading
from tkinter import messagebox from tkinter import messagebox
from core.Function.network_fun import NetworkManager from core.Function.network_fun import NetworkManager
from core.ui.components import Console, Page, action_bar, button, combo, field, set_entry_state from core.ui.components import Page, action_bar, button, combo, field, set_entry_state
class NetworkTab(Page): class NetworkTab(Page):
def __init__(self, parent): def __init__(self, parent, console):
super().__init__(parent, "网卡配置", "查看本机网卡信息,切换 DHCP,或写入静态 IPv4 / DNS 配置。") super().__init__(parent, "网卡配置", "查看本机网卡信息,切换 DHCP,或写入静态 IPv4 / DNS 配置。")
self.body.rowconfigure(4, weight=1) self.console = console
self.body.rowconfigure(3, weight=1)
self.adapters = [] self.adapters = []
self.profiles = {} self.profiles = {}
@@ -23,6 +24,9 @@ class NetworkTab(Page):
identity = self.section("网卡信息", 1, columns=4) identity = self.section("网卡信息", 1, columns=4)
self.description = field(identity, "设备描述", 1, 0, "", 46, colspan=2) self.description = field(identity, "设备描述", 1, 0, "", 46, colspan=2)
self.mac = field(identity, "MAC 地址", 1, 2, "", 24) self.mac = field(identity, "MAC 地址", 1, 2, "", 24)
adapter_actions = action_bar(identity, 2, 4)
self.enable_adapter_btn = button(adapter_actions, "启用网卡", self.enable_adapter, "Primary.TButton")
self.disable_adapter_btn = button(adapter_actions, "禁用网卡", self.disable_adapter, "Danger.TButton")
for item in (self.description, self.mac): for item in (self.description, self.mac):
item["entry"].configure(state="disabled") item["entry"].configure(state="disabled")
@@ -46,12 +50,6 @@ class NetworkTab(Page):
self.apply_profile_btn = button(profile_actions, "套用模板到表单", self.apply_profile_to_form, "Secondary.TButton") self.apply_profile_btn = button(profile_actions, "套用模板到表单", self.apply_profile_to_form, "Secondary.TButton")
self.delete_profile_btn = button(profile_actions, "删除模板", self.delete_profile, "Danger.TButton") self.delete_profile_btn = button(profile_actions, "删除模板", self.delete_profile, "Danger.TButton")
output = self.section("输出控制台", 4, columns=1)
output.rowconfigure(1, weight=1)
output.columnconfigure(0, weight=1)
self.console = Console(output, height=12)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.netmgr = NetworkManager(self.write) self.netmgr = NetworkManager(self.write)
self.load_profiles() self.load_profiles()
self.after(250, self.refresh_adapters) self.after(250, self.refresh_adapters)
@@ -62,6 +60,8 @@ class NetworkTab(Page):
def refresh_adapters(self, clear=True): def refresh_adapters(self, clear=True):
self.refresh_btn.configure(state="disabled") self.refresh_btn.configure(state="disabled")
self.reload_btn.configure(state="disabled") self.reload_btn.configure(state="disabled")
self.enable_adapter_btn.configure(state="disabled")
self.disable_adapter_btn.configure(state="disabled")
if clear: if clear:
self.console.clear() self.console.clear()
self.write("正在读取本机网卡信息...\n", "muted") self.write("正在读取本机网卡信息...\n", "muted")
@@ -86,6 +86,7 @@ class NetworkTab(Page):
self.write(f"读取完成,共发现 {len(names)} 个网卡\n", "success") self.write(f"读取完成,共发现 {len(names)} 个网卡\n", "success")
else: else:
self.write("未发现可用网卡\n", "warning") self.write("未发现可用网卡\n", "warning")
self.update_adapter_action_state()
self.refresh_btn.configure(state="normal") self.refresh_btn.configure(state="normal")
self.reload_btn.configure(state="normal") self.reload_btn.configure(state="normal")
@@ -93,6 +94,7 @@ class NetworkTab(Page):
self.write(f"读取网卡失败: {exc}\n", "error") self.write(f"读取网卡失败: {exc}\n", "error")
self.refresh_btn.configure(state="normal") self.refresh_btn.configure(state="normal")
self.reload_btn.configure(state="normal") self.reload_btn.configure(state="normal")
self.update_adapter_action_state()
messagebox.showerror("读取网卡失败", str(exc)) messagebox.showerror("读取网卡失败", str(exc))
def load_selected_adapter(self): def load_selected_adapter(self):
@@ -118,6 +120,7 @@ class NetworkTab(Page):
self.description["entry"].configure(state="disabled") self.description["entry"].configure(state="disabled")
self.mac["entry"].configure(state="disabled") self.mac["entry"].configure(state="disabled")
self.update_entry_state() self.update_entry_state()
self.update_adapter_action_state(adapter)
self.write_current_adapter(adapter) self.write_current_adapter(adapter)
def current_adapter(self): def current_adapter(self):
@@ -132,6 +135,59 @@ class NetworkTab(Page):
for item in (self.ipv4, self.netmask, self.gateway, self.dns1, self.dns2): for item in (self.ipv4, self.netmask, self.gateway, self.dns1, self.dns2):
set_entry_state(item, static) set_entry_state(item, static)
def update_adapter_action_state(self, adapter=None):
adapter = adapter or self.current_adapter()
if not adapter:
self.enable_adapter_btn.configure(state="disabled")
self.disable_adapter_btn.configure(state="disabled")
return
status = str(adapter.get("status", "")).lower()
disabled = "disabled" in status or "禁用" in status
self.enable_adapter_btn.configure(state="normal" if disabled else "disabled")
self.disable_adapter_btn.configure(state="disabled" if disabled else "normal")
def enable_adapter(self):
self.set_adapter_enabled(True)
def disable_adapter(self):
adapter = self.current_adapter()
if not adapter:
messagebox.showwarning("无法禁用网卡", "请先选择网卡")
return
name = adapter.get("name", "")
if not messagebox.askyesno("确认禁用网卡", f"确定禁用网卡“{name}”吗?\n\n这可能会中断当前网络连接。"):
return
self.set_adapter_enabled(False)
def set_adapter_enabled(self, enabled: bool):
adapter = self.current_adapter()
if not adapter:
messagebox.showwarning("无法操作网卡", "请先选择网卡")
return
self.enable_adapter_btn.configure(state="disabled")
self.disable_adapter_btn.configure(state="disabled")
action = "启用" if enabled else "禁用"
def worker():
try:
self.netmgr.set_adapter_enabled(adapter.get("name", ""), enabled)
self.after(0, lambda: self.on_adapter_action_success(action))
except Exception as exc:
self.after(0, lambda: self.on_adapter_action_failed(action, exc))
threading.Thread(target=worker, daemon=True).start()
def on_adapter_action_success(self, action: str):
self.write(f"网卡{action}完成,正在刷新网卡信息...\n", "success")
self.refresh_adapters(clear=False)
def on_adapter_action_failed(self, action: str, exc):
self.write(f"网卡{action}失败: {exc}\n", "error")
self.update_adapter_action_state()
messagebox.showerror(f"网卡{action}失败", f"{exc}\n\n请确认程序已用管理员权限运行。")
def write_current_adapter(self, adapter): def write_current_adapter(self, adapter):
self.write("\n当前网卡:\n", "muted") self.write("\n当前网卡:\n", "muted")
rows = [ rows = [
+4 -9
View File
@@ -5,7 +5,7 @@ from tkinter import filedialog, messagebox, ttk
from core.Function.network_fun import NetworkManager from core.Function.network_fun import NetworkManager
from core.Function.ping_fun import PingFun from core.Function.ping_fun import PingFun
from core.ui.components import Console, Page, action_bar, button, combo, field from core.ui.components import Page, action_bar, button, combo, field
DEFAULT_SOURCE = "默认路由" DEFAULT_SOURCE = "默认路由"
@@ -13,9 +13,10 @@ IP_PATTERN = re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)")
class PingTab(Page): class PingTab(Page):
def __init__(self, parent): def __init__(self, parent, console):
super().__init__(parent, "Ping 探测", "单点 Ping、批量探活、参数化诊断与结果导出。") super().__init__(parent, "Ping 探测", "单点 Ping、批量探活、参数化诊断与结果导出。")
self.body.rowconfigure(2, weight=1) self.console = console
self.body.rowconfigure(1, weight=1)
status = self.section("实时状态", 0, columns=7) status = self.section("实时状态", 0, columns=7)
self.state = field(status, "状态", 1, 0, "等待", 12) self.state = field(status, "状态", 1, 0, "等待", 12)
@@ -38,12 +39,6 @@ class PingTab(Page):
self.build_single_tab() self.build_single_tab()
self.build_batch_tab() self.build_batch_tab()
output = self.section("输出控制台", 2, columns=1)
output.rowconfigure(1, weight=1)
output.columnconfigure(0, weight=1)
self.console = Console(output, height=16)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.ping_fun = PingFun(self.write, self.on_task_done, self.update_status) self.ping_fun = PingFun(self.write, self.on_task_done, self.update_status)
self.source_loader = NetworkManager(lambda _text, _tag=None: None) self.source_loader = NetworkManager(lambda _text, _tag=None: None)
self.after(350, self.load_source_ips) self.after(350, self.load_source_ips)
+4 -9
View File
@@ -2,7 +2,7 @@ import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from core.Function.telnet_fun import PortScanner from core.Function.telnet_fun import PortScanner
from core.ui.components import Console, Page, action_bar, button, combo, field from core.ui.components import Page, action_bar, button, combo, field
PORT_PRESETS = { PORT_PRESETS = {
@@ -16,9 +16,10 @@ PORT_PRESETS = {
class TelnetTab(Page): class TelnetTab(Page):
def __init__(self, parent): def __init__(self, parent, console):
super().__init__(parent, "端口扫描", "单端口测试、端口扫描、批量主机巡检、服务识别与结果导出。") super().__init__(parent, "端口扫描", "单端口测试、端口扫描、批量主机巡检、服务识别与结果导出。")
self.body.rowconfigure(3, weight=1) self.console = console
self.body.rowconfigure(2, weight=1)
status = self.section("实时状态", 0, columns=7) status = self.section("实时状态", 0, columns=7)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
@@ -84,12 +85,6 @@ class TelnetTab(Page):
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) self.results_tree.configure(yscrollcommand=result_scroll.set)
output = self.section("输出控制台", 3, columns=1)
output.rowconfigure(1, weight=1)
output.columnconfigure(0, weight=1)
self.console = Console(output, height=12)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.scanner = PortScanner(self.write, self.on_task_done, self.update_status, self.add_result) self.scanner = PortScanner(self.write, self.on_task_done, self.update_status, self.add_result)
def build_single_tab(self): def build_single_tab(self):
+4 -9
View File
@@ -2,13 +2,14 @@ import tkinter as tk
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
from core.Function.tracert_fun import TracertFun from core.Function.tracert_fun import TracertFun
from core.ui.components import Console, Page, action_bar, button, combo, field from core.ui.components import Page, action_bar, button, combo, field
class TracertTab(Page): class TracertTab(Page):
def __init__(self, parent): def __init__(self, parent, console):
super().__init__(parent, "路由追踪", "结构化查看跳点、延迟、超时、波动和诊断摘要。") super().__init__(parent, "路由追踪", "结构化查看跳点、延迟、超时、波动和诊断摘要。")
self.body.rowconfigure(3, weight=1) self.console = console
self.body.rowconfigure(2, weight=1)
status = self.section("实时状态", 0, columns=7) status = self.section("实时状态", 0, columns=7)
self.state = field(status, "状态", 1, 0, "等待", 10) self.state = field(status, "状态", 1, 0, "等待", 10)
@@ -91,12 +92,6 @@ class TracertTab(Page):
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18)) result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
self.results_tree.configure(yscrollcommand=result_scroll.set) self.results_tree.configure(yscrollcommand=result_scroll.set)
output = self.section("原始输出", 3, columns=1)
output.rowconfigure(1, weight=1)
output.columnconfigure(0, weight=1)
self.console = Console(output, height=12)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
self.tracert_fun = TracertFun(self.write, self.on_task_done, self.update_status, self.add_result) self.tracert_fun = TracertFun(self.write, self.on_task_done, self.update_status, self.add_result)
def _check(self, parent, text, variable, row, column): def _check(self, parent, text, variable, row, column):
+100 -32
View File
@@ -3,30 +3,36 @@ from tkinter import ttk
COLORS = { COLORS = {
"bg": "#eef3f8", "bg": "#f4f7fb",
"panel": "#ffffff", "panel": "#ffffff",
"panel_alt": "#f7f9fc", "panel_alt": "#f8fafc",
"sidebar": "#102033", "topbar": "#071424",
"sidebar_hover": "#1a3552", "sidebar": "#0b1b2f",
"sidebar_active": "#246bfe", "sidebar_hover": "#132943",
"text": "#172033", "sidebar_active": "#2f6df6",
"muted": "#607086", "text": "#0f1f35",
"border": "#dbe4ef", "muted": "#5f7088",
"primary": "#246bfe", "border": "#dce5f0",
"primary_dark": "#1451c8", "border_dark": "#cbd8e6",
"danger": "#df3b3b", "primary": "#2f6df6",
"success": "#1a9b6c", "primary_dark": "#1f56d8",
"primary_soft": "#edf4ff",
"danger": "#ef4444",
"danger_soft": "#fff1f2",
"success": "#16a36a",
"success_soft": "#dcfce7",
"warning": "#b7791f", "warning": "#b7791f",
"console_bg": "#0f1724", "console_bg": "#07111f",
"console_fg": "#dbeafe", "console_fg": "#dbeafe",
"console_muted": "#9fb4d0", "console_muted": "#93a8c5",
"console_accent": "#22d3ee",
} }
FONT_BODY = ("Microsoft YaHei UI", 10) FONT_BODY = ("Microsoft YaHei UI", 10)
FONT_SMALL = ("Microsoft YaHei UI", 9) FONT_SMALL = ("Microsoft YaHei UI", 9)
FONT_TITLE = ("Microsoft YaHei UI", 17, "bold") FONT_TITLE = ("Microsoft YaHei UI", 18, "bold")
FONT_SECTION = ("Microsoft YaHei UI", 11, "bold") FONT_SECTION = ("Microsoft YaHei UI", 12, "bold")
FONT_MONO = ("Consolas", 10) FONT_MONO = ("Consolas", 10)
@@ -43,9 +49,11 @@ def apply_theme(root: tk.Tk) -> None:
style.configure("TFrame", background=COLORS["bg"]) style.configure("TFrame", background=COLORS["bg"])
style.configure("Panel.TFrame", background=COLORS["panel"]) style.configure("Panel.TFrame", background=COLORS["panel"])
style.configure("Alt.TFrame", background=COLORS["panel_alt"]) style.configure("Alt.TFrame", background=COLORS["panel_alt"])
style.configure("Page.TFrame", background=COLORS["bg"])
style.configure("TLabel", background=COLORS["panel"], foreground=COLORS["text"]) style.configure("TLabel", background=COLORS["panel"], foreground=COLORS["text"])
style.configure("Muted.TLabel", background=COLORS["panel"], foreground=COLORS["muted"], font=FONT_SMALL) style.configure("Muted.TLabel", background=COLORS["panel"], foreground=COLORS["muted"], font=FONT_SMALL)
style.configure("Title.TLabel", background=COLORS["panel"], foreground=COLORS["text"], font=FONT_TITLE) style.configure("PageMuted.TLabel", background=COLORS["bg"], foreground=COLORS["muted"], font=FONT_SMALL)
style.configure("Title.TLabel", background=COLORS["bg"], foreground=COLORS["text"], font=FONT_TITLE)
style.configure("Section.TLabel", background=COLORS["panel"], foreground=COLORS["text"], font=FONT_SECTION) style.configure("Section.TLabel", background=COLORS["panel"], foreground=COLORS["text"], font=FONT_SECTION)
style.configure( style.configure(
@@ -55,44 +63,104 @@ def apply_theme(root: tk.Tk) -> None:
bordercolor=COLORS["border"], bordercolor=COLORS["border"],
lightcolor=COLORS["border"], lightcolor=COLORS["border"],
darkcolor=COLORS["border"], darkcolor=COLORS["border"],
padding=7, relief="solid",
padding=(10, 8),
) )
style.map("TEntry", bordercolor=[("focus", COLORS["primary"]), ("disabled", COLORS["border"])])
style.configure( style.configure(
"TCombobox", "TCombobox",
fieldbackground="#ffffff", fieldbackground="#ffffff",
foreground=COLORS["text"], foreground=COLORS["text"],
bordercolor=COLORS["border"], bordercolor=COLORS["border"],
arrowcolor=COLORS["muted"], arrowcolor=COLORS["muted"],
padding=6, relief="solid",
padding=(10, 7),
) )
style.map("TCombobox", bordercolor=[("focus", COLORS["primary"])], fieldbackground=[("readonly", "#ffffff")])
style.configure( style.configure(
"Primary.TButton", "Primary.TButton",
background=COLORS["primary"], background=COLORS["primary"],
foreground="#ffffff", foreground="#ffffff",
borderwidth=0, borderwidth=1,
bordercolor=COLORS["primary"],
focusthickness=0, focusthickness=0,
padding=(14, 8), padding=(16, 9),
)
style.map(
"Primary.TButton",
background=[("active", COLORS["primary_dark"]), ("disabled", "#a9c0f4")],
bordercolor=[("active", COLORS["primary_dark"]), ("disabled", "#a9c0f4")],
foreground=[("disabled", "#eef4ff")],
) )
style.map("Primary.TButton", background=[("active", COLORS["primary_dark"]), ("disabled", "#9bb7f5")])
style.configure( style.configure(
"Secondary.TButton", "Secondary.TButton",
background="#e7eef8", background="#ffffff",
foreground=COLORS["text"], foreground=COLORS["primary"],
borderwidth=0, borderwidth=1,
padding=(14, 8), bordercolor="#bcd0ff",
lightcolor="#bcd0ff",
darkcolor="#bcd0ff",
focusthickness=0,
padding=(16, 9),
)
style.map(
"Secondary.TButton",
background=[("active", COLORS["primary_soft"]), ("disabled", "#f1f5f9")],
foreground=[("disabled", "#9aa8ba")],
bordercolor=[("active", COLORS["primary"]), ("disabled", COLORS["border"])],
) )
style.map("Secondary.TButton", background=[("active", "#d6e2f3")])
style.configure( style.configure(
"Danger.TButton", "Danger.TButton",
background=COLORS["danger"], background="#ffffff",
foreground="#ffffff", foreground=COLORS["danger"],
borderwidth=0, borderwidth=1,
padding=(14, 8), bordercolor="#fca5a5",
lightcolor="#fca5a5",
darkcolor="#fca5a5",
focusthickness=0,
padding=(16, 9),
) )
style.map("Danger.TButton", background=[("active", "#bd2929")]) style.map("Danger.TButton", background=[("active", COLORS["danger_soft"])], bordercolor=[("active", COLORS["danger"])])
style.configure(
"Treeview",
background="#ffffff",
fieldbackground="#ffffff",
foreground=COLORS["text"],
rowheight=30,
bordercolor=COLORS["border"],
borderwidth=1,
)
style.configure(
"Treeview.Heading",
background=COLORS["panel_alt"],
foreground=COLORS["muted"],
font=("Microsoft YaHei UI", 9, "bold"),
relief="flat",
padding=(8, 7),
)
style.map("Treeview", background=[("selected", COLORS["primary_soft"])], foreground=[("selected", COLORS["text"])])
style.configure("TNotebook", background=COLORS["panel"], borderwidth=0)
style.configure("TNotebook.Tab", padding=(18, 8), background=COLORS["panel_alt"], foreground=COLORS["muted"])
style.map("TNotebook.Tab", background=[("selected", "#ffffff")], foreground=[("selected", COLORS["primary"])])
style.configure(
"Modern.Vertical.TScrollbar",
gripcount=0,
background="#d7e0ec",
darkcolor="#d7e0ec",
lightcolor="#d7e0ec",
troughcolor=COLORS["bg"],
bordercolor=COLORS["bg"],
arrowcolor=COLORS["muted"],
relief="flat",
width=10,
)
style.map("Modern.Vertical.TScrollbar", background=[("active", "#bfccdc")])
def build_app_icon(size: int = 64) -> tk.PhotoImage: def build_app_icon(size: int = 64) -> tk.PhotoImage:
+136 -24
View File
@@ -1,6 +1,10 @@
import ctypes
import platform
import socket
import tkinter as tk import tkinter as tk
from tkinter import ttk from tkinter import ttk
from core.ui.components import Console
from core.ui.tab_device_discovery import DeviceDiscoveryTab from core.ui.tab_device_discovery import DeviceDiscoveryTab
from core.ui.tab_dns import DnsTab from core.ui.tab_dns import DnsTab
from core.ui.tab_ip_conflict import IpConflictTab from core.ui.tab_ip_conflict import IpConflictTab
@@ -17,33 +21,80 @@ class MainUI:
self.root = root self.root = root
self.base_dir = base_dir self.base_dir = base_dir
self.root.title("NetPilot 网络调试工具") self.root.title("NetPilot 网络调试工具")
self.root.geometry("1160x720") self.root.geometry("1440x820")
self.root.minsize(1280, 760)
apply_theme(root) apply_theme(root)
self.icon_image = build_app_icon() self.icon_image = build_app_icon()
self.root.iconphoto(True, self.icon_image) self.root.iconphoto(True, self.icon_image)
self.root.columnconfigure(1, weight=1) self.root.columnconfigure(1, weight=1)
self.root.rowconfigure(0, weight=1) self.root.rowconfigure(1, weight=1)
self.sidebar = tk.Frame(root, width=232, bg=COLORS["sidebar"]) self._build_topbar()
self.sidebar.grid(row=0, column=0, sticky="ns")
self.sidebar = tk.Frame(root, width=260, bg=COLORS["sidebar"])
self.sidebar.grid(row=1, column=0, rowspan=2, sticky="ns")
self.sidebar.grid_propagate(False) self.sidebar.grid_propagate(False)
self.content = ttk.Frame(root, style="Panel.TFrame") self.content = ttk.Frame(root, style="Page.TFrame")
self.content.grid(row=0, column=1, sticky="nsew") self.content.grid(row=1, column=1, sticky="nsew")
self.content.rowconfigure(0, weight=1) self.content.rowconfigure(0, weight=1)
self.content.columnconfigure(0, weight=1) self.content.columnconfigure(0, weight=1)
self.console_shell = tk.Frame(root, width=410, bg=COLORS["bg"])
self.console_shell.grid(row=1, column=2, sticky="nsew", padx=(0, 16), pady=(16, 0))
self.console_shell.grid_propagate(False)
self.console_shell.rowconfigure(0, weight=1)
self.console_shell.columnconfigure(0, weight=1)
self.console_panel = tk.Frame(
self.console_shell,
bg=COLORS["panel"],
highlightbackground=COLORS["border"],
highlightcolor=COLORS["border"],
highlightthickness=1,
bd=0,
)
self.console_panel.grid(row=0, column=0, sticky="nsew")
self.console_panel.grid_propagate(False)
self.console_panel.rowconfigure(1, weight=1)
self.console_panel.columnconfigure(0, weight=1)
self._build_console_panel()
self._build_statusbar()
self.pages = {} self.pages = {}
self.nav_buttons = {} self.nav_buttons = {}
self._build_sidebar() self._build_sidebar()
self._build_pages() self._build_pages()
self.show_page("network") self.show_page("network")
def _build_topbar(self) -> None:
bar = tk.Frame(self.root, height=44, bg=COLORS["topbar"])
bar.grid(row=0, column=0, columnspan=3, sticky="ew")
bar.grid_propagate(False)
icon = tk.Label(
bar,
text="NP",
bg=COLORS["primary"],
fg="#ffffff",
font=("Microsoft YaHei UI", 9, "bold"),
width=3,
height=1,
)
icon.pack(side="left", padx=(24, 12), pady=8)
tk.Label(
bar,
text="NetPilot 网络调试工具",
bg=COLORS["topbar"],
fg="#ffffff",
font=("Microsoft YaHei UI", 11, "bold"),
).pack(side="left")
def _build_sidebar(self) -> None: def _build_sidebar(self) -> None:
brand = tk.Frame(self.sidebar, bg=COLORS["sidebar"]) brand = tk.Frame(self.sidebar, bg=COLORS["sidebar"])
brand.pack(fill="x", padx=20, pady=(24, 26)) brand.pack(fill="x", padx=28, pady=(28, 28))
logo = tk.Label( logo = tk.Label(
brand, brand,
@@ -52,14 +103,14 @@ class MainUI:
height=2, height=2,
bg=COLORS["primary"], bg=COLORS["primary"],
fg="#ffffff", fg="#ffffff",
font=("Microsoft YaHei UI", 14, "bold"), font=("Microsoft YaHei UI", 18, "bold"),
) )
logo.pack(side="left") logo.pack(side="left")
title = tk.Frame(brand, bg=COLORS["sidebar"]) title = tk.Frame(brand, bg=COLORS["sidebar"])
title.pack(side="left", padx=12) title.pack(side="left", padx=12)
tk.Label(title, text="NetPilot", bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 16, "bold")).pack(anchor="w") tk.Label(title, text="NetPilot", bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 17, "bold")).pack(anchor="w")
tk.Label(title, text="网络调试控制台", bg=COLORS["sidebar"], fg="#9fb4d0", font=("Microsoft YaHei UI", 9)).pack(anchor="w") tk.Label(title, text="网络调试控制台", bg=COLORS["sidebar"], fg="#9fb4d0", font=("Microsoft YaHei UI", 10)).pack(anchor="w", pady=(4, 0))
items = [ items = [
("network", "", "网卡配置", "IP / DNS / DHCP"), ("network", "", "网卡配置", "IP / DNS / DHCP"),
@@ -85,16 +136,16 @@ class MainUI:
def _nav_button(self, key: str, icon: str, title: str, subtitle: str) -> tk.Frame: def _nav_button(self, key: str, icon: str, title: str, subtitle: str) -> tk.Frame:
frame = tk.Frame(self.sidebar, bg=COLORS["sidebar"], cursor="hand2") frame = tk.Frame(self.sidebar, bg=COLORS["sidebar"], cursor="hand2")
frame.pack(fill="x", padx=14, pady=4) frame.pack(fill="x", padx=16, pady=5)
icon_label = tk.Label(frame, text=icon, width=3, bg=COLORS["sidebar"], fg="#c8d7ec", font=("Microsoft YaHei UI", 18)) icon_label = tk.Label(frame, text=icon, width=3, bg=COLORS["sidebar"], fg="#d9e6f7", font=("Microsoft YaHei UI", 18))
icon_label.pack(side="left", padx=(8, 6), pady=10) icon_label.pack(side="left", padx=(12, 8), pady=12)
text_frame = tk.Frame(frame, bg=COLORS["sidebar"]) text_frame = tk.Frame(frame, bg=COLORS["sidebar"])
text_frame.pack(side="left", fill="x", expand=True) text_frame.pack(side="left", fill="x", expand=True)
title_label = tk.Label(text_frame, text=title, bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 10, "bold")) title_label = tk.Label(text_frame, text=title, bg=COLORS["sidebar"], fg="#ffffff", font=("Microsoft YaHei UI", 11, "bold"))
title_label.pack(anchor="w") title_label.pack(anchor="w")
subtitle_label = tk.Label(text_frame, text=subtitle, bg=COLORS["sidebar"], fg="#9fb4d0", font=("Microsoft YaHei UI", 8)) subtitle_label = tk.Label(text_frame, text=subtitle, bg=COLORS["sidebar"], fg="#a6b7ce", font=("Microsoft YaHei UI", 9))
subtitle_label.pack(anchor="w", pady=(2, 0)) subtitle_label.pack(anchor="w", pady=(2, 0))
widgets = (frame, icon_label, text_frame, title_label, subtitle_label) widgets = (frame, icon_label, text_frame, title_label, subtitle_label)
@@ -107,14 +158,14 @@ class MainUI:
def _build_pages(self) -> None: def _build_pages(self) -> None:
self.pages = { self.pages = {
"network": NetworkTab(self.content), "network": NetworkTab(self.content, self.console),
"dns": DnsTab(self.content), "dns": DnsTab(self.content, self.console),
"ping": PingTab(self.content), "ping": PingTab(self.content, self.console),
"ports": TelnetTab(self.content), "ports": TelnetTab(self.content, self.console),
"trace": TracertTab(self.content), "trace": TracertTab(self.content, self.console),
"loop": LoopTab(self.content), "loop": LoopTab(self.content, self.console),
"ip_conflict": IpConflictTab(self.content), "ip_conflict": IpConflictTab(self.content, self.console),
"devices": DeviceDiscoveryTab(self.content), "devices": DeviceDiscoveryTab(self.content, self.console),
} }
for page in self.pages.values(): for page in self.pages.values():
page.grid(row=0, column=0, sticky="nsew") page.grid(row=0, column=0, sticky="nsew")
@@ -129,8 +180,69 @@ class MainUI:
active = getattr(self, "active_page", None) == key active = getattr(self, "active_page", None) == key
bg = COLORS["sidebar_active"] if active else COLORS["sidebar_hover"] if hover else COLORS["sidebar"] bg = COLORS["sidebar_active"] if active else COLORS["sidebar_hover"] if hover else COLORS["sidebar"]
fg = "#ffffff" if active or hover else "#c8d7ec" fg = "#ffffff" if active or hover else "#c8d7ec"
muted = "#dce9ff" if active else "#9fb4d0" muted = "#dce9ff" if active else "#a6b7ce"
for index, widget in enumerate(widgets): for index, widget in enumerate(widgets):
widget.configure(bg=bg) widget.configure(bg=bg)
if isinstance(widget, tk.Label): if isinstance(widget, tk.Label):
widget.configure(fg=fg if index in (1, 3) else muted) widget.configure(fg=fg if index in (1, 3) else muted)
def _build_console_panel(self) -> None:
header = tk.Frame(self.console_panel, bg=COLORS["panel"])
header.grid(row=0, column=0, sticky="ew", padx=18, pady=(16, 12))
header.columnconfigure(0, weight=1)
tk.Label(
header,
text="输出控制台",
bg=COLORS["panel"],
fg=COLORS["text"],
font=("Microsoft YaHei UI", 13, "bold"),
).grid(row=0, column=0, sticky="w")
ttk.Button(header, text="⌫ 清空", command=lambda: self.console.clear(), style="Secondary.TButton").grid(row=0, column=1, sticky="e")
self.console = Console(self.console_panel)
self.console.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 16))
def _build_statusbar(self) -> None:
status = tk.Frame(
self.root,
height=40,
bg=COLORS["panel"],
highlightbackground=COLORS["border"],
highlightthickness=1,
bd=0,
)
status.grid(row=2, column=1, columnspan=2, sticky="ew")
status.grid_propagate(False)
status.columnconfigure(1, weight=1)
left = tk.Frame(status, bg=COLORS["panel"])
left.grid(row=0, column=0, sticky="w", padx=28, pady=9)
self._status_dot(left)
self._status_label(left, "就绪", color=COLORS["muted"])
self._status_separator(left)
self._status_label(left, "本地连接正常", color=COLORS["muted"])
right = tk.Frame(status, bg=COLORS["panel"])
right.grid(row=0, column=1, sticky="e", padx=28, pady=9)
self._status_label(right, f"本机名:{socket.gethostname()}", color=COLORS["muted"])
self._status_separator(right)
self._status_label(right, f"操作系统:{platform.system()} {platform.release()}", color=COLORS["muted"])
self._status_separator(right)
self._status_label(right, "管理员权限" if self._is_admin() else "普通权限", color=COLORS["muted"])
def _status_dot(self, parent) -> None:
tk.Label(parent, text="", bg=COLORS["panel"], fg="#22c55e", font=("Microsoft YaHei UI", 10)).pack(side="left", padx=(0, 10))
def _status_label(self, parent, text: str, color: str) -> None:
tk.Label(parent, text=text, bg=COLORS["panel"], fg=color, font=("Microsoft YaHei UI", 9)).pack(side="left")
def _status_separator(self, parent) -> None:
tk.Label(parent, text="|", bg=COLORS["panel"], fg=COLORS["border_dark"], font=("Microsoft YaHei UI", 9)).pack(side="left", padx=14)
def _is_admin(self) -> bool:
try:
return bool(ctypes.windll.shell32.IsUserAnAdmin())
except Exception:
return False
+1 -1
View File
@@ -13,7 +13,7 @@ def get_base_dir() -> str:
def main() -> None: def main() -> None:
root = tk.Tk() root = tk.Tk()
root.minsize(960, 600) root.minsize(1200, 640)
MainUI(root, base_dir=get_base_dir()) MainUI(root, base_dir=get_base_dir())
root.mainloop() root.mainloop()