OK
This commit is contained in:
@@ -1,104 +1,218 @@
|
|||||||
# NetPilot 网络调试工具
|
# NetPilot 网络调试工具
|
||||||
|
|
||||||
NetPilot 是一个基于 Python Tkinter 的 Windows 网络调试工具,面向日常桌面网络排障、网卡配置、局域网资产发现和连通性检测场景。
|
NetPilot 是一个基于 Python Tkinter 的 Windows 网络调试工具,面向桌面网络排障、网卡配置、局域网设备发现、DNS 对比诊断和连通性测试等场景。
|
||||||
|
|
||||||
应用采用左侧功能导航、右侧公共输出控制台的布局。所有功能页的执行日志、诊断过程和错误提示都会统一输出到右侧控制台,便于在不同工具之间切换时保留排障上下文。
|
应用采用左侧功能导航、中间功能工作区、右侧公共输出控制台的布局。所有功能页的执行日志、诊断过程、错误提示和摘要都会统一输出到右侧控制台,便于在不同工具之间切换时保留排障上下文。
|
||||||
|
|
||||||
## 功能概览
|
## 当前功能
|
||||||
|
|
||||||
|
当前界面保留以下功能页:
|
||||||
|
|
||||||
|
- 网卡配置
|
||||||
|
- DNS 诊断
|
||||||
|
- 设备发现
|
||||||
|
- Ping 探测
|
||||||
|
- 端口扫描
|
||||||
|
- 路由追踪
|
||||||
|
|
||||||
|
`IP 冲突检测` 和 `环网检测` 功能页已从主界面移除。
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
### 网卡配置
|
### 网卡配置
|
||||||
|
|
||||||
- 读取本机所有网卡,包括已启用和已禁用的网卡。
|
用于查看本机网卡信息,并对常见 IPv4 / DNS 配置进行调整。
|
||||||
|
|
||||||
|
主要能力:
|
||||||
|
|
||||||
|
- 读取本机启用和禁用的网卡。
|
||||||
- 显示网卡描述、MAC 地址、连接状态、IPv4 地址、子网掩码、默认网关、DNS 等信息。
|
- 显示网卡描述、MAC 地址、连接状态、IPv4 地址、子网掩码、默认网关、DNS 等信息。
|
||||||
- 支持启用网卡、禁用网卡。
|
- 支持启用网卡、禁用网卡。
|
||||||
- 支持 DHCP 自动获取和静态 IPv4 / DNS 配置切换。
|
- 支持 DHCP 自动获取。
|
||||||
- 支持重新读取网卡信息,便于配置变更后快速刷新。
|
- 支持写入静态 IPv4、子网掩码、默认网关、首选 DNS、备用 DNS。
|
||||||
|
- 支持重新读取网卡信息。
|
||||||
- 支持网卡配置模板保存、套用和删除。
|
- 支持网卡配置模板保存、套用和删除。
|
||||||
|
|
||||||
说明:修改网卡状态、IP、网关、DNS 等配置通常需要以管理员权限运行程序。
|
权限说明:
|
||||||
|
|
||||||
|
修改网卡状态、IP、网关、DNS、DHCP 等配置通常需要以管理员权限运行程序。
|
||||||
|
|
||||||
### DNS 诊断
|
### DNS 诊断
|
||||||
|
|
||||||
- 读取本机网卡 DNS 配置,并支持选择不同网卡进行诊断。
|
用于比较本机 DNS 与常用公共 DNS 的解析结果、耗时和异常原因。它能判断“DNS 是否能解析成功”,但不等同于判断目标主机一定能 Ping 通。
|
||||||
- 支持手动填写或自动补全 DNS 服务器。
|
|
||||||
- 支持多个域名、多个记录类型的解析测试。
|
主要能力:
|
||||||
|
|
||||||
|
- 只读取活动网卡用于 DNS 诊断。
|
||||||
|
- 自动读取当前网卡 DNS。
|
||||||
|
- 支持手动填写 DNS 服务器。
|
||||||
|
- 支持自动补全常用 DNS。
|
||||||
|
- 支持多个域名批量测试。
|
||||||
|
- 支持 A、AAAA、CNAME、MX、TXT、NS 等记录类型。
|
||||||
|
- 支持设置重复次数和超时时间。
|
||||||
- 对不同 DNS 服务器的解析结果、耗时、失败原因进行对比。
|
- 对不同 DNS 服务器的解析结果、耗时、失败原因进行对比。
|
||||||
- 诊断结果以表格展示,支持复制摘要和导出 CSV。
|
- 诊断结果以表格展示。
|
||||||
- 提供“修复异常”功能,用于处理常见 DNS 配置异常。
|
- 支持复制诊断摘要。
|
||||||
|
- 支持导出 CSV。
|
||||||
|
- 支持“修复异常”,可将指定网卡 DNS 设置为常用 DNS 并刷新 DNS 缓存。
|
||||||
|
|
||||||
|
注意事项:
|
||||||
|
|
||||||
|
- 域名 A 记录能解析成功,只代表 DNS 返回了 IP。
|
||||||
|
- Ping 不通可能是目标服务器禁 Ping、防火墙拦截、路由不可达、运营商策略、目标只开放 TCP/HTTPS、不响应 ICMP 等原因。
|
||||||
|
- DNS 诊断负责解析层排查,连通性需要结合 Ping 探测、端口扫描、路由追踪继续判断。
|
||||||
|
|
||||||
### 设备发现
|
### 设备发现
|
||||||
|
|
||||||
- 按所选网卡的配置网段进行局域网设备发现。
|
用于按所选网卡的配置网段发现局域网内在线或 ARP 可见设备。
|
||||||
- 通过 Ping 探活和 ARP 信息补充发现设备。
|
|
||||||
- 展示 IP、MAC、厂商、来源网卡、延迟、发现方式和备注。
|
|
||||||
- 支持自动范围、刷新网卡、开始发现、停止发现。
|
|
||||||
- 支持复制设备清单和导出 CSV。
|
|
||||||
|
|
||||||
当前设备发现按配置网段扫描,不再做跨网段邻居表扩展,也不做主机名解析。
|
主要能力:
|
||||||
|
|
||||||
|
- 按当前网卡 IPv4 和子网掩码自动生成扫描范围。
|
||||||
|
- 通过 Ping 探活发现在线设备。
|
||||||
|
- 结合 ARP 信息补充 MAC 地址和 ARP 可见设备。
|
||||||
|
- 展示 IP、MAC、厂商、来源网卡、延迟、发现方式和备注。
|
||||||
|
- 支持刷新网卡、自动范围、开始发现、停止发现。
|
||||||
|
- 支持复制设备清单。
|
||||||
|
- 支持导出 CSV。
|
||||||
|
|
||||||
|
当前扫描边界:
|
||||||
|
|
||||||
|
- 只按所选网卡的配置网段扫描。
|
||||||
|
- 不跨网段扫描。
|
||||||
|
- 不使用邻居表扩展跨网段目标。
|
||||||
|
- 不解析主机名。
|
||||||
|
- 目标设备如果禁用 ICMP Echo,可能无法通过 Ping 发现,但如果本机 ARP 表中可见,仍可能以 ARP 可见方式显示。
|
||||||
|
|
||||||
|
#### 厂商识别和 OUI 数据库
|
||||||
|
|
||||||
|
设备发现里的“厂商”列基于 MAC 地址前缀匹配 OUI 厂商库。
|
||||||
|
|
||||||
|
加载优先级:
|
||||||
|
|
||||||
|
1. `%LOCALAPPDATA%\NetPilot\oui_vendors.json`
|
||||||
|
2. 程序内置的 `assets/oui_vendors_seed.json`
|
||||||
|
3. 代码内的小型兜底表
|
||||||
|
|
||||||
|
更新策略:
|
||||||
|
|
||||||
|
- 程序启动后会在后台检查 OUI 缓存是否过期。
|
||||||
|
- 默认缓存超过 7 天后尝试联网更新。
|
||||||
|
- 更新成功后写入 `%LOCALAPPDATA%\NetPilot\oui_vendors.json`。
|
||||||
|
- 更新失败不会阻塞扫描,只会在右侧输出控制台提示。
|
||||||
|
|
||||||
|
识别规则:
|
||||||
|
|
||||||
|
- 优先匹配 36 bit 前缀。
|
||||||
|
- 其次匹配 28 bit 前缀。
|
||||||
|
- 最后匹配 24 bit 前缀。
|
||||||
|
- 本地管理地址或随机 MAC 会显示为“随机/本地管理地址”。
|
||||||
|
- 没有命中的 MAC 显示为“未知”。
|
||||||
|
|
||||||
### Ping 探测
|
### Ping 探测
|
||||||
|
|
||||||
- 支持单目标 Ping 和批量 Ping。
|
用于检查一个或多个目标的 ICMP 连通性和延迟质量。
|
||||||
|
|
||||||
|
主要能力:
|
||||||
|
|
||||||
|
- 支持单目标 Ping。
|
||||||
|
- 支持批量 Ping。
|
||||||
|
- 批量目标支持 CIDR、IP 范围和多行列表。
|
||||||
- 支持指定次数、持续探测、间隔、超时、包大小、TTL、禁止分片。
|
- 支持指定次数、持续探测、间隔、超时、包大小、TTL、禁止分片。
|
||||||
- 支持选择本地源 IP。
|
- 支持选择本地源 IP。
|
||||||
- 批量目标支持 CIDR、IP 范围和列表输入。
|
- 实时输出探测过程。
|
||||||
- 输出实时探测结果,并统计丢包率、平均延迟、抖动和质量状态。
|
- 统计丢包率、平均延迟、抖动和质量状态。
|
||||||
- 支持导入目标和导出 CSV。
|
- 支持导入目标。
|
||||||
|
- 支持导出 CSV。
|
||||||
|
|
||||||
|
注意事项:
|
||||||
|
|
||||||
|
Ping 依赖 ICMP。目标设备禁 Ping 时,Ping 失败不一定代表 TCP 服务不可用。
|
||||||
|
|
||||||
### 端口扫描
|
### 端口扫描
|
||||||
|
|
||||||
- 支持单主机单端口连通性测试。
|
用于检查 TCP 端口连通性,适合判断服务是否开放。
|
||||||
|
|
||||||
|
主要能力:
|
||||||
|
|
||||||
|
- 支持单主机单端口测试。
|
||||||
- 支持端口列表、端口范围和常用端口预设。
|
- 支持端口列表、端口范围和常用端口预设。
|
||||||
- 支持批量主机扫描,目标可使用 CIDR、IP 范围或列表。
|
- 支持批量主机扫描。
|
||||||
- 支持并发数、超时、仅显示开放端口等控制项。
|
- 批量目标支持 CIDR、IP 范围和多行列表。
|
||||||
- 支持服务名识别和可选 Banner 探测。
|
- 支持并发数、超时、仅显示开放端口。
|
||||||
- 支持复制开放端口和导出 CSV。
|
- 支持服务名识别。
|
||||||
|
- 支持可选 Banner 探测。
|
||||||
|
- 结果表格展示主机、解析 IP、端口、服务、状态、耗时、错误等信息。
|
||||||
|
- 支持复制开放端口。
|
||||||
|
- 支持导出 CSV。
|
||||||
|
|
||||||
|
注意事项:
|
||||||
|
|
||||||
|
端口扫描只判断 TCP 连接层结果。服务协议是否正常,还需要结合业务客户端或协议工具进一步确认。
|
||||||
|
|
||||||
### 路由追踪
|
### 路由追踪
|
||||||
|
|
||||||
- 支持 IPv4 / IPv6 路由追踪。
|
用于查看到目标地址的逐跳路径和延迟情况。
|
||||||
- 支持主机名解析开关。
|
|
||||||
- 支持单次、指定次数和持续追踪。
|
主要能力:
|
||||||
|
|
||||||
|
- 支持 IPv4 / IPv6。
|
||||||
|
- 支持单次追踪、指定次数追踪和持续追踪。
|
||||||
- 支持最大跳数、单跳超时、高延迟阈值等参数。
|
- 支持最大跳数、单跳超时、高延迟阈值等参数。
|
||||||
- 展示逐跳 RTT 表格,并保留原始命令输出。
|
- 展示每一跳 IP、探测耗时、平均延迟、抖动和状态。
|
||||||
- 对超时、高延迟、波动等情况给出诊断摘要。
|
- 保留原始命令输出。
|
||||||
- 支持复制摘要和导出 CSV。
|
- 对超时、高延迟、波动等情况生成诊断摘要。
|
||||||
|
- 支持复制摘要。
|
||||||
|
- 支持导出 CSV。
|
||||||
|
|
||||||
### 环网检测
|
注意事项:
|
||||||
|
|
||||||
- 基于本机网卡统计、广播 / 组播流量、ARP / 邻居表变化和网关 Ping 稳定性进行综合判断。
|
部分路由节点会屏蔽 ICMP 或限制 TTL Exceeded 响应,因此某些跳点超时不一定代表链路中断。应重点关注最后目标是否可达,以及连续多跳异常的位置。
|
||||||
- 用于辅助发现疑似二层环路、广播风暴或局域网异常波动。
|
|
||||||
- 支持检测单个网卡或全部活动网卡。
|
|
||||||
- 输出诊断摘要,支持复制和导出 CSV。
|
|
||||||
|
|
||||||
### IP 冲突
|
## 界面布局
|
||||||
|
|
||||||
- 支持检测本机 IP 冲突风险。
|
- 左侧:功能导航区。
|
||||||
- 支持按网段进行安全限速扫描。
|
- 中间:当前功能页的参数、操作按钮和结果列表。
|
||||||
- 结合地址状态、系统事件、ARP / 邻居信息、IP / MAC 异常关系判断冲突风险。
|
- 右侧:公共输出控制台,所有功能页共用。
|
||||||
- 支持自动范围、刷新网卡、复制摘要和导出 CSV。
|
- 底部:状态栏,显示当前状态、本机名、操作系统和权限状态。
|
||||||
|
|
||||||
## 界面说明
|
公共输出控制台用于查看执行过程、错误提示、更新信息和诊断摘要。点击“清空”可以清除当前控制台内容。
|
||||||
|
|
||||||
- 左侧为功能导航区。
|
|
||||||
- 中间为当前功能页的参数和结果区域。
|
|
||||||
- 右侧为公共输出控制台,所有页面共用。
|
|
||||||
- 底部状态栏显示当前状态、本机名、操作系统和权限状态。
|
|
||||||
|
|
||||||
公共控制台可用于查看执行过程、错误提示和诊断摘要;点击“清空”可以清除当前控制台内容。
|
|
||||||
|
|
||||||
## 运行环境
|
## 运行环境
|
||||||
|
|
||||||
|
建议环境:
|
||||||
|
|
||||||
- 操作系统:Windows 10 / Windows 11
|
- 操作系统:Windows 10 / Windows 11
|
||||||
- Python:建议 Python 3.10 或更高版本
|
- Python:Python 3.10 或更高版本
|
||||||
- UI 框架:Tkinter
|
- UI 框架:Tkinter
|
||||||
- 系统依赖:PowerShell、Windows 网络相关命令
|
- 系统依赖:PowerShell、Windows 网络相关命令
|
||||||
|
|
||||||
大部分只读诊断功能可以普通权限运行。涉及以下操作时建议使用管理员权限:
|
项目主要调用 Windows 系统命令获取网络信息,例如:
|
||||||
|
|
||||||
- 启用或禁用网卡
|
- `Get-NetAdapter`
|
||||||
- 修改 IP、网关、DNS、DHCP
|
- `Get-NetIPConfiguration`
|
||||||
- 执行部分需要系统网络信息的诊断或修复动作
|
- `netsh`
|
||||||
|
- `ipconfig`
|
||||||
|
- `arp`
|
||||||
|
- `ping`
|
||||||
|
- `tracert`
|
||||||
|
|
||||||
|
## 权限说明
|
||||||
|
|
||||||
|
普通权限可以运行大部分只读诊断功能。
|
||||||
|
|
||||||
|
以下操作建议使用管理员权限:
|
||||||
|
|
||||||
|
- 启用网卡
|
||||||
|
- 禁用网卡
|
||||||
|
- 修改 IP 地址
|
||||||
|
- 修改子网掩码
|
||||||
|
- 修改默认网关
|
||||||
|
- 修改 DNS
|
||||||
|
- 切换 DHCP
|
||||||
|
- 执行 DNS 修复
|
||||||
|
|
||||||
|
如果按钮不可用或操作失败,优先确认程序是否以管理员身份运行。
|
||||||
|
|
||||||
## 源码运行
|
## 源码运行
|
||||||
|
|
||||||
@@ -108,7 +222,7 @@ NetPilot 是一个基于 Python Tkinter 的 Windows 网络调试工具,面向
|
|||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
如果需要管理员权限,可以使用管理员 PowerShell 进入项目目录后再执行:
|
如果需要管理员权限,可以打开管理员 PowerShell 后执行:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
cd E:\Git\Network-tools
|
cd E:\Git\Network-tools
|
||||||
@@ -117,7 +231,7 @@ python main.py
|
|||||||
|
|
||||||
## 打包
|
## 打包
|
||||||
|
|
||||||
项目提供了 Windows 打包脚本:
|
项目提供 Windows 打包脚本:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\打包.bat
|
.\打包.bat
|
||||||
@@ -129,27 +243,38 @@ python main.py
|
|||||||
dist\NetworkTool.exe
|
dist\NetworkTool.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
打包脚本会优先使用本机指定的 Python 运行时,并在 `.packaging\pyinstaller` 下安装 PyInstaller;如果指定运行时不存在,则尝试直接调用系统中的 `pyinstaller`。
|
打包脚本会将 OUI 离线厂商库一并打入程序:
|
||||||
|
|
||||||
|
```text
|
||||||
|
assets\oui_vendors_seed.json
|
||||||
|
```
|
||||||
|
|
||||||
|
这样即使目标电脑没有互联网,设备发现功能也可以使用内置 OUI 种子库识别常见厂商。
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Network-tools
|
Network-tools
|
||||||
├─ assets # 图标等静态资源
|
├─ assets
|
||||||
|
│ └─ oui_vendors_seed.json # 内置 OUI 厂商种子库
|
||||||
├─ core
|
├─ core
|
||||||
│ ├─ Function # 网络检测和配置相关业务逻辑
|
│ ├─ Function # 网络检测和配置业务逻辑
|
||||||
│ └─ ui # Tkinter 界面、主题和各功能页
|
│ └─ ui # Tkinter 界面、主题和功能页
|
||||||
├─ main.py # 程序入口
|
├─ main.py # 程序入口
|
||||||
├─ 打包.bat # PyInstaller 打包脚本
|
├─ 打包.bat # PyInstaller 打包脚本
|
||||||
├─ NetworkTool.spec # PyInstaller 配置
|
├─ NetworkTool.spec # PyInstaller 配置
|
||||||
└─ README.md
|
└─ README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## 配置和导出文件
|
## 本地文件
|
||||||
|
|
||||||
- 网卡配置模板会保存到程序同级目录的 `network_profiles.json`。
|
程序运行过程中可能产生以下本地文件:
|
||||||
- 各功能页的 CSV 导出文件由用户在导出时选择保存路径。
|
|
||||||
- 程序不会主动上传网络检测结果,所有诊断和导出均在本机完成。
|
- `%LOCALAPPDATA%\NetPilot\oui_vendors.json`:联网更新后的 OUI 厂商缓存。
|
||||||
|
- `network_profiles.json`:网卡配置模板,保存位置为程序运行目录。
|
||||||
|
- CSV 导出文件:由用户在导出时选择保存路径。
|
||||||
|
|
||||||
|
程序不会主动上传网络检测结果。诊断、缓存和导出均在本机完成。
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|
||||||
@@ -157,25 +282,53 @@ Network-tools
|
|||||||
|
|
||||||
请确认程序以管理员权限运行。Windows 对网卡启用/禁用、IP 配置、DNS 配置等操作有权限限制。
|
请确认程序以管理员权限运行。Windows 对网卡启用/禁用、IP 配置、DNS 配置等操作有权限限制。
|
||||||
|
|
||||||
|
### 为什么 DNS 能解析成功,但 Ping 不通?
|
||||||
|
|
||||||
|
DNS 解析成功只说明域名能转换成 IP。Ping 依赖 ICMP,目标服务器可能禁用了 ICMP,或者链路中有防火墙、运营商策略、路由策略阻止 ICMP。
|
||||||
|
|
||||||
|
排查建议:
|
||||||
|
|
||||||
|
- 用 DNS 诊断确认解析结果是否正常。
|
||||||
|
- 用 Ping 探测确认 ICMP 是否可达。
|
||||||
|
- 用端口扫描测试目标业务端口,例如 80、443、3389、502 等。
|
||||||
|
- 用路由追踪查看路径中是否存在连续超时或高延迟跳点。
|
||||||
|
|
||||||
### 为什么设备发现扫不到某些设备?
|
### 为什么设备发现扫不到某些设备?
|
||||||
|
|
||||||
常见原因包括:
|
常见原因包括:
|
||||||
|
|
||||||
- 目标设备禁用了 ICMP Echo 响应,无法被 Ping 探活。
|
- 目标设备禁用了 ICMP Echo 响应。
|
||||||
- 目标设备与当前网卡不在同一配置网段内。
|
- 目标设备不在当前网卡配置网段内。
|
||||||
- 本机防火墙或安全软件限制了探测行为。
|
- 本机防火墙或安全软件限制了探测行为。
|
||||||
- 交换机、VLAN、无线隔离或终端防火墙阻止了局域网发现。
|
- 交换机、VLAN、无线隔离或终端防火墙阻止了局域网发现。
|
||||||
- 扫描期间目标设备未产生可见 ARP 信息。
|
- 目标设备未产生可见 ARP 信息。
|
||||||
|
- 当前选择的网卡不是实际通信所用网卡。
|
||||||
|
|
||||||
当前设备发现按所选网卡的配置网段扫描,不会跨网段扫描,也不会解析主机名。
|
当前设备发现只按所选网卡的配置网段扫描,不跨网段,也不解析主机名。
|
||||||
|
|
||||||
|
### 为什么厂商显示“未知”?
|
||||||
|
|
||||||
|
可能原因:
|
||||||
|
|
||||||
|
- MAC 地址为空。
|
||||||
|
- MAC 地址是随机地址或本地管理地址。
|
||||||
|
- OUI 厂商库中没有该前缀。
|
||||||
|
- 本地缓存损坏且离线种子库未正确打包。
|
||||||
|
- 设备使用了虚拟网卡、代理网关或桥接设备暴露的 MAC。
|
||||||
|
|
||||||
|
可以查看右侧输出控制台,确认是否出现 OUI 厂商库加载或更新提示。
|
||||||
|
|
||||||
|
### 为什么部分手机显示“随机/本地管理地址”?
|
||||||
|
|
||||||
|
很多手机系统会默认开启随机 MAC 或私有 Wi-Fi 地址。此时 MAC 前缀不是厂商真实注册地址,无法通过 OUI 准确识别手机品牌。
|
||||||
|
|
||||||
### 为什么 DNS 诊断的网卡数量和系统里看到的不一样?
|
### 为什么 DNS 诊断的网卡数量和系统里看到的不一样?
|
||||||
|
|
||||||
程序会尽量读取启用和禁用网卡,但部分虚拟网卡、隧道网卡、隐藏适配器或驱动异常的网卡,可能无法提供完整 DNS/IP 信息。可以先在网卡配置页刷新网卡,确认目标网卡是否能被系统命令读取。
|
DNS 诊断只使用活动网卡。禁用网卡、隐藏适配器、隧道网卡、部分虚拟网卡即使能在系统里看到,也可能不会进入 DNS 诊断列表。
|
||||||
|
|
||||||
### 为什么部分按钮是灰色不可用?
|
### 为什么部分按钮是灰色不可用?
|
||||||
|
|
||||||
按钮状态会根据当前任务状态和网卡状态变化。例如扫描正在运行时会禁用开始按钮;没有选择有效网卡或当前网卡状态不支持某项操作时,也会禁用对应按钮。
|
按钮状态会根据当前任务状态、网卡状态和输入内容变化。例如任务运行中会禁用开始按钮;未选择有效网卡、缺少目标、当前状态不支持某项操作时,也会禁用对应按钮。
|
||||||
|
|
||||||
### 导出的 CSV 用 Excel 打开乱码怎么办?
|
### 导出的 CSV 用 Excel 打开乱码怎么办?
|
||||||
|
|
||||||
@@ -188,20 +341,24 @@ Network-tools
|
|||||||
- `ui_main.py`:主窗口布局、侧边栏、公共输出控制台、页面切换。
|
- `ui_main.py`:主窗口布局、侧边栏、公共输出控制台、页面切换。
|
||||||
- `theme.py`:全局颜色、字体、ttk 样式。
|
- `theme.py`:全局颜色、字体、ttk 样式。
|
||||||
- `components.py`:公共 Page、Console、字段、按钮等组件。
|
- `components.py`:公共 Page、Console、字段、按钮等组件。
|
||||||
- `tab_*.py`:各功能页 UI 和事件逻辑。
|
- `tab_network.py`:网卡配置页。
|
||||||
|
- `tab_dns.py`:DNS 诊断页。
|
||||||
|
- `tab_device_discovery.py`:设备发现页。
|
||||||
|
- `tab_ping.py`:Ping 探测页。
|
||||||
|
- `tab_telnet.py`:端口扫描页。
|
||||||
|
- `tab_tracert.py`:路由追踪页。
|
||||||
|
|
||||||
业务逻辑主要位于 `core/Function`:
|
业务逻辑主要位于 `core/Function`:
|
||||||
|
|
||||||
- `network_fun.py`:网卡读取和配置。
|
- `network_fun.py`:网卡读取、启用、禁用和配置写入。
|
||||||
- `dns_diag_fun.py`:DNS 诊断。
|
- `dns_diag_fun.py`:DNS 解析诊断、摘要和导出。
|
||||||
- `device_discovery_fun.py`:设备发现。
|
- `device_discovery_fun.py`:设备发现、ARP 补充、结果整理和导出。
|
||||||
|
- `oui_lookup.py`:OUI 厂商库加载、查询和联网更新。
|
||||||
- `ping_fun.py`:Ping 探测。
|
- `ping_fun.py`:Ping 探测。
|
||||||
- `telnet_fun.py`:端口连通性和扫描。
|
- `telnet_fun.py`:TCP 连通性和端口扫描。
|
||||||
- `tracert_fun.py`:路由追踪。
|
- `tracert_fun.py`:路由追踪。
|
||||||
- `loop_fun.py`:环网检测。
|
|
||||||
- `ip_conflict_fun.py`:IP 冲突检测。
|
|
||||||
|
|
||||||
开发时建议先运行:
|
开发时建议先执行编译检查:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
python -m compileall main.py core
|
python -m compileall main.py core
|
||||||
@@ -212,3 +369,17 @@ python -m compileall main.py core
|
|||||||
```powershell
|
```powershell
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 验证重点
|
||||||
|
|
||||||
|
改动功能后建议重点验证:
|
||||||
|
|
||||||
|
- 主界面是否能正常启动。
|
||||||
|
- 左侧导航是否只显示当前保留的功能页。
|
||||||
|
- 公共输出控制台是否能接收各页面日志。
|
||||||
|
- 网卡配置是否能正确读取启用和禁用网卡。
|
||||||
|
- DNS 诊断是否只列出活动网卡。
|
||||||
|
- 设备发现是否能根据所选网卡生成正确扫描范围。
|
||||||
|
- OUI 厂商库是否能加载缓存或离线种子库。
|
||||||
|
- 各结果列表是否无横向滚动条,且关键列内容可见。
|
||||||
|
- 打包后的 `dist\NetworkTool.exe` 是否能在无网络环境下启动并读取内置 OUI 种子库。
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -12,6 +12,7 @@ from typing import Callable, Optional
|
|||||||
from core.Function.common import run_hidden
|
from core.Function.common import run_hidden
|
||||||
from core.Function.loop_fun import normalize_mac
|
from core.Function.loop_fun import normalize_mac
|
||||||
from core.Function.network_fun import NetworkManager
|
from core.Function.network_fun import NetworkManager
|
||||||
|
from core.Function.oui_lookup import OuiVendorLookup
|
||||||
|
|
||||||
|
|
||||||
OutputCallback = Callable[[str, Optional[str]], None]
|
OutputCallback = Callable[[str, Optional[str]], None]
|
||||||
@@ -63,6 +64,9 @@ OUI_VENDOR_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
OUI_LOOKUP = OuiVendorLookup(OUI_VENDOR_MAP)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class DeviceDiscoveryOptions:
|
class DeviceDiscoveryOptions:
|
||||||
scan_range: str = ""
|
scan_range: str = ""
|
||||||
@@ -106,6 +110,8 @@ class DeviceDiscovery:
|
|||||||
self.worker = None
|
self.worker = None
|
||||||
self.last_results: list[dict] = []
|
self.last_results: list[dict] = []
|
||||||
self.last_summary = ""
|
self.last_summary = ""
|
||||||
|
OUI_LOOKUP.load(self.output)
|
||||||
|
OUI_LOOKUP.update_if_stale_async(self.output)
|
||||||
|
|
||||||
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._active_adapters(self.network.get_network_info())
|
||||||
@@ -543,10 +549,7 @@ def normalize_neighbor(item: dict) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def vendor_name(mac: str) -> str:
|
def vendor_name(mac: str) -> str:
|
||||||
normalized = normalize_mac(mac)
|
return OUI_LOOKUP.lookup(mac)
|
||||||
if len(normalized) < 8:
|
|
||||||
return "未知"
|
|
||||||
return OUI_VENDOR_MAP.get(normalized[:8], "未知")
|
|
||||||
|
|
||||||
|
|
||||||
def build_summary(rows: list[dict], stopped: bool = False) -> str:
|
def build_summary(rows: list[dict], stopped: bool = False) -> str:
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ 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.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_dns(self) -> tuple[list[str], str]:
|
def get_adapter_choices_and_default_dns(self) -> tuple[list[str], str]:
|
||||||
adapters = self.network.get_network_info()
|
adapters = self._active_adapters(self.network.get_network_info())
|
||||||
choices = [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
choices = [ALL_ADAPTERS] + [adapter["name"] for adapter in adapters]
|
||||||
servers = collect_adapter_dns(self._active_adapters(adapters))
|
servers = collect_adapter_dns(adapters)
|
||||||
default_dns = ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
|
default_dns = ",".join(list(dict.fromkeys(servers + PUBLIC_DNS_SERVERS)))
|
||||||
return choices, default_dns
|
return choices, default_dns
|
||||||
|
|
||||||
@@ -324,9 +324,9 @@ class DnsDiagnostic:
|
|||||||
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]:
|
def _select_dns_source_adapters(self, adapter_name: str) -> list[dict]:
|
||||||
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 self._active_adapters(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_repair_adapters(self, adapter_name: str) -> list[dict]:
|
def _select_repair_adapters(self, adapter_name: str) -> list[dict]:
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
|
||||||
|
OutputCallback = Callable[[str, Optional[str]], None]
|
||||||
|
|
||||||
|
|
||||||
|
IEEE_OUI_SOURCES = [
|
||||||
|
("MA-L", "https://standards-oui.ieee.org/oui/oui.csv"),
|
||||||
|
("MA-M", "https://standards-oui.ieee.org/oui28/mam.csv"),
|
||||||
|
("MA-S", "https://standards-oui.ieee.org/oui36/oui36.csv"),
|
||||||
|
]
|
||||||
|
UPDATE_INTERVAL_SECONDS = 7 * 24 * 60 * 60
|
||||||
|
UNKNOWN_VENDOR = "未知"
|
||||||
|
LOCAL_ADMIN_VENDOR = "随机/本地管理地址"
|
||||||
|
|
||||||
|
|
||||||
|
class OuiVendorLookup:
|
||||||
|
def __init__(self, fallback_map: Optional[dict[str, str]] = None):
|
||||||
|
self.fallback_map = {normalize_prefix(key): value for key, value in (fallback_map or {}).items()}
|
||||||
|
self.vendors: dict[str, str] = {}
|
||||||
|
self.source = "未加载"
|
||||||
|
self.updated_at = 0.0
|
||||||
|
self._loaded = False
|
||||||
|
self._update_started = False
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
|
||||||
|
def load(self, output: Optional[OutputCallback] = None) -> None:
|
||||||
|
with self._lock:
|
||||||
|
if self._loaded:
|
||||||
|
return
|
||||||
|
|
||||||
|
loaded = self._load_json_file(cache_file())
|
||||||
|
if loaded:
|
||||||
|
self.vendors, self.updated_at = loaded
|
||||||
|
self.source = "本地缓存"
|
||||||
|
self._loaded = True
|
||||||
|
write(output, f"已加载 OUI 厂商缓存:{len(self.vendors)} 条\n", "muted")
|
||||||
|
return
|
||||||
|
|
||||||
|
loaded = self._load_json_file(seed_file())
|
||||||
|
if loaded:
|
||||||
|
self.vendors, self.updated_at = loaded
|
||||||
|
self.source = "离线内置"
|
||||||
|
self._loaded = True
|
||||||
|
write(output, f"已加载离线 OUI 厂商库:{len(self.vendors)} 条\n", "muted")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.vendors = dict(self.fallback_map)
|
||||||
|
self.updated_at = 0.0
|
||||||
|
self.source = "内置兜底"
|
||||||
|
self._loaded = True
|
||||||
|
write(output, f"已加载内置 OUI 兜底表:{len(self.vendors)} 条\n", "muted")
|
||||||
|
|
||||||
|
def lookup(self, mac: str) -> str:
|
||||||
|
self.load()
|
||||||
|
normalized = normalize_mac_hex(mac)
|
||||||
|
if len(normalized) < 6:
|
||||||
|
return UNKNOWN_VENDOR
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
for prefix_length in (9, 7, 6):
|
||||||
|
if len(normalized) >= prefix_length:
|
||||||
|
vendor = self.vendors.get(normalized[:prefix_length])
|
||||||
|
if vendor:
|
||||||
|
return vendor
|
||||||
|
fallback = self.fallback_map.get(normalized[:prefix_length])
|
||||||
|
if fallback:
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
if is_locally_administered(normalized):
|
||||||
|
return LOCAL_ADMIN_VENDOR
|
||||||
|
return UNKNOWN_VENDOR
|
||||||
|
|
||||||
|
def update_if_stale_async(self, output: Optional[OutputCallback] = None) -> None:
|
||||||
|
self.load(output)
|
||||||
|
with self._lock:
|
||||||
|
if self._update_started or not self.is_stale():
|
||||||
|
return
|
||||||
|
self._update_started = True
|
||||||
|
|
||||||
|
thread = threading.Thread(target=self._update_worker, args=(output,), daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def update_now(self, output: Optional[OutputCallback] = None) -> bool:
|
||||||
|
self.load(output)
|
||||||
|
vendors = download_ieee_oui()
|
||||||
|
if not vendors:
|
||||||
|
raise RuntimeError("未下载到有效 OUI 数据")
|
||||||
|
payload = {"updated_at": time.time(), "vendors": vendors}
|
||||||
|
save_json_atomic(cache_file(), payload)
|
||||||
|
with self._lock:
|
||||||
|
self.vendors = vendors
|
||||||
|
self.updated_at = float(payload["updated_at"])
|
||||||
|
self.source = "联网更新"
|
||||||
|
write(output, f"OUI 厂商库已更新:{len(vendors)} 条\n", "success")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_stale(self) -> bool:
|
||||||
|
if not self.updated_at:
|
||||||
|
return True
|
||||||
|
return time.time() - self.updated_at > UPDATE_INTERVAL_SECONDS
|
||||||
|
|
||||||
|
def _update_worker(self, output: Optional[OutputCallback]) -> None:
|
||||||
|
try:
|
||||||
|
self.update_now(output)
|
||||||
|
except Exception as exc:
|
||||||
|
write(output, f"OUI 厂商库更新失败,继续使用离线库:{exc}\n", "warning")
|
||||||
|
|
||||||
|
def _load_json_file(self, path: Path) -> Optional[tuple[dict[str, str], float]]:
|
||||||
|
try:
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
with path.open("r", encoding="utf-8") as file:
|
||||||
|
payload = json.load(file)
|
||||||
|
raw_vendors = payload.get("vendors", {})
|
||||||
|
vendors = {
|
||||||
|
normalize_prefix(prefix): compact_vendor_name(vendor)
|
||||||
|
for prefix, vendor in raw_vendors.items()
|
||||||
|
if normalize_prefix(prefix) and compact_vendor_name(vendor)
|
||||||
|
}
|
||||||
|
if not vendors:
|
||||||
|
return None
|
||||||
|
return vendors, float(payload.get("updated_at") or 0)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def download_ieee_oui() -> dict[str, str]:
|
||||||
|
vendors: dict[str, str] = {}
|
||||||
|
for _registry, url in IEEE_OUI_SOURCES:
|
||||||
|
text = download_text(url)
|
||||||
|
vendors.update(parse_ieee_csv(text))
|
||||||
|
return vendors
|
||||||
|
|
||||||
|
|
||||||
|
def download_text(url: str) -> str:
|
||||||
|
request = urllib.request.Request(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
"User-Agent": "NetPilot/1.0 (+https://standards.ieee.org/develop/regauth/)",
|
||||||
|
"Accept": "text/csv,text/plain,*/*",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(request, timeout=25) as response:
|
||||||
|
return response.read().decode("utf-8-sig", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ieee_csv(text: str) -> dict[str, str]:
|
||||||
|
rows = csv.DictReader(text.splitlines())
|
||||||
|
vendors: dict[str, str] = {}
|
||||||
|
for row in rows:
|
||||||
|
prefix = normalize_prefix(row.get("Assignment", ""))
|
||||||
|
vendor = compact_vendor_name(row.get("Organization Name", ""))
|
||||||
|
if prefix and vendor:
|
||||||
|
vendors[prefix] = vendor
|
||||||
|
return vendors
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_prefix(value: str) -> str:
|
||||||
|
cleaned = re.sub(r"[^0-9A-Fa-f]", "", str(value or "")).upper()
|
||||||
|
if len(cleaned) in (6, 7, 9):
|
||||||
|
return cleaned
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_mac_hex(value: str) -> str:
|
||||||
|
return re.sub(r"[^0-9A-Fa-f]", "", str(value or "")).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def compact_vendor_name(value: str) -> str:
|
||||||
|
return re.sub(r"\s+", " ", str(value or "")).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def is_locally_administered(mac_hex: str) -> bool:
|
||||||
|
try:
|
||||||
|
return bool(int(mac_hex[:2], 16) & 0x02)
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def cache_file() -> Path:
|
||||||
|
base = os.environ.get("LOCALAPPDATA") or os.path.join(Path.home(), "AppData", "Local")
|
||||||
|
return Path(base) / "NetPilot" / "oui_vendors.json"
|
||||||
|
|
||||||
|
|
||||||
|
def seed_file() -> Path:
|
||||||
|
candidates = []
|
||||||
|
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||||
|
candidates.append(Path(sys._MEIPASS) / "assets" / "oui_vendors_seed.json")
|
||||||
|
candidates.append(Path(__file__).resolve().parents[2] / "assets" / "oui_vendors_seed.json")
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
candidates.append(Path(sys.executable).resolve().parent / "assets" / "oui_vendors_seed.json")
|
||||||
|
for path in candidates:
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
def save_json_atomic(path: Path, payload: dict) -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fd, temp_name = tempfile.mkstemp(prefix=path.name, suffix=".tmp", dir=str(path.parent))
|
||||||
|
try:
|
||||||
|
with os.fdopen(fd, "w", encoding="utf-8") as file:
|
||||||
|
json.dump(payload, file, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
os.replace(temp_name, path)
|
||||||
|
finally:
|
||||||
|
if os.path.exists(temp_name):
|
||||||
|
os.remove(temp_name)
|
||||||
|
|
||||||
|
|
||||||
|
def write(output: Optional[OutputCallback], text: str, tag: Optional[str] = None) -> None:
|
||||||
|
if output:
|
||||||
|
output(text, tag)
|
||||||
@@ -86,14 +86,14 @@ class Page(ttk.Frame):
|
|||||||
self.rowconfigure(1, weight=1)
|
self.rowconfigure(1, weight=1)
|
||||||
|
|
||||||
header = ttk.Frame(self, style="Page.TFrame")
|
header = ttk.Frame(self, style="Page.TFrame")
|
||||||
header.grid(row=0, column=0, sticky="ew", padx=28, pady=(28, 12))
|
header.grid(row=0, column=0, sticky="ew", padx=32, 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="PageMuted.TLabel").grid(row=1, column=0, sticky="w", pady=(8, 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="Page.TFrame")
|
body_shell = ttk.Frame(self, style="Page.TFrame")
|
||||||
body_shell.grid(row=1, column=0, sticky="nsew", padx=28, pady=(0, 22))
|
body_shell.grid(row=1, column=0, sticky="nsew", padx=32, 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)
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ class Page(ttk.Frame):
|
|||||||
frame.columnconfigure(col, weight=1)
|
frame.columnconfigure(col, weight=1)
|
||||||
|
|
||||||
title_bar = ttk.Frame(frame, style="Panel.TFrame")
|
title_bar = ttk.Frame(frame, style="Panel.TFrame")
|
||||||
title_bar.grid(row=0, column=0, columnspan=columns, sticky="ew", padx=22, pady=(18, 10))
|
title_bar.grid(row=0, column=0, columnspan=columns, sticky="ew", padx=20, 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="▣", style="Section.TLabel", foreground=COLORS["primary"]).pack(side="left", padx=(0, 10))
|
||||||
ttk.Label(title_bar, text=title, style="Section.TLabel").pack(side="left")
|
ttk.Label(title_bar, text=title, style="Section.TLabel").pack(side="left")
|
||||||
return frame
|
return frame
|
||||||
@@ -161,7 +161,7 @@ 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=22, pady=(4, 16))
|
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=16, 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, 7))
|
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7))
|
||||||
@@ -174,7 +174,7 @@ 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=22, pady=(4, 16))
|
frame.grid(row=row, column=column, columnspan=colspan, sticky="ew", padx=16, 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, 7))
|
ttk.Label(frame, text=label, style="Muted.TLabel").grid(row=0, column=0, sticky="w", pady=(0, 7))
|
||||||
@@ -186,7 +186,7 @@ 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=22, pady=(0, 18))
|
frame.grid(row=row, column=0, columnspan=columnspan, sticky="ew", padx=16, pady=(0, 18))
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,30 +12,30 @@ class DeviceDiscoveryTab(Page):
|
|||||||
self.console = console
|
self.console = console
|
||||||
self.body.rowconfigure(2, weight=1)
|
self.body.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=6)
|
status = self.section("实时状态", 0, columns=7)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
self.current = field(status, "当前网卡", 1, 1, "-", 18)
|
self.current = field(status, "当前网卡", 1, 1, "-", 18)
|
||||||
self.scan_scope = field(status, "扫描范围", 1, 2, "-", 20)
|
self.scan_scope = field(status, "扫描范围", 1, 2, "-", 22, colspan=2)
|
||||||
self.progress = field(status, "进度", 1, 3, "0/0", 10)
|
self.progress = field(status, "进度", 1, 4, "0/0", 10)
|
||||||
self.found = field(status, "发现设备", 1, 4, "0", 8)
|
self.found = field(status, "发现设备", 1, 5, "0", 8)
|
||||||
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10)
|
self.elapsed = field(status, "耗时", 1, 6, "0.0s", 10)
|
||||||
for item in (self.state, self.current, self.scan_scope, self.progress, self.found, self.elapsed):
|
for item in (self.state, self.current, self.scan_scope, self.progress, self.found, self.elapsed):
|
||||||
item["entry"].configure(state="disabled")
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
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.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, "", 42, colspan=3)
|
||||||
self.workers = field(params, "并发数", 1, 3, "24", 10)
|
self.workers = field(params, "并发数", 1, 4, "24", 10)
|
||||||
self.timeout = field(params, "超时 ms", 1, 4, "500", 10)
|
self.timeout = field(params, "超时 ms", 1, 5, "500", 10)
|
||||||
self.max_hosts = field(params, "最大地址数", 1, 5, "254", 10)
|
self.max_hosts = field(params, "最大地址数", 2, 0, "254", 10)
|
||||||
primary_actions = action_bar(params, 2, 6)
|
primary_actions = action_bar(params, 3, 6)
|
||||||
self.start_btn = button(primary_actions, "开始发现", self.start_discovery, "Primary.TButton")
|
self.start_btn = button(primary_actions, "开始发现", self.start_discovery, "Primary.TButton")
|
||||||
self.stop_btn = button(primary_actions, "停止", self.stop_discovery, "Danger.TButton")
|
self.stop_btn = button(primary_actions, "停止", self.stop_discovery, "Danger.TButton")
|
||||||
self.refresh_btn = button(primary_actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
|
self.refresh_btn = button(primary_actions, "刷新网卡", self.load_adapters, "Secondary.TButton")
|
||||||
self.auto_range_btn = button(primary_actions, "自动范围", self.fill_default_range, "Secondary.TButton")
|
self.auto_range_btn = button(primary_actions, "自动范围", self.fill_default_range, "Secondary.TButton")
|
||||||
self.stop_btn.configure(state="disabled")
|
self.stop_btn.configure(state="disabled")
|
||||||
secondary_actions = action_bar(params, 3, 6)
|
secondary_actions = action_bar(params, 4, 6)
|
||||||
self.copy_btn = button(secondary_actions, "复制清单", self.copy_inventory, "Secondary.TButton")
|
self.copy_btn = button(secondary_actions, "复制清单", self.copy_inventory, "Secondary.TButton")
|
||||||
self.export_btn = button(secondary_actions, "导出CSV", self.export_results, "Secondary.TButton")
|
self.export_btn = button(secondary_actions, "导出CSV", self.export_results, "Secondary.TButton")
|
||||||
|
|
||||||
@@ -58,26 +58,29 @@ class DeviceDiscoveryTab(Page):
|
|||||||
"note": "备注",
|
"note": "备注",
|
||||||
}
|
}
|
||||||
widths = {
|
widths = {
|
||||||
"ip": 112,
|
"ip": 105,
|
||||||
"mac": 132,
|
"mac": 170,
|
||||||
"vendor": 120,
|
"vendor": 270,
|
||||||
"adapter": 150,
|
"adapter": 80,
|
||||||
"latency": 72,
|
"latency": 65,
|
||||||
"method": 80,
|
"method": 80,
|
||||||
"note": 140,
|
"note": 100,
|
||||||
}
|
}
|
||||||
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)
|
||||||
self.results_tree.column(column, width=widths[column], anchor="w")
|
self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False)
|
||||||
self.results_tree.tag_configure("在线", foreground="#15803d")
|
self.results_tree.tag_configure("在线", foreground="#15803d")
|
||||||
self.results_tree.tag_configure("ARP 可见", foreground="#b7791f")
|
self.results_tree.tag_configure("ARP 可见", foreground="#b7791f")
|
||||||
self.results_tree.tag_configure("本机", foreground="#246bfe")
|
self.results_tree.tag_configure("本机", foreground="#246bfe")
|
||||||
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(
|
||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
results,
|
||||||
x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview)
|
orient="vertical",
|
||||||
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18))
|
command=self.results_tree.yview,
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
|
style="Modern.Vertical.TScrollbar",
|
||||||
|
)
|
||||||
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18))
|
||||||
|
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
||||||
|
|
||||||
self.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)
|
||||||
|
|||||||
+22
-18
@@ -12,13 +12,13 @@ class DnsTab(Page):
|
|||||||
self.console = console
|
self.console = console
|
||||||
self.body.rowconfigure(2, weight=1)
|
self.body.rowconfigure(2, weight=1)
|
||||||
|
|
||||||
status = self.section("实时状态", 0, columns=6)
|
status = self.section("实时状态", 0, columns=7)
|
||||||
self.state = field(status, "状态", 1, 0, "等待", 10)
|
self.state = field(status, "状态", 1, 0, "等待", 10)
|
||||||
self.current_dns = field(status, "当前 DNS", 1, 1, "-", 24, colspan=2)
|
self.current_dns = field(status, "当前 DNS", 1, 1, "-", 18, colspan=2)
|
||||||
self.target_total = field(status, "目标数", 1, 3, "0", 8)
|
self.target_total = field(status, "目标数", 1, 3, "0", 8)
|
||||||
self.progress = field(status, "进度", 1, 4, "0/0", 10)
|
self.progress = field(status, "进度", 1, 4, "0/0", 10)
|
||||||
self.abnormal = field(status, "异常数", 1, 5, "0", 8)
|
self.abnormal = field(status, "异常数", 1, 5, "0", 8)
|
||||||
self.elapsed = field(status, "耗时", 2, 0, "0.0s", 10)
|
self.elapsed = field(status, "耗时", 1, 6, "0.0s", 10)
|
||||||
for item in (self.state, self.current_dns, self.target_total, self.progress, self.abnormal, self.elapsed):
|
for item in (self.state, self.current_dns, self.target_total, self.progress, self.abnormal, self.elapsed):
|
||||||
item["entry"].configure(state="disabled")
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
@@ -35,8 +35,9 @@ class DnsTab(Page):
|
|||||||
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.repair_btn = button(actions, "修复异常", self.repair_dns, "Secondary.TButton")
|
||||||
self.copy_btn = button(actions, "复制摘要", self.copy_summary, "Secondary.TButton")
|
secondary_actions = action_bar(params, 4, 6)
|
||||||
self.export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
self.copy_btn = button(secondary_actions, "复制摘要", self.copy_summary, "Secondary.TButton")
|
||||||
|
self.export_btn = button(secondary_actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||||
|
|
||||||
results = self.section("诊断结果", 2, columns=1)
|
results = self.section("诊断结果", 2, columns=1)
|
||||||
results.rowconfigure(1, weight=1)
|
results.rowconfigure(1, weight=1)
|
||||||
@@ -57,26 +58,29 @@ class DnsTab(Page):
|
|||||||
"verdict": "错误 / 判断",
|
"verdict": "错误 / 判断",
|
||||||
}
|
}
|
||||||
widths = {
|
widths = {
|
||||||
"domain": 130,
|
"domain": 135,
|
||||||
"type": 56,
|
"type": 55,
|
||||||
"server": 116,
|
"server": 115,
|
||||||
"status": 64,
|
"status": 65,
|
||||||
"elapsed": 76,
|
"elapsed": 78,
|
||||||
"values": 220,
|
"values": 250,
|
||||||
"verdict": 260,
|
"verdict": 130,
|
||||||
}
|
}
|
||||||
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)
|
||||||
self.results_tree.column(column, width=widths[column], anchor="w")
|
self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False)
|
||||||
self.results_tree.tag_configure("正常", foreground="#15803d")
|
self.results_tree.tag_configure("正常", foreground="#15803d")
|
||||||
self.results_tree.tag_configure("无记录", foreground="#b7791f")
|
self.results_tree.tag_configure("无记录", foreground="#b7791f")
|
||||||
self.results_tree.tag_configure("失败", foreground="#dc2626")
|
self.results_tree.tag_configure("失败", foreground="#dc2626")
|
||||||
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(
|
||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
results,
|
||||||
x_scroll = ttk.Scrollbar(results, orient="horizontal", command=self.results_tree.xview)
|
orient="vertical",
|
||||||
x_scroll.grid(row=2, column=0, sticky="ew", padx=18, pady=(0, 18))
|
command=self.results_tree.yview,
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
|
style="Modern.Vertical.TScrollbar",
|
||||||
|
)
|
||||||
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18))
|
||||||
|
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
||||||
|
|
||||||
self.diagnostic = DnsDiagnostic(self.write, self.on_task_done, self.update_status, self.add_result)
|
self.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)
|
||||||
|
|||||||
+12
-10
@@ -15,7 +15,7 @@ class IpConflictTab(Page):
|
|||||||
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)
|
||||||
self.adapter_total = field(status, "网卡数", 1, 1, "0", 8)
|
self.adapter_total = field(status, "网卡数", 1, 1, "0", 8)
|
||||||
self.current = field(status, "当前对象", 1, 2, "-", 18)
|
self.current = field(status, "当前对象", 1, 2, "-", 24)
|
||||||
self.max_risk = field(status, "最高风险", 1, 3, "正常", 10)
|
self.max_risk = field(status, "最高风险", 1, 3, "正常", 10)
|
||||||
self.progress = field(status, "进度", 1, 4, "0/0", 10)
|
self.progress = field(status, "进度", 1, 4, "0/0", 10)
|
||||||
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10)
|
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10)
|
||||||
@@ -25,7 +25,7 @@ class IpConflictTab(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.mode = combo(params, "检测模式", 1, 1, [MODE_BOTH, MODE_LOCAL, MODE_SUBNET], MODE_BOTH, 14)
|
self.mode = combo(params, "检测模式", 1, 1, [MODE_BOTH, MODE_LOCAL, MODE_SUBNET], MODE_BOTH, 14)
|
||||||
self.scan_range = field(params, "扫描范围", 1, 2, "", 30, colspan=2)
|
self.scan_range = field(params, "扫描范围", 1, 2, "", 42, colspan=2)
|
||||||
self.workers = field(params, "并发数", 1, 4, "64", 10)
|
self.workers = field(params, "并发数", 1, 4, "64", 10)
|
||||||
self.timeout = field(params, "超时 ms", 1, 5, "500", 10)
|
self.timeout = field(params, "超时 ms", 1, 5, "500", 10)
|
||||||
self.max_hosts = field(params, "最大地址数", 2, 0, "254", 10)
|
self.max_hosts = field(params, "最大地址数", 2, 0, "254", 10)
|
||||||
@@ -56,13 +56,13 @@ class IpConflictTab(Page):
|
|||||||
"verdict": "判断",
|
"verdict": "判断",
|
||||||
}
|
}
|
||||||
widths = {
|
widths = {
|
||||||
"adapter": 130,
|
"adapter": 120,
|
||||||
"ip": 150,
|
"ip": 110,
|
||||||
"mac": 150,
|
"mac": 135,
|
||||||
"conflict": 180,
|
"conflict": 145,
|
||||||
"type": 110,
|
"type": 90,
|
||||||
"level": 90,
|
"level": 70,
|
||||||
"verdict": 420,
|
"verdict": 200,
|
||||||
}
|
}
|
||||||
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,7 +73,9 @@ class IpConflictTab(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))
|
||||||
|
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
+19
-17
@@ -15,7 +15,7 @@ class LoopTab(Page):
|
|||||||
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)
|
||||||
self.adapter_total = field(status, "网卡数", 1, 1, "0", 8)
|
self.adapter_total = field(status, "网卡数", 1, 1, "0", 8)
|
||||||
self.current_adapter = field(status, "当前网卡", 1, 2, "-", 18)
|
self.current_adapter = field(status, "当前网卡", 1, 2, "-", 24)
|
||||||
self.risk_level = field(status, "风险等级", 1, 3, "正常", 10)
|
self.risk_level = field(status, "风险等级", 1, 3, "正常", 10)
|
||||||
self.max_score = field(status, "最高分", 1, 4, "0", 8)
|
self.max_score = field(status, "最高分", 1, 4, "0", 8)
|
||||||
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10)
|
self.elapsed = field(status, "耗时", 1, 5, "0.0s", 10)
|
||||||
@@ -23,10 +23,10 @@ class LoopTab(Page):
|
|||||||
item["entry"].configure(state="disabled")
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
params = self.section("检测参数", 1, columns=5)
|
params = self.section("检测参数", 1, columns=5)
|
||||||
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 24)
|
self.adapter = combo(params, "检测网卡", 1, 0, [ALL_ADAPTERS], ALL_ADAPTERS, 34, colspan=2)
|
||||||
self.duration = field(params, "检测时长 s", 1, 1, "15", 10)
|
self.duration = field(params, "检测时长 s", 1, 2, "15", 10)
|
||||||
self.interval = field(params, "采样间隔 s", 1, 2, "1", 10)
|
self.interval = field(params, "采样间隔 s", 1, 3, "1", 10)
|
||||||
self.ping_timeout = field(params, "网关 Ping 超时 ms", 1, 3, "800", 12)
|
self.ping_timeout = field(params, "网关 Ping 超时 ms", 1, 4, "800", 12)
|
||||||
actions = action_bar(params, 2, 5)
|
actions = action_bar(params, 2, 5)
|
||||||
self.start_btn = button(actions, "开始检测", self.start_detection, "Primary.TButton")
|
self.start_btn = button(actions, "开始检测", self.start_detection, "Primary.TButton")
|
||||||
self.stop_btn = button(actions, "停止", self.stop_detection, "Danger.TButton")
|
self.stop_btn = button(actions, "停止", self.stop_detection, "Danger.TButton")
|
||||||
@@ -57,17 +57,17 @@ class LoopTab(Page):
|
|||||||
"verdict": "判断",
|
"verdict": "判断",
|
||||||
}
|
}
|
||||||
widths = {
|
widths = {
|
||||||
"adapter": 140,
|
"adapter": 110,
|
||||||
"ipv4": 120,
|
"ipv4": 96,
|
||||||
"gateway": 120,
|
"gateway": 96,
|
||||||
"non_unicast": 90,
|
"non_unicast": 72,
|
||||||
"ratio": 90,
|
"ratio": 78,
|
||||||
"loss": 80,
|
"loss": 70,
|
||||||
"jitter": 80,
|
"jitter": 72,
|
||||||
"arp": 80,
|
"arp": 58,
|
||||||
"score": 60,
|
"score": 50,
|
||||||
"level": 80,
|
"level": 62,
|
||||||
"verdict": 360,
|
"verdict": 150,
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -78,7 +78,9 @@ class LoopTab(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))
|
||||||
|
self.results_tree.configure(yscrollcommand=result_scroll.set, xscrollcommand=x_scroll.set)
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
+16
-16
@@ -13,39 +13,39 @@ class NetworkTab(Page):
|
|||||||
self.adapters = []
|
self.adapters = []
|
||||||
self.profiles = {}
|
self.profiles = {}
|
||||||
|
|
||||||
selector = self.section("网卡选择", 0, columns=4)
|
selector = self.section("网卡选择", 0, columns=6)
|
||||||
self.iface = combo(selector, "选择网卡", 1, 0, [], "", 44, colspan=2)
|
self.iface = combo(selector, "选择网卡", 1, 0, [], "", 56, colspan=3)
|
||||||
self.iface["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.load_selected_adapter())
|
self.iface["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.load_selected_adapter())
|
||||||
self.status = field(selector, "连接状态", 1, 2, "", 14)
|
self.status = field(selector, "连接状态", 1, 3, "", 18)
|
||||||
self.status["entry"].configure(state="disabled")
|
self.status["entry"].configure(state="disabled")
|
||||||
actions = action_bar(selector, 2, 4)
|
actions = action_bar(selector, 2, 6)
|
||||||
self.refresh_btn = button(actions, "刷新网卡", self.refresh_adapters, "Primary.TButton")
|
self.refresh_btn = button(actions, "刷新网卡", self.refresh_adapters, "Primary.TButton")
|
||||||
|
|
||||||
identity = self.section("网卡信息", 1, columns=4)
|
identity = self.section("网卡信息", 1, columns=6)
|
||||||
self.description = field(identity, "设备描述", 1, 0, "", 46, colspan=2)
|
self.description = field(identity, "设备描述", 1, 0, "", 62, colspan=3)
|
||||||
self.mac = field(identity, "MAC 地址", 1, 2, "", 24)
|
self.mac = field(identity, "MAC 地址", 1, 3, "", 26)
|
||||||
adapter_actions = action_bar(identity, 2, 4)
|
adapter_actions = action_bar(identity, 2, 6)
|
||||||
self.enable_adapter_btn = button(adapter_actions, "启用网卡", self.enable_adapter, "Primary.TButton")
|
self.enable_adapter_btn = button(adapter_actions, "启用网卡", self.enable_adapter, "Primary.TButton")
|
||||||
self.disable_adapter_btn = button(adapter_actions, "禁用网卡", self.disable_adapter, "Danger.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")
|
||||||
|
|
||||||
config = self.section("IPv4 配置", 2, columns=4)
|
config = self.section("IPv4 配置", 2, columns=6)
|
||||||
self.dhcp = combo(config, "地址模式", 1, 0, ["DHCP 自动获取", "静态手动配置"], "DHCP 自动获取", 18)
|
self.dhcp = combo(config, "地址模式", 1, 0, ["DHCP 自动获取", "静态手动配置"], "DHCP 自动获取", 18)
|
||||||
self.dhcp["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.update_entry_state())
|
self.dhcp["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.update_entry_state())
|
||||||
self.ipv4 = field(config, "IPv4 地址", 1, 1, "", 18)
|
self.ipv4 = field(config, "IPv4 地址", 1, 1, "", 18)
|
||||||
self.netmask = field(config, "子网掩码", 1, 2, "", 18)
|
self.netmask = field(config, "子网掩码", 1, 2, "", 18)
|
||||||
self.gateway = field(config, "默认网关", 1, 3, "", 18)
|
self.gateway = field(config, "默认网关", 1, 3, "", 18)
|
||||||
self.dns1 = field(config, "首选 DNS", 2, 1, "", 18)
|
self.dns1 = field(config, "首选 DNS", 1, 4, "", 18)
|
||||||
self.dns2 = field(config, "备用 DNS", 2, 2, "", 18)
|
self.dns2 = field(config, "备用 DNS", 1, 5, "", 18)
|
||||||
config_actions = action_bar(config, 3, 4)
|
config_actions = action_bar(config, 2, 6)
|
||||||
self.apply_btn = button(config_actions, "应用配置", self.apply_settings, "Primary.TButton")
|
self.apply_btn = button(config_actions, "应用配置", self.apply_settings, "Primary.TButton")
|
||||||
self.reload_btn = button(config_actions, "重新读取", self.refresh_adapters, "Secondary.TButton")
|
self.reload_btn = button(config_actions, "重新读取", self.refresh_adapters, "Secondary.TButton")
|
||||||
|
|
||||||
profiles = self.section("配置模板", 3, columns=4)
|
profiles = self.section("配置模板", 3, columns=6)
|
||||||
self.profile_name = field(profiles, "模板名称", 1, 0, "", 20)
|
self.profile_name = field(profiles, "模板名称", 1, 0, "", 28, colspan=2)
|
||||||
self.profile_select = combo(profiles, "选择模板", 1, 1, [], "", 24)
|
self.profile_select = combo(profiles, "选择模板", 1, 2, [], "", 34, colspan=2)
|
||||||
profile_actions = action_bar(profiles, 2, 4)
|
profile_actions = action_bar(profiles, 2, 6)
|
||||||
self.save_profile_btn = button(profile_actions, "保存当前为模板", self.save_profile, "Primary.TButton")
|
self.save_profile_btn = button(profile_actions, "保存当前为模板", self.save_profile, "Primary.TButton")
|
||||||
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")
|
||||||
|
|||||||
+19
-17
@@ -30,8 +30,10 @@ class PingTab(Page):
|
|||||||
item["entry"].configure(state="disabled")
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
modes = self.section("探测模式", 1, columns=1)
|
modes = self.section("探测模式", 1, columns=1)
|
||||||
|
modes.rowconfigure(1, weight=1)
|
||||||
|
modes.columnconfigure(0, weight=1)
|
||||||
self.tabs = ttk.Notebook(modes)
|
self.tabs = ttk.Notebook(modes)
|
||||||
self.tabs.grid(row=1, column=0, sticky="ew", padx=18, pady=(0, 18))
|
self.tabs.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||||
self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||||
self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||||
self.tabs.add(self.single_tab, text="单点探测")
|
self.tabs.add(self.single_tab, text="单点探测")
|
||||||
@@ -47,18 +49,18 @@ class PingTab(Page):
|
|||||||
for col in range(6):
|
for col in range(6):
|
||||||
self.single_tab.columnconfigure(col, weight=1)
|
self.single_tab.columnconfigure(col, weight=1)
|
||||||
|
|
||||||
self.local_ip = combo(self.single_tab, "本地源 IP", 0, 0, [], "", 22)
|
self.local_ip = combo(self.single_tab, "本地源 IP", 0, 0, [], "", 28)
|
||||||
self.target = field(self.single_tab, "目标 IP / 域名", 0, 1, "127.0.0.1", 28)
|
self.target = field(self.single_tab, "目标 IP / 域名", 0, 1, "127.0.0.1", 36, colspan=2)
|
||||||
self.single_mode = combo(self.single_tab, "模式", 0, 2, ["持续", "指定次数"], "持续", 12)
|
self.single_mode = combo(self.single_tab, "模式", 0, 3, ["持续", "指定次数"], "持续", 12)
|
||||||
self.count = field(self.single_tab, "次数", 0, 3, "4", 8)
|
self.count = field(self.single_tab, "次数", 0, 4, "4", 8)
|
||||||
self.interval = field(self.single_tab, "间隔 ms", 0, 4, "1000", 10)
|
self.interval = field(self.single_tab, "间隔 ms", 0, 5, "1000", 10)
|
||||||
self.timeout = field(self.single_tab, "超时 ms", 0, 5, "1200", 10)
|
self.timeout = field(self.single_tab, "超时 ms", 1, 0, "1200", 10)
|
||||||
|
|
||||||
self.size = field(self.single_tab, "包大小 bytes", 1, 0, "32", 12)
|
self.size = field(self.single_tab, "包大小 bytes", 1, 1, "32", 12)
|
||||||
self.ttl = field(self.single_tab, "TTL(0=默认)", 1, 1, "0", 12)
|
self.ttl = field(self.single_tab, "TTL(0=默认)", 1, 2, "0", 12)
|
||||||
self.df_var = tk.BooleanVar(value=False)
|
self.df_var = tk.BooleanVar(value=False)
|
||||||
df_frame = ttk.Frame(self.single_tab, style="Panel.TFrame")
|
df_frame = ttk.Frame(self.single_tab, style="Panel.TFrame")
|
||||||
df_frame.grid(row=1, column=2, sticky="ew", padx=18, pady=(22, 14))
|
df_frame.grid(row=1, column=3, sticky="ew", padx=18, pady=(22, 14))
|
||||||
ttk.Checkbutton(df_frame, text="禁止分片", variable=self.df_var).pack(anchor="w")
|
ttk.Checkbutton(df_frame, text="禁止分片", variable=self.df_var).pack(anchor="w")
|
||||||
|
|
||||||
actions = action_bar(self.single_tab, 2, 6)
|
actions = action_bar(self.single_tab, 2, 6)
|
||||||
@@ -70,16 +72,16 @@ class PingTab(Page):
|
|||||||
for col in range(6):
|
for col in range(6):
|
||||||
self.batch_tab.columnconfigure(col, weight=1)
|
self.batch_tab.columnconfigure(col, weight=1)
|
||||||
|
|
||||||
self.batch_local_ip = combo(self.batch_tab, "本地源 IP", 0, 0, [], "", 22)
|
self.batch_local_ip = combo(self.batch_tab, "本地源 IP", 0, 0, [], "", 28)
|
||||||
self.targets = field(self.batch_tab, "目标范围 / 列表", 0, 1, "192.168.1.0/24", 42, colspan=2)
|
self.targets = field(self.batch_tab, "目标范围 / 列表", 0, 1, "192.168.1.0/24", 52, colspan=3)
|
||||||
self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 3, "1200", 10)
|
self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 4, "1200", 10)
|
||||||
self.batch_size = field(self.batch_tab, "包大小 bytes", 0, 4, "32", 12)
|
self.batch_size = field(self.batch_tab, "包大小 bytes", 0, 5, "32", 12)
|
||||||
self.workers = field(self.batch_tab, "并发数", 0, 5, "64", 10)
|
self.workers = field(self.batch_tab, "并发数", 1, 0, "64", 10)
|
||||||
|
|
||||||
self.batch_ttl = field(self.batch_tab, "TTL(0=默认)", 1, 0, "0", 12)
|
self.batch_ttl = field(self.batch_tab, "TTL(0=默认)", 1, 1, "0", 12)
|
||||||
self.batch_df_var = tk.BooleanVar(value=False)
|
self.batch_df_var = tk.BooleanVar(value=False)
|
||||||
df_frame = ttk.Frame(self.batch_tab, style="Panel.TFrame")
|
df_frame = ttk.Frame(self.batch_tab, style="Panel.TFrame")
|
||||||
df_frame.grid(row=1, column=1, sticky="ew", padx=18, pady=(22, 14))
|
df_frame.grid(row=1, column=2, sticky="ew", padx=18, pady=(22, 14))
|
||||||
ttk.Checkbutton(df_frame, text="禁止分片", variable=self.batch_df_var).pack(anchor="w")
|
ttk.Checkbutton(df_frame, text="禁止分片", variable=self.batch_df_var).pack(anchor="w")
|
||||||
|
|
||||||
actions = action_bar(self.batch_tab, 2, 6)
|
actions = action_bar(self.batch_tab, 2, 6)
|
||||||
|
|||||||
+29
-22
@@ -33,8 +33,10 @@ class TelnetTab(Page):
|
|||||||
item["entry"].configure(state="disabled")
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
modes = self.section("扫描模式", 1, columns=1)
|
modes = self.section("扫描模式", 1, columns=1)
|
||||||
|
modes.rowconfigure(1, weight=1)
|
||||||
|
modes.columnconfigure(0, weight=1)
|
||||||
self.tabs = ttk.Notebook(modes)
|
self.tabs = ttk.Notebook(modes)
|
||||||
self.tabs.grid(row=1, column=0, sticky="ew", padx=18, pady=(0, 18))
|
self.tabs.grid(row=1, column=0, sticky="nsew", padx=18, pady=(0, 18))
|
||||||
self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
self.single_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||||
self.scan_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
self.scan_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||||
self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
self.batch_tab = ttk.Frame(self.tabs, style="Panel.TFrame")
|
||||||
@@ -64,25 +66,30 @@ class TelnetTab(Page):
|
|||||||
"banner": "Banner / 错误",
|
"banner": "Banner / 错误",
|
||||||
}
|
}
|
||||||
widths = {
|
widths = {
|
||||||
"host": 150,
|
"host": 185,
|
||||||
"resolved_ip": 130,
|
"resolved_ip": 145,
|
||||||
"port": 70,
|
"port": 70,
|
||||||
"service": 100,
|
"service": 125,
|
||||||
"status": 80,
|
"status": 80,
|
||||||
"latency": 80,
|
"latency": 85,
|
||||||
"banner": 320,
|
"banner": 120,
|
||||||
}
|
}
|
||||||
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)
|
||||||
self.results_tree.column(column, width=widths[column], anchor="w")
|
self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False)
|
||||||
self.results_tree.tag_configure("open", foreground="#15803d")
|
self.results_tree.tag_configure("open", foreground="#15803d")
|
||||||
self.results_tree.tag_configure("timeout", foreground="#b7791f")
|
self.results_tree.tag_configure("timeout", foreground="#b7791f")
|
||||||
self.results_tree.tag_configure("unreachable", foreground="#b7791f")
|
self.results_tree.tag_configure("unreachable", foreground="#b7791f")
|
||||||
self.results_tree.tag_configure("dns_error", foreground="#dc2626")
|
self.results_tree.tag_configure("dns_error", foreground="#dc2626")
|
||||||
self.results_tree.tag_configure("error", foreground="#dc2626")
|
self.results_tree.tag_configure("error", foreground="#dc2626")
|
||||||
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(
|
||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
results,
|
||||||
|
orient="vertical",
|
||||||
|
command=self.results_tree.yview,
|
||||||
|
style="Modern.Vertical.TScrollbar",
|
||||||
|
)
|
||||||
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18))
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
||||||
|
|
||||||
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)
|
||||||
@@ -91,11 +98,11 @@ class TelnetTab(Page):
|
|||||||
for col in range(5):
|
for col in range(5):
|
||||||
self.single_tab.columnconfigure(col, weight=1)
|
self.single_tab.columnconfigure(col, weight=1)
|
||||||
|
|
||||||
self.single_host = field(self.single_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 26)
|
self.single_host = field(self.single_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 36, colspan=2)
|
||||||
self.single_port = field(self.single_tab, "目标端口", 0, 1, "443", 12)
|
self.single_port = field(self.single_tab, "目标端口", 0, 2, "443", 12)
|
||||||
self.single_timeout = field(self.single_tab, "超时 ms", 0, 2, "1000", 10)
|
self.single_timeout = field(self.single_tab, "超时 ms", 0, 3, "1000", 10)
|
||||||
self.single_banner_var = tk.BooleanVar(value=False)
|
self.single_banner_var = tk.BooleanVar(value=False)
|
||||||
self._check(self.single_tab, "Banner 探测", self.single_banner_var, 0, 3)
|
self._check(self.single_tab, "Banner 探测", self.single_banner_var, 0, 4)
|
||||||
actions = action_bar(self.single_tab, 1, 5)
|
actions = action_bar(self.single_tab, 1, 5)
|
||||||
self.single_btn = button(actions, "测试连接", self.test_single, "Primary.TButton")
|
self.single_btn = button(actions, "测试连接", self.test_single, "Primary.TButton")
|
||||||
self.single_export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
self.single_export_btn = button(actions, "导出 CSV", self.export_results, "Secondary.TButton")
|
||||||
@@ -104,15 +111,15 @@ class TelnetTab(Page):
|
|||||||
for col in range(6):
|
for col in range(6):
|
||||||
self.scan_tab.columnconfigure(col, weight=1)
|
self.scan_tab.columnconfigure(col, weight=1)
|
||||||
|
|
||||||
self.scan_host = field(self.scan_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 26)
|
self.scan_host = field(self.scan_tab, "目标 IP / 域名", 0, 0, "127.0.0.1", 30)
|
||||||
self.scan_ports = field(self.scan_tab, "端口列表 / 范围", 0, 1, PORT_PRESETS["常用端口"], 44, colspan=2)
|
self.scan_ports = field(self.scan_tab, "端口列表 / 范围", 0, 1, PORT_PRESETS["常用端口"], 58, colspan=3)
|
||||||
self.scan_preset = combo(self.scan_tab, "预设", 0, 3, list(PORT_PRESETS), "常用端口", 16)
|
self.scan_preset = combo(self.scan_tab, "预设", 0, 4, list(PORT_PRESETS), "常用端口", 16)
|
||||||
self.scan_timeout = field(self.scan_tab, "超时 ms", 0, 4, "800", 10)
|
self.scan_timeout = field(self.scan_tab, "超时 ms", 0, 5, "800", 10)
|
||||||
self.scan_workers = field(self.scan_tab, "并发数", 0, 5, "128", 10)
|
self.scan_workers = field(self.scan_tab, "并发数", 1, 0, "128", 10)
|
||||||
self.scan_show_closed_var = tk.BooleanVar(value=False)
|
self.scan_show_closed_var = tk.BooleanVar(value=False)
|
||||||
self.scan_banner_var = tk.BooleanVar(value=False)
|
self.scan_banner_var = tk.BooleanVar(value=False)
|
||||||
self._check(self.scan_tab, "显示关闭端口", self.scan_show_closed_var, 1, 0)
|
self._check(self.scan_tab, "显示关闭端口", self.scan_show_closed_var, 1, 1)
|
||||||
self._check(self.scan_tab, "Banner 探测", self.scan_banner_var, 1, 1)
|
self._check(self.scan_tab, "Banner 探测", self.scan_banner_var, 1, 2)
|
||||||
self.scan_preset["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.apply_preset(self.scan_preset, self.scan_ports))
|
self.scan_preset["combobox"].bind("<<ComboboxSelected>>", lambda _event: self.apply_preset(self.scan_preset, self.scan_ports))
|
||||||
|
|
||||||
actions = action_bar(self.scan_tab, 2, 6)
|
actions = action_bar(self.scan_tab, 2, 6)
|
||||||
@@ -125,8 +132,8 @@ class TelnetTab(Page):
|
|||||||
for col in range(6):
|
for col in range(6):
|
||||||
self.batch_tab.columnconfigure(col, weight=1)
|
self.batch_tab.columnconfigure(col, weight=1)
|
||||||
|
|
||||||
self.batch_hosts = field(self.batch_tab, "目标列表 / CIDR / IP 段", 0, 0, "192.168.1.1-254", 38, colspan=2)
|
self.batch_hosts = field(self.batch_tab, "目标列表 / CIDR / IP 段", 0, 0, "192.168.1.1-254", 46, colspan=2)
|
||||||
self.batch_ports = field(self.batch_tab, "端口列表 / 范围", 0, 2, "22,80,443,3389", 32, colspan=2)
|
self.batch_ports = field(self.batch_tab, "端口列表 / 范围", 0, 2, "22,80,443,3389", 42, colspan=2)
|
||||||
self.batch_preset = combo(self.batch_tab, "预设", 0, 4, list(PORT_PRESETS), "远程管理", 16)
|
self.batch_preset = combo(self.batch_tab, "预设", 0, 4, list(PORT_PRESETS), "远程管理", 16)
|
||||||
self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 5, "800", 10)
|
self.batch_timeout = field(self.batch_tab, "超时 ms", 0, 5, "800", 10)
|
||||||
self.batch_workers = field(self.batch_tab, "并发数", 1, 0, "128", 10)
|
self.batch_workers = field(self.batch_tab, "并发数", 1, 0, "128", 10)
|
||||||
|
|||||||
+25
-23
@@ -31,16 +31,16 @@ class TracertTab(Page):
|
|||||||
item["entry"].configure(state="disabled")
|
item["entry"].configure(state="disabled")
|
||||||
|
|
||||||
params = self.section("追踪参数", 1, columns=7)
|
params = self.section("追踪参数", 1, columns=7)
|
||||||
self.target = field(params, "目标 IP / 域名", 1, 0, "8.8.8.8", 26)
|
self.target = field(params, "目标 IP / 域名", 1, 0, "8.8.8.8", 36, colspan=2)
|
||||||
self.address_family = combo(params, "地址族", 1, 1, ["自动", "IPv4", "IPv6"], "自动", 10)
|
self.address_family = combo(params, "地址族", 1, 2, ["自动", "IPv4", "IPv6"], "自动", 10)
|
||||||
self.mode = combo(params, "模式", 1, 2, ["单次", "指定次数", "持续"], "单次", 12)
|
self.mode = combo(params, "模式", 1, 3, ["单次", "指定次数", "持续"], "单次", 12)
|
||||||
self.repeat_count = field(params, "次数", 1, 3, "3", 8)
|
self.repeat_count = field(params, "次数", 1, 4, "3", 8)
|
||||||
self.interval_ms = field(params, "间隔 ms", 1, 4, "1000", 10)
|
self.interval_ms = field(params, "间隔 ms", 1, 5, "1000", 10)
|
||||||
self.max_hops = field(params, "最大跳数", 1, 5, "20", 10)
|
self.max_hops = field(params, "最大跳数", 1, 6, "20", 10)
|
||||||
self.timeout_ms = field(params, "单跳超时 ms", 1, 6, "800", 12)
|
self.timeout_ms = field(params, "单跳超时 ms", 2, 0, "800", 12)
|
||||||
self.high_latency_ms = field(params, "高延迟阈值 ms", 2, 0, "100", 12)
|
self.high_latency_ms = field(params, "高延迟阈值 ms", 2, 1, "100", 12)
|
||||||
self.resolve_names_var = tk.BooleanVar(value=False)
|
self.resolve_names_var = tk.BooleanVar(value=False)
|
||||||
self._check(params, "解析主机名", self.resolve_names_var, 2, 1)
|
self._check(params, "解析主机名", self.resolve_names_var, 2, 2)
|
||||||
actions = action_bar(params, 3, 7)
|
actions = action_bar(params, 3, 7)
|
||||||
self.start_btn = button(actions, "开始追踪", self.start_trace, "Primary.TButton")
|
self.start_btn = button(actions, "开始追踪", self.start_trace, "Primary.TButton")
|
||||||
self.stop_btn = button(actions, "停止", self.stop_trace, "Danger.TButton")
|
self.stop_btn = button(actions, "停止", self.stop_trace, "Danger.TButton")
|
||||||
@@ -52,7 +52,7 @@ class TracertTab(Page):
|
|||||||
results.columnconfigure(0, weight=1)
|
results.columnconfigure(0, weight=1)
|
||||||
self.results_tree = ttk.Treeview(
|
self.results_tree = ttk.Treeview(
|
||||||
results,
|
results,
|
||||||
columns=("run", "hop", "probe1", "probe2", "probe3", "avg", "jitter", "host", "ip", "status"),
|
columns=("run", "hop", "probe1", "probe2", "probe3", "avg", "jitter", "ip", "status"),
|
||||||
show="headings",
|
show="headings",
|
||||||
height=9,
|
height=9,
|
||||||
)
|
)
|
||||||
@@ -64,32 +64,35 @@ class TracertTab(Page):
|
|||||||
"probe3": "RTT 3",
|
"probe3": "RTT 3",
|
||||||
"avg": "平均",
|
"avg": "平均",
|
||||||
"jitter": "波动",
|
"jitter": "波动",
|
||||||
"host": "主机名",
|
|
||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"status": "判断",
|
"status": "判断",
|
||||||
}
|
}
|
||||||
widths = {
|
widths = {
|
||||||
"run": 60,
|
"run": 60,
|
||||||
"hop": 60,
|
"hop": 60,
|
||||||
"probe1": 70,
|
"probe1": 78,
|
||||||
"probe2": 70,
|
"probe2": 78,
|
||||||
"probe3": 70,
|
"probe3": 78,
|
||||||
"avg": 80,
|
"avg": 88,
|
||||||
"jitter": 80,
|
"jitter": 88,
|
||||||
"host": 200,
|
"ip": 190,
|
||||||
"ip": 150,
|
"status": 190,
|
||||||
"status": 90,
|
|
||||||
}
|
}
|
||||||
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)
|
||||||
self.results_tree.column(column, width=widths[column], anchor="w")
|
self.results_tree.column(column, width=widths[column], minwidth=widths[column], anchor="w", stretch=False)
|
||||||
self.results_tree.tag_configure("ok", foreground="#15803d")
|
self.results_tree.tag_configure("ok", foreground="#15803d")
|
||||||
self.results_tree.tag_configure("timeout", foreground="#b7791f")
|
self.results_tree.tag_configure("timeout", foreground="#b7791f")
|
||||||
self.results_tree.tag_configure("high_latency", foreground="#dc2626")
|
self.results_tree.tag_configure("high_latency", foreground="#dc2626")
|
||||||
self.results_tree.tag_configure("jitter", foreground="#b7791f")
|
self.results_tree.tag_configure("jitter", foreground="#b7791f")
|
||||||
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(
|
||||||
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18))
|
results,
|
||||||
|
orient="vertical",
|
||||||
|
command=self.results_tree.yview,
|
||||||
|
style="Modern.Vertical.TScrollbar",
|
||||||
|
)
|
||||||
|
result_scroll.grid(row=1, column=1, sticky="ns", pady=(0, 18), padx=(0, 18))
|
||||||
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
self.results_tree.configure(yscrollcommand=result_scroll.set)
|
||||||
|
|
||||||
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)
|
||||||
@@ -207,7 +210,6 @@ class TracertTab(Page):
|
|||||||
row.get("probe3", ""),
|
row.get("probe3", ""),
|
||||||
avg,
|
avg,
|
||||||
jitter,
|
jitter,
|
||||||
row.get("host", ""),
|
|
||||||
row.get("ip", ""),
|
row.get("ip", ""),
|
||||||
row.get("status_text", ""),
|
row.get("status_text", ""),
|
||||||
),
|
),
|
||||||
|
|||||||
+33
-43
@@ -7,8 +7,6 @@ from tkinter import ttk
|
|||||||
from core.ui.components import Console
|
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_loop import LoopTab
|
|
||||||
from core.ui.tab_network import NetworkTab
|
from core.ui.tab_network import NetworkTab
|
||||||
from core.ui.tab_ping import PingTab
|
from core.ui.tab_ping import PingTab
|
||||||
from core.ui.tab_telnet import TelnetTab
|
from core.ui.tab_telnet import TelnetTab
|
||||||
@@ -21,29 +19,29 @@ 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("1440x820")
|
self.root.geometry("1600x820")
|
||||||
self.root.minsize(1280, 760)
|
self.root.minsize(1440, 780)
|
||||||
apply_theme(root)
|
apply_theme(root)
|
||||||
|
self._apply_windows_titlebar_color()
|
||||||
|
self.root.after(100, self._apply_windows_titlebar_color)
|
||||||
|
|
||||||
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(1, weight=1)
|
self.root.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
self._build_topbar()
|
self.sidebar = tk.Frame(root, width=250, bg=COLORS["sidebar"])
|
||||||
|
self.sidebar.grid(row=0, column=0, rowspan=2, 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="Page.TFrame")
|
self.content = ttk.Frame(root, style="Page.TFrame")
|
||||||
self.content.grid(row=1, column=1, sticky="nsew")
|
self.content.grid(row=0, 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 = tk.Frame(root, width=360, bg=COLORS["bg"])
|
||||||
self.console_shell.grid(row=1, column=2, sticky="nsew", padx=(0, 16), pady=(16, 0))
|
self.console_shell.grid(row=0, column=2, sticky="nsew", padx=(0, 16), pady=(16, 0))
|
||||||
self.console_shell.grid_propagate(False)
|
self.console_shell.grid_propagate(False)
|
||||||
self.console_shell.rowconfigure(0, weight=1)
|
self.console_shell.rowconfigure(0, weight=1)
|
||||||
self.console_shell.columnconfigure(0, weight=1)
|
self.console_shell.columnconfigure(0, weight=1)
|
||||||
@@ -69,37 +67,14 @@ class MainUI:
|
|||||||
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=28, pady=(28, 28))
|
brand.pack(fill="x", padx=24, pady=(28, 28))
|
||||||
|
|
||||||
logo = tk.Label(
|
logo = tk.Label(
|
||||||
brand,
|
brand,
|
||||||
text="NP",
|
text="NP",
|
||||||
width=4,
|
width=3,
|
||||||
height=2,
|
height=2,
|
||||||
bg=COLORS["primary"],
|
bg=COLORS["primary"],
|
||||||
fg="#ffffff",
|
fg="#ffffff",
|
||||||
@@ -119,8 +94,6 @@ class MainUI:
|
|||||||
("ping", "⌁", "Ping 探测", "单点与批量探活"),
|
("ping", "⌁", "Ping 探测", "单点与批量探活"),
|
||||||
("ports", "⌕", "端口扫描", "TCP 连通性检测"),
|
("ports", "⌕", "端口扫描", "TCP 连通性检测"),
|
||||||
("trace", "↗", "路由追踪", "跳点路径分析"),
|
("trace", "↗", "路由追踪", "跳点路径分析"),
|
||||||
("loop", "◇", "环网检测", "二层环路风险"),
|
|
||||||
("ip_conflict", "≠", "IP 冲突", "地址占用排查"),
|
|
||||||
]
|
]
|
||||||
for key, icon, title, subtitle in items:
|
for key, icon, title, subtitle in items:
|
||||||
self.nav_buttons[key] = self._nav_button(key, icon, title, subtitle)
|
self.nav_buttons[key] = self._nav_button(key, icon, title, subtitle)
|
||||||
@@ -136,7 +109,7 @@ 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=16, pady=5)
|
frame.pack(fill="x", padx=14, pady=5)
|
||||||
|
|
||||||
icon_label = tk.Label(frame, text=icon, width=3, bg=COLORS["sidebar"], fg="#d9e6f7", 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=(12, 8), pady=12)
|
icon_label.pack(side="left", padx=(12, 8), pady=12)
|
||||||
@@ -163,8 +136,6 @@ class MainUI:
|
|||||||
"ping": PingTab(self.content, self.console),
|
"ping": PingTab(self.content, self.console),
|
||||||
"ports": TelnetTab(self.content, self.console),
|
"ports": TelnetTab(self.content, self.console),
|
||||||
"trace": TracertTab(self.content, self.console),
|
"trace": TracertTab(self.content, self.console),
|
||||||
"loop": LoopTab(self.content, self.console),
|
|
||||||
"ip_conflict": IpConflictTab(self.content, self.console),
|
|
||||||
"devices": DeviceDiscoveryTab(self.content, self.console),
|
"devices": DeviceDiscoveryTab(self.content, self.console),
|
||||||
}
|
}
|
||||||
for page in self.pages.values():
|
for page in self.pages.values():
|
||||||
@@ -213,7 +184,7 @@ class MainUI:
|
|||||||
highlightthickness=1,
|
highlightthickness=1,
|
||||||
bd=0,
|
bd=0,
|
||||||
)
|
)
|
||||||
status.grid(row=2, column=1, columnspan=2, sticky="ew")
|
status.grid(row=1, column=1, columnspan=2, sticky="ew")
|
||||||
status.grid_propagate(False)
|
status.grid_propagate(False)
|
||||||
status.columnconfigure(1, weight=1)
|
status.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
@@ -246,3 +217,22 @@ class MainUI:
|
|||||||
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _apply_windows_titlebar_color(self) -> None:
|
||||||
|
try:
|
||||||
|
hwnd = self.root.winfo_id()
|
||||||
|
caption_color = ctypes.c_int(self._colorref(COLORS["topbar"]))
|
||||||
|
text_color = ctypes.c_int(self._colorref("#ffffff"))
|
||||||
|
border_color = ctypes.c_int(self._colorref(COLORS["topbar"]))
|
||||||
|
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 35, ctypes.byref(caption_color), ctypes.sizeof(caption_color))
|
||||||
|
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 36, ctypes.byref(text_color), ctypes.sizeof(text_color))
|
||||||
|
ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 34, ctypes.byref(border_color), ctypes.sizeof(border_color))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _colorref(self, color: str) -> int:
|
||||||
|
color = color.lstrip("#")
|
||||||
|
red = int(color[0:2], 16)
|
||||||
|
green = int(color[2:4], 16)
|
||||||
|
blue = int(color[4:6], 16)
|
||||||
|
return red | (green << 8) | (blue << 16)
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ if exist "%PYTHON_EXE%" (
|
|||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
)
|
)
|
||||||
set "PYTHONPATH=%PYINSTALLER_PATH%"
|
set "PYTHONPATH=%PYINSTALLER_PATH%"
|
||||||
"%PYTHON_EXE%" -m PyInstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%"
|
"%PYTHON_EXE%" -m PyInstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%" --add-data "%CD%\assets\oui_vendors_seed.json;assets"
|
||||||
) else (
|
) else (
|
||||||
pyinstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%"
|
pyinstaller -F -w main.py -n NetworkTool --icon "%APP_ICON%" --add-data "%CD%\assets\oui_vendors_seed.json;assets"
|
||||||
)
|
)
|
||||||
|
|
||||||
endlocal
|
endlocal
|
||||||
|
|||||||
Reference in New Issue
Block a user