Skip to content

Commit

Permalink
Fixes #11
Browse files Browse the repository at this point in the history
* Added option to hide background processes from the list
  ("hide_background"), disabled by default.
* Added "(foreground)" or "(background)" to the label of the items
  indicating if it's a foreground or background process (only visible if
  hide_background is disabled)

Processes are considered background if they have no visible window. The
code to get the list of visible windows with their associated processes
is directly taken from the TaskSwitcher Package from the main
application. (https://github.com/Keypirinha/Packages/blob/9e1a0645b16577a8cefd64510cbc15690ae8ceeb/TaskSwitcher/lib/alttab.py)
  • Loading branch information
ueffel committed Nov 19, 2017
1 parent acb572b commit ad7707e
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 13 deletions.
7 changes: 6 additions & 1 deletion kill.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[main]
# Plugin's main configuration section.

# What should happen when you don't explicitly choose an action to execute
# What should happen if you don't explicitly choose an action to execute
#
# Possible values are:
# * kill_by_name - Kills all processes that have the name of the
Expand All @@ -15,3 +15,8 @@
# * kill_by_id - Kills only the selected process (one single process)
# * kill_by_id_admin - Same as above but requests elevated rights
#default_action = kill_by_id

# If set to "yes", filters out every process that doesn't have visible window
#
# Default: no
# hide_background = no
66 changes: 54 additions & 12 deletions kill.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import subprocess
import ctypes as ct
import time
from .lib.alttab import AltTab

try:
import comtypes.client as com_cl
Expand Down Expand Up @@ -37,9 +38,11 @@ def __init__(self):
"""
super().__init__()
self._processes = []
self._processes_with_window = []
self._actions = []
self._icons = {}
self._default_action = "kill_by_id"
self._hide_background = False
# self._debug = True

def on_events(self, flags):
Expand Down Expand Up @@ -69,6 +72,8 @@ def _read_config(self):
possible_actions
)

self._hide_background = settings.get_bool("hide_background", "main", False)

def on_start(self):
"""
Creates the actions for killing the processes and register them
Expand Down Expand Up @@ -172,10 +177,22 @@ def _get_processes(self):
elapsed = time.time() - start_time

self.info("Found {} running processes in {:0.1f} seconds".format(len(self._processes), elapsed))
self.dbg("%d icons loaded" % len(self._icons))
# self.dbg(self._icons)
# for prc in self._processes:
# self.dbg(prc.raw_args())
self.dbg("{} icons loaded".format(len(self._icons)))

def _get_windows(self):
try:
handles = AltTab.list_alttab_windows()
except OSError:
self.err("Failed to list windows.", str(exc))

self._processes_with_window = []

for hwnd in handles:
try:
(_, proc_id) = AltTab.get_window_thread_process_id(hwnd)
self._processes_with_window.append(proc_id)
except OSError:
continue

def _get_processes_from_com_object(self, wmi):
"""
Expand All @@ -185,23 +202,27 @@ def _get_processes_from_com_object(self, wmi):
result_wmi = wmi.ExecQuery("SELECT ProcessId, Caption, Name, ExecutablePath, CommandLine "
+ "FROM Win32_Process")
for proc in result_wmi:
is_foreground = proc.Properties_["ProcessId"].Value in self._processes_with_window
if self._hide_background and not is_foreground:
continue

short_desc = ""
category = kp.ItemCategory.KEYWORD
databag = {}
if proc.Properties_["CommandLine"].Value:
short_desc = "(pid: {:5}) {}".format(
short_desc = "(pid: {:>5}) {}".format(
proc.Properties_["ProcessId"].Value,
proc.Properties_["CommandLine"].Value
)
category = RESTARTABLE
databag["CommandLine"] = proc.Properties_["CommandLine"].Value
elif proc.Properties_["ExecutablePath"].Value:
short_desc = "(pid: {:5}) {}".format(
short_desc = "(pid: {:>5}) {}".format(
proc.Properties_["ProcessId"].Value,
proc.Properties_["ExecutablePath"].Value
)
elif proc.Properties_["Name"].Value:
short_desc = "(pid: {:5}) {} ({})".format(
short_desc = "(pid: {:>5}) {} ({})".format(
proc.Properties_["ProcessId"].Value,
proc.Properties_["Name"].Value,
"Probably only killable as admin or not at all"
Expand All @@ -210,9 +231,16 @@ def _get_processes_from_com_object(self, wmi):
if proc.Properties_["ExecutablePath"].Value:
databag["ExecutablePath"] = proc.Properties_["ExecutablePath"].Value

label = proc.Properties_["Caption"].Value
if not self._hide_background:
if is_foreground:
label = label + " (foreground)"
else:
label = label + " (background)"

item = self.create_item(
category=category,
label=proc.Properties_["Caption"].Value,
label=label,
short_desc=short_desc,
target=proc.Properties_["Name"].Value + "|"
+ str(proc.Properties_["ProcessId"].Value),
Expand Down Expand Up @@ -260,33 +288,44 @@ def _get_processes_from_ext_call(self):
if line.strip() == "":
# build catalog item with gathered information from parsing
if info and "Caption" in info:
is_foreground = int(info["ProcessId"]) in self._processes_with_window
if self._hide_background and not is_foreground:
continue

short_desc = ""
category = kp.ItemCategory.KEYWORD
databag = {}
if "CommandLine" in info and info["CommandLine"] != "":
short_desc = "(pid: {:5}) {}".format(
short_desc = "(pid: {:>5}) {}".format(
info["ProcessId"],
info["CommandLine"]
)
category = RESTARTABLE
databag["CommandLine"] = info["CommandLine"]
elif "ExecutablePath" in info and info["ExecutablePath"] != "":
short_desc = "(pid: {:5}) {}".format(
short_desc = "(pid: {:>5}) {}".format(
info["ProcessId"],
info["ExecutablePath"]
)
elif "Name" in info:
short_desc = "(pid: {:5}) {}".format(
short_desc = "(pid: {:>5}) {}".format(
info["ProcessId"],
info["Name"]
)

if "ExecutablePath" in info and info["ExecutablePath"] != "":
databag["ExecutablePath"] = info["ExecutablePath"]

label = info["Caption"]
if not self._hide_background:
if is_foreground:
label = label + " (foreground)"
else:
label = label + " (background)"

item = self.create_item(
category=category,
label=info["Caption"],
label=label,
short_desc=short_desc,
target=info["Name"] + "|" + info["ProcessId"],
icon_handle=self._get_icon(info["ExecutablePath"]),
Expand Down Expand Up @@ -323,6 +362,9 @@ def on_suggest(self, user_input, items_chain):
if not items_chain:
return

if not self._processes_with_window:
self._get_windows()

if not self._processes:
self._get_processes()

Expand Down
173 changes: 173 additions & 0 deletions lib/alttab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Keypirinha: a fast launcher for Windows (keypirinha.com)

import ctypes

class AltTab:
"""
A class to ease the finding of Alt+Tab eligible windows and the interaction
between Python and the Win32 API via ctypes.
"""

@classmethod
def list_alttab_windows(cls):
"""
Return the list of the windows handles that are currently guessed to be
eligible to the Alt+Tab panel.
Raises a OSError exception on error.
"""
# LPARAM is defined as LONG_PTR (signed type)
if ctypes.sizeof(ctypes.c_long) == ctypes.sizeof(ctypes.c_void_p):
LPARAM = ctypes.c_long
elif ctypes.sizeof(ctypes.c_longlong) == ctypes.sizeof(ctypes.c_void_p):
LPARAM = ctypes.c_longlong
EnumWindowsProc = ctypes.WINFUNCTYPE(
ctypes.c_bool, ctypes.c_void_p, LPARAM)

def _enum_proc(hwnd, lparam):
try:
if cls.is_alttab_window(hwnd):
handles.append(hwnd)
except OSError:
pass
return True

handles = []
ctypes.windll.user32.EnumWindows(EnumWindowsProc(_enum_proc), 0)
return handles

@classmethod
def is_alttab_window(cls, hwnd):
"""
Guess if the given window handle is eligible to the Alt+Tab panel.
Raises a OSError exception on error.
"""
WS_EX_APPWINDOW = 0x00040000
WS_EX_NOACTIVATE = 0x08000000
WS_EX_TOOLWINDOW = 0x00000080
IsWindowVisible = ctypes.windll.user32.IsWindowVisible

# * Initial windows filtering based on Raymond Chen's blog post:
# "Which windows appear in the Alt+Tab list?"
# https://blogs.msdn.microsoft.com/oldnewthing/20071008-00/?p=24863/
# * Also see MSDN documentation ""The Taskbar" (especially the "Managing
# Taskbar Buttons"):
# https://msdn.microsoft.com/en-us/library/bb776822(VS.85).aspx
# * "Getting a list of windows like those displayed in the alt-tab list,
# taskbar buttons and task manager" post on MSDN Forum:
# https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/5b337500-32dc-442d-8f77-62cad15ef46a
if not IsWindowVisible(hwnd):
return False
if ctypes.windll.user32.GetWindowTextLengthW(hwnd) <= 0:
return False
exstyle = cls.get_window_long(hwnd, -16) # GWL_EXSTYLE
if (exstyle & WS_EX_APPWINDOW) == WS_EX_APPWINDOW:
return True
if (exstyle & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW:
return False
if (exstyle & WS_EX_NOACTIVATE) == WS_EX_NOACTIVATE:
return False
owner_hwnd = ctypes.windll.user32.GetWindow(hwnd, 4) # GW_OWNER
if owner_hwnd and IsWindowVisible(owner_hwnd):
return False

# skip root Excel window
# https://mail.python.org/pipermail/python-win32/2010-January/010012.html
if ctypes.windll.user32.GetPropW(hwnd, "ITaskList_Deleted"):
return False

# avoids double entries for store apps on windows 10
# trick from Switcheroo (http://www.switcheroo.io/)
class_name = cls.get_window_class_name(hwnd)
if class_name == "Windows.UI.Core.CoreWindow":
return False

# skip the "Program Manager" window ("explorer" process, tested on 8.1)
if class_name == "Progman":
return False

return True

@staticmethod
def switch_to_window(hwnd):
"""Wrapper over the SwitchToThisWindow() Win32 function"""
ctypes.windll.user32.SwitchToThisWindow(hwnd, True)

@staticmethod
def get_window_text(hwnd):
"""
Wrapper over the GetWindowTextW() Win32 function
Raises a OSError exception on error.
"""
length = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
buff = ctypes.create_unicode_buffer(length + 1)
ctypes.windll.kernel32.SetLastError(0)
res = ctypes.windll.user32.GetWindowTextW(hwnd, buff, length + 1)
if not res and ctypes.GetLastError() != 0:
raise ctypes.WinError()
return buff.value

@staticmethod
def get_window_long(hwnd, index):
"""
Wrapper over the GetWindowLongW() Win32 function
Raises a OSError exception on error.
"""
ctypes.windll.kernel32.SetLastError(0)
style = ctypes.windll.user32.GetWindowLongW(hwnd, index)
if ctypes.GetLastError() != 0:
raise ctypes.WinError()
return style

@staticmethod
def get_window_class_name(hwnd):
"""
Wrapper over the GetClassNameW() Win32 function
Raises a OSError exception on error.
"""
max_length = 256 # see WNDCLASS documentation
buff = ctypes.create_unicode_buffer(max_length + 1)
if not ctypes.windll.user32.GetClassNameW(hwnd, buff, max_length + 1):
raise ctypes.WinError()
return buff.value

@staticmethod
def get_window_thread_process_id(hwnd):
"""
Wrapper over the GetWindowThreadProcessId() win32 function.
Get the IDs of the parent thread and process of the given window handle.
Returns a tuple: (thread_id, proc_id)
Raises a OSError exception on error.
"""
proc_id = ctypes.c_ulong()
thread_id = ctypes.windll.user32.GetWindowThreadProcessId(
hwnd, ctypes.byref(proc_id))
if not thread_id or not proc_id.value:
raise ctypes.WinError()
return (thread_id, proc_id.value)

@staticmethod
def get_process_image_path(proc_id):
"""
Return the full path of the PE image of the given process ID.
Raises a OSError exception on error.
"""
# get process handle
# PROCESS_QUERY_INFORMATION = 0x400
hproc = ctypes.windll.kernel32.OpenProcess(0x400, False, proc_id)
if not hproc:
raise ctypes.WinError()

# get image path
# MAX_PATH is 260 but we're using the Unicode variant of the API
max_length = 1024
length = ctypes.c_ulong(max_length)
buff = ctypes.create_unicode_buffer(max_length)
ctypes.windll.kernel32.SetLastError(0)
res = ctypes.windll.kernel32.QueryFullProcessImageNameW(
hproc, 0, buff, ctypes.byref(length))
error = ctypes.GetLastError()
ctypes.windll.kernel32.CloseHandle(hproc)
ctypes.windll.kernel32.SetLastError(error)
if not res:
raise ctypes.WinError()
return buff.value

0 comments on commit ad7707e

Please sign in to comment.