386 lines
9.2 KiB
Go
386 lines
9.2 KiB
Go
// 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)
|
||
}
|