mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
494 lines
18 KiB
Bash
494 lines
18 KiB
Bash
#!/usr/bin/env bash
|
||
#
|
||
# vocat install / update script for systemd and OpenWrt/procd deployments.
|
||
#
|
||
# Usage:
|
||
# bash install.sh [version] # run directly when already root
|
||
# sudo bash install.sh [version] # run through sudo as a normal user
|
||
# bash install.sh --check-env # check VoWiFi host prerequisites
|
||
#
|
||
# Behavior:
|
||
# - Prompts for script language (中文 / English) as soon as it runs.
|
||
# - If the installed version equals the target version, does nothing (unless --force).
|
||
# - On first install, generates a random 32-char admin password, writes it to
|
||
# /etc/vocat/env (0600, loaded by the systemd unit), and prints it ONCE.
|
||
# - On update, preserves the existing env file and credentials.
|
||
# - Verifies Linux XFRM/IPsec support required by IMS; on OpenWrt it tries
|
||
# the matching opkg packages first.
|
||
# - (Re)writes a systemd or OpenWrt/procd service and restarts it.
|
||
#
|
||
# Published script: must contain no secrets, IPs, or passwords.
|
||
|
||
set -euo pipefail
|
||
|
||
# --- Publisher configuration -------------------------------------------------
|
||
# Default GitHub repository in owner/name form. Publishers: set this to your
|
||
# own repo, or override per-run with VOCAT_REPO.
|
||
REPO="${VOCAT_REPO:-MengMengCode/VoCat}"
|
||
|
||
INSTALL_DIR="/opt/vocat/bin"
|
||
BINARY_PATH="${INSTALL_DIR}/vocat"
|
||
LINK_PATH="/usr/local/bin/vocat"
|
||
ENV_DIR="/etc/vocat"
|
||
ENV_FILE="${ENV_DIR}/env"
|
||
UNIT_PATH="/etc/systemd/system/vocat.service"
|
||
OPENWRT_INIT_PATH="/etc/init.d/vocat"
|
||
|
||
# --- Language ----------------------------------------------------------------
|
||
LANG_CHOICE=""
|
||
|
||
msg() {
|
||
# $1 = zh text, $2 = en text
|
||
if [ "$LANG_CHOICE" = "en" ]; then
|
||
printf '%s\n' "$2"
|
||
else
|
||
printf '%s\n' "$1"
|
||
fi
|
||
}
|
||
|
||
prompt_language() {
|
||
if ! ( : </dev/tty ) 2>/dev/null; then
|
||
case "${VOCAT_LANG:-en}" in
|
||
zh|zh-CN|cn) LANG_CHOICE="zh" ;;
|
||
*) LANG_CHOICE="en" ;;
|
||
esac
|
||
return
|
||
fi
|
||
while true; do
|
||
echo "选择语言 / Select language: 1) 中文 2) English" >/dev/tty
|
||
printf '> ' >/dev/tty
|
||
if ! read -r choice </dev/tty; then
|
||
LANG_CHOICE="en"
|
||
return
|
||
fi
|
||
case "$choice" in
|
||
1|"") LANG_CHOICE="zh"; return ;;
|
||
2) LANG_CHOICE="en"; return ;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
die() {
|
||
msg "$1" "$2" >&2
|
||
exit 1
|
||
}
|
||
|
||
# --- Root --------------------------------------------------------------------
|
||
[ "$(id -u)" -eq 0 ] || die "请以 root 身份运行此脚本。" "Run this script as root."
|
||
|
||
prompt_language
|
||
|
||
# BusyBox/OpenWrt images often omit coreutils' install(1). Provide the small
|
||
# subset used by this script so the same installer works on router firmware.
|
||
if ! command -v install >/dev/null 2>&1; then
|
||
install() {
|
||
if [ "${1:-}" = "-d" ]; then
|
||
shift
|
||
local mode="0755"
|
||
if [ "${1:-}" = "-m" ]; then
|
||
mode="$2"
|
||
shift 2
|
||
fi
|
||
mkdir -p "$@"
|
||
chmod "$mode" "$@"
|
||
return
|
||
fi
|
||
local mode="0755"
|
||
if [ "${1:-}" = "-m" ]; then
|
||
mode="$2"
|
||
shift 2
|
||
fi
|
||
[ "$#" -eq 2 ] || return 2
|
||
cp "$1" "$2"
|
||
chmod "$mode" "$2"
|
||
}
|
||
fi
|
||
|
||
# --- Parse args --------------------------------------------------------------
|
||
FORCE=0
|
||
CHECK_ENV=0
|
||
SKIP_VOWIFI_CHECK="${VOCAT_SKIP_VOWIFI_CHECK:-0}"
|
||
TARGET_VERSION=""
|
||
for arg in "$@"; do
|
||
case "$arg" in
|
||
--force) FORCE=1 ;;
|
||
--check-env) CHECK_ENV=1 ;;
|
||
--skip-vowifi-check) SKIP_VOWIFI_CHECK=1 ;;
|
||
-h|--help)
|
||
msg "用法: bash install.sh [--force] [--check-env] [--skip-vowifi-check] [版本]" "Usage: bash install.sh [--force] [--check-env] [--skip-vowifi-check] [version]"
|
||
exit 0
|
||
;;
|
||
*) TARGET_VERSION="${arg#v}" ;;
|
||
esac
|
||
done
|
||
|
||
# --- Resolve target version --------------------------------------------------
|
||
resolve_target_version() {
|
||
if [ -n "$TARGET_VERSION" ]; then
|
||
TARGET_VERSION="${TARGET_VERSION#v}"
|
||
return
|
||
fi
|
||
local api_url="https://api.github.com/repos/${REPO}/releases/latest"
|
||
local auth_hdr=()
|
||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||
auth_hdr=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
|
||
fi
|
||
local resp
|
||
resp=$(curl -fsSL "${auth_hdr[@]}" "$api_url") || die "无法获取最新版本信息。检查网络或 REPO 设置。" "Failed to fetch latest release. Check network or REPO."
|
||
# Parse "tag_name": "vX.Y.Z" without jq.
|
||
local tag
|
||
tag=$(printf '%s\n' "$resp" | grep -m1 '"tag_name"' | sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
|
||
[ -n "$tag" ] || die "无法解析最新版本的 tag_name。" "Could not parse tag_name from the release response."
|
||
TARGET_VERSION="${tag#v}"
|
||
}
|
||
|
||
# --- Host prerequisites ------------------------------------------------------
|
||
is_openwrt() {
|
||
[ -f /etc/openwrt_release ] || [ -x /sbin/procd ]
|
||
}
|
||
|
||
xfrm_works() {
|
||
command -v ip >/dev/null 2>&1 && ip xfrm state list >/dev/null 2>&1
|
||
}
|
||
|
||
opkg_has_package() {
|
||
opkg list "$1" 2>/dev/null | grep -q "^$1 -"
|
||
}
|
||
|
||
install_openwrt_vowifi_packages() {
|
||
msg "正在检查 OpenWrt/Kwrt 的 VoWiFi 内核组件..." "Checking OpenWrt/Kwrt VoWiFi kernel components..."
|
||
opkg update >/dev/null 2>&1 || msg \
|
||
"警告:opkg 软件源更新失败,将使用现有索引继续检查。" \
|
||
"Warning: opkg feed update failed; checking the existing index."
|
||
|
||
local packages=""
|
||
local package
|
||
for package in \
|
||
ip-full \
|
||
kmod-ipsec kmod-ipsec4 kmod-ipsec6 \
|
||
kmod-crypto-authenc kmod-crypto-cbc kmod-crypto-aes \
|
||
kmod-crypto-hmac kmod-crypto-sha1; do
|
||
if opkg_has_package "$package"; then
|
||
packages="$packages $package"
|
||
fi
|
||
done
|
||
if [ -n "$packages" ]; then
|
||
# Kernel packages must come from this firmware's own feed. opkg checks
|
||
# the kernel ABI and refuses mismatched modules; never bypass that check.
|
||
# shellcheck disable=SC2086
|
||
opkg install $packages >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
install_linux_ip_tool() {
|
||
command -v ip >/dev/null 2>&1 && return 0
|
||
if command -v apt-get >/dev/null 2>&1; then
|
||
apt-get update -qq && apt-get install -y iproute2
|
||
elif command -v dnf >/dev/null 2>&1; then
|
||
dnf install -y iproute
|
||
elif command -v yum >/dev/null 2>&1; then
|
||
yum install -y iproute
|
||
elif command -v pacman >/dev/null 2>&1; then
|
||
pacman -Sy --noconfirm iproute2
|
||
elif command -v apk >/dev/null 2>&1; then
|
||
apk add --no-cache iproute2
|
||
fi
|
||
}
|
||
|
||
check_vowifi_environment() {
|
||
if [ "$SKIP_VOWIFI_CHECK" = "1" ]; then
|
||
msg \
|
||
"已跳过 VoWiFi 内核环境检查;IMS 通话和短信可能不可用。" \
|
||
"Skipped the VoWiFi kernel check; IMS calls and SMS may not work."
|
||
return
|
||
fi
|
||
|
||
if is_openwrt && command -v opkg >/dev/null 2>&1; then
|
||
# Install the crypto algorithms even when NETLINK_XFRM already works;
|
||
# some minimal images provide xfrm_user but omit AES-CBC/authenc.
|
||
install_openwrt_vowifi_packages
|
||
elif ! xfrm_works; then
|
||
install_linux_ip_tool
|
||
fi
|
||
if xfrm_works; then
|
||
msg "VoWiFi XFRM/IPsec 环境安装并验证成功。" "VoWiFi XFRM/IPsec environment installed and verified."
|
||
return
|
||
fi
|
||
|
||
if is_openwrt; then
|
||
die \
|
||
"当前 OpenWrt/Kwrt 内核 $(uname -r) 不支持 NETLINK_XFRM,且软件源没有匹配的 kmod-ipsec。请使用包含 kmod-ipsec、kmod-ipsec4、kmod-ipsec6、kmod-crypto-authenc、kmod-crypto-cbc、kmod-crypto-aes 和 kmod-crypto-sha1 的同版本固件;严禁安装其他内核版本的 kmod。仅使用非 VoWiFi 功能时可加 --skip-vowifi-check。" \
|
||
"The OpenWrt/Kwrt kernel $(uname -r) lacks NETLINK_XFRM and its feed has no matching kmod-ipsec. Use a firmware built with matching kmod-ipsec, kmod-ipsec4/6, crypto-authenc, CBC, AES and SHA1 modules. Never force kmods from another kernel. Use --skip-vowifi-check only for non-VoWiFi operation."
|
||
fi
|
||
die \
|
||
"当前 Linux 内核不支持 XFRM/IPsec,VoWiFi IMS 无法工作。请启用 CONFIG_XFRM、CONFIG_XFRM_USER、CONFIG_INET_ESP、CONFIG_INET6_ESP、AES-CBC 和 HMAC-SHA1。" \
|
||
"This Linux kernel lacks XFRM/IPsec required by VoWiFi IMS. Enable CONFIG_XFRM, CONFIG_XFRM_USER, CONFIG_INET_ESP, CONFIG_INET6_ESP, AES-CBC and HMAC-SHA1."
|
||
}
|
||
|
||
# --- Skip if already installed at the same version ---------------------------
|
||
skip_if_equal() {
|
||
[ -x "$BINARY_PATH" ] || return 0
|
||
[ "$FORCE" -eq 1 ] && return 0
|
||
local installed
|
||
installed=$("$BINARY_PATH" version 2>/dev/null | awk '{print $2}' | sed -E 's/[[:space:]]*\(.*$//') || return 0
|
||
[ -z "$installed" ] && return 0
|
||
if [ "$installed" = "$TARGET_VERSION" ]; then
|
||
install -d -m 0755 "$(dirname "$LINK_PATH")"
|
||
ln -sfn "$BINARY_PATH" "$LINK_PATH"
|
||
msg "已安装版本 $installed,与目标版本相同,跳过更新。" "Installed version $installed equals target; skipping."
|
||
exit 0
|
||
fi
|
||
msg "当前 $installed -> $TARGET_VERSION,开始更新。" "Updating $installed -> $TARGET_VERSION."
|
||
}
|
||
|
||
# --- Detect architecture -----------------------------------------------------
|
||
detect_arch() {
|
||
ARCH_FALLBACK=""
|
||
case "$(uname -m)" in
|
||
x86_64) ARCH="amd64" ;;
|
||
i386|i486|i586|i686) ARCH="386" ;;
|
||
aarch64) ARCH="aarch64"; ARCH_FALLBACK="arm64" ;;
|
||
arm64) ARCH="arm64"; ARCH_FALLBACK="aarch64" ;;
|
||
armv7l|armv7*) ARCH="armv7" ;;
|
||
*) die "不支持的架构: $(uname -m)" "Unsupported architecture: $(uname -m)" ;;
|
||
esac
|
||
}
|
||
|
||
# --- Download + verify -------------------------------------------------------
|
||
VOCAT_TMP=""
|
||
download_and_verify() {
|
||
VOCAT_TMP=$(mktemp -d)
|
||
trap 'rm -rf "$VOCAT_TMP"' EXIT
|
||
local base="https://github.com/${REPO}/releases/download/v${TARGET_VERSION}"
|
||
local asset="vocat-linux-${ARCH}"
|
||
if [ -n "$ARCH_FALLBACK" ] && ! curl -fsIL -o /dev/null "${base}/${asset}"; then
|
||
asset="vocat-linux-${ARCH_FALLBACK}"
|
||
fi
|
||
msg "下载 $asset ..." "Downloading $asset ..."
|
||
curl -fsSL -o "${VOCAT_TMP}/vocat" "${base}/${asset}" || die "下载二进制失败。" "Failed to download the binary."
|
||
curl -fsSL -o "${VOCAT_TMP}/SHA256SUMS" "${base}/SHA256SUMS" || die "下载 SHA256SUMS 失败。" "Failed to download SHA256SUMS."
|
||
|
||
local expected actual
|
||
# Match a line whose filename field equals the asset (with optional binary-mode * prefix).
|
||
expected=$(awk -v a="$asset" '$2 == a || $2 == ("*" a) {print $1; exit}' "${VOCAT_TMP}/SHA256SUMS")
|
||
[ -n "$expected" ] || die "SHA256SUMS 中找不到 $asset 的校验行。" "$asset not found in SHA256SUMS."
|
||
actual=$(sha256sum "${VOCAT_TMP}/vocat" | awk '{print $1}')
|
||
[ "$actual" = "$expected" ] || die "SHA-256 校验失败。" "SHA-256 verification failed."
|
||
chmod 0755 "${VOCAT_TMP}/vocat"
|
||
"${VOCAT_TMP}/vocat" version >/dev/null 2>&1 || die \
|
||
"Downloaded binary cannot run on this system; keeping the installed version." \
|
||
"The downloaded binary cannot run on this host; the installed version was not changed."
|
||
}
|
||
|
||
# --- Install binary ----------------------------------------------------------
|
||
install_binary() {
|
||
install -d -m 0755 "$INSTALL_DIR"
|
||
install -m 0755 "${VOCAT_TMP}/vocat" "${BINARY_PATH}.new"
|
||
if [ -e "$BINARY_PATH" ]; then
|
||
cp -a "$BINARY_PATH" "${BINARY_PATH}.bak"
|
||
fi
|
||
mv -f "${BINARY_PATH}.new" "$BINARY_PATH"
|
||
install -d -m 0755 "$(dirname "$LINK_PATH")"
|
||
ln -sfn "$BINARY_PATH" "$LINK_PATH"
|
||
}
|
||
|
||
# --- Data directory ----------------------------------------------------------
|
||
ensure_data_dir() {
|
||
install -d -m 0755 /opt/vocat/data
|
||
chown -R root:root /opt/vocat
|
||
}
|
||
|
||
# --- Env file (first install only) -------------------------------------------
|
||
# Generates a random 32-char secret, stores it in the 0600 env file, and flags
|
||
# FIRST_INSTALL so we can print the secret once at the end.
|
||
FIRST_INSTALL=0
|
||
setup_env() {
|
||
if [ -f "$ENV_FILE" ]; then
|
||
return
|
||
fi
|
||
install -d -m 0755 "$ENV_DIR"
|
||
local secret
|
||
secret=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
|
||
[ -n "$secret" ] || die "生成随机密钥失败。" "Failed to generate a random secret."
|
||
printf 'VOCAT_ADMIN_PASSWORD=%s\n' "$secret" > "$ENV_FILE"
|
||
chmod 0600 "$ENV_FILE"
|
||
FIRST_INSTALL=1
|
||
}
|
||
|
||
# --- systemd unit ------------------------------------------------------------
|
||
write_unit() {
|
||
cat > "$UNIT_PATH" <<EOF
|
||
[Unit]
|
||
Description=vocat cellular and VoWiFi control service
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
Group=root
|
||
WorkingDirectory=/opt/vocat
|
||
EnvironmentFile=${ENV_FILE}
|
||
Environment=VOCAT_DATABASE_PATH=/opt/vocat/data/vocat.db
|
||
ExecStart=${BINARY_PATH}
|
||
Restart=on-failure
|
||
RestartSec=3s
|
||
TimeoutStartSec=30s
|
||
# HTTP, VoWiFi, and modem cleanup have bounded shutdown contexts totalling up
|
||
# to 30 seconds. Leave a small margin before systemd resorts to SIGKILL.
|
||
TimeoutStopSec=40s
|
||
RuntimeDirectory=vocat
|
||
RuntimeDirectoryMode=0755
|
||
|
||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW
|
||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW
|
||
NoNewPrivileges=true
|
||
PrivateTmp=true
|
||
PrivateDevices=false
|
||
ProtectSystem=strict
|
||
ProtectHome=true
|
||
ProtectKernelLogs=true
|
||
ProtectKernelModules=true
|
||
ProtectKernelTunables=true
|
||
ProtectControlGroups=true
|
||
# The web/CLI self-updater verifies a release in this directory and atomically
|
||
# renames it over the running binary. Keep the rest of the host read-only.
|
||
ReadWritePaths=/opt/vocat/data /opt/vocat/bin
|
||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_PACKET
|
||
RestrictRealtime=true
|
||
LockPersonality=true
|
||
MemoryDenyWriteExecute=true
|
||
UMask=0077
|
||
LimitNOFILE=65536
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
chmod 0644 "$UNIT_PATH"
|
||
}
|
||
|
||
write_openwrt_init() {
|
||
cat > "$OPENWRT_INIT_PATH" <<'EOF'
|
||
#!/bin/sh /etc/rc.common
|
||
START=95
|
||
STOP=10
|
||
USE_PROCD=1
|
||
PROCD_TERM_TIMEOUT=40
|
||
PROGRAM=/opt/vocat/bin/vocat
|
||
ENV_FILE=/etc/vocat/env
|
||
start_service() {
|
||
procd_open_instance
|
||
procd_set_param command "$PROGRAM" serve
|
||
procd_set_param env VOCAT_DATABASE_PATH=/opt/vocat/data/vocat.db
|
||
if [ -r "$ENV_FILE" ]; then
|
||
while IFS='=' read -r name value; do
|
||
case "$name" in VOCAT_*) procd_append_param env "$name=$value" ;; esac
|
||
done < "$ENV_FILE"
|
||
fi
|
||
procd_set_param respawn 3600 5 5
|
||
procd_set_param stdout 1
|
||
procd_set_param stderr 1
|
||
procd_close_instance
|
||
}
|
||
service_triggers() { procd_add_reload_trigger vocat; }
|
||
EOF
|
||
chmod 0755 "$OPENWRT_INIT_PATH"
|
||
}
|
||
|
||
write_service() {
|
||
if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
|
||
write_unit
|
||
return
|
||
fi
|
||
if [ -x /sbin/procd ] || [ -x /sbin/ubusd ]; then
|
||
write_openwrt_init
|
||
return
|
||
fi
|
||
die "Unsupported service manager." "Neither systemd nor OpenWrt procd was detected."
|
||
}
|
||
|
||
enable_and_start() {
|
||
if [ -x "$OPENWRT_INIT_PATH" ] && { [ -x /sbin/procd ] || [ -x /sbin/ubusd ]; }; then
|
||
"$OPENWRT_INIT_PATH" enable
|
||
if "$OPENWRT_INIT_PATH" restart; then
|
||
# Modems may need several seconds to release and reopen their AT
|
||
# port after procd stops the previous process. Require consecutive
|
||
# healthy observations so a short-lived respawn is not mistaken for
|
||
# a successful upgrade.
|
||
local attempt stable
|
||
stable=0
|
||
for attempt in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30; do
|
||
sleep 1
|
||
if "$OPENWRT_INIT_PATH" running; then
|
||
stable=$((stable + 1))
|
||
if [ "$stable" -ge 3 ]; then
|
||
rm -f "${BINARY_PATH}.bak"
|
||
return
|
||
fi
|
||
else
|
||
stable=0
|
||
fi
|
||
done
|
||
fi
|
||
if [ -e "${BINARY_PATH}.bak" ]; then
|
||
cp -a "${BINARY_PATH}.bak" "$BINARY_PATH"
|
||
"$OPENWRT_INIT_PATH" restart || true
|
||
fi
|
||
die "OpenWrt vocat service failed to start." "The OpenWrt vocat service failed to start."
|
||
fi
|
||
systemctl daemon-reload
|
||
systemctl enable vocat
|
||
if systemctl restart vocat; then
|
||
local attempt
|
||
for attempt in 1 2 3 4 5; do
|
||
if systemctl is-active --quiet vocat; then
|
||
rm -f "${BINARY_PATH}.bak"
|
||
return
|
||
fi
|
||
sleep 1
|
||
done
|
||
fi
|
||
if [ -e "${BINARY_PATH}.bak" ]; then
|
||
msg "新版本启动失败,正在恢复旧二进制。" "The new version failed to start; restoring the previous binary."
|
||
cp -a "${BINARY_PATH}.bak" "$BINARY_PATH"
|
||
systemctl restart vocat || true
|
||
fi
|
||
die "vocat 服务启动失败。" "The vocat service failed to start."
|
||
}
|
||
|
||
# --- Main --------------------------------------------------------------------
|
||
detect_arch
|
||
check_vowifi_environment
|
||
if [ "$CHECK_ENV" -eq 1 ]; then
|
||
msg "VoCat 运行环境检查完成。" "VoCat host environment check completed."
|
||
exit 0
|
||
fi
|
||
resolve_target_version
|
||
skip_if_equal
|
||
download_and_verify
|
||
install_binary
|
||
ensure_data_dir
|
||
setup_env
|
||
write_service
|
||
enable_and_start
|
||
|
||
if [ "$FIRST_INSTALL" -eq 1 ]; then
|
||
secret=$(grep -E '^VOCAT_ADMIN_PASSWORD=' "$ENV_FILE" | cut -d= -f2-)
|
||
echo
|
||
msg "================ 安装完成 ================" "================ Install complete ================"
|
||
msg "首次安装已生成管理员初始密码 (仅显示一次):" "First-install admin password (shown once):"
|
||
echo
|
||
echo " $secret"
|
||
echo
|
||
msg "用户名为 admin。请立即记录此密码。" "Username is admin. Record this password now."
|
||
msg "登录后或运行以下命令修改密码:" "Change it via the web UI or run:"
|
||
echo " vocat menu"
|
||
msg "==========================================" "=============================================="
|
||
else
|
||
echo
|
||
msg "================ 更新完成 ================" "================ Update complete ================"
|
||
msg "已更新到 $TARGET_VERSION,服务已重启。" "Updated to $TARGET_VERSION; service restarted."
|
||
msg "管理员密码保持不变。" "Admin password unchanged."
|
||
msg "==========================================" "=============================================="
|
||
fi
|