Files
fun-cli/run.go
T
chiyi e29767362a feat: fun-cli v1.0.0 — gen/run/docs 三命令,fun 框架零改动
- fun gen [ts|go]:AST 扫描 BindService* 调用,临时生成 go test 辅助采集,
  经 fun v1.3.7 公开 API 生成,产物与内置 GenCode 字节级一致(回归测试锁定)
- fun run:热更新开发循环,先编译成功再切换,失败保留旧进程
- fun docs:浅色标准文档站——参数表/State 添加字段/请求 JSON 可编辑/
  响应格式化/NDJSON 流式逐行展示,反向代理 /cell 同源无 CORS
- 元数据:TS 产物两遍解析重建(枚举/可空/流式工厂/嵌套引用全支持)
- 纯标准库实现,零第三方依赖
2026-09-19 06:32:45 +08:00

197 lines
4.3 KiB
Go
Raw 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
// 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
}