windows: use/accept platform-specific signal for stopping agent (#26780)

On Windows, the `os.Process.Signal` method returns an error when sending
`os.Interrupt` (SIGINT) because it isn't implemented. This causes test servers
in the `testutil` packages to break on Windows. Use the platform specific
syscalls to generate the SIGINT instead.

The agent's signal handler also did not correctly handle the Ctrl-C because we
were masking os.Interrupt instead of SIGINT.

Fixes: https://github.com/hashicorp/nomad/issues/26775

Co-authored-by: Chris Roberts <croberts@hashicorp.com>
This commit is contained in:
Tim Gross
2025-09-17 11:32:20 -04:00
committed by GitHub
parent fca783c566
commit 4e75e99f1a
8 changed files with 127 additions and 13 deletions

3
.changelog/26780.txt Normal file
View File

@@ -0,0 +1,3 @@
```release-note:bug
windows: Fixed a bug where agents would not gracefully shut down on Ctrl-C
```

View File

@@ -240,7 +240,9 @@ func NewTestServer(t testing.TB, cb ServerConfigCallback) *TestServer {
// Stop stops the test Nomad server, and removes the Nomad data // Stop stops the test Nomad server, and removes the Nomad data
// directory once we are done. // directory once we are done.
func (s *TestServer) Stop() { func (s *TestServer) Stop() {
defer func() { _ = os.RemoveAll(s.Config.DataDir) }() s.t.Cleanup(func() {
_ = os.RemoveAll(s.Config.DataDir)
})
// wait for the process to exit to be sure that the data dir can be // wait for the process to exit to be sure that the data dir can be
// deleted on all platforms. // deleted on all platforms.
@@ -251,7 +253,7 @@ func (s *TestServer) Stop() {
}() }()
// kill and wait gracefully // kill and wait gracefully
err := s.cmd.Process.Signal(os.Interrupt) err := s.gracefulStop()
must.NoError(s.t, err) must.NoError(s.t, err)
select { select {

View File

@@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !windows
package testutil
import (
"os"
)
// gracefulStop performs a platform-specific graceful stop. On non-Windows this
// uses the Go API for SIGINT
func (s *TestServer) gracefulStop() error {
err := s.cmd.Process.Signal(os.Interrupt)
return err
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build windows
package testutil
import (
"fmt"
"syscall"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procSetCtrlHandler = kernel32.NewProc("SetConsoleCtrlHandler")
procGenCtrlEvent = kernel32.NewProc("GenerateConsoleCtrlEvent")
)
// gracefulStop performs a platform-specific graceful stop. On Windows the Go
// API does not implement SIGINT even though it's supported on Windows via
// CTRL_C_EVENT
func (s *TestServer) gracefulStop() error {
// note: err is always non-nil from these proc Call methods because it's
// always populated from GetLastError and you need to check the result
// returned against the docs.
pid := s.cmd.Process.Pid
result, _, err := procSetCtrlHandler.Call(0, 1)
if result == 0 {
return fmt.Errorf("failed to modify handlers for ctrl-c on pid %d: %w", pid, err)
}
result, _, err = procGenCtrlEvent.Call(syscall.CTRL_C_EVENT, uintptr(pid))
if result == 0 {
return fmt.Errorf("failed to send ctrl-C event to pid %d: %w", pid, err)
}
return nil
}

View File

@@ -1081,7 +1081,7 @@ func (c *Command) handleSignals() int {
signalCh := make(chan os.Signal, 4) signalCh := make(chan os.Signal, 4)
defer signal.Stop(signalCh) defer signal.Stop(signalCh)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE) signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGPIPE)
// Signal readiness only once signal handlers are setup // Signal readiness only once signal handlers are setup
sdSock, err := openNotify() sdSock, err := openNotify()
@@ -1120,7 +1120,7 @@ func (c *Command) handleSignals() int {
} }
return c.terminateGracefully(signalCh, sdSock) return c.terminateGracefully(signalCh, sdSock)
case os.Interrupt: case syscall.SIGINT:
if !c.agent.GetConfig().LeaveOnInt { if !c.agent.GetConfig().LeaveOnInt {
return 1 return 1
} }

View File

@@ -29,6 +29,7 @@ import (
"github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/ci"
"github.com/hashicorp/nomad/helper/discover" "github.com/hashicorp/nomad/helper/discover"
"github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/helper/pointer"
"github.com/shoenig/test/must"
) )
// TestServerConfig is the main server configuration struct. // TestServerConfig is the main server configuration struct.
@@ -268,21 +269,21 @@ func NewTestServer(t testing.TB, cb ServerConfigCallback) *TestServer {
// Stop stops the test Nomad server, and removes the Nomad data // Stop stops the test Nomad server, and removes the Nomad data
// directory once we are done. // directory once we are done.
func (s *TestServer) Stop() { func (s *TestServer) Stop() {
defer os.RemoveAll(s.Config.DataDir) s.t.Cleanup(func() {
_ = os.RemoveAll(s.Config.DataDir)
})
// wait for the process to exit to be sure that the data dir can be // wait for the process to exit to be sure that the data dir can be
// deleted on all platforms. // deleted on all platforms.
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
defer close(done) defer close(done)
_ = s.cmd.Wait()
s.cmd.Wait()
}() }()
// kill and wait gracefully // kill and wait gracefully
if err := s.cmd.Process.Signal(os.Interrupt); err != nil { err := s.gracefulStop()
s.t.Errorf("err: %s", err) must.NoError(s.t, err)
}
select { select {
case <-done: case <-done:
@@ -291,9 +292,9 @@ func (s *TestServer) Stop() {
s.t.Logf("timed out waiting for process to gracefully terminate") s.t.Logf("timed out waiting for process to gracefully terminate")
} }
if err := s.cmd.Process.Kill(); err != nil { err = s.cmd.Process.Kill()
s.t.Errorf("err: %s", err) must.NoError(s.t, err, must.Sprint("failed to kill process"))
}
select { select {
case <-done: case <-done:
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):

View File

@@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build !windows
package testutil
import (
"os"
)
// gracefulStop performs a platform-specific graceful stop. On non-Windows this
// uses the Go API for SIGINT
func (s *TestServer) gracefulStop() error {
err := s.cmd.Process.Signal(os.Interrupt)
return err
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
//go:build windows
package testutil
import (
"fmt"
"syscall"
)
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procSetCtrlHandler = kernel32.NewProc("SetConsoleCtrlHandler")
procGenCtrlEvent = kernel32.NewProc("GenerateConsoleCtrlEvent")
)
// gracefulStop performs a platform-specific graceful stop. On Windows the Go
// API does not implement SIGINT even though it's supported on Windows via
// CTRL_C_EVENT
func (s *TestServer) gracefulStop() error {
// note: err is always non-nil from these proc Call methods because it's
// always populated from GetLastError and you need to check the result
// returned against the docs.
pid := s.cmd.Process.Pid
result, _, err := procSetCtrlHandler.Call(0, 1)
if result == 0 {
return fmt.Errorf("failed to modify handlers for ctrl-c on pid %d: %w", pid, err)
}
result, _, err = procGenCtrlEvent.Call(syscall.CTRL_C_EVENT, uintptr(pid))
if result == 0 {
return fmt.Errorf("failed to send ctrl-C event to pid %d: %w", pid, err)
}
return nil
}