package executor import ( "context" "fmt" "io" "io/fs" "os" "os/exec" "path/filepath" "runtime" "sort" "strconv" "strings" ) // runDiskCheck implements `kopsc disk check`: comprehensive disk health check. func runDiskCheck(ctx context.Context, req BuiltinRequest) error { opts, err := parseDiskCheckArgs(req.Args) if err != nil { return err } w := req.Stdout fmt.Fprintln(w, "=== 磁盘健康检查 ===") fmt.Fprintln(w) if opts.all { // Check all mount points return diskCheckAll(ctx, req) } path := opts.path if path == "" { path = "/" } return diskCheckPath(ctx, req, path) } type diskCheckOptions struct { path string all bool } func parseDiskCheckArgs(args []string) (diskCheckOptions, error) { var opts diskCheckOptions var positional []string for i := 0; i < len(args); i++ { arg := args[i] switch { case arg == "--": positional = append(positional, args[i+1:]...) i = len(args) case arg == "--all" || arg == "-a": opts.all = true case strings.HasPrefix(arg, "-") && arg != "-": return opts, fmt.Errorf("未知选项 %q", arg) default: positional = append(positional, arg) } } if len(positional) > 0 { opts.path = positional[0] } return opts, nil } func diskCheckAll(ctx context.Context, req BuiltinRequest) error { // Get all mount points from df var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.CommandContext(ctx, "wmic", "logicaldisk", "get", "caption,size,freespace", "/format:list") } else { cmd = exec.CommandContext(ctx, "df", "-h") } out, err := cmd.Output() if err != nil { return fmt.Errorf("获取磁盘信息失败: %w", err) } // Parse df output to get mount points lines := strings.Split(strings.TrimSpace(string(out)), "\n") if len(lines) < 2 { return fmt.Errorf("无法解析磁盘信息") } // Skip header line for _, line := range lines[1:] { fields := strings.Fields(line) if len(fields) < 6 { continue } mountPoint := fields[len(fields)-1] if runtime.GOOS != "windows" { fmt.Fprintf(req.Stdout, "\n--- %s ---\n\n", mountPoint) if err := diskCheckPath(ctx, req, mountPoint); err != nil { fmt.Fprintf(req.Stderr, "检查 %s 失败: %v\n", mountPoint, err) } } } return nil } func diskCheckPath(ctx context.Context, req BuiltinRequest, path string) error { w := req.Stdout // 1. Space usage fmt.Fprintln(w, "[1/5] 空间使用") diskCheckSpace(ctx, w, path) fmt.Fprintln(w) // 2. Inode usage (Unix only) if runtime.GOOS != "windows" { fmt.Fprintln(w, "[2/5] inode 使用") diskCheckInode(ctx, w, path) fmt.Fprintln(w) } else { fmt.Fprintln(w, "[2/5] inode 使用") fmt.Fprintln(w, " Windows 不支持 inode") fmt.Fprintln(w) } // 3. Large files fmt.Fprintln(w, "[3/5] 大文件 TOP 5(>100M)") diskCheckLargeFiles(ctx, w, path) fmt.Fprintln(w) // 4. Large directories fmt.Fprintln(w, "[4/5] 大目录 TOP 5") diskCheckLargeDirs(ctx, w, path) fmt.Fprintln(w) // 5. Recommendations fmt.Fprintln(w, "[5/5] 建议") diskCheckRecommendations(ctx, w, path) return nil } func diskCheckSpace(ctx context.Context, w io.Writer, path string) { var cmd *exec.Cmd if runtime.GOOS == "windows" { cmd = exec.CommandContext(ctx, "wmic", "logicaldisk", "get", "size,freespace,caption", "/format:list") } else { cmd = exec.CommandContext(ctx, "df", "-h", path) } out, err := cmd.Output() if err != nil { fmt.Fprintln(w, " 无法获取磁盘空间信息") return } lines := strings.Split(strings.TrimSpace(string(out)), "\n") if len(lines) < 2 { fmt.Fprintln(w, " 无法解析磁盘信息") return } // Parse and display for _, line := range lines[1:] { fields := strings.Fields(line) if len(fields) < 5 { continue } if runtime.GOOS == "windows" { // Parse WMIC output fmt.Fprintf(w, " %s\n", line) } else { // Parse df output: Filesystem Size Used Avail Use% Mounted filesystem := fields[0] size := fields[1] used := fields[2] avail := fields[3] usePct := fields[4] mount := fields[5] status := "✓ 正常" if pct, err := strconv.Atoi(strings.TrimSuffix(usePct, "%")); err == nil { if pct >= 90 { status = "✗ 危险!空间即将耗尽" } else if pct >= 80 { status = "! 警告:空间使用率较高" } } fmt.Fprintf(w, " %s %s 总计 %s 已用 %s 可用 %s 使用率 %s\n", filesystem, size, used, avail, usePct, status) if mount != path { fmt.Fprintf(w, " 挂载点: %s\n", mount) } } } } func diskCheckInode(ctx context.Context, w io.Writer, path string) { cmd := exec.CommandContext(ctx, "df", "-i", path) out, err := cmd.Output() if err != nil { fmt.Fprintln(w, " 无法获取 inode 信息") return } lines := strings.Split(strings.TrimSpace(string(out)), "\n") if len(lines) < 2 { fmt.Fprintln(w, " 无法解析 inode 信息") return } for _, line := range lines[1:] { fields := strings.Fields(line) if len(fields) < 5 { continue } filesystem := fields[0] inodes := fields[1] used := fields[2] free := fields[3] usePct := fields[4] status := "✓ 正常" if pct, err := strconv.Atoi(strings.TrimSuffix(usePct, "%")); err == nil { if pct >= 90 { status = "✗ 危险!inode 即将耗尽" } else if pct >= 80 { status = "! 警告:inode 使用率较高" } } fmt.Fprintf(w, " %s %s 总计 %s 已用 %s 可用 %s 使用率 %s\n", filesystem, inodes, used, free, usePct, status) } } func diskCheckLargeFiles(ctx context.Context, w io.Writer, path string) { type fileEntry struct { path string size int64 } var files []fileEntry threshold := int64(100 * 1024 * 1024) // 100MB err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error { if err != nil { return nil // Skip errors } if d.IsDir() { // Skip common non-user directories base := filepath.Base(p) if base == ".git" || base == "node_modules" || base == ".cache" { return filepath.SkipDir } return nil } if info, err := d.Info(); err == nil && info.Mode().IsRegular() { if info.Size() >= threshold { files = append(files, fileEntry{path: p, size: info.Size()}) } } return nil }) if err != nil { fmt.Fprintf(w, " 扫描失败: %v\n", err) return } if len(files) == 0 { fmt.Fprintln(w, " 没有发现大于 100M 的文件 ✓") return } // Sort by size descending sort.Slice(files, func(i, j int) bool { return files[i].size > files[j].size }) // Show top 5 limit := 5 if len(files) < limit { limit = len(files) } for i := 0; i < limit; i++ { fmt.Fprintf(w, " %s %s\n", humanSize(files[i].size), files[i].path) } if len(files) > 5 { fmt.Fprintf(w, " ... 还有 %d 个大文件\n", len(files)-5) } } func diskCheckLargeDirs(ctx context.Context, w io.Writer, path string) { type dirEntry struct { path string size int64 } var dirs []dirEntry // Only check direct children entries, err := os.ReadDir(path) if err != nil { fmt.Fprintf(w, " 无法读取目录: %v\n", err) return } for _, entry := range entries { if !entry.IsDir() { continue } dirPath := filepath.Join(path, entry.Name()) size, err := dirSize(ctx, dirPath, 0, false) if err != nil { continue } if size > 0 { dirs = append(dirs, dirEntry{path: dirPath, size: size}) } } if len(dirs) == 0 { fmt.Fprintln(w, " 没有子目录") return } // Sort by size descending sort.Slice(dirs, func(i, j int) bool { return dirs[i].size > dirs[j].size }) // Show top 5 limit := 5 if len(dirs) < limit { limit = len(dirs) } for i := 0; i < limit; i++ { fmt.Fprintf(w, " %s %s\n", humanSize(dirs[i].size), dirs[i].path) } } func diskCheckRecommendations(ctx context.Context, w io.Writer, path string) { var recommendations []string // Check disk usage percentage cmd := exec.CommandContext(ctx, "df", path) out, err := cmd.Output() if err == nil { lines := strings.Split(strings.TrimSpace(string(out)), "\n") if len(lines) >= 2 { fields := strings.Fields(lines[1]) if len(fields) >= 5 { usePct := fields[4] if pct, err := strconv.Atoi(strings.TrimSuffix(usePct, "%")); err == nil { if pct >= 90 { recommendations = append(recommendations, "✗ 磁盘空间严重不足!建议立即清理或扩容") } else if pct >= 80 { recommendations = append(recommendations, "! 磁盘空间使用率较高,建议清理不必要的文件") } else { recommendations = append(recommendations, "✓ 磁盘空间充足") } } } } } // Check inode usage if runtime.GOOS != "windows" { cmd = exec.CommandContext(ctx, "df", "-i", path) out, err = cmd.Output() if err == nil { lines := strings.Split(strings.TrimSpace(string(out)), "\n") if len(lines) >= 2 { fields := strings.Fields(lines[1]) if len(fields) >= 5 { usePct := fields[4] if pct, err := strconv.Atoi(strings.TrimSuffix(usePct, "%")); err == nil { if pct >= 90 { recommendations = append(recommendations, "✗ inode 严重不足!大量小文件可能耗尽 inode") } else if pct >= 80 { recommendations = append(recommendations, "! inode 使用率较高") } else { recommendations = append(recommendations, "✓ inode 使用正常") } } } } } } // Check for common cleanup targets commonTargets := []string{ "/var/log", "/tmp", "/var/cache", } for _, target := range commonTargets { if info, err := os.Stat(target); err == nil && info.IsDir() { size, _ := dirSize(ctx, target, 0, false) if size > 1024*1024*1024 { // > 1GB recommendations = append(recommendations, fmt.Sprintf("! %s 占用 %s,可考虑清理", target, humanSize(size))) } } } if len(recommendations) == 0 { fmt.Fprintln(w, " ✓ 一切正常") } else { for _, rec := range recommendations { fmt.Fprintf(w, " %s\n", rec) } } }