
sonar v0.4.1
用于检查和管理监听本地主机端口的服务的CLI工具
██████ ███ ██████ ██ ██████
██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ████
██ ██ ██ ██ ██ ██ ██ ██
██████ ███ ██ ██ ██ ██ ██ ██
了解你的机器上正在运行什么。
Sonar 会显示所有在 localhost 上监听的内容,并将其整理得井井有条:每个端口
都属于一个组——通常是它启动时所在的仓库——而在该组内部,
则属于一个命名的服务。使用 sonar start 启动你的开发服务器,
整个项目就变成一个整体,你可以将其作为树状结构列出、
等待、跟踪日志,并用一条命令停止。Docker 容器、Compose
项目以及你手动启动的进程也会被自动识别,无需任何
配置。
$ sonar list --tree
my-app (3 ports, running) ~/code/my-app
├─ 5432 db postgres:17 http://localhost:5432
├─ 5173 frontend vite (v5.4) http://localhost:5173
└─ 8000 api uvicorn app:app http://localhost:8000
ungrouped (1 port)
└─ 3000 next-server (v16.1.6) http://localhost:3000
安装
Homebrew (macOS / Linux)
brew install raskrebs/sonar/sonar
Homebrew 6 会拒绝来自第三方 tap 的 formula,直到你信任该 tap 一次
(Error: Refusing to load formula raskrebs/sonar/sonar from untrusted tap):
brew trust raskrebs/sonar
安装脚本
curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | bash
下载最新二进制文件到 ~/.local/bin,并在需要时将其添加到你的 PATH 中。重启终端或执行 source ~/.zshrc。
在 Windows 上(PowerShell):
irm https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.ps1 | iex
自定义安装位置:
curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | SONAR_INSTALL_DIR=/usr/local/bin bash
安装特定版本:
curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | SONAR_VERSION=vX.Y.Z bash
$env:SONAR_VERSION="vX.Y.Z"; irm https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.ps1 | iex
使用 Go
go install github.com/raskrebs/sonar@latest
Shell 补全(端口号 Tab 补全):
sonar completion zsh > "${fpath[1]}/_sonar" # zsh
sonar completion bash > /etc/bash_completion.d/sonar # bash
sonar completion fish | source # fish
六十秒
在仓库根目录下用 sonar.yaml 描述你的项目如何运行,
以替代 dev.sh:
name: my-app
services:
- name: db
cmd: docker compose up db
port: 5432
- name: api
cmd: uv run uvicorn app:app --port ${port}
port: auto
depends_on: [db]
- name: frontend
cmd: npm run dev -- --port ${port} --strictPort
port: auto
depends_on: [api]
env:
VITE_API_URL: ${api.url}
然后启动它:
sonar start
✓ db http://localhost:5432 pid 41022 ~/.config/sonar/logs/my-app/db.log
✓ api http://localhost:21408 pid 41040 ~/.config/sonar/logs/my-app/api.log
✓ frontend http://localhost:21409 pid 41077 ~/.config/sonar/logs/my-app/frontend.log
3 started
following the logs; Ctrl+C stops the 3 services started here
api | INFO: Uvicorn running on http://127.0.0.1:21408
frontend | VITE v5.4 ready in 312 ms
Sonar 会为每个 port: auto 服务挑选一个空闲端口,并告知每个服务其他服务的位置,因此第二个工作树可以与第一个并行运行,而不会发生任何端口冲突。sonar start -d 在后台执行相同的操作,而 sonar down 会停止它并归还端口。在另一个终端中:
sonar list --tree
my-app (3 ports, running) ~/code/my-app
├─ 5432 db postgres:17 http://localhost:5432
├─ 21408 api uvicorn app:app http://localhost:21408
└─ 21409 frontend vite (v5.4) http://localhost:21409
没有 sonar.yaml?sonar start -- npm run dev 会将一条命令作为命名服务运行,而 sonar list 和 sonar kill 可作用于任何正在监听的对象。
下面标记为 # check 的示例会在每次 CI 运行时,由 scripts/readme-check.sh 针对全新构建执行。
命令
sonar list
sonar list
sonar list --tree
sonar list --group my-app
sonar list --json
# check
sonar list --stats # CPU, memory, threads, uptime, state
sonar list --health # HTTP health checks
sonar list --filter docker # only Docker ports
sonar list --sort name # port | pid | name | type
sonar list -a # include desktop apps
sonar list -c port,process,group,cpu,mem
sonar list --host user@server # scan a remote machine over SSH
默认列是 port、process、group、container、image、
containerport、url,其中 process 显示你为端口指定的名称
(sonar rename),然后是服务名称,最后是检测到的内容。
可用列:port、process、pid、type、url、group、cpu、
mem、threads、uptime、state、connections、health、latency、
container、image、containerport、compose、project、user、bind、
ip。
恰好正在监听的桌面应用和系统服务——Figma、Discord、
Spotify、ControlCenter、macOS .app 包、/System/Library/ 守护进程——
除非你传入 -a,否则会被隐藏。
sonar start
从项目的 sonar.yaml 启动项目:
sonar start # every service in the nearest sonar.yaml
sonar start api frontend # only these (they still wait for their dependencies)
sonar start ../other-project # the sonar.yaml in or above another directory
sonar start -d # in the background, like `sonar up`
在前台,sonar 通过守护进程启动服务,在每行日志前加上服务名称来跟踪其日志,当你按下 Ctrl+C 时,它会停止自己启动的服务。已经在运行的服务保持不变,一旦 sonar 启动的每个服务都退出,sonar 会自行返回。
-- 之前的参数指定一个目录或服务;-- 之后的所有内容都是一个命令。或者将一条命令作为组中的命名服务运行:
sonar start -- npm run dev
sonar start --group my-app --name frontend -- npm run dev
sonar start --port 5173 -- npm run dev # expected port, before it binds
sonar start --detach --name api -- uv run uvicorn app:app
sonar start --list
无需传入任何参数:
- Group —
--group,否则取最近的sonar.yaml中的name,否则取 git 根目录的目录名(工作树会变成repo@worktree),否则取当前目录 的名称。 - Name —
--name,否则取sonar.yaml中cmd匹配的服务,否则 从命令推断(npm run dev→dev,uv run api→api,python -m uvicorn→uvicorn,./dev.sh→dev.sh)。 - Port —
--port只是一个提示,并非绑定:在端口实际监听之前, 运行状态会显示为starting,守护进程用它来将进程与端口进行匹配。
子进程继承 stdin、stdout、stderr、cwd 和环境变量,外加
SONAR_GROUP、SONAR_NAME 和 SONAR_RUN_ID。它会获得自己的进程组,
因此 sonar kill 会终止整棵进程树——包括开发服务器及其监视器和
工作进程。Ctrl+C 会被转发,sonar 以子进程的退出码退出。
--detach 会立即返回,并将输出写入
~/.config/sonar/logs/<group>/<name>.log。--list 显示 sonar 启动了什么,
并在其下方显示此后已结束的内容——包括退出码,以及它是崩溃了
还是被停止了(--json 用于机器可读形式):
sonar start --list
sonar start --detach --name demo --port 8123 -- sleep 5
sonar start --list --json
# check
sonar.yaml
项目在仓库根目录的 sonar.yaml 中为自身及其服务命名。该文件是可选的——即使没有它,sonar 也会按 git 根目录进行分组——并且它应当被提交:
name: my-app
services:
- name: db
cmd: docker compose up db
port: 5432
health: /
description: Postgres 17
icon: database
color: "#4f8cc9"
- name: api
cmd: uv run uvicorn app:app --port 8000
cwd: backend
port: 8000
health: /healthz
depends_on: [db]
- name: frontend
cmd: npm run dev
port: 5173
depends_on: [api]
ports: [9229] # ports that belong to this project without a service
name— 组名。不能包含斜杠,不能包含空白字符。cmd、cwd、port—sonar start如何启动服务。cwd相对于该文件,且不能逃逸其所在目录。health— 服务运行期间守护进程轮询的 HTTP 路径,因此服务可能处于运行中但尚未健康。它会报告ok、fail或unknown,并附带失败原因。description、icon、color— 桌面应用的自由格式元数据;sonar 从不推断它们。depends_on— 启动顺序。指定文件中不存在的服务,或形成循环,都是错误;无效文件只报告一次,且绝不会阻止扫描。env— sonar 启动服务时为该服务设置的环境变量。
port: auto 与引用
服务可以将端口交给 sonar 决定,并通过其环境变量找到其他服务,而不是硬编码 localhost:8080:
services:
- name: api
cmd: uv run uvicorn app:app --port ${port}
port: auto
- name: frontend
cmd: npm run dev -- --port ${port} --strictPort
port: auto
depends_on: [api]
env:
VITE_API_URL: ${api.url}
sonar start 会为它启动的每个 port: auto 服务占用一个空闲端口:在同一个检出中每次都是同一个端口,而在其他每个检出中都是不同的端口,因此工作树永远不会与主检出冲突。服务会以 PORT、SONAR_PORT 和 ${port} 的形式获得该端口,并且必须使用它——读取 PORT,或在命令行中传递 ${port}。已经在运行的服务会保留其端口,因此稍后启动的服务仍然能找到它。已分配端口背后的占用从最后一次启动起持续一天;claims.ttl 可以改变这一点(参见 The daemon)。
${port} 和 ${url}(http://localhost:<port>)是服务自身的;${<service>.port} 和 ${<service>.url} 是另一个服务的。它们可用于 cmd 和 env 中。引用文件中不存在的服务,或引用没有端口的服务,会在加载文件时报错。${ 和 } 之间的其他内容,如 ${HOME},会完全按原样保留。