diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..1fb5bec --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,30 @@ +package cmd + +import ( + "github.com/qiniu/goc/v2/pkg/client" + "github.com/qiniu/goc/v2/pkg/config" + "github.com/spf13/cobra" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "Lists all the registered services", + Long: "Lists all the registered services", + Example: ` +goc list [flags] +`, + + Run: list, +} + +var listWide bool + +func init() { + listCmd.Flags().StringVar(&config.GocConfig.Host, "host", "127.0.0.1:7777", "specify the host of the goc server") + listCmd.Flags().BoolVar(&listWide, "wide", false, "list all services with more information (such as pid)") + rootCmd.AddCommand(listCmd) +} + +func list(cmd *cobra.Command, args []string) { + client.NewWorker("http://" + config.GocConfig.Host).ListAgents(listWide) +} diff --git a/go.mod b/go.mod index 759dc88..59817bc 100644 --- a/go.mod +++ b/go.mod @@ -9,12 +9,15 @@ require ( github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.13 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d + github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.7.0 github.com/tongjingran/copy v1.4.2 go.uber.org/zap v1.17.0 golang.org/x/mod v0.4.2 golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d golang.org/x/tools v0.1.3 k8s.io/kubectl v0.21.2 k8s.io/test-infra v0.0.0-20210618100605-34aa2f2aa75b diff --git a/go.sum b/go.sum index a252527..3bdd798 100644 --- a/go.sum +++ b/go.sum @@ -853,6 +853,8 @@ github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v0.0.0-20160514122348-38ee283dabf1/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -932,6 +934,8 @@ github.com/octago/sflags v0.2.0/go.mod h1:G0bjdxh4qPRycF74a2B8pU36iTp9QHGx0w0dFZ github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..0a39272 --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,147 @@ +package client + +import ( + "encoding/json" + "fmt" + "golang.org/x/term" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + + "github.com/olekukonko/tablewriter" + "github.com/qiniu/goc/v2/pkg/log" +) + +// Action provides methods to contact with the covered agent under test +type Action interface { + ListAgents(bool) +} + +const ( + // CoverAgentsListAPI list all the registered agents + CoverAgentsListAPI = "/v2/rpcagents" +) + +type client struct { + Host string + client *http.Client +} + +// gocListAgents response of the list request +type gocListAgents struct { + Items []gocCoveredAgent `json:"items"` +} + +// gocCoveredAgent represents a covered client +type gocCoveredAgent struct { + Id string `json:"id"` + RemoteIP string `json:"remoteip"` + Hostname string `json:"hostname"` + CmdLine string `json:"cmdline"` + Pid string `json:"pid"` +} + +// NewWorker creates a worker to contact with host +func NewWorker(host string) Action { + _, err := url.ParseRequestURI(host) + if err != nil { + log.Fatalf("parse url %s failed, err: %v", host, err) + } + return &client{ + Host: host, + client: http.DefaultClient, + } +} + +func (c *client) ListAgents(wide bool) { + u := fmt.Sprintf("%s%s", c.Host, CoverAgentsListAPI) + _, body, err := c.do("GET", u, "", nil) + if err != nil && isNetworkError(err) { + _, body, err = c.do("GET", u, "", nil) + } + if err != nil { + err = fmt.Errorf("goc list failed: %v", err) + log.Fatalf(err.Error()) + } + agents := gocListAgents{} + err = json.Unmarshal(body, &agents) + if err != nil { + err = fmt.Errorf("goc list failed: json unmarshal failed: %v", err) + log.Fatalf(err.Error()) + } + table := tablewriter.NewWriter(os.Stdout) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding(" ") // pad with 3 blank spaces + table.SetNoWhiteSpace(true) + table.SetReflowDuringAutoWrap(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAutoWrapText(false) + if wide { + table.SetHeader([]string{"ID", "REMOTEIP", "HOSTNAME", "PID", "CMD"}) + table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}) + } else { + table.SetHeader([]string{"ID", "REMOTEIP", "CMD"}) + table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}) + } + for _, agent := range agents.Items { + if wide { + table.Append([]string{agent.Id, agent.RemoteIP, agent.Hostname, agent.Pid, agent.CmdLine}) + } else { + preLen := len(agent.Id) + len(agent.RemoteIP) + 9 + table.Append([]string{agent.Id, agent.RemoteIP, getSimpleCmdLine(preLen, agent.CmdLine)}) + } + } + table.Render() + return +} + +// getSimpleCmdLine +func getSimpleCmdLine(preLen int, cmdLine string) string { + pathLen := len(cmdLine) + width, _, err := term.GetSize(int(os.Stdin.Fd())) + if err != nil || width <= preLen+16 { + width = 16 + preLen // show at least 16 words of the command + } + if pathLen > width-preLen { + return cmdLine[:width-preLen] + } + return cmdLine +} + +func (c *client) do(method, url, contentType string, body io.Reader) (*http.Response, []byte, error) { + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, nil, err + } + + if contentType != "" { + req.Header.Set("Content-Type", contentType) + } + + res, err := c.client.Do(req) + if err != nil { + return nil, nil, err + } + defer res.Body.Close() + + responseBody, err := ioutil.ReadAll(res.Body) + if err != nil { + return res, nil, err + } + return res, responseBody, nil +} + +func isNetworkError(err error) bool { + if err == io.EOF { + return true + } + _, ok := err.(net.Error) + return ok +} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go new file mode 100644 index 0000000..2bc5720 --- /dev/null +++ b/pkg/client/client_test.go @@ -0,0 +1,89 @@ +package client + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func captureStdout(f func()) string { + r, w, _ := os.Pipe() + stdout := os.Stdout + os.Stdout = w + defer func() { + os.Stdout = stdout + }() + + f() + w.Close() + + var buf bytes.Buffer + io.Copy(&buf, r) + + return buf.String() +} + +func TestClientListAgents(t *testing.T) { + mockAgents := `{"items": [{"id": "testID", "remoteip": "1.1.1.1", "hostname": "testHost", "cmdline": "./testCmd -f testArgs", "pid": "0"}]}` + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte(mockAgents)) + })) + defer mockServer.Close() + + c := NewWorker(mockServer.URL) + testCases := map[string]struct { + input bool + expected string + }{ + "simple list": { + false, + `ID REMOTEIP CMD +testID 1.1.1.1 ./testCmd -f tes +`, + }, + "wide list": { + true, + `ID REMOTEIP HOSTNAME PID CMD +testID 1.1.1.1 testHost 0 ./testCmd -f testArgs +`, + }, + } + for name, tt := range testCases { + t.Run(name, func(t *testing.T) { + f := func() { c.ListAgents(tt.input) } + output := captureStdout(f) + fmt.Println(output) + assert.Equal(t, output, tt.expected) + }) + } +} + +func TestClientDo(t *testing.T) { + c := &client{ + client: http.DefaultClient, + } + _, _, err := c.do(" ", "http://127.0.0.1:7777", "", nil) // a invalid method + assert.Contains(t, err.Error(), "invalid method") +} + +func TestGetSimpleSvcName(t *testing.T) { + testCases := map[string]struct { + input string + expected string + }{ + "short path": {"1234567890abc.go", "1234567890abc.go"}, + "long path": {"1234567890abcdef.go", "1234567890abcdef"}, + } + for name, tt := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, getSimpleCmdLine(0, tt.input), tt.expected) + }) + } +} diff --git a/pkg/cover/agent.tpl b/pkg/cover/agent.tpl index 64d32aa..fa70c6a 100644 --- a/pkg/cover/agent.tpl +++ b/pkg/cover/agent.tpl @@ -204,7 +204,7 @@ func getRegisterInfo() (*processInfo, error) { pid := os.Getpid() - cmdline := os.Args[0] + cmdline := strings.Join(os.Args, " ") return &processInfo{ hostname: hostname, diff --git a/pkg/cover/agentwatch.tpl b/pkg/cover/agentwatch.tpl index c0ddc89..69191c8 100644 --- a/pkg/cover/agentwatch.tpl +++ b/pkg/cover/agentwatch.tpl @@ -6,6 +6,7 @@ import ( "os" "log" "strconv" + "strings" "net/url" "{{.GlobalCoverVarImportPath}}/websocket" @@ -115,7 +116,7 @@ func getRegisterInfo() (*processInfo, error) { pid := os.Getpid() - cmdline := os.Args[0] + cmdline := strings.Join(os.Args, " ") return &processInfo{ hostname: hostname,