返回更新列表
新发布Sep 4, 2026

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)```sh
brew install raskrebs/sonar/sonar

Homebrew 6 会拒绝来自第三方 tap 的 formulae,直到你信任该 tap 一次 (Error: Refusing to load formula raskrebs/sonar/sonar from untrusted tap):```sh brew trust raskrebs/sonar

### 安装脚本```sh
curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | bash

下载最新二进制文件到 ~/.local/bin,如有需要会将其添加到你的 PATH 中。重启终端或执行 source ~/.zshrc

在 Windows 上(PowerShell):```powershell irm https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.ps1 | iex

自定义安装位置:```sh
curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | SONAR_INSTALL_DIR=/usr/local/bin bash

安装特定版本:```sh curl -sfL https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.sh | SONAR_VERSION=vX.Y.Z bash

请提供需要翻译的 Markdown 内容。```powershell
$env:SONAR_VERSION="vX.Y.Z"; irm https://raw.githubusercontent.com/raskrebs/sonar/main/scripts/install.ps1 | iex

使用 Go```sh

go install github.com/raskrebs/sonar@latest

Shell 补全(Tab 补全端口号):```sh
sonar completion zsh > "${fpath[1]}/_sonar"   # zsh
sonar completion bash > /etc/bash_completion.d/sonar  # bash
sonar completion fish | source                 # fish

六十秒

在你的 dev.sh 中为命令加上 sonar start 前缀:```sh #!/usr/bin/env bash sonar start --name db --port 5432 -- docker compose up db & sonar start --name api --port 8000 -- uv run uvicorn app:app & sonar start --name frontend --port 5173 -- npm run dev & wait

组名来自仓库,因此无需其他配置。
在另一个终端中:```sh
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

完成后,停止整个项目——服务器、监视器和工作者:```sh
sonar kill -g my-app

以下示例中标记为 # check 的命令会在每次 CI 运行时由 scripts/readme-check.sh 针对全新构建执行。

命令

`sonar list````sh

sonar list sonar list --tree sonar list --group my-app sonar list --json

check

## 安装

### 通过 pip 安装

```bash
pip install pywhisker

通过源码安装

git clone https://github.com/ShutdownRepo/pywhisker
cd pywhisker
python3 setup.py install

用法

usage: pywhisker.py [-h] [-v] -t TARGET [-u USERNAME] [-p PASSWORD] [-d DOMAIN]
                    [-l] [-a ADD] [-e EDIT] [-r REMOVE] [-s SID] [-f FILE]
                    [-o OUTPUT] [--hashes HASHES] [--no-pass] [-k]
                    [--dc-ip DC_IP] [--use-ldaps] [--path PATH]

pywhisker 是一个用于操作目标对象的 msDS-KeyCredentialLink 属性的 Python 工具。该工具允许你添加、编辑、移除和列出目标对象的密钥凭据链接。

参数

参数描述
-h, --help显示帮助信息并退出
-v, --verbose启用详细输出
-t TARGET, --target TARGET目标对象(用户或计算机)
-u USERNAME, --username USERNAME用于认证的用户名
-p PASSWORD, --password PASSWORD用于认证的密码
-d DOMAIN, --domain DOMAIN目标域名
-l, --list列出目标对象的 msDS-KeyCredentialLink 属性
-a ADD, --add ADD向目标对象添加新的密钥凭据链接
-e EDIT, --edit EDIT编辑目标对象的现有密钥凭据链接
-r REMOVE, --remove REMOVE移除目标对象的密钥凭据链接
-s SID, --sid SID要移除的密钥凭据链接的 SID
-f FILE, --file FILE包含密钥凭据链接的文件
-o OUTPUT, --output OUTPUT输出文件路径
--hashes HASHESNTLM 哈希(LM:NT)
--no-pass不提示输入密码
-k, --kerberos使用 Kerberos 认证
--dc-ip DC_IP域控制器的 IP 地址
--use-ldaps使用 LDAPS 而非 LDAP
--path PATH证书文件的路径

示例

列出目标对象的密钥凭据链接

pywhisker.py -d domain.local -u user -p password -t target_user --list

向目标对象添加新的密钥凭据链接

pywhisker.py -d domain.local -u user -p password -t target_user --add

编辑目标对象的现有密钥凭据链接

pywhisker.py -d domain.local -u user -p password -t target_user --edit

移除目标对象的密钥凭据链接

pywhisker.py -d domain.local -u user -p password -t target_user --remove

致谢

  • 该工具基于 pywhisker 项目,灵感来源于 Whisker 工具。
  • 特别感谢 Dirk-jan Mollema 提供的 pyldap 库,该库用于 LDAP 通信。```sh 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`

在某个组中作为命名服务运行一条命令:```sh
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,否则使用最近的 .sonar.yaml 中的 name,否则使用 git 根目录的目录名(工作树变为 repo@worktree),否则使用 当前目录的名称。
  • 名称--name,否则使用 .sonar.yamlcmd 匹配的服务,否则 从命令推断(npm run devdevuv run apiapipython -m uvicornuvicorn./dev.shdev.sh)。
  • 端口--port 只是一个提示,并非绑定:运行会显示为 starting 直到端口真正开始监听,守护进程用它来将进程与端口匹配。

子进程继承 stdin、stdout、stderr、cwd 和环境变量,外加 SONAR_GROUPSONAR_NAMESONAR_RUN_ID。它拥有自己的进程组, 因此 sonar kill 会终止整棵进程树——包括开发服务器及其监视器和 工作进程。Ctrl+C 会被转发,sonar 以子进程的退出码退出。

--detach 会立即返回并将输出写入 ~/.config/sonar/logs/<group>/<name>.log--list 显示 sonar 启动的内容 (--json 用于机器可读格式):```sh sonar start --list sonar start --detach --name demo --port 8123 -- sleep 5 sonar start --list --json

check

### `.sonar.yaml`

项目在仓库根目录下的 `.sonar.yaml` 文件中定义自身及其服务的名称。该文件是可选的——没有它时,sonar 会按 git 根目录进行分组——并且它应当被提交到版本控制中:```yaml
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 — 组名称。不允许斜杠或空白字符。
  • cmdcwdportsonar up 启动服务的方式。cwd 相对于文件路径,且不能超出其所在目录。
  • health — 守护进程在服务运行期间轮询的 HTTP 路径,因此服务可能处于运行中但尚未健康状态。它会报告 okfailunknown,并附带失败原因。
  • descriptioniconcolor — 桌面应用的自由格式元数据;sonar 从不推断这些内容。
  • depends_on — 启动顺序。引用文件中不存在的服务或形成循环依赖会报错;无效文件只会报告一次,且绝不会中断扫描。

