3

I am using a python driver that's written in go, the driver connects to a certain service and do some processing. We found an issue with an "unheanlthy" instance of the service and that was making the driver to get stuck and was impossible to terminate unless we kill the process.

Doing some experimentation I saw that when an extension written in go is executed the program ignores the Ctrl+C commands issued until control comes back to Python, not happening this with a C written extension, in which the KeyboardInterrupt is raised while executing C code. My question is why this happens and if there's a way to circunvent this problem or issue a kind of timeout. Tried to raise an exception to emulate a timeout using the threading.Timer but the problem is that the exception is thrown in it's own thread and doesn't interrupt the main thread. Tested in Python (cpython) 3.9 and 3.10. As for the extensions I wrote as a poc the Go version is 1.20.4 and the C compiler is 9.4.0.

I'll leave a small poc below.

Python code:

import ctypes

import ctypes
go_library = ctypes.cdll.LoadLibrary('./go_library.so')
hello_Go = go_library.helloWorld

c_library = ctypes.cdll.LoadLibrary("./c_library.so")
hello_c = c_library.helloWorld

try:
    print("Calling golang code")
    hello_Go()

    print("Calling C code")
    hello_c()
except KeyboardInterrupt:
    print("Ctrl+C issued DD:")
finally:
    print("Done")

Go extension

package main

import (
   "C"
   "log"
   "time"
)

func helloWorld(){
   log.Println("Hello World")
   time.Sleep(10 * time.Second)
   log.Println("Done sleeping")
}

func main(){
}

C extension

#include <stdio.h>

int helloWorld() {
    printf("Hello from C\n");
    sleep(10);
    printf("Done sleeping from C\n");

    return 0;
}
puerkito66
  • 41
  • 2
  • If a Ctrl+C typed while the Go function is running does not immediately cause control to return to Python (to be reflected by a `KeyboardInterrupt`) then the Go function or something within must be either (i) blocking `SIGINT` or (ii) installing its own handler for `SIGINT`. My guess would be that it is `time.Sleep()` doing that, but I'm having trouble finding any documentation to support that. – John Bollinger May 18 '23 at 16:11
  • @JohnBollinger I was thinking the same, maybe go is doing some signal handling by itself. As for the `time.Sleep` I don't think that's related, just wrote that as a placeholder to simulate a task that would have some delay. In the real extension I'm using is reading from a socket and that's when it stucks. – puerkito66 May 18 '23 at 17:07
  • If indeed the Go runtime or something within the function is changing the disposition of `SIGINT`, then you may be out of luck. Aside from the fact that any such manipulation presumably has a purpose that it could be risky to thwart, it is very likely that either you have no way to access the original disposition of the signal (needed to restore that), or else that you have no opportunity to modify the signal disposition in the required scope. – John Bollinger May 18 '23 at 17:15
  • Which OS? I don't think Windows does SIGINT on its own. – Mark Ransom Jun 20 '23 at 03:49

1 Answers1

0

I met same case. After some research, I found the solution.

According to this answer:

Python has a signal handler installed on SIGINT which simply sets a flag that is checked by the main interpreter loop. For this handler to work properly, the Python interpreter has to be running Python code.

It seems that when running golang code, the golang call the python installed SIGINT signal handler to set a flag. So when running back to python, a KeyboardInterrupt is deteced.

And from golang doc:

(When Non-Go programs that call Go code) If Notify is called for an asynchronous signal, a Go signal handler will be installed for that signal. If, later, Reset is called for that signal, the original handling for that signal will be reinstalled, restoring the non-Go signal handler if any.

So, the following code will catch SIGINT in go code and return the function immediately when SIGINT.

func helloWorld() {
    c := make(chan os.Signal)
    signal.Notify(c, syscall.SIGINT)

    // the original handling for that signal will be reinstalled, restoring the non-Go signal handler if any.
    defer signal.Reset(syscall.SIGINT)

    go func() {
        log.Println("Hello World")
        time.Sleep(10 * time.Second)
        log.Println("Done sleeping")
        c <- nil

    }()

    // wait go routine to finish or signal received
    <-c
}
WangWeimin
  • 116
  • 5