feat: 新增 net pids 端口占用查询并补齐 README

- kopsc net pids <端口> 等价 lsof -ti:<端口>,输出 PID 升序去重、每行一个
- Linux 回退链 lsof -> ss -> /proc;macOS 使用 lsof;Windows 解析 netstat -ano
- 端口 1-65535 校验,无占用时无输出并正常退出
- README 新增 net.pids 小节,修正过时的匹配示例、-n 例外说明与目录结构
This commit is contained in:
2026-10-31 14:38:40 +08:00
parent e02f3010cf
commit 6002a3afd1
7 changed files with 611 additions and 15 deletions
+35 -8
View File
@@ -8,8 +8,8 @@ $ kopsc docker clearsafe --dry-run
将执行: docker system prune -f
平台: linux/amd64, distro=centos, runtimes=docker
方式: 直接执行
命中: 变体 #1(5 分)CentOS 上的 Docker 运行时
依据: 匹配 os=linux, distro=centos (+3), runtime=docker (+2)
命中: 变体 #1(2 分)Linux 上的 Docker 运行时
依据: 匹配 os=linux, runtime=docker (+2)
```
在 CentOS 上它执行 `docker system prune -f`;在装有 Podman 的 CentOS 上自动改走
@@ -66,6 +66,9 @@ kopsc diagnose api-0 -n prod # K8s:本命令里 -n 表示 namespace
kopsc diagnose api-0 --kind k8s --context staging
kopsc diagnose web --lines 200 --no-events
kopsc net pids 8080 # 等价 lsof -ti:8080,输出占用端口的 PID
kopsc net ports # 列出监听中的端口
kopsc disk usage /var # 磁盘占用排行(du -x --max-depth=1 -h | sort -hr 的跨平台版)
kopsc disk usage --path /var # 同上,用命名参数指定路径
kopsc disk usage /var --dry-run # 预览(内置命令同样支持)
@@ -103,7 +106,7 @@ $ kopsc disk usage /var
| 参数 | 行为 |
| --- | --- |
| `-n`, `--dry-run`, `--dry-run=true` | 置为预览模式,不执行 |
| `-n`, `--dry-run`, `--dry-run=true` | 置为预览模式,不执行。**例外**:`kopsc diagnose` 中 `-n` 表示 namespace,预览请用 `--dry-run` |
| `--dry-run=false` | 关闭预览模式 |
| `-y`, `--yes` | 确认执行破坏性命令;未提供时只打印影响范围并以退出码 2 结束 |
| `-h`, `--help` | 打印该命令的帮助 |
@@ -172,11 +175,34 @@ $ kopsc disk usage /var
Kubernetes 的 CrashLoopBackOff、ImagePullBackOff/ErrImagePull、CreateContainerConfigError、OOMKilled、
Pending(结合 FailedScheduling/FailedMount 事件)、Evicted 等。
### 端口占用查询(net.pids)
`kopsc net pids <端口>` 等价 `lsof -ti:<端口>`:输出占用该端口的进程 PID,每行一个、升序、去重;没有进程占用时无输出并正常退出。
```bash
kopsc net pids 8080
kopsc net pids --port 8080
kopsc net pids 8080 --dry-run
```
底层按平台自动选择,但对外的命令与输出保持一致:
| 平台 | 依次尝试的底层来源 |
| --- | --- |
| Linux | `lsof -ti:<port>` → `ss -H -t -u -n -p`(解析 `pid=`) → 读 `/proc/net/*` 与 `/proc/*/fd`(纯内置,零外部依赖) |
| macOS | `lsof -ti:<port>`(系统自带,无 `/proc` 回退) |
| Windows | `netstat -ano`(解析本地地址与 PID) |
- 端口必须是 1–65535;非法端口直接报错,不执行任何外部命令。
- `lsof` 无匹配时退出码为 1 且无输出,kopsc 视为“没有进程占用”,返回 0。
- 只读命令,不需要 `--yes`。
### 其它
| 命令 | 说明 | 平台变体 |
| --- | --- | --- |
| `diagnose` | 容器/Pod 状态根因诊断(自动识别 Docker 与 Kubernetes) | Linux/macOS/Windows 均使用 kopsc 内置实现 |
| `net.pids` | 查找占用指定端口的进程 PID(等价 `lsof -ti:<端口>`) | Linux `lsof` → `ss` → `/proc`;macOS `lsof`;Windows `netstat -ano`。三平台命令与输出一致(PID 升序、每行一个) |
| `disk.usage` | 目录占用排行(等价 `du -x --max-depth=1 -h <路径> \| sort -hr`) | Linux/macOS/Windows 均使用 kopsc 内置实现,保证输出一致 |
| `net.ports` | 列出监听中的端口 | Linux `ss -tulnp`、macOS `lsof -iTCP -sTCP:LISTEN -nP`、Windows `netstat -ano` |
| `pkg.refreshindex` | 刷新包管理器索引 | centos `dnf makecache`、ID_LIKE rhel/fedora `dnf makecache`、ID_LIKE debian/ubuntu `apt-get update`、alpine `apk update`、macOS `brew update`、Windows `winget source update` |
@@ -203,10 +229,10 @@ Pending(结合 FailedScheduling/FailedMount 事件)、Evicted 等。
例如 CentOS 上同时装了 docker 与 podman:
```text
✓ #0 5 分 匹配 os=linux, distro=centos (+3), runtime=podman (+2) -> podman system prune -f
✓ #1 5 分 匹配 os=linux, distro=centos (+3), runtime=docker (+2) -> docker system prune -f
✓ #2 2 分 匹配 os=linux, runtime=podman (+2) -> podman system prune -f
✗ #4 0 分 os=darwin 不匹配(当前 linux) -> docker system prune -f
✓ #0 2 分 匹配 os=linux, runtime=podman (+2) -> podman system prune -f
✓ #1 2 分 匹配 os=linux, runtime=docker (+2) -> docker system prune -f
✗ #2 0 分 os=darwin 不匹配(当前 linux) -> docker system prune -f
✗ #3 0 分 os=windows 不匹配(当前 linux) -> docker system prune -f
```
## 新增命令或平台变体
@@ -244,7 +270,8 @@ cmd/kopsc 入口,注入版本号
internal/platform 环境探测(可注入,便于测试)
internal/registry 命令模型、校验、内置注册表
internal/resolve 变体打分与选择
internal/executor 进程执行、dry-run、退出码透传
internal/executor 进程执行、内置动作、dry-run、退出码透传
internal/diagnose Docker/Podman 与 Kubernetes 的状态根因诊断(内置动作)
internal/cli cobra 命令树与辅助命令
scripts/build 跨平台构建脚本(go run ./scripts/build)
```
+23
View File
@@ -460,6 +460,29 @@ func TestDiagnoseRequiresTarget(t *testing.T) {
}
}
func TestNetPidsDryRun(t *testing.T) {
app, stdout, stderr := testApp(centos("docker"))
if code := app.Execute([]string{"net", "pids", "8080", "--dry-run"}); code != 0 {
t.Fatalf("exit code = %d, stderr=%s", code, stderr.String())
}
out := stdout.String()
if !strings.Contains(out, "(内置) net.pids 8080") {
t.Errorf("dry-run output missing builtin plan:\n%s", out)
}
}
func TestNetPidsRequiresPort(t *testing.T) {
app, _, stderr := testApp(centos("docker"))
if code := app.Execute([]string{"net", "pids"}); code != 1 {
t.Fatalf("exit code = %d, want 1", code)
}
if !strings.Contains(stderr.String(), "缺少参数") {
t.Errorf("stderr = %s", stderr.String())
}
}
// TestCLIHelperProcess is re-executed as a child to verify exit-code passthrough.
func TestCLIHelperProcess(t *testing.T) {
if os.Getenv("KOPSC_CLI_HELPER") != "1" {
+1
View File
@@ -24,6 +24,7 @@ type Builtin func(ctx context.Context, req BuiltinRequest) error
var builtins = map[string]Builtin{
"disk.usage": runDiskUsage,
"diagnose": runDiagnose,
"net.pids": runNetPIDs,
}
// runDiagnose bridges the registry builtin to the diagnose package.
+333
View File
@@ -0,0 +1,333 @@
package executor
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strconv"
"strings"
)
// pidPattern extracts "pid=1234" from ss output.
var pidPattern = regexp.MustCompile(`pid=(\d+)`)
// runNetPIDs implements `lsof -ti:<port>`: print the PID of every process
// holding the given port, one per line.
//
// The kopsc command and its output are identical on every platform; only the
// backend differs:
// - Linux: lsof -> ss -> /proc (first available wins)
// - macOS: lsof (the system ships it; no /proc fallback)
// - Windows: netstat -ano
func runNetPIDs(ctx context.Context, req BuiltinRequest) error {
port, err := parsePortArgs(req.Args)
if err != nil {
return err
}
if runtime.GOOS == "windows" {
return netPIDsWindows(ctx, port, req)
}
return netPIDsUnix(ctx, port, req)
}
func parsePortArgs(args []string) (int, error) {
var value string
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "--port":
if i+1 >= len(args) {
return 0, fmt.Errorf("--port 缺少端口号")
}
i++
value = args[i]
case strings.HasPrefix(arg, "--port="):
value = strings.TrimPrefix(arg, "--port=")
case strings.HasPrefix(arg, "-") && arg != "-":
return 0, fmt.Errorf("未知选项 %q", arg)
default:
if value != "" {
return 0, fmt.Errorf("一次只支持一个端口,收到多个参数")
}
value = arg
}
}
if value == "" {
return 0, fmt.Errorf("必须提供端口,例如 kopsc net pids 8080")
}
port, err := strconv.Atoi(value)
if err != nil || port < 1 || port > 65535 {
return 0, fmt.Errorf("端口必须是 1-65535 的整数,收到 %q", value)
}
return port, nil
}
func netPIDsUnix(ctx context.Context, port int, req BuiltinRequest) error {
pids, err := unixPIDs(ctx, port)
if err != nil {
return err
}
writePIDs(req.Stdout, pids)
return nil
}
// unixPIDs tries lsof, then ss, then /proc on Linux.
func unixPIDs(ctx context.Context, port int) ([]string, error) {
var lsofErr error
if path, err := exec.LookPath("lsof"); err == nil {
pids, matched, err := lsofPIDs(ctx, path, port)
if err == nil {
if !matched {
return nil, nil
}
return pids, nil
}
lsofErr = err
}
var ssErr error
if path, err := exec.LookPath("ss"); err == nil {
pids, err := ssPIDs(ctx, path, port)
if err == nil {
return pids, nil
}
ssErr = err
}
if runtime.GOOS == "linux" {
if pids, err := procPIDs(port, "/proc"); err == nil {
return pids, nil
} else if lsofErr == nil && ssErr == nil {
return nil, err
}
}
switch {
case lsofErr != nil:
return nil, lsofErr
case ssErr != nil:
return nil, ssErr
}
return nil, fmt.Errorf("未找到 lsof 或 ss(macOS 自带 lsof;Linux 请安装 lsof 或 iproute2)")
}
func lsofPIDs(ctx context.Context, path string, port int) ([]string, bool, error) {
cmd := exec.CommandContext(ctx, path, "-ti:"+strconv.Itoa(port))
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
var exitErr *exec.ExitError
// lsof exits 1 with no output when nothing holds the port.
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 && stdout.Len() == 0 {
return nil, false, nil
}
message := strings.TrimSpace(stderr.String())
if message == "" {
message = err.Error()
}
return nil, false, fmt.Errorf("lsof -ti:%d 失败: %s", port, message)
}
return normalizePIDs(strings.Fields(stdout.String())), true, nil
}
func ssPIDs(ctx context.Context, path string, port int) ([]string, error) {
cmd := exec.CommandContext(ctx, path, "-H", "-t", "-u", "-n", "-p")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
message := strings.TrimSpace(stderr.String())
if message == "" {
message = err.Error()
}
return nil, fmt.Errorf("ss -t -u -n -p 失败: %s", message)
}
return parseSSPIDs(stdout.String(), port), nil
}
// parseSSPIDs extracts PIDs for a local port from `ss -t -u -n -p` output.
func parseSSPIDs(output string, port int) []string {
suffix := ":" + strconv.Itoa(port)
var pids []string
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) < 5 {
continue
}
// Netid State Recv-Q Send-Q Local:Port Peer:Port Process
if !strings.HasSuffix(fields[4], suffix) {
continue
}
match := pidPattern.FindStringSubmatch(line)
if len(match) == 2 {
pids = append(pids, match[1])
}
}
return normalizePIDs(pids)
}
// procPIDs derives PIDs from /proc without external tools (Linux only).
func procPIDs(port int, procRoot string) ([]string, error) {
inodes := make(map[string]bool)
for _, name := range []string{"net/tcp", "net/tcp6", "net/udp", "net/udp6"} {
data, err := os.ReadFile(filepath.Join(procRoot, filepath.FromSlash(name)))
if err != nil {
continue
}
lines := strings.Split(string(data), "\n")
if len(lines) > 0 {
lines = lines[1:] // header
}
for _, line := range lines {
fields := strings.Fields(line)
if len(fields) < 10 {
continue
}
local := fields[1]
idx := strings.LastIndex(local, ":")
if idx < 0 {
continue
}
hexPort, err := strconv.ParseInt(local[idx+1:], 16, 32)
if err != nil || int(hexPort) != port {
continue
}
inodes[fields[9]] = true // inode column
}
}
if len(inodes) == 0 {
return nil, nil
}
entries, err := os.ReadDir(procRoot)
if err != nil {
return nil, fmt.Errorf("读取 %s 失败: %w", procRoot, err)
}
var pids []string
for _, entry := range entries {
if !entry.IsDir() {
continue
}
pid := entry.Name()
if _, err := strconv.Atoi(pid); err != nil {
continue
}
fdDir := filepath.Join(procRoot, pid, "fd")
fds, err := os.ReadDir(fdDir)
if err != nil {
continue // permission denied for other users
}
for _, fd := range fds {
link, err := os.Readlink(filepath.Join(fdDir, fd.Name()))
if err != nil {
continue
}
if !strings.HasPrefix(link, "socket:[") {
continue
}
inode := strings.TrimSuffix(strings.TrimPrefix(link, "socket:["), "]")
if inodes[inode] {
pids = append(pids, pid)
break
}
}
}
return normalizePIDs(pids), nil
}
func netPIDsWindows(ctx context.Context, port int, req BuiltinRequest) error {
cmd := exec.CommandContext(ctx, "netstat", "-ano")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
message := strings.TrimSpace(stderr.String())
if message == "" {
message = err.Error()
}
return fmt.Errorf("netstat -ano 失败: %s", message)
}
writePIDs(req.Stdout, parseNetstatPIDs(stdout.String(), port))
return nil
}
// parseNetstatPIDs extracts unique owning PIDs for a local port from
// `netstat -ano` output.
func parseNetstatPIDs(output string, port int) []string {
suffix := ":" + strconv.Itoa(port)
var pids []string
for _, line := range strings.Split(output, "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
proto := strings.ToUpper(fields[0])
if proto != "TCP" && proto != "UDP" {
continue
}
if !strings.HasSuffix(fields[1], suffix) {
continue
}
pid := fields[len(fields)-1]
if _, err := strconv.Atoi(pid); err != nil {
continue
}
pids = append(pids, pid)
}
return normalizePIDs(pids)
}
// normalizePIDs de-duplicates and sorts PIDs numerically so every platform
// prints the same order.
func normalizePIDs(pids []string) []string {
seen := make(map[string]bool)
out := make([]string, 0, len(pids))
for _, pid := range pids {
pid = strings.TrimSpace(pid)
if pid == "" {
continue
}
if _, err := strconv.Atoi(pid); err != nil {
continue
}
if seen[pid] {
continue
}
seen[pid] = true
out = append(out, pid)
}
sort.Slice(out, func(i, j int) bool {
a, _ := strconv.Atoi(out[i])
b, _ := strconv.Atoi(out[j])
return a < b
})
return out
}
func writePIDs(w interface{ Write([]byte) (int, error) }, pids []string) {
for _, pid := range pids {
fmt.Fprintln(w, pid)
}
}
+188
View File
@@ -0,0 +1,188 @@
package executor
import (
"bytes"
"context"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestParsePortArgs(t *testing.T) {
cases := []struct {
name string
args []string
want int
wantErr string
}{
{"positional", []string{"8080"}, 8080, ""},
{"port flag", []string{"--port", "443"}, 443, ""},
{"port flag equals", []string{"--port=443"}, 443, ""},
{"missing", nil, 0, "必须提供端口"},
{"zero", []string{"0"}, 0, "1-65535"},
{"too large", []string{"65536"}, 0, "1-65535"},
{"not a number", []string{"abc"}, 0, "1-65535"},
{"two ports", []string{"80", "81"}, 0, "一次只支持一个端口"},
{"unknown option", []string{"--wat"}, 0, "未知选项"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
port, err := parsePortArgs(tc.args)
if tc.wantErr != "" {
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
t.Fatalf("err = %v, want %q", err, tc.wantErr)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if port != tc.want {
t.Errorf("port = %d, want %d", port, tc.want)
}
})
}
}
func TestParseNetstatPIDs(t *testing.T) {
output := `Active Connections
Proto Local Address Foreign Address State PID
TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING 1234
TCP [::]:8080 [::]:0 LISTENING 1234
TCP 127.0.0.1:8080 127.0.0.1:51515 ESTABLISHED 5678
TCP 0.0.0.0:8081 0.0.0.0:0 LISTENING 9999
UDP 0.0.0.0:8080 *:* 4321
`
got := parseNetstatPIDs(output, 8080)
want := []string{"1234", "4321", "5678"}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Fatalf("pids = %v, want %v", got, want)
}
if got := parseNetstatPIDs(output, 9999); len(got) != 0 {
t.Errorf("port 9999 should have no match, got %v", got)
}
}
func TestNetPIDsUnixWithFakeLsof(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("lsof fixture is not executable on Windows")
}
dir := t.TempDir()
script := filepath.Join(dir, "lsof")
if err := os.WriteFile(script, []byte("#!/bin/sh\necho 111\necho 222\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
var out bytes.Buffer
if err := runNetPIDs(context.Background(), BuiltinRequest{Args: []string{"8080"}, Stdout: &out}); err != nil {
t.Fatalf("runNetPIDs error: %v", err)
}
if out.String() != "111\n222\n" {
t.Fatalf("output = %q", out.String())
}
}
func TestNetPIDsUnixNoMatchIsNotAnError(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("lsof fixture is not executable on Windows")
}
dir := t.TempDir()
script := filepath.Join(dir, "lsof")
if err := os.WriteFile(script, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
var out bytes.Buffer
if err := runNetPIDs(context.Background(), BuiltinRequest{Args: []string{"8080"}, Stdout: &out}); err != nil {
t.Fatalf("no-match should not fail: %v", err)
}
if out.Len() != 0 {
t.Fatalf("expected no output, got %q", out.String())
}
}
func TestNetPIDsRejectsInvalidPortBeforeRunning(t *testing.T) {
var out bytes.Buffer
err := runNetPIDs(context.Background(), BuiltinRequest{Args: []string{"70000"}, Stdout: &out})
if err == nil || !strings.Contains(err.Error(), "1-65535") {
t.Fatalf("err = %v", err)
}
}
func TestParseSSPIDs(t *testing.T) {
output := `tcp ESTAB 0 0 127.0.0.1:8080 127.0.0.1:51515 users:(("app",pid=4321,fd=6))
udp UNCONN 0 0 0.0.0.0:8080 0.0.0.0:* users:(("dns",pid=123,fd=5))
tcp LISTEN 0 128 0.0.0.0:8081 0.0.0.0:* users:(("other",pid=9999,fd=3))`
got := parseSSPIDs(output, 8080)
want := []string{"123", "4321"}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Fatalf("pids = %v, want %v", got, want)
}
}
func TestNetPIDsUnixFallsBackToSS(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("shell fixtures are not executable on Windows")
}
dir := t.TempDir()
body := "#!/bin/sh\n" +
"echo 'tcp ESTAB 0 0 127.0.0.1:8080 127.0.0.1:51515 users:((\"app\",pid=4321,fd=6))'\n" +
"echo 'tcp LISTEN 0 128 0.0.0.0:8081 0.0.0.0:* users:((\"other\",pid=9999,fd=3))'\n"
if err := os.WriteFile(filepath.Join(dir, "ss"), []byte(body), 0o755); err != nil {
t.Fatal(err)
}
// Only the fixture directory: there is no lsof, so ss must be used.
t.Setenv("PATH", dir)
var out bytes.Buffer
if err := runNetPIDs(context.Background(), BuiltinRequest{Args: []string{"8080"}, Stdout: &out}); err != nil {
t.Fatalf("runNetPIDs error: %v", err)
}
if out.String() != "4321\n" {
t.Fatalf("output = %q, want %q", out.String(), "4321\n")
}
}
func TestProcPIDs(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("procPIDs is Linux-only")
}
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "net"), 0o755); err != nil {
t.Fatal(err)
}
tcp := " sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode\n" +
" 0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 12345 1\n"
if err := os.WriteFile(filepath.Join(root, "net", "tcp"), []byte(tcp), 0o644); err != nil {
t.Fatal(err)
}
fdDir := filepath.Join(root, "4242", "fd")
if err := os.MkdirAll(fdDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.Symlink("socket:[12345]", filepath.Join(fdDir, "3")); err != nil {
t.Fatal(err)
}
got, err := procPIDs(8080, root) // 0x1F90 == 8080
if err != nil {
t.Fatalf("procPIDs error: %v", err)
}
if strings.Join(got, ",") != "4242" {
t.Fatalf("pids = %v, want [4242]", got)
}
}
+30 -6
View File
@@ -325,17 +325,41 @@ func Builtin() *Registry {
},
},
},
{
ID: "net.pids",
Group: "net",
Summary: "查找占用指定端口的进程 PID",
Action: "等价 lsof -ti:<端口>:每行输出一个占用该端口的进程 PID。Linux/macOS 调用 lsof,Windows 解析 netstat -ano;无进程占用时无输出",
Usage: "<端口>",
MinArgs: 1,
Variants: []Variant{
{
Match: Match{OS: "linux"},
Builtin: "net.pids",
Note: "Linux 使用 lsof -ti",
},
{
Match: Match{OS: "darwin"},
Builtin: "net.pids",
Note: "macOS 使用 lsof -ti",
},
{
Match: Match{OS: "windows"},
Builtin: "net.pids",
Note: "Windows 解析 netstat -ano",
},
},
},
{
ID: "net.ports",
Group: "net",
Summary: "列出本机正在监听的 TCP/UDP 端口",
Action: "以只读方式列出监听中的套接字及持有进程",
Variants: []Variant{
{
Match: Match{OS: "linux"},
Argv: []string{"ss", "-tulnp"},
Note: "Linux 使用 iproute2 的 ss",
},
Variants: []Variant{{
Match: Match{OS: "linux"},
Argv: []string{"ss", "-tulnp"},
Note: "Linux 使用 iproute2 的 ss",
},
{
Match: Match{OS: "darwin"},
Argv: []string{"lsof", "-iTCP", "-sTCP:LISTEN", "-nP"},
+1 -1
View File
@@ -25,7 +25,7 @@ func TestBuiltinIDsStable(t *testing.T) {
"docker.prunebuilder", "docker.prunevolumes", "docker.clearall",
"docker.compose.up", "docker.compose.down", "docker.compose.ps", "docker.compose.logs",
"diagnose",
"disk.usage", "net.ports", "pkg.refreshindex",
"disk.usage", "net.pids", "net.ports", "pkg.refreshindex",
}
if got := reg.IDs(); !reflect.DeepEqual(got, want) {
t.Fatalf("IDs() =\n%v\nwant\n%v", got, want)