Skip to content

Instantly share code, notes, and snippets.

@jgentil
Last active June 11, 2026 18:50
Show Gist options
  • Select an option

  • Save jgentil/665fafb6dbab67d378723372373e25fc to your computer and use it in GitHub Desktop.

Select an option

Save jgentil/665fafb6dbab67d378723372373e25fc to your computer and use it in GitHub Desktop.
Safely Shutdown a Windows Console process by name
### 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