OccupyFinder — 原理与实现详解

从 IP Helper API、Restart Manager 到 Electron + C++ DLL 架构,说明 OccupyFinder 如何在本机查询端口占用与文件锁,以及结束进程的能力边界。

本文面向对技术原理感兴趣的同学和读者,尽量用真实、可验证的方式解释:Windows 如何记录「谁占了哪个端口、谁锁了哪个文件」;OccupyFinder 调用了哪些系统 API;界面上的「结束进程」究竟做了什么;又有哪些场景查不全或杀不掉。若您只想了解如何使用,请先阅读 《功能介绍》


阅读导航

章节 内容
一、从两个报错说起 端口占用与文件被锁背后是什么
二、Windows 网络与进程基础 连接表、PID、权限
三、和 netstat / Handle 的区别 本工具在做什么
四、整体架构 Electron + C++ DLL
五、端口占用查询 IP Helper API 全流程
六、文件占用查询 Restart Manager 全流程
七、结束进程与资源释放 TerminateProcess 与边界
八、JSON 契约与界面层 C ABI → React 表格
九、权限与完整性级别 为何非管理员会 <access denied>
十、真实场景走查 开发端口、日志文件、系统服务
十一、能力边界 FAQ 能做什么、不能做什么
附录 A API 与错误码对照

一、从两个报错说起

1.1 「端口已被占用」

开发时常见:

Error: listen EADDRINUSE: address already in use :::8080

含义是:本机已有一个(或多个)套接字绑定了 8080。操作系统维护一张「当前 TCP/UDP 连接与监听表」,每条记录带有本地地址、端口、协议、连接状态,以及所属进程 PID

OccupyFinder 的端口页,就是把这张表按您输入的端口号(或「留空列出全部监听」)筛出来,再补上进程名、路径、用户名,方便定位是谁在听。

1.2 「文件正在使用,无法删除」

删除或覆盖文件时,若其他进程仍打开着该文件的句柄(handle),资源管理器会提示「文件正在使用」。

Windows 从 Vista 起提供 Restart Manager(重启管理器) API:安装程序在更新被占用的 DLL/EXE 前,可先问系统「哪些进程正在使用这些文件」,必要时提示用户关闭。OccupyFinder 借用同一套机制做只读枚举——列出持有该路径相关资源的进程。

1.3 「清理」在本工具里指什么

需要先说清楚:OccupyFinder 不会单独「释放端口」或「解锁文件句柄」

用户操作 实际发生的事
端口页点「结束进程」 对占用该端口的进程调用 TerminateProcess,进程退出后内核回收其全部套接字
文件页点「结束进程」 对持有文件句柄的进程同样调用 TerminateProcess,句柄随进程销毁而关闭

这是粗暴但常见的排障手段:适合您确认可以关掉的 node.exe、临时日志查看器等;不适合不经确认就结束 sqlservr.exeSystem 等关键服务。


二、Windows 网络与进程基础(科普)

2.1 套接字、监听与连接

概念 说明
bind / listen 服务进程在端口上「占位」,等待连接;TCP 状态常为 LISTEN
ESTABLISHED 已建立的 TCP 连接;也占用本地端口,但语义与「端口被服务监听」不同
UDP 无连接状态;只要 socket 绑定了端口,表中就会出现对应条目

OccupyFinder 在 port = 0(列出全部) 时:

指定具体端口号时,TCP/UDP 不过滤 LISTEN,该端口上的全部条目都会返回(含已建立连接)。

2.2 PID:从端口到进程

内核为每个进程分配 PID(Process ID)。IP Helper API 扩展 TCP/UDP 表的关键字段是 dwOwningPid——「这条连接/监听归谁管」。

知道 PID 之后,还可以:

若当前用户权限不足,OpenProcess 失败,界面会显示 <access denied>,但 PID 与端口信息通常仍可见(来自内核表,不依赖打开目标进程)。

2.3 Restart Manager 与文件句柄

文件占用查询不遍历全盘句柄表(那是 Sysinternals Handle 的做法),而是:

  1. 确认路径存在;
  2. 创建 RM 会话,把该路径注册为「待检查资源」;
  3. RmGetList 返回正在使用该资源的进程列表及应用类型(服务、资源管理器、普通窗口等)。

空闲文件返回 空数组 [],与「路径不存在」区分开(后者返回 E_NOT_FOUND)。


三、和 netstat / Handle 的区别

