fuckoff
This commit is contained in:
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Binaries
|
||||||
|
/fo
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Build
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Go stuff
|
||||||
|
vendor/
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# IDEs
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 iwasforcedtobehere
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
13
Makefile
Normal file
13
Makefile
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
BINARY=fo
|
||||||
|
PKG=git.gostacks.org/iwasforcedtobehere/fo/cmd/fo
|
||||||
|
|
||||||
|
.PHONY: build tidy clean
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build -o $(BINARY) $(PKG)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f $(BINARY)
|
||||||
|
|
||||||
|
tidy:
|
||||||
|
go mod tidy
|
91
README.md
Normal file
91
README.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# fo — (FUCK OFF) for ports
|
||||||
|
|
||||||
|
|
||||||
|
## What is this?
|
||||||
|
`fo` is a cross-platform command-line tool that:
|
||||||
|
- Detects which process is hogging a TCP port (e.g., `3000`).
|
||||||
|
- Tries to end it gracefully; if it plays tough, we go full send (`kill -9` / `taskkill /F`).
|
||||||
|
- Prompts for `sudo` if your mortal shell lacks the power.
|
||||||
|
- Prints a colorful report with PID, name, and how long it’s been loitering.
|
||||||
|
- Drops a few lighthearted quips, and yes, the occasional fuck.
|
||||||
|
|
||||||
|
In short: `fo 3000` → Port 3000 is yours again.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
```
|
||||||
|
$ fo 3000
|
||||||
|
Scanning for suspicious activity on port 3000...
|
||||||
|
Found PID 1234 (node) possibly hogging port 3000 for 2h 13m 05s.
|
||||||
|
Process survived the gentle nudge. Time for the big red button.
|
||||||
|
Killed process 1234 (node) which was partying on port 3000 for 2h 13m 05s.
|
||||||
|
Port 3000’s unwanted guest has been evicted with extreme prejudice!
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
- Go 1.21+
|
||||||
|
- Linux/macOS/Windows
|
||||||
|
|
||||||
|
### go install
|
||||||
|
```
|
||||||
|
go install git.gostacks.org/iwasforcedtobehere/fo/cmd/fo@latest
|
||||||
|
```
|
||||||
|
This will give you a `fo` binary in your `$GOPATH/bin` or `$GOBIN`.
|
||||||
|
|
||||||
|
### From source
|
||||||
|
```
|
||||||
|
git clone https://git.gostacks.org/iwasforcedtobehere/fo
|
||||||
|
cd fo
|
||||||
|
make build # or go build ./cmd/fo
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
```
|
||||||
|
fo <port>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
fo 3000 # tell 3000 to fuck off
|
||||||
|
fo 8080 # 8080 is not a personality trait
|
||||||
|
fo --help # usage info
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
- Input validation: accepts only ports 1–65535.
|
||||||
|
- Process detection:
|
||||||
|
- Linux/macOS: prefers `lsof`, falls back to `ss` or `netstat`.
|
||||||
|
- Windows: uses `netstat -ano` + process introspection.
|
||||||
|
- Duration: approximates how long the process has been up via `gopsutil` (process create time). If we can’t be sure, we try to be honest and say “unknown duration”.
|
||||||
|
- Killing strategy:
|
||||||
|
- Gentle: `kill -TERM` or `taskkill /PID`.
|
||||||
|
- Force: `kill -KILL` or `taskkill /F`.
|
||||||
|
- Permissions: if the OS says “nope”, we’ll prompt for `sudo` and try again.
|
||||||
|
- Output: colorful, readable, and a bit cheeky.
|
||||||
|
|
||||||
|
## Philosophy (polite satire included)
|
||||||
|
- Ports are public spaces. If your process is double-parking on `:3000`, it can fuck off.
|
||||||
|
- We try kindness first, then consequences. It’s DevOps, not diplomacy.
|
||||||
|
- Tooling should be honest, helpful, and slightly entertaining when it fails.
|
||||||
|
|
||||||
|
## Dev plan (tl;dr of the build)
|
||||||
|
- CLI parsing with `flag`.
|
||||||
|
- Cross-platform port → PID resolution.
|
||||||
|
- Uptime via `gopsutil`.
|
||||||
|
- Elevation fallback via `sudo` where needed.
|
||||||
|
- Colorful output via `fatih/color`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- On Linux/macOS, you may need `lsof`, `ss`, or `netstat` installed.
|
||||||
|
- On Windows, `netstat` and `taskkill` are built-in.
|
||||||
|
- `sudo` prompts are interactive in your terminal.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
- Add `--json` output for scripts and CI.
|
||||||
|
- Add `--udp` support.
|
||||||
|
- Add `--ask` mode to confirm before killing when multiple contenders exist.
|
||||||
|
- Homebrew/Scoop packaging for easy install.
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT. Be kind. Don’t be reckless in production unless your SLAs can take a hit.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
- Built in Go. Colored by `fatih/color`. Informed by `gopsutil`.
|
||||||
|
- Proudly hosted under: https://git.gostacks.org/iwasforcedtobehere/fo
|
387
cmd/fo/main.go
Normal file
387
cmd/fo/main.go
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/color"
|
||||||
|
"github.com/shirou/gopsutil/v3/process"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProcInfo struct {
|
||||||
|
PID int32
|
||||||
|
Name string
|
||||||
|
Started time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var showHelp bool
|
||||||
|
flag.BoolVar(&showHelp, "help", false, "Show help")
|
||||||
|
flag.BoolVar(&showHelp, "h", false, "Show help")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if showHelp || flag.NArg() != 1 {
|
||||||
|
usage()
|
||||||
|
if flag.NArg() != 1 {
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
portStr := flag.Arg(0)
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil || port < 1 || port > 65535 {
|
||||||
|
color.Red("Invalid port: %s (must be 1-65535)", portStr)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Cyan("Scanning for suspicious activity on port %d...", port)
|
||||||
|
|
||||||
|
pid, name, err := findPIDByPort(port)
|
||||||
|
if err != nil {
|
||||||
|
color.Red("No process found for port %d: %v", port, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := ProcInfo{PID: int32(pid), Name: name}
|
||||||
|
if p, err := process.NewProcess(int32(pid)); err == nil {
|
||||||
|
if ct, err := p.CreateTime(); err == nil && ct > 0 {
|
||||||
|
info.Started = time.Unix(0, ct*int64(time.Millisecond))
|
||||||
|
}
|
||||||
|
if n, err := p.Name(); err == nil && n != "" {
|
||||||
|
info.Name = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration estimation
|
||||||
|
var durStr string
|
||||||
|
if !info.Started.IsZero() {
|
||||||
|
dur := time.Since(info.Started)
|
||||||
|
durStr = humanizeDuration(dur)
|
||||||
|
} else {
|
||||||
|
durStr = "unknown duration"
|
||||||
|
}
|
||||||
|
|
||||||
|
color.Yellow("Found PID %d (%s) possibly hogging port %d for %s.", info.PID, info.Name, port, durStr)
|
||||||
|
|
||||||
|
// Try graceful kill first
|
||||||
|
if err := tryKill(info.PID, false); err != nil {
|
||||||
|
if isPermissionError(err) {
|
||||||
|
color.Magenta("Insufficient permissions. Attempting elevated termination (we'll ask nicely, then not-so-nicely).")
|
||||||
|
if err := tryKillElevated(info.PID, false); err != nil {
|
||||||
|
if !isNoSuchProcessError(err) && processExists(info.PID) {
|
||||||
|
color.Red("Failed gentle kill even with elevation: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if !isNoSuchProcessError(err) {
|
||||||
|
color.Red("Gentle kill error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the process a moment to exit after TERM
|
||||||
|
waitForExit(info.PID, 1500*time.Millisecond)
|
||||||
|
|
||||||
|
// Check if process still exists
|
||||||
|
alive := processExists(info.PID)
|
||||||
|
if alive {
|
||||||
|
color.Yellow("Process survived the gentle nudge. Time for the big red button.")
|
||||||
|
if err := tryKill(info.PID, true); err != nil {
|
||||||
|
if isNoSuchProcessError(err) {
|
||||||
|
// It's gone — treat as success
|
||||||
|
} else if isPermissionError(err) {
|
||||||
|
if err := tryKillElevated(info.PID, true); err != nil && !isNoSuchProcessError(err) {
|
||||||
|
// Re-check after elevation attempt
|
||||||
|
waitForExit(info.PID, 800*time.Millisecond)
|
||||||
|
if processExists(info.PID) {
|
||||||
|
color.Red("Even the big red button failed: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Transient errors may happen; wait and verify
|
||||||
|
waitForExit(info.PID, 800*time.Millisecond)
|
||||||
|
if processExists(info.PID) {
|
||||||
|
color.Red("Force kill failed: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// After force, wait briefly and confirm
|
||||||
|
waitForExit(info.PID, 500*time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final confirmation
|
||||||
|
if processExists(info.PID) {
|
||||||
|
color.Red("The process refuses to die. Consider manual intervention.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
quips := []string{
|
||||||
|
"That pesky process is toast!",
|
||||||
|
fmt.Sprintf("Off with its PID! Bye-bye %d.", info.PID),
|
||||||
|
fmt.Sprintf("Port %d’s unwanted guest has been evicted with extreme prejudice!", port),
|
||||||
|
fmt.Sprintf("It’s RIP time for PID %d—no more freeloading!", info.PID),
|
||||||
|
"We came, we saw, we `kill -9`'d.",
|
||||||
|
"Sometimes you just have to say: fuck it, and reclaim the port.",
|
||||||
|
}
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
msg := quips[rand.Intn(len(quips))]
|
||||||
|
|
||||||
|
color.Green("Killed process %d (%s) which was partying on port %d for %s.", info.PID, info.Name, port, durStr)
|
||||||
|
color.HiBlue(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Printf("fo — ruthlessly liberate a busy port (fo stands for 'FUCK OFF')\n")
|
||||||
|
fmt.Printf("Usage: fo <port>\n")
|
||||||
|
fmt.Printf("Example: fo 3000\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func humanizeDuration(d time.Duration) string {
|
||||||
|
if d < time.Minute {
|
||||||
|
return fmt.Sprintf("%ds", int(d.Seconds()))
|
||||||
|
}
|
||||||
|
hours := int(d.Hours())
|
||||||
|
minutes := int(d.Minutes()) % 60
|
||||||
|
seconds := int(d.Seconds()) % 60
|
||||||
|
if hours > 0 {
|
||||||
|
return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%dm %ds", minutes, seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPIDByPort(port int) (int, string, error) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return findPIDByPortWindows(port)
|
||||||
|
}
|
||||||
|
// Prefer lsof, fallback to ss/netstat
|
||||||
|
if pid, name, err := findByLsof(port); err == nil {
|
||||||
|
return pid, name, nil
|
||||||
|
}
|
||||||
|
if pid, name, err := findBySS(port); err == nil {
|
||||||
|
return pid, name, nil
|
||||||
|
}
|
||||||
|
return findByNetstat(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByLsof(port int) (int, string, error) {
|
||||||
|
cmd := exec.Command("bash", "-lc", fmt.Sprintf("lsof -nP -iTCP:%d -sTCP:LISTEN -Fp -a +c 15 | sed -n 's/^p//p' | head -n1", port))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil || len(out) == 0 {
|
||||||
|
return 0, "", errors.New("lsof no match")
|
||||||
|
}
|
||||||
|
pidStr := strings.TrimSpace(string(out))
|
||||||
|
pid, _ := strconv.Atoi(pidStr)
|
||||||
|
if pid == 0 {
|
||||||
|
return 0, "", errors.New("invalid pid from lsof")
|
||||||
|
}
|
||||||
|
name := procName(int32(pid))
|
||||||
|
return pid, name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBySS(port int) (int, string, error) {
|
||||||
|
// ss -ltnp | grep :PORT
|
||||||
|
cmd := exec.Command("bash", "-lc", fmt.Sprintf("ss -ltnp | grep ':%d' || true", port))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil || len(out) == 0 {
|
||||||
|
return 0, "", errors.New("ss no match")
|
||||||
|
}
|
||||||
|
line := string(out)
|
||||||
|
// look for pid=1234
|
||||||
|
pid := parsePIDFromSS(line)
|
||||||
|
if pid == 0 {
|
||||||
|
return 0, "", errors.New("no pid in ss")
|
||||||
|
}
|
||||||
|
name := procName(int32(pid))
|
||||||
|
return pid, name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePIDFromSS(s string) int {
|
||||||
|
// typical: users:(('node',pid=1234,fd=23))
|
||||||
|
idx := strings.Index(s, "pid=")
|
||||||
|
if idx == -1 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
rest := s[idx+4:]
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range rest {
|
||||||
|
if r >= '0' && r <= '9' {
|
||||||
|
b.WriteRune(r)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pid, _ := strconv.Atoi(b.String())
|
||||||
|
return pid
|
||||||
|
}
|
||||||
|
|
||||||
|
func findByNetstat(port int) (int, string, error) {
|
||||||
|
cmd := exec.Command("bash", "-lc", fmt.Sprintf("netstat -nlp 2>/dev/null | grep ':%d' || true", port))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil || len(out) == 0 {
|
||||||
|
return 0, "", errors.New("netstat no match")
|
||||||
|
}
|
||||||
|
line := string(out)
|
||||||
|
// look for pid/program-name
|
||||||
|
pid := parsePIDFromNetstat(line)
|
||||||
|
if pid == 0 {
|
||||||
|
return 0, "", errors.New("no pid in netstat")
|
||||||
|
}
|
||||||
|
name := procName(int32(pid))
|
||||||
|
return pid, name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePIDFromNetstat(s string) int {
|
||||||
|
// typical: ... LISTEN 1234/node
|
||||||
|
// find field with slash
|
||||||
|
fields := strings.Fields(s)
|
||||||
|
for _, f := range fields {
|
||||||
|
if strings.Contains(f, "/") {
|
||||||
|
parts := strings.SplitN(f, "/", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
pid, _ := strconv.Atoi(parts[0])
|
||||||
|
if pid > 0 {
|
||||||
|
return pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPIDByPortWindows(port int) (int, string, error) {
|
||||||
|
// netstat -ano | findstr :PORT
|
||||||
|
cmd := exec.Command("cmd", "/C", fmt.Sprintf("netstat -ano | findstr :%d", port))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil || len(out) == 0 {
|
||||||
|
return 0, "", errors.New("netstat no match")
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(string(out)))
|
||||||
|
var line string
|
||||||
|
for scanner.Scan() {
|
||||||
|
l := scanner.Text()
|
||||||
|
if strings.Contains(l, fmt.Sprintf(":%d", port)) && strings.Contains(l, "LISTENING") {
|
||||||
|
line = l
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if line == "" {
|
||||||
|
return 0, "", errors.New("no listening line")
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 5 {
|
||||||
|
return 0, "", errors.New("unexpected netstat format")
|
||||||
|
}
|
||||||
|
pid, _ := strconv.Atoi(fields[len(fields)-1])
|
||||||
|
name := windowsProcessName(pid)
|
||||||
|
return pid, name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func windowsProcessName(pid int) string {
|
||||||
|
if p, err := process.NewProcess(int32(pid)); err == nil {
|
||||||
|
if n, err := p.Name(); err == nil {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func procName(pid int32) string {
|
||||||
|
if p, err := process.NewProcess(pid); err == nil {
|
||||||
|
if n, err := p.Name(); err == nil && n != "" {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
if e, err := p.Exe(); err == nil && e != "" {
|
||||||
|
parts := strings.Split(e, "/")
|
||||||
|
return parts[len(parts)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryKill(pid int32, force bool) error {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// taskkill /PID pid [/F]
|
||||||
|
args := []string{"/C", "taskkill", "/PID", fmt.Sprintf("%d", pid)}
|
||||||
|
if force {
|
||||||
|
args = append(args, "/F")
|
||||||
|
}
|
||||||
|
cmd := exec.Command("cmd", args...)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
sig := "-TERM"
|
||||||
|
if force {
|
||||||
|
sig = "-KILL"
|
||||||
|
}
|
||||||
|
cmd := exec.Command("kill", sig, fmt.Sprintf("%d", pid))
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryKillElevated(pid int32, force bool) error {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
// No sudo on Windows; rely on current privileges
|
||||||
|
return errors.New("elevation not supported on windows")
|
||||||
|
}
|
||||||
|
sig := "-TERM"
|
||||||
|
if force {
|
||||||
|
sig = "-KILL"
|
||||||
|
}
|
||||||
|
// Use sudo to prompt interactively in the terminal
|
||||||
|
cmd := exec.Command("sudo", "kill", sig, fmt.Sprintf("%d", pid))
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPermissionError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
msg := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(msg, "operation not permitted") || strings.Contains(msg, "permission") || strings.Contains(msg, "access is denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isNoSuchProcessError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
msg := strings.ToLower(err.Error())
|
||||||
|
return strings.Contains(msg, "no such process") || strings.Contains(msg, "process not found") || strings.Contains(msg, "esrch")
|
||||||
|
}
|
||||||
|
|
||||||
|
func processExists(pid int32) bool {
|
||||||
|
if pid <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(fmt.Sprintf("/proc/%d", pid)); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Fallback via gopsutil (cross-platform)
|
||||||
|
if p, err := process.NewProcess(pid); err == nil {
|
||||||
|
if ok, err := p.IsRunning(); err == nil {
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForExit(pid int32, timeout time.Duration) {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
if !processExists(pid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
21
go.mod
Normal file
21
go.mod
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module git.gostacks.org/iwasforcedtobehere/fo
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fatih/color v1.18.0
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
golang.org/x/sys v0.25.0 // indirect
|
||||||
|
)
|
45
go.sum
Normal file
45
go.sum
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||||
|
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||||
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
|
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||||
|
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||||
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||||
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||||
|
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
Reference in New Issue
Block a user