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

     1  package run
     2  
     3  import (
     4  	"bufio"
     5  	"crypto/md5"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"os/signal"
    11  	"path/filepath"
    12  	"strconv"
    13  	"strings"
    14  	"syscall"
    15  	"time"
    16  
    17  	"github.com/urfave/cli/v2"
    18  	"go-micro.dev/v5/cmd"
    19  )
    20  
    21  // Color codes for log output
    22  var colors = []string{
    23  	"\033[31m", // red
    24  	"\033[32m", // green
    25  	"\033[33m", // yellow
    26  	"\033[34m", // blue
    27  	"\033[35m", // magenta
    28  	"\033[36m", // cyan
    29  }
    30  
    31  func colorFor(idx int) string {
    32  	return colors[idx%len(colors)]
    33  }
    34  
    35  func Run(c *cli.Context) error {
    36  	dir := c.Args().Get(0)
    37  	var tmpDir string
    38  	if len(dir) == 0 {
    39  		dir = "."
    40  	} else if strings.HasPrefix(dir, "github.com/") || strings.HasPrefix(dir, "https://github.com/") {
    41  		// Handle git URLs
    42  		repo := dir
    43  		if strings.HasPrefix(repo, "https://") {
    44  			repo = strings.TrimPrefix(repo, "https://")
    45  		}
    46  		// Clone to a temp directory
    47  		tmp, err := os.MkdirTemp("", "micro-run-")
    48  		if err != nil {
    49  			return fmt.Errorf("failed to create temp dir: %w", err)
    50  		}
    51  		tmpDir = tmp
    52  		cloneURL := repo
    53  		if !strings.HasPrefix(cloneURL, "https://") {
    54  			cloneURL = "https://" + repo
    55  		}
    56  		// Run git clone
    57  		cmd := exec.Command("git", "clone", cloneURL, tmpDir)
    58  		cmd.Stdout = os.Stdout
    59  		cmd.Stderr = os.Stderr
    60  		if err := cmd.Run(); err != nil {
    61  			return fmt.Errorf("failed to clone repo %s: %w", cloneURL, err)
    62  		}
    63  		dir = tmpDir
    64  	}
    65  
    66  	homeDir, err := os.UserHomeDir()
    67  	if err != nil {
    68  		return fmt.Errorf("failed to get home dir: %w", err)
    69  	}
    70  	logsDir := filepath.Join(homeDir, "micro", "logs")
    71  	if err := os.MkdirAll(logsDir, 0755); err != nil {
    72  		return fmt.Errorf("failed to create logs dir: %w", err)
    73  	}
    74  	runDir := filepath.Join(homeDir, "micro", "run")
    75  	if err := os.MkdirAll(runDir, 0755); err != nil {
    76  		return fmt.Errorf("failed to create run dir: %w", err)
    77  	}
    78  	binDir := filepath.Join(homeDir, "micro", "bin")
    79  	if err := os.MkdirAll(binDir, 0755); err != nil {
    80  		return fmt.Errorf("failed to create bin dir: %w", err)
    81  	}
    82  
    83  	// Always run all services (find all main.go)
    84  	var mainFiles []string
    85  	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
    86  		if err != nil {
    87  			return err
    88  		}
    89  		if info.IsDir() {
    90  			return nil
    91  		}
    92  		if info.Name() == "main.go" {
    93  			mainFiles = append(mainFiles, path)
    94  		}
    95  		return nil
    96  	})
    97  	if err != nil {
    98  		return fmt.Errorf("error walking the path: %w", err)
    99  	}
   100  	if len(mainFiles) == 0 {
   101  		return fmt.Errorf("no main.go files found in %s", dir)
   102  	}
   103  	var procs []*exec.Cmd
   104  	var pidFiles []string
   105  	for i, mainFile := range mainFiles {
   106  		serviceDir := filepath.Dir(mainFile)
   107  		var serviceName string
   108  		absServiceDir, _ := filepath.Abs(serviceDir)
   109  		// Determine service name: if absServiceDir matches the provided dir (which may be "."), use cwd
   110  		if absServiceDir == dir {
   111  			cwd, _ := os.Getwd()
   112  			serviceName = filepath.Base(cwd)
   113  		} else {
   114  			serviceName = filepath.Base(serviceDir)
   115  		}
   116  		serviceNameForPid := serviceName + "-" + fmt.Sprintf("%x", md5.Sum([]byte(absServiceDir)))[:8]
   117  		logFilePath := filepath.Join(logsDir, serviceNameForPid+".log")
   118  		binPath := filepath.Join(binDir, serviceNameForPid)
   119  		pidFilePath := filepath.Join(runDir, serviceNameForPid+".pid")
   120  
   121  		// Check if pid file exists and process is running
   122  		if pidBytes, err := os.ReadFile(pidFilePath); err == nil {
   123  			lines := strings.Split(string(pidBytes), "\n")
   124  			if len(lines) > 0 && len(lines[0]) > 0 {
   125  				pid := lines[0]
   126  				if _, err := os.FindProcess(parsePid(pid)); err == nil {
   127  					if processRunning(pid) {
   128  						fmt.Fprintf(os.Stderr, "Service %s already running (pid %s)\n", serviceNameForPid, pid)
   129  						continue
   130  					}
   131  				}
   132  			}
   133  		}
   134  
   135  		logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
   136  		if err != nil {
   137  			fmt.Fprintf(os.Stderr, "failed to open log file for %s: %v\n", serviceName, err)
   138  			continue
   139  		}
   140  		buildCmd := exec.Command("go", "build", "-o", binPath, ".")
   141  		buildCmd.Dir = serviceDir
   142  		buildOut, buildErr := buildCmd.CombinedOutput()
   143  		if buildErr != nil {
   144  			logFile.WriteString(string(buildOut))
   145  			logFile.Close()
   146  			fmt.Fprintf(os.Stderr, "failed to build %s: %v\n", serviceName, buildErr)
   147  			continue
   148  		}
   149  		cmd := exec.Command(binPath)
   150  		cmd.Dir = serviceDir
   151  		pr, pw := io.Pipe()
   152  		cmd.Stdout = pw
   153  		cmd.Stderr = pw
   154  		color := colorFor(i)
   155  		go func(name string, color string, pr *io.PipeReader, logFile *os.File) {
   156  			defer logFile.Close()
   157  			scanner := bufio.NewScanner(pr)
   158  			for scanner.Scan() {
   159  				line := scanner.Text()
   160  				// Write to terminal with color and service name
   161  				fmt.Printf("%s[%s]\033[0m %s\n", color, name, line)
   162  				// Write to log file with service name prefix
   163  				logFile.WriteString("[" + name + "] " + line + "\n")
   164  			}
   165  		}(serviceName, color, pr, logFile)
   166  		if err := cmd.Start(); err != nil {
   167  			fmt.Fprintf(os.Stderr, "failed to start service %s: %v\n", serviceName, err)
   168  			pw.Close()
   169  			continue
   170  		}
   171  		procs = append(procs, cmd)
   172  		pidFiles = append(pidFiles, pidFilePath)
   173  		os.WriteFile(pidFilePath, []byte(fmt.Sprintf("%d\n%s\n%s\n%s\n", cmd.Process.Pid, absServiceDir, serviceName, time.Now().Format(time.RFC3339))), 0644)
   174  	}
   175  	ch := make(chan os.Signal, 1)
   176  	signal.Notify(ch, os.Interrupt)
   177  	go func() {
   178  		<-ch
   179  		for _, proc := range procs {
   180  			if proc.Process != nil {
   181  				_ = proc.Process.Kill()
   182  			}
   183  		}
   184  		for _, pf := range pidFiles {
   185  			_ = os.Remove(pf)
   186  		}
   187  		os.Exit(1)
   188  	}()
   189  	for _, proc := range procs {
   190  		_ = proc.Wait()
   191  	}
   192  	return nil
   193  }
   194  
   195  // Add helpers for process check
   196  func parsePid(pidStr string) int {
   197  	pid, _ := strconv.Atoi(pidStr)
   198  	return pid
   199  }
   200  func processRunning(pidStr string) bool {
   201  	pid := parsePid(pidStr)
   202  	if pid <= 0 {
   203  		return false
   204  	}
   205  	proc, err := os.FindProcess(pid)
   206  	if err != nil {
   207  		return false
   208  	}
   209  	// On Unix, sending signal 0 checks if process exists
   210  	return proc.Signal(syscall.Signal(0)) == nil
   211  }
   212  
   213  func init() {
   214  	cmd.Register(&cli.Command{
   215  		Name:   "run",
   216  		Usage:  "Run all services in a directory",
   217  		Action: Run,
   218  		Flags: []cli.Flag{
   219  			&cli.StringFlag{
   220  				Name:    "address",
   221  				Aliases: []string{"a"},
   222  				Usage:   "Address to bind the micro web UI (default :8080)",
   223  				Value:   ":8080",
   224  			},
   225  		},
   226  	})
   227  }