.sonar.yml 会按该拼写方式被读取;sonar init 始终写入 .sonar.yaml。守护进程会监视其已知的项目,并在无需重启的情况下拾取对文件的编辑。sonar 所做的每一次编辑——无论是来自桌面应用、sonar groups addrenameremove,还是来自代理——都会经过守护进程,由其基于自身的语法树重新渲染文件,因此注释、键顺序和布局在添加、重命名或移除服务的编辑中得以保留,就像元数据变更时一样。唯一的例外:对齐尾随注释的多余空格(cmd: x # note)会压缩为一个,因为 YAML 库保留了注释但未保留其列位置。

`sonar up````sh

sonar up # the .sonar.yaml at or above this directory sonar up my-app # a group by name sonar up --only api,frontend sonar up --json

启动该组 `.sonar.yaml` 中声明的每个服务,按 `depends_on` 顺序执行:
一个服务会等待其依赖项声明的端口就绪后才启动,而
已经在监听的服务会被跳过。每个服务都在其自己的进程组中独立运行,
其输出位于 `~/.config/sonar/logs/<group>/<service>.log`。```
  ✓ db        pid 41022  ~/.config/sonar/logs/my-app/db.log
  - api       already running
  ✓ frontend  pid 41108  ~/.config/sonar/logs/my-app/frontend.log

2 started, 1 already running

一个服务启动失败时,会在单独一行中报告,并使命令以非零状态退出,无论其他服务状态如何。再次使用 sonar kill -g my-app 停止所有服务。sonar up 需要守护进程,如果它尚未运行则会启动它。

sonar groups 和 `sonar init````sh

sonar init --dry-run sonar init --service api:8000:/healthz --service web:5173 sonar groups sonar groups --json

group=$(basename "$PWD") # sonar init names the group after the directory sonar groups add "$group" worker --port 9000 --cmd 'uv run worker' --depends-on api sonar groups rename "$group" worker jobs sonar groups remove "$group" jobs

check

`sonar groups` 会列出 sonar 能看到的每个组,以及每个名称的来源:
`manual`(你用 `sonar assign` 固定的)、`start`(一次 `sonar start` 运行)、
`file`(一个 `.sonar.yaml`)或 `auto`(git 根目录或 Compose 项目)。
`sonar groups <name>` 会显示一个组的端口和服务,以及那些已声明但未运行的服务。

`sonar init` 会根据当前正在监听的内容在 git 根目录写入一个 `.sonar.yaml` —— 桌面应用和 1024 以下的端口会被排除在外。没有 `--force` 时它会拒绝覆盖,而 `--dry-run` 会打印文件内容而不是写入。
`--merge` 会追加到已存在的文件而不是拒绝,`--service name:port[:health]` —— 可重复使用 —— 会写入你指定的服务而不是它发现的那些,同时保留它为你保留的端口所猜测的命令。`--force` 和 `--merge` 是互斥的。

`sonar groups add <group> <name> --port N` 会将一个服务追加到该组的
`.sonar.yaml` 中,其余部分可用 `--cmd`、`--cwd`、`--health`、`--description`、`--icon`、
`--color` 以及可重复的 `--depends-on` 来指定。`sonar groups
rename <group> <old> <new>` 会在文件中的任何位置重命名一个条目,包括 `depends_on`
引用,而 `sonar groups remove <group> <name>` 会删除一个条目,并将其从每个提到它的 `depends_on` 中移除。这三个命令都会拒绝会导致文件无效的编辑 —— 重复的名称、已被其他服务占用的端口、不存在的服务 —— 而且在整个编辑被确认无误之前,它们不会写入任何字节。

写入操作由守护进程完成,这就是为什么无论编辑来自 CLI、桌面应用还是代理,文件返回时都会保留其注释和键顺序。`sonar groups add`、`rename` 和 `remove` 需要守护进程,如果它尚未运行则会启动它;这三个名称是子命令,因此一个实际名为 `add` 的组需要用 `sonar groups --json` 来读取。

### `sonar kill````sh
sonar kill 3000                            # SIGTERM, then SIGKILL after 5s
sonar kill 3000 5432 -f                    # SIGKILL both straight away
sonar kill 3000 --tree                     # the listener and everything below it
sonar kill --pid 12345 --tree              # by process id
sonar kill -g my-app                       # a whole group, confirms unless -y
sonar kill --all --filter docker -y        # every container publishing a port
sonar kill --all --project my-app          # one Compose project
sonar kill 3000 --ip 127.0.0.1             # one bind address of several
sonar kill --all --dry-run --json          # the plan for the whole machine

--dry-run 接受任意选择器,但不会更改任何内容:它会先打印 kill 将要执行的操作(子进程优先),然后让所有进程继续运行。端到端地针对您自己的监听器运行:```sh sonar start --detach --name plan --port 8231 -- sonar map 3000 8231 sonar wait 8231 sonar kill 8231 --dry-run --json # the plan; the mapping keeps running sonar kill 8231 -y # and now for real

check

位置参数会被读取为端口,仅当该端口号上没有进程在监听时,才会被读取为 PID。`-g` 会匹配解析后的组、旧版运行标签或 ID,以及 Compose 项目,匹配时不区分大小写。

如果某个进程忽略 SIGTERM,且 `--grace`(5 秒)之后端口仍在监听,则会向其发送 SIGKILL;`--no-escalate` 可关闭此行为。子进程会先于父进程收到信号,因此进程树会按顺序关闭。Docker 容器通过 `docker stop` 停止,绝不会收到信号。由 `sonar start` 启动的监听器会与其进程组一起被停止。

`--json` 会为每个进程输出一行:
`{port, bind_address, pid, name, method, ok, error}`,其中 `method` 为
`sigterm`、`sigkill`、`docker_stop`、`map_stop` 或 `none`。空扫描以 0 退出;未知组以 1 退出。

### `sonar map````sh
sonar map 6873 3002        # also serve the service on 6873 from port 3002

在前台运行一个 TCP 代理,直到你将其停止。sonar kill 会将其停止的映射报告为 map_stop

sonar renamesonar assign、`sonar history````sh

sonar rename 3000 storefront # a name of your own, survives restarts sonar rename 3000 --clear sonar assign 3000 my-app # pin a port to a group by hand sonar assign 3000 --clear sonar history # everything that came up, went down, restarted sonar history 3000 --since 24h --limit 20

请提供需要翻译的 Markdown 内容。```sh
sonar history --since 1h
sonar history --json
# check

名称和引脚存储在 sonar 的数据库中,以关于端口最具体的信息为键:运行(run:<group>/<name>)、容器(docker:<project>/<service>)、工作目录,最后是端口号。重命名的开发服务器在重启后仍保留其名称;仅固定到端口 3000 的名称适用于任何在该端口响应的内容。以下三个命令需要守护进程,如果它未运行则会启动它。

读取端口```sh

sonar info 3000 # command, user, bind, stats, health sonar logs 3000 # tail; docker logs for containers sonar wait 5432 3000 --timeout 60s # block until ready sonar wait 5432 --http=/health # wait for HTTP 200-399, not just TCP sonar next 3000 # first free port from 3000 sonar next 3000-3100 -n 3 # three consecutive free ports sonar graph # who is connected to whom sonar graph --dot # Graphviz sonar open 3000 # open in the browser sonar attach 3000 # shell into the container, or TCP sonar watch # live view sonar watch --stats --notify

## 安装

```bash
# 克隆仓库
git clone https://github.com/yourusername/yourproject.git

# 进入项目目录
cd yourproject

# 安装依赖
pip install -r requirements.txt

使用方法

python main.py --help

许可证

本项目采用 MIT 许可证。有关详细信息,请参阅 LICENSE 文件。```sh sonar next 3000 sonar next 3000-3100 -n 3 --json sonar graph --json sonar info --help

check

`sonar wait` 退出码为 `0`(就绪)、`1`(超时)或 `2`(被中断),因此它非常适合放在启动某个服务与对其进行测试之间:```sh
docker compose up -d
sonar wait 5432 3000 --timeout 60s && npm run migrate && npm run test

守护进程或直接扫描。 每条读取命令都会询问守护进程(如果正在运行),因为它已经掌握了答案,无需再派生 lsof 进程。如果没有守护进程在运行,则直接扫描,并在 stderr 上打印一条说明提示。sonar kill 遵循相同规则:可访问的守护进程负责执行终止操作,因此它会立即重新扫描,其下一条答案——以及端口历史记录——已经知道该端口已消失。读取和终止操作都不会在后台擅自启动守护进程。--no-daemon 强制静默直接扫描,并适用于任何命令:```sh sonar list --no-daemon --json

check

### `sonar host````sh
sonar host          # cpu, load, memory and disk of the machine sonar watches
sonar host --json

由于您未提供需要翻译的具体内容,我无法执行翻译任务。请提供第67块(共107块)的源文本,我将按照指定规则将其从英文翻译为中文。```sh sonar host

check

守护进程按扫描节奏测量自身所在机器,并将其发布为快照 `hosts` 集合中的 `localhost` 行:操作系统与内核、运行时间、CPU 百分比、平均负载、内存以及承载 `/` 的磁盘。CPU 百分比是两次扫描之间完成的工作量,因此在守护进程完成两次扫描之前为 null;平台无法生成的数值——例如 Windows 上的平均负载,该平台没有此指标——为 null 而非零。通过 `sonar remote add` 注册的每台主机都以自身负载加入同一张表。该命令需要运行中的守护进程:因为守护进程持有用于计算百分比的先前样本。

### `sonar remote install````sh
sonar remote install [email protected]        # same version as this sonar
sonar remote install hetzner --version v0.6.0  # a Host from ~/.ssh/config
sonar remote install deploy@box --no-service   # the binary, no daemon

将 sonar 部署到一台你已能通过 SSH 访问的主机上,并在那里启动其守护进程。发布归档会在远程主机上下载并校验——不会从本机复制任何内容——二进制文件会放到 ~/.local/bin/sonar,因此整个过程都不需要 root 权限。守护进程以 systemd 用户单元运行(在主机支持的情况下,位于 ~/.config/systemd/user/sonar.service),不支持时则以分离方式运行;当用户会话会在注销时结束并带走守护进程时,会打印 loginctl enable-linger 作为建议。

安装的版本是你运行 sonar 时所用的版本,因此两端使用相同的协议进行通信。再次运行会在原地升级并重启守护进程,这正是让安装和更新成为同一条命令的原因。

目标主机会原样传给 ssh:来自 ~/.ssh/configHost 别名可用,其设置的 ProxyJumpIdentityFilePort 也同样有效。--identity--ssh-arg 用于补充配置文件未覆盖的标记。

`sonar remote````sh

sonar remote add [email protected] # name taken from the target sonar remote add hetzner [email protected] # or given sonar remote list # status, latency, version, load sonar remote remove hetzner

sonar list --host hetzner # that host's ports sonar list --host "*" # every host, with a HOST column sonar info 3000 --host hetzner

已注册的主机运行相同的守护进程,而本机上的守护进程会保持一条到它的 SSH 连接——`ssh <target> sonar daemon stdio`——并将其报告的内容多路复用进每个客户端已经读取的状态中。任何地方都不会有新的监听端口:远程守护进程的套接字仅对 SSH 用户私有,客户端自身从不使用 SSH。

现在每一行都带有其来源主机。本地行显示 `localhost` 并保留它们一直以来的键,因此今天读取 sonar 的任何内容都不会改变;远程行显示注册名称,并以 `<host>/<port>:<bind>` 作为键,这使得两台机器上的 3000 端口可以成为两行。订阅者默认只看到 localhost,除非它请求更多(`state.subscribe {"hosts": ["*"]}`)。

目标地址原样传给 `ssh`,因此 `~/.ssh/config` 别名、`ProxyJump` 和身份文件都适用;`--ssh-arg`、`--identity` 和 `--port` 则覆盖配置未涵盖的部分。sonar 不存储密码,也不存储密钥。当主机离线时,只要它仍处于注册状态,守护进程就会重试,退避时间从一秒逐渐增加到三十秒,期间该行及其状态会一直保留。

`--host` 也仍然接受一个 sonar 完全不了解的裸 `user@host` 形式:此时会回退到无代理的 `ssh` + `ss`/`lsof` 扫描,并打印一条提示,指向 `sonar remote install`。

#### 在另一台机器上执行操作

每个写入操作也接受 `--host`,并在那里执行与本机完全相同的操作:```sh
sonar kill 3000 --host hetzner                 # stop a port on that machine
sonar kill -g api --host hetzner               # a whole group of its services
sonar kill-all --filter docker --host hetzner  # its containers
sonar up api --host hetzner                    # start a group from its .sonar.yaml
sonar logs 3000 --host hetzner                 # tail its output here
sonar rename 3000 storefront --host hetzner    # its name, in its database
sonar assign 3000 storefront --host hetzner

本地守护进程会将该调用转发到该主机的网桥上,并把远程守护进程的应答原样交回,其封装方式与本地调用返回时完全相同——每一行结果都会标明它发生在哪台主机上,而 kill 操作的 affected 字段携带的正是流用于这些行的 <host>/<port>:<bind> 键。流式命令会持续输出:sonar up --host 会在远端启动每个服务时即时打印,而 Ctrl-C 会停止远端的工作,而不只是终止当前终端。

由于行的键已经包含了主机名,客户端可以直接把它当作选择器传回——{"key": "hetzner/3000:127.0.0.1"} 本身就是完整的选择器,主机名已包含在内。一次调用只作用于一台机器;同时指定两台会报错,而不是在每台上各执行一半的 kill。

有两件事始终在本地进行。sonar attach 会把当前终端连接到某个进程前面,因此它会拒绝 --host,并提示改用 ssh 登录到那边再执行 attach。此外,代理会话是当前守护进程持有的状态,所以 sonar kill --session 没有远程形式。除此之外的所有操作都需要本机运行守护进程——因为到另一台机器的连接就存在于这里——并且会明确说明这一点,而不是悄悄改为扫描本机。

sonar up --host 需要指定组名:你工作目录下的 .sonar.yaml 是本机上的路径,而读取该文件并启动服务的是远程守护进程。

守护进程

一个后台进程负责扫描端口、解析组、轮询健康状态、维护数据库,并向所有订阅者流式推送变更——包括 CLI、桌面应用和编辑器。```sh sonar serve # in the foreground sonar serve --detach # in the background sonar daemon status # pid, uptime, subscribers, scans, intervals sonar daemon path # the socket it listens on sonar daemon log -n 50 -f # what it is doing sonar daemon restart sonar daemon stop

## 安装

### 使用 pip

```bash
pip install pyinstaller

使用 pipx

pipx install pyinstaller

使用 Homebrew(macOS)

brew install pyinstaller

使用 conda

conda install pyinstaller

快速入门

  1. 创建一个 Python 脚本,例如 hello.py
print("Hello, World!")
  1. 使用 PyInstaller 打包它:
pyinstaller hello.py
  1. dist/hello/ 目录中找到可执行文件(Linux/macOS 上为 hello,Windows 上为 hello.exe)。

常用选项

选项描述
--onefile创建单个可执行文件
--windowed不显示控制台窗口(适用于 GUI 应用)
--name指定输出名称
--icon指定可执行文件的图标
--add-data添加额外数据文件
--hidden-import手动指定隐藏导入
--exclude-module排除特定模块
--clean在构建前清理缓存

示例

创建单个可执行文件

pyinstaller --onefile hello.py

创建 GUI 应用

pyinstaller --windowed --onefile myapp.py

添加数据文件

pyinstaller --add-data "assets:assets" myapp.py

指定图标

pyinstaller --onefile --icon=app.ico myapp.py

高级用法

使用 spec 文件

PyInstaller 会为每个构建生成一个 .spec 文件。您可以编辑该文件以进行更高级的配置:

# myapp.spec
a = Analysis(['myapp.py'],
             pathex=[],
             binaries=[],
             datas=[('assets', 'assets')],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=None,
             noarchive=False)

pyz = PYZ(a.pure, a.zipped_data, cipher=None)

exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='myapp',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          console=True)

