Day 284
幫 port 請一個管理員
我一直以來都習慣讓家裡不關機的 MacBook Pro 幫我把產出的文件變成網頁,人在外面就用 MacBook Air 打開來看。Pro 上同時跑著不只一個專案,每個專案都會開自己的網頁服務,各自佔一個 port(連接埠:同一台電腦上用來區分不同服務的號碼,網址最後的 :8790 就是它)。
今天終於遇到了:同一個網址 http://dawsons-macbook-pro:8790 被兩個服務同時佔著。一個是原本就在跑的 hidock-hub,另一個是後來開起來的 ai-workflow-talk。兩邊都沒有報錯,我從 Air 打開那個網址,看到的是後來的那一個。
第二個服務開得起來,本身就是問題。所以我需要一個管 port 的管理員:誰在用哪個號碼、哪個號碼已經被預約,一眼就看得到;AI agent(會自己寫程式、跑指令的 AI 助手)要開新服務之前,也得先經過它。我跟 Claude 一起做了一個,叫 portman。
這篇分享:兩個服務怎麼會同時佔住同一個 port、portman 怎麼管、以及怎麼讓 agent 照規矩拿 port。
為什麼第二個服務沒有被擋下來
一般印象中,port 被佔了,第二個程式會跳出 address already in use,開不起來。但這只在兩個程式綁同一個位址(程式在哪個位址上等人連進來)時才會發生。
hidock-hub 綁的是「這台電腦的所有位址」,ai-workflow-talk 只綁 Tailscale(把我幾台裝置串成私人網路的工具)給 Pro 的那個位址 100.108.103.101。macOS 把這兩種當成不同的東西,兩個都讓它綁上。有人連進來時,綁得比較精確的那一個優先接走。結果就是:我在 Pro 本機打開 localhost:8790 看到 hidock-hub,從 Air 透過 Tailscale 連進來,看到的卻是 ai-workflow-talk。
這也代表一個常見的檢查方式不可靠:agent 開服務前常會先試著綁一下那個 port,綁得上就當作沒人在用。綁得上,不代表沒人在用。
portman 的清單今天還抓到另一組:兩個 devtown-backend 一個綁 127.0.0.1、一個綁 [::1](兩個都是「這台電腦自己」的寫法),都在 4567。打 localhost:4567 可能連到其中任何一個。
portman 管三件事:看得到、用名字預約、停得乾淨
看得到。portman ls 列出其他機器連得到的服務:port、綁在哪個位址、名字、跑了多久、Air 要用的網址。有衝突的放在最上面,只綁本機的也會列進來,像這樣:
CONFLICTS
4567 devtown-backend (pid 88034) answers [::1] and devtown-backend (pid 97215) answers 127.0.0.1; "localhost:4567" may reach either
PORT BIND KIND NAME UP URL / COMMAND
4317 all service 100days-studio 1d http://dawsons-macbook-pro:4317
8790 all service hidock-hub 2d http://dawsons-macbook-pro:8790
8800 tailnet dev ai-workflow-talk 1h http://dawsons-macbook-pro:8800
ai-workflow-talk 現在搬到 8800 了。同一份清單也有網頁版,在 Air 上直接打開 http://dawsons-macbook-pro:8899 就看得到,只有這台 Mac 和 Tailscale 裡的裝置連得到。
用名字預約。portman claim hidock-hub 幫這個名字留一個 port,之後同一個名字再問,拿到的還是同一個號碼。portman run 更直接:預約、用那個號碼把服務開起來、印出 Air 要用的網址,一個指令做完。
停得乾淨。portman stop ai-workflow-talk 會連同它底下的子程式一起停掉,但不會順手把開它的終端機或 Claude Code 也關掉。
讓 agent 照規矩拿 port
真正會開一堆網頁服務的是 agent,不是我。所以光有工具不夠,要讓 agent 每次都用它。我分兩層做。
第一層是告訴它。在 Claude Code 的全域規則 ~/.claude/CLAUDE.md 裡寫清楚:
On the MacBook Pro, never pick a port by hand: start servers with
`portman run <name> --bind tailnet --detach -- <command using {port} and {ip}>`.
A specific-IP bind can silently steal traffic from a server on *:<port>,
so a port that merely binds is not proof it is free.
If the port hook blocks a start, choose another port; never stop the server that holds it.
第二層是攔住它。規則寫了,agent 還是可能忘記,所以在 ~/.claude/settings.json 掛一個 hook(Claude Code 每次執行指令前會先呼叫的檢查程式):
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.local/bin/portman hook", "timeout": 5 }]
}
]
}
agent 想跑 python3 -m http.server 8790 的時候,portman 會擋下來,並直接告訴它該怎麼改:
portman: port 8790 is reserved for "hidock-hub"; another server there would take its traffic.
Do not stop it. Use a port portman hands out instead:
portman run tmp --bind tailnet --detach -- python3 -m http.server {port}
這段回覆有兩個重點。一是「Do not stop it」:agent 遇到 port 被佔,第一個反應常常是把佔著的那個程式關掉,但那個程式就是我原本在用的服務。二是附上改好的指令,agent 照抄就能換到一個沒人用的 port。
hook 每次執行指令前都會跑,所以 portman 會先看一眼指令裡有沒有 port、serve 這類字眼,跟開服務無關的指令直接放行,不拖慢平常的操作。
什麼情況用不到
只有一個專案、只在本機用 localhost 開來看,port 撞到的機會很低,用不到這一套。portman 現在也只認 macOS 內建的工具(lsof、netstat 和 launchd),換到 Linux 要改寫。程式碼已經開源在 GitHub:https://github.com/andrew54068/portman
以前我自己挑號碼,agent 也自己挑號碼;現在號碼由 portman 發,誰都不用猜哪個 port 還空著。



