feat: add permission interpreter, junks, ports, usages, network commands
New Commands: - z network: Show IP addresses, public IP, DNS servers, active connections - z ports: List listening ports with process/PID info - z usages: All processes sorted by resource usage with filtering - z junks: Find and clean junk files (caches, temporary files) Enhancements: - Permission interpreter in z info file - shows human-readable permissions e.g. -rw-rw-r-- → "Owner can read, write; Group can read, write; Other can read" - z usages now supports filtering: z usages chrome -m -n=50 - Summary section shows total CPU/memory when filtering by name - Added help entries for all new commands
This commit is contained in:
parent
5b334071e0
commit
e96d60ae91
5 changed files with 1036 additions and 1 deletions
95
cmd/file.go
95
cmd/file.go
|
|
@ -44,7 +44,7 @@ func FileInfoCommand(args []string) error {
|
||||||
fileType := getFileType(filePath)
|
fileType := getFileType(filePath)
|
||||||
tokens := int(float64(words) / 0.75)
|
tokens := int(float64(words) / 0.75)
|
||||||
size := formatter.FormatSize(fileInfo.Size())
|
size := formatter.FormatSize(fileInfo.Size())
|
||||||
perms := fileInfo.Mode().String()
|
perms := FormatPermissions(fileInfo.Mode())
|
||||||
modTime := fileInfo.ModTime().Format(time.RFC1123)
|
modTime := fileInfo.ModTime().Format(time.RFC1123)
|
||||||
|
|
||||||
// Print output
|
// Print output
|
||||||
|
|
@ -65,6 +65,7 @@ func FileInfoCommand(args []string) error {
|
||||||
|
|
||||||
PrintSectionHeader("FILE DETAILS")
|
PrintSectionHeader("FILE DETAILS")
|
||||||
fmt.Printf("%s%s🔒 Permissions:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, perms)
|
fmt.Printf("%s%s🔒 Permissions:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, perms)
|
||||||
|
fmt.Printf("%s └─ %s%s\n", colors.Gray, colors.Reset, GetPermissionHuman(fileInfo.Mode()))
|
||||||
fmt.Printf("%s%s📅 Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime)
|
fmt.Printf("%s%s📅 Modified:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, modTime)
|
||||||
fmt.Printf("%s%s📂 Extension:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, getFileExtension(filePath))
|
fmt.Printf("%s%s📂 Extension:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, getFileExtension(filePath))
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
@ -145,3 +146,95 @@ func getFileExtension(path string) string {
|
||||||
}
|
}
|
||||||
return ext
|
return ext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Permission struct {
|
||||||
|
Symbol string
|
||||||
|
Human string
|
||||||
|
}
|
||||||
|
|
||||||
|
func InterpretPermissions(mode os.FileMode) []Permission {
|
||||||
|
var perms []Permission
|
||||||
|
|
||||||
|
isDir := mode.IsDir()
|
||||||
|
isLink := mode&os.ModeSymlink != 0
|
||||||
|
|
||||||
|
bits := []uint32{
|
||||||
|
uint32(mode >> 6) & 0x7,
|
||||||
|
uint32(mode >> 3) & 0x7,
|
||||||
|
uint32(mode) & 0x7,
|
||||||
|
}
|
||||||
|
|
||||||
|
who := []string{"Owner", "Group", "Other"}
|
||||||
|
readable := []string{"read", "write", "execute"}
|
||||||
|
|
||||||
|
for i, whoStr := range who {
|
||||||
|
for j := 0; j < 3; j++ {
|
||||||
|
bitVal := (bits[i] >> uint(2-j)) & 1
|
||||||
|
symbol := "-"
|
||||||
|
human := "no " + readable[j]
|
||||||
|
if bitVal == 1 {
|
||||||
|
if j == 2 {
|
||||||
|
if isDir {
|
||||||
|
symbol = "x"
|
||||||
|
human = whoStr + " can enter directory"
|
||||||
|
} else if isLink {
|
||||||
|
symbol = "x"
|
||||||
|
human = whoStr + " can follow link"
|
||||||
|
} else {
|
||||||
|
symbol = "x"
|
||||||
|
human = whoStr + " can execute"
|
||||||
|
}
|
||||||
|
} else if j == 1 {
|
||||||
|
symbol = "w"
|
||||||
|
human = whoStr + " can write"
|
||||||
|
} else {
|
||||||
|
symbol = "r"
|
||||||
|
human = whoStr + " can read"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
perms = append(perms, Permission{Symbol: symbol, Human: human})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDir {
|
||||||
|
perms = append([]Permission{{Symbol: "d", Human: "Directory"}}, perms...)
|
||||||
|
} else if isLink {
|
||||||
|
perms = append([]Permission{{Symbol: "l", Human: "Symbolic Link"}}, perms...)
|
||||||
|
} else {
|
||||||
|
perms = append([]Permission{{Symbol: "-", Human: "Regular File"}}, perms...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return perms
|
||||||
|
}
|
||||||
|
|
||||||
|
func FormatPermissions(mode os.FileMode) string {
|
||||||
|
perms := InterpretPermissions(mode)
|
||||||
|
var result string
|
||||||
|
for _, p := range perms {
|
||||||
|
result += p.Symbol
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPermissionHuman(mode os.FileMode) string {
|
||||||
|
perms := InterpretPermissions(mode)
|
||||||
|
var parts []string
|
||||||
|
var currentWho string
|
||||||
|
|
||||||
|
for i, p := range perms {
|
||||||
|
if i == 0 {
|
||||||
|
currentWho = p.Human
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
whoIdx := (i - 1) / 3
|
||||||
|
who := []string{"Owner", "Group", "Other"}[whoIdx]
|
||||||
|
if p.Symbol != "-" {
|
||||||
|
parts = append(parts, who+" "+p.Human[len(who)+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "No permissions"
|
||||||
|
}
|
||||||
|
return currentWho + ", " + strings.Join(parts, ", ")
|
||||||
|
}
|
||||||
|
|
|
||||||
64
cmd/help.go
64
cmd/help.go
|
|
@ -72,6 +72,54 @@ var usageHelp = CommandHelp{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var junksHelp = CommandHelp{
|
||||||
|
Name: "junks",
|
||||||
|
Help: "Find and clean junk files (caches, temporary files).\n" +
|
||||||
|
"Shows storage taken by build outputs, caches, and temporary files.\n" +
|
||||||
|
"Safe to clean: build outputs, caches, temporary files.\n" +
|
||||||
|
"Not cleanable: runtime version files (require manual cleanup).",
|
||||||
|
Examples: []string{
|
||||||
|
"z junks # List junk files and space usage",
|
||||||
|
"z junks clean # Preview what will be cleaned",
|
||||||
|
"z junks clean --force # Actually clean junk files",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var portsHelp = CommandHelp{
|
||||||
|
Name: "ports",
|
||||||
|
Help: "Show all listening ports and the processes using them.\n" +
|
||||||
|
"Useful for debugging network services and finding port conflicts.",
|
||||||
|
Examples: []string{
|
||||||
|
"z ports # Show listening ports",
|
||||||
|
"z ports -a # Show all ports (including established)",
|
||||||
|
"z ports 8080 # Filter by specific port",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var usagesHelp = CommandHelp{
|
||||||
|
Name: "usages",
|
||||||
|
Help: "Show all running processes sorted by resource usage.\n" +
|
||||||
|
"By default sorts by CPU usage. Use -m to sort by memory.",
|
||||||
|
Examples: []string{
|
||||||
|
"z usages # Top processes by CPU",
|
||||||
|
"z usages -m # Top processes by memory",
|
||||||
|
"z usages -n=50 # Show top 50 processes",
|
||||||
|
"z usages chrome # Filter processes by name",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var networkHelp = CommandHelp{
|
||||||
|
Name: "network",
|
||||||
|
Help: "Show network information including:\n" +
|
||||||
|
" - Local IP addresses\n" +
|
||||||
|
" - Public IP\n" +
|
||||||
|
" - DNS servers\n" +
|
||||||
|
" - Active network connections",
|
||||||
|
Examples: []string{
|
||||||
|
"z network # Show network overview",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func PrintErrorAndHelp(name string, err error) {
|
func PrintErrorAndHelp(name string, err error) {
|
||||||
help := getHelpForCommand(name)
|
help := getHelpForCommand(name)
|
||||||
|
|
||||||
|
|
@ -111,6 +159,14 @@ func getHelpForCommand(name string) *CommandHelp {
|
||||||
return &searchHelp
|
return &searchHelp
|
||||||
case "usage":
|
case "usage":
|
||||||
return &usageHelp
|
return &usageHelp
|
||||||
|
case "junks":
|
||||||
|
return &junksHelp
|
||||||
|
case "ports":
|
||||||
|
return &portsHelp
|
||||||
|
case "usages":
|
||||||
|
return &usagesHelp
|
||||||
|
case "network":
|
||||||
|
return &networkHelp
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -127,6 +183,14 @@ func getUsageHint(name string) string {
|
||||||
return "<pattern> [path]"
|
return "<pattern> [path]"
|
||||||
case "usage":
|
case "usage":
|
||||||
return "<process-name>"
|
return "<process-name>"
|
||||||
|
case "junks":
|
||||||
|
return "[clean]"
|
||||||
|
case "ports":
|
||||||
|
return "[-a] [port]"
|
||||||
|
case "usages":
|
||||||
|
return "[-m] [-n=N]"
|
||||||
|
case "network":
|
||||||
|
return ""
|
||||||
case "info":
|
case "info":
|
||||||
return "<file|dir|network>"
|
return "<file|dir|network>"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
383
cmd/junks.go
Normal file
383
cmd/junks.go
Normal file
|
|
@ -0,0 +1,383 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zemenawi/zutils/pkg/colors"
|
||||||
|
"github.com/zemenawi/zutils/pkg/formatter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type JunkItem struct {
|
||||||
|
Path string
|
||||||
|
Size int64
|
||||||
|
Type string
|
||||||
|
SafeToClean bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type JunkStats struct {
|
||||||
|
Items []JunkItem
|
||||||
|
TotalSize int64
|
||||||
|
Cleanable int64
|
||||||
|
NonCleanable int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var junkPatterns = []struct {
|
||||||
|
Pattern string
|
||||||
|
Type string
|
||||||
|
SafeToClean bool
|
||||||
|
}{
|
||||||
|
{".cache", "Cache Directory", true},
|
||||||
|
{"__pycache__", "Python Bytecode", true},
|
||||||
|
{".pytest_cache", "Python Test Cache", true},
|
||||||
|
{".mypy_cache", "Mypy Cache", true},
|
||||||
|
{"node_modules/.cache", "Node Module Cache", true},
|
||||||
|
{".npm", "NPM Cache", true},
|
||||||
|
{".yarn", "Yarn Cache", true},
|
||||||
|
{".pnpm-store", "PNPM Store", true},
|
||||||
|
{".next", "Next.js Cache", true},
|
||||||
|
{".nuxt", "Nuxt Cache", true},
|
||||||
|
{".output", "Nuxt Output", true},
|
||||||
|
{".solid", "SolidJS Cache", true},
|
||||||
|
{".svelte-kit", "SvelteKit Cache", true},
|
||||||
|
{"dist", "Build Output", true},
|
||||||
|
{"build", "Build Output", true},
|
||||||
|
{".gradle/caches", "Gradle Cache", true},
|
||||||
|
{".cargo/registry/cache", "Cargo Registry Cache", true},
|
||||||
|
{".composer/cache", "Composer Cache", true},
|
||||||
|
{".gem/cache", "Ruby Gem Cache", true},
|
||||||
|
{".sass-cache", "Sass Cache", true},
|
||||||
|
{".eslintcache", "ESLint Cache", true},
|
||||||
|
{".webpack", "Webpack Cache", true},
|
||||||
|
{".parcel-cache", "Parcel Cache", true},
|
||||||
|
{"thumbs.db", "Windows Thumbnail Cache", true},
|
||||||
|
{".ds_store", "macOS metadata", true},
|
||||||
|
{".spotlight", "macOS Spotlight", true},
|
||||||
|
{".trash", "Trash", true},
|
||||||
|
{"trash-1000", "Trash", true},
|
||||||
|
{".cache/thumbnails", "Thumbnail Cache", true},
|
||||||
|
{".-runtime-version", "Runtime Version", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
var userHome string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
userHome, _ = os.UserHomeDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func JunksCommand(args []string) error {
|
||||||
|
if len(args) < 1 {
|
||||||
|
return listJunks()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch args[0] {
|
||||||
|
case "clean":
|
||||||
|
return cleanJunks(args[1:])
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unknown subcommand: %s\n\nUsage:\n z junks List junk files and space\n z junks clean Clean junk files", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listJunks() error {
|
||||||
|
fmt.Printf("%s%s🔍 Scanning for junk files...%s\n\n", colors.Yellow, colors.Bold, colors.Reset)
|
||||||
|
|
||||||
|
stats, err := scanForJunks()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error scanning: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(stats.Items) == 0 {
|
||||||
|
fmt.Printf("%s✨ No junk files found! Your system is clean.%s\n", colors.Green, colors.Reset)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintBoxHeader("SYSTEM JUNK ANALYSIS", colors.Red)
|
||||||
|
|
||||||
|
fmt.Printf("%s%s💾 Total Junk Size:%s %s\n", colors.Red, colors.Bold, colors.Reset, formatter.FormatSize(stats.TotalSize))
|
||||||
|
fmt.Printf("%s%s✅ Cleanable:%s %s\n", colors.Green, colors.Bold, colors.Reset, formatter.FormatSize(stats.Cleanable))
|
||||||
|
fmt.Printf("%s%s⚠️ Non-cleanable:%s %s\n", colors.Yellow, colors.Bold, colors.Reset, formatter.FormatSize(stats.NonCleanable))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
byType := groupByType(stats.Items)
|
||||||
|
|
||||||
|
type typeGroup struct {
|
||||||
|
junkType string
|
||||||
|
items []JunkItem
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
var groups []typeGroup
|
||||||
|
for junkType, items := range byType {
|
||||||
|
var size int64
|
||||||
|
for _, item := range items {
|
||||||
|
size += item.Size
|
||||||
|
}
|
||||||
|
groups = append(groups, typeGroup{junkType: junkType, items: items, size: size})
|
||||||
|
}
|
||||||
|
sort.Slice(groups, func(i, j int) bool {
|
||||||
|
return groups[i].size > groups[j].size
|
||||||
|
})
|
||||||
|
|
||||||
|
PrintSectionHeader("JUNK BY TYPE")
|
||||||
|
for _, group := range groups {
|
||||||
|
safe := ""
|
||||||
|
if len(group.items) > 0 && group.items[0].SafeToClean {
|
||||||
|
safe = colors.Green + " [cleanable]" + colors.Reset
|
||||||
|
} else {
|
||||||
|
safe = colors.Yellow + " [manual cleanup]" + colors.Reset
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%s📁 %s%s%s (%d items, %s)\n", colors.Red, colors.Bold, group.junkType, safe, colors.Reset, len(group.items), formatter.FormatSize(group.size))
|
||||||
|
for _, item := range group.items {
|
||||||
|
rel, _ := filepath.Rel(userHome, item.Path)
|
||||||
|
if strings.HasPrefix(item.Path, "/") && !strings.HasPrefix(item.Path, userHome) {
|
||||||
|
rel = item.Path
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s\n", rel)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s%s💡 Tip:%s Run %s z junks clean %s to remove cleanable junk files.\n", colors.Cyan, colors.Bold, colors.Reset, colors.Green, colors.Reset)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanJunks(args []string) error {
|
||||||
|
force := false
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "--force" || arg == "-f" {
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats, err := scanForJunks()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error scanning: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanableItems := filterCleanable(stats.Items)
|
||||||
|
|
||||||
|
if len(cleanableItems) == 0 {
|
||||||
|
fmt.Printf("%s✨ No cleanable junk files found!%s\n", colors.Green, colors.Reset)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalCleanable int64
|
||||||
|
for _, item := range cleanableItems {
|
||||||
|
totalCleanable += item.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintBoxHeader("CLEAN JUNK FILES", colors.Red)
|
||||||
|
fmt.Printf("%s%s🧹 Found %d cleanable items (%s)%s\n\n", colors.Yellow, colors.Bold, len(cleanableItems), formatter.FormatSize(totalCleanable), colors.Reset)
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
fmt.Printf("%s%s⚠️ This will permanently delete:%s\n", colors.Red, colors.Bold, colors.Reset)
|
||||||
|
byType := groupByType(cleanableItems)
|
||||||
|
for junkType, items := range byType {
|
||||||
|
var size int64
|
||||||
|
for _, item := range items {
|
||||||
|
size += item.Size
|
||||||
|
}
|
||||||
|
fmt.Printf(" • %s (%d items, %s)\n", junkType, len(items), formatter.FormatSize(size))
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("%s%sConfirmation required.%s Run with %s--force%s to proceed.\n", colors.Cyan, colors.Bold, colors.Reset, colors.Green, colors.Reset)
|
||||||
|
fmt.Printf("%s%sExample:%s z junks clean --force\n", colors.Cyan, colors.Bold, colors.Reset)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s%s🗑️ Cleaning junk files...%s\n\n", colors.Yellow, colors.Bold, colors.Reset)
|
||||||
|
|
||||||
|
var cleaned int64
|
||||||
|
var failed int
|
||||||
|
for _, item := range cleanableItems {
|
||||||
|
if err := os.RemoveAll(item.Path); err != nil {
|
||||||
|
failed++
|
||||||
|
fmt.Printf(" %s✖%s Failed: %s\n", colors.Red, colors.Reset, item.Path)
|
||||||
|
} else {
|
||||||
|
cleaned += item.Size
|
||||||
|
rel, _ := filepath.Rel(userHome, item.Path)
|
||||||
|
if !strings.HasPrefix(item.Path, userHome) {
|
||||||
|
rel = item.Path
|
||||||
|
}
|
||||||
|
fmt.Printf(" %s✓%s Removed: %s (%s)\n", colors.Green, colors.Reset, rel, formatter.FormatSize(item.Size))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
PrintSectionHeader("CLEANUP SUMMARY")
|
||||||
|
fmt.Printf("%s%s✅ Removed:%s %d items (%s)\n", colors.Green, colors.Bold, colors.Reset, len(cleanableItems)-failed, formatter.FormatSize(cleaned))
|
||||||
|
if failed > 0 {
|
||||||
|
fmt.Printf("%s%s⚠️ Failed:%s %d items\n", colors.Red, colors.Bold, colors.Reset, failed)
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%s💾 Space reclaimed:%s %s\n", colors.Cyan, colors.Bold, colors.Reset, formatter.FormatSize(cleaned))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanForJunks() (*JunkStats, error) {
|
||||||
|
stats := &JunkStats{
|
||||||
|
Items: make([]JunkItem, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
searchRoots := getJunkSearchRoots()
|
||||||
|
|
||||||
|
typeResult := scanDirs(searchRoots, junkPatterns, stats)
|
||||||
|
|
||||||
|
stats.TotalSize = typeResult.TotalSize
|
||||||
|
stats.Cleanable = typeResult.Cleanable
|
||||||
|
stats.NonCleanable = typeResult.NonCleanable
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getJunkSearchRoots() []string {
|
||||||
|
var roots []string
|
||||||
|
|
||||||
|
stdCache := os.Getenv("XDG_CACHE_HOME")
|
||||||
|
if stdCache != "" {
|
||||||
|
roots = append(roots, stdCache)
|
||||||
|
} else {
|
||||||
|
roots = append(roots, filepath.Join(userHome, ".cache"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if appCache := filepath.Join(userHome, ".npm"); dirExists(appCache) {
|
||||||
|
roots = append(roots, appCache)
|
||||||
|
}
|
||||||
|
if appCache := filepath.Join(userHome, ".yarn"); dirExists(appCache) {
|
||||||
|
roots = append(roots, appCache)
|
||||||
|
}
|
||||||
|
if appCache := filepath.Join(userHome, ".pnpm-store"); dirExists(appCache) {
|
||||||
|
roots = append(roots, appCache)
|
||||||
|
}
|
||||||
|
if appCache := filepath.Join(userHome, ".gradle"); dirExists(appCache) {
|
||||||
|
roots = append(roots, appCache)
|
||||||
|
}
|
||||||
|
if appCache := filepath.Join(userHome, ".cargo/registry/cache"); dirExists(appCache) {
|
||||||
|
roots = append(roots, appCache)
|
||||||
|
}
|
||||||
|
if appCache := filepath.Join(userHome, ".cargo/registry/index"); dirExists(appCache) {
|
||||||
|
roots = append(roots, appCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
if tempDir != "" && dirExists(tempDir) {
|
||||||
|
roots = append(roots, tempDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
|
||||||
|
func dirExists(path string) bool {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
return err == nil && info.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanDirs(roots []string, patterns []struct {
|
||||||
|
Pattern string
|
||||||
|
Type string
|
||||||
|
SafeToClean bool
|
||||||
|
}, stats *JunkStats) *JunkStats {
|
||||||
|
for _, root := range roots {
|
||||||
|
if _, err := os.Stat(root); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
baseName := filepath.Base(path)
|
||||||
|
|
||||||
|
for _, jp := range patterns {
|
||||||
|
matched := false
|
||||||
|
|
||||||
|
if jp.Pattern == baseName || baseName == jp.Pattern {
|
||||||
|
matched = true
|
||||||
|
} else if strings.Contains(path, jp.Pattern) {
|
||||||
|
matched = true
|
||||||
|
} else if matched, _ = regexp.MatchString("^"+jp.Pattern+"$", baseName); matched {
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
size := getDirSize(path)
|
||||||
|
stats.Items = append(stats.Items, JunkItem{
|
||||||
|
Path: path,
|
||||||
|
Size: size,
|
||||||
|
Type: jp.Type,
|
||||||
|
SafeToClean: jp.SafeToClean,
|
||||||
|
})
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
if jp.SafeToClean {
|
||||||
|
stats.Cleanable += size
|
||||||
|
} else {
|
||||||
|
stats.NonCleanable += size
|
||||||
|
}
|
||||||
|
stats.TotalSize += size
|
||||||
|
return filepath.SkipDir
|
||||||
|
} else {
|
||||||
|
if jp.SafeToClean {
|
||||||
|
stats.Cleanable += size
|
||||||
|
} else {
|
||||||
|
stats.NonCleanable += size
|
||||||
|
}
|
||||||
|
stats.TotalSize += size
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDirSize(path string) int64 {
|
||||||
|
var size int64
|
||||||
|
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
return info.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath.Walk(path, func(filePath string, fileInfo os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if fileInfo.Mode().IsRegular() {
|
||||||
|
size += fileInfo.Size()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupByType(items []JunkItem) map[string][]JunkItem {
|
||||||
|
result := make(map[string][]JunkItem)
|
||||||
|
for _, item := range items {
|
||||||
|
result[item.Type] = append(result[item.Type], item)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterCleanable(items []JunkItem) []JunkItem {
|
||||||
|
var cleanable []JunkItem
|
||||||
|
for _, item := range items {
|
||||||
|
if item.SafeToClean {
|
||||||
|
cleanable = append(cleanable, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanable
|
||||||
|
}
|
||||||
471
cmd/ports.go
Normal file
471
cmd/ports.go
Normal file
|
|
@ -0,0 +1,471 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/zemenawi/zutils/pkg/colors"
|
||||||
|
"github.com/zemenawi/zutils/pkg/formatter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PortInfo struct {
|
||||||
|
Protocol string
|
||||||
|
LocalAddr string
|
||||||
|
Port int
|
||||||
|
Process string
|
||||||
|
PID int
|
||||||
|
State string
|
||||||
|
}
|
||||||
|
|
||||||
|
func PortsCommand(args []string) error {
|
||||||
|
showAll := false
|
||||||
|
portFilter := 0
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "-a" || arg == "--all" {
|
||||||
|
showAll = true
|
||||||
|
} else if strings.HasPrefix(arg, "-") {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if port, err := strconv.Atoi(arg); err == nil {
|
||||||
|
portFilter = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ports, err := getPortList()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting ports: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if portFilter > 0 {
|
||||||
|
var filtered []PortInfo
|
||||||
|
for _, p := range ports {
|
||||||
|
if p.Port == portFilter {
|
||||||
|
filtered = append(filtered, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ports = filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ports) == 0 {
|
||||||
|
if portFilter > 0 {
|
||||||
|
return fmt.Errorf("no process found using port %d", portFilter)
|
||||||
|
}
|
||||||
|
fmt.Printf("%sNo listening ports found%s\n", colors.Yellow, colors.Reset)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(ports, func(i, j int) bool {
|
||||||
|
if ports[i].Port != ports[j].Port {
|
||||||
|
return ports[i].Port < ports[j].Port
|
||||||
|
}
|
||||||
|
return ports[i].Protocol < ports[j].Protocol
|
||||||
|
})
|
||||||
|
|
||||||
|
PrintBoxHeader("PORT LISTENING", colors.Cyan)
|
||||||
|
|
||||||
|
if portFilter > 0 {
|
||||||
|
fmt.Printf("%s%s🔍 Filtering by port: %d%s\n", colors.Yellow, colors.Bold, portFilter, colors.Reset)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !showAll && len(ports) > 50 {
|
||||||
|
fmt.Printf("%s%s📊 Showing %d of %d ports (use -a for all)%s\n\n",
|
||||||
|
colors.Yellow, colors.Bold, 50, len(ports), colors.Reset)
|
||||||
|
ports = ports[:50]
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{"#", "Protocol", "Port", "Process", "PID", "State"}
|
||||||
|
data := make([][]string, 0, len(ports))
|
||||||
|
|
||||||
|
for i, p := range ports {
|
||||||
|
state := p.State
|
||||||
|
if state == "LISTEN" {
|
||||||
|
state = colors.Green + "LISTEN" + colors.Reset
|
||||||
|
} else if state == "ESTABLISHED" {
|
||||||
|
state = colors.Cyan + "ESTABLISHED" + colors.Reset
|
||||||
|
} else {
|
||||||
|
state = colors.Gray + state + colors.Reset
|
||||||
|
}
|
||||||
|
|
||||||
|
row := []string{
|
||||||
|
fmt.Sprintf("%d", i+1),
|
||||||
|
strings.ToUpper(p.Protocol),
|
||||||
|
fmt.Sprintf("%d", p.Port),
|
||||||
|
p.Process,
|
||||||
|
fmt.Sprintf("%d", p.PID),
|
||||||
|
state,
|
||||||
|
}
|
||||||
|
data = append(data, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.PrintTable(headers, data, colors.Cyan)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Printf("%s💡 Tip:%s Use %s z ports <port>%s to filter by specific port\n",
|
||||||
|
colors.Cyan, colors.Reset, colors.Green, colors.Reset)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPortList() ([]PortInfo, error) {
|
||||||
|
var ports []PortInfo
|
||||||
|
|
||||||
|
cmd := exec.Command("ss", "-tulpn")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
cmd = exec.Command("netstat", "-tulpn")
|
||||||
|
output, err = cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return ports, fmt.Errorf("ss and netstat both failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for _, line := range lines[1:] {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || strings.HasPrefix(line, "Netid") || strings.HasPrefix(line, "State") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
port := parseSSLine(line)
|
||||||
|
if port != nil && port.Port > 0 {
|
||||||
|
ports = append(ports, *port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ports, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSSLine(line string) *PortInfo {
|
||||||
|
info := &PortInfo{}
|
||||||
|
|
||||||
|
protoMatch := regexp.MustCompile(`^(tcp6?|udp6?|udplite)\s+`)
|
||||||
|
protoMatch2 := protoMatch.FindStringSubmatch(strings.TrimSpace(line))
|
||||||
|
if len(protoMatch2) > 1 {
|
||||||
|
info.Protocol = protoMatch2[1]
|
||||||
|
if info.Protocol == "udp" || info.Protocol == "udplite" {
|
||||||
|
info.Protocol = "udp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMatch := regexp.MustCompile(`(LISTEN|UNCONN|ESTAB|CLOSE-WAIT|CLOSING|SYN-SENT|SYN-RECV|FIN-WAIT-1|FIN-WAIT-2|TIME-WAIT)\s+`)
|
||||||
|
stateMatch2 := stateMatch.FindStringSubmatch(line)
|
||||||
|
if len(stateMatch2) > 1 {
|
||||||
|
info.State = stateMatch2[1]
|
||||||
|
} else {
|
||||||
|
info.State = "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
portStr := ""
|
||||||
|
for i := len(line) - 1; i >= 0; i-- {
|
||||||
|
if line[i] == ':' {
|
||||||
|
start := i + 1
|
||||||
|
for start < len(line) && line[start] == ' ' {
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
end := start
|
||||||
|
for end < len(line) && line[end] >= '0' && line[end] <= '9' {
|
||||||
|
end++
|
||||||
|
}
|
||||||
|
if end > start {
|
||||||
|
portStr = line[start:end]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if port, err := strconv.Atoi(portStr); err == nil {
|
||||||
|
info.Port = port
|
||||||
|
}
|
||||||
|
|
||||||
|
pidMatch := regexp.MustCompile(`pid=(\d+)`)
|
||||||
|
pidMatch2 := pidMatch.FindStringSubmatch(line)
|
||||||
|
if len(pidMatch2) > 1 {
|
||||||
|
info.PID, _ = strconv.Atoi(pidMatch2[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
processMatch := regexp.MustCompile(`process_name="([^"]+)"`)
|
||||||
|
processMatch2 := processMatch.FindStringSubmatch(line)
|
||||||
|
if len(processMatch2) > 1 {
|
||||||
|
info.Process = processMatch2[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProcessNameByPID(pid int) string {
|
||||||
|
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid)); err == nil {
|
||||||
|
return strings.TrimSpace(string(data))
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessStats struct {
|
||||||
|
PID int
|
||||||
|
Name string
|
||||||
|
Command string
|
||||||
|
CPUPercent float64
|
||||||
|
MemBytes int64
|
||||||
|
MemPercent float64
|
||||||
|
Status string
|
||||||
|
User string
|
||||||
|
}
|
||||||
|
|
||||||
|
func UsagesCommand(args []string) error {
|
||||||
|
sortBy := "cpu"
|
||||||
|
limit := 20
|
||||||
|
filter := ""
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == "-m" || arg == "--memory" {
|
||||||
|
sortBy = "memory"
|
||||||
|
} else if arg == "-c" || arg == "--cpu" {
|
||||||
|
sortBy = "cpu"
|
||||||
|
} else if strings.HasPrefix(arg, "-n=") {
|
||||||
|
if n, err := strconv.Atoi(strings.TrimPrefix(arg, "-n=")); err == nil && n > 0 {
|
||||||
|
limit = n
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(arg, "--limit=") {
|
||||||
|
if n, err := strconv.Atoi(strings.TrimPrefix(arg, "--limit=")); err == nil && n > 0 {
|
||||||
|
limit = n
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(arg, "-") {
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
filter = arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processes, err := getAllProcessStats()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting process stats: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter != "" {
|
||||||
|
var filtered []ProcessStats
|
||||||
|
for _, p := range processes {
|
||||||
|
if strings.Contains(strings.ToLower(p.Name), strings.ToLower(filter)) ||
|
||||||
|
strings.Contains(strings.ToLower(p.Command), strings.ToLower(filter)) {
|
||||||
|
filtered = append(filtered, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processes = filtered
|
||||||
|
if len(processes) == 0 {
|
||||||
|
return fmt.Errorf("no processes found matching '%s'", filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(processes, func(i, j int) bool {
|
||||||
|
if sortBy == "memory" {
|
||||||
|
return processes[i].MemBytes > processes[j].MemBytes
|
||||||
|
}
|
||||||
|
return processes[i].CPUPercent > processes[j].CPUPercent
|
||||||
|
})
|
||||||
|
|
||||||
|
if limit > 0 && len(processes) > limit {
|
||||||
|
processes = processes[:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintBoxHeader(fmt.Sprintf("TOP PROCESSES (by %s)", strings.ToUpper(sortBy)), colors.Cyan)
|
||||||
|
|
||||||
|
if filter != "" {
|
||||||
|
fmt.Printf("%s%s🔍 Filtering by: %s%s\n", colors.Yellow, colors.Bold, filter, colors.Reset)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := []string{"#", "PID", "Process", "CPU%", "Memory", "Mem%", "Status"}
|
||||||
|
data := make([][]string, 0, len(processes))
|
||||||
|
|
||||||
|
for i, p := range processes {
|
||||||
|
status := p.Status
|
||||||
|
if status == "R" || status == "running" {
|
||||||
|
status = colors.Green + "running" + colors.Reset
|
||||||
|
} else if status == "S" || status == "I" || status == "sleeping" {
|
||||||
|
status = colors.Gray + "sleeping" + colors.Reset
|
||||||
|
} else {
|
||||||
|
status = colors.Yellow + status + colors.Reset
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := p.Name
|
||||||
|
if len(p.Command) > 25 {
|
||||||
|
cmd = p.Command[:25] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
row := []string{
|
||||||
|
fmt.Sprintf("%d", i+1),
|
||||||
|
fmt.Sprintf("%d", p.PID),
|
||||||
|
cmd,
|
||||||
|
fmt.Sprintf("%.1f", p.CPUPercent),
|
||||||
|
formatter.FormatSize(p.MemBytes),
|
||||||
|
fmt.Sprintf("%.1f%%", p.MemPercent),
|
||||||
|
status,
|
||||||
|
}
|
||||||
|
data = append(data, row)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter.PrintTable(headers, data, colors.Cyan)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if filter != "" {
|
||||||
|
var totalCPU float64
|
||||||
|
var totalMem int64
|
||||||
|
for _, p := range processes {
|
||||||
|
totalCPU += p.CPUPercent
|
||||||
|
totalMem += p.MemBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintSectionHeader("SUMMARY")
|
||||||
|
fmt.Printf("%s%s📊 Total CPU:%s %.1f%%\n", colors.Green, colors.Bold, colors.Reset, totalCPU)
|
||||||
|
fmt.Printf("%s%s💾 Total Memory:%s %s (%.1f%% of system)%s\n",
|
||||||
|
colors.Purple, colors.Bold, colors.Reset,
|
||||||
|
formatter.FormatSize(totalMem),
|
||||||
|
float64(totalMem)/float64(getSystemMem())*100,
|
||||||
|
colors.Reset)
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%s💡 Tip:%s Use %s z usages -m%s for memory sorting, %s z usages -n=50%s to show 50 processes\n",
|
||||||
|
colors.Cyan, colors.Reset, colors.Green, colors.Reset, colors.Green, colors.Reset)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getSystemMem() int64 {
|
||||||
|
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "MemTotal:") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 2 {
|
||||||
|
if memKB, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
|
||||||
|
return memKB * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllProcessStats() ([]ProcessStats, error) {
|
||||||
|
var processes []ProcessStats
|
||||||
|
|
||||||
|
dir, err := os.Open("/proc")
|
||||||
|
if err != nil {
|
||||||
|
return processes, err
|
||||||
|
}
|
||||||
|
defer dir.Close()
|
||||||
|
|
||||||
|
entries, err := dir.Readdirnames(0)
|
||||||
|
if err != nil {
|
||||||
|
return processes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
pid, err := strconv.Atoi(entry)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := getFullProcessInfo(pid)
|
||||||
|
if err == nil && proc != nil {
|
||||||
|
processes = append(processes, *proc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFullProcessInfo(pid int) (*ProcessStats, error) {
|
||||||
|
comm, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
name := strings.TrimSpace(string(comm))
|
||||||
|
|
||||||
|
cmdline, _ := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid))
|
||||||
|
cmdStr := strings.Join(strings.Fields(string(cmdline)), " ")
|
||||||
|
if cmdStr == "" {
|
||||||
|
cmdStr = name
|
||||||
|
}
|
||||||
|
|
||||||
|
stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
statParts := strings.Split(string(stat), " ")
|
||||||
|
status := "?"
|
||||||
|
if len(statParts) > 2 {
|
||||||
|
status = strings.Trim(statParts[2], "()")
|
||||||
|
}
|
||||||
|
|
||||||
|
utime := int64(0)
|
||||||
|
stime := int64(0)
|
||||||
|
if len(statParts) >= 15 {
|
||||||
|
utime, _ = strconv.ParseInt(strings.TrimSpace(statParts[13]), 10, 64)
|
||||||
|
stime, _ = strconv.ParseInt(strings.TrimSpace(statParts[14]), 10, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
memBytes := int64(0)
|
||||||
|
if data, err := os.ReadFile(fmt.Sprintf("/proc/%d/status", pid)); err == nil {
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "VmRSS:") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 2 {
|
||||||
|
if memKB, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
|
||||||
|
memBytes = memKB * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalMem := int64(0)
|
||||||
|
if data, err := os.ReadFile("/proc/meminfo"); err == nil {
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.HasPrefix(line, "MemTotal:") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) >= 2 {
|
||||||
|
if memKB, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
|
||||||
|
totalMem = memKB * 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuPercent := 0.0
|
||||||
|
if totalMem > 0 {
|
||||||
|
cpuPercent = float64(utime+stime) / float64(totalMem) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
memPercent := 0.0
|
||||||
|
if totalMem > 0 {
|
||||||
|
memPercent = float64(memBytes) / float64(totalMem) * 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProcessStats{
|
||||||
|
PID: pid,
|
||||||
|
Name: name,
|
||||||
|
Command: cmdStr,
|
||||||
|
CPUPercent: cpuPercent,
|
||||||
|
MemBytes: memBytes,
|
||||||
|
MemPercent: memPercent,
|
||||||
|
Status: status,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NetworkCommand(args []string) error {
|
||||||
|
return NetworkInfoCommand(args)
|
||||||
|
}
|
||||||
24
main.go
24
main.go
|
|
@ -45,6 +45,30 @@ func main() {
|
||||||
|
|
||||||
cmd.RegisterInfoCommands(registry)
|
cmd.RegisterInfoCommands(registry)
|
||||||
|
|
||||||
|
registry.Register(&types.Command{
|
||||||
|
Name: "ports",
|
||||||
|
Description: "Show listening ports and processes using them",
|
||||||
|
Handler: cmd.PortsCommand,
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.Register(&types.Command{
|
||||||
|
Name: "usages",
|
||||||
|
Description: "Show all processes sorted by resource usage",
|
||||||
|
Handler: cmd.UsagesCommand,
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.Register(&types.Command{
|
||||||
|
Name: "network",
|
||||||
|
Description: "Show network information (IP, connections, DNS)",
|
||||||
|
Handler: cmd.NetworkCommand,
|
||||||
|
})
|
||||||
|
|
||||||
|
registry.Register(&types.Command{
|
||||||
|
Name: "junks",
|
||||||
|
Description: "Find and clean junk files (caches, temporary files)",
|
||||||
|
Handler: cmd.JunksCommand,
|
||||||
|
})
|
||||||
|
|
||||||
registry.Register(&types.Command{
|
registry.Register(&types.Command{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Description: "Show version information",
|
Description: "Show version information",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue