This commit is contained in:
qsc
2026-06-06 01:57:43 +08:00
parent 2a97e1df17
commit 129dc41208
17 changed files with 2582 additions and 1048 deletions
+280 -106
View File
@@ -1,129 +1,303 @@
import subprocess
import locale
import json
import os
import re
import tkinter as tk
from tkinter import scrolledtext, messagebox
import subprocess
import sys
from typing import Callable, Optional
import logging
from core.Function.common import preferred_encoding, prefix_to_netmask, run_hidden, validate_ip
OutputCallback = Callable[[str, Optional[str]], None]
class NetworkManager:
"""获取本地网卡详细信息(含 DNS、DHCP)"""
def __init__(self, result_box: scrolledtext.ScrolledText):
self.result_box = result_box
def __init__(self, output: OutputCallback):
self.output = output
self.profiles_file = os.path.join(self._base_dir(), "network_profiles.json")
def get_network_info(self):
# 自动获取系统编码(例如 'cp936' 中文Windows
system_encoding = locale.getpreferredencoding(False)
# 调用 ipconfig /all
result = subprocess.run(
"ipconfig /all",
shell=True,
capture_output=True,
text=True,
encoding=system_encoding, # 自动根据系统语言选择
errors="ignore" # 忽略解码错误
)
def _base_dir(self) -> str:
if getattr(sys, "frozen", False):
return os.path.dirname(sys.executable)
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
output = result.stdout
if not output:
raise RuntimeError("无法获取 ipconfig 输出,请检查系统命令执行权限。")
def get_network_info(self) -> list[dict]:
script = r"""
$ErrorActionPreference = "SilentlyContinue"
$items = Get-NetIPConfiguration | ForEach-Object {
$alias = $_.InterfaceAlias
$index = $_.InterfaceIndex
$adapter = Get-NetAdapter -InterfaceAlias $alias -ErrorAction SilentlyContinue
$ipif = Get-NetIPInterface -InterfaceAlias $alias -AddressFamily IPv4 -ErrorAction SilentlyContinue | Select-Object -First 1
$cim = Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" -ErrorAction SilentlyContinue | Where-Object { $_.InterfaceIndex -eq $index } | Select-Object -First 1
[PSCustomObject]@{
name = $alias
description = $_.InterfaceDescription
mac = if ($adapter) { $adapter.MacAddress } else { "" }
status = if ($adapter) { [string]$adapter.Status } else { "" }
link_speed = if ($adapter) { [string]$adapter.LinkSpeed } else { "" }
interface_index = $index
ipv4 = @($_.IPv4Address | Select-Object -ExpandProperty IPAddress)[0]
ipv6 = @($_.IPv6Address | Select-Object -ExpandProperty IPAddress)
prefix_length = @($_.IPv4Address | Select-Object -ExpandProperty PrefixLength)[0]
gateway = @($_.IPv4DefaultGateway | Select-Object -ExpandProperty NextHop)[0]
dns = @($_.DNSServer.ServerAddresses)
dhcp_server = if ($cim) { $cim.DHCPServer } else { "" }
dhcp_lease_obtained = if ($cim) { [string]$cim.DHCPLeaseObtained } else { "" }
dhcp_lease_expires = if ($cim) { [string]$cim.DHCPLeaseExpires } else { "" }
dhcp_enabled = if ($ipif) { [string]$ipif.Dhcp -eq "Enabled" } else { $false }
}
}
$items | ConvertTo-Json -Depth 5 -Compress
"""
result = run_hidden(["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], timeout=15)
if result.returncode != 0:
raise RuntimeError(result.stdout.strip() or "PowerShell 获取网卡信息失败")
# 按块拆分
import re
adapter_blocks = re.split(r"\r?\n(?=\S.*?:)", output)
output = result.stdout.strip()
json_start = min([idx for idx in (output.find("["), output.find("{")) if idx >= 0], default=-1)
if json_start < 0:
return self._get_network_info_from_ipconfig()
data = json.loads(output[json_start:])
if isinstance(data, dict):
data = [data]
adapters = []
for block in adapter_blocks:
if not re.search(r"适配器", block):
for item in data:
dns = item.get("dns") or []
if isinstance(dns, str):
dns = [dns]
ipv6 = item.get("ipv6") or []
if isinstance(ipv6, str):
ipv6 = [ipv6]
adapters.append(
{
"name": item.get("name") or "",
"description": item.get("description") or "",
"mac": item.get("mac") or "",
"status": item.get("status") or "",
"link_speed": item.get("link_speed") or "",
"interface_index": item.get("interface_index") or "",
"ipv4": item.get("ipv4") or "",
"ipv6": ipv6,
"prefix_length": item.get("prefix_length") or "",
"netmask": prefix_to_netmask(item.get("prefix_length")),
"gateway": item.get("gateway") or "",
"dns1": dns[0] if len(dns) > 0 else "",
"dns2": dns[1] if len(dns) > 1 else "",
"dhcp_server": item.get("dhcp_server") or "",
"dhcp_lease_obtained": item.get("dhcp_lease_obtained") or "",
"dhcp_lease_expires": item.get("dhcp_lease_expires") or "",
"dhcp_enabled": bool(item.get("dhcp_enabled")),
}
)
return sorted(adapters, key=lambda value: value["name"])
def _get_network_info_from_ipconfig(self) -> list[dict]:
result = run_hidden(["ipconfig", "/all"], timeout=15)
if result.returncode != 0 or not result.stdout.strip():
raise RuntimeError(result.stdout.strip() or "无法读取 ipconfig /all 输出")
adapters = []
current = None
last_key = None
header_pattern = re.compile(r"^(?:\S.* adapter|.+适配器)\s+(.+):$")
unknown_header_pattern = re.compile(r"^(?:Unknown adapter|未知适配器)\s+(.+):$")
value_pattern = re.compile(r"^\s*([^:]+?)\s*:\s*(.*)$")
def push_current():
if current and current.get("name"):
adapters.append(current.copy())
for raw_line in result.stdout.splitlines():
line = raw_line.rstrip()
header = header_pattern.match(line) or unknown_header_pattern.match(line)
if header:
push_current()
current = {
"name": header.group(1).strip(),
"description": "",
"mac": "",
"status": "Disconnected" if "Media disconnected" in line else "",
"link_speed": "",
"interface_index": "",
"ipv4": "",
"ipv6": [],
"prefix_length": "",
"netmask": "",
"gateway": "",
"dns1": "",
"dns2": "",
"dhcp_server": "",
"dhcp_lease_obtained": "",
"dhcp_lease_expires": "",
"dhcp_enabled": False,
}
last_key = None
continue
info = {}
match = re.match(r"(.+?):", block)
if current is None:
continue
if ("Media State" in line and "Media disconnected" in line) or ("媒体状态" in line and "媒体已断开连接" in line):
current["status"] = "Disconnected"
continue
match = value_pattern.match(line)
if match:
info["name"] = match.group(1).strip()
key = match.group(1).replace(".", "").strip()
value = self._clean_ipconfig_value(match.group(2))
last_key = key
self._assign_ipconfig_value(current, key, value)
continue
match = re.search(r"描述.*?:\s*(.+)", block)
if match:
info["description"] = match.group(1).strip()
continuation = line.strip()
if continuation and last_key == "DNS Servers":
self._assign_ipconfig_value(current, last_key, self._clean_ipconfig_value(continuation))
match = re.search(r"物理地址.*?:\s*(.+)", block)
if match:
info["mac"] = match.group(1).strip()
push_current()
return sorted(adapters, key=lambda value: value["name"])
match = re.search(r"DHCP 已启用.*?:\s*(.+)", block)
if match:
value = match.group(1).strip()
info["dhcp_enabled"] = "" if value == "" else ""
def _assign_ipconfig_value(self, adapter: dict, key: str, value: str) -> None:
if key == "Description":
adapter["description"] = value
elif key == "描述":
adapter["description"] = value
elif key == "Physical Address":
adapter["mac"] = value
elif key == "物理地址":
adapter["mac"] = value
elif key == "DHCP Enabled":
adapter["dhcp_enabled"] = value.lower() == "yes" or value == ""
elif key == "DHCP 已启用":
adapter["dhcp_enabled"] = value.lower() == "yes" or value == ""
elif key in ("IPv4 Address", "Autoconfiguration IPv4 Address"):
adapter["ipv4"] = value
if not adapter["status"]:
adapter["status"] = "Up"
elif key in ("IPv4 地址", "自动配置 IPv4 地址"):
adapter["ipv4"] = value
if not adapter["status"]:
adapter["status"] = "Up"
elif key == "Link-local IPv6 Address" or key == "IPv6 Address":
if value:
adapter["ipv6"].append(value)
elif key == "本地链接 IPv6 地址" or key == "IPv6 地址":
if value:
adapter["ipv6"].append(value)
elif key == "Subnet Mask":
adapter["netmask"] = value
elif key == "子网掩码":
adapter["netmask"] = value
elif key == "Default Gateway" and value:
adapter["gateway"] = value
elif key == "默认网关" and value:
adapter["gateway"] = value
elif key == "DNS Servers" and value:
if not adapter["dns1"]:
adapter["dns1"] = value
elif not adapter["dns2"]:
adapter["dns2"] = value
elif key == "DNS 服务器" and value:
if not adapter["dns1"]:
adapter["dns1"] = value
elif not adapter["dns2"]:
adapter["dns2"] = value
elif key == "DHCP Server":
adapter["dhcp_server"] = value
elif key == "DHCP 服务器":
adapter["dhcp_server"] = value
elif key == "Lease Obtained":
adapter["dhcp_lease_obtained"] = value
elif key == "获得租约的时间":
adapter["dhcp_lease_obtained"] = value
elif key == "Lease Expires":
adapter["dhcp_lease_expires"] = value
elif key == "租约过期的时间":
adapter["dhcp_lease_expires"] = value
match = re.search(r"IPv4 地址.*?:\s*([0-9.]+)", block)
if match:
info["ipv4"] = match.group(1)
def _clean_ipconfig_value(self, value: str) -> str:
return re.sub(r"\s*\((?:Preferred|首选)\)\s*$", "", value.strip())
match = re.search(r"子网掩码.*?:\s*([0-9.]+)", block)
if match:
info["netmask"] = match.group(1)
def set_network_info(self, settings: dict) -> None:
name = settings["name"].strip()
if not name:
raise ValueError("请选择网卡")
gateway = ""
# 先找出 "默认网关" 所在行及后续可能的下一行
gw_match = re.search(r"默认网关[.\s:]*([^\r\n]*)\r?\n(?:\s*([^\r\n]+))?", block)
if gw_match:
# 合并两行文本
gw_text = " ".join(gw_match.groups(default=""))
# 提取 IPv4 地址
ipv4_match = re.search(r"\b\d{1,3}(?:\.\d{1,3}){3}\b", gw_text)
if ipv4_match:
gateway = ipv4_match.group(0)
info["gateway"] = gateway
self.output("准备应用网卡配置:\n", "muted")
for key in ("name", "dhcp_enabled", "ipv4", "netmask", "gateway", "dns1", "dns2"):
self.output(f" {key}: {settings.get(key, '')}\n", "muted")
# --- DNS 服务器提取(支持多行 + IPv4优先)---
dns1 = dns2 = ""
# 找出 “DNS 服务器” 开始的位置
dns_match = re.search(r"DNS 服务器[.\s:]*([^\r\n]*)((?:\r?\n\s+[^\r\n]+)*)", block)
if dns_match:
# 合并所有行
dns_text = dns_match.group(1) + dns_match.group(2)
# 提取所有 IPv4 地址(优先),如果没有,再取 IPv6
dns_ipv4 = re.findall(r"\b\d{1,3}(?:\.\d{1,3}){3}\b", dns_text)
if dns_ipv4:
dns1 = dns_ipv4[0]
if len(dns_ipv4) > 1:
dns2 = dns_ipv4[1]
else:
# 没有 IPv4,则尝试 IPv6
dns_ipv6 = re.findall(r"[a-fA-F0-9:]+(?:%[0-9]+)?", dns_text)
if dns_ipv6:
dns1 = dns_ipv6[0]
if len(dns_ipv6) > 1:
dns2 = dns_ipv6[1]
if settings.get("dhcp_enabled"):
self._run_netsh(["interface", "ip", "set", "address", f"name={name}", "source=dhcp"])
self._run_netsh(["interface", "ip", "set", "dnsservers", f"name={name}", "source=dhcp"])
self.output("已切换为 DHCP 自动获取\n", "success")
return
info["dns1"] = dns1
info["dns2"] = dns2
ipv4 = validate_ip(settings.get("ipv4", ""))
netmask = validate_ip(settings.get("netmask", ""))
gateway = validate_ip(settings.get("gateway", ""))
self._run_netsh(
[
"interface",
"ip",
"set",
"address",
f"name={name}",
"source=static",
f"addr={ipv4}",
f"mask={netmask}",
f"gateway={gateway}",
]
)
adapters.append(info)
return adapters
def set_network_info(self, settings):
"""设置指定网卡的网络配置"""
# 自动获取系统编码(例如 'cp936' 中文Windows
system_encoding = locale.getpreferredencoding(False)
try:
self.result_box.insert(tk.END, f"尝试将网卡配置修改为:\n")
for key, value in settings.items():
self.result_box.insert(tk.END, f" {key}: {value}\n")
if settings['dhcp_enabled']:
# 启用 DHCP 自动获取 IP 地址
subprocess.run(
f'netsh interface ip set address name="{settings['name']}" source=dhcp',
shell=True, check=True
)
else:
subprocess.run(
f'netsh interface ip set address name="{settings['name']}" source=static addr={settings['ipv4']} mask={settings['netmask']} gateway={settings['gateway']}',
shell=True, check=True)
self.result_box.insert(tk.END, f"网络配置设置完成\n")
dns1 = validate_ip(settings.get("dns1", ""), allow_empty=True)
dns2 = validate_ip(settings.get("dns2", ""), allow_empty=True)
if dns1:
self._run_netsh(["interface", "ip", "set", "dnsservers", f"name={name}", "source=static", f"address={dns1}", "index=1"])
if dns2:
self._run_netsh(["interface", "ip", "add", "dnsservers", f"name={name}", f"address={dns2}", "index=2"])
else:
self._run_netsh(["interface", "ip", "set", "dnsservers", f"name={name}", "source=dhcp"])
except subprocess.CalledProcessError as e:
self.result_box.insert(tk.END, f"设置{settings['name']}网络配置失败: {e}\n")
self.result_box.see(tk.END)
self.output("静态 IPv4 配置已应用\n", "success")
def load_profiles(self) -> dict:
if not os.path.exists(self.profiles_file):
return {}
with open(self.profiles_file, "r", encoding="utf-8") as file:
data = json.load(file)
return data if isinstance(data, dict) else {}
def save_profile(self, profile_name: str, settings: dict) -> None:
name = profile_name.strip()
if not name:
raise ValueError("请输入模板名称")
profiles = self.load_profiles()
profiles[name] = {
"dhcp_enabled": bool(settings.get("dhcp_enabled")),
"ipv4": settings.get("ipv4", ""),
"netmask": settings.get("netmask", ""),
"gateway": settings.get("gateway", ""),
"dns1": settings.get("dns1", ""),
"dns2": settings.get("dns2", ""),
}
self._write_profiles(profiles)
def delete_profile(self, profile_name: str) -> None:
profiles = self.load_profiles()
if profile_name in profiles:
del profiles[profile_name]
self._write_profiles(profiles)
def _write_profiles(self, profiles: dict) -> None:
with open(self.profiles_file, "w", encoding="utf-8") as file:
json.dump(profiles, file, ensure_ascii=False, indent=2)
def _run_netsh(self, args: list[str]) -> None:
command = ["netsh"] + args
result = subprocess.run(command, capture_output=True, text=True, encoding=preferred_encoding(), errors="replace")
if result.returncode != 0:
message = (result.stdout + result.stderr).strip()
raise RuntimeError(message or "netsh 命令执行失败,请确认已用管理员权限运行")