工具 数据来源 OccupyFinder 的差异
netstat -ano 同源的 IP Helper 表 已解析 PID→进程名/路径/用户;图形筛选协议与常用端口
任务管理器 按进程看资源 难以「从端口 5173 反查进程」
Handle.exe 对象管理器句柄 命令行、偏文件;OccupyFinder 用 Restart Manager + 图形界面,并合并端口查询
TCPView 类似扩展表 功能接近;OccupyFinder 是轻量自研壳 + 文件占用

结论:端口侧与 netstat/TCPView 同源;文件侧与安装程序更新前用的 Restart Manager 同源。OccupyFinder 的价值在于合并场景、友好展示、本机一体化,而非发明新的内核钩子。


四、整体架构:界面与 C++ 引擎如何配合

工具分三层:React 界面Electron 主进程C++ 查询引擎(backend.dll

flowchart TB
  User[用户在 PortPage / FilePage 点查询或结束进程]
  UI[Electron Renderer React]
  Main[Electron 主进程]
  DLL[C++ sdtech_occupyfinder.dll]
  Kernel[Windows IP Helper / Restart Manager / Process API]

  User --> UI
  UI -->|IPC: of:queryPort / of:queryFile / of:terminateProcess| Main
  Main -->|koffi 调用 C ABI| DLL
  DLL -->|GetExtendedTcpTable RmGetList TerminateProcess| Kernel
  DLL -->|JSON 字符串| Main
  Main --> UI

4.1 为什么查询放在主进程?

Renderer 进程沙箱化,不宜直接加载原生 DLL。主进程:

4.2 为什么用 C++ 动态库?

4.3 C ABI 表面

对外稳定接口定义在 sdtech_occupyfinder_c.h

函数 作用
sdtech_occupyfinder_init / cleanup 库生命周期
sdtech_occupyfinder_query_port 端口查询,port=0 列出全部监听/绑定
sdtech_occupyfinder_query_file 文件占用查询
sdtech_occupyfinder_terminate_process 结束进程
sdtech_occupyfinder_free_string 释放 JSON 输出缓冲区

protocol_filter 取值:"all" | "TCP" | "UDP"

列出全部监听端口

端口页:port=0 时枚举 TCP LISTEN 与 UDP 绑定,并按端口号排序


五、端口占用查询

实现位于 PortQueryManager::queryPortJsonPortQueryManager.cpp)。

5.1 调用链概览

sequenceDiagram
  participant UI as PortPage
  participant Main as Electron 主进程
  participant DLL as PortQueryManager
  participant IPH as iphlpapi.dll
  participant PI as ProcessInfo

  UI->>Main: queryPort(port, protocol)
  Main->>DLL: sdtech_occupyfinder_query_port
  alt 包含 TCP
    DLL->>IPH: GetExtendedTcpTable IPv4/IPv6
    IPH-->>DLL: MIB_TCPTABLE_OWNER_PID 行
  end
  alt 包含 UDP
    DLL->>IPH: GetExtendedUdpTable IPv4/IPv6
    IPH-->>DLL: MIB_UDPTABLE_OWNER_PID 行
  end
  loop 每一行
    DLL->>PI: resolveProcessDetails(pid)
    PI-->>DLL: processName path user
  end
  DLL-->>Main: JSON 数组
  Main-->>UI: PortOccupant[]

5.2 GetExtendedTcpTable:两步分配

与许多 Win32「先问大小、再分配」API 相同:

  1. nullptr 缓冲区 → 返回 ERROR_INSUFFICIENT_BUFFER,得到所需 size
  2. 分配 std::vector<BYTE>,再次调用 → 得到 MIB_TCPTABLE_OWNER_PID 或 IPv6 的 MIB_TCP6TABLE_OWNER_PID

使用的表类型:TCP_TABLE_OWNER_PID_ALL——包含连接状态与 Owning PID

核心过滤逻辑(简化):

// listAllListening = (port == 0)
if (listenOnly && row.dwState != 2) continue;  // 2 = MIB_TCP_STATE_LISTEN
const uint16_t localPort = ntohs(row.dwLocalPort);
if (targetPort != 0 && localPort != targetPort) continue;

5.3 GetExtendedUdpTable

UDP 表类型为 UDP_TABLE_OWNER_PID。无 LISTEN 状态,故:

5.4 地址格式化

TCP 状态映射为可读字符串(LISTENESTABLISHED 等)。

5.5 排序与进程详情缓存

port == 0 时,结果按端口号升序,同端口再按协议、地址字符串排序。

同一 PID 可能对应多行(多地址族、多连接),ProcessInfounordered_map 缓存 resolveProcessDetails(pid),避免重复 OpenProcess

5.6 JSON 行字段

