feat(gateway): add stop subcommand to gracefully shutdown gateway

Add `picoclaw gateway stop` command that:
- Reads the PID file from picoclaw home directory
- Validates the gateway process is running
- Sends SIGTERM (Unix) or Kill (Windows) to stop it

Also update tests to reflect new subcommand.

Fixes #2373
This commit is contained in:
曾文锋0668000834 2026-04-07 14:49:02 +08:00
parent 374e5cccf2
commit 395a55d3d9
4 changed files with 66 additions and 1 deletions

View file

@ -2,12 +2,14 @@ package gateway
import ( import (
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/sipeed/picoclaw/cmd/picoclaw/internal" "github.com/sipeed/picoclaw/cmd/picoclaw/internal"
"github.com/sipeed/picoclaw/pkg/gateway" "github.com/sipeed/picoclaw/pkg/gateway"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/pid"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
) )
@ -48,5 +50,42 @@ func NewGatewayCommand() *cobra.Command {
"Continue starting even when no default model is configured", "Continue starting even when no default model is configured",
) )
// Add stop subcommand
cmd.AddCommand(newGatewayStopCommand())
return cmd return cmd
} }
func newGatewayStopCommand() *cobra.Command {
return &cobra.Command{
Use: "stop",
Short: "Stop the running picoclaw gateway",
Long: `Stop the running picoclaw gateway by reading its PID file and sending a termination signal.`,
Args: cobra.NoArgs,
RunE: func(_ *cobra.Command, _ []string) error {
homePath := internal.GetPicoclawHome()
// Read PID file and check if process is running
data := pid.ReadPidFileWithCheck(homePath)
if data == nil {
fmt.Println("gateway is not running")
return nil
}
// Find the process
process, err := os.FindProcess(data.PID)
if err != nil {
return fmt.Errorf("failed to find gateway process (PID: %d): %w", data.PID, err)
}
// Send termination signal
fmt.Printf("stopping gateway (PID: %d)...\n", data.PID)
if err := stopProcess(process); err != nil {
return fmt.Errorf("failed to stop gateway (PID: %d): %w", data.PID, err)
}
fmt.Println("gateway stopped successfully")
return nil
},
}
}

View file

@ -24,7 +24,8 @@ func TestNewGatewayCommand(t *testing.T) {
assert.Nil(t, cmd.PersistentPreRun) assert.Nil(t, cmd.PersistentPreRun)
assert.Nil(t, cmd.PersistentPostRun) assert.Nil(t, cmd.PersistentPostRun)
assert.False(t, cmd.HasSubCommands()) assert.True(t, cmd.HasSubCommands())
assert.NotNil(t, cmd.Commands())
assert.True(t, cmd.HasFlags()) assert.True(t, cmd.HasFlags())
assert.NotNil(t, cmd.Flags().Lookup("debug")) assert.NotNil(t, cmd.Flags().Lookup("debug"))

View file

@ -0,0 +1,13 @@
//go:build !windows
package gateway
import (
"os"
"syscall"
)
// stopProcess sends SIGTERM to the process for graceful shutdown on Unix-like systems.
func stopProcess(process *os.Process) error {
return process.Signal(syscall.SIGTERM)
}

View file

@ -0,0 +1,12 @@
//go:build windows
package gateway
import (
"os"
)
// stopProcess kills the process on Windows (SIGTERM is not supported).
func stopProcess(process *os.Process) error {
return process.Kill()
}