feat: dashboard
This commit is contained in:
@@ -14,7 +14,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// scriptTimeout 脚本执行超时上限(15 分钟)
|
||||
// scriptTimeout 脚本执行超时上限(60 分钟)
|
||||
const scriptTimeout = 60 * time.Minute
|
||||
|
||||
// buildTask 单次构建任务的快照,handler 入队时构造。
|
||||
@@ -44,14 +44,25 @@ var (
|
||||
func enqueueBuildTask(t buildTask) {
|
||||
select {
|
||||
case buildCh <- t:
|
||||
return // 入队成功
|
||||
// 入队成功后再登记到 dashboard,保证 channel 与列表一致
|
||||
if dashboard != nil {
|
||||
dashboard.AddPending(t)
|
||||
}
|
||||
return
|
||||
default:
|
||||
// 队列已满,丢弃旧 task
|
||||
select {
|
||||
case <-buildCh:
|
||||
if dashboard != nil {
|
||||
// 同步移除最旧一条,避免 pending 列表与 channel 错位
|
||||
dashboard.RemovePendingAndPeek(t)
|
||||
}
|
||||
default:
|
||||
}
|
||||
buildCh <- t // 阻塞不会发生,因为我们是唯一发送方
|
||||
if dashboard != nil {
|
||||
dashboard.AddPending(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +74,10 @@ func startBuildWorker() {
|
||||
// consumeBuild worker 主循环;取出 task,按冷却规则执行或跳过
|
||||
func consumeBuild() {
|
||||
for task := range buildCh {
|
||||
if dashboard != nil {
|
||||
dashboard.RemovePendingAndPeek(task)
|
||||
}
|
||||
|
||||
lockKey := fmt.Sprintf("%s:%s", task.repoName, task.ref)
|
||||
entryVal, _ := lastExecMap.LoadOrStore(lockKey, &debounceEntry{})
|
||||
entry := entryVal.(*debounceEntry)
|
||||
@@ -77,6 +92,13 @@ func consumeBuild() {
|
||||
entry.mu.Unlock()
|
||||
log.Printf("[SKIP] 3 分钟冷却中,跳过构建 [%s] commit %s 剩余 %d 秒\n",
|
||||
lockKey, shortCommit(task.commitID), remainingSeconds)
|
||||
// 冷却跳过也写入历史,方便追踪
|
||||
if dashboard != nil {
|
||||
dashboard.StartExecution(task)
|
||||
dashboard.FinishExecution(task, statusSkipped,
|
||||
fmt.Sprintf("冷却中,剩余 %d 秒", remainingSeconds),
|
||||
time.Duration(0))
|
||||
}
|
||||
continue
|
||||
}
|
||||
entry.lastExec = now
|
||||
@@ -100,12 +122,16 @@ func shortCommit(commitID string) string {
|
||||
}
|
||||
|
||||
// streamLog 逐行读取并实时打印脚本输出,直到 r 关闭
|
||||
func streamLog(r io.Reader, tag string) {
|
||||
// onLine 在每行非空时回调一次(携带换行符)
|
||||
func streamLog(r io.Reader, tag string, onLine func(line string)) {
|
||||
br := bufio.NewReader(r)
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if line != "" {
|
||||
log.Printf("[SCRIPT %s] %s", tag, strings.TrimRight(line, "\r\n"))
|
||||
if onLine != nil {
|
||||
onLine(line)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
@@ -130,13 +156,24 @@ func notifyResult(ok bool, repoName, ref, commitID string, cost time.Duration, r
|
||||
log.Println("[INFO] 推送通知发送成功")
|
||||
}
|
||||
|
||||
// runScript 异步执行 Shell 脚本,输出实时流式打印,超时 15 分钟
|
||||
// runScript 异步执行 Shell 脚本,输出实时流式打印,超时上限 scriptTimeout
|
||||
func runScript(scriptPath, repoName, ref, commitID string) {
|
||||
task := buildTask{
|
||||
scriptPath: scriptPath,
|
||||
repoName: repoName,
|
||||
ref: ref,
|
||||
commitID: commitID,
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
tag := fmt.Sprintf("%s:%s", repoName, ref)
|
||||
log.Printf("[INFO] 开始执行脚本: %s (仓库: %s, 分支: %s) 超时上限 %s",
|
||||
scriptPath, repoName, ref, scriptTimeout)
|
||||
|
||||
if dashboard != nil {
|
||||
dashboard.StartExecution(task)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), scriptTimeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -161,12 +198,20 @@ func runScript(scriptPath, repoName, ref, commitID string) {
|
||||
pr.Close()
|
||||
log.Printf("[ERROR] 脚本启动失败 [%s]: %v", scriptPath, err)
|
||||
notifyResult(false, repoName, ref, commitID, time.Since(start), fmt.Sprintf("启动失败: %v", err))
|
||||
if dashboard != nil {
|
||||
dashboard.FinishExecution(task, statusFailed,
|
||||
fmt.Sprintf("启动失败: %v", err), time.Since(start))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(func() {
|
||||
streamLog(pr, tag)
|
||||
streamLog(pr, tag, func(line string) {
|
||||
if dashboard != nil {
|
||||
dashboard.AppendLog(line)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
err := cmd.Wait() // 进程退出 + 内部 copy goroutine 结束
|
||||
@@ -180,13 +225,22 @@ func runScript(scriptPath, repoName, ref, commitID string) {
|
||||
case errors.Is(ctx.Err(), context.DeadlineExceeded):
|
||||
log.Printf("[TIMEOUT] 脚本执行超时 [%s] 已运行 %s 上限 %s", scriptPath, cost.Round(time.Second), scriptTimeout)
|
||||
notifyResult(false, repoName, ref, commitID, cost, "执行超时,已强制终止")
|
||||
if dashboard != nil {
|
||||
dashboard.FinishExecution(task, statusTimeout, "执行超时,已强制终止", cost)
|
||||
}
|
||||
return
|
||||
case err != nil:
|
||||
log.Printf("[ERROR] 脚本执行失败 [%s] 耗时 %s: %v", scriptPath, cost.Round(time.Millisecond), err)
|
||||
notifyResult(false, repoName, ref, commitID, cost, err.Error())
|
||||
if dashboard != nil {
|
||||
dashboard.FinishExecution(task, statusFailed, err.Error(), cost)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[SUCCESS] 脚本执行成功 [%s] 耗时 %s", scriptPath, cost.Round(time.Millisecond))
|
||||
notifyResult(true, repoName, ref, commitID, cost, "执行成功")
|
||||
if dashboard != nil {
|
||||
dashboard.FinishExecution(task, statusSuccess, "执行成功", cost)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user