Skip to content

Commit

Permalink
frpc: support stop command (#3511)
Browse files Browse the repository at this point in the history
  • Loading branch information
fatedier authored Jun 30, 2023
1 parent 4c4d5f0 commit fc4e787
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 16 deletions.
5 changes: 5 additions & 0 deletions Release.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
### Features

* frpc supports connecting to frps via the wss protocol by enabling the configuration `protocol = wss`.
* frpc supports stopping the service through the stop command.

### Improvements

* service.Run supports passing in context.

### Fixes

Expand Down
1 change: 1 addition & 0 deletions client/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func (svr *Service) RunAdminServer(address string) (err error) {

// api, see admin_api.go
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
Expand Down
25 changes: 21 additions & 4 deletions client/admin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"sort"
"strconv"
"strings"
"time"

"github.com/samber/lo"

Expand All @@ -42,7 +43,7 @@ func (svr *Service) healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}

// GET api/reload
// GET /api/reload
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}

Expand Down Expand Up @@ -72,6 +73,22 @@ func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
log.Info("success reload conf")
}

// POST /api/stop
func (svr *Service) apiStop(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}

log.Info("api request [/api/stop]")
defer func() {
log.Info("api response [/api/stop], code [%d]", res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()

go svr.GracefulClose(100 * time.Millisecond)
}

type StatusResp map[string][]ProxyStatusResp

type ProxyStatusResp struct {
Expand Down Expand Up @@ -106,7 +123,7 @@ func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxySta
return psr
}

// GET api/status
// GET /api/status
func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
var (
buf []byte
Expand Down Expand Up @@ -135,7 +152,7 @@ func (svr *Service) apiStatus(w http.ResponseWriter, r *http.Request) {
}
}

// GET api/config
// GET /api/config
func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}

Expand Down Expand Up @@ -175,7 +192,7 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, r *http.Request) {
res.Msg = strings.Join(newRows, "\n")
}

// PUT api/config
// PUT /api/config
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}

Expand Down
11 changes: 3 additions & 8 deletions cmd/frpc/sub/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,11 @@ func Execute() {
}
}

func handleSignal(svr *client.Service, doneCh chan struct{}) {
func handleTermSignal(svr *client.Service) {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
svr.GracefulClose(500 * time.Millisecond)
close(doneCh)
}

func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) {
Expand Down Expand Up @@ -227,16 +226,12 @@ func startService(
return
}

closedDoneCh := make(chan struct{})
shouldGracefulClose := cfg.Protocol == "kcp" || cfg.Protocol == "quic"
// Capture the exit signal if we use kcp or quic.
if shouldGracefulClose {
go handleSignal(svr, closedDoneCh)
go handleTermSignal(svr)
}

err = svr.Run(context.Background())
if err == nil && shouldGracefulClose {
<-closedDoneCh
}
_ = svr.Run(context.Background())
return
}
84 changes: 84 additions & 0 deletions cmd/frpc/sub/stop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2023 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package sub

import (
"encoding/base64"
"fmt"
"io"
"net/http"
"os"
"strings"

"github.com/spf13/cobra"

"github.com/fatedier/frp/pkg/config"
)

func init() {
rootCmd.AddCommand(stopCmd)
}

var stopCmd = &cobra.Command{
Use: "stop",
Short: "Stop the running frpc",
RunE: func(cmd *cobra.Command, args []string) error {
cfg, _, _, err := config.ParseClientConfig(cfgFile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

err = stopClient(cfg)
if err != nil {
fmt.Printf("frpc stop error: %v\n", err)
os.Exit(1)
}
fmt.Printf("stop success\n")
return nil
},
}

func stopClient(clientCfg config.ClientCommonConf) error {
if clientCfg.AdminPort == 0 {
return fmt.Errorf("admin_port shoud be set if you want to use stop feature")
}

req, err := http.NewRequest("POST", "http://"+
clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/stop", nil)
if err != nil {
return err
}

authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+
clientCfg.AdminPwd))

req.Header.Add("Authorization", authStr)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode == 200 {
return nil
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
8 changes: 4 additions & 4 deletions server/dashboard_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ func (svr *Service) Healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
}

// api/serverinfo
// /api/serverinfo
func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
defer func() {
Expand Down Expand Up @@ -176,7 +176,7 @@ type GetProxyInfoResp struct {
Proxies []*ProxyStatsInfo `json:"proxies"`
}

// api/proxy/:type
// /api/proxy/:type
func (svr *Service) APIProxyByType(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
params := mux.Vars(r)
Expand Down Expand Up @@ -244,7 +244,7 @@ type GetProxyStatsResp struct {
Status string `json:"status"`
}

// api/proxy/:type/:name
// /api/proxy/:type/:name
func (svr *Service) APIProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
params := mux.Vars(r)
Expand Down Expand Up @@ -307,7 +307,7 @@ func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName strin
return
}

// api/traffic/:name
// /api/traffic/:name
type GetProxyTrafficResp struct {
Name string `json:"name"`
TrafficIn []int64 `json:"traffic_in"`
Expand Down
28 changes: 28 additions & 0 deletions test/e2e/basic/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,32 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() {
}).Port(dashboardPort).
Ensure(framework.ExpectResponseCode(401))
})

ginkgo.It("stop", func() {
serverConf := consts.DefaultServerConfig

adminPort := f.AllocPort()
testPort := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
admin_port = %d
[test]
type = tcp
local_port = {{ .%s }}
remote_port = %d
`, adminPort, framework.TCPEchoServerPort, testPort)

f.RunProcesses([]string{serverConf}, []string{clientConf})

framework.NewRequestExpect(f).Port(testPort).Ensure()

client := clientsdk.New("127.0.0.1", adminPort)
err := client.Stop()
framework.ExpectNoError(err)

time.Sleep(3 * time.Second)

// frpc stopped so the port is not listened, expect error
framework.NewRequestExpect(f).Port(testPort).ExpectError(true).Ensure()
})
})
9 changes: 9 additions & 0 deletions test/e2e/pkg/sdk/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ func (c *Client) Reload() error {
return err
}

func (c *Client) Stop() error {
req, err := http.NewRequest("POST", "http://"+c.address+"/api/stop", nil)
if err != nil {
return err
}
_, err = c.do(req)
return err
}

func (c *Client) GetConfig() (string, error) {
req, err := http.NewRequest("GET", "http://"+c.address+"/api/config", nil)
if err != nil {
Expand Down

0 comments on commit fc4e787

Please sign in to comment.