"""定时更新机制(每日 4 次,默认整点 00:00/06:00/12:00/18:00)
每轮自动执行三件事:
1. 框架自动更新:git pull --rebase --autostash 拉取 Agent 框架仓
(https://gitcode.com/Alice_zhou/Vector_fuzz_cpp_testing,含 AGENTS.md/scripts/skills/
turing_sec)——多台压测机各自定时拉取,保持框架版本一致;本地未提交改动由
autostash 自动保护;**压测进程(aclnnAPI*)运行中自动跳过本轮框架更新**(防止
中断在跑任务/编译产物被触碰),等待下一轮;turing_sec 若为独立克隆仓(含自己的
.git)同样 pull
2. 结果表格兜底同步:补推 out_res/批量压测结果.xlsx 至 GitCode 仓 Alice_zhou/issue
(多机安全合并,同 sync_batch_results_git.py;实时同步失败/漏推的兜底,
文件不存在则静默跳过)
3. 每日 issue 在线表格兜底同步:补推当日已提 issue 汇总表(out_res/issues/{今日}/,
表格含 aclnn接口名/测试场景/问题/issue链接)至 GitCode 仓 issue日报_{今日}.xlsx
(同 sync_daily_issues_git.py;提单后实时同步失败/漏推的兜底,当日无提单记录则静默跳过)
4. 问题统计目录兜底同步:补推 问题统计/接口压测结果与提单汇总.xlsx(每次压测结果依次填充,
三态着色 pass绿/待复核黄/fail红)与 问题统计/issue汇总.xlsx(同 sync_issue_stats_git.py;
实时同步失败/漏推的兜底,无数据则静默跳过)
安装(一次性,执行机上):
python scripts/scheduled_update.py --install-cron # Linux: crontab;Windows: 打印 schtasks(管理员执行)
python scripts/scheduled_update.py --install-cron --hours 0,6,12,18 # 自定义整点
python scripts/scheduled_update.py --uninstall-cron # 移除定时任务
手动执行(定时任务入口,也可立即跑一轮):
python scripts/scheduled_update.py
日志: out_res/scheduled_update.log(追加式;stdout 同步输出,cron 重定向亦写入该文件)
"""
import argparse
import contextlib
import os
import socket
import subprocess
import sys
import time
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.dirname(SCRIPTS_DIR)
LOG_PATH = os.path.join(PROJECT_ROOT, "out_res", "scheduled_update.log")
DEFAULT_HOURS = "0,6,12,18"
CRON_MARKER = "scheduled_update.py"
WIN_TASK_PREFIX = "FuzzScheduledUpdate"
def log(msg):
line = "[{}] [{}] {}".format(time.strftime("%Y-%m-%d %H:%M:%S"), socket.gethostname(), msg)
print(line)
with contextlib.suppress(Exception):
os.makedirs(os.path.dirname(LOG_PATH), exist_ok=True)
with open(LOG_PATH, "a", encoding="utf-8") as fh:
fh.write(line + "\n")
def fuzz_processes_running():
"""检测是否有压测进程在跑(aclnnAPI* 可执行/样例执行器),检测失败不阻塞更新。"""
try:
if os.name == "posix":
r = subprocess.run(["pgrep", "-af", "aclnnAPI"],
capture_output=True, text=True, timeout=30)
return r.returncode == 0 and r.stdout.strip() != ""
r = subprocess.run(["tasklist", "/FO", "CSV"],
capture_output=True, text=True, timeout=30)
return any("aclnnAPI" in line for line in r.stdout.splitlines())
except Exception:
return False
def git_pull(repo_path):
"""git pull --rebase --autostash(本地未提交改动自动 stash 保护),返回 (ok, 摘要)。"""
try:
r = subprocess.run(["git", "-C", repo_path, "pull", "--rebase", "--autostash"],
capture_output=True, text=True, timeout=600)
out = ((r.stdout or "") + (r.stderr or "")).strip()
lines = [ln for ln in out.splitlines() if ln.strip()]
summary = " | ".join(lines[-3:]) if lines else "no output"
return r.returncode == 0, summary
except Exception as exc:
return False, str(exc)
def update_framework(root):
"""步骤1:框架自动更新(agent 仓 + 独立克隆的 turing_sec 仓)。"""
if fuzz_processes_running():
log("SKIP 框架更新:检测到压测进程(aclnnAPI*)运行中,本轮跳过,等待下一轮")
return
ok, summary = git_pull(root)
log("框架仓 git pull {}: {}".format("OK" if ok else "FAIL", summary))
if not ok:
return
tsec = os.path.join(root, "turing_sec")
if os.path.isdir(os.path.join(tsec, ".git")):
ok2, summary2 = git_pull(tsec)
log("turing_sec git pull {}: {}".format("OK" if ok2 else "FAIL", summary2))
def push_results_fallback(root):
"""步骤2:批量压测结果.xlsx 定时兜底推送 GitCode 仓(Alice_zhou/issue)。"""
xlsx = os.path.join(root, "out_res", "批量压测结果.xlsx")
if not os.path.isfile(xlsx):
log("SKIP 结果兜底同步:out_res/批量压测结果.xlsx 不存在")
return
try:
sys.path.insert(0, SCRIPTS_DIR)
from sync_batch_results_git import sync_local_xlsx
ok, url, detail = sync_local_xlsx(
xlsx, commit_msg="results: scheduled fallback sync from {}".format(socket.gethostname()))
if ok:
log("结果表格兜底同步 OK: {} ({})".format(url or "Alice_zhou/issue", detail))
else:
log("结果表格兜底同步 FAIL(不阻塞,下轮重试): {}".format(detail))
except Exception as exc:
log("结果表格兜底同步异常(不阻塞,下轮重试): {}".format(exc))
def push_daily_issues_fallback(root):
"""步骤3:当日 issue 在线表格兜底推送(out_res/issues/{今日}/ → GitCode 仓 issue日报_{今日}.xlsx)。
当日无提单记录(daily_issues.json/filed.json 均无)时静默跳过;失败不阻塞(下轮重试)。"""
date_str = time.strftime("%Y-%m-%d")
day_dir = os.path.join(root, "out_res", "issues", date_str)
if not os.path.isdir(day_dir):
return
if not any(os.path.isfile(os.path.join(day_dir, n)) for n in ("daily_issues.json", "filed.json")):
return
try:
sys.path.insert(0, SCRIPTS_DIR)
from sync_daily_issues_git import sync_daily_issues
ok, url, detail = sync_daily_issues(date_str)
if ok and url:
log("每日issue表格兜底同步 OK: {} ({})".format(url, detail))
elif ok:
log("SKIP 每日issue表格兜底同步: {}".format(detail))
else:
log("每日issue表格兜底同步 FAIL(不阻塞,下轮重试): {}".format(detail))
except Exception as exc:
log("每日issue表格兜底同步异常(不阻塞,下轮重试): {}".format(exc))
def push_issue_stats_fallback(root):
"""步骤4:问题统计目录兜底推送(out_res/问题统计/ 两表 → GitCode 仓 问题统计/)。
接口压测结果与提单汇总.xlsx(数据源本地 out_res/批量压测结果.xlsx)+ issue汇总.xlsx
(数据源 out_res/issues/);无数据/文件缺失静默跳过;失败不阻塞(下轮重试)。"""
xlsx = os.path.join(root, "out_res", "批量压测结果.xlsx")
if not os.path.isfile(xlsx):
log("SKIP 问题统计兜底同步:out_res/批量压测结果.xlsx 不存在")
return
try:
sys.path.insert(0, SCRIPTS_DIR)
from sync_issue_stats_git import issue_stats_enabled, sync_api_summary, sync_issue_summary
enabled, why = issue_stats_enabled()
if not enabled:
log("SKIP 问题统计兜底同步: 开关关闭({})".format(why))
return
ok1, url1, detail1 = sync_api_summary(xlsx)
if ok1 and url1:
log("问题统计-接口压测结果与提单汇总 兜底同步 OK: {} ({})".format(url1, detail1))
elif not ok1:
log("问题统计-接口压测结果与提单汇总 兜底同步 FAIL(不阻塞,下轮重试): {}".format(detail1))
ok2, url2, detail2 = sync_issue_summary()
if ok2 and url2:
log("问题统计-issue汇总 兜底同步 OK: {} ({})".format(url2, detail2))
elif not ok2:
log("问题统计-issue汇总 兜底同步 FAIL(不阻塞,下轮重试): {}".format(detail2))
except Exception as exc:
log("问题统计兜底同步异常(不阻塞,下轮重试): {}".format(exc))
def run_once(root):
log("========== 定时更新开始(每日 {} 整点轮) ==========".format(DEFAULT_HOURS))
update_framework(root)
push_results_fallback(root)
push_daily_issues_fallback(root)
push_issue_stats_fallback(root)
log("========== 定时更新结束 ==========")
return 0
def parse_hours(hours_str):
hours = []
for part in str(hours_str).split(","):
part = part.strip()
if not part:
continue
try:
h = int(part)
except ValueError:
raise SystemExit("[ERROR] 非法整点值: {}(格式如 0,6,12,18)".format(part))
if not 0 <= h <= 23:
raise SystemExit("[ERROR] 整点值越界: {}(0~23)".format(h))
hours.append(h)
if not hours:
raise SystemExit("[ERROR] 至少指定一个整点(如 --hours 0,6,12,18)")
return sorted(set(hours))
def install_cron(root, hours):
if os.name == "posix":
cron_line = "0 {} * * * cd {} && python3 scripts/scheduled_update.py >> out_res/scheduled_update.log 2>&1".format(
",".join(str(h) for h in hours), root)
current = os.popen("crontab -l 2>/dev/null").read()
if CRON_MARKER in current:
print("[CRON] crontab 已包含 scheduled_update 定时任务,跳过安装")
return 0
new_crontab = (current.rstrip("\n") + "\n" if current.strip() else "") + cron_line + "\n"
os.system("echo '{}' | crontab -".format(new_crontab.replace("'", "'\\''")))
print("[CRON] 已安装每日 {} 整点定时更新任务(共 {} 轮): {}".format(
"/".join("{:02d}:00".format(h) for h in hours), len(hours), cron_line))
return 0
print("[CRON] Windows 环境请以管理员身份执行以下命令安装计划任务(每日 {} 轮):".format(len(hours)))
for h in hours:
print(' schtasks /Create /TN "{}_{:02d}" /SC DAILY /ST {:02d}:00 /TR '
'"cmd /c cd /d {} && python scripts\\scheduled_update.py >> out_res\\scheduled_update.log 2>&1" /F'
.format(WIN_TASK_PREFIX, h, h, root))
return 0
def uninstall_cron(hours):
if os.name == "posix":
current = os.popen("crontab -l 2>/dev/null").read()
kept = [ln for ln in current.splitlines() if CRON_MARKER not in ln]
os.system("printf '%s\\n' '{}' | crontab -".format("\\n".join(kept).replace("'", "'\\''")))
print("[CRON] 已移除 scheduled_update 定时任务")
return 0
print("[CRON] Windows 环境请执行:")
for h in hours:
print(' schtasks /Delete /TN "{}_{:02d}" /F'.format(WIN_TASK_PREFIX, h))
return 0
def main():
parser = argparse.ArgumentParser(
description="定时更新机制(每日 4 次整点:框架仓 git pull 自动更新 + 批量压测结果.xlsx 兜底推送 GitCode 仓)")
parser.add_argument("--hours", default=DEFAULT_HOURS,
help="每日执行的整点列表,默认 0,6,12,18(每日 4 次)")
parser.add_argument("--install-cron", action="store_true",
help="安装定时任务(Linux: crontab / Windows: 打印 schtasks 命令)后退出")
parser.add_argument("--uninstall-cron", action="store_true", help="移除定时任务后退出")
args = parser.parse_args()
hours = parse_hours(args.hours)
if args.install_cron:
return install_cron(PROJECT_ROOT, hours)
if args.uninstall_cron:
return uninstall_cron(hours)
return run_once(PROJECT_ROOT)
if __name__ == "__main__":
sys.exit(main())