From 439531765105e11b97bc95c1a0e448f794834802 Mon Sep 17 00:00:00 2001 From: Qiufeng Date: Tue, 1 Sep 2026 11:42:25 +0800 Subject: [PATCH] feat: add friendly installer progress logs --- README.md | 28 ++++++++++++++++++++ docs/release.md | 21 +++++++++++++++ install.sh | 55 ++++++++++++++++++++++++++++++++++----- scripts/test-installer.sh | 3 +++ 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index b1aac84..4d40843 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,34 @@ curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/ra 脚本会从公开仓库的 latest Release 获取当前架构归档和 `SHA256SUMS`,并在安装前始终校验 SHA-256。也可以通过 `TALLYNOTE_REPOSITORY_URL`、`TALLYNOTE_RELEASE_API_URL`、`TALLYNOTE_RELEASE_ALLOWED_HOSTS` 和 `--release-base-url` 指向自己的仓库或受信 CDN。需要固定版本或预览时,仍可使用 `TALLYNOTE_VERSION`、`--version` 或 `--dry-run` 等高级选项。 +安装过程会持续输出带统一前缀的阶段日志,不会在下载、校验或启动服务时静默等待。交互式 SSH/终端中下载还会显示 curl 进度条;非交互式运行(例如 CI)只输出干净的阶段日志。典型输出如下(版本号、架构和耗时会按实际环境变化): + +```text +tallynote installer: [阶段] 检查运行环境、权限和目标架构 +tallynote installer: [完成] 运行环境可用:x64/glibc +tallynote installer: [阶段] 从 Release API 获取最新版本 +tallynote installer: [完成] 已解析最新版本:1.1.2 +tallynote installer: [完成] Release 下载地址已准备 +tallynote installer: [阶段] 获取发布包:tallynote-1.1.2-linux-x64-glibc.tar.gz +tallynote installer: [完成] 发布包已下载并通过大小限制 +tallynote installer: [阶段] 获取 SHA-256 校验清单 +tallynote installer: [完成] SHA-256 校验清单已准备 +tallynote installer: [阶段] 校验 SHA-256 和发布签名 +tallynote installer: [完成] 发布包校验通过 +tallynote installer: [阶段] 解包、校验包结构并原子切换到版本 1.1.2 +tallynote installer: [完成] 版本 1.1.2 已切换为当前版本 +tallynote installer: [阶段] 安装 systemd 单元、更新辅助程序和卸载器 +tallynote installer: [完成] systemd 单元、更新辅助程序和卸载器已安装 +tallynote installer: [阶段] 重新加载 systemd 并启动 TallyNote +tallynote installer: [完成] TallyNote 服务已启用并启动 +tallynote installer: [阶段] 清理旧版本并完成安装 +tallynote installer: [完成] 旧版本清理完成 +tallynote installer: [完成] 安装完成:TallyNote 1.1.2 +tallynote installer: 查看服务状态:systemctl status tallynote.service +``` + +任何阶段失败都会以 `tallynote installer:` 前缀写出原因并立即停止;不会把不完整版本切换为当前版本。 + 如需额外启用签名校验,在环境中设置 `TALLYNOTE_INSTALL_REQUIRE_SIGNATURE=true` 并提供 `--signing-key`;不设置时不会要求公钥或 `SHA256SUMS.sig`。 已有安装默认拒绝降级到不高于当前版本;确需回退时显式使用 `--allow-downgrade`,正常更新不会覆盖当前或更高版本。 diff --git a/docs/release.md b/docs/release.md index 1e0bdc1..4962745 100644 --- a/docs/release.md +++ b/docs/release.md @@ -40,6 +40,27 @@ curl --proto '=https' --tlsv1.2 -fsSL https://git.awaioi.com/awaioi/TallyNote/ra 脚本会从 `https://git.awaioi.com/awaioi/TallyNote/releases/download/v<版本>/` 下载当前架构归档和 `SHA256SUMS`,限制 HTTPS 重定向只能落在配置的受信主机,校验压缩/展开大小、条目数量、路径和特殊文件,再原子切换 `/opt/tallynote/current`。自定义仓库时同时设置 `TALLYNOTE_REPOSITORY_URL`、`TALLYNOTE_RELEASE_API_URL` 和 `TALLYNOTE_RELEASE_ALLOWED_HOSTS`;若使用独立 CDN,必须把 CDN 主机显式加入白名单。需要预览时显式加 `--dry-run`,需要固定版本时使用 `--version`。 +安装器会在每个关键阶段输出统一格式的日志,便于在 SSH 或 systemd 安装会话中确认进度;交互式终端下载时还会显示 curl 进度条,CI 或日志重定向时则保持纯文本输出: + +```text +tallynote installer: [阶段] 检查运行环境、权限和目标架构 +tallynote installer: [阶段] 从 Release API 获取最新版本 +tallynote installer: [阶段] 获取发布包:tallynote-<版本>-linux-x64-glibc.tar.gz +tallynote installer: [阶段] 获取 SHA-256 校验清单 +tallynote installer: [阶段] 校验 SHA-256 和发布签名 +tallynote installer: [阶段] 解包、校验包结构并原子切换到版本 <版本> +tallynote installer: [完成] 版本 <版本> 已切换为当前版本 +tallynote installer: [阶段] 安装 systemd 单元、更新辅助程序和卸载器 +tallynote installer: [完成] systemd 单元、更新辅助程序和卸载器已安装 +tallynote installer: [阶段] 重新加载 systemd 并启动 TallyNote +tallynote installer: [完成] TallyNote 服务已启用并启动 +tallynote installer: [阶段] 清理旧版本并完成安装 +tallynote installer: [完成] 旧版本清理完成 +tallynote installer: [完成] 安装完成:TallyNote <版本> +``` + +每个阶段完成时会输出 `[完成]`;错误会立即以 `tallynote installer:` 前缀输出,不会静默等待或切换半成品版本。 + 如需启用签名校验,设置 `TALLYNOTE_INSTALL_REQUIRE_SIGNATURE=true` 并提供 `--signing-key`;后台更新同样可通过 `TALLYNOTE_UPDATE_REQUIRE_SIGNATURE=true` 和 `TALLYNOTE_UPDATE_PUBLIC_KEY_FILE` 开启。默认关闭签名要求,方便公开自维护仓库直接更新。 已有安装默认拒绝安装不高于当前版本的 release;只有在明确执行 `--allow-downgrade`(或设置 `TALLYNOTE_ALLOW_DOWNGRADE=true`)时才允许回退版本。 diff --git a/install.sh b/install.sh index eb8621f..63b5ea6 100755 --- a/install.sh +++ b/install.sh @@ -69,6 +69,8 @@ EOF } die() { printf 'tallynote installer: %s\n' "$*" >&2; exit 1; } log() { printf 'tallynote installer: %s\n' "$*"; } +stage() { log "[阶段] $*"; } +stage_done() { log "[完成] $*"; } [[ "$REQUIRE_SIGNATURE" == true || "$REQUIRE_SIGNATURE" == false ]] || die 'TALLYNOTE_INSTALL_REQUIRE_SIGNATURE 必须是 true 或 false' [[ "$ALLOW_DOWNGRADE" == true || "$ALLOW_DOWNGRADE" == false ]] || die 'TALLYNOTE_ALLOW_DOWNGRADE 必须是 true 或 false' @@ -197,15 +199,23 @@ assert_allowed_url() { download() { local url=$1 out=$2 max_bytes=${3:-$((MAX_RELEASE_MB * 1024 * 1024))} local current="$url" headers status location actual origin scheme authority + local -a curl_args=(--proto '=https' --tlsv1.2 --fail --show-error --max-redirs 0 + --connect-timeout "$CONNECT_TIMEOUT" --max-time "$MAX_TIME" --max-filesize "$max_bytes" + --retry 2 --retry-connrefused) + # Keep CI and journal output clean, while showing curl's standard progress + # bar during an interactive SSH/terminal installation. + if [[ -t 2 ]]; then + curl_args+=(--progress-bar) + else + curl_args+=(--silent) + fi require_https "$url" assert_allowed_url "$url" [[ ! -L "$out" && ! -e "$out" ]] || die "download destination already exists: $out" for _redirect in 0 1 2 3; do headers="${out}.headers-${RANDOM}-$$" - status=$(curl --proto '=https' --tlsv1.2 --fail --silent --show-error --max-redirs 0 \ - --connect-timeout "$CONNECT_TIMEOUT" --max-time "$MAX_TIME" --max-filesize "$max_bytes" \ - --retry 2 --retry-connrefused --output "$out" --dump-header "$headers" \ - --write-out '%{http_code}' "$current" 2>/dev/null) || status=000 + status=$(curl "${curl_args[@]}" --output "$out" --dump-header "$headers" \ + --write-out '%{http_code}' "$current") || status=000 if [[ "$status" =~ ^2[0-9][0-9]$ ]]; then rm -f -- "$headers" break @@ -715,6 +725,7 @@ prune_releases() { } main() { + stage '检查运行环境、权限和目标架构' # These variables are useful for isolated tests, but a root install must # never execute an untrusted PATH entry supplied through sudo's environment. if (( APPLY )) || [[ -n "${TALLYNOTE_UNAME_BIN+x}" ]]; then @@ -737,14 +748,21 @@ main() { # TALLYNOTE_RELEASE_ALLOWED_HOSTS when the operator has reviewed it. append_allowed_host "$(url_host "$RELEASE_API_URL")" append_allowed_host "$(url_host "$REPOSITORY_URL")" + stage_done "运行环境可用:${TALLYNOTE_ARCH}/${TALLYNOTE_LIBC}" if [[ "$VERSION" == "latest" ]]; then if (( ! APPLY )); then [[ -z "$RELEASE_BASE_URL" ]] || require_https "$RELEASE_BASE_URL" + stage '预览最新版本解析(dry-run 不访问 Release)' log 'version: latest (release lookup skipped in dry-run)' log 'dry-run: pass --version VERSION to preview an exact artifact' + stage_done 'dry-run 预览完成:不会下载、解包或修改 systemd' return 0 fi + stage '从 Release API 获取最新版本' resolve_latest_version + stage_done "已解析最新版本:${VERSION#v}" + else + stage "使用指定版本:${VERSION#v}" fi validate_semver "$VERSION" || die 'version must be a semantic version (for example 1.2.3)' VERSION=${VERSION#v} @@ -755,15 +773,21 @@ main() { die "拒绝安装不高于当前版本的 release:当前 $current_version,候选 $VERSION(如确需降级请使用 --allow-downgrade)" fi fi + stage '准备 Release 下载地址和发布包' release_urls local artifact archive checksum signature artifact_url work release_dir artifact=${RELEASE_FILE:+$(basename -- "$RELEASE_FILE")} artifact=${artifact:-tallynote-${VERSION}-linux-${TALLYNOTE_ARCH}-${TALLYNOTE_LIBC}.tar.gz} [[ "$artifact" =~ ^[A-Za-z0-9][A-Za-z0-9._+\-]*\.(tar\.gz|tgz|tar)$ ]] || die 'release 文件名无效' artifact_url="$RELEASE_BASE_URL/$artifact" + stage_done 'Release 下载地址已准备' log "platform: ${TALLYNOTE_ARCH}/${TALLYNOTE_LIBC}; release: ${VERSION#v}" log "layout: $PREFIX/releases + atomic $PREFIX/current; data: $DATA_DIR" - if (( ! APPLY )); then log 'dry-run: no download, extraction, or systemd changes'; return 0; fi + if (( ! APPLY )); then + log 'dry-run: no download, extraction, or systemd changes' + stage_done 'dry-run 预览完成:不会下载、解包或修改 systemd' + return 0 + fi [[ "$("$UNAME_BIN" -s)" == Linux ]] || die '安装器只允许在 Linux 上执行' [[ $EUID -eq 0 ]] || die '安装必须以 root 运行' for command_name in curl sha256sum tar install sed awk find systemctl; do @@ -777,6 +801,7 @@ main() { INSTALL_BACKUP_DIR="$work/original" trap rollback_install_if_needed EXIT archive="$work/$artifact" + stage "获取发布包:$artifact" if [[ -n "$RELEASE_FILE" && -f "$RELEASE_FILE" && ! -L "$RELEASE_FILE" ]]; then cp -- "$RELEASE_FILE" "$archive" chmod 600 "$archive" @@ -785,8 +810,10 @@ main() { [[ -z "$RELEASE_FILE" ]] || die '本地 release 文件不存在或是符号链接' download "$artifact_url" "$archive" fi + stage_done '发布包已下载并通过大小限制' checksum="$work/SHA256SUMS" SHA256_URL=${SHA256_URL:-$RELEASE_BASE_URL/SHA256SUMS} + stage '获取 SHA-256 校验清单' if [[ -n "$SHA256_FILE" && -f "$SHA256_FILE" && ! -L "$SHA256_FILE" ]]; then cp -- "$SHA256_FILE" "$checksum" chmod 600 "$checksum" @@ -795,9 +822,11 @@ main() { [[ -z "$SHA256_FILE" ]] || die '本地 SHA256SUMS 文件不存在或是符号链接' download "$SHA256_URL" "$checksum" $((2 * 1024 * 1024)) fi + stage_done 'SHA-256 校验清单已准备' SIGNING_KEY=${SIGNING_KEY:-$UPDATE_PUBLIC_KEY_FILE} signature='' if [[ "$REQUIRE_SIGNATURE" == true || -n "$SIGNATURE_URL" || -n "$SIGNING_KEY" ]]; then + stage '获取发布签名' if [[ "$SIGNATURE_FORMAT" == gpg ]]; then SIGNATURE_URL=${SIGNATURE_URL:-$RELEASE_BASE_URL/$artifact.asc} signature="$work/$artifact.asc" @@ -806,10 +835,14 @@ main() { signature="$work/SHA256SUMS.sig" fi download "$SIGNATURE_URL" "$signature" $((64 * 1024)) + stage_done '发布签名已准备' fi + stage '校验 SHA-256 和发布签名' verify_archive "$archive" "$checksum" "$signature" "$SIGNING_KEY" + stage_done '发布包校验通过' [[ "$PREFIX" = /* && "$DATA_DIR" = /* && "$CONFIG_DIR" = /* ]] || die '安装、数据和配置目录必须是绝对路径' [[ ! -L "$DATA_DIR" && ! -L "$PREFIX" && ! -L "$CONFIG_DIR" ]] || die 'installation/data/config paths must not be symlinks' + stage '停止旧服务并准备安装、配置和数据目录' id tallynote >/dev/null 2>&1 || useradd --system --user-group --home-dir "$DATA_DIR" --shell /usr/sbin/nologin tallynote backup_install_files "$INSTALL_BACKUP_DIR" stop_existing_services @@ -821,10 +854,14 @@ main() { if [[ -e "$CONFIG_DIR/tallynote.env" ]]; then validate_existing_env "$CONFIG_DIR/tallynote.env" fi + stage_done '目录、权限和旧服务状态已准备' + stage "解包、校验包结构并原子切换到版本 ${VERSION#v}" install_release "$archive" "$VERSION" + stage_done "版本 ${VERSION#v} 已切换为当前版本" release_dir="$PREFIX/releases/$VERSION" [[ -f "$release_dir/systemd/tallynote.service" && -f "$release_dir/systemd/tallynote-update.service" && -f "$release_dir/systemd/tallynote-update.path" ]] || die 'release package is missing systemd unit files' [[ -f "$release_dir/systemd/tallynote.env.example" && -f "$release_dir/scripts/tallynote-update-runner.sh" && -x "$release_dir/uninstall.sh" ]] || die 'release package is missing update/uninstall support files' + stage '安装 systemd 单元、更新辅助程序和卸载器' install -d -m 755 /usr/local/libexec /etc/systemd/system local unit_tmp unit_tmp=$(mktemp -d) @@ -879,15 +916,21 @@ main() { printf 'TALLYNOTE_UPDATE_PUBLIC_KEY_FILE=%s\n' "$CONFIG_DIR/update-signing-key.pub" >> "$CONFIG_DIR/tallynote.env" fi fi + stage_done 'systemd 单元、更新辅助程序和卸载器已安装' + stage '重新加载 systemd 并启动 TallyNote' chown root:root "$CONFIG_DIR/tallynote.env" chmod 640 "$CONFIG_DIR/tallynote.env" systemctl daemon-reload systemctl enable --now tallynote.service tallynote-update.path + stage_done 'TallyNote 服务已启用并启动' + stage '清理旧版本并完成安装' prune_releases + stage_done '旧版本清理完成' INSTALL_COMMITTED=1 trap - EXIT rm -rf -- "$INSTALL_WORK_DIR" 2>/dev/null || true INSTALL_WORK_DIR='' - log 'installed; inspect with systemctl status tallynote.service' + stage_done "安装完成:TallyNote ${VERSION#v}" + log '查看服务状态:systemctl status tallynote.service' } main "$@" diff --git a/scripts/test-installer.sh b/scripts/test-installer.sh index eecad6b..1b26ca1 100755 --- a/scripts/test-installer.sh +++ b/scripts/test-installer.sh @@ -4,8 +4,11 @@ root=$(cd "$(dirname "$0")/.." && pwd) bash -n "$root/install.sh" "$root/scripts/tallynote-update.sh" output=$(bash "$root/install.sh" --dry-run --release-base-url https://releases.example.test/releases) grep -q 'dry-run' <<<"$output" +grep -q '\[阶段\] 检查运行环境' <<<"$output" +grep -q '\[完成\] dry-run 预览完成' <<<"$output" output=$(bash "$root/install.sh" --dry-run --version 1.2.3 --release-base-url https://releases.example.test/releases) grep -q 'release: 1.2.3' <<<"$output" +grep -q '\[阶段\] 使用指定版本:1.2.3' <<<"$output" if bash "$root/install.sh" --dry-run --release-base-url http://insecure.example.test/releases >/dev/null 2>&1; then echo 'expected non-HTTPS URL to fail' >&2 exit 1