Last active
November 8, 2023 10:21
-
-
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 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
// Used in https://github.com/SCP002/terminator. | |
package main | |
import ( | |
"errors" | |
"fmt" | |
"unsafe" | |
"github.com/gonutz/w32/v2" | |
"github.com/samber/lo" | |
"golang.org/x/sys/windows" | |
) | |
// Dll files. | |
var ( | |
k32 = windows.NewLazyDLL("kernel32.dll") | |
) | |
// CloseWindow sends WM_CLOSE message to the main window of the process 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 := GetWindow(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 errors.New("Failed to close the window with PID " + fmt.Sprint(pid)) | |
} | |
return nil | |
} | |
// GetWindow returns main window handle of the process 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 GetWindow(pid int, allowOwnConsole bool) (w32.HWND, bool, error) { | |
var wnd w32.HWND | |
var isUWP bool | |
w32.EnumWindows(func(hwnd w32.HWND) bool { | |
_, currentPid := w32.GetWindowThreadProcessId(hwnd) | |
if int(currentPid) == pid { | |
if IsUWPApp(hwnd) { | |
isUWP = true | |
wnd = hwnd | |
// Stop enumerating. | |
return false | |
} | |
if IsMainWindow(hwnd) { | |
wnd = hwnd | |
// 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, errors.New("No window found for PID " + fmt.Sprint(pid)) | |
} | |
} | |
// IsUWPApp returns true if a window with the specified handle is a window of Universal Windows Platform application. | |
func IsUWPApp(hwnd w32.HWND) bool { | |
info, _ := w32.GetWindowInfo(hwnd) | |
return info.AtomWindowType == 49223 | |
} | |
// IsMainWindow returns true if a window with the specified handle is a main window. | |
func IsMainWindow(hwnd w32.HWND) bool { | |
return w32.GetWindow(hwnd, w32.GW_OWNER) == 0 && w32.IsWindowVisible(hwnd) | |
} | |
// IsAttachedToCaller returns true if the given PID is attached to the 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 parameter - 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) { | |
k32Proc := k32.NewProc("GetConsoleProcessList") | |
pids := make([]uint32, pidsLen) | |
r1, _, err := k32Proc.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