字段 含义
protocol "TCP" / "UDP"
localAddress 本地地址:端口
state TCP 状态;UDP 为 "—"
pid 所属进程 ID
processName 可执行文件名;无权限时为 <access denied>
processPath 完整路径
user 可选,运行账户

六、文件占用查询

实现位于 FileQueryManager::queryFileJsonFileQueryManager.cpp)。

6.1 Restart Manager 会话

flowchart TD
  Start[queryFileJson path] --> Exists{GetFileAttributesW 存在?}
  Exists -->|否| NotFound[E_NOT_FOUND]
  Exists -->|是| Session[RmStartSession]
  Session --> Register[RmRegisterResources 注册路径]
  Register --> Size[RmGetList 第一次: 取 procInfoNeeded]
  Size --> List[RmGetList 第二次: 填充 RM_PROCESS_INFO]
  List --> End[RmEndSession]
  End --> JSON[组装 JSON + resolveProcessDetails]

步骤说明:

步骤 API 说明
1 GetFileAttributesW 路径必须存在;不存在直接 E_NOT_FOUND
2 RmStartSession 创建 RM 会话,获得 sessionKey
3 RmRegisterResources 注册 1 个文件/目录路径(UTF-16)
4 RmGetList(两次) 第一次 nullptr 缓冲区得所需数量;第二次填入 RM_PROCESS_INFO 数组
5 RmEndSession 关闭会话

6.2 占用类型 lockKind

根据 RM_PROCESS_INFO.ApplicationType 映射为中文标签:

RM 类型 界面 lockKind
RmService 服务
RmExplorer 资源管理器
RmMainWindow / RmOtherWindow 应用程序 / 窗口
RmCritical 关键进程
其他 占用

6.3 路径限制与已知边界

文件占用查询

文件页:对临时测试文件查询,无进程占用时的空态

6.4 与 Handle 的对比

维度 Restart Manager Handle.exe
入口 给定路径,问「谁妨碍我更新/使用它」 枚举进程或全局句柄,再过滤
权限 受 UAC 影响,系统进程可能不全 需管理员才较完整
适用 安装程序、OccupyFinder 文件页 深度调试

七、结束进程(「清理」)

实现位于 terminateProcessByPidProcessTerminator.cpp)。这是 OccupyFinder 唯一的「释放资源」手段。

7.1 调用流程

sequenceDiagram
  participant UI as PortPage / FilePage
  participant Main as 主进程
  participant DLL as ProcessTerminator
  participant OS as Windows 内核

  UI->>UI: ConfirmModal 用户确认
  UI->>Main: terminateProcess(pid)
  Main->>DLL: sdtech_occupyfinder_terminate_process
  DLL->>DLL: 保护 PID 4 / 自身 / 非法值
  DLL->>OS: OpenProcess PROCESS_TERMINATE
  DLL->>OS: TerminateProcess(exitCode=1)
  OS-->>DLL: 成功或 ACCESS_DENIED
  UI->>UI: 刷新查询

7.2 保护规则

条件 返回
pid <= 0 E_PROTECTED
pid == 4(System) E_PROTECTED
等于当前进程 PID E_PROTECTED
OpenProcess 失败 E_ACCESS_DENIED
TerminateProcess 失败 E_ACCESS_DENIED

没有「只关闭某个 socket」或「只释放某个句柄」的 API 封装——那是驱动级或调试器级能力,超出本工具范围。

7.3 结束后的效果

场景 预期
开发服务器占 5173 进程退出 → 端口释放 → 可重新 npm run dev
记事本打开日志 进程退出 → 句柄关闭 → 可删除文件
系统服务占 443 往往 E_ACCESS_DENIED 或杀后服务自动重启;不应随意结束

前端在成功后调用 runQuery() 自动刷新列表,确认占用是否消失。


八、JSON 契约与界面层

8.1 主进程 → Renderer

Preload 暴露 window.occupyFinderAPI.queryPort / queryFile / terminateProcess。Renderer 的 queryService.ts 只做错误码映射与类型包装。

8.2 错误码

常量 界面典型文案
0 OK 成功
-1 E_INVALID_ARG 参数无效
-2 E_NOT_FOUND 路径不存在
-10 E_ACCESS_DENIED 权限不足 / 终止失败
-11 E_PROTECTED 无法终止该系统进程
-99 E_INTERNAL 内部错误

8.3 内存管理

DLL 通过 copy_string 分配 JSON 字符串;主进程 koffi 读取后调用 sdtech_occupyfinder_free_string 释放,避免泄漏。

8.4 管理员提示

