Files
Chan/live/deploy/install.sh
T
jackandCursor d884de2c36 dryrun 的 ssh 探测适配强制命令,并分清指纹与认证两种失败
采集机那侧的 authorized_keys 用了强制命令,把这把 key 锁成只能跑 tail,
拿不到 shell。副作用是任何请求都变成 tail -F 而它永不返回,原先
`ssh <host> 'echo ok'` 的探测会永久挂住——ConnectTimeout 只管建连,不管
命令时长。改成照 ship_signals 的真实用法读流:tail -c +0 先吐出整个文件,
数行数就等于对端总线条数,之后由 timeout 收掉。

失败诊断原先一律说"装 key",但 Host key verification failed 是 known_hosts
空的,装 key 修不了,会把人引到错方向。现在分三类:指纹未确认、认证被拒、
其他,各给对应做法。指纹那条明确不建议 StrictHostKeyChecking=no——这条链路
上跑的是下单信号。

install.sh 的第 2 步补上 known_hosts:原先只说装 key,而服务跑起来会撞同一
个墙(BatchMode=yes 不允许交互确认)。

实测:强制命令下故意请求错路径,仍拿到真总线内容且 stderr 干净。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-28 18:01:01 +08:00

204 lines
9.5 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
# 在生产机(有 Bitget API key 白名单的那台)上装实盘执行器。
#
# 只装执行侧:标准库 + aiohttp。**不装** Docker / Hummingbot / pandas /
# chanlun 引擎——那些是采集与研究侧的依赖,理由见 live/live_exec.py 文件头。
#
# sudo ./install.sh # 从当前 checkout 安装
# sudo REPO=git@host:jack/chan.git ./install.sh # 或指定远程克隆
#
# 幂等:重复跑只会更新代码与依赖,不动 /etc/chan-live/live.env 和状态目录。
set -euo pipefail
APP=/opt/chan
STATE=/var/lib/chan-live
CONF=/etc/chan-live
USER_NAME=chan
BRANCH="${BRANCH:-chan}"
REPO="${REPO:-}"
die() { echo "⛔ $*" >&2; exit 1; }
say() { echo " $*"; }
[[ $EUID -eq 0 ]] || die "要 rootsudo $0"
# ── --sync-env:把模板新增的项追加到已有配置 ────────────────────────
# 独立成一个模式而不是塞进安装流程:追加会改一个装着密钥的文件,这种事应当
# 由你显式发起。只追加缺的项(连它上面的注释一起),**不动**已有任何一行。
if [[ "${1:-}" == "--sync-env" ]]; then
EX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/live.env.example"
[[ -f "$EX" ]] || die "找不到模板 $EX"
[[ -f "$CONF/live.env" ]] || die "$CONF/live.env 还不存在,先跑一次 sudo $0"
cp -a "$CONF/live.env" "$CONF/live.env.bak.$(date +%Y%m%d%H%M%S)"
python3 - "$EX" "$CONF/live.env" <<'PY'
import re
import sys
ex_path, live_path = sys.argv[1], sys.argv[2]
ex = open(ex_path, encoding="utf-8").read().splitlines()
live = open(live_path, encoding="utf-8").read()
KEY = re.compile(r"^([A-Z_][A-Z0-9_]*)=")
have = set(KEY.match(x).group(1) for x in live.splitlines() if KEY.match(x))
add, block = [], []
for line in ex:
if KEY.match(line):
k = KEY.match(line).group(1)
if k not in have:
add += block + [line]
block = []
elif line.startswith("#") or (not line.strip() and block):
block.append(line)
else:
block = []
if not add:
print(" 配置已是最新,无需追加")
raise SystemExit(0)
with open(live_path, "a", encoding="utf-8") as f:
f.write("\n\n# ── 以下由 install.sh --sync-env 追加 ──\n")
f.write("\n".join(add) + "\n")
n = sum(1 for x in add if KEY.match(x))
print(f" 追加了 {n} 项:" +
" ".join(KEY.match(x).group(1) for x in add if KEY.match(x)))
PY
say "原文件已备份为 $CONF/live.env.bak.*"
say "追加的项多半是空值,逐项填完再重启:sudo systemctl restart chan-live-exec"
exit 0
fi
# ── 1. 系统依赖 ────────────────────────────────────────────────────
say "装系统包"
if command -v apt-get >/dev/null; then
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
# chrony 不是可选项:staleness 闸靠两机时钟一致才有意义,
# 采集机时钟快 5 分钟就等于把闸放宽 5 分钟(见 ship_signals.py
apt-get install -y -qq python3-venv python3-pip git chrony openssh-client
elif command -v dnf >/dev/null; then
dnf install -y -q python3 python3-pip git chrony openssh-clients
else
die "只认 apt/dnf,其他发行版请手工装 python3-venv git chrony"
fi
systemctl enable --now chrony 2>/dev/null || systemctl enable --now chronyd
# ── 2. 专用用户与目录 ──────────────────────────────────────────────
if ! id -u "$USER_NAME" >/dev/null 2>&1; then
say "建系统用户 $USER_NAME(无登录 shell"
useradd --system --home-dir "$STATE/home" --create-home \
--shell /usr/sbin/nologin "$USER_NAME"
fi
install -d -o "$USER_NAME" -g "$USER_NAME" -m 750 "$STATE" "$STATE/state" "$STATE/home"
install -d -o "$USER_NAME" -g "$USER_NAME" -m 700 "$STATE/home/.ssh"
install -d -o root -g "$USER_NAME" -m 750 "$CONF"
# ── 3. 代码 ────────────────────────────────────────────────────────
if [[ -n "$REPO" ]]; then
if [[ -d "$APP/.git" ]]; then
say "更新已有 checkout"
git -C "$APP" fetch --quiet origin "$BRANCH"
git -C "$APP" checkout --quiet "$BRANCH"
git -C "$APP" reset --hard --quiet "origin/$BRANCH"
else
say "克隆 $REPO"
rm -rf "$APP"; git clone --quiet --branch "$BRANCH" "$REPO" "$APP"
fi
else
SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
[[ -f "$SRC/live/live_exec.py" ]] || die "$SRC 不像仓库根(缺 live/live_exec.py"
if [[ "$SRC" != "$APP" ]]; then
say "从 $SRC 同步代码到 $APP"
install -d "$APP"
# 只同步生产要的那一个子树。研究侧的 research/ 不上生产机:
# 它带 pandas/pyarrow/hummingbot,而且探针 OOM 过一次(14.9GB)
cp -a "$SRC/live" "$APP/"
fi
fi
chown -R root:root "$APP/live"
find "$APP/live" -type f -exec chmod 644 {} + ; chmod 755 "$APP/live" "$APP/live/deploy"
chmod 755 "$APP"/live/deploy/*.sh
# ── 4. venv ───────────────────────────────────────────────────────
say "建 venv 并装依赖(应当只有 aiohttp"
[[ -d "$APP/.venv" ]] || python3 -m venv "$APP/.venv"
"$APP/.venv/bin/pip" install --quiet --upgrade pip
"$APP/.venv/bin/pip" install --quiet -r "$APP/live/requirements.txt"
# 断言生产进程没被拖进重量级依赖。这条会真挡住——曾经为读两个常量
# import 研究侧的 step43,把 numpy/pandas/pyarrow 全拉进实盘进程
say "验依赖面"
"$APP/.venv/bin/python" - <<'PY' || die "生产进程拖进了重量级依赖,看上面输出"
import sys
sys.path.insert(0, "/opt/chan/live")
import live_exec
live_exec.assert_decomposable()
heavy = [m for m in ("numpy", "pandas", "pyarrow", "hummingbot", "scipy")
if m in sys.modules]
if heavy:
print(f" ⛔ 启动路径加载了 {heavy}")
raise SystemExit(1)
print(f" ✓ 只有标准库 + aiohttp · 出场结构 "
f"{live_exec.SL_ATR}/{live_exec.SCALE_ATR}/{live_exec.RUNNER_ATR} ATR "
f"/{live_exec.MAXB} 根")
PY
# ── 5. 配置模板 ────────────────────────────────────────────────────
if [[ ! -f "$CONF/live.env" ]]; then
say "写配置模板 $CONF/live.env(密钥要你手工填)"
install -o root -g "$USER_NAME" -m 640 \
"$APP/live/deploy/live.env.example" "$CONF/live.env"
NEED_FILL=1
else
say "$CONF/live.env 已存在,不覆盖(里面是密钥)"
# 但要报出模板新增的项。不报的话,以后往 example 里加配置,已有部署会
# **永远拿不到且毫无提示**——静默漂移,等到出事才发现某个开关根本没生效
keys() { grep -oE '^[A-Z_][A-Z0-9_]*=' "$1" | tr -d '=' | sort -u; }
MISSING="$(comm -23 <(keys "$APP/live/deploy/live.env.example") \
<(keys "$CONF/live.env") | tr '\n' ' ')"
if [[ -n "${MISSING// }" ]]; then
say "⚠ 模板比你的配置多了这些项:$MISSING"
say " 逐项看说明:$APP/live/deploy/live.env.example"
say " 要把缺的那几行连注释一起追加过去,跑:"
say " sudo $APP/live/deploy/install.sh --sync-env"
NEED_FILL=1
fi
OBSOLETE="$(comm -13 <(keys "$APP/live/deploy/live.env.example") \
<(keys "$CONF/live.env") | tr '\n' ' ')"
[[ -n "${OBSOLETE// }" ]] && \
say " 另有模板里已没有的项(可能已废弃):$OBSOLETE"
fi
# ── 6. systemd ────────────────────────────────────────────────────
say "装 systemd 单元"
install -m 644 "$APP"/live/deploy/chan-live-*.service /etc/systemd/system/
systemctl daemon-reload
echo
echo "装好了。接下来按顺序做(**不要**跳过空跑那步):"
echo
if [[ -n "${NEED_FILL:-}" ]]; then
echo " 1. 填密钥与参数:sudo vi $CONF/live.env"
echo " BITGET_API_KEY / SECRET / PASSPHRASE 用只读+交易权限,"
echo " **不要开提币权限**。SHIP_FROM 填采集机的 ssh 目标。"
echo
fi
echo " 2. 装拉总线用的 ssh key"
echo " sudo -u $USER_NAME ssh-keygen -t ed25519 -N '' -f $STATE/home/.ssh/id_ed25519"
echo " # 把 $STATE/home/.ssh/id_ed25519.pub 加到采集机的 authorized_keys"
echo
echo " 还要写 known_hosts,否则报 Host key verification failed —— 服务跑"
echo " 起来会撞同一个墙,因为 BatchMode=yes 不允许交互确认:"
echo " sudo -u $USER_NAME ssh-keyscan -t ed25519 <采集机IP> \\"
echo " | sudo -u $USER_NAME tee -a $STATE/home/.ssh/known_hosts"
echo " ssh-keygen -lf $STATE/home/.ssh/known_hosts"
echo " # 把指纹跟采集机上 ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub"
echo " # 的输出比一遍。**不要**图省事用 StrictHostKeyChecking=no"
echo " # 那等于放弃中间人防护,而这条链路上跑的是下单信号"
echo
echo " 3. 空跑验全链(不下真单,跑够看到一次心跳再停):"
echo " sudo $APP/live/deploy/dryrun.sh"
echo
echo " 4. 真跑:"
echo " sudo systemctl enable --now chan-live-ship chan-live-exec"
echo " $APP/live/deploy/status.sh"