Mallachar
Mallachar

Reputation: 239

GoLang: fatal error: too many callback functions

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

Answers (2)

Mallachar
Mallachar

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

torek
torek

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

Related Questions