Skip to content

Instantly share code, notes, and snippets.

@SCP002
Last active November 8, 2023 10:21
Show Gist options
  • Save SCP002/ab863ef9ffbacedc2c0b1b4d30e80805 to your computer and use it in GitHub Desktop.
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.
// 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