Last active
January 11, 2025 09:20
-
-
Save SCP002/ab863ef9ffbacedc2c0b1b4d30e80805 to your computer and use it in GitHub Desktop.
Golang: Close main window of the process with the specified PID for graceful termination of processes on Windows.
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
//go:build windows | |
// Used in https://github.com/SCP002/terminator. | |
package main | |
import ( | |
"fmt" | |
"unsafe" | |
"github.com/gonutz/w32/v2" | |
"github.com/samber/lo" | |
"golang.org/x/sys/windows" | |
) | |
// Dll files. | |
var ( | |
kernel32 = windows.NewLazyDLL("kernel32.dll") | |
) | |
// CloseWindow sends WM_CLOSE message to the main window of the process with PID `pid` or WM_QUIT message | |
// to UWP application process. | |
// | |
// If `allowOwnConsole` is set to true, allow to close own console window of the process. | |
// | |
// If `wait` is set to true, wait for the window procedure to process the message. It will stop execution until user, | |
// for example, answer a confirmation dialogue box. | |
// | |
// Return value (error) is nil only if application successfully processes this message, but not necessarily means that | |
// the window was actually closed. | |
func CloseWindow(pid int, allowOwnConsole bool, wait bool) error { | |
wnd, isUWP, err := GetMainWindow(pid, allowOwnConsole) | |
if err != nil { | |
return err | |
} | |
var ok bool | |
message := lo.Ternary(isUWP, w32.WM_QUIT, w32.WM_CLOSE) | |
if wait { | |
ok = w32.SendMessage(wnd, uint32(message), 0, 0) == 0 | |
} else { | |
ok = w32.PostMessage(wnd, uint32(message), 0, 0) | |
} | |
if !ok { | |
return fmt.Errorf("Failed to close the window with PID %v", pid) | |
} | |
return nil | |
} | |
// GetMainWindow returns main window handle of the process with PID `pid` and true if window | |
// belongs to UWP application. | |
// | |
// If `allowOwnConsole` is set to true, allow to return own console window of the process. | |
// | |
// Inspired by https://stackoverflow.com/a/21767578. | |
func GetMainWindow(pid int, allowOwnConsole bool) (w32.HWND, bool, error) { | |
var wnd w32.HWND | |
var isUWP bool | |
w32.EnumWindows(func(currWnd w32.HWND) bool { | |
_, currPid := w32.GetWindowThreadProcessId(currWnd) | |
if int(currPid) == pid { | |
if IsUWPApp(currWnd) { | |
isUWP = true | |
wnd = currWnd | |
// Stop enumerating. | |
return false | |
} | |
if IsMainWindow(currWnd) { | |
wnd = currWnd | |
// Stop enumerating. | |
return false | |
} | |
} | |
// Continue enumerating. | |
return true | |
}) | |
if wnd != 0 { | |
return wnd, isUWP, nil | |
} else { | |
if allowOwnConsole { | |
if attached, _ := IsAttachedToCaller(pid); attached { | |
return w32.GetConsoleWindow(), isUWP, nil | |
} | |
} | |
return wnd, isUWP, fmt.Errorf("No main window found for PID %v", pid) | |
} | |
} | |
// IsUWPApp returns true if a window with handle `wnd` is a window of Universal Windows Platform application. | |
func IsUWPApp(wnd w32.HWND) bool { | |
info, _ := w32.GetWindowInfo(wnd) | |
return info.AtomWindowType == 49223 | |
} | |
// IsMainWindow returns true if a window with handle `wnd` is a main window. | |
func IsMainWindow(wnd w32.HWND) bool { | |
return w32.GetWindow(wnd, w32.GW_OWNER) == 0 && w32.IsWindowVisible(wnd) | |
} | |
// IsAttachedToCaller returns true if process with PID `pid` is attached to current console. | |
func IsAttachedToCaller(pid int) (bool, error) { | |
pids, err := GetConsolePids(1) | |
if err != nil { | |
return false, err | |
} | |
for _, currentPid := range pids { | |
if currentPid == uint32(pid) { | |
return true, nil | |
} | |
} | |
return false, nil | |
} | |
// GetConsolePids returns a slice of PID's attached to the current console. | |
// | |
// `pidsLen` is the maximum number of PID's that can be stored in buffer. | |
// Must be > 0. Can be increased automatically (safe to pass 1). | |
// | |
// See https://docs.microsoft.com/en-us/windows/console/getconsoleprocesslist. | |
func GetConsolePids(pidsLen int) ([]uint32, error) { | |
getConsoleProcessList := kernel32.NewProc("GetConsoleProcessList") | |
pids := make([]uint32, pidsLen) | |
r1, _, err := getConsoleProcessList.Call( | |
// Actually passing the whole slice. Must be [0] due the way syscall works. | |
uintptr(unsafe.Pointer(&pids[0])), | |
uintptr(pidsLen), | |
) | |
if r1 == 0 { | |
return pids, err | |
} | |
if r1 <= uintptr(pidsLen) { | |
// Success, return the slice. | |
return pids, nil | |
} else { | |
// The initial buffer was too small. Call self again with the exact capacity. | |
return GetConsolePids(int(r1)) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment