// Package main provides the repository-local go run launcher behind `make run`. package main import ( "bufio" "context" "errors" "flag" "fmt" "io" "os" "os/exec" "os/signal" "path/filepath" "sort" "strconv" "strings" "sync" "syscall" "time" ) const stopTimeout = 8 * time.Second type serviceSpec struct { Name string Dir string Args []string Ports []int Color string } type serviceRunner struct { spec serviceSpec mu sync.Mutex cmd *exec.Cmd done chan struct{} stopping bool } var logMu sync.Mutex func main() { serviceList := flag.String("services", "all", "comma-separated services to run, or all") flag.Parse() root, err := os.Getwd() if err != nil { fatal(err) } specs, err := selectServices(*serviceList) if err != nil { fatal(err) } if len(specs) == 0 { fatal(errors.New("no services selected")) } ctx, stop := signalContext() defer stop() runners := make(map[string]*serviceRunner, len(specs)) for _, spec := range specs { runner := &serviceRunner{spec: spec} runners[spec.Name] = runner killPortOwners(spec) if err := runner.start(root); err != nil { logf(os.Stderr, spec, "start failed: %v", err) } } printPlain(os.Stdout, "dev-run started %d service(s): %s", len(specs), serviceNames(specs)) printPlain(os.Stdout, "press Ctrl-C to stop all services") <-ctx.Done() stopAll(runners) } func allServices() []serviceSpec { // 启动顺序按低层依赖到入口服务排列;gRPC dial 当前不阻塞,但这样能让日志更接近真实依赖链路。 return []serviceSpec{ appService("user-service", []int{13005, 13105}, "36"), appService("activity-service", []int{13006, 13106}, "35"), appService("wallet-service", []int{13004, 13104}, "33"), appService("lucky-gift-service", []int{13013, 13113}, "31"), appService("robot-service", []int{13011, 13111}, "93"), appService("game-service", []int{13008, 13108}, "32"), appService("room-service", []int{13001, 13101}, "34"), appService("notice-service", []int{13009, 13109}, "96"), appService("cron-service", []int{13007, 13107}, "95"), appService("gateway-service", []int{13000}, "92"), appService("statistics-service", []int{13010, 13110}, "94"), { Name: "luck-gateway", Dir: "server/luck-gateway", Args: []string{"run", "./cmd/server"}, Ports: []int{13014}, Color: "36", }, { Name: "admin", Dir: "server/admin", Args: []string{"run", "./cmd/server", "-config", "configs/config.yaml"}, Ports: []int{13100}, Color: "91", }, } } func appService(name string, ports []int, color string) serviceSpec { args := []string{"run", "./services/" + name + "/cmd/server", "-config", ""} if devRunMQEnabled() { args = []string{"run", "./services/" + name + "/cmd/server", "-config", "services/" + name + "/configs/config.yaml"} } return serviceSpec{ Name: name, Dir: ".", Args: args, Ports: ports, Color: color, } } func devRunMQEnabled() bool { value := strings.ToLower(strings.TrimSpace(os.Getenv("DEV_RUN_MQ"))) return value == "1" || value == "true" || value == "yes" } func selectServices(raw string) ([]serviceSpec, error) { all := allServices() if strings.TrimSpace(raw) == "" || strings.TrimSpace(raw) == "all" { return all, nil } byName := make(map[string]serviceSpec, len(all)) for _, spec := range all { byName[spec.Name] = spec } aliases := map[string]string{ "gateway": "gateway-service", "gs": "gateway-service", "room": "room-service", "rs": "room-service", "wallet": "wallet-service", "ws": "wallet-service", "user": "user-service", "us": "user-service", "activity": "activity-service", "as": "activity-service", "lucky": "lucky-gift-service", "lucky-gift": "lucky-gift-service", "lgs": "lucky-gift-service", "luckygift": "lucky-gift-service", "lg": "luck-gateway", "cron": "cron-service", "cs": "cron-service", "robot": "robot-service", "robots": "robot-service", "rb": "robot-service", "game": "game-service", "games": "game-service", "notice": "notice-service", "ns": "notice-service", "stats": "statistics-service", "statistics": "statistics-service", } var selected []serviceSpec seen := map[string]bool{} for _, token := range strings.Split(raw, ",") { token = strings.TrimSpace(token) if token == "" { continue } if canonical, ok := aliases[token]; ok { token = canonical } spec, ok := byName[token] if !ok { return nil, fmt.Errorf("unknown service %q", token) } if !seen[spec.Name] { selected = append(selected, spec) seen[spec.Name] = true } } return selected, nil } func (r *serviceRunner) start(repoRoot string) error { r.mu.Lock() defer r.mu.Unlock() cmd := exec.Command("go", r.spec.Args...) cmd.Dir = filepath.Join(repoRoot, r.spec.Dir) cmd.Env = append(os.Environ(), "TZ=UTC") // go run 会再托管一个编译后的子进程;独立进程组保证重启时能一起收到 SIGINT。 cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} stdout, err := cmd.StdoutPipe() if err != nil { return err } stderr, err := cmd.StderrPipe() if err != nil { return err } if err := cmd.Start(); err != nil { return err } r.cmd = cmd r.done = make(chan struct{}) r.stopping = false go prefixPipe(r.spec, stdout) go prefixPipe(r.spec, stderr) go r.wait(cmd, r.done) logf(os.Stdout, r.spec, "started: go %s", strings.Join(r.spec.Args, " ")) return nil } func (r *serviceRunner) stop() { r.mu.Lock() cmd := r.cmd done := r.done r.stopping = true r.mu.Unlock() if cmd == nil || cmd.Process == nil || done == nil { return } _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGINT) select { case <-done: case <-time.After(stopTimeout): _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) <-done } } func (r *serviceRunner) wait(cmd *exec.Cmd, done chan struct{}) { err := cmd.Wait() close(done) r.mu.Lock() stopping := r.stopping if r.cmd == cmd { r.cmd = nil r.done = nil r.stopping = false } r.mu.Unlock() if err != nil { if stopping { logf(os.Stdout, r.spec, "stopped") return } logf(os.Stderr, r.spec, "exited: %v", err) return } logf(os.Stdout, r.spec, "exited") } func stopAll(runners map[string]*serviceRunner) { names := make([]string, 0, len(runners)) for name := range runners { names = append(names, name) } sort.Strings(names) var wg sync.WaitGroup for _, name := range names { wg.Add(1) go func(runner *serviceRunner) { defer wg.Done() runner.stop() }(runners[name]) } wg.Wait() } func killPortOwners(spec serviceSpec) { seen := map[int]bool{} for _, port := range spec.Ports { if port <= 0 || seen[port] { continue } seen[port] = true pids, err := listeningPIDs(port) if err != nil { logf(os.Stderr, spec, "port %d lookup failed: %v", port, err) continue } if len(pids) == 0 { continue } for _, pid := range pids { // 本地开发启动以抢占端口为准:旧进程可能是 go run 托管的子进程,直接杀监听 PID 最可靠。 if err := syscall.Kill(pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) { logf(os.Stderr, spec, "kill pid %d on port %d failed: %v", pid, port, err) continue } logf(os.Stdout, spec, "killed pid %d occupying port %d", pid, port) } } } func listeningPIDs(port int) ([]int, error) { output, err := exec.Command("lsof", "-nP", "-tiTCP:"+strconv.Itoa(port), "-sTCP:LISTEN").Output() if err != nil { var exitErr *exec.ExitError if errors.As(err, &exitErr) && len(output) == 0 { return nil, nil } return nil, err } lines := strings.Fields(string(output)) pids := make([]int, 0, len(lines)) for _, line := range lines { pid, err := strconv.Atoi(strings.TrimSpace(line)) if err != nil { continue } pids = append(pids, pid) } return pids, nil } func prefixPipe(spec serviceSpec, reader io.Reader) { scanner := bufio.NewScanner(reader) buf := make([]byte, 0, 64*1024) scanner.Buffer(buf, 1024*1024) for scanner.Scan() { logf(os.Stdout, spec, "%s", scanner.Text()) } if err := scanner.Err(); err != nil { logf(os.Stderr, spec, "log pipe failed: %v", err) } } func logf(w io.Writer, spec serviceSpec, format string, args ...any) { line := fmt.Sprintf(format, args...) prefix := "[" + spec.Name + "]" if colorEnabled() && spec.Color != "" { prefix = "\x1b[" + spec.Color + "m" + prefix + "\x1b[0m" } printPlain(w, "%s %s", prefix, line) } func printPlain(w io.Writer, format string, args ...any) { logMu.Lock() defer logMu.Unlock() fmt.Fprintf(w, format+"\n", args...) } func colorEnabled() bool { if _, ok := os.LookupEnv("NO_COLOR"); ok { return false } return os.Getenv("TERM") != "dumb" } func serviceNames(specs []serviceSpec) string { names := make([]string, 0, len(specs)) for _, spec := range specs { names = append(names, spec.Name) } return strings.Join(names, ", ") } func signalContext() (context.Context, context.CancelFunc) { ctx, cancel := context.WithCancel(context.Background()) signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) go func() { <-signals cancel() }() return ctx, cancel } func fatal(err error) { fmt.Fprintln(os.Stderr, err) os.Exit(1) }