├── .gitignore ├── replace.sed ├── test ├── kmsn.expected ├── kmsp.expected └── Makefile ├── kmsp ├── LICENSE ├── kmsn ├── Makefile ├── kms_functions └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | test/testenv 2 | test/*.pid 3 | test/*.out 4 | obj 5 | -------------------------------------------------------------------------------- /replace.sed: -------------------------------------------------------------------------------- 1 | /source "$(dirname $0)\/kms_functions"/ { 2 | r kms_functions 3 | d 4 | } 5 | -------------------------------------------------------------------------------- /test/kmsn.expected: -------------------------------------------------------------------------------- 1 | No process matches given pattern notexisting 2 | Now sending SIGTERM to PID X... didn't terminate, sending SIGKILL... killed successfully 3 | Now sending SIGTERM to PID X... killed successfully 4 | Now sending SIGTERM to PID X... killed successfully 5 | Now sending SIGTERM to PID X... killed successfully 6 | -------------------------------------------------------------------------------- /test/kmsp.expected: -------------------------------------------------------------------------------- 1 | PID X not found among running processes, skipping 2 | Now sending SIGTERM to PID X... didn't terminate, sending SIGKILL... killed successfully 3 | Now sending SIGTERM to PID X... killed successfully 4 | Now sending SIGTERM to PID X... killed successfully 5 | Now sending SIGTERM to PID X... killed successfully 6 | -------------------------------------------------------------------------------- /kmsp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source "$(dirname $0)/kms_functions" 3 | 4 | [ -z "$1" ] && { 5 | echo -e "Kill processes softly by pid number" 6 | echo -e "usage: $(basename "${0}") first_pid_to_kill [second_pid_to_kill] <...>\n" 7 | exit 1 8 | } 9 | 10 | while (( "$#" )); do 11 | [[ ! "$1" =~ ^[0-9]+$ ]] && { echo "Invalid process pid ${1}, skipping"; shift; continue; } 12 | checkPsExists $1 || { echo "PID ${1} not found among running processes, skipping"; shift; continue; } 13 | killmesoftly $1 14 | shift 15 | done 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Alan Franzoni 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /kmsn: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | source "$(dirname $0)/kms_functions" 3 | 4 | [ -z "$1" ] && { 5 | echo -e "Kill processes softly by name matching pattern." 6 | echo -e "usage: $(basename "${0}") first_pgrep_pattern [second_pgrep_pattern] <...> \n" 7 | exit 1 8 | } 9 | 10 | while (( "$#" )); do 11 | PROCESS_PIDS=($(pgrep $1)) 12 | [ "${#PROCESS_PIDS[@]}" -gt 1 ] && { echo "More than one process matches $1, won't kill multiple processes"; shift; continue; } 13 | [ "${#PROCESS_PIDS[@]}" -eq 0 ] && { echo "No process matches given pattern $1"; shift; continue; } 14 | PID=${PROCESS_PIDS[0]} 15 | killmesoftly ${PID} 16 | shift 17 | done 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX = /usr/local 2 | BINDIR = $(PREFIX)/bin 3 | OBJDIR = obj 4 | INSTALLCMD = install -c 5 | 6 | .PHONY: all test install clean 7 | 8 | # Define source files and mapping to obj/ directory 9 | SRCFILES = kmsn kmsp 10 | OBJFILES = $(addprefix $(OBJDIR)/,$(SRCFILES)) 11 | 12 | # create obj/ directory and use sed to generate a "standalone" kms* script 13 | $(OBJDIR)/%: % kms_functions 14 | mkdir -p $(@D) 15 | sed -f replace.sed $< > $@ 16 | chmod +x $@ 17 | 18 | all: $(OBJFILES) 19 | 20 | clean: 21 | rm -rf $(OBJDIR) 22 | 23 | test: all 24 | cd test ; make distclean test 25 | 26 | install: all 27 | $(INSTALLCMD) -d $(BINDIR) 28 | $(INSTALLCMD) obj/* $(BINDIR) 29 | -------------------------------------------------------------------------------- /kms_functions: -------------------------------------------------------------------------------- 1 | function killmesoftly { 2 | PID=$1 3 | echo -n "Now sending SIGTERM to PID ${PID}... " 4 | kill ${PID} 5 | [ 0 -ne "$?" ] && { echo "Could not kill process. You are probably not allowed to perform the operation"; exit 3; } 6 | 7 | # wait 15 seconds by default between SIGTERM and SIGKILL and between SIGKILL and abandon the operation. 8 | COUNT=0 9 | SIGKILL_SENT=1 10 | while kill -0 "${PID}" &>/dev/null; do 11 | sleep 0.2 12 | COUNT=$((COUNT+1)) 13 | [ "${COUNT}" -gt 150 ] && SIGKILL_SENT && { echo "Could not stop. Probably stuck in kernel call. Abandoning the whole kill chain."; exit 2; } 14 | [ "${COUNT}" -gt 75 ] && { echo -n "didn't terminate, sending SIGKILL... "; kill -9 ${PID}; COUNT=0; SIGKILL_SENT=0; } 15 | done 16 | echo "killed successfully" 17 | } 18 | 19 | # return 0 if a PID exists, 1 in any other case (regardless of ps exit code) 20 | function checkPsExists { 21 | PID=$1 22 | ps -p ${PID} >/dev/null 23 | [ 0 -eq $? ] && return 0 24 | return 1 25 | } 26 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | # this requires GNU Make on OSX as well! 2 | .PHONY: test testkmsn testkmsp distclean clean testkmsn.standalone testkmsp.standalone 3 | 4 | SHELL := /bin/bash 5 | 6 | test: testkmsn testkmsn.standalone testkmsp testkmsp.standalone 7 | 8 | testkmsn: testenv 9 | @rm -f kmsn*.out 10 | @testenv/bin/hardtokill -c "import time; import signal; signal.signal(signal.SIGTERM, lambda x,y: time.sleep(120)); time.sleep(120)"& 11 | @testenv/bin/asdfirst -c "import time; time.sleep(120)"& 12 | @testenv/bin/asdsecond -c "import time; time.sleep(120)"& 13 | @testenv/bin/asdthird -c "import time; time.sleep(120)"& 14 | @sleep 1 15 | 16 | @echo -e "Will now try killing five processes. The first should fail because of nonmatching name, the second should die with SIGKILL, the other three should die instantly:\n" 17 | @../kmsn notexisting hardtokill asdfirst asdsecond asdthird | tee kmsn.out 18 | @sed --regexp-extended -e 's/[0-9]+/X/g' kmsn.out > kmsn.sedded.out 19 | @diff -u kmsn.sedded.out kmsn.expected 20 | @echo 21 | 22 | testkmsn.standalone: testenv 23 | @rm -f kmsn*.out 24 | @testenv/bin/hardtokill -c "import time; import signal; signal.signal(signal.SIGTERM, lambda x,y: time.sleep(120)); time.sleep(120)"& 25 | @testenv/bin/asdfirst -c "import time; time.sleep(120)"& 26 | @testenv/bin/asdsecond -c "import time; time.sleep(120)"& 27 | @testenv/bin/asdthird -c "import time; time.sleep(120)"& 28 | @sleep 1 29 | @echo -e "Will now try killing five processes. The first should fail because of nonmatching name, the second should die with SIGKILL, the other three should die instantly:\n" 30 | @../obj/kmsn notexisting hardtokill asdfirst asdsecond asdthird | tee kmsn.standalone.out 31 | @sed --regexp-extended -e 's/[0-9]+/X/g' kmsn.standalone.out > kmsn.standalone.sedded.out 32 | @diff -u kmsn.standalone.sedded.out kmsn.expected 33 | @echo 34 | 35 | testkmsp: testenv 36 | @rm -f *.pid kmsp*.out 37 | @{ testenv/bin/python -u -c "import time; import signal; signal.signal(signal.SIGTERM, lambda x,y: time.sleep(120)); time.sleep(120)" & echo $$! > hardtokill.pid; } 38 | @{ testenv/bin/python -u -c "import time; time.sleep(120)" & echo $$! > 1.pid; } 39 | @{ testenv/bin/python -u -c "import time; time.sleep(120)" & echo $$! > 2.pid; } 40 | @{ testenv/bin/python -u -c "import time; time.sleep(120)" & echo $$! > 3.pid; } 41 | @{ testenv/bin/python -u -c "pass" & echo $$! > exited.pid; } 42 | @sleep 2 43 | @echo -e "Will now try killing five processes. The first should fail because of nonexisting pid, the second should die with SIGKILL, the other three should die instantly:\n" 44 | @cat exited.pid hardtokill.pid 1.pid 2.pid 3.pid | xargs ../kmsp | tee kmsp.out 45 | @sed --regexp-extended -e 's/[0-9]+/X/g' kmsp.out > kmsp.sedded.out 46 | @diff -u kmsp.sedded.out kmsp.expected 47 | @echo 48 | 49 | testkmsp.standalone: testenv 50 | @rm -f *.pid kmsp*.out 51 | @{ testenv/bin/python -u -c "import time; import signal; signal.signal(signal.SIGTERM, lambda x,y: time.sleep(120)); time.sleep(120)" & echo $$! > hardtokill.pid; } 52 | @{ testenv/bin/python -u -c "import time; time.sleep(120)" & echo $$! > 1.pid; } 53 | @{ testenv/bin/python -u -c "import time; time.sleep(120)" & echo $$! > 2.pid; } 54 | @{ testenv/bin/python -u -c "import time; time.sleep(120)" & echo $$! > 3.pid; } 55 | @{ testenv/bin/python -u -c "pass" & echo $$! > exited.pid; } 56 | @sleep 2 57 | @echo -e "Will now try killing five processes. The first should fail because of nonexisting pid, the second should die with SIGKILL, the other three should die instantly:\n" 58 | @cat exited.pid hardtokill.pid 1.pid 2.pid 3.pid | xargs ../obj/kmsp | tee kmsp.standalone.out 59 | @sed --regexp-extended -e 's/[0-9]+/X/g' kmsp.standalone.out > kmsp.standalone.sedded.out 60 | @diff -u kmsp.standalone.sedded.out kmsp.expected 61 | @echo 62 | 63 | 64 | distclean: 65 | @rm -rf testenv *.pid *.out 66 | 67 | testenv: 68 | virtualenv testenv 69 | cp testenv/bin/python testenv/bin/hardtokill 70 | cp testenv/bin/python testenv/bin/asdfirst 71 | cp testenv/bin/python testenv/bin/asdsecond 72 | cp testenv/bin/python testenv/bin/asdthird 73 | 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kill Me Softly 2 | 3 | Kill processes The Right Way. 4 | 5 | ## Purpose 6 | 7 | An incredible number of times, I had to perform a simple task: killing a process, 8 | and making sure the process was dead before proceeding. 9 | 10 | Sometimes the process doesn't die by SIGTERM, and you need to send a SIGKILL. 11 | 12 | In some (rare) situations, the process doesn't die by SIGKILL either. 13 | 14 | Sometimes the process just takes some time to die. 15 | 16 | By the way, what I usually want is something like **hey, kill this process and don't go on until it's dead**. 17 | 18 | It's stupid, but it's everything *killmesoftly* does; it tries to send a SIGTERM to a process, if it doesn't die within 19 | 15 seconds it sends a SIGKILL, if it still doesn't die it reports it. 20 | 21 | AFAIK there's no other tool to do that, even though such logic is implemented in a lot of software and service scripts. 22 | 23 | ## Install 24 | 25 | ### Standalone executables 26 | 27 | **This is the recommended way to go** if you just want to use this tool, at least until I don't submit it to Homebrew and Linuxbrew. 28 | 29 | Go anywhere in your executable path and download the latest standalone version of [kmsp](https://www.franzoni.eu/releases/killmesoftly/master/kmsp) and [kmsn](https://www.franzoni.eu/releases/killmesoftly/master/kmsn). 30 | 31 | ``` 32 | wget https://www.franzoni.eu/releases/killmesoftly/master/kmsp https://www.franzoni.eu/releases/killmesoftly/master/kmsn 33 | chmod +x ./kmsp ./kmsn 34 | ``` 35 | 36 | ### From Github source 37 | 38 | #### Build and install 39 | 40 | Just clone this repository, or [download a packed zip](../../archive/master.zip), and use 41 | 42 | ``` 43 | make install 44 | ``` 45 | 46 | by default, this will install standalone executables in ```/usr/local/bin```. If you want to change your install destination: 47 | 48 | ``` 49 | make PREFIX=/tmp/asd install 50 | ``` 51 | 52 | will install in ```/tmp/asd/bin``` or 53 | 54 | ``` 55 | make BINDIR=/tmp/mydir install 56 | ``` 57 | 58 | will install in ```/tmp/mydir```. 59 | 60 | #### Make-less installation 61 | 62 | Otherwise, for a make-less install, you could just copy ```kmsp``` and ```kmsn``` in your path, set them executable, make sure ```kms_functions``` is in the same directory, and you're ready to go. 63 | 64 | #### Developer-wise installation 65 | 66 | If you want to develop changes to killmesoftly, just symlink ```kmsp```, ```kmsn``` and ```kms_functions``` to a directory in your path (e.g. ~/bin) 67 | 68 | 69 | ## Usage 70 | 71 | You can feed the two scripts with process names (or matching expression, that will be sent to [pgrep](http://linux.die.net/man/1/pgrep)) or PIDs, respectively. 72 | 73 | The approach is blocking by design: the script won't exit until the process is actually dead, and latter PIDs or names won't be killed until previous ones are dead. 74 | 75 | If there's an error on one parameter (e.g. you've specified a name which matches more than one process, or a non-existent PID) the scripts will go on with the other parameters; on the contrary, if there's an hard error (e.g. a process that doesn't exit on a SIGKILL) the whole kill chain will be stopped and an error will be returned. 76 | 77 | The default timeout is 15 seconds. It's currently hardcoded. 78 | 79 | ### kmsn 80 | 81 | ``` 82 | Kill processes softly by name matching pattern. 83 | Usage: 84 | kmsn first_pgrep_pattern [second_pgrep_pattern] <...> 85 | ``` 86 | 87 | ### kmsp 88 | ``` 89 | Kill processes softly by pid number 90 | Usage: 91 | kmsp first_pid_to_kill [second_pid_to_kill] <...> 92 | ``` 93 | 94 | ## Requirements 95 | 96 | Those should be available on almost any Linux system, even on minimal installs, 97 | and are installed by default in OSX. 98 | 99 | * Linux or OSX 100 | * Bash 101 | * *Kill* and *pgrep* executables (e.g. *procps* package on Ubuntu/Debian) 102 | 103 | ## License 104 | 105 | This is Apache-2.0 licensed, which is the most permissive license I could think of. Feel free to include and modify this code however you like. 106 | 107 | ## TODOs 108 | * \*BSD support 109 | * a ```--all``` switch for kmsp that allows killing all processes that match a certain parameter 110 | * consider whether we should return different exit codes in the event some parameters are wrong; currently we return a nonzero exit code only on zero-parameters passed or hard failures. 111 | * allow specifying a different signal rather than SIGTERM for soft kill, and/or additional signals to be sent before/after SIGTERM. 112 | * configurable timeouts 113 | * consider transforming such scripts in a set of functions that can be sourced, rather than used as executables 114 | * add options for killing process groups and/or check whether children have actually exited 115 | * add options for matching on full name, not just on process name 116 | * remove dependency on bash and let sh suffice. 117 | * add tool in Linuxbrew and Homebrew repositories for easy install. 118 | * make the testsuite runnable on OSX (currently prevented by extended sed regexp) 119 | 120 | ## Thanks 121 | 122 | The following people helped developing this software in some way or another, without 123 | a specific order. 124 | 125 | * Jean-Philippe Daigle 126 | * Laurent Cozic 127 | --------------------------------------------------------------------------------