fix(gateway): verify pid identity before stop
This commit is contained in:
parent
d38d9fade1
commit
e8458e169c
4 changed files with 181 additions and 14 deletions
|
|
@ -12,6 +12,11 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/pid"
|
||||
)
|
||||
|
||||
type gatewayTarget struct {
|
||||
data *pid.PidFileData
|
||||
process *os.Process
|
||||
}
|
||||
|
||||
func newStatusCommand() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "status",
|
||||
|
|
@ -35,30 +40,36 @@ func newStopCommand() *cobra.Command {
|
|||
}
|
||||
|
||||
func gatewayStatusCmd(homePath string) error {
|
||||
data := pid.ReadPidFileWithCheck(homePath)
|
||||
if data == nil {
|
||||
target, err := resolveGatewayTarget(homePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target == nil {
|
||||
fmt.Println("Gateway status: stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf(
|
||||
"Gateway status: running (PID: %d, host: %s, port: %d)\n",
|
||||
data.PID,
|
||||
data.Host,
|
||||
data.Port,
|
||||
target.data.PID,
|
||||
target.data.Host,
|
||||
target.data.Port,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func gatewayStopCmd(homePath string) error {
|
||||
data := pid.ReadPidFileWithCheck(homePath)
|
||||
if data == nil {
|
||||
target, err := resolveGatewayTarget(homePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if target == nil {
|
||||
return fmt.Errorf("gateway is not running")
|
||||
}
|
||||
|
||||
process, err := os.FindProcess(data.PID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find gateway process (PID: %d): %w", data.PID, err)
|
||||
process := target.process
|
||||
if process == nil {
|
||||
return fmt.Errorf("failed to find gateway process (PID: %d)", target.data.PID)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
|
|
@ -67,9 +78,31 @@ func gatewayStopCmd(homePath string) error {
|
|||
err = process.Signal(syscall.SIGTERM)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to stop gateway (PID: %d): %w", data.PID, err)
|
||||
return fmt.Errorf("failed to stop gateway (PID: %d): %w", target.data.PID, err)
|
||||
}
|
||||
|
||||
fmt.Printf("Sent stop signal to gateway (PID: %d)\n", data.PID)
|
||||
fmt.Printf("Sent stop signal to gateway (PID: %d)\n", target.data.PID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveGatewayTarget(homePath string) (*gatewayTarget, error) {
|
||||
data := pid.ReadPidFileWithCheck(homePath)
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
process, err := os.FindProcess(data.PID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find gateway process (PID: %d): %w", data.PID, err)
|
||||
}
|
||||
|
||||
err = verifyGatewayProcessIdentity(data.PID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gatewayTarget{
|
||||
data: data,
|
||||
process: process,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ func TestGatewayStopCmdNotRunning(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "gateway is not running")
|
||||
}
|
||||
|
||||
func TestGatewayStopCmdRunningProcess(t *testing.T) {
|
||||
func TestGatewayStatusCmdRejectsNonGatewayPID(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("requires POSIX signal semantics")
|
||||
}
|
||||
|
|
@ -89,6 +89,51 @@ func TestGatewayStopCmdRunningProcess(t *testing.T) {
|
|||
|
||||
writeGatewayPidFile(t, homePath, sleepCmd.Process.Pid)
|
||||
|
||||
_, err := captureGatewayStdout(t, func() error {
|
||||
return gatewayStatusCmd(homePath)
|
||||
})
|
||||
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "non-gateway process")
|
||||
}
|
||||
|
||||
func TestGatewayStopCmdRejectsNonGatewayPID(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("requires POSIX signal semantics")
|
||||
}
|
||||
|
||||
homePath := t.TempDir()
|
||||
sleepCmd := exec.Command("sleep", "30")
|
||||
require.NoError(t, sleepCmd.Start())
|
||||
t.Cleanup(func() {
|
||||
if sleepCmd.Process != nil {
|
||||
_ = sleepCmd.Process.Kill()
|
||||
}
|
||||
_ = sleepCmd.Wait()
|
||||
})
|
||||
|
||||
writeGatewayPidFile(t, homePath, sleepCmd.Process.Pid)
|
||||
|
||||
err := gatewayStopCmd(homePath)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "non-gateway process")
|
||||
}
|
||||
|
||||
func TestGatewayStopCmdRunningProcess(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("requires POSIX signal semantics")
|
||||
}
|
||||
|
||||
homePath := t.TempDir()
|
||||
helperCmd := startGatewayHelperProcess(t)
|
||||
t.Cleanup(func() {
|
||||
if helperCmd.Process != nil {
|
||||
_ = helperCmd.Process.Kill()
|
||||
}
|
||||
})
|
||||
|
||||
writeGatewayPidFile(t, homePath, helperCmd.Process.Pid)
|
||||
|
||||
output, err := captureGatewayStdout(t, func() error {
|
||||
return gatewayStopCmd(homePath)
|
||||
})
|
||||
|
|
@ -98,7 +143,7 @@ func TestGatewayStopCmdRunningProcess(t *testing.T) {
|
|||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- sleepCmd.Wait()
|
||||
done <- helperCmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
|
|
@ -110,3 +155,39 @@ func TestGatewayStopCmdRunningProcess(t *testing.T) {
|
|||
t.Fatal("gateway process did not exit after stop signal")
|
||||
}
|
||||
}
|
||||
|
||||
func startGatewayHelperProcess(t *testing.T) *exec.Cmd {
|
||||
t.Helper()
|
||||
|
||||
exePath, err := os.Executable()
|
||||
require.NoError(t, err)
|
||||
|
||||
cmd := exec.Command(
|
||||
exePath,
|
||||
"-test.run=TestGatewayCommandHelperProcess",
|
||||
"--",
|
||||
"gateway",
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "GO_WANT_GATEWAY_HELPER_PROCESS=1")
|
||||
require.NoError(t, cmd.Start())
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestGatewayCommandHelperProcess(t *testing.T) {
|
||||
if os.Getenv("GO_WANT_GATEWAY_HELPER_PROCESS") != "1" {
|
||||
return
|
||||
}
|
||||
|
||||
for i, arg := range os.Args {
|
||||
if arg != "--" {
|
||||
continue
|
||||
}
|
||||
args := os.Args[i+1:]
|
||||
if len(args) > 0 && args[0] == "gateway" {
|
||||
select {}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
os.Exit(2)
|
||||
}
|
||||
|
|
|
|||
41
cmd/picoclaw/internal/gateway/process_identity_linux.go
Normal file
41
cmd/picoclaw/internal/gateway/process_identity_linux.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//go:build linux
|
||||
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func verifyGatewayProcessIdentity(processID int) error {
|
||||
targetExe, err := os.Readlink(filepath.Join("/proc", strconv.Itoa(processID), "exe"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect gateway process executable (PID: %d): %w", processID, err)
|
||||
}
|
||||
|
||||
currentExe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect current executable: %w", err)
|
||||
}
|
||||
|
||||
if filepath.Base(targetExe) != filepath.Base(currentExe) {
|
||||
return fmt.Errorf("pid file points to a non-gateway process (PID: %d)", processID)
|
||||
}
|
||||
|
||||
rawCmdline, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(processID), "cmdline"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to inspect gateway process command line (PID: %d): %w", processID, err)
|
||||
}
|
||||
|
||||
argv := bytes.Split(rawCmdline, []byte{0})
|
||||
for _, arg := range argv[1:] {
|
||||
if string(arg) == "gateway" || string(arg) == "g" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("pid file points to a non-gateway process (PID: %d)", processID)
|
||||
}
|
||||
12
cmd/picoclaw/internal/gateway/process_identity_other.go
Normal file
12
cmd/picoclaw/internal/gateway/process_identity_other.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
//go:build !linux
|
||||
|
||||
package gateway
|
||||
|
||||
import "fmt"
|
||||
|
||||
func verifyGatewayProcessIdentity(processID int) error {
|
||||
return fmt.Errorf(
|
||||
"gateway process identity verification is not supported on this platform (PID: %d)",
|
||||
processID,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue