Files
DCTS/scripts/push_import_results.sh
T
fmq c8fd24b120 feat(all): 科学计算正确性修复、调度竞态消除、节点优雅退出、安全加固与收敛分析重构
科学计算正确性:
- 修复 Fortran 无-E 科学记数法(指数≥100 时 E 被挤掉,如 -1.35+118)导致
  发散行被静默跳过、误判收敛的 bug;扩展大气无效检测覆盖 Inf 与 *** 溢出标记
- 种子匹配改为 CNO 有向距离(富金属方向重罚 4×、贫金属方向轻罚 1×),
  基于 1191 个真实种子配对回测标定,回测净改善 314 个点
- GridAxisValue 反序列化拒绝非法文本(不再静默 NaN);chmax≤0 显式报错
- ions 行宽列宽对齐真实 fort.5 格式

调度与队列竞态:
- 原子选点(IMMEDIATE 事务 SELECT+UPDATE)消除并发调度重复派发 (#5)
- 调度互斥锁 + 冷启动优先策略(SeedStep 仅作失败后救援,不再正常路径热启动)
- 毒消息 dead_letter 标记防出队死循环;clear_queue 保留 claimed 行 (#6)
- 孤儿 running 点回收兜底;stale_sec 默认 7800→21600s(3 倍超时缓冲)

节点生命周期:
- SIGTERM+SIGINT 双信号监听(修复 Docker stop 发 SIGTERM 不触发优雅退出)
- SlotGuard RAII 防活动 slot 泄漏;子进程超时增加二级 30s wait 防 Fortran hang
- SeedStep 种子下载 fail-fast + 沙盒私有副本解耦 LRU 清理竞争
- reqwest Client 增加连接/请求超时;启动清理残留 task_* 沙盒

安全加固:
- 节点注册 registration_secret 二次凭据 (H8),恒定时间比对防时序旁路
- token 缓存 generation 机制消除 reissue 后旧 token TOCTOU 复活窗口
- 新增 /api/auth/logout 服务端 session 即时撤销;fail-closed 鉴权启动策略
- 前端 token 迁移 sessionStorage;YAML 高亮改 DOM API 消除 XSS 注入面
- 备份文件权限收紧 0600;点表动态值全面 escapeHtml

前端 Dashboard:
- 收敛性分析从热力图重构为 Parallel Sets 平行集合图(6 维+状态轴,手写 SVG 零依赖)
- 进度曲线横轴改为真实时间(服务端 now 锚定,停滞期诚实留白);轮询指数退避
- 移除 imported 收敛途径分类,导入点按实际途径 cold_run/seed_step 归类
- 初始化时 /api/auth/check 校验 token;401 toast 提示替代静默 reload

服务端恢复与工具链:
- 启动恢复 initializing 态工作流;默认工作流 INSERT-only 不覆盖 API 编辑
- body limit 分层(10MB 不再截断 256MB report);multipart 显式错误处理
- 嵌入二进制原子写(tmp+rename)防半写损坏
- import_results 判定收敛途径透传 success_method;conv.json 格式对齐本项目
- push_import_results.sh 退出码修复 + .bat UTF-8 BOM + scp 上传
- Docker USE_MIRRORS 默认关闭;移除无用 assets 挂载;删除 hosts.ini 入库
2026-08-01 17:01:40 +08:00

329 lines
16 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
# ==============================================================================
# push_import_results.sh — 推送 import_results 工具到 Windows 计算节点并编译
# ==============================================================================
# 把 import_results 工具的源码推送到 deploy.env.d/<profile> 指定的 Windows 节点,
# 在该节点原生编译(Windows 上 cargo build),并打印确切的运行命令。
#
# 设计前提:
# - 旧版 Python run_grid.py 的结果在 Windows 机的 E:/fmq/grid(工具必须在该机运行才能读到)
# - 该机已部署 nodeDocker Compose),data 目录在 E:/fmq/dcts/data
# - DCTS 服务端地址来自 profile 的 DCTS_SERVER_URL
# - Windows 机需已装 Rust 工具链(cargo/rustc);脚本会自检并给出安装提示
#
# 用法:
# ./scripts/push_import_results.sh # 默认用 node-dckj-win-01.env
# ./scripts/push_import_results.sh -f deploy.env.d/other-node.env # 指定其他 profile
# ./scripts/push_import_results.sh --no-build # 只推源码不编译(远端已有缓存时加速)
# ./scripts/push_import_results.sh --archive-dir E:/fmq/dcts/data/result # 覆盖归档目录
#
# 运行(推完编译后,在 Windows 机上执行,或本脚本末尾会打印):
# E:\fmq\dcts-src\target\release\import_results.exe \
# -d E:/fmq/grid -c E:/fmq/dcts-src/workflows/sdB_cno.yaml \
# -r E:/fmq/dcts/data/result -s http://100.66.1.2:8090 \
# -w sdB_cno -t <admin_token>
# ==============================================================================
set -eo pipefail
GREEN='\033[0;32m'
BLUE='\033[0;34m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${WORK_DIR}"
# 默认值(可被 profile 覆盖)
PROFILE_FILE="deploy.env.d/node-dckj-win-01.env"
REMOTE_USER="fmq"
REMOTE_IP="192.168.6.102"
REMOTE_PORT="22"
REMOTE_SRC_DIR="E:/fmq/dcts-src"
DCTS_SERVER_URL="http://100.66.1.2:8090"
RESULT_DIR="" # 空 → 用 profile 推导(默认 E:/fmq/dcts/data/result
DO_BUILD=true
# SSH ControlMaster 复用目录(与 deploy.sh 一致,便于共享连接)
SSH_CONTROL_DIR="${SSH_CONTROL_DIR:-$HOME/.ssh/cm}"
# =============================================================================
# 解析命令行参数
# =============================================================================
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--profile) PROFILE_FILE="$2"; shift 2 ;;
--no-build) DO_BUILD=false; shift ;;
--archive-dir) RESULT_DIR="$2"; shift 2 ;;
-h|--help)
sed -n '2,30p' "$0"
exit 0 ;;
*) echo -e "${RED}未知参数: $1${NC}"; exit 1 ;;
esac
done
# =============================================================================
# 加载 profile(SSH 连接信息 + 服务端地址)
# =============================================================================
if [ ! -f "$PROFILE_FILE" ]; then
echo -e "${RED}错误: 找不到 profile '${PROFILE_FILE}'${NC}"
exit 1
fi
echo -e "${CYAN}[i] 加载 profile: ${PROFILE_FILE}${NC}"
# shellcheck disable=SC1090
set -a; source "$PROFILE_FILE"; set +a
# profile 提供的变量优先(REMOTE_* / DCTS_SERVER_URL
[ -n "${REMOTE_USER:-}" ] && REMOTE_USER="${REMOTE_USER}"
[ -n "${REMOTE_IP:-}" ] && REMOTE_IP="${REMOTE_IP}"
[ -n "${REMOTE_PORT:-}" ] && REMOTE_PORT="${REMOTE_PORT}"
# 源码目录默认放到部署目录旁(E:/fmq/dcts-src),与运行中的 nodeE:/fmq/dcts)隔离
[ -n "${REMOTE_DIR:-}" ] && REMOTE_SRC_DIR="${REMOTE_DIR}-src"
# 结果目录:命令行 > 参数 > 默认(部署目录下的 data/resultnode 的 DCTS_RESULT_DIR 卷映射到此)
if [ -z "${RESULT_DIR}" ]; then
RESULT_DIR="${REMOTE_DIR}/data/result"
fi
echo -e "${CYAN}📢 目标节点: ${YELLOW}${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PORT}${NC}"
echo -e "${CYAN}📢 源码推送: ${YELLOW}${REMOTE_SRC_DIR}${NC}"
echo -e "${CYAN}📢 结果目录: ${YELLOW}${RESULT_DIR}${NC}"
echo -e "${CYAN}📢 服务端: ${YELLOW}${DCTS_SERVER_URL}${NC}"
echo ""
# =============================================================================
# 建立 SSH ControlMaster 连接复用(一次性输密码,后续 scp/ssh 复用)
# =============================================================================
mkdir -p "${SSH_CONTROL_DIR}"
SOCK="${SSH_CONTROL_DIR}/${REMOTE_USER}@${REMOTE_IP}:${REMOTE_PORT}"
SSH_OPTS="-o ControlMaster=auto -o ControlPath=${SOCK} -o ControlPersist=1800 -o StrictHostKeyChecking=no"
if ssh -p "${REMOTE_PORT}" -o ControlPath="${SOCK}" -O check "${REMOTE_USER}@${REMOTE_IP}" >/dev/null 2>&1; then
echo -e "${GREEN}[√] 复用已有 SSH 连接${NC}"
else
echo -e "${CYAN}=== 建立 SSH 连接(请输入 ${REMOTE_USER}@${REMOTE_IP} 的密码)===${NC}"
if ! ssh -p "${REMOTE_PORT}" ${SSH_OPTS} -fN "${REMOTE_USER}@${REMOTE_IP}"; then
echo -e "${RED}SSH 连接失败${NC}"
exit 1
fi
echo -e "${GREEN}[√] SSH 连接已建立${NC}"
fi
trap 'ssh -p "${REMOTE_PORT}" -o ControlPath="${SOCK}" "${REMOTE_USER}@${REMOTE_IP}" -O exit 2>/dev/null || true' EXIT
# =============================================================================
# 自检远端 Rust 工具链
# =============================================================================
echo -e "\n${CYAN}=== 检查远端 Rust 工具链 ===${NC}"
HAS_CARGO=$(ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" \
"powershell -NoProfile -Command \"if (Get-Command cargo -ErrorAction SilentlyContinue) { 'yes' } else { 'no' }\"" 2>/dev/null | tr -d '[:space:]')
if [ "$HAS_CARGO" != "yes" ]; then
echo -e "${RED}[✗] 远端 ${REMOTE_IP} 未检测到 cargo/rustc${NC}"
echo -e "${YELLOW}请先在该 Windows 机上安装 Rust 工具链:${NC}"
echo -e " 方式1PowerShell,需联网): winget install Rustlang.Rustup"
echo -e " 方式2(浏览器): 访问 https://rustup.rs 下载 rustup-init.exe 并运行"
echo -e " 安装后重开 PowerShell 再跑本脚本。"
exit 1
fi
echo -e "${GREEN}[√] 远端已安装 Rust 工具链${NC}"
# =============================================================================
# 同步源码到远端(仅 git 跟踪文件,排除 target/data/assets 等大目录)
# =============================================================================
echo -e "\n${CYAN}=== 同步源码到 ${REMOTE_SRC_DIR} ===${NC}"
# 远端先建目录
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" \
"powershell -NoProfile -Command \"New-Item -ItemType Directory -Force -Path '${REMOTE_SRC_DIR}' | Out-Null\""
# 单文件 scp 辅助:失败时打印诊断并退出(不静默吞错)。
# Windows OpenSSH scp 对带正斜杠的远端路径需用 ':' 分隔主机,路径部分正斜杠 OK。
scp_one() {
local src="$1" dst="$2"
if ! scp -P "${REMOTE_PORT}" ${SSH_OPTS} "$src" "${REMOTE_USER}@${REMOTE_IP}:${dst}"; then
echo -e "${RED}[✗] 传输失败: ${src} -> ${dst}${NC}"
exit 1
fi
}
# 递归目录 scp:先建/清远端目标目录再传,避免 Windows scp -r 创建子目录失败。
scp_dir() {
local sub="$1"
[ -d "$sub" ] || return 0
# 远端先删除旧目录并重建空目录(确保 scp -r 能写入)
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" \
"powershell -NoProfile -Command \"Remove-Item -Recurse -Force '${REMOTE_SRC_DIR}/${sub}' -ErrorAction SilentlyContinue; New-Item -ItemType Directory -Force -Path '${REMOTE_SRC_DIR}/${sub}' | Out-Null\"" || true
echo -e " -> ${sub}/"
if ! scp -P "${REMOTE_PORT}" ${SSH_OPTS} -r "$sub" "${REMOTE_USER}@${REMOTE_IP}:${REMOTE_SRC_DIR}/"; then
echo -e "${RED}[✗] 传输目录失败: ${sub}${NC}"
echo -e "${YELLOW} 若反复失败,可在 Windows 机上手动 git clone 后仅同步增量,或检查磁盘空间/权限${NC}"
exit 1
fi
}
echo -e "${CYAN} -> 传输顶层文件 + crates/tools/workflows ...${NC}"
scp_one Cargo.toml "${REMOTE_SRC_DIR}/"
[ -f Cargo.lock ] && scp_one Cargo.lock "${REMOTE_SRC_DIR}/"
# import_results 依赖 common crate,二者都要同步;workflows 含配置(决定命名精度)。
scp_dir crates
scp_dir tools
scp_dir workflows
echo -e "${GREEN}[√] 源码同步完成${NC}"
# =============================================================================
# 远端编译 import_resultsrelease
# =============================================================================
if [ "$DO_BUILD" = true ]; then
echo -e "\n${CYAN}=== 远端编译 import_results (release) ===${NC}"
echo -e "${YELLOW} (首次编译会拉 crate 依赖,可能耗时数分钟,请耐心等待...)${NC}"
# 用 ssh 透传的退出码判断成败,不依赖输出最后一行文本:
# cargo 把 Compiling/Finished 进度写到 stderr,与 PowerShell 打印 $LASTEXITCODE 的 stdout
# 在 2>&1 合并后顺序会交错,tail -1 可能抓到 "Finished ..." 而非 "0",导致把成功误判成失败。
# 这里让 powershell `exit $LASTEXITCODE` → ssh 透传 → ${PIPESTATUS[0]} 取管道首段退出码。
# 暂时关掉 errexit/pipefail:否则 cargo 失败时管道整体非零会先触发 set -e 退出,丢失自定义提示。
set +e +o pipefail
ssh -p "${REMOTE_PORT}" ${SSH_OPTS} "${REMOTE_USER}@${REMOTE_IP}" \
"powershell -NoProfile -Command \"Set-Location '${REMOTE_SRC_DIR}'; cargo build --release -p import_results; exit \$LASTEXITCODE\"" 2>&1 | tee /tmp/import_build.log | tail -20
BUILD_RC=${PIPESTATUS[0]}
set -e -o pipefail
rm -f /tmp/import_build.log
if [ "$BUILD_RC" -ne 0 ]; then
echo -e "${RED}[✗] 远端编译失败 (exit=${BUILD_RC})${NC}"
exit 1
fi
echo -e "${GREEN}[√] 编译成功${NC}"
else
echo -e "${YELLOW}[i] 跳过编译(--no-build${NC}"
fi
# =============================================================================
# 在远端生成 run_import.batWindows 批处理),用户双击或在 cmd 里运行即可,无需手敲命令
# =============================================================================
# 交互式 .bat 各参数的默认值(来自 profile 推导,用户双击时可逐项覆盖,回车即用默认)
EXE_PATH_WIN=$(echo "${REMOTE_SRC_DIR}/target/release/import_results.exe" | sed 's|/|\\|g')
CONFIG_PATH="${REMOTE_SRC_DIR}/workflows/sdB_cno.yaml"
WORK_DIR_WIN=$(echo "${REMOTE_SRC_DIR}" | sed 's|/|\\|g')
GRID_DIR="E:/fmq/grid" # 旧版 run_grid.py 的 results 根目录
WORKFLOW_NAME="sdB_cno" # 工作流名,须匹配将来在 Dashboard 创建/启动的工作流
# .bat 内容:用 quoted heredoc 避免反引号/%/$ 被 bash 解释,占位符稍后替换。
# 每个参数都用 set /p 交互读取,空回车则采用 [方括号] 里的默认值;token 空回车则只迁移 result 不入库。
BAT_CONTENT=$(cat <<'BATEOF'
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
cd /d "__WORK_DIR__"
echo ============================================================
echo DCTS 历史计算结果导入工具
echo ============================================================
echo 以下每项直接回车即采用 [方括号] 内的默认值
echo ============================================================
echo.
set /p DATA_DIR="旧数据目录 (run_grid.py results 根) [__DATA_DIR__]: "
if "!DATA_DIR!"=="" set "DATA_DIR=__DATA_DIR__"
set /p CONFIG="workflow 配置文件 [__CONFIG__]: "
if "!CONFIG!"=="" set "CONFIG=__CONFIG__"
set /p RESULT_DIR="结果目录 [__RESULT__]: "
if "!RESULT_DIR!"=="" set "RESULT_DIR=__RESULT__"
set /p SERVER="服务端地址 [__SERVER__]: "
if "!SERVER!"=="" set "SERVER=__SERVER__"
set /p WORKFLOW="工作流名 -w (须匹配将来启动的工作流) [__WORKFLOW__]: "
if "!WORKFLOW!"=="" set "WORKFLOW=__WORKFLOW__"
set /p ADMIN_TOKEN="Admin Token (直接回车则只迁移 result 不入库): "
echo.
echo ============================================================
echo 即将执行,请确认:
echo 旧数据目录 : !DATA_DIR!
echo workflow : !CONFIG!
echo 结果目录 : !RESULT_DIR!
echo 服务端 : !SERVER!
echo 工作流名 : !WORKFLOW!
echo Token : !ADMIN_TOKEN!
echo ============================================================
echo.
if "!ADMIN_TOKEN!"=="" (
echo [i] 未输入 token,仅迁移产物树到 result(入库会因无鉴权失败,但不阻断迁移)
"__EXE__" -d "!DATA_DIR!" -c "!CONFIG!" -r "!RESULT_DIR!" -s "!SERVER!" -w "!WORKFLOW!"
) else (
"__EXE__" -d "!DATA_DIR!" -c "!CONFIG!" -r "!RESULT_DIR!" -s "!SERVER!" -w "!WORKFLOW!" -t "!ADMIN_TOKEN!"
)
echo.
echo ============================================================
echo 执行结束。按任意键退出...
pause >nul
BATEOF
)
BAT_CONTENT="${BAT_CONTENT//__WORK_DIR__/${WORK_DIR_WIN}}"
BAT_CONTENT="${BAT_CONTENT//__EXE__/${EXE_PATH_WIN}}"
BAT_CONTENT="${BAT_CONTENT//__DATA_DIR__/${GRID_DIR}}"
BAT_CONTENT="${BAT_CONTENT//__CONFIG__/${CONFIG_PATH}}"
BAT_CONTENT="${BAT_CONTENT//__RESULT__/${RESULT_DIR}}"
BAT_CONTENT="${BAT_CONTENT//__SERVER__/${DCTS_SERVER_URL}}"
BAT_CONTENT="${BAT_CONTENT//__WORKFLOW__/${WORKFLOW_NAME}}"
# 本地写临时 .batCRLF 行尾 + UTF-8 with BOM),再用 scp 上传——
# 绕开 “ssh 管道 + powershell $input 自动变量 + stdin 双重消费” 导致的空文件问题。
BAT_REMOTE_PATH="${REMOTE_SRC_DIR}/run_import.bat"
# 用固定可预测的临时路径,便于在成功/失败两条出口都显式清理(脚本头部已有 SSH ControlMaster 的 EXIT trap
# 这里不再叠加 trap 以免覆盖它)。
TMP_BAT="${TMPDIR:-/tmp}/dcts_run_import.$$.bat"
# 关键:必须写成 UTF-8 **带 BOM**EF BB BF)。
# 原因:chcp 65001 只改控制台显示编码,不改 cmd.exe 解析 .bat 文件的编码。无 BOM 时 cmd 按系统 ANSI(中文
# Windows 为 GBK)逐字节解析 UTF-8 文件,中文多字节序列错位会导致 echo/if 解析错乱——
# 报 "'数据目录' is not recognized as an internal or external command" 且 if 块边界被破坏,
# 使 import_results.exe 根本不执行。带 BOM 后现代 cmdWin10 1903+)会按 UTF-8 正确解析整个文件。
# CRLF 行尾:命令替换 $(...) 会吞掉末尾换行,直接 printf '%s\r\n' 只在末尾加一个 \r
# 故先按 LF 展开再用 sed 给每行补 \r,确保每行都是 CRLF。
{
printf '\xEF\xBB\xBF' # UTF-8 BOM
printf '%s\n' "$BAT_CONTENT" | sed 's/$/\r/'
} > "$TMP_BAT"
echo -e "\n${CYAN}=== 上传 run_import.bat 到远端 ===${NC}"
if scp -P "${REMOTE_PORT}" ${SSH_OPTS} "$TMP_BAT" "${REMOTE_USER}@${REMOTE_IP}:${BAT_REMOTE_PATH}"; then
echo -e "${GREEN}[√] 已生成: ${BAT_REMOTE_PATH}${NC}"
else
echo -e "${RED}[✗] 上传 run_import.bat 失败${NC}"
rm -f "$TMP_BAT"
exit 1
fi
rm -f "$TMP_BAT"
# =============================================================================
# 输出最终提示
# =============================================================================
echo ""
echo -e "${BLUE}==============================================================${NC}"
echo -e "${GREEN}🎉 推送 + 编译完成!${NC}"
echo -e "${BLUE}==============================================================${NC}"
echo ""
echo -e "${CYAN}在 Windows 机 ${REMOTE_IP} 上启动导入(任选其一):${NC}"
echo ""
echo -e " 方式1(推荐):双击运行 ${YELLOW}${WORK_DIR_WIN}\\run_import.bat${NC}"
echo -e " 会逐项交互提示:旧数据目录 / workflow 配置 / 结果目录 / 服务端 / 工作流名 / Token"
echo -e " 每项直接回车即采用 [方括号] 内的默认值;Token 空回车则只迁移 result 不入库"
echo ""
echo -e " 方式2cmd): cd /d ${WORK_DIR_WIN} 然后 run_import.bat"
echo ""
echo -e "${YELLOW}参数默认值(双击时可逐项覆盖):${NC}"
echo -e " -d ${GRID_DIR} 旧版 Python run_grid.py 的 results 根目录"
echo -e " -c ${CONFIG_PATH}"
echo -e " -r ${RESULT_DIR} 结果写入目标(node 容器卷映射处)"
echo -e " -s ${DCTS_SERVER_URL} 服务端地址(来自 profile"
echo -e " -w ${WORKFLOW_NAME} 工作流名(必须等于将来要启动的工作流名)"
echo ""
echo -e "${YELLOW}⚠️ 注意:${NC}"
echo -e " - -w sdB_cno 必须匹配将来在 Dashboard 创建/启动的工作流名,否则 converged 标记被隔离看不到、导致重算"
echo -e " - 工具可重复运行(DB 幂等 + result 原子覆盖),网络中断后重跑 run_import.bat 即可补齐"
echo ""