Last active
June 11, 2026 18:50
-
-
Save jgentil/665fafb6dbab67d378723372373e25fc to your computer and use it in GitHub Desktop.
Safely Shutdown a Windows Console process by name
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ### Pass an executable name to this and it will terminate it | |
| ### sending a Control-C event to it. It must be running in | |
| ### a tradiditonal Windows console host window. | |
| ### For Windows 10+ | |
| ### Requires Python 3.9+ and no dependencies. | |
| import ctypes | |
| from ctypes import wintypes | |
| import sys | |
| import time | |
| if sys.version_info < (3, 9): | |
| print("This script requires Python 3.9 or higher.") | |
| print(f"You are running {sys.version.split()[0]}") | |
| sys.exit(1) | |
| kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) | |
| TH32CS_SNAPPROCESS = 0x00000002 | |
| CTRL_C_EVENT = 0 | |
| CTRL_BREAK_EVENT = 1 | |
| MAX_PATH = 260 | |
| class PROCESSENTRY32(ctypes.Structure): | |
| _fields_ = [ | |
| ("dwSize", wintypes.DWORD), | |
| ("cntUsage", wintypes.DWORD), | |
| ("th32ProcessID", wintypes.DWORD), | |
| ("th32DefaultHeapID", ctypes.c_void_p), | |
| ("th32ModuleID", wintypes.DWORD), | |
| ("cntThreads", wintypes.DWORD), | |
| ("th32ParentProcessID", wintypes.DWORD), | |
| ("pcPriClassBase", ctypes.c_long), | |
| ("dwFlags", wintypes.DWORD), | |
| ("szExeFile", ctypes.c_char * MAX_PATH), | |
| ] | |
| kernel32.CreateToolhelp32Snapshot.argtypes = [ | |
| wintypes.DWORD, | |
| wintypes.DWORD | |
| ] | |
| kernel32.CreateToolhelp32Snapshot.restype = wintypes.HANDLE | |
| kernel32.Process32First.argtypes = [ | |
| wintypes.HANDLE, | |
| ctypes.POINTER(PROCESSENTRY32) | |
| ] | |
| kernel32.Process32First.restype = wintypes.BOOL | |
| kernel32.Process32Next.argtypes = [ | |
| wintypes.HANDLE, | |
| ctypes.POINTER(PROCESSENTRY32) | |
| ] | |
| kernel32.Process32Next.restype = wintypes.BOOL | |
| kernel32.CloseHandle.argtypes = [wintypes.HANDLE] | |
| kernel32.CloseHandle.restype = wintypes.BOOL | |
| kernel32.AttachConsole.argtypes = [wintypes.DWORD] | |
| kernel32.AttachConsole.restype = wintypes.BOOL | |
| kernel32.FreeConsole.argtypes = [] | |
| kernel32.FreeConsole.restype = wintypes.BOOL | |
| kernel32.GenerateConsoleCtrlEvent.argtypes = [ | |
| wintypes.DWORD, | |
| wintypes.DWORD | |
| ] | |
| kernel32.GenerateConsoleCtrlEvent.restype = wintypes.BOOL | |
| kernel32.SetConsoleCtrlHandler.argtypes = [ | |
| ctypes.c_void_p, | |
| wintypes.BOOL | |
| ] | |
| kernel32.SetConsoleCtrlHandler.restype = wintypes.BOOL | |
| def get_matching_pids(exe_name): | |
| exe_name = exe_name.lower() | |
| snapshot = kernel32.CreateToolhelp32Snapshot( | |
| TH32CS_SNAPPROCESS, | |
| 0 | |
| ) | |
| if snapshot == wintypes.HANDLE(-1).value: | |
| raise ctypes.WinError(ctypes.get_last_error()) | |
| pids = [] | |
| try: | |
| entry = PROCESSENTRY32() | |
| entry.dwSize = ctypes.sizeof(PROCESSENTRY32) | |
| if kernel32.Process32First(snapshot, ctypes.byref(entry)): | |
| while True: | |
| name = entry.szExeFile.decode( | |
| errors="ignore" | |
| ).lower() | |
| if name == exe_name: | |
| pids.append(entry.th32ProcessID) | |
| if not kernel32.Process32Next( | |
| snapshot, | |
| ctypes.byref(entry) | |
| ): | |
| break | |
| finally: | |
| kernel32.CloseHandle(snapshot) | |
| return pids | |
| def send_ctrl_c(pid): | |
| kernel32.FreeConsole() | |
| if not kernel32.AttachConsole(pid): | |
| print( | |
| f"AttachConsole({pid}) failed " | |
| f"error={ctypes.get_last_error()}" | |
| ) | |
| return False | |
| try: | |
| kernel32.SetConsoleCtrlHandler(None, True) | |
| if not kernel32.GenerateConsoleCtrlEvent( | |
| CTRL_C_EVENT, | |
| 0 | |
| ): | |
| print( | |
| f"GenerateConsoleCtrlEvent failed " | |
| f"error={ctypes.get_last_error()}" | |
| ) | |
| return False | |
| time.sleep(1) | |
| return True | |
| finally: | |
| kernel32.FreeConsole() | |
| kernel32.SetConsoleCtrlHandler(None, False) | |
| def main(): | |
| if len(sys.argv) != 2: | |
| print( | |
| f"Usage: {sys.argv[0]} executable_name" | |
| ) | |
| return 1 | |
| exe_name = sys.argv[1] | |
| if not exe_name.lower().endswith(".exe"): | |
| exe_name += ".exe" | |
| pids = get_matching_pids(exe_name) | |
| if not pids: | |
| print(f"No processes found: {exe_name}") | |
| return 1 | |
| for pid in pids: | |
| print(f"Sending Ctrl-C to PID {pid}") | |
| send_ctrl_c(pid) | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment