Who's locking this file? Find the process that's holding your file hostage — and free it.
谁占用了我的文件? 一条命令找出锁住文件的进程,并解除占用。
English | 中文
You know these errors:
The process cannot access the file because it is being used by another process. (Windows)
PermissionError: [WinError 32] (Python on Windows)
rm: cannot remove 'data.db': Device or resource busy (Linux)
OSError: [Errno 16] Device or resource busy (Python on Linux)
They all mean the same thing: some process is holding the file.
But which one? That's the question wholock answers — in one command,
on Windows, Linux, and macOS, with zero dependencies.
$ wholock report.xlsx
report.xlsx - locked by 1 process:
PID NAME KIND VIA EXE
17048 EXCEL.EXE window handle C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE
$ wholock --kill report.xlsx # terminate it (asks first)
$ wholock --wait -t 30 out.dll # or wait until it's releasedpip install wholock # or: pipx install wholockNo compiler, no admin rights, no other packages — it's pure standard library.
Until the PyPI release is live you can also install straight from GitHub:
pip install git+https://github.com/hc-ui/wholock| wholock | PowerToys File Locksmith | Sysinternals Handle | openfiles.exe | Resource Monitor | fuser / lsof | psutil script | |
|---|---|---|---|---|---|---|---|
| Windows | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Linux / macOS | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Install | pip install |
full PowerToys install | manual download | built-in | built-in | built-in | needs compiled dep |
| Works without admin | ✅ usually | partially | ❌ admin required | ❌ global flag + reboot | ✅ | partially | partially |
| Scriptable JSON | ✅ | ✅ | CSV | ❌ | ❌ (GUI) | ❌ | DIY |
| Kill / wait built in | ✅ both | ✅ both | ❌ | ❌ | kill only | kill only (fuser -k) | DIY |
| Python API | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ~ |
| Open source | ✅ MIT | ✅ | ❌ | — | — | ✅ | ✅ |
The niche wholock fills: one pip install that works on all three platforms,
needs no elevation for everyday cases, and is also a library you can call from
your own Python code, tests, and CI.
wholock [options] PATH [PATH ...]
output:
-j, --json machine-readable JSON output
--pids print only the PIDs, space-separated (fuser-style)
-q, --quiet no output, just the exit code
--no-color disable colors (also respects NO_COLOR)
actions:
-k, --kill terminate the locking processes
-y, --yes don't ask for confirmation before killing
-f, --force SIGKILL instead of SIGTERM (POSIX); allow killing critical processes
-w, --wait wait until the path(s) are free
-t, --timeout SECS give up waiting after SECS (default: wait forever)
--interval SECS polling interval for --wait (default: 0.5)
Exit codes (script-friendly, fuser-inspired):
| Code | Meaning |
|---|---|
| 0 | lockers found (query) / path freed (--kill, --wait) |
| 1 | nothing is holding the path(s) |
| 2 | error: missing path, permission problem, bad usage |
| 3 | --wait timed out |
| 4 | --kill could not free the path(s) |
Examples:
wholock data.db # who is holding it?
wholock --json data.db | jq '.results[0].lockers[].pid'
wholock --pids data.db # e.g. "1234 5678"
wholock -k -y build/output.dll # free it, no questions asked
wholock -w -t 60 video.mp4 && mv video.mp4 done/ # wait, then act
wholock ~/Downloads # directories work tooThe part no other tool has. Ever seen this in your logs?
PermissionError: [WinError 32] The process cannot access the file because
it is being used by another process: 'C:\\app\\cache.db'
Now you can log who:
import os
import wholock
try:
os.replace("cache.db.tmp", "cache.db")
except PermissionError:
raise RuntimeError(wholock.explain("cache.db"))
# RuntimeError: cache.db is in use by antivirus.exe (pid 4242)Full surface — four functions, one dataclass:
import wholock
# 1) Query: list of Locker(pid, name, exe, kind, via, user, started)
for p in wholock.who_locks("report.xlsx"):
print(p.pid, p.name, p.via)
# 2) Explain: one-line summary, never raises on missing files
msg = wholock.explain("report.xlsx")
# 3) Wait: block until the file is released (True) or timeout (False)
if wholock.wait_until_free("report.xlsx", timeout=30):
process_file("report.xlsx")
# 4) Kill: terminate the holders (with PID-reuse protection)
for locker, ok, message in wholock.kill_lockers("report.xlsx"):
print(locker.pid, ok, message)Typical uses: retry loops around os.replace, cleaning up temp dirs in test
teardown on Windows, CI steps that wait for a build artifact to be released,
better error messages in installers and updaters.
| OS | Backend | Notes |
|---|---|---|
| Windows | Restart Manager API (Rstrtmgr.dll via ctypes) |
The official mechanism behind Windows' own "file in use" dialogs. Fast, race-free, and usually works without admin. |
| Linux | direct /proc scan |
No lsof needed. Detects open fds, memory maps, cwd, exe, and chroot references. |
| macOS | system lsof (ships with macOS) |
Machine-readable -F output parsing. |
The via field tells you how a process holds the path:
handle (Windows), open (fd), mmap (memory-mapped), cwd (it's the
process's working directory), exe (it's the running binary), root (chroot).
Safety rails when killing:
- Never kills the calling process or system PIDs (≤ 4).
- Windows/Linux: verifies the process start time before killing, so a recycled PID is never killed by mistake.
- Windows-critical processes are skipped unless you pass
--force. - Zombie processes are treated as already dead (they hold nothing).
- Processes of other users may be invisible without elevation
(
sudo/ administrator).wholockwarns when that could be the reason you see zero results. - On Windows, querying a directory checks the files inside it
(up to 10,000). A process merely
cd-ed into the folder holds no file handle Restart Manager can see — if a folder won't delete, look for Explorer windows or terminals sitting in it. - The Windows Restart Manager cannot inspect paths ≥ 260 characters
(verified experimentally — the
\\?\prefix does not help).wholockraises a clear error for such files and skips them with a warning when they appear inside a queried directory. - On macOS, scanning big directories can be slow — that's
lsof +D. - Files on network shares are only as visible as the OS makes them.
--killon Windows is always forceful (TerminateProcess); Windows has no polite cross-process SIGTERM.
Why not just use psutil?
psutil is a fine library, but: it's a compiled dependency (~x MB, needs
wheels), finding a file's holder means scanning open_files() of every
process (slow, often permission-blocked, misses memory-mapped files), and
there's no CLI. wholock asks the OS the way the OS wants to be asked.
Do I need admin/root? Usually not for your own processes — which covers the typical "my build can't overwrite a DLL" and "Excel still has the CSV" cases. System services and other users' processes may need elevation to show up.
Can it close a single handle instead of killing the process?
No, by design. Force-closing a handle inside a live process (what
Sysinternals Handle -c does) can corrupt that process's state. Kill or
wait — both are safe.
Is it fast?
Windows: a Restart Manager session takes a few ms per file. Linux: one pass
over /proc. macOS: one lsof invocation.
Bug reports and PRs welcome. The test suite runs on every push across Windows / Linux / macOS × Python 3.9–3.14:
python -m unittest discover -s tests -t . -vIf wholock saved your file (or your afternoon), a ⭐ helps others find it.
English | 中文
你一定见过这些报错:
另一个程序正在使用此文件,进程无法访问。 (Windows)
PermissionError: [WinError 32] (Windows 上的 Python)
rm: cannot remove 'data.db': Device or resource busy (Linux)
OSError: [Errno 16] Device or resource busy (Linux 上的 Python)
意思都一样:有个进程占着你的文件。但到底是哪个?
wholock 一条命令给你答案——Windows / Linux / macOS 全平台,零依赖。
$ wholock 报告.xlsx
报告.xlsx - locked by 1 process:
PID NAME KIND VIA EXE
17048 EXCEL.EXE window handle C:\Program Files\Microsoft Office\root\Office16\EXCEL.EXE
$ wholock --kill 报告.xlsx # 直接结束占用进程(会先确认)
$ wholock --wait -t 30 out.dll # 或者等它自己释放pip install wholock # 或 pipx install wholock纯 Python 标准库实现:不需要编译器、不需要管理员权限、不装任何第三方包。
PyPI 发布前也可以直接从 GitHub 安装:
pip install git+https://github.com/hc-ui/wholock| wholock | PowerToys File Locksmith | Sysinternals Handle | openfiles | 资源监视器 | fuser / lsof | psutil 脚本 | |
|---|---|---|---|---|---|---|---|
| Windows | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| Linux / macOS | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| 安装方式 | pip install |
要装整个 PowerToys | 手动下载 | 系统自带 | 系统自带 | 系统自带 | 需编译依赖 |
| 免管理员 | ✅ 多数场景 | 部分 | ❌ 必须管理员 | ❌ 要开全局标志并重启 | ✅ | 部分 | 部分 |
| JSON 输出 | ✅ | ✅ | CSV | ❌ | ❌(图形) | ❌ | 自己写 |
| 内置杀进程/等待 | ✅ 都有 | ✅ 都有 | ❌ | ❌ | 只能杀 | fuser -k 只能杀 | 自己写 |
| Python API | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ~ |
| 开源 | ✅ MIT | ✅ | ❌ | — | — | ✅ | ✅ |
wholock 填补的空缺:一条 pip install 三平台通用、日常场景免提权,
而且是唯一能当 Python 库调用的——在你自己的代码、测试、CI 里直接查占用。
wholock [选项] 路径 [路径 ...]
输出:
-j, --json 输出机器可读的 JSON
--pids 只输出 PID,空格分隔(fuser 风格)
-q, --quiet 不输出任何内容,只返回退出码
--no-color 关闭彩色输出(也支持 NO_COLOR 环境变量)
动作:
-k, --kill 结束占用进程
-y, --yes 杀进程前不再询问确认
-f, --force POSIX 上用 SIGKILL 代替 SIGTERM;并允许结束关键进程
-w, --wait 等待路径被释放
-t, --timeout 秒 等待超时时间(默认:一直等)
--interval 秒 轮询间隔(默认 0.5)
退出码(方便写脚本,参考 fuser 惯例):
| 码 | 含义 |
|---|---|
| 0 | 查到占用进程(查询模式)/ 路径已释放(--kill、--wait) |
| 1 | 没有任何进程占用 |
| 2 | 错误:路径不存在、权限问题、参数错误 |
| 3 | --wait 等待超时 |
| 4 | --kill 没能释放路径 |
常用示例:
wholock data.db # 谁占着它?
wholock --json data.db | jq '.results[0].lockers[].pid'
wholock --pids data.db # 输出如 "1234 5678"
wholock -k -y build/output.dll # 直接释放,不询问
wholock -w -t 60 视频.mp4 && mv 视频.mp4 done/ # 等释放后再操作
wholock ~/Downloads # 目录也支持日志里见过这个吗?
PermissionError: [WinError 32] The process cannot access the file because
it is being used by another process: 'C:\\app\\cache.db'
现在可以把"凶手"也记进日志:
import os
import wholock
try:
os.replace("cache.db.tmp", "cache.db")
except PermissionError:
raise RuntimeError(wholock.explain("cache.db"))
# RuntimeError: cache.db is in use by antivirus.exe (pid 4242)完整 API——四个函数、一个数据类:
import wholock
# 1) 查询:返回 Locker(pid, name, exe, kind, via, user, started) 列表
for p in wholock.who_locks("报告.xlsx"):
print(p.pid, p.name, p.via)
# 2) 一句话解释:适合放进异常信息,文件不存在也不会抛错
msg = wholock.explain("报告.xlsx")
# 3) 等待释放:释放返回 True,超时返回 False
if wholock.wait_until_free("报告.xlsx", timeout=30):
process_file("报告.xlsx")
# 4) 结束占用进程(带 PID 复用保护)
for locker, ok, message in wholock.kill_lockers("报告.xlsx"):
print(locker.pid, ok, message)典型用法:给 os.replace 加重试、Windows 测试 teardown 清理临时目录、
CI 里等待构建产物被释放、给安装器/更新器输出更有用的报错。
| 系统 | 后端 | 说明 |
|---|---|---|
| Windows | Restart Manager API(ctypes 调 Rstrtmgr.dll) |
Windows 自家"文件被占用"对话框背后的官方机制,快、无竞态,多数场景不需要管理员 |
| Linux | 直接扫描 /proc |
不依赖 lsof,能看到打开的 fd、内存映射、工作目录、可执行文件、chroot |
| macOS | 系统自带 lsof |
解析机器可读的 -F 输出 |
via 字段告诉你进程是怎么占用的:handle(Windows 句柄)、open(打开的 fd)、
mmap(内存映射)、cwd(它的工作目录)、exe(正在运行的程序本体)、root(chroot)。
杀进程的安全护栏:
- 永远不会杀掉调用者自己和系统 PID(≤ 4)。
- Windows/Linux 上先校验进程启动时间再杀,PID 被复用也不会误杀。
- Windows 关键系统进程默认跳过,除非
--force。 - 僵尸进程视为已死(它们不占任何文件)。
- 未提权时可能看不到其他用户的进程。查不到结果且存在这种可能时,
wholock会提示你。 - Windows 上查询目录时检查的是目录里的文件(上限 10,000 个)。
如果一个进程只是
cd进了该文件夹,Restart Manager 看不到——文件夹删不掉时, 先找找开着它的资源管理器窗口或终端。 - Windows Restart Manager 无法检查 ≥260 字符的路径(实测
\\?\前缀也无效)。wholock对这类文件会给出明确报错;目录里遇到超长文件则跳过并警告。 - macOS 上扫描大目录较慢——那是
lsof +D的特性。 - 网络共享上的文件,系统能暴露多少就能查到多少。
- Windows 的
--kill永远是强制的(TerminateProcess),Windows 没有温和的跨进程 SIGTERM。
为什么不用 psutil?
psutil 是编译依赖;要找文件占用者得遍历每个进程的 open_files()
(慢、常被权限挡、看不到内存映射),而且没有 CLI。
wholock 用操作系统官方推荐的方式直接问系统。
需要管理员/root 吗? 自己的进程通常不需要——"构建产物覆盖不了"、"Excel 还开着 CSV" 这类日常场景直接可用。系统服务和其他用户的进程可能需要提权才能看到。
能只关句柄不杀进程吗?
故意不支持。在活进程里强关句柄(Sysinternals Handle 的 -c)可能把那个进程
搞出更严重的状态损坏。杀掉或等待,两条路都是安全的。
欢迎提 issue 和 PR。测试矩阵覆盖 Windows / Linux / macOS × Python 3.9–3.14:
python -m unittest discover -s tests -t . -v如果 wholock 救回了你的文件(或你的下午),点个 ⭐ 能帮更多人找到它。