Skip to content

Instantly share code, notes, and snippets.

@mtfelian
Last active March 5, 2018 14:03
Show Gist options
  • Save mtfelian/9e34f8d6fa2d6c8fd468260ba4d5b508 to your computer and use it in GitHub Desktop.
Save mtfelian/9e34f8d6fa2d6c8fd468260ba4d5b508 to your computer and use it in GitHub Desktop.
Windows graceful process shutdown (WM_CLOSE / Ctrl-Break)
package main
import (
"fmt"
"os/exec"
"syscall"
"time"
"unsafe"
)
//noinspection GoSnakeCaseUsage
const (
// WAIT_TIMEOUT means the time-out interval elapsed, and the object's state is nonsignaled.
WAIT_TIMEOUT = 0x00000102
// WAIT_FAILED means the function WaitForSingleObject() has failed
WAIT_FAILED = 0xFFFFFFFF
// WM_CLOSE is as a signal that a window or an application should terminate.
WM_CLOSE = 16
// SYNCHRONIZE is a right to use the object for synchronization. This enables a
// thread to wait until the object is in the signaled state.
SYNCHRONIZE = 0x00100000
// CTRL_BREAK_EVENT is a CTRL+BREAK signal
CTRL_BREAK_EVENT = 1
)
var (
user32DLL = syscall.NewLazyDLL("user32.dll")
kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
procEnumWindows = user32DLL.NewProc("EnumWindows")
procPostMessage = user32DLL.NewProc("PostMessageW")
procGetWindowThreadProcessId = user32DLL.NewProc("GetWindowThreadProcessId")
procWaitForSingleObject = kernel32DLL.NewProc("WaitForSingleObject")
procOpenProcess = kernel32DLL.NewProc("OpenProcess")
procCloseHandle = kernel32DLL.NewProc("CloseHandle")
procGenerateConsoleCtrlEvent = kernel32DLL.NewProc("GenerateConsoleCtrlEvent")
)
// GenerateConsoleCtrlEvent dwCtrlEvent for the process group with ID dwProcessGroupId
func GenerateConsoleCtrlEvent(dwCtrlEvent, dwProcessGroupId uintptr) error {
_, _, err := procGenerateConsoleCtrlEvent.Call(dwCtrlEvent, dwProcessGroupId)
if err != nil && err == syscall.Errno(0) {
err = nil
}
return err
}
// EnumWindows with callback func pointer lpEnumFunc and user-given param lParam
func EnumWindows(lpEnumFunc, lParam uintptr) (bool, error) {
r1, _, err := procEnumWindows.Call(lpEnumFunc, lParam)
if err != nil && err == syscall.Errno(0) {
err = nil
}
return r1 != 0, err
}
// PostMessage Msg to windows handle hWnd, wParam and lParam is not used
func PostMessage(hWnd, Msg, wParam, lParam uintptr) (bool, error) {
r1, _, err := procPostMessage.Call(hWnd, Msg, wParam, lParam)
if err != nil && err == syscall.Errno(0) {
err = nil
}
return r1 != 0, err
}
// GetWindowThreadProcessId for window with handle hWnd
// Returns: ID of the thread that created the window, PID (ID of the process that created the window)
func GetWindowThreadProcessId(hWnd uintptr) (uintptr, int, error) {
var lpdwProcessId int
r1, _, err := procGetWindowThreadProcessId.Call(hWnd, uintptr(unsafe.Pointer(&lpdwProcessId)))
if err != nil && err == syscall.Errno(0) {
err = nil
}
return r1, lpdwProcessId, err
}
// OpenProcess with given access rights and dwProcessId
func OpenProcess(dwDesiredAccess, bInheritHandle, dwProcessId uintptr) (uintptr, error) {
r1, _, err := procOpenProcess.Call(dwDesiredAccess, bInheritHandle, dwProcessId)
if err != nil && err == syscall.Errno(0) {
err = nil
}
return r1, err
}
// CloseHandle hObject
func CloseHandle(hObject uintptr) (bool, error) {
r1, _, err := procCloseHandle.Call(hObject)
if err != nil && err == syscall.Errno(0) {
err = nil
}
return r1 != 0, err
}
// WaitForSingleObject until the specified object with hHandle is in the signaled state or
// the time-out interval dwMilliseconds elapses
func WaitForSingleObject(hHandle, dwMilliseconds uintptr) (uintptr, error) {
r1, _, err := procWaitForSingleObject.Call(hHandle, dwMilliseconds)
if r1 == WAIT_FAILED {
if err != nil && err == syscall.Errno(0) {
err = nil
}
return r1, err
}
return r1, nil
}
func main() {
const (
windowTimeout = 15000
killTimeout uintptr = 3000
)
cmdLine := `winver`
cmd := exec.Command(cmdLine)
fmt.Println("cmd.Start()")
if err := cmd.Start(); err != nil {
fmt.Println("cmd.Start():", err)
return
}
fmt.Println("cmd.Process.Pid:", cmd.Process.Pid)
fmt.Println("waiting for window...")
var found bool
var windowHandle uintptr
for i := 0; i < windowTimeout/100 && !found; i++ {
time.Sleep(100 * time.Millisecond) // wait for a window to draw
EnumWindows(syscall.NewCallback(func(hWnd, lParam uintptr) uintptr {
// hWnd is a current iteration windows process handle
// lParam we passing here (into callback) from EnumWindows() call to match window's PID with it
_, pid, err := GetWindowThreadProcessId(hWnd)
if err != nil {
fmt.Println("[ERR] GetWindowThreadProcessId():", err)
}
if pid == int(lParam) {
fmt.Println("window initialized")
found, windowHandle = true, hWnd
return 0 // it is used to stop, so should not check the return value of EnumWindows()
}
return 1 // continue
}), uintptr(cmd.Process.Pid))
}
if !found {
fmt.Println("[ERR] failed to find window at start")
}
fmt.Println("OpenProcess()")
processHandle, err := OpenProcess(SYNCHRONIZE, 0, uintptr(cmd.Process.Pid))
if err != nil {
fmt.Println("[ERR] OpenProcess():", err)
}
fmt.Println("PostMessage()")
postOK, err := PostMessage(windowHandle, WM_CLOSE, 0, 0)
if err != nil || !postOK {
fmt.Println("[ERR] PostMessage():", err)
// try to send it the ctrl-break event; MSDN docs tells that we can't send ctrl-c to
// another process group
fmt.Println("GenerateConsoleCtrlEvent()")
if err := GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, uintptr(cmd.Process.Pid)); err != nil {
fmt.Println("[ERR] GenerateConsoleCtrlEvent():", err)
}
}
fmt.Println("WaitForSingleObject()")
waitResult, err := WaitForSingleObject(processHandle, killTimeout)
if err != nil {
fmt.Println("[ERR] WaitForSingleObject():", err)
}
closed, err := CloseHandle(processHandle)
if err != nil || !closed {
fmt.Println("[ERR] CloseHandle():", err)
}
if waitResult == WAIT_TIMEOUT {
fmt.Println("cmd.Process.Kill()")
if err := cmd.Process.Kill(); err != nil {
fmt.Println("[ERR] cmd.Process.Kill():", err)
}
}
fmt.Println("OK")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment