386 lines
9.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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)
}