然后使用 spec 文件构建:

pyinstaller myapp.spec

多平台构建

PyInstaller 不是交叉编译器。您必须在每个目标平台上分别构建:

  • 在 Windows 上构建 Windows 可执行文件
  • 在 macOS 上构建 macOS 可执行文件
  • 在 Linux 上构建 Linux 可执行文件

使用虚拟环境

建议在虚拟环境中使用 PyInstaller:

python -m venv venv
source venv/bin/activate  # Windows 上为 venv\Scripts\activate
pip install pyinstaller
pyinstaller myapp.py

故障排除

常见问题

  1. 缺少模块:使用 --hidden-import 添加缺失的模块
  2. 文件过大:使用 --exclude-module 排除不必要的模块
  3. 杀毒软件误报:使用 UPX 压缩或签名您的可执行文件
  4. 运行时错误:检查 --debug 选项以获取详细输出

调试

pyinstaller --debug myapp.py

最佳实践

  • 始终在干净的环境中测试您的构建
  • 使用版本控制管理您的 spec 文件
  • 记录您的构建配置
  • 在多个系统上测试您的可执行文件
  • 考虑使用 CI/CD 进行自动构建

相关工具

  • Nuitka:Python 编译器,可生成更快的可执行文件
  • cx_Freeze:另一个 Python 打包工具
  • py2exe:Windows 专用的打包工具
  • PyOxidizer:用于创建独立可执行文件的现代工具

许可证

PyInstaller 在 GPL 许可证下发布,并带有特殊的例外条款,允许您分发使用 PyInstaller 构建的应用程序,而无需根据 GPL 发布您的源代码。

贡献

欢迎贡献!请提交拉取请求或打开问题来报告错误和提出功能建议。

致谢

PyInstaller 项目感谢所有贡献者以及使其成为可能的开源社区。```sh sonar daemon path sonar daemon status --json sonar daemon log -n 5

check

| 内容 | 位置 |
|---|---|
| Socket | `$XDG_RUNTIME_DIR/sonar/daemon.sock`,否则为 `~/.config/sonar/daemon.sock`;Windows 上为 `\\.\pipe\sonar` |
| 数据库 | `~/.config/sonar/sonar.db`(可用 `SONAR_DB` 覆盖) |
| 守护进程日志 | `~/.config/sonar/daemon.log`,达到 5 MiB 时轮转,保留三份 |
| 运行日志 | `~/.config/sonar/logs/<group>/<service>.log` |
| 配置 | `~/.config/sonar/config.yaml` |

`SONAR_SOCKET` 可在任何位置覆盖 socket 路径,对守护进程及其客户端均适用——便于运行第二个隔离实例。socket 在 0700 权限的目录中以 0600 权限创建,因此只有你能访问它。同一时间只运行一个守护进程;崩溃遗留的 socket 会在下次启动时被清理。

守护进程在 30 分钟内没有客户端且没有订阅者时会自行停止。可在配置文件中设置 `daemon.idle_timeout` 来更改该时长,或设为 `0` 使其持续运行。

当有变化发生时,端口每 2 秒扫描一次;无变化时,扫描器会在有订阅者连接时自动降速至 5 秒,无订阅者时降速至 10 秒。`daemon.scan_interval` 可调整该基准——最小为 1 秒——两个上限会随之缩放,因此将其提高到 `5s` 会退避至 12.5 秒和 25 秒,而不是将曲线固定在旧上限。`daemon.stats_interval` 是独立的节奏,用于在有订阅时刷新 cpu、内存和主机负载条。两者均在守护进程启动时读取:编辑文件后,执行 `sonar daemon restart`。`sonar daemon status` 会打印当前生效的值(`scan base`、`stats tick`),并显示扫描器当前所处的自适应间隔。

请求 `include: ["health"]` 的订阅者会使守护进程以较慢的节奏探测**每个监听端口**,而不仅仅是声明了 `health:` 路径的服务——后者在每个周期都会被轮询,并无论是否请求了 health 都会送达每个订阅者。

### 配置

`~/.config/sonar/config.yaml` 为可选文件;命令行标志始终优先。```sh
sonar config path
sonar config init
# check

