This commit is contained in:
tao.chen
2026-08-18 10:29:58 +08:00
parent 08bd0d0662
commit db586f6532
5 changed files with 400 additions and 226 deletions
+121
View File
@@ -0,0 +1,121 @@
package main
import (
"bufio"
"context"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"strings"
"sync"
"time"
)
// scriptTimeout 脚本执行超时上限(15 分钟)
const scriptTimeout = 15 * time.Minute
// shortCommit 安全地取 commit 短 ID,避免空值/短值切片越界 panic
func shortCommit(commitID string) string {
if len(commitID) > 7 {
return commitID[:7]
}
if commitID == "" {
return "unknown"
}
return commitID
}
// streamLog 逐行读取并实时打印脚本输出,直到 r 关闭
func streamLog(r io.Reader, tag 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 err != nil {
return
}
}
}
// notifyResult 发送执行结果通知
func notifyResult(ok bool, repoName, ref, commitID string, cost time.Duration, reason string) {
state := "成功"
if !ok {
state = "失败"
}
title := fmt.Sprintf("%s更新%s", repoName, state)
body := fmt.Sprintf("分支: %s\nCommit: %s\n耗时: %s\n原因: %s\n服务器已接收到 Push,更新%s",
ref, shortCommit(commitID), cost.Round(time.Second), reason, state)
if err := SendNotification(pushURL, globalConfig.DeviceKeys, title, body); err != nil {
log.Printf("[WARNING] 推送通知失败: %v", err)
return
}
log.Println("[INFO] 推送通知发送成功")
}
// runScript 异步执行 Shell 脚本,输出实时流式打印,超时 15 分钟
func runScript(scriptPath, repoName, ref, commitID string) {
start := time.Now()
tag := fmt.Sprintf("%s:%s", repoName, ref)
log.Printf("[INFO] 开始执行脚本: %s (仓库: %s, 分支: %s) 超时上限 %s",
scriptPath, repoName, ref, scriptTimeout)
ctx, cancel := context.WithTimeout(context.Background(), scriptTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, "bash", scriptPath)
// 超时/取消后最多再给 10 秒让进程优雅退出;超时则强制 SIGKILL
cmd.WaitDelay = 10 * time.Second
// 传入环境变量,脚本内可读取
cmd.Env = append(os.Environ(),
fmt.Sprintf("GIT_REPO_NAME=%s", repoName),
fmt.Sprintf("GIT_REF=%s", ref),
fmt.Sprintf("GIT_COMMIT_ID=%s", commitID),
)
// stdout / stderr 合并到同一个管道,保持原有交错顺序
pr, pw := io.Pipe()
cmd.Stdout = pw
cmd.Stderr = pw
if err := cmd.Start(); err != nil {
pw.Close()
pr.Close()
log.Printf("[ERROR] 脚本启动失败 [%s]: %v", scriptPath, err)
notifyResult(false, repoName, ref, commitID, time.Since(start), fmt.Sprintf("启动失败: %v", err))
return
}
var wg sync.WaitGroup
wg.Go(func() {
streamLog(pr, tag)
})
err := cmd.Wait() // 进程退出 + 内部 copy goroutine 结束
pw.Close() // 关闭写端,读端收到 EOF
wg.Wait() // 等日志全部刷完再往下走
cost := time.Since(start)
// 先判断超时(ctx.Err() 在超时/取消时非 nil
switch {
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, "执行超时,已强制终止")
return
case err != nil:
log.Printf("[ERROR] 脚本执行失败 [%s] 耗时 %s: %v", scriptPath, cost.Round(time.Millisecond), err)
notifyResult(false, repoName, ref, commitID, cost, err.Error())
return
}
log.Printf("[SUCCESS] 脚本执行成功 [%s] 耗时 %s", scriptPath, cost.Round(time.Millisecond))
notifyResult(true, repoName, ref, commitID, cost, "执行成功")
}