Reputation: 239
I am trying to write a go app that will monitor the status of a server application I run in windows. The application will run for roughly 16 hours before throwing out the following error: (Small snippet)
fatal error: too many callback functions
goroutine 137 [running]:
runtime.throw(0xc4c0a1, 0x1b)
H:/Program Files/Go/src/runtime/panic.go:1117 +0x79 fp=0xc000639d30 sp=0xc000639d00 pc=0x899379
syscall.compileCallback(0xbd18a0, 0xc00041fce0, 0x1, 0x0)
H:/Program Files/Go/src/runtime/syscall_windows.go:201 +0x5e5 fp=0xc000639e28 sp=0xc000639d30 pc=0x8c90e5
syscall.NewCallback(...)
H:/Program Files/Go/src/syscall/syscall_windows.go:177
main.FindWindow(0xc47278, 0x13, 0xc000639f50, 0x2, 0x2)
I have two files. One is the file that is calling a bunch of Windows API stuff, and one is a goroutine that is being performed every 30 seconds to get an update.
I am fairly new to go, especially in windows related development, so I am struggling to find the issue and how to prevent it.
Here is the main file (test example).
func main() {
go updateServerStats()
select {}
}
func ServerStats() {
serverStatsTicker := time.NewTicker(30 * time.Second)
for range serverStatsTicker.C {
serverRunning, serverHung, err := ServerHangCheck()
if err != nil {
ErrorLogger.Println("Server Check Error: ", err)
}
if serverHung {
fmt.Println("Server is hung")
}
}
}
Here is the primary callback/windows file. Its very much still a very rough, very work in progress. Something I found and modified from go playground.
package main
import (
"fmt"
"syscall"
"unsafe"
)
var (
user32 = syscall.MustLoadDLL("user32.dll")
procEnumWindows = user32.MustFindProc("EnumWindows")
procGetWindowTextW = user32.MustFindProc("GetWindowTextW")
procIsHungAppWindow = user32.MustFindProc("IsHungAppWindow")
)
//EnumWindows iterates over each window to be used for callbacks
func EnumWindows(enumFunc uintptr, lparam uintptr) (err error) {
r1, _, e1 := syscall.Syscall(procEnumWindows.Addr(), 2, uintptr(enumFunc), uintptr(lparam), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
//GetWindowText gets the description of the Window, which is the "Text" of the window.
func GetWindowText(hwnd syscall.Handle, str *uint16, maxCount int32) (len int32, err error) {
r0, _, e1 := syscall.Syscall(procGetWindowTextW.Addr(), 3, uintptr(hwnd), uintptr(unsafe.Pointer(str)), uintptr(maxCount))
len = int32(r0)
if len == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
//IsHungAppWindow uses the IsHungAppWindow to see if Windows has been getting responses.
func IsHungAppWindow(hwnd syscall.Handle) (ishung bool, err error) {
r2, _, err := syscall.Syscall(procIsHungAppWindow.Addr(), 2, uintptr(hwnd), 0, 0)
if r2 == 1{
return true, err
}
return false, err
}
//FindWindow uses EnumWindows with a callback to GetWindowText, and if matches given Title, checks if its hung, and returns state.
func FindWindow(title string) (bool ,bool, error) {
var hwnd syscall.Handle
var isHung bool = false
cb := syscall.NewCallback(func(h syscall.Handle, p uintptr) uintptr {
b := make([]uint16, 200)
_, err := GetWindowText(h, &b[0], int32(len(b)))
if err != nil {
// ignore the error
return 1 // continue enumeration
}
if syscall.UTF16ToString(b) == title {
// note the window
isHung, _ = IsHungAppWindow(h)
hwnd = h
return 0 // stop enumeration
}
return 1 // continue enumeration
})
EnumWindows(cb, 0)
if hwnd == 0 {
return false, false, fmt.Errorf("DCS Not Found")
}
if isHung == true {
return true, isHung, fmt.Errorf("DCS Is Running But Hung")
}
return true, isHung, nil
}
//ServerHangCheck checks the server to see if the window is hung or process is running.
func ServerHangCheck() (bool, bool, error) {
const title = "server_application"
running, hung, err := FindWindow(title)
return running, hung, err
}
Upvotes: 0
Views: 1109
Reputation: 239
Per the advice of torek I got a work around.
I created a global variable
var callbacker uintptr
Then then I created a function which is called by init
func init() {
callbacker = syscall.NewCallback(CallBackCreator)
}
func CallBackCreator(h syscall.Handle, p uintptr) uintptr {
hwnd = 0
b := make([]uint16, 200)
_, err := GetWindowText(h, &b[0], int32(len(b)))
if err != nil {
// ignore the error
return 1 // continue enumeration
}
if syscall.UTF16ToString(b) == title {
// note the window
isHung, _ = IsHungAppWindow(h)
hwnd = h
return 0 // stop enumeration
}
return 1 // continue enumeration
}
which is now called by the function
EnumWindows(callbacker, 0)
Likely not the most "Go" appropriate way, but I am now making better progress.
Upvotes: 0
Reputation: 490068
Looking at syscall.NewCallback
, we find that it's actually implemented via runtime/syscall_windows.go
as the function compileCallback
:
func NewCallback(fn interface{}) uintptr {
return compileCallback(fn, true)
}
Looking at the runtime/syscall_windows.go code we find that it has a fixed size table of all registered Go callbacks. This code varies a lot between Go releases so it's not too productive to delve any further here. However, there's one thing that is clear: the code checks to see if the callback function is already registered, and if so, re-uses it. So a single callback function occupies one table slot, but adding multiple functions will eventually use up all the table slots and result in the fatal error
that you encountered.
You asked in a comment (the comments popped up while I was writing this):
Would I need to reuse it, or can I "close" the original? – Mallachar 7 mins ago
You cannot close out the original. There's an actual table of functions elsewhere in memory; a registered callback uses up a slot in this table, and slots are never released.
Upvotes: 1