go-micro.dev/v5@v5.12.0/cmd/micro/cli/cli.go (about)

     1  package microcli
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"os"
     9  	"os/exec"
    10  	"os/signal"
    11  	"path/filepath"
    12  	"strings"
    13  	"syscall"
    14  
    15  	"github.com/urfave/cli/v2"
    16  	"go-micro.dev/v5/client"
    17  	"go-micro.dev/v5/cmd"
    18  	"go-micro.dev/v5/codec/bytes"
    19  	"go-micro.dev/v5/genai"
    20  	"go-micro.dev/v5/registry"
    21  
    22  	"go-micro.dev/v5/cmd/micro/cli/new"
    23  	"go-micro.dev/v5/cmd/micro/cli/util"
    24  )
    25  
    26  var (
    27  	// version is set by the release action
    28  	// this is the default for local builds
    29  	version = "5.0.0-dev"
    30  )
    31  
    32  func genProtoHandler(c *cli.Context) error {
    33  	cmd := exec.Command("find", ".", "-name", "*.proto", "-exec", "protoc", "--proto_path=.", "--micro_out=.", "--go_out=.", `{}`, `;`)
    34  	cmd.Stdout = os.Stdout
    35  	cmd.Stderr = os.Stderr
    36  	return cmd.Run()
    37  }
    38  
    39  func genTextHandler(c *cli.Context) error {
    40  	prompt := c.String("prompt")
    41  	if len(prompt) == 0 {
    42  		return nil
    43  	}
    44  
    45  	gen := genai.DefaultGenAI
    46  	if gen.String() == "noop" {
    47  		return nil
    48  	}
    49  
    50  	res, err := gen.Generate(prompt)
    51  	if err != nil {
    52  		return err
    53  	}
    54  
    55  	fmt.Println(res.Text)
    56  	return nil
    57  }
    58  
    59  func lastNonEmptyLine(s string) string {
    60  	lines := strings.Split(s, "\n")
    61  	for i := len(lines) - 1; i >= 0; i-- {
    62  		if strings.TrimSpace(lines[i]) != "" {
    63  			return lines[i]
    64  		}
    65  	}
    66  	return ""
    67  }
    68  
    69  func lastLogLine(path string) string {
    70  	f, err := os.Open(path)
    71  	if err != nil {
    72  		return ""
    73  	}
    74  	defer f.Close()
    75  	var last string
    76  	scan := bufio.NewScanner(f)
    77  	for scan.Scan() {
    78  		if strings.TrimSpace(scan.Text()) != "" {
    79  			last = scan.Text()
    80  		}
    81  	}
    82  	return last
    83  }
    84  
    85  func waitAndCleanup(procs []*exec.Cmd, pidFiles []string) {
    86  	ch := make(chan os.Signal, 1)
    87  	signal.Notify(ch, os.Interrupt)
    88  	go func() {
    89  		<-ch
    90  		for _, proc := range procs {
    91  			if proc.Process != nil {
    92  				_ = proc.Process.Kill()
    93  			}
    94  		}
    95  		for _, pf := range pidFiles {
    96  			_ = os.Remove(pf)
    97  		}
    98  		os.Exit(1)
    99  	}()
   100  	for i, proc := range procs {
   101  		_ = proc.Wait()
   102  		if proc.Process != nil {
   103  			_ = os.Remove(pidFiles[i])
   104  		}
   105  	}
   106  }
   107  
   108  func init() {
   109  	cmd.Register([]*cli.Command{
   110  		{
   111  			Name:   "new",
   112  			Usage:  "Create a new service",
   113  			Action: new.Run,
   114  		},
   115  		{
   116  			Name:  "gen",
   117  			Usage: "Generate various things",
   118  			Subcommands: []*cli.Command{
   119  				{
   120  					Name:   "text",
   121  					Usage:  "Generate text via an LLM",
   122  					Action: genTextHandler,
   123  					Flags: []cli.Flag{
   124  						&cli.StringFlag{
   125  							Name:    "prompt",
   126  							Aliases: []string{"p"},
   127  							Usage:   "The prompt to generate text from",
   128  						},
   129  					},
   130  				},
   131  				{
   132  					Name:   "proto",
   133  					Usage:  "Generate proto requires protoc and protoc-gen-micro",
   134  					Action: genProtoHandler,
   135  				},
   136  			},
   137  		},
   138  		{
   139  			Name:  "services",
   140  			Usage: "List available services",
   141  			Action: func(ctx *cli.Context) error {
   142  				services, err := registry.ListServices()
   143  				if err != nil {
   144  					return err
   145  				}
   146  				for _, service := range services {
   147  					fmt.Println(service.Name)
   148  				}
   149  				return nil
   150  			},
   151  		},
   152  		{
   153  			Name:  "call",
   154  			Usage: "Call a service",
   155  			Action: func(ctx *cli.Context) error {
   156  				args := ctx.Args()
   157  
   158  				if args.Len() < 2 {
   159  					return fmt.Errorf("Usage: [service] [endpoint] [request]")
   160  				}
   161  
   162  				service := args.Get(0)
   163  				endpoint := args.Get(1)
   164  				request := `{}`
   165  
   166  				if args.Len() == 3 {
   167  					request = args.Get(2)
   168  				}
   169  
   170  				req := client.NewRequest(service, endpoint, &bytes.Frame{Data: []byte(request)})
   171  				var rsp bytes.Frame
   172  				err := client.Call(context.TODO(), req, &rsp)
   173  				if err != nil {
   174  					return err
   175  				}
   176  
   177  				fmt.Print(string(rsp.Data))
   178  				return nil
   179  			},
   180  		},
   181  		{
   182  			Name:  "describe",
   183  			Usage: "Describe a service",
   184  			Action: func(ctx *cli.Context) error {
   185  				args := ctx.Args()
   186  
   187  				if args.Len() != 1 {
   188  					return fmt.Errorf("Usage: [service]")
   189  				}
   190  
   191  				service := args.Get(0)
   192  				services, err := registry.GetService(service)
   193  				if err != nil {
   194  					return err
   195  				}
   196  				if len(services) == 0 {
   197  					return nil
   198  				}
   199  				b, _ := json.MarshalIndent(services[0], "", "    ")
   200  				fmt.Println(string(b))
   201  				return nil
   202  			},
   203  		},
   204  		{
   205  			Name:  "status",
   206  			Usage: "Check status of running services",
   207  			Action: func(ctx *cli.Context) error {
   208  				homeDir, err := os.UserHomeDir()
   209  				if err != nil {
   210  					return fmt.Errorf("failed to get home dir: %w", err)
   211  				}
   212  				runDir := filepath.Join(homeDir, "micro", "run")
   213  				files, err := os.ReadDir(runDir)
   214  				if err != nil {
   215  					return fmt.Errorf("failed to read run dir: %w", err)
   216  				}
   217  				fmt.Printf("%-20s %-8s %-8s %s\n", "SERVICE", "PID", "STATUS", "DIRECTORY")
   218  				for _, f := range files {
   219  					if f.IsDir() || !strings.HasSuffix(f.Name(), ".pid") {
   220  						continue
   221  					}
   222  					service := f.Name()[:len(f.Name())-4]
   223  					pidFilePath := filepath.Join(runDir, f.Name())
   224  					pidFile, err := os.Open(pidFilePath)
   225  					if err != nil {
   226  						continue
   227  					}
   228  					var pid int
   229  					var dir string
   230  					scanner := bufio.NewScanner(pidFile)
   231  					if scanner.Scan() {
   232  						fmt.Sscanf(scanner.Text(), "%d", &pid)
   233  					}
   234  					if scanner.Scan() {
   235  						dir = scanner.Text()
   236  					}
   237  					pidFile.Close()
   238  					status := "stopped"
   239  					if pid > 0 {
   240  						proc, err := os.FindProcess(pid)
   241  						if err == nil {
   242  							if err := proc.Signal(syscall.Signal(0)); err == nil {
   243  								status = "running"
   244  							}
   245  						}
   246  					}
   247  					fmt.Printf("%-20s %-8d %-8s %-40s %s\n", service, pid, status, "", dir)
   248  				}
   249  				return nil
   250  			},
   251  		},
   252  		{
   253  			Name:  "stop",
   254  			Usage: "Stop a running service",
   255  			Action: func(ctx *cli.Context) error {
   256  				if ctx.Args().Len() != 1 {
   257  					return fmt.Errorf("Usage: micro stop [service]")
   258  				}
   259  				service := ctx.Args().Get(0)
   260  				homeDir, err := os.UserHomeDir()
   261  				if err != nil {
   262  					return fmt.Errorf("failed to get home dir: %w", err)
   263  				}
   264  				runDir := filepath.Join(homeDir, "micro", "run")
   265  				pidFilePath := filepath.Join(runDir, service+".pid")
   266  				pidFile, err := os.Open(pidFilePath)
   267  				if err != nil {
   268  					return fmt.Errorf("no pid file for service %s", service)
   269  				}
   270  				var pid int
   271  				var dir string
   272  				scanner := bufio.NewScanner(pidFile)
   273  				if scanner.Scan() {
   274  					fmt.Sscanf(scanner.Text(), "%d", &pid)
   275  				}
   276  				if scanner.Scan() {
   277  					dir = scanner.Text()
   278  				}
   279  				pidFile.Close()
   280  				if pid <= 0 {
   281  					_ = os.Remove(pidFilePath)
   282  					return fmt.Errorf("service %s is not running", service)
   283  				}
   284  				proc, err := os.FindProcess(pid)
   285  				if err != nil {
   286  					_ = os.Remove(pidFilePath)
   287  					return fmt.Errorf("could not find process for %s", service)
   288  				}
   289  				if err := proc.Signal(syscall.SIGTERM); err != nil {
   290  					_ = os.Remove(pidFilePath)
   291  					return fmt.Errorf("failed to stop service %s: %v", service, err)
   292  				}
   293  				_ = os.Remove(pidFilePath)
   294  				fmt.Printf("Stopped service %s (pid %d) in directory %s\n", service, pid, dir)
   295  				return nil
   296  			},
   297  		},
   298  		{
   299  			Name:  "logs",
   300  			Usage: "Show logs for a service, or list available logs if no service is specified",
   301  			Action: func(ctx *cli.Context) error {
   302  				homeDir, err := os.UserHomeDir()
   303  				if err != nil {
   304  					return fmt.Errorf("failed to get home dir: %w", err)
   305  				}
   306  				logsDir := filepath.Join(homeDir, "micro", "logs")
   307  				if ctx.Args().Len() == 0 {
   308  					// List available logs
   309  					dirEntries, err := os.ReadDir(logsDir)
   310  					if err != nil {
   311  						return fmt.Errorf("could not list logs directory: %v", err)
   312  					}
   313  					fmt.Println("Available logs:")
   314  					found := false
   315  					for _, entry := range dirEntries {
   316  						if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") {
   317  							fmt.Println("  ", strings.TrimSuffix(entry.Name(), ".log"))
   318  							found = true
   319  						}
   320  					}
   321  					if !found {
   322  						fmt.Println("  (no logs found)")
   323  					}
   324  					return nil
   325  				}
   326  				service := ctx.Args().Get(0)
   327  				logFilePath := filepath.Join(logsDir, service+".log")
   328  				f, err := os.Open(logFilePath)
   329  				if err != nil {
   330  					return fmt.Errorf("could not open log file for service %s: %v", service, err)
   331  				}
   332  				defer f.Close()
   333  				scan := bufio.NewScanner(f)
   334  				for scan.Scan() {
   335  					fmt.Println(scan.Text())
   336  				}
   337  				return scan.Err()
   338  			},
   339  		},
   340  	}...)
   341  
   342  	cmd.App().Action = func(c *cli.Context) error {
   343  		if c.Args().Len() == 0 {
   344  			return nil
   345  		}
   346  
   347  		v, err := exec.LookPath("micro-" + c.Args().First())
   348  		if err == nil {
   349  			ce := exec.Command(v, c.Args().Slice()[1:]...)
   350  			ce.Stdout = os.Stdout
   351  			ce.Stderr = os.Stderr
   352  			return ce.Run()
   353  		}
   354  
   355  		command := c.Args().Get(0)
   356  		args := c.Args().Slice()
   357  
   358  		if srv, err := util.LookupService(command); err != nil {
   359  			return util.CliError(err)
   360  		} else if srv != nil && util.ShouldRenderHelp(args) {
   361  			return cli.Exit(util.FormatServiceUsage(srv, c), 0)
   362  		} else if srv != nil {
   363  			err := util.CallService(srv, args)
   364  			return util.CliError(err)
   365  		}
   366  
   367  		return nil
   368  	}
   369  }