# Windows CLI locks killer in Python

Some coworkers and I sometimes want to delete folders, only to get the very informative: "Permission Denied" message. I had discovered Handle.exe and Process Explorer to help with this, but the method was still too manual and cumbersome.

Thus, I decided to write this Python CLI script to release locks on a given file. It runs Handle.exe under the hood and parses the output to prompt the user for confirmation to kill any given process (unless a quiet-like option flag is given).

We all work on Windows 7 using basically the same setup (IT takes care of setting up all the machines). The default Python is ActivateState's Python2.6.9. While some people use cmd.exe, a lot of guys use Bash or Cygwin, so I wanted to support all of these.

I am not sure if using logging solely for the purpose of adding a prefix to my output messages was a good idea, but it felt very redundant to add it to every print. Making a function that wraps print made the code look weird.

Also, I'm at a loss as to how to test this. I've added tests for abspath function (see repo), but I'm having trouble writing tests for the other functions.

"""Dammit let me delete this file!

Usage:
dammit [-y | -s] <name>
dammit (-h)

Options:
-y        Kill without permission.
-s        Enable hotkey
-h        Show this screen.

"""
from __future__ import print_function

from docopt import docopt
from functools import partial
import logging
import msvcrt
import os
import posixpath
import re
import subprocess
import sys
import time

log = logging.getLogger()
log.setLevel(logging.INFO)
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
PREFIX = 'dammit:'
formatter = logging.Formatter(PREFIX + ' %(message)s')
ch.setFormatter(formatter)

CYGPATH = ['C:\\GnuNT\\bin\\cygpath.exe']
HANDLE = ["handle64.exe"]
# handle.exe provides way to close specified handle, which would be preferable,
# but this requires administrator rights :(
ACTIVATE = ["activatePID.exe"]
EXPLORER = "explorer.exe"
CANT_EXECUTE = """Could not execute command: %s"""

def abspath(path):
"""Return the absolute path of given path.

Taken to be relative from current working directory if input is not an
absolute path.

Args:
path (str) : Pathname to absolutize.

Notes:
Should work for POSIX and Windows paths to accomodate
different shells.
"""
if posixpath.isabs(path):  # Starts with "/"
if path[1:9] != 'cygdrive':
path = posixpath.join('/cygdrive', path[1:])
proc = subprocess.Popen(CYGPATH + ['-w', path],
stdout=subprocess.PIPE)
return proc.stdout.read().rstrip()  # Remove trailing newline
return os.path.abspath(path)

def find_locks(name):
"""Find which processes currently have a particular file/directory open.

Args:
name (str) : Name of the file which is locked.

Returns:
List of (process, PID)-pairs which are accessing the file.
"""
try:
output = subprocess.check_output(HANDLE + [name])
except WindowsError:
log.info(CANT_EXECUTE % HANDLE)
sys.exit(1)
except subprocess.CalledProcessError:
return None
pattern = r"([^\s]+)(?:\s+)pid: ([0-9]{4})"
return frozenset(re.findall(pattern, output))

def kill(pid):
"""Kill process with given PID

Args:
pid (str) : PID to kill
"""
try:
with open(os.devnull, 'w') as nul:
subprocess.Popen(KILL + [str(pid)], stdout=nul, stderr=nul)
except WindowsError:
log.info(CANT_EXECUTE % KILL)
sys.exit(1)
return

def _activatePID(pid, enable_hotkeys):
"""Try to activate window associated with given process ID

Args:
pid (str) : PID of process to activate.
enable_hotkeys (bool) : If True, Autohotkey will send "y" and "n"
key presses to the prompt upon activating requested window.
"""
try:
subprocess.Popen(ACTIVATE + [pid, str(enable_hotkeys)])
except WindowsError:
log.info(CANT_EXECUTE % ACTIVATE)
return

def query(process, pid, show_callback):
"""Query user to kill or show process.

Args:
process (str) : Process name
pid (str) : Process PID
show_callback (func(x)) : Ran when user answers "show"

y : Kill process
n : Do not kill process
s : Try to show (activate) window linked to the process.

Answering "show" does not exit the prompt.

Returns:
bool: True if yes, False if no.
"""
question = " Kill process %s with PID %s [y/n/s]? " % (process, pid)
sys.stdout.write(PREFIX + question)
while True:
return True
return False
show_callback(pid)
else:
sys.exit(1)

def open_explorer():
"""Open explorer.exe process."""
# Just opening 'explorer.exe' merely launches a Windows Explorer window,
# which is not the desired effect here.
log.info("Re-opening %s" % EXPLORER)
windir = os.environ['windir']
subprocess.Popen(os.path.join(windir, EXPLORER))

def main():
arguments = docopt(__doc__, options_first=True)
name = arguments['<name>']
quiet = arguments['-y']
hotkeys = arguments['-s']

activatePID = partial(_activatePID, enable_hotkeys=hotkeys)
name_abspath = abspath(name)
log.debug("name_abspath=%s" % name_abspath)
locks = find_locks(name_abspath)
if not locks:
log.info("Nothing locking %s" % name_abspath)
sys.exit(0)

for process, pid in locks:
if not quiet:
do_kill = query(process, pid, show_callback=activatePID)
if do_kill is not True:
continue
kill(pid)
log.debug("Killed PID %s" % pid)
if process == EXPLORER:
time.sleep(0.1)
open_explorer()

if __name__ == '__main__':
main()

• I can only nitpick about return statements at the end of some functions that can be simply omitted, and a return None that can be simplified to simply return. – janos Jul 13 '15 at 20:06
• Fixed in latest commit! I switched the return None to a return [] because I find it conveys a clearer "I've found no locks for that name" than return or return None. Personal preference I guess. – selimb Jul 13 '15 at 21:59

# String formatting

Generally, if you're using Python 2.6 or higher, you should be using the function str.format to format strings, rather than %. For example, this line in your code:

question = " Kill process %s with PID %s [y/n/s]? " % (process, pid)


Could become this:

question = " Kill process {process} with PID {pid} [y/n/s]? ".format(
process=process,
pid=pid
)


Another example would be turning this:

log.debug("Killed PID %s" % pid)


Into this:

log.debug("Killed PID {pid}".format(pid=pid))


# Miscellaneous

On a small note, strings like this:

'C:\\GnuNT\\bin\\cygpath.exe'


Can be prefixed with r, like this so that you can do this, and get rid of the double-backslashes:

r'C:\GnuNT\bin\cygpath.exe'


Anything like this:

return None


Can simply be changed to this:

return


In the case of the kill function, the return at the end can be omitted completely.

Other than that, your code looks really nice and clean. Good job!

• Thanks! I'm actually never sure if it's good practice to use raw strings. – selimb Jul 26 '15 at 19:20
• @Kreger51 No problem! Usually, when I'm working with file paths, I find it easier to use raw strings. – Ethan Bierlein Jul 26 '15 at 19:21