- docs 后端从 go run 改为 build+spawn(编译成功才切换,与 fun run 同语义) - watchLoop 复用:.go/go.mod/go.sum 变更→重采元数据+重启后端 - 新增 /fun-version 端点;文档页轮询版本变化自动刷新(编辑中推迟) - snapshot 跳过 fun_gen_helper_test.go,避免采集辅助文件自触发 - -from 静态模式不启用;-no-run 仍刷新元数据(后端由外部管理) Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
201 lines
4.5 KiB
Go
201 lines
4.5 KiB
Go
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 name == helperTestFile {
|
||
return nil
|
||
}
|
||
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
|
||
}
|