主进程 of:isElevated 通过 net session 是否成功粗测是否提升权限;非管理员时 PortPage/FilePage 显示黄条,与 <access denied> 占位符呼应。


九、权限与完整性级别

9.1 为何能看到 PID 却看不到进程名?

IP Helper 与 Restart Manager 返回的 PID 来自内核/系统服务,一般不需要打开目标进程。

QueryFullProcessImageNameW 需要 OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)。目标若为高完整性(如 SYSTEM 服务),普通用户进程会被 UIPI 拒绝 → processName / processPath 显示 <access denied>

9.2 为何终止失败?

OpenProcess(PROCESS_TERMINATE) 同样受完整性级别与 DACL 约束。保护进程、关键系统服务、其他用户的进程,常返回 E_ACCESS_DENIED

9.3 建议


十、真实场景走查

10.1 Vite 5173 被占用

  1. query_port(5173, "all") → 返回 TCP LISTEN 行,pid 指向 node.exe
  2. 确认无重要工作后 terminate_process(pid)
  3. 再次查询 → 空列表或 EADDRINUSE 消失。

10.2 列出全部监听端口

  1. query_port(0, "all") → 仅 TCP LISTEN + 全部 UDP 绑定;
  2. 按端口排序浏览,定位异常监听;
  3. 截图中可见 108 条监听项,系统进程路径可能为 <access denied>

10.3 无法删除的日志文件

  1. query_file("D:\\logs\\app.log") → Restart Manager 返回占用进程列表;
  2. [] → 文件未被占用,可能是路径错误、权限或网络盘延迟;
  3. 若有行 → 结束对应进程后再删;或关闭占用程序而非强杀。

10.4 GTest 如何验证

测试 验证点
PortQueryApiTest 非法端口、port=0 仅 LISTEN、排序、自发进程监听 135
FileQueryApiTest 空闲文件 []、共享/独占锁持有进程
TerminateProcessApiTest PID 4、自身、不存在进程

运行:cd Frontend && npm run test:backend


十一、能力边界 FAQ

Q:能不能只释放端口、不杀进程?
A:当前实现不能。需自行在任务管理器结束线程/服务,或使用目标软件自带的「停止」功能。OccupyFinder 仅提供 TerminateProcess

Q:UDP 为什么状态是
A:UDP 无 TCP 式连接状态机;表项只表示「该进程绑定了此端口」。

Q:port=0 为什么 TCP 只有 LISTEN?
A:有意设计为「监听一览」;若查某端口上的所有连接(含 ESTABLISHED),请输入具体端口号。

Q:文件夹路径为什么有时失败?
A:Restart Manager 对「目录」与「文件」的支持因系统/路径而异;建议指向具体文件,或保证目录内存在可注册的文件路径。

Q:和 Linux lsof / fuser 一样吗?
A:思路类似(查端口/查文件占用),但本 DLL 仅实现 Windows;非 Win32 平台 API 返回 E_INTERNAL

Q:数据会上传吗?
A:不会。查询与终止均在本地 DLL 完成,JSON 只经 Electron IPC 到界面。

Q:结束进程安全吗?
A:取决于您结束的进程。工具在 UI 层有确认框,DLL 层拒绝 PID 4 与自身;其余风险由操作者承担。


附录 A:API 与错误码对照

用户动作 DLL 入口 核心 Win32 API
查端口 query_port GetExtendedTcpTable / GetExtendedUdpTable
查文件 query_file RmStartSessionRmRegisterResourcesRmGetList
结束进程 terminate_process OpenProcess + TerminateProcess
进程详情 (内部) QueryFullProcessImageNameWOpenProcessToken
条件 端口查询 文件查询 终止进程
参数非法 E_INVALID_ARG E_INVALID_ARG E_PROTECTED
路径不存在 E_NOT_FOUND
无占用 空数组 空数组
权限不足 行内 <access denied> 可能列表不全 E_ACCESS_DENIED
系统/自身 PID E_PROTECTED

结语

OccupyFinder 的原理可以浓缩为:

端口占用问 IP Helper 表,文件占用问 Restart Manager,进程信息问 Process API;「清理」则是经用户确认后终止进程,由内核回收端口与句柄。

读懂这三条系统接口,就读懂了本工具的能力边界:能查的是内核愿意告诉你的 PID 与连接,能放的是你愿意结束的进程

若您想亲手试用,请参阅 《功能介绍》。若希望继续深入,可在 Microsoft Learn 查阅 GetExtendedTcpTableRestart ManagerTerminateProcessUIPI 等主题。