diff options
author | Valery Piashchynski <[email protected]> | 2022-01-15 12:08:20 +0300 |
---|---|---|
committer | Valery Piashchynski <[email protected]> | 2022-01-15 12:08:20 +0300 |
commit | 5254c8eb27311e2a8a53a4c90c3829cf1238c563 (patch) | |
tree | b51c9a4c1dd4c25adc511498ce0380a7078c5572 /internal/cli | |
parent | 13609dd03dd0d2fa85b9fb850be787bf4e2ea67f (diff) |
Repository content update
Signed-off-by: Valery Piashchynski <[email protected]>
Diffstat (limited to 'internal/cli')
-rw-r--r-- | internal/cli/reset/command.go | 104 | ||||
-rw-r--r-- | internal/cli/reset/command_test.go | 21 | ||||
-rw-r--r-- | internal/cli/root.go | 98 | ||||
-rw-r--r-- | internal/cli/root_test.go | 85 | ||||
-rw-r--r-- | internal/cli/serve/command.go | 104 | ||||
-rw-r--r-- | internal/cli/serve/command_test.go | 21 | ||||
-rw-r--r-- | internal/cli/workers/command.go | 143 | ||||
-rw-r--r-- | internal/cli/workers/command_test.go | 49 | ||||
-rw-r--r-- | internal/cli/workers/render.go | 135 |
9 files changed, 760 insertions, 0 deletions
diff --git a/internal/cli/reset/command.go b/internal/cli/reset/command.go new file mode 100644 index 00000000..d6cf7087 --- /dev/null +++ b/internal/cli/reset/command.go @@ -0,0 +1,104 @@ +package reset + +import ( + "fmt" + "sync" + + internalRpc "github.com/spiral/roadrunner-binary/v2/internal/rpc" + + "github.com/fatih/color" + "github.com/mattn/go-runewidth" + "github.com/spf13/cobra" + "github.com/spiral/errors" + "github.com/spiral/roadrunner-plugins/v2/config" + "github.com/vbauerster/mpb/v5" + "github.com/vbauerster/mpb/v5/decor" +) + +var spinnerStyle = []string{"∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙∙∙"} //nolint:gochecknoglobals + +// NewCommand creates `reset` command. +func NewCommand(cfgPlugin *config.Plugin) *cobra.Command { //nolint:funlen + return &cobra.Command{ + Use: "reset", + Short: "Reset workers of all or specific RoadRunner service", + RunE: func(_ *cobra.Command, args []string) error { + const ( + op = errors.Op("reset_handler") + resetterList = "resetter.List" + resetterReset = "resetter.Reset" + ) + + client, err := internalRpc.NewClient(cfgPlugin) + if err != nil { + return err + } + + defer func() { _ = client.Close() }() + + services := args // by default we expect services list from user + if len(services) == 0 { // but if nothing was passed - request all services list + if err = client.Call(resetterList, true, &services); err != nil { + return err + } + } + + var wg sync.WaitGroup + wg.Add(len(services)) + + pr := mpb.New(mpb.WithWaitGroup(&wg), mpb.WithWidth(6)) //nolint:gomnd + + for _, service := range services { + var ( + bar *mpb.Bar + name = runewidth.FillRight(fmt.Sprintf("Resetting plugin: [%s]", color.HiYellowString(service)), 27) + result = make(chan interface{}) + ) + + bar = pr.AddSpinner( + 1, + mpb.SpinnerOnMiddle, + mpb.SpinnerStyle(spinnerStyle), + mpb.PrependDecorators(decor.Name(name)), + mpb.AppendDecorators(onComplete(result)), + ) + + // simulating some work + go func(service string, result chan interface{}) { + defer wg.Done() + defer bar.Increment() + + var done bool + <-client.Go(resetterReset, service, &done, nil).Done + + if err != nil { + result <- errors.E(op, err) + + return + } + + result <- nil + }(service, result) + } + + pr.Wait() + + return nil + }, + } +} + +func onComplete(result chan interface{}) decor.Decorator { + return decor.Any(func(s decor.Statistics) string { + select { + case r := <-result: + if err, ok := r.(error); ok { + return color.HiRedString(err.Error()) + } + + return color.HiGreenString("done") + default: + return "" + } + }) +} diff --git a/internal/cli/reset/command_test.go b/internal/cli/reset/command_test.go new file mode 100644 index 00000000..00cd046e --- /dev/null +++ b/internal/cli/reset/command_test.go @@ -0,0 +1,21 @@ +package reset_test + +import ( + "testing" + + "github.com/spiral/roadrunner-binary/v2/internal/cli/reset" + + "github.com/spiral/roadrunner-plugins/v2/config" + "github.com/stretchr/testify/assert" +) + +func TestCommandProperties(t *testing.T) { + cmd := reset.NewCommand(&config.Plugin{}) + + assert.Equal(t, "reset", cmd.Use) + assert.NotNil(t, cmd.RunE) +} + +func TestExecution(t *testing.T) { + t.Skip("Command execution is not implemented yet") +} diff --git a/internal/cli/root.go b/internal/cli/root.go new file mode 100644 index 00000000..8572bdc6 --- /dev/null +++ b/internal/cli/root.go @@ -0,0 +1,98 @@ +package cli + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/spiral/roadrunner-binary/v2/internal/cli/reset" + "github.com/spiral/roadrunner-binary/v2/internal/cli/serve" + "github.com/spiral/roadrunner-binary/v2/internal/cli/workers" + dbg "github.com/spiral/roadrunner-binary/v2/internal/debug" + "github.com/spiral/roadrunner-binary/v2/internal/meta" + + "github.com/joho/godotenv" + "github.com/spf13/cobra" + "github.com/spiral/roadrunner-plugins/v2/config" +) + +// NewCommand creates root command. +func NewCommand(cmdName string) *cobra.Command { //nolint:funlen + const envDotenv = "DOTENV_PATH" // env var name: path to the .env file + + var ( // flag values + cfgFile string // path to the .rr.yaml + workDir string // working directory + dotenv string // path to the .env file + debug bool // debug mode + override []string // override config values + ) + + var configPlugin = &config.Plugin{} // will be overwritten on pre-run action + + cmd := &cobra.Command{ + Use: cmdName, + Short: "High-performance PHP application server, load-balancer and process manager", + SilenceErrors: true, + SilenceUsage: true, + Version: fmt.Sprintf("%s (build time: %s, %s)", meta.Version(), meta.BuildTime(), runtime.Version()), + PersistentPreRunE: func(*cobra.Command, []string) error { + if cfgFile != "" { + if absPath, err := filepath.Abs(cfgFile); err == nil { + cfgFile = absPath // switch config path to the absolute + + // force working absPath related to config file + if err = os.Chdir(filepath.Dir(absPath)); err != nil { + return err + } + } + } + + if workDir != "" { + if err := os.Chdir(workDir); err != nil { + return err + } + } + + if v, ok := os.LookupEnv(envDotenv); ok { // read path to the dotenv file from environment variable + dotenv = v + } + + if dotenv != "" { + _ = godotenv.Load(dotenv) // error ignored because dotenv is optional feature + } + + cfg := &config.Plugin{Path: cfgFile, Prefix: "rr", Flags: override} + if err := cfg.Init(); err != nil { + return err + } + + if debug { + srv := dbg.NewServer() + go func() { _ = srv.Start(":6061") }() // TODO implement graceful server stopping + } + + // overwrite + *configPlugin = *cfg + + return nil + }, + } + + f := cmd.PersistentFlags() + + f.StringVarP(&cfgFile, "config", "c", ".rr.yaml", "config file") + f.StringVarP(&workDir, "WorkDir", "w", "", "working directory") // TODO change to `workDir`? + f.StringVarP(&dotenv, "dotenv", "", "", fmt.Sprintf("dotenv file [$%s]", envDotenv)) + f.BoolVarP(&debug, "debug", "d", false, "debug mode") + f.StringArrayVarP(&override, "override", "o", nil, "override config value (dot.notation=value)") + + cmd.AddCommand( + workers.NewCommand(configPlugin), + reset.NewCommand(configPlugin), + serve.NewCommand(configPlugin), + ) + + return cmd +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go new file mode 100644 index 00000000..59af9294 --- /dev/null +++ b/internal/cli/root_test.go @@ -0,0 +1,85 @@ +package cli_test + +import ( + "testing" + + "github.com/spiral/roadrunner-binary/v2/internal/cli" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +func TestCommandSubcommands(t *testing.T) { + cmd := cli.NewCommand("unit test") + + cases := []struct { + giveName string + }{ + {giveName: "workers"}, + {giveName: "reset"}, + {giveName: "serve"}, + } + + // get all existing subcommands and put into the map + subcommands := make(map[string]*cobra.Command) + for _, sub := range cmd.Commands() { + subcommands[sub.Name()] = sub + } + + for _, tt := range cases { + tt := tt + t.Run(tt.giveName, func(t *testing.T) { + if _, exists := subcommands[tt.giveName]; !exists { + assert.Failf(t, "command not found", "command [%s] was not found", tt.giveName) + } + }) + } +} + +func TestCommandFlags(t *testing.T) { + cmd := cli.NewCommand("unit test") + + cases := []struct { + giveName string + wantShorthand string + wantDefault string + }{ + {giveName: "config", wantShorthand: "c", wantDefault: ".rr.yaml"}, + {giveName: "WorkDir", wantShorthand: "w", wantDefault: ""}, + {giveName: "dotenv", wantShorthand: "", wantDefault: ""}, + {giveName: "debug", wantShorthand: "d", wantDefault: "false"}, + {giveName: "override", wantShorthand: "o", wantDefault: "[]"}, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.giveName, func(t *testing.T) { + flag := cmd.Flag(tt.giveName) + + if flag == nil { + assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName) + + return + } + + assert.Equal(t, tt.wantShorthand, flag.Shorthand) + assert.Equal(t, tt.wantDefault, flag.DefValue) + }) + } +} + +func TestCommandSimpleExecuting(t *testing.T) { + cmd := cli.NewCommand("unit test") + cmd.SetArgs([]string{"-c", "./../../.rr.yaml"}) + + var executed bool + + if cmd.Run == nil { // override "Run" property for test (if it was not set) + cmd.Run = func(cmd *cobra.Command, args []string) { + executed = true + } + } + + assert.NoError(t, cmd.Execute()) + assert.True(t, executed) +} diff --git a/internal/cli/serve/command.go b/internal/cli/serve/command.go new file mode 100644 index 00000000..6679d795 --- /dev/null +++ b/internal/cli/serve/command.go @@ -0,0 +1,104 @@ +package serve + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/spiral/roadrunner-binary/v2/internal/container" + "github.com/spiral/roadrunner-binary/v2/internal/meta" + + "github.com/spf13/cobra" + "github.com/spiral/errors" + configImpl "github.com/spiral/roadrunner-plugins/v2/config" +) + +// NewCommand creates `serve` command. +func NewCommand(cfgPlugin *configImpl.Plugin) *cobra.Command { //nolint:funlen + return &cobra.Command{ + Use: "serve", + Short: "Start RoadRunner server", + RunE: func(*cobra.Command, []string) error { + const op = errors.Op("handle_serve_command") + + // create endure container config + containerCfg, err := container.NewConfig(cfgPlugin) + if err != nil { + return errors.E(op, err) + } + + // set the grace period which would be same for all the plugins + cfgPlugin.Timeout = containerCfg.GracePeriod + cfgPlugin.Version = meta.Version() + + // create endure container + endureContainer, err := container.NewContainer(*containerCfg) + if err != nil { + return errors.E(op, err) + } + + // register config plugin + if err = endureContainer.Register(cfgPlugin); err != nil { + return errors.E(op, err) + } + + // register another container plugins + for i, plugins := 0, container.Plugins(); i < len(plugins); i++ { + if err = endureContainer.Register(plugins[i]); err != nil { + return errors.E(op, err) + } + } + + // init container and all services + if err = endureContainer.Init(); err != nil { + return errors.E(op, err) + } + + // start serving the graph + errCh, err := endureContainer.Serve() + if err != nil { + return errors.E(op, err) + } + + oss, stop := make(chan os.Signal, 2), make(chan struct{}, 1) //nolint:gomnd + signal.Notify(oss, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + go func() { + // first catch - stop the container + <-oss + // send signal to stop execution + stop <- struct{}{} + + // after first hit we are waiting for the second + // second catch - exit from the process + <-oss + fmt.Println("exit forced") + os.Exit(1) + }() + + fmt.Printf("[INFO] RoadRunner server started; version: %s, buildtime: %s\n", meta.Version(), meta.BuildTime()) + + for { + select { + case e := <-errCh: + fmt.Printf("error occurred: %v, plugin: %s\n", e.Error, e.VertexID) + + // return error, container already stopped internally + if !containerCfg.RetryOnFail { + return errors.E(op, e.Error) + } + + case <-stop: // stop the container after first signal + fmt.Printf("stop signal received, grace timeout is: %d seconds\n", uint64(containerCfg.GracePeriod.Seconds())) + + if err = endureContainer.Stop(); err != nil { + fmt.Printf("error occurred during the stopping container: %v\n", err) + } + + return nil + } + } + }, + } +} diff --git a/internal/cli/serve/command_test.go b/internal/cli/serve/command_test.go new file mode 100644 index 00000000..0e61ce83 --- /dev/null +++ b/internal/cli/serve/command_test.go @@ -0,0 +1,21 @@ +package serve_test + +import ( + "testing" + + "github.com/spiral/roadrunner-binary/v2/internal/cli/serve" + + "github.com/spiral/roadrunner-plugins/v2/config" + "github.com/stretchr/testify/assert" +) + +func TestCommandProperties(t *testing.T) { + cmd := serve.NewCommand(&config.Plugin{}) + + assert.Equal(t, "serve", cmd.Use) + assert.NotNil(t, cmd.RunE) +} + +func TestExecution(t *testing.T) { + t.Skip("Command execution is not implemented yet") +} diff --git a/internal/cli/workers/command.go b/internal/cli/workers/command.go new file mode 100644 index 00000000..283887e4 --- /dev/null +++ b/internal/cli/workers/command.go @@ -0,0 +1,143 @@ +package workers + +import ( + "fmt" + "net/rpc" + "os" + "os/signal" + "syscall" + "time" + + "github.com/roadrunner-server/api/v2/plugins/jobs" + internalRpc "github.com/spiral/roadrunner-binary/v2/internal/rpc" + + tm "github.com/buger/goterm" + "github.com/fatih/color" + "github.com/spf13/cobra" + "github.com/spiral/errors" + "github.com/spiral/roadrunner-plugins/v2/config" + "github.com/spiral/roadrunner-plugins/v2/informer" +) + +// NewCommand creates `workers` command. +func NewCommand(cfgPlugin *config.Plugin) *cobra.Command { //nolint:funlen + var ( + // interactive workers updates + interactive bool + ) + + cmd := &cobra.Command{ + Use: "workers", + Short: "Show information about active RoadRunner workers", + RunE: func(_ *cobra.Command, args []string) error { + const ( + op = errors.Op("handle_workers_command") + informerList = "informer.List" + ) + + client, err := internalRpc.NewClient(cfgPlugin) + if err != nil { + return err + } + + defer func() { _ = client.Close() }() + + plugins := args // by default we expect plugins list from user + if len(plugins) == 0 { // but if nothing was passed - request all informers list + if err = client.Call(informerList, true, &plugins); err != nil { + return err + } + } + + if !interactive { + return showWorkers(plugins, client) + } + + oss := make(chan os.Signal, 1) + signal.Notify(oss, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + + tm.Clear() + + tt := time.NewTicker(time.Second) + defer tt.Stop() + + for { + select { + case <-oss: + return nil + + case <-tt.C: + tm.MoveCursor(1, 1) + tm.Flush() + + if err = showWorkers(plugins, client); err != nil { + return errors.E(op, err) + } + } + } + }, + } + + cmd.Flags().BoolVarP( + &interactive, + "interactive", + "i", + false, + "render interactive workers table", + ) + + return cmd +} + +func showWorkers(plugins []string, client *rpc.Client) error { + const ( + op = errors.Op("show_workers") + informerWorkers = "informer.Workers" + informerJobs = "informer.Jobs" + // this is only one exception to Render the workers, service plugin has the same workers as other plugins, + // but they are RAW processes and needs to be handled in a different way. We don't need a special RPC call, but + // need a special render method. + servicePluginName = "service" + ) + + for _, plugin := range plugins { + list := &informer.WorkerList{} + + if err := client.Call(informerWorkers, plugin, &list); err != nil { + return errors.E(op, err) + } + + if len(list.Workers) == 0 { + continue + } + + if plugin == servicePluginName { + fmt.Printf("Workers of [%s]:\n", color.HiYellowString(plugin)) + ServiceWorkerTable(os.Stdout, list.Workers).Render() + + continue + } + + fmt.Printf("Workers of [%s]:\n", color.HiYellowString(plugin)) + + WorkerTable(os.Stdout, list.Workers).Render() + } + + for _, plugin := range plugins { + var jst []*jobs.State + + if err := client.Call(informerJobs, plugin, &jst); err != nil { + return errors.E(op, err) + } + + // eq to nil + if len(jst) == 0 { + continue + } + + fmt.Printf("Jobs of [%s]:\n", color.HiYellowString(plugin)) + JobsTable(os.Stdout, jst).Render() + } + + return nil +} diff --git a/internal/cli/workers/command_test.go b/internal/cli/workers/command_test.go new file mode 100644 index 00000000..e593686d --- /dev/null +++ b/internal/cli/workers/command_test.go @@ -0,0 +1,49 @@ +package workers_test + +import ( + "testing" + + "github.com/spiral/roadrunner-binary/v2/internal/cli/workers" + + "github.com/spiral/roadrunner-plugins/v2/config" + "github.com/stretchr/testify/assert" +) + +func TestCommandProperties(t *testing.T) { + cmd := workers.NewCommand(&config.Plugin{}) + + assert.Equal(t, "workers", cmd.Use) + assert.NotNil(t, cmd.RunE) +} + +func TestCommandFlags(t *testing.T) { + cmd := workers.NewCommand(&config.Plugin{}) + + cases := []struct { + giveName string + wantShorthand string + wantDefault string + }{ + {giveName: "interactive", wantShorthand: "i", wantDefault: "false"}, + } + + for _, tt := range cases { + tt := tt + t.Run(tt.giveName, func(t *testing.T) { + flag := cmd.Flag(tt.giveName) + + if flag == nil { + assert.Failf(t, "flag not found", "flag [%s] was not found", tt.giveName) + + return + } + + assert.Equal(t, tt.wantShorthand, flag.Shorthand) + assert.Equal(t, tt.wantDefault, flag.DefValue) + }) + } +} + +func TestExecution(t *testing.T) { + t.Skip("Command execution is not implemented yet") +} diff --git a/internal/cli/workers/render.go b/internal/cli/workers/render.go new file mode 100644 index 00000000..0bdf09b6 --- /dev/null +++ b/internal/cli/workers/render.go @@ -0,0 +1,135 @@ +package workers + +import ( + "io" + "strconv" + "time" + + "github.com/dustin/go-humanize" + "github.com/fatih/color" + "github.com/olekukonko/tablewriter" + "github.com/roadrunner-server/api/v2/plugins/jobs" + "github.com/spiral/roadrunner/v2/state/process" +) + +const ( + Ready string = "READY" + Paused string = "PAUSED/STOPPED" +) + +// WorkerTable renders table with information about rr server workers. +func WorkerTable(writer io.Writer, workers []*process.State) *tablewriter.Table { + tw := tablewriter.NewWriter(writer) + tw.SetHeader([]string{"PID", "Status", "Execs", "Memory", "CPU%", "Created"}) + tw.SetColMinWidth(0, 7) + tw.SetColMinWidth(1, 9) + tw.SetColMinWidth(2, 7) + tw.SetColMinWidth(3, 7) + tw.SetColMinWidth(4, 7) + tw.SetColMinWidth(5, 18) + + for i := 0; i < len(workers); i++ { + tw.Append([]string{ + strconv.Itoa(workers[i].Pid), + renderStatus(workers[i].Status), + renderJobs(workers[i].NumJobs), + humanize.Bytes(workers[i].MemoryUsage), + renderCPU(workers[i].CPUPercent), + renderAlive(time.Unix(0, workers[i].Created)), + }) + } + + return tw +} + +// ServiceWorkerTable renders table with information about rr server workers. +func ServiceWorkerTable(writer io.Writer, workers []*process.State) *tablewriter.Table { + tw := tablewriter.NewWriter(writer) + tw.SetAutoWrapText(false) + tw.SetHeader([]string{"PID", "Memory", "CPU%", "Command"}) + tw.SetColMinWidth(0, 7) + tw.SetColMinWidth(1, 7) + tw.SetColMinWidth(2, 7) + tw.SetColMinWidth(3, 18) + tw.SetAlignment(tablewriter.ALIGN_LEFT) + + for i := 0; i < len(workers); i++ { + tw.Append([]string{ + strconv.Itoa(workers[i].Pid), + humanize.Bytes(workers[i].MemoryUsage), + renderCPU(workers[i].CPUPercent), + workers[i].Command, + }) + } + + return tw +} + +// JobsTable renders table with information about rr server jobs. +func JobsTable(writer io.Writer, jobs []*jobs.State) *tablewriter.Table { + tw := tablewriter.NewWriter(writer) + tw.SetAutoWrapText(false) + tw.SetHeader([]string{"Status", "Pipeline", "Driver", "Queue", "Active", "Delayed", "Reserved"}) + tw.SetColWidth(10) + tw.SetColWidth(10) + tw.SetColWidth(7) + tw.SetColWidth(15) + tw.SetColWidth(10) + tw.SetColWidth(10) + tw.SetColWidth(10) + tw.SetAlignment(tablewriter.ALIGN_LEFT) + + for i := 0; i < len(jobs); i++ { + tw.Append([]string{ + renderReady(jobs[i].Ready), + jobs[i].Pipeline, + jobs[i].Driver, + jobs[i].Queue, + strconv.Itoa(int(jobs[i].Active)), + strconv.Itoa(int(jobs[i].Delayed)), + strconv.Itoa(int(jobs[i].Reserved)), + }) + } + + return tw +} + +func renderReady(ready bool) string { + if ready { + return Ready + } + + return Paused +} + +//go:inline +func renderCPU(cpu float64) string { + return strconv.FormatFloat(cpu, 'f', 2, 64) +} + +func renderStatus(status string) string { + switch status { + case "inactive": + return color.YellowString("inactive") + case "ready": + return color.CyanString("ready") + case "working": + return color.GreenString("working") + case "invalid": + return color.YellowString("invalid") + case "stopped": + return color.RedString("stopped") + case "errored": + return color.RedString("errored") + default: + return status + } +} + +func renderJobs(number uint64) string { + return humanize.Comma(int64(number)) +} + +func renderAlive(t time.Time) string { + return humanize.RelTime(t, time.Now(), "ago", "") +} |