mirror of
https://github.com/kemko/nomad.git
synced 2026-01-01 16:05:42 +03:00
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:
3
.changelog/26780.txt
Normal file
3
.changelog/26780.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:bug
|
||||||
|
windows: Fixed a bug where agents would not gracefully shut down on Ctrl-C
|
||||||
|
```
|
||||||
@@ -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 {
|
||||||
|
|||||||
17
api/internal/testutil/server_default.go
Normal file
17
api/internal/testutil/server_default.go
Normal 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
|
||||||
|
}
|
||||||
37
api/internal/testutil/server_windows.go
Normal file
37
api/internal/testutil/server_windows.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
17
testutil/server_default.go
Normal file
17
testutil/server_default.go
Normal 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
|
||||||
|
}
|
||||||
37
testutil/server_windows.go
Normal file
37
testutil/server_windows.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user