live.env 装着密钥,所以安装时刻意不覆盖。但这样一来,往 live.env.example 里加配置,已有部署会永远拿不到、且毫无提示——静默漂移,等到某个开关根本 没生效才发现。这次加 TG_TOKEN 就撞上了。 现在重复安装会 diff 两边的变量名:报模板多出的项(提示跑 --sync-env), 也报配置里已被模板删掉的项(可能已废弃)。 --sync-env 独立成一个模式而不是塞进安装流程:追加要改一个装着密钥的文件, 这种事应当由人显式发起。只追加缺的项、连它上面的注释一起,不动已有任何 一行,先备份。实测原有内容逐行未变且重复跑幂等。 Co-authored-by: Cursor <cursoragent@cursor.com>
196 lines
9.0 KiB
Bash
Executable File
196 lines
9.0 KiB
Bash
Executable File
#!/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 "要 root:sudo $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 " sudo -u $USER_NAME ssh -o BatchMode=yes <采集机> 'echo ok'"
|
||
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"
|