├── .gitattributes ├── .gitignore ├── README.md ├── killunresponsive └── killunresponsive.go /.gitattributes: -------------------------------------------------------------------------------- 1 | killunresponsive filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | unresponsive/ 2 | unresponsive-test/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kill Unresponsive 2 | 3 | Terminal command to terminate unresponsive processes. 4 | 5 | ## How it works 6 | 7 | This command generates a spin dump report using Apple's provided "spindump" command. The report generated contains samples of user and kernel stacks for every process in the system. This command parses that report to obtain a list of unresponsive processes and then terminates them. 8 | 9 | ## How to compile 10 | 11 | Simply run the `go build` command from the terminal while browsing this directory. 12 | 13 | ## How to install 14 | 15 | I copied the compiled app into my /usr/local/sbin directory but you're welcome to install it anywhere you like. -------------------------------------------------------------------------------- /killunresponsive: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:daab64aeca93374534a0ec8f9362a390a1c732575af0c4f973b7b329919450e3 3 | size 2723528 4 | -------------------------------------------------------------------------------- /killunresponsive.go: -------------------------------------------------------------------------------- 1 | /******************************************* 2 | * Kill Unresponsive Processes v1.0 3 | * Written By: Christopher Davison 4 | * Updated: 2015-02-10 5 | ******************************************* 6 | * Flow 7 | * 1. Generates a spin dump 8 | * 2. Parses said dump, extracting unresponsive processes (name and pid) 9 | * 3. Kills unresponsive processes with extreme prejudice ;) 10 | */ 11 | 12 | // A package clause starts every source file. 13 | // Main is a special name declaring an executable rather than a library. 14 | package main 15 | 16 | // Import declarations 17 | import ( 18 | "bufio" // Buffered file reading 19 | "bytes" // Bytes of bits 20 | "log" // For logging 21 | "os" // Platform-independent interface 22 | "os/exec" // For executing shell commands 23 | "os/user" // For identifying the user 24 | "regexp" // Regular expressions 25 | "strconv" // String conversion 26 | s "strings" // String manipulation 27 | ) 28 | 29 | // Process struct 30 | type Process struct { 31 | Name string 32 | PID int 33 | } 34 | 35 | // Error checker 36 | func check(e error) { 37 | if e != nil { 38 | panic(e) 39 | } 40 | } 41 | 42 | // Parses a spindump file and returns unresponsive processes 43 | func parse(path string) (processes []Process, e error) { 44 | file, err := os.Open(path) 45 | if err != nil { 46 | return nil, err 47 | } 48 | defer file.Close() 49 | 50 | var p Process 51 | var insideProcess bool 52 | 53 | scanner := bufio.NewScanner(file) 54 | re := regexp.MustCompile(`^Process:\s{9}(.+?)\s\[([0-9]+?)\]`) 55 | 56 | for scanner.Scan() { 57 | if s.Contains(scanner.Text(), "Process: ") { 58 | insideProcess = true // Set a flag to determine when we've found a process 59 | matches := re.FindStringSubmatch(scanner.Text()) // Search for the process name and PID 60 | if len(matches) == 3 { 61 | pname := matches[1] // Process name is the first capture group 62 | pid, e := strconv.ParseInt(matches[2], 10, 0) // PID is the second capture group 63 | check(e) 64 | p = Process{pname, int(pid)} // Set p to a new Process type 65 | } 66 | } else if insideProcess && s.Contains(scanner.Text(), "Unresponsive") { 67 | processes = append(processes, p) // Append p to the result 68 | } else if insideProcess && scanner.Text() == "" { 69 | insideProcess = false // Clear flag when an empty line is detected 70 | } 71 | } 72 | return processes, scanner.Err() 73 | } 74 | 75 | func kill(processes []Process) { 76 | l := len(processes) 77 | for i := 0; i < l; i++ { 78 | log.Printf("Attempting to kill %v [%v]\n", processes[i].Name, processes[i].PID) 79 | p, _ := os.FindProcess(processes[i].PID) 80 | err := p.Kill() 81 | if err != nil { 82 | log.Println("Process couldn't be killed: ", err) 83 | } 84 | } 85 | return 86 | } 87 | 88 | func initiateSpin(path string) { 89 | cmd := exec.Command("spindump", "-notarget", "1", "-noBulkSymbolication", "-file", path) 90 | var out bytes.Buffer 91 | cmd.Stdout = &out 92 | err := cmd.Run() 93 | if err != nil { 94 | log.Fatal(err) 95 | } 96 | } 97 | 98 | // A function definition. Main is special. It is the entry point for the 99 | // executable program. Love it or hate it, Go uses brace brackets. 100 | func main() { 101 | log.SetFlags(0) // Disable date prefix for log 102 | usr, _ := user.Current() // Get the current user 103 | if usr.Uid == "0" { // Make sure it's the superuser 104 | spindump := "/tmp/spindump.txt" // Location to save the spindump 105 | initiateSpin(spindump) // Generate the spindump 106 | processes, err := parse(spindump) // Process the spindump 107 | os.Remove(spindump) // Remove the spindump file 108 | if err == nil { // If there were no errors 109 | kill(processes) // Kill the unresponsive processes 110 | } else { 111 | log.Println("Encountered an error while parsing the spindump file.") 112 | } 113 | } else { 114 | log.Fatal("This app requires superior authority to terminate unresponsive processes.\nTry again with sudo... ;)") 115 | } 116 | } 117 | --------------------------------------------------------------------------------