refactor: 入口移至 cmd/fun,go install 后命令名为 fun

This commit is contained in:
2026-09-19 12:16:22 +08:00
parent e29767362a
commit 49ca7f098a
20 changed files with 1 additions and 1 deletions
+196
View File
@@ -0,0 +1,196 @@
package main
// fun run:热更新开发循环——改代码自动重编译重启。
// 纯标准库实现:轮询 mtime 检测变更(400ms),先编译成功再切换进程,
// 编译失败保留旧进程继续跑
import (
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
)
func cmdRun(args []string) {
pkg := "."
var childArgs []string
for i := 0; i < len(args); i++ {
if args[i] == "-p" && i+1 < len(args) {
pkg = args[i+1]
i++
continue
}
if args[i] == "--" {
childArgs = append(childArgs, args[i+1:]...)
break
}
if strings.HasPrefix(args[i], "-p") && len(args[i]) > 2 {
pkg = args[i][2:]
continue
}
childArgs = append(childArgs, args[i])
}
binPath := filepath.Join(os.TempDir(), fmt.Sprintf("fun-run-%d", os.Getpid()))
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
var child *exec.Cmd
var childDone chan struct{}
restart := make(chan struct{}, 1)
// 初始编译
if !build(pkg, binPath) {
fmt.Println("fun: 初始编译失败,等待文件变更后重试")
} else {
child, childDone = spawn(binPath, childArgs)
}
go watchLoop(pkg, restart)
for {
select {
case <-sigs:
fmt.Println("\nfun: 退出")
killChild(child, childDone)
return
case <-restart:
fmt.Println("fun: 检测到变更,重新编译...")
if !build(pkg, binPath) {
fmt.Println("fun: 编译失败,旧进程继续运行")
continue
}
killChild(child, childDone)
child, childDone = spawn(binPath, childArgs)
}
}
}
func build(pkg, binPath string) bool {
cmd := goCommand("build", pkg, "-o", binPath)
cmd.Stdout, cmd.Stderr = os.Stderr, os.Stderr
return cmd.Run() == nil
}
func spawn(binPath string, args []string) (*exec.Cmd, chan struct{}) {
cmd := exec.Command(binPath, args...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
// 独立进程组:杀父进程时连带子进程(业务可能 fork 了 worker)
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
done := make(chan struct{})
if err := cmd.Start(); err != nil {
fmt.Fprintln(os.Stderr, "fun: 启动失败:", err)
close(done)
return nil, done
}
go func() {
if err := cmd.Wait(); err != nil {
fmt.Fprintf(os.Stderr, "fun: 进程退出: %v\n", err)
}
close(done)
}()
return cmd, done
}
func killChild(child *exec.Cmd, done chan struct{}) {
if child == nil || child.Process == nil {
return
}
syscall.Kill(-child.Process.Pid, syscall.SIGTERM)
if done == nil {
done = make(chan struct{})
go func() { child.Wait(); close(done) }()
}
select {
case <-done:
case <-time.After(3 * time.Second):
syscall.Kill(-child.Process.Pid, syscall.SIGKILL)
}
}
var skipDirs = map[string]bool{
".git": true, "vendor": true, "node_modules": true, ".fun": true,
"gen": true, "testdata": true,
}
// snapshot 收集监控文件的 (路径, mtime)
func snapshot(root string) map[string]int64 {
out := map[string]int64{}
filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
name := d.Name()
if d.IsDir() {
if path != root && skipDirs[name] {
return filepath.SkipDir
}
return nil
}
ext := filepath.Ext(name)
if ext == ".go" || name == "go.mod" || name == "go.sum" {
if info, err := d.Info(); err == nil {
out[path] = info.ModTime().UnixNano()
}
}
return nil
})
return out
}
func diffSnapshot(a, b map[string]int64) bool {
if len(a) != len(b) {
return true
}
for k, v := range a {
if b[k] != v {
return true
}
}
return false
}
func watchLoop(root string, changed chan struct{}) {
last := snapshot(root)
var pending bool
var pendingAt time.Time
for {
time.Sleep(400 * time.Millisecond)
cur := snapshot(root)
if !diffSnapshot(last, cur) {
// 静默 300ms 后触发(编辑器分多次写盘时合并)
if pending && time.Since(pendingAt) > 300*time.Millisecond {
pending = false
select {
case changed <- struct{}{}:
default:
}
}
last = cur
continue
}
last = cur
if !pending {
pending = true
pendingAt = time.Now()
} else {
pendingAt = time.Now()
}
}
}
// sortedKeys 调试用(保留给未来扩展)
func sortedKeys(m map[string]int64) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}