lsof 只告诉你 node,不告诉你是哪个项目
Pier 复盘:端口列表里全是 node、python、com.docker.backend。用完整 argv 指纹识别 40 种开发服务、proc_pidinfo 拿工作目录、docker ps 反查容器名——三条信息拼出「这个端口到底是谁」。
Pier 的端口 tab 一直有个尴尬:lsof 能列出谁在监听,但答案全是 node、node、python、com.docker.backend。开着三个前端项目的人看到三行 node,等于什么都没看到。真正的问题是「3000 上跑的是哪个项目的 Vite」,而不是「3000 上有个 node」。
2.0 把这个问题拆成三条正交的信息,各自有独立的数据来源。
信息一:这是什么服务——argv 指纹
进程名没有信息量,但完整命令行有:node /work/shop/node_modules/.bin/vite 一眼就是 Vite。做法是 lsof 拿到 pid 后,一次 ps -o command= 批量拉全部 argv(约 30ms),过一张规则表:
Rule(executables: ["node", "bun"], #"(^|/)vite( |$)|/bin/vite"#,
.init(id: "vite", displayName: "Vite", icon: "bolt.fill", category: .frontend)),
Rule(executables: ["node", "bun"], #"\bnext(-server)?( |$)|\bnext (dev|start|build)\b"#,
.init(id: "nextjs", displayName: "Next.js", ...)),
每条规则两层过滤:可执行名 basename 前缀匹配(node 同时覆盖 node22,bun 覆盖 bun-1.x),再跑一条预编译的正则(规则表会对每个端口顺序扫描,每次现编译正则是白烧 CPU)。规则按 specificity 从窄到宽排,第一条命中即返回。目前约 40 条,覆盖 Next.js、Vite、Django、Rails、Postgres、Redis 这些常客——每条规则五分钟就能加,规则表的边际成本几乎为零。
信息二:这是谁开的——来源分类
「是什么」和「谁开的」是两个正交问题:同一个 vite 端口,可以是你自己 npm run dev 起的,也可以是 Cursor 里的 AI 帮你起的。第二根轴:
enum SourceCategory { case project, aiTool, system, otherApp }
判定按先到先得排优先级:root/UID<500 归 system;可执行路径落在 AI 工具的 .app 包内(Cursor.app、Claude.app、Windsurf.app……一张白名单)归 aiTool;命中服务指纹、或 cwd 在用户 home 的项目区,归 project;剩下兜底 otherApp。列表按 project 浮顶排序——你自己的东西永远在最上面。
这里有个诚实记录的已知局限:Cline、Copilot 这类 VSCode 扩展形态的 AI 工具没有独立 .app,端口由宿主 VSCode 进程持有,识别不到,会落进 otherApp。要修需要做 parent process tracking,暂时不值得。
信息三:在哪个目录——以及 Docker 的洞
工作目录用 proc_pidinfo(PROC_PIDVNODEPATHINFO) 系统调用批量拿(全部 pid 循环一遍 10–50ms,不用 fork lsof)。有了 cwd,端口行可以直接显示项目路径,点击在 Finder 里定位,右键用终端或任何已安装的编辑器打开——编辑器列表是探测式的,只列出磁盘上真实存在的 .app,路径挪了用 bundle ID 兜底。
但 Docker 容器把这条路挡死了:lsof 看到的 pid 是 com.docker.backend 这种 proxy 进程,容器信息一点没有。补法是反查:
docker ps --format "{{.Names}}\t{{.Ports}}"
→ "shop_nginx_1 0.0.0.0:8080->80/tcp"
→ 正则 :(\d+)-> 抽 host port → [8080: "shop_nginx_1"]
凡是 proxy 进程占用的端口,chip 直接显示容器名。两个防御细节:只有进程列表里真的存在 docker/orbstack/vpnkit 进程才去 fork docker ps(没装 Docker 的用户零开销);daemon 没启动时 CLI 会无限等连接,挂一个 6 秒 SIGKILL 兜底,超时就当没有容器。
拼装:三路并行
三条信息来源独立,没理由串行。lsof 主扫描、ps 批量 argv、proc_pidinfo 循环、docker ps 全部 async let 并发,refresh 的总耗时等于最慢的一路——早期版本 docker 查询是串行的,daemon 一卡整个端口列表跟着卡 6 秒,这就是教训。
复盘
- 原始数据没信息量时,找正交的第二数据源,而不是在原始数据上硬挖。进程名挖不出 Vite,argv 一眼就是;
- 分类轴不要混。「是什么服务」和「谁开的」各自一个 enum,UI 上才能分别筛选,规则也各自演进;
- 每个 fork 出去的子进程都是负债:先判断有没有必要跑(docker proxy 探测),再给必跑的挂超时(6s SIGKILL)——外部工具的行为不归你管,你只能管自己等多久;
- 识别不了的场景写进注释,不要假装覆盖了。VSCode 扩展那条局限就明明白白写在白名单旁边。
留言