diff --git a/install_efka.sh b/install_efka.sh index 987e38d..533abaa 100755 --- a/install_efka.sh +++ b/install_efka.sh @@ -152,6 +152,15 @@ strip_archive_suffix() { esac } +archive_name_from_url() { + local download_url="$1" + local clean_url="$download_url" + + clean_url="${clean_url%%\?*}" + clean_url="${clean_url%%#*}" + basename "$clean_url" +} + parse_args() { while [[ $# -gt 0 ]]; do case "$1" in @@ -394,21 +403,17 @@ install_program() { local stop_command="$4" local service_user="$5" local service_group="$6" - local clean_url="$download_url" local archive_name validate_install_item "$service_name" "$download_url" "$start_command" - clean_url="${clean_url%%\?*}" - clean_url="${clean_url%%#*}" - archive_name="$(basename "$clean_url")" + archive_name="$(archive_name_from_url "$download_url")" [[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url" - local app_dir_name - app_dir_name="$(strip_archive_suffix "$archive_name")" + strip_archive_suffix "$archive_name" >/dev/null local archive_path="${WORK_DIR}/${archive_name}" - local install_dir="${WORK_DIR}/${app_dir_name}" + local install_dir="${WORK_DIR}/${service_name}" local service_file="/etc/systemd/system/${service_name}.service" local temp_archive temp_archive="$(mktemp)" @@ -429,6 +434,9 @@ install_program() { log "Extracting archive to $install_dir" run_as_root tar -zxvf "$archive_path" -C "$install_dir" + log "Removing downloaded archive: $archive_path" + run_as_root rm -f "$archive_path" + log "Setting owner for install directory: ${service_user}:${service_group}" run_as_root chown -R "${service_user}:${service_group}" "$install_dir" diff --git a/install_iot.sh b/install_iot.sh index 01b0594..b38f61c 100755 --- a/install_iot.sh +++ b/install_iot.sh @@ -158,6 +158,15 @@ strip_archive_suffix() { esac } +archive_name_from_url() { + local download_url="$1" + local clean_url="$download_url" + + clean_url="${clean_url%%\?*}" + clean_url="${clean_url%%#*}" + basename "$clean_url" +} + parse_args() { while [[ $# -gt 0 ]]; do case "$1" in @@ -441,6 +450,18 @@ create_parent_dependency_dirs_from_env() { done < <(service_parent_dir_env_keys "$service_name") } +link_iot_ctrl() { + local install_dir="$1" + local iot_ctrl_path="${install_dir}/bin/iot_ctrl" + local link_path="/usr/local/bin/iot_ctrl" + + [[ -x "$iot_ctrl_path" ]] || fail "Missing executable iot control tool: $iot_ctrl_path. Build it before packaging the release" + + log "Linking iot control tool: $link_path -> $iot_ctrl_path" + run_as_root install -d -m 0755 "$(dirname "$link_path")" + run_as_root ln -sfn "$iot_ctrl_path" "$link_path" +} + install_program() { local service_name="$1" local download_url="$2" @@ -448,21 +469,17 @@ install_program() { local stop_command="$4" local service_user="$5" local service_group="$6" - local clean_url="$download_url" local archive_name validate_install_item "$service_name" "$download_url" "$start_command" - clean_url="${clean_url%%\?*}" - clean_url="${clean_url%%#*}" - archive_name="$(basename "$clean_url")" + archive_name="$(archive_name_from_url "$download_url")" [[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url" - local app_dir_name - app_dir_name="$(strip_archive_suffix "$archive_name")" + strip_archive_suffix "$archive_name" >/dev/null local archive_path="${WORK_DIR}/${archive_name}" - local install_dir="${WORK_DIR}/${app_dir_name}" + local install_dir="${WORK_DIR}/${service_name}" local service_file="/etc/systemd/system/${service_name}.service" local temp_archive temp_archive="$(mktemp)" @@ -483,11 +500,15 @@ install_program() { log "Extracting archive to $install_dir" run_as_root tar -zxvf "$archive_path" -C "$install_dir" + log "Removing downloaded archive: $archive_path" + run_as_root rm -f "$archive_path" + log "Setting owner for install directory: ${service_user}:${service_group}" run_as_root chown -R "${service_user}:${service_group}" "$install_dir" create_dependency_dirs_from_env "$service_name" "$install_dir" "$service_user" "$service_group" create_parent_dependency_dirs_from_env "$service_name" "$install_dir" "$service_user" "$service_group" + link_iot_ctrl "$install_dir" write_service_file "$service_file" "$service_name" "$service_user" "$service_group" "$install_dir" "$start_command" "$stop_command" INSTALLED_SERVICES+=("$service_name") @@ -549,6 +570,7 @@ main() { require_command wget require_root_runner require_command dirname + require_command ln require_command tar require_command systemctl diff --git a/install_service.md b/install_service.md index 469d72b..6282297 100644 --- a/install_service.md +++ b/install_service.md @@ -13,11 +13,13 @@ - 下载服务发布包。 - 解压到 `WORK_DIR`。 +- 删除下载到 `WORK_DIR` 的压缩包。 - 创建运行所需目录。 - 生成 `/etc/systemd/system/.service`。 - 将服务环境变量写入对应 `.service` 文件。 - 执行 `systemctl daemon-reload`。 - 设置服务开机启动。 +- `install_iot.sh` 会将 `{install_dir}/bin/iot_ctrl` 软链接到 `/usr/local/bin/iot_ctrl`。 ## 入口规则 @@ -72,7 +74,7 @@ INSTALL_=( 命令中可使用占位符: -- `{install_dir}`: 解压后的服务安装目录。 +- `{install_dir}`: 解压后的服务安装目录,当前为 `{work_dir}/{service_name}`,例如 `/opt/app/iot`、`/opt/app/efka`。 - `{work_dir}`: 脚本工作目录。 - `{service_name}`: 当前服务名。 @@ -123,7 +125,7 @@ Environment="KEY=value" 1. 解析命令行参数。 2. 校验 `WORK_DIR` 和服务配置。 -3. 检查依赖命令:`wget`、`dirname`、`tar`、`systemctl`。 +3. 检查依赖命令:`wget`、`dirname`、`ln`、`tar`、`systemctl`。 4. 创建 `WORK_DIR`。 5. 安装当前服务。 6. 重新加载 systemd。 @@ -132,15 +134,51 @@ Environment="KEY=value" 单个服务安装流程: 1. 校验服务名、下载地址、启动命令。 -2. 根据下载地址推导压缩包名和安装目录。 +2. 根据服务名推导安装目录,默认是 `/opt/app/`。 3. 下载压缩包到临时文件。 4. 保存压缩包到 `WORK_DIR`。 5. 创建安装目录。 6. 解压压缩包。 -7. 设置安装目录 owner。 -8. 根据 `*_DIR_ENV_KEYS` 创建数据目录,并根据 `*_PARENT_DIR_ENV_KEYS` 创建文件路径父目录。 -9. 生成 systemd service 文件。 -10. 记录服务名,后续统一 enable。 +7. 删除保存到 `WORK_DIR` 的压缩包。 +8. 设置安装目录 owner。 +9. 根据 `*_DIR_ENV_KEYS` 创建数据目录,并根据 `*_PARENT_DIR_ENV_KEYS` 创建文件路径父目录。 +10. iot 安装时创建 `/usr/local/bin/iot_ctrl` 软链接。 +11. 生成 systemd service 文件。 +12. 记录服务名,后续统一 enable。 + +## iot_ctrl 构建和打包原则 + +`iot_ctrl` 位于 `iot/go_extend`,应在发布包构建阶段编译,不在 `install_iot.sh` 中编译。 + +目标机器不应依赖 Go 编译环境。发布包需要提前包含: + +```text +iot/ + bin/ + iot + iot_ctrl +``` + +安装后脚本会检查 `{install_dir}/bin/iot_ctrl` 是否存在且可执行。如果不存在,安装会失败并提示需要在打包前构建该工具。 + +安装成功后会创建软链接: + +```text +/usr/local/bin/iot_ctrl -> /opt/app/iot/bin/iot_ctrl +``` + +构建示例: + +```bash +cd iot/go_extend +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 make build +``` + +如果目标机器是 arm64: + +```bash +GOOS=linux GOARCH=arm64 CGO_ENABLED=0 make build +``` ## systemd service 生成规则 @@ -233,6 +271,8 @@ SERVICE_GROUP="${SERVICE_GROUP:-$(id -gn "$SERVICE_USER")}" 如果该变量是目录,需要加入对应 `*_DIR_ENV_KEYS`。如果该变量是文件路径,需要加入对应 `*_PARENT_DIR_ENV_KEYS`。 +`{install_dir}` 不从压缩包文件名推导,不带版本号,固定使用服务名作为目录名。 + ## 修改和扩展原则 ### 修改环境变量 @@ -309,8 +349,8 @@ bash -n shell/install_efka.sh 建议额外验证 service 渲染结果: ```bash -bash -c "$(sed '$d' shell/install_iot.sh); render_service_file iot app app /opt/app/iot-0.1.0 '/opt/app/iot-0.1.0/bin/iot foreground' '/opt/app/iot-0.1.0/bin/iot stop'" -bash -c "$(sed '$d' shell/install_efka.sh); render_service_file efka app app /opt/app/efka-0.1.0 '/opt/app/efka-0.1.0/bin/efka foreground' '/opt/app/efka-0.1.0/bin/efka stop'" +bash -c "$(sed '$d' shell/install_iot.sh); render_service_file iot app app /opt/app/iot '/opt/app/iot/bin/iot foreground' '/opt/app/iot/bin/iot stop'" +bash -c "$(sed '$d' shell/install_efka.sh); render_service_file efka app app /opt/app/efka '/opt/app/efka/bin/efka foreground' '/opt/app/efka/bin/efka stop'" ``` 如果目标机器有 `systemd-analyze`,可进一步验证生成后的 unit 文件: @@ -325,4 +365,5 @@ systemd-analyze verify /etc/systemd/system/efka.service - 环境变量中如包含空格或特殊字符,需要确认 systemd `Environment=` 语法是否仍然正确。 - token 等敏感信息会写入 systemd service 文件,需控制 `/etc/systemd/system/*.service` 的读取权限和服务器访问权限。 - 数据目录和文件路径型变量必须使用绝对路径。 +- iot 发布包必须包含可执行的 `bin/iot_ctrl`,否则不会创建 `/usr/local/bin/iot_ctrl` 链接。 - 现有卸载脚本如果需要同步目录清理规则,应按 `IOT_DIR_ENV_KEYS` / `EFKA_DIR_ENV_KEYS` 的思路同步调整。 diff --git a/uninstall_service.sh b/uninstall_service.sh index ca2af3e..dcd0f20 100755 --- a/uninstall_service.sh +++ b/uninstall_service.sh @@ -27,7 +27,8 @@ Default behavior: - Disable services from boot. - Remove /etc/systemd/system/.service and service backups. - Run systemctl daemon-reload and reset-failed. - - Remove downloaded archives and extracted program directories under WORK_DIR. + - Remove downloaded archives and service-name program directories under WORK_DIR. + - Remove /usr/local/bin/iot_ctrl. - Keep dependency/data directories unless --purge-data is passed. EOF } @@ -100,16 +101,6 @@ validate_args() { [[ "$item_count" -gt 0 ]] || fail "No programs configured. Fill INSTALL_ITEMS inside this script" } -strip_archive_suffix() { - local filename="$1" - - case "$filename" in - *.tar.gz) printf '%s\n' "${filename%.tar.gz}" ;; - *.tgz) printf '%s\n' "${filename%.tgz}" ;; - *) fail "Unsupported archive suffix: $filename. Expected .tar.gz or .tgz" ;; - esac -} - archive_name_from_url() { local download_url="$1" local clean_url="$download_url" @@ -191,7 +182,6 @@ remove_program_files() { local service_user local service_group local archive_name - local app_dir_name local archive_path local install_dir @@ -205,9 +195,8 @@ remove_program_files() { archive_name="$(archive_name_from_url "$download_url")" [[ -n "$archive_name" ]] || fail "Cannot parse archive filename from URL: $download_url" - app_dir_name="$(strip_archive_suffix "$archive_name")" archive_path="${WORK_DIR}/${archive_name}" - install_dir="${WORK_DIR}/${app_dir_name}" + install_dir="${WORK_DIR}/${service_name}" log "Removing downloaded archive: $archive_path" run_as_root rm -f "$archive_path" @@ -223,6 +212,13 @@ remove_program_files() { set -u } +remove_global_links() { + local link_path="/usr/local/bin/iot_ctrl" + + log "Removing global command link: $link_path" + run_as_root rm -f "$link_path" +} + remove_dependency_dirs() { local dependency_dirs="$1" local install_dir="$2" @@ -263,6 +259,7 @@ main() { stop_and_disable_services remove_service_files reload_systemd + remove_global_links remove_program_files log "Uninstall completed"