John White
John White

Reputation: 977

Gracefully terminate a process on Windows

I'm writing a server application in Go, and using a wrapper to run it as a Windows service.

There's a need to gracefully shut down the server (to close resources and connections properly), and in UNIX it would be handled through the SIGTERM signal. No big deal.

Though on Windows things seem very different. I see on this guide that signals actually exist on windows (?), and the SIGTERM is defined, though other pages indicate they don't, or to use other mechanisms like WM_CLOSE.

What is the preferable way to tell a headless process to gracefully terminate? How should it be implemented in Go?

The server is designed to be multiplatform, so the most standard way of doing it is preferable.

Upvotes: 15

Views: 8483

Answers (5)

Michael Chourdakis
Michael Chourdakis

Reputation: 11158

The idea of using *nix constructs and functions to Windows is generally bad and prone to weird tricks that may or may not work.

The proper way to cleanup on shutdown a GUI application is the handling of WM_QUERYENDSESSION and WM_ENDSESSION. Also, you have flexibility on generic shut-down mechanism, check here.

The proper way to get notified if you are a service is the service handler of your console application (SERVICE_STOPPED). For more, see Writing a ServiceMain.

Upvotes: 1

John White
John White

Reputation: 977

So, after a while, I just wanted to share how I solved it. It's ugly, but the only real way I found. Windows golang programs do listen to a CTRL+C, which is not a signal or anything like it, but it does trigger a graceful shutdown in the go program using the same signal UNIX has.

Here's the relevant code:

// Create shutdown channel
exitChan := make(chan int)
interruptChan := make(chan os.Signal, 1)
signal.Notify(interruptChan, os.Interrupt)
select {
case exitCode := <-exitChan:
    os.Exit(exitCode)
case <-interruptChan:
    // This works in Windows if ctrl-c is invoked. It sucks though
    svr.Stop()
    os.Exit(-1)
}

To trigger the CTRL+C given the process, I use a small program called windows-kill.exe

// Windows is "special" and we need to use a library for killing processes gracefully
// Unfortunately setting up the C++ library with go is a hassle, so we resort to a CLI tool
ex, _ := os.Executable()
cmdPath := filepath.Join(filepath.Dir(ex), "windows-kill.exe")
cmd := exec.CommandContext(context.Background(), cmdPath, "-SIGINT", strconv.Itoa(l.proc.Pid))

err := cmd.Start()
if err != nil {
    panic(err)
}

Here's the repo of the library and tool: https://github.com/alirdn/windows-kill

Upvotes: 1

Signaling is implemented on Windows but Unix signals are unavailable. There is an example in the signal package of golang for Windows to send a Ctrl-Break. It is refactored for interactive use here.

Upvotes: 0

colm.anseo
colm.anseo

Reputation: 22037

The go way to initiate canceling a task/service, is to use the context.Context package.

So if you want a signal handler to trigger the closing of a task's context.Context:

func registerSigHandler() context.Context {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)

        rootCtx := context.Background()
        taskCtx, cancelFn := context.WithCancel(rootCtx)

        go func() {
                sig := <-sigCh
                log.Println("received signal:", sig)

                // let sub-task know to wrap up: cancel taskCtx
                cancelFn()
        }()

        return taskCtx
}

and then pass the returned taskCtx on to your worker task for it to listen on.

select {
    case <-taskCtx.Done():
        // context was canceled
    default: // poll - rather than block in this example
}

Playground source-code.

Output:

2019/03/10 19:18:51 Worker PID: 33186
2019/03/10 19:18:51 Will terminate on these signals: interrupt terminated
2019/03/10 19:18:51 2019-03-10 19:18:51.018962 -0400 EDT m=+0.001727305
2019/03/10 19:18:52 2019-03-10 19:18:52.022782 -0400 EDT m=+1.005517010
2019/03/10 19:18:53 2019-03-10 19:18:53.019925 -0400 EDT m=+2.002630457

$ kill -INT 33186

2019/03/10 19:18:53 received signal: interrupt
2019/03/10 19:18:53 task context terminated reason: context canceled
2019/03/10 19:18:53 wrapping up task...
2019/03/10 19:18:53 workerTask() exited cleanly
2019/03/10 19:18:53 main exited cleanly

EDIT:

I tested this on Windows 10 and the clean-up is triggered when a Ctrl-C is issued from the same console. Not sure how to send signals externally on Windows - which may be the OP's original issue. Using say killtask /F /PID 33186 would indeed kill the process without any signal handler being triggered.

Upvotes: 2

colm.anseo
colm.anseo

Reputation: 22037

If you are running some form of web-services, may be better to use the web-service itself to initiate a shutdown, as tracking PIDS for signals can get messy.

To stop a http web-service, simply add a route like this:

http.HandleFunc("/shutdown",
        func(w http.ResponseWriter, r *http.Request) {
            // stops server - may want to add admin credentials check here!
            srv.Shutdown(context.Background())
        })

Playground source-code example.

2019/03/10 16:58:15 Listening on: :8080

$ curl 'localhost:8080/shutdown'

2019/03/10 17:04:17 Listening on: :8080
2019/03/10 17:04:19 exited cleanly

Upvotes: 0

Related Questions