由于 README 内容未提供,我无法进行翻译。请提供要翻译的文本。```sh sonar config edit # open it in $EDITOR

I need the input content to translate. Please provide the chunk you'd like me to translate.```yaml
list:
  columns: [port, process, group, container, image, containerport, url]
  sort: port            # port | pid | name | type
  filter: ""            # docker | user | system | "" (all)
  all: false            # include desktop apps by default
daemon:
  idle_timeout: 30m     # 0 keeps the daemon running
  log_level: info       # debug | info | warn | error
  scan_interval: 2s     # base port-scan cadence, minimum 1s
  stats_interval: 1s    # cpu/memory refresh while subscribed, minimum 250ms
color: true
services:               # label custom/unknown ports
  9000: php-fpm
  5050: my-dashboard

无效值会被忽略并给出警告,sonar 会继续使用默认值。 没有对应配置键的环境变量覆盖项:SONAR_DBSONAR_SOCKETSONAR_NO_HINTS=1 用于静默下面的迁移提示,以及 SONAR_NO_AUTOSTART=1 用于阻止任何 sonar 客户端启动它未找到的守护进程——这在 CI 中很有用,因为构建绝不应留下残留进程。

sonar 自身的测试套件会为每个测试二进制设置 SONAR_NO_AUTOSTART=1,并在运行结束后查找是否有存活的守护进程。该门控只认领从运行私有临时根目录启动的 serve,因此同一台机器上并行运行的两套测试套件不会干扰彼此的守护进程; SONAR_TESTENV_GATE_ALL=1 会将其范围扩大回临时目录下任何位置的每个 sonar serve,这正是拥有整台机器的 CI 运行器所需要的。

代理:MCP、技能与钩子```sh

sonar install mcp --claude-code # merge into /.mcp.json sonar install mcp --cursor --scope user # ~/.cursor/mcp.json sonar install mcp --codex # codex mcp add sonar install skills --claude-code # the bundled sonar skill sonar install hooks --claude-code # optional, see below

## 安装

### 通过 pip 安装

```bash
pip install pywhisker

通过源码安装

git clone https://github.com/ShutdownRepo/pywhisker
cd pywhisker
python3 setup.py install

用法

usage: pywhisker.py [-h] [-v] -t TARGET -a ACTION [-u USERNAME] [-p PASSWORD] [-d DOMAIN] [--dc-ip DC_IP] [--target-ip TARGET_IP] [-k] [--use-ldaps] [--no-pass] [--hashes HASHES] [--aes-key AES_KEY] [--device-id DEVICE_ID] [--target-device-id TARGET_DEVICE_ID] [--target-user TARGET_USER] [--target-spn TARGET_SPN] [--target-sam TARGET_SAM] [--target-sid TARGET_SID] [--target-guid TARGET_GUID] [--target-ntds NTDS] [--target-ntds-file NTDS_FILE] [--target-ntds-dit NTDS_DIT] [--target-ntds-system NTDS_SYSTEM] [--target-ntds-bootkey NTDS_BOOTKEY] [--target-ntds-key NTDS_KEY] [--target-ntds-iv NTDS_IV] [--target-ntds-cipher NTDS_CIPHER] [--target-ntds-hash NTDS_HASH] [--target-ntds-rc4 NTDS_RC4] [--target-ntds-aes NTDS_AES] [--target-ntds-des NTDS_DES] [--target-ntds-3des NTDS_3DES] [--target-ntds-rc2 NTDS_RC2] [--target-ntds-rc4-hmac NTDS_RC4_HMAC] [--target-ntds-aes128 NTDS_AES128] [--target-ntds-aes256 NTDS_AES256] [--target-ntds-des-cbc NTDS_DES_CBC] [--target-ntds-3des-cbc NTDS_3DES_CBC] [--target-ntds-rc2-cbc NTDS_RC2_CBC] [--target-ntds-rc4-hmac-nt NTDS_RC4_HMAC_NT] [--target-ntds-aes128-ctr NTDS_AES128_CTR] [--target-ntds-aes256-ctr NTDS_AES256_CTR] [--target-ntds-des-cbc-md5 NTDS_DES_CBC_MD5] [--target-ntds-3des-cbc-md5 NTDS_3DES_CBC_MD5] [--target-ntds-rc2-cbc-md5 NTDS_RC2_CBC_MD5] [--target-ntds-rc4-hmac-md5 NTDS_RC4_HMAC_MD5] [--target-ntds-aes128-ctr-hmac-sha1 NTDS_AES128_CTR_HMAC_SHA1] [--target-ntds-aes256-ctr-hmac-sha1 NTDS_AES256_CTR_HMAC_SHA1] [--target-ntds-des-cbc-md5-ni NTDS_DES_CBC_MD5_NI] [--target-ntds-3des-cbc-md5-ni NTDS_3DES_CBC_MD5_NI] [--target-ntds-rc2-cbc-md5-ni NTDS_RC2_CBC_MD5_NI] [--target-ntds-rc4-hmac-md5-ni NTDS_RC4_HMAC_MD5_NI] [--target-ntds-aes128-ctr-hmac-sha1-ni NTDS_AES128_CTR_HMAC_SHA1_NI] [--target-ntds-aes256-ctr-hmac-sha1-ni NTDS_AES256_CTR_HMAC_SHA1_NI] [--target-ntds-des-cbc-md5-ni-2 NTDS_DES_CBC_MD5_NI_2] [--target-ntds-3des-cbc-md5-ni-2 NTDS_3DES_CBC_MD5_NI_2] [--target-ntds-rc2-cbc-md5-ni-2 NTDS_RC2_CBC_MD5_NI_2] [--target-ntds-rc4-hmac-md5-ni-2 NTDS_RC4_HMAC_MD5_NI_2] [--target-ntds-aes128-ctr-hmac-sha1-ni-2 NTDS_AES128_CTR_HMAC_SHA1_NI_2] [--target-ntds-aes256-ctr-hmac-sha1-ni-2 NTDS_AES256_CTR_HMAC_SHA1_NI_2] [--target-ntds-des-cbc-md5-ni-3 NTDS_DES_CBC_MD5_NI_3] [--target-ntds-3des-cbc-md5-ni-3 NTDS_3DES_CBC_MD5_NI_3] [--target-ntds-rc2-cbc-md5-ni-3 NTDS_RC2_CBC_MD5_NI_3] [--target-ntds-rc4-hmac-md5-ni-3 NTDS_RC4_HMAC_MD5_NI_3] [--target-ntds-aes128-ctr-hmac-sha1-ni-3 NTDS_AES128_CTR_HMAC_SHA1_NI_3] [--target-ntds-aes256-ctr-hmac-sha1-ni-3 NTDS_AES256_CTR_HMAC_SHA1_NI_3] [--target-ntds-des-cbc-md5-ni-4 NTDS_DES_CBC_MD5_NI_4] [--target-ntds-3des-cbc-md5-ni-4 NTDS_3DES_CBC_MD5_NI_4] [--target-ntds-rc2-cbc-md5-ni-4 NTDS_RC2_CBC_MD5_NI_4] [--target-ntds-rc4-hmac-md5-ni-4 NTDS_RC4_HMAC_MD5_NI_4] [--target-ntds-aes128-ctr-hmac-sha1-ni-4 NTDS_AES128_CTR_HMAC_SHA1_NI_4] [--target-ntds-aes256-ctr-hmac-sha1-ni-4 NTDS_AES256_CTR_HMAC_SHA1_NI_4] [--target-ntds-des-cbc-md5-ni-5 NTDS_DES_CBC_MD5_NI_5] [--target-ntds-3des-cbc-md5-ni-5 NTDS_3DES_CBC_MD5_NI_5] [--target-ntds-rc2-cbc-md5-ni-5 NTDS_RC2_CBC_MD5_NI_5] [--target-ntds-rc4-hmac-md5-ni-5 NTDS_RC4_HMAC_MD5_NI_5] [--target-ntds-aes128-ctr-hmac-sha1-ni-5 NTDS_AES128_CTR_HMAC_SHA1_NI_5] [--target-ntds-aes256-ctr-hmac-sha1-ni-5 NTDS_AES256_CTR_HMAC_SHA1_NI_5] [--target-ntds-des-cbc-md5-ni-6 NTDS_DES_CBC_MD5_NI_6] [--target-ntds-3des-cbc-md5-ni-6 NTDS_3DES_CBC_MD5_NI_6] [--target-ntds-rc2-cbc-md5-ni-6 NTDS_RC2_CBC_MD5_NI_6] [--target-ntds-rc4-hmac-md5-ni-6 NTDS_RC4_HMAC_MD5_NI_6] [--target-ntds-aes128-ctr-hmac-sha1-ni-6 NTDS_AES128_CTR_HMAC_SHA1_NI_6] [--target-ntds-aes256-ctr-hmac-sha1-ni-6 NTDS_AES256_CTR_HMAC_SHA1_NI_6] [--target-ntds-des-cbc-md5-ni-7 NTDS_DES_CBC_MD5_NI_7] [--target-ntds-3des-cbc-md5-ni-7 NTDS_3DES_CBC_MD5_NI_7] [--target-ntds-rc2-cbc-md5-ni-7 NTDS_RC2_CBC_MD5_NI_7] [--target-ntds-rc4-hmac-md5-ni-7 NTDS_RC4_HMAC_MD5_NI_7] [--target-ntds-aes128-ctr-hmac-sha1-ni-7 NTDS_AES128_CTR_HMAC_SHA1_NI_7] [--target-ntds-aes256-ctr-hmac-sha1-ni-7 NTDS_AES256_CTR_HMAC_SHA1_NI_7] [--target-ntds-des-cbc-md5-ni-8 NTDS_DES_CBC_MD5_NI_8] [--target-ntds-3des-cbc-md5-ni-8 NTDS_3DES_CBC_MD5_NI_8] [--target-ntds-rc2-cbc-md5-ni-8 NTDS_RC2_CBC_MD5_NI_8] [--target-ntds-rc4-hmac-md5-ni-8 NTDS_RC4_HMAC_MD5_NI_8] [--target-ntds-aes128-ctr-hmac-sha1-ni-8 NTDS_AES128_CTR_HMAC_SHA1_NI_8] [--target-ntds-aes256-ctr-hmac-sha1-ni-8 NTDS_AES256_CTR_HMAC_SHA1_NI_8] [--target-ntds-des-cbc-md5-ni-9 NTDS_DES_CBC_MD5_NI_9] [--target-ntds-3des-cbc-md5-ni-9 NTDS_3DES_CBC_MD5_NI_9] [--target-ntds-rc2-cbc-md5-ni-9 NTDS_RC2_CBC_MD5_NI_9] [--target-ntds-rc4-hmac-md5-ni-9 NTDS_RC4_HMAC_MD5_NI_9] [--target-ntds-aes128-ctr-hmac-sha1-ni-9 NTDS_AES128_CTR_HMAC_SHA1_NI_9] [--target-ntds-aes256-ctr-hmac-sha1-ni-9 NTDS_AES256_CTR_HMAC_SHA1_NI_9] [--target-ntds-des-cbc-md5-ni-10 NTDS_DES_CBC_MD5_NI_10] [--target-ntds-3des-cbc-md5-ni-10 NTDS_3DES_CBC_MD5_NI_10] [--target-ntds-rc2-cbc-md5-ni-10 NTDS_RC2_CBC_MD5_NI_10] [--target-ntds-rc4-hmac-md5-ni-10 NTDS_RC4_HMAC_MD5_NI_10] [--target-ntds-aes128-ctr-hmac-sha1-ni-10 NTDS_AES128_CTR_HMAC_SHA1_NI_10] [--target-ntds-aes256-ctr-hmac-sha1-ni-10 NTDS_AES256_CTR_HMAC_SHA1_NI_10] [--target-ntds-des-cbc-md5-ni-11 NTDS_DES_CBC_MD5_NI_11] [--target-ntds-3des-cbc-md5-ni-11 NTDS_3DES_CBC_MD5_NI_11] [--target-ntds-rc2-cbc-md5-ni-11 NTDS_RC2_CBC_MD5_NI_11] [--target-ntds-rc4-hmac-md5-ni-11 NTDS_RC4_HMAC_MD5_NI_11] [--target-ntds-aes128-ctr-hmac-sha1-ni-11 NTDS_AES128_CTR_HMAC_SHA1_NI_11] [--target-ntds-aes256-ctr-hmac-sha1-ni-11 NTDS_AES256_CTR_HMAC_SHA1_NI_11] [--target-ntds-des-cbc-md5-ni-12 NTDS_DES_CBC_MD5_NI_12] [--target-ntds-3des-cbc-md5-ni-12 NTDS_3DES_CBC_MD5_NI_12] [--target-ntds-rc2-cbc-md5-ni-12 NTDS_RC2_CBC_MD5_NI_12] [--target-ntds-rc4-hmac-md5-ni-12 NTDS_RC4_HMAC_MD5_NI_12] [--target-ntds-aes128-ctr-hmac-sha1-ni-12 NTDS_AES128_CTR_HMAC_SHA1_NI_12] [--target-ntds-aes256-ctr-hmac-sha1-ni-12 NTDS_AES256_CTR_HMAC_SHA1_NI_12] [--target-ntds-des-cbc-md5-ni-13 NTDS_DES_CBC_MD5_NI_13] [--target-ntds-3des-cbc-md5-ni-13 NTDS_3DES_CBC_MD5_NI_13] [--target-ntds-rc2-cbc-md5-ni-13 NTDS_RC2_CBC_MD5_NI_13] [--target-ntds-rc4-hmac-md5-ni-13 NTDS_RC4_HMAC_MD5_NI_13] [--target-ntds-aes128-ctr-hmac-sha1-ni-13 NTDS_AES128_CTR_HMAC_SHA1_NI_13] [--target-ntds-aes256-ctr-hmac-sha1-ni-13 NTDS_AES256_CTR_HMAC_SHA1_NI_13] [--target-ntds-des-cbc-md5-ni-14 NTDS_DES_CBC_MD5_NI_14] [--target-ntds-3des-cbc-md5-ni-14 NTDS_3DES_CBC_MD5_NI_14] [--target-ntds-rc2-cbc-md5-ni-14 NTDS_RC2_CBC_MD5_NI_14] [--target-ntds-rc4-hmac-md5-ni-14 NTDS_RC4_HMAC_MD5_NI_14] [--target-ntds-aes128-ctr-hmac-sha1-ni-14 NTDS_AES128_CTR_HMAC_SHA1_NI_14] [--target-ntds-aes256-ctr-hmac-sha1-ni-14 NTDS_AES256_CTR_HMAC_SHA1_NI_14] [--target-ntds-des-cbc-md5-ni-15 NTDS_DES_CBC_MD5_NI_15] [--target-ntds-3des-cbc-md5-ni-15 NTDS_3DES_CBC_MD5_NI_15] [--target-ntds-rc2-cbc-md5-ni-15 NTDS_RC2_CBC_MD5_NI_15] [--target-ntds-rc4-hmac-md5-ni-15 NTDS_RC4_HMAC_MD5_NI_15] [--target-ntds-aes128-ctr-hmac-sha1-ni-15 NTDS_AES128_CTR_HMAC_SHA1_NI_15] [--target-ntds-aes256-ctr-hmac-sha1-ni-15 NTDS_AES256_CTR_HMAC_SHA1_NI_15] [--target-ntds-des-cbc-md5-ni-16 NTDS_DES_CBC_MD5_NI_16] [--target-ntds-3des-cbc-md5-ni-16 NTDS_3DES_CBC_MD5_NI_16] [--target-ntds-rc2-cbc-md5-ni-16 NTDS_RC2_CBC_MD5_NI_16] [--target-ntds-rc4-hmac-md5-ni-16 NTDS_RC4_HMAC_MD5_NI_16] [--target-ntds-aes128-ctr-hmac-sha1-ni-16 NTDS_AES128_CTR_HMAC_SHA1_NI_16] [--target-ntds-aes256-ctr-hmac-sha1-ni-16 NTDS_AES256_CTR_HMAC_SHA1_NI_16] [--target-ntds-des-cbc-md5-ni-17 NTDS_DES_CBC_MD5_NI_17] [--target-ntds-3des-cbc-md5-ni-17 NTDS_3DES_CBC_MD5_NI_17] [--target-ntds-rc2-cbc-md5-ni-17 NTDS_RC2_CBC_MD5_NI_17] [--target-ntds-rc4-hmac-md5-ni-17 NTDS_RC4_HMAC_MD5_NI_17] [--target-ntds-aes128-ctr-hmac-sha1-ni-17 NTDS_AES128_CTR_HMAC_SHA1_NI_17] [--target-ntds-aes256-ctr-hmac-sha1-ni-17 NTDS_AES256_CTR_HMAC_SHA1_NI_17] [--target-ntds-des-cbc-md5-ni-18 NTDS_DES_CBC_MD5_NI_18] [--target-ntds-3des-cbc-md5-ni-18 NTDS_3DES_CBC_MD5_NI_18] [--target-ntds-rc2-cbc-md5-ni-18 NTDS_RC2_CBC_MD5_NI_18] [--target-ntds-rc4-hmac-md5-ni-18 NTDS_RC4_HMAC_MD5_NI_18] [--target-ntds-aes128-ctr-hmac-sha1-ni-18 NTDS_AES128_CTR_HMAC_SHA1_NI_18] [--target-ntds-aes256-ctr-hmac-sha1-ni-18 NTDS_AES256_CTR_HMAC_SHA1_NI_18] [--target-ntds-des-cbc-md5-ni-19 NTDS_DES_CBC_MD5_NI_19] [--target-ntds-3des-cbc-md5-ni-19 NTDS_3DES_CBC_MD5_NI_19] [--target-ntds-rc2-cbc-md5-ni-19 NTDS_RC2_CBC_MD5_NI_19] [--target-ntds-rc4-hmac-md5-ni-19 NTDS_RC4_HMAC_MD5_NI_19] [--target-ntds-aes128-ctr-hmac-sha1-ni-19 NTDS_AES128_CTR_HMAC_SHA1_NI_19] [--target-ntds-aes256-ctr-hmac-sha1-ni-19 NTDS_AES256_CTR_HMAC_SHA1_NI_19] [--target-ntds-des-cbc-md5-ni-20 NTDS_DES_CBC_MD5_NI_20] [--target-ntds-3des-cbc-md5-ni-20 NTDS_3DES_CBC_MD5_NI_20] [--target-ntds-rc2-cbc-md5-ni-20 NTDS_RC2_CBC_MD5_NI_20] [--target-ntds-rc4-hmac-md5-ni-20 NTDS_RC4_HMAC_MD5_NI_20] [--target-ntds-aes128-ctr-hmac-sha1-ni-20 NTDS_AES128_CTR_HMAC_SHA1_NI_20] [--target-ntds-aes256-ctr-hmac-sha1-ni-20 NTDS_AES256_CTR_HMAC_SHA1_NI_20] [--target-ntds-des-cbc-md5-ni-21 NTDS_DES_CBC_MD5_NI_21] [--target-ntds-3des-cbc-md5-ni-21 NTDS_3DES_CBC_MD5_NI_21] [--target-ntds-rc2-cbc-md5-ni-21 NTDS_RC2_CBC_MD5_NI_21] [--target-ntds-rc4-hmac-md5-ni-21 NTDS_RC4_HMAC_MD5_NI_21] [--target-ntds-aes128-ctr-hmac-sha1-ni-21 NTDS_AES128_CTR_HMAC_SHA1_NI_21] [--target-ntds-aes256-ctr-hmac-sha1-ni-21 NTDS_AES256_CTR_HMAC_SHA1_NI_21] [--target-ntds-des-cbc-md5-ni-22 NTDS_DES_CBC_MD5_NI_22] [--target-ntds-3des-cbc-md5-ni-22 NTDS_3DES_CBC_MD5_NI_22] [--target-ntds-rc2-cbc-md5-ni-22 NTDS_RC2_CBC_MD5_NI_22] [--target-ntds-rc4-hmac-md5-ni-22 NTDS_RC4_HMAC_MD5_NI_22] [--target-ntds-aes128-ctr-hmac-sha1-ni-22 NTDS_AES128_CTR_HMAC_SHA1_NI_22] [--target-ntds-aes256-ctr-hmac-sha1-ni-22 NTDS_AES256_CTR_HMAC_SHA1_NI_22] [--target-ntds-des-cbc-md5-ni-23 NTDS_DES_CBC_MD5_NI_23] [--target-ntds-3des-cbc-md5-ni-23 NTDS_3DES_CBC_MD5_NI_23] [--target-ntds-rc2-cbc-md5-ni-23 NTDS_RC2_CBC_MD5_NI_23] [--target-ntds-rc4-hmac-md5-ni-23 NTDS_RC4_HMAC_MD5_NI_23] [--target-ntds-aes128-ctr-hmac-sha1-ni-23 NTDS_AES128_CTR_HMAC_SHA1_NI_23] [--target-ntds-aes256-ctr-hmac-sha1-ni-23 NTDS_AES256_CTR_HMAC_SHA1_NI_23] [--target-ntds-des-cbc-md5-ni-24 NTDS_DES_CBC_MD5_NI_24] [--target-ntds-3des-cbc-md5-ni-24 NTDS_3DES_CBC_MD5_NI_24] [--target-ntds-rc2-cbc-md5-ni-24 NTDS_RC2_CBC_MD5_NI_24] [--target-ntds-rc4-hmac-md5-ni-24 NTDS_RC4_HMAC_MD5_NI_24] [--target-ntds-aes128-ctr-hmac-sha1-ni-24 NTDS_AES128_CTR_HMAC_SHA1_NI_24] [--target-ntds-aes256-ctr-hmac-sha1-ni-24 NTDS_AES256_CTR_HMAC_SHA1_NI_24] [--target-ntds-des-cbc-md5-ni-25 NTDS_DES_CBC_MD5_NI_25] [--target-ntds-3des-cbc-md5-ni-25 NTDS_3DES_CBC_MD5_NI_25] [--target-ntds-rc2-cbc-md5-ni-25 NTDS_RC2_CBC_MD5_NI_25] [--target-ntds-rc4-hmac-md5-ni-25 NTDS_RC4_HMAC_MD5_NI_25] [--target-ntds-aes128-ctr-hmac-sha1-ni-25 NTDS_AES128_CTR_HMAC_SHA1_NI_25] [--target-ntds-aes256-ctr-hmac-sha1-ni-25 NTDS_AES256_CTR_HMAC_SHA1_NI_25] [--target-ntds-des-cbc-md5-ni-26 NTDS_DES_CBC_MD5_NI_26] [--target-ntds-3des-cbc-md5-ni-26 NTDS_3DES_CBC_MD5_NI_26] [--target-ntds-rc2-cbc-md5-ni-26 NTDS_RC2_CBC_MD5_NI_26] [--target-ntds-rc4-hmac-md5-ni-26 NTDS_RC4_HMAC_MD5_NI_26] [--target-ntds-aes128-ctr-hmac-sha1-ni-26 NTDS_AES128_CTR_HMAC_SHA1_NI_26] [--target-ntds-aes256-ctr-hmac-sha1-ni-26 NTDS_AES256_CTR_HMAC_SHA1_NI_26] [--target-ntds-des-cbc-md5-ni-27 NTDS_DES_CBC_MD5_NI_27] [--target-ntds-3des-cbc-md5-ni-27 NTDS_3DES_CBC_MD5_NI_27] [--target-ntds-rc2-cbc-md5-ni-27 NTDS_RC2_CBC_MD5_NI_27] [--target-ntds-rc4-hmac-md5-ni-27 NTDS_RC4_HMAC_MD5_NI_27] [--target-ntds-aes128-ctr-hmac-sha1-ni-27 NTDS_AES128_CTR_HMAC_SHA1_NI_27] [--target-ntds-aes256-ctr-hmac-sha1-ni-27 NTDS_AES256_CTR_HMAC_SHA1_NI_27] [--target-ntds-des-cbc-md5-ni-28 NTDS_DES_CBC_MD5_NI_28] [--target-ntds-3des-cbc-md5-ni-28 NTDS_3DES_CBC_MD5_NI_28] [--target-ntds-rc2-cbc-md5-ni-28 NTDS_RC2_CBC_MD5_NI_28] [--target-ntds-rc4-hmac-md5-ni-28 NTDS_RC4_HMAC_MD5_NI_28] [--target-ntds-aes128-ctr-hmac-sha1-ni-28 NTDS_AES128_CTR_HMAC_SHA1_NI_28] [--target-ntds-aes256-ctr-hmac-sha1-ni-28 NTDS_AES256_CTR_HMAC_SHA1_NI_28] [--target-ntds-des-cbc-md5-ni-29 NTDS_DES_CBC_MD5_NI_29] [--target-ntds-3des-cbc-md5-ni-29 NTDS_3DES_CBC_MD5_NI_29] [--target-ntds-rc2-cbc-md5-ni-29 NTDS_RC2_CBC_MD5_NI_29] [--target-ntds-rc4-hmac-md5-ni-29 NTDS_RC4_HMAC_MD5_NI_29] [--target-ntds-aes128-ctr-hmac-sha1-ni-29 NTDS_AES128_CTR_HMAC_SHA1_NI_29] [--target-ntds-aes256-ctr-hmac-sha1-ni-29 NTDS_AES256_CTR_HMAC_SHA1_NI_29] [--target-ntds-des-cbc-md5-ni-30 NTDS_DES_CBC_MD5_NI_30] [--target-ntds-3des-cbc-md5-ni-30 NTDS_3DES_CBC_MD5_NI_30] [--target-ntds-rc2-cbc-md5-ni-30 NTDS_RC2_CBC_MD5_NI_30] [--target-ntds-rc4-hmac-md5-ni-30 NTDS_RC4_HMAC_MD5_NI_30] [--target-ntds-aes128-ctr-hmac-sha1-ni-30 NTDS_AES128_CTR_HMAC_SHA1_NI_30] [--target-ntds-aes256-ctr-hmac-sha1-ni-30 NTDS_AES256_CTR_HMAC_SHA1_NI_30] [--target-ntds-des-cbc-md5-ni-31 NTDS_DES_CBC_MD5_NI_31] [--target-ntds-3des-cbc-md5-ni-31 NTDS_3DES_CBC_MD5_NI_31] [--target-ntds-rc2-cbc-md5-ni-31 NTDS_RC2_CBC_MD5_NI_31] [--target-ntds-rc4-hmac-md5-ni-31 NTDS_RC4_HMAC_MD5_NI_31] [--target-ntds-aes128-ctr-hmac-sha1-ni-31 NTDS_AES128_CTR_HMAC_SHA1_NI_31] [--target-ntds-aes256-ctr-hmac-sha1-ni-31 NTDS_AES256_CTR_HMAC_SHA1_NI_31] [--target-ntds-des-cbc-md5-ni-32 NTDS_DES_CBC_MD5_NI_32] [--target-ntds-3des-cbc-md5-ni-32 NTDS_3DES_CBC_MD5_NI_32] [--target-ntds-rc2-cbc-md5-ni-32 NTDS_RC2_CBC_MD5_NI_32] [--target-ntds-rc4-hmac-md5-ni-32 NTDS_RC4_HMAC_MD5_NI_32] [--target-ntds-aes128-ctr-hmac-sha1-ni-32 NTDS_AES128_CTR_HMAC_SHA1_NI_32] [--target-ntds-aes256-ctr-hmac-sha1-ni-32 NTDS_AES256_CTR_HMAC_SHA1_NI_32] [--target-ntds-des-cbc-md5-ni-33 NTDS_DES_CBC_MD5_NI_33] [--target-ntds-3des-cbc-md5-ni-33 NTDS_3DES_CBC_MD5_NI_33] [--target-ntds-rc2-cbc-md5-ni-33 NTDS_RC2_CBC_MD5_NI_33] [--target-ntds-rc4-hmac-md5-ni-33 NTDS_RC4_HMAC_MD5_NI_33] [--target-ntds-aes128-ctr-hmac-sha1-ni-33 NTDS_AES128_CTR_HMAC_SHA1_NI_33] [--target-ntds-aes256-ctr-hmac-sha1-ni-33 NTDS_AES256_CTR_HMAC_SHA1_NI_33] [--target-ntds-des-cbc-md5-ni-34 NTDS_DES_CBC_MD5_NI_34] [--target-ntds-3des-cbc-md5-ni-34 NTDS_3DES_CBC_MD5_NI_34] [--target-ntds-rc2-cbc-md5-ni-34 NTDS_RC2_CBC_MD5_NI_34] [--target-ntds-rc4-hmac-md5-ni-34 NTDS_RC4_HMAC_MD5_NI_34] [--target-ntds-aes128-ctr-hmac-sha1-ni-34 NTDS_AES128_CTR_HMAC_SHA1_NI_34] [--target-ntds-aes256-ctr-hmac-sha1-ni-34 NTDS_AES256_CTR_HMAC_SHA1_NI_34] [--target-ntds-des-cbc-md5-ni-35 NTDS_DES_CBC_MD5_NI_35] [--target-ntds-3des-cbc-md5-ni-35 NTDS_3DES_CBC_MD5_NI_35] [--target-ntds-rc2-cbc-md5-ni-35 NTDS_RC2_CBC_MD5_NI_35] [--target-ntds-rc4-hmac-md5-ni-35 NTDS_RC4_HMAC_MD5_NI_35] [--target-ntds-aes128-ctr-hmac-sha1-ni-35 NTDS_AES128_CTR_HMAC_SHA1_NI_35] [--target-ntds-aes256-ctr-hmac-sha1-ni-35 NTDS_AES256_CTR_HMAC_SHA1_NI_35] [--target-ntds-des-cbc-md5-ni-36 NTDS_DES_CBC_MD5_NI_36] [--target-ntds-3des-cbc-md5-ni-36 NTDS_3DES_CBC_MD5_NI_36] [--target-ntds-rc2-cbc-md5-ni-36 NTDS_RC2_CBC_MD5_NI_36] [--target-ntds-rc4-hmac-md5-ni-36 NTDS_RC4_HMAC_MD5_NI_36] [--target-ntds-aes128-ctr-hmac-sha1-ni-36 NTDS_AES128_CTR_HMAC_SHA1_NI_36] [--target-ntds-aes256-ctr-hmac-sha1-ni-36 NTDS_AES256_CTR_HMAC_SHA1_NI_36] [--target-ntds-des-cbc-md5-ni-37 NTDS_DES_CBC_MD5_NI_37] [--target-ntds-3des-cbc-md5-ni-37 NTDS_3DES_CBC_MD5_NI_37] [--target-ntds-rc2-cbc-md5-ni-37 NTDS_RC2_CBC_MD5_NI_37] [--target-ntds-rc4-hmac-md5-ni-37 NTDS_RC4_HMAC_MD5_NI_37] [--target-ntds-aes128-ctr-hmac-sha1-ni-37 NTDS_AES128_CTR_HMAC_SHA1_NI_37] [--target-ntds-aes256-ctr-hmac-sha1-ni-```sh
sonar install mcp --generic --print
sonar install skills --print
sonar install hooks --print
# check

install mcp 会注册 {"command": "sonar", "args": ["mcp"]},并保持文件中的其他所有服务器和键不变;运行两次不会产生任何变化,而 --uninstall 只会移除 sonar 写入的内容。

sonar mcp 就是那个服务器:一个内置于二进制文件中的 stdio MCP 服务器,为代理提供守护进程对机器的视角。它通过 list_portsinspect_port 读取端口,通过 wait_for_port 等待端口,通过 next_free_portclaim_port 挑选并预留端口,并通过 tail_logshealth_checkdependency_graphport_historylist_sessions 回答代理的其他问题;操作和资源随后推出。如果守护进程未运行,它会启动一个;如果守护进程消失,它会自行重新连接;其日志输出到 stderr,因为 stdout 承载协议。

install skills 会写入捆绑的技能,该技能教导代理使用 sonar start -- 启动服务器、使用 sonar wait 代替休眠,并清理其启动的内容。install hooks 会添加两个 Claude Code 钩子:一个导出 SONAR_SESSION,使会话启动的所有内容都归属于该会话;另一个在裸开发服务器即将运行时建议使用 sonar start --(它只提供建议,从不阻止)。两者都支持 --scope project|user--print--uninstall

sonar doctor

一条命令,检查 sonar 依赖的所有内容,并说明如何处理任何问题。桌面应用在引导期间运行的就是它,当出现异常时,你也应该自行运行它。```sh sonar doctor # the table, and a one-line verdict sonar doctor --json # {ok, checks, version, daemon_version} sonar doctor --only db_ok,tray # just these sonar doctor --only mcp_registered # a whole family sonar doctor --project ~/code/api # a project other than the working directory sonar doctor --fix --yes # apply the safe repairs, then check again

请提供需要翻译的Markdown内容。```sh
# check
sonar doctor --only daemon_reachable,daemon_protocol,socket_permissions,db_ok
sonar doctor --json --only config_parses | grep -q '"status": "ok"'
sonar doctor --only mcp_registered --project . > /dev/null

每项检查都会报告 okwarnfailskipskip 表示没有可检查的内容——Cursor 未安装、机器上没有 docker、套接字是 Windows 上的命名管道——并且永远不会对你不利。退出码为 0,除非有失败项,因此 sonar doctor 适合放在设置脚本中。

检查项含义
cli_on_path你运行的二进制文件是 PATH 解析到的那个;如果不是,则指出遮蔽它的安装
cli_version_current与最新版本比较,若 2 秒内无法访问 GitHub 则返回 skip
config_parses你的 config.yaml 能加载;语法错误会报告行、列和插入符号
config_dir_writable守护进程能写入其日志、锁和数据库
daemon_reachable有东西在套接字上监听
daemon_version_matches正在运行的守护进程版本与你使用的 CLI 版本一致
daemon_protocol守护进程的协议主版本与此构建匹配
socket_permissions套接字属于你且权限为 0600,位于权限为 0700 的目录中(Windows 上为 skip
db_ok数据库能打开、处于最新模式,以及其大小
mcp_registered.{claude_code,cursor,codex}sonar 的 MCP 服务器位于该客户端的配置中
skills_installed捆绑的技能已安装且为最新
hooks_installed可选的 Claude Code 钩子已安装
project_config此项目有一个能加载的 .sonar.yaml
dockerdocker CLI 存在且其守护进程有响应
desktop_installed桌面应用已安装,以及版本号(Windows 上为 skip
tray已被取代的 macOS sonar-tray 二进制文件是否仍然存在

--fix 仅应用可安全无人值守执行的修复,并且除非你传入 --yes,否则会先询问:它会将无法解析的 config.yaml 移动到 config.yaml.broken-<timestamp> 并写入新的模板(绝不删除任何内容),重启未运行的守护进程,并运行检查项所指名的 sonar install mcp|skills|hooks 命令——从工作目录运行,就像你手动输入那样,因此请在你要修复的项目内运行 --fix,而不是用 --project 指向它。然后它会再次检查。任何它不会触碰的内容——PATH 上遮蔽的二进制文件、非 sonar 写入的技能——都会留给你处理,并在 fix 列中给出确切命令。

桌面应用通过守护进程的 daemon.doctor 方法调用相同的检查,而不是通过 shell 执行。守护进程在其自身进程中运行所有能运行的内容;与你调用的 CLI 二进制文件相关的三项检查(cli_on_pathcli_version_currentdaemon_version_matches)会返回 skip,并附带说明原因的详细信息。

桌面应用

Sonar 应用是同一画面的窗口版和菜单栏或系统托盘版:侧边分组、端口网格带实时统计和健康状态、日志,以及上述所有操作的按钮。它连接同一个守护进程,因此 CLI 和应用永远不会不一致。sonar install desktop 安装它,sonar tray 启动它。

在应用发布之前,macOS 发布压缩包仍携带旧的 sonar-tray 菜单栏二进制文件,当应用未安装时,sonar tray 会回退到它。

sonar install desktop

该应用处于测试阶段,尚未由 Apple 签名,因此 CLI 会安装它:```sh brew install raskrebs/sonar/sonar && sonar install desktop

这就是完整的测试器设置。Sonar 会获取已发布构建的清单,选择适用于你机器的那个,校验其 sha256 和大小,然后安装并打开它。

**这就是 CLI 负责下载的原因。** macOS 会给*浏览器*保存的任何内容附加隔离属性,而 Gatekeeper 会拒绝打开未经 Apple 公证的隔离应用。此 CLI 下载的文件从一开始就不会获得该属性,因此测试版无需任何提示,也无需右键点击“打开”的操作即可启动。Sonar 既不会设置也不会移除隔离属性——因为根本没有需要移除的内容。```sh
sonar install desktop                    # install and launch
sonar install desktop --no-launch        # install only
sonar install desktop --update           # update; does nothing if current
sonar install desktop --check            # exit 1 when an update is available
sonar install desktop --version 0.1.0-beta.1
sonar install desktop --force            # ask a running Sonar to quit first
sonar install desktop --json             # for scripts

该命令无需网络即可告诉你它的作用:```sh sonar install desktop --help | grep -- '--no-launch'

check

安装位置:

| | |
| --- | --- |
| macOS | `/Applications/Sonar.app`,若该目录不可写则安装到 `~/Applications/Sonar.app`(sonar 从不使用 `sudo`) |
| Linux | `~/.local/opt/sonar-desktop/Sonar.AppImage`,并在 `~/.local/share/applications` 中添加菜单项,在 `~/.local/bin` 中创建 `sonar-desktop` 链接 |
| Windows | 尚不支持——命令会提示并退出,返回码为 1 |

`--dir` 可覆盖两者的安装目录。在 Linux 上,若发布版本提供了 `.deb` 包,`--deb` 会通过 `apt`/`dpkg` 安装该包,而非使用 AppImage。

安装是原子性的:新应用会解压到旧应用旁边,然后通过重命名进行替换,因此下载失败绝不会让你失去可用的应用。如果应用正在运行,sonar 会拒绝替换其底层包;`--force` 会要求其退出并等待最多十秒。

`sonar install desktop` 会将 `desktop.installed_version` 和 `desktop.installed_path` 记录到 `~/.config/sonar/config.yaml` 中,这正是 `sonar tray` 查找通过 `--dir` 安装的应用、以及 `sonar doctor` 的 `desktop_installed` 检查获知版本号的方式。构建来源由 `desktop.download_base` 指定,可被 `SONAR_DESKTOP_BASE` 覆盖,再被 `--base` 覆盖——将其指向你自己的构建即可进行测试。

### `sonar relay`

relay 是 sonar 的服务端:一个轻量 HTTP 服务,由我们为托管应用运行,并发布为 `ghcr.io/raskrebs/sonar-relay`,以便你可以自行部署。它与本地守护进程无关——`sonar serve` 监听你的端口,`sonar relay serve` 为一组设备响应 HTTP 请求——它之所以与守护进程打包在同一个二进制文件中,只是为了便于部署单一构件。

目前它收集匿名产品遥测数据:每次安装一批命名事件,不包含路径、主机名或 URL,若某个值看起来像这些内容,会在入口处被拒绝。未来它将用于终止暴露的隧道并处理登录,目前仍是同一服务。```sh
sonar relay serve --db ./relay.db --project-keys "$(openssl rand -hex 24)"

docs/RELAY.md 包含路由、精确的验证规则、存储模式,以及在任何装有 Docker 的机器上通过 Caddy 一键部署的说明。

从旧命令迁移

分组前的命令仍然可用,并会在 stderr 上打印一行提示,说明它们被什么替代了。这些旧命令将在下一个次要版本发布时移除。设置 SONAR_NO_HINTS=1 可静默这些提示,--json 输出也永远不会包含它们。

旧命令新命令
sonar run --tag X -- cmdsonar start --group X -- cmd
sonar runssonar start --list
sonar list --tag Xsonar list --group X
sonar kill-all --filter dockersonar kill --all --filter docker
sonar down Xsonar kill -g X
sonar profile create Xsonar init
sonar profile show Xsonar groups X
sonar up X(检查配置文件)sonar up X 现在会启动该分组
sonar tray(Swift 菜单栏应用)sonar tray 启动桌面应用

配置文件是每台机器上端口的快照;.sonar.yaml 随项目一起提交。转换一个配置文件并在保留前先阅读它——系统不会为你写入任何内容:```sh sonar profile list

check

(empty)```sh
sonar profile export my-app > .sonar.yaml

配置文件从未记录服务的启动方式,因此提案中包含端口、名称和健康检查路径,由你来填写 cmd

故障排查

守护进程出现问题。 在复现问题时运行 sonar daemon log -f,并用 sonar daemon status 查看 PID、运行时长和扫描次数。使用 sonar daemon stop 停止它;所有读取命令在无守护进程时仍可正常工作。

“守护进程不可用,使用直接扫描”。 套接字上没有进程在监听。这属于正常现象——读取操作不会启动守护进程。如需守护进程,请运行 sonar serve -d

崩溃后遗留的套接字。 sonar daemon path 可显示其位置;启动守护进程时会自动清除过期的套接字。如果第一个守护进程已消失而第二个拒绝启动,运行 sonar daemon restart 可清除锁。

列表中缺少端口。 其他用户拥有的进程在无权限时不可见;sonar 会在表格下方注明这一点。使用 sudo sonar list 重新运行即可查看。在 Linux 上,需安装 ssiproute2);在 Windows 上,则使用 netstat

执行 kill 无效果。 Docker 容器通过 Docker 守护进程停止:请检查 docker ps。忽略 SIGTERM 的进程需要 -f 参数;由其他程序(systemd、Compose 的 restart: always)监管的进程会按设计自动恢复——请停止监管程序。

一切失效且原因不明。 sonar doctor 会一次性检查二进制文件、配置、守护进程、数据库及所有集成,并打印修复每项问题的命令。

报告 Bug。 请包含以下内容,以及 sonar daemon log 的最后几行:```sh sonar version sonar daemon status sonar doctor --json

check

## 支持的平台

- macOS(使用 `lsof`)
- Linux(使用 `ss`)
- Windows(使用 `netstat`)

分组需要每个进程的工作目录,现在每个平台都有相应的方式:
Linux 上通过 `/proc`,macOS 上通过 `lsof`,Windows 上则通过读取进程自身的
PEB。因此 git-root 分组、`project_root` 以及基于 cwd 的命名在所有平台上
工作方式一致,`sonar init` 也可以根据这三个平台中任意一个正在监听的端口
来建议生成 `.sonar.yaml`。

桌面应用目前范围较窄:`sonar install desktop` 可在 macOS(Apple Silicon 和
Intel)以及 Linux(x86_64 和 aarch64)上安装。在 Windows 上,该命令会提示
应用尚不可用并以退出码 1 结束。

唯一的一个缺口是在 64 位 Windows 上运行 32 位 `sonar.exe`:它无法读取
64 位进程的内存,因此这些端口返回时没有工作目录,会从其 git-root 分组中
脱离。请使用 64 位版本——它可以同时读取 64 位和 32 位进程。在其他平台上,
如果某个端口的进程拒绝访问(例如以其他用户身份运行的服务、受保护的系统
进程),该端口只会被简单地标记为没有工作目录;扫描的其余部分不受影响。

## 贡献者

感谢所有为 sonar 做出贡献的人!

<a href="https://github.com/RasKrebs/sonar/graphs/contributors">
  <img src="https://stg.contrib.rocks/image?repo=RasKrebs/sonar" />
</a>

分类