#!/bin/sh # uninstall-synology.sh — remove the exact Synology DSM installation that # contains this script. DSM Task Scheduler entries remain a manual cleanup. set -u YES=0 for arg in "$@"; do case "$arg" in --yes) YES=1 ;; --help | -h) echo "Usage: sudo sh $0 [--yes]"; exit 0 ;; *) echo "!! 未知参数:$arg" >&2; exit 2 ;; esac done ROOT_PREFIX="${MYVPN_UNINSTALL_ROOT:-}" case "$ROOT_PREFIX" in "" | /) ROOT_PREFIX="" ;; /*) ROOT_PREFIX="${ROOT_PREFIX%/}" ;; *) echo "!! MYVPN_UNINSTALL_ROOT 必须是绝对路径" >&2; exit 2 ;; esac root_path() { printf '%s%s\n' "$ROOT_PREFIX" "$1"; } effective_uid="$(id -u)" if [ -n "$ROOT_PREFIX" ]; then effective_uid="${MYVPN_UNINSTALL_TEST_UID:-$effective_uid}" fi [ "$effective_uid" -eq 0 ] || { echo "!! 卸载需要 root。请用:sudo sh $0" >&2 exit 1 } SCRIPT_DIR="$(CDPATH= cd "$(dirname "$0")" && pwd -P)" if [ -n "${MYVPN_APP:-}" ]; then APP="$MYVPN_APP" elif [ -n "$ROOT_PREFIX" ]; then case "$SCRIPT_DIR" in "$ROOT_PREFIX"/*) APP="${SCRIPT_DIR#"$ROOT_PREFIX"}" ;; *) APP="/volume1/myvpn" ;; esac else APP="$SCRIPT_DIR" fi case "$APP" in /*) ;; *) echo "!! MYVPN_APP 必须是绝对路径" >&2; exit 2 ;; esac REAL_APP="$(root_path "$APP")" case "$REAL_APP" in "" | / | "$ROOT_PREFIX") echo "!! 拒绝删除不安全的安装路径:$REAL_APP" >&2; exit 1 ;; esac [ -d "$REAL_APP" ] || { echo "myvpn 已不在 ${APP},视为卸载完成。" echo "提醒:控制台中的设备记录仍需管理员另行删除;DSM 任务计划也请手工检查。" exit 0 } APP_DIR="$(CDPATH= cd "$REAL_APP" && pwd -P)" [ "$APP_DIR" = "$SCRIPT_DIR" ] || { echo "!! 拒绝卸载:$0 不在目标安装目录 $APP 内。" >&2 exit 1 } TARGET_APP="$APP_DIR" case "$TARGET_APP" in "" | / | "$ROOT_PREFIX") echo "!! 拒绝删除不安全的物理安装路径:$TARGET_APP" >&2; exit 1 ;; esac logical_app_is_link=0 [ -L "$REAL_APP" ] && logical_app_is_link=1 LOGICAL_SUPERVISOR="$REAL_APP/myvpn-run.sh" echo "即将完整卸载 $APP 下的 myvpn 程序、登录状态、凭据和日志。" echo "本地卸载不会删除控制台中的设备记录;如需删除,请让管理员在控制台另行操作。" if [ "$YES" -ne 1 ]; then printf "继续卸载?输入 y 确认 [y/N]: " answer="" IFS= read -r answer || true case "$answer" in y | Y | yes | YES | Yes) ;; *) echo "已取消,未做任何更改。"; exit 0 ;; esac fi SOCK="$TARGET_APP/agent.sock" CLI="$TARGET_APP/myvpn" if [ -x "$CLI" ]; then MYVPN_SOCKET="$SOCK" "$CLI" down >/dev/null 2>&1 || true fi # Match argv structurally rather than flattening cmdline and doing a substring # search. The agent must be argv[0]. The supervisor is either argv[0] itself or # argv[1] of a known shell (the normal #!/bin/sh representation). This avoids # killing an editor/logger merely because one of its arguments mentions a path # beginning with "$TARGET_APP/myvpn-agent". PROC_DIR="${MYVPN_PROC:-$(root_path /proc)}" cmdline_is_myvpn() { cmdline="$1" argv0="$(tr '\0' '\n' <"$cmdline" 2>/dev/null | sed -n '1p')" argv1="$(tr '\0' '\n' <"$cmdline" 2>/dev/null | sed -n '2p')" [ "$argv0" = "$TARGET_APP/myvpn-agent" ] && return 0 { [ "$argv0" = "$TARGET_APP/myvpn-run.sh" ] || [ "$argv0" = "$LOGICAL_SUPERVISOR" ]; } && return 0 case "$argv0" in sh | ash | bash | dash | */sh | */ash | */bash | */dash) { [ "$argv1" = "$TARGET_APP/myvpn-run.sh" ] || [ "$argv1" = "$LOGICAL_SUPERVISOR" ]; } && return 0 ;; esac return 1 } matching_pids() { [ -d "$PROC_DIR" ] || return 0 for d in "$PROC_DIR"/[0-9]*; do [ -d "$d" ] || continue [ -r "$d/cmdline" ] || continue p="${d##*/}" [ "$p" = "$$" ] && continue cmdline_is_myvpn "$d/cmdline" && echo "$p" done } pids="$(matching_pids)" if [ -n "$pids" ]; then for p in $pids; do kill -TERM "$p" 2>/dev/null || true; done i=0 while [ "$i" -lt 20 ] && [ -n "$(matching_pids)" ]; do sleep 1 i=$((i + 1)) done for p in $(matching_pids); do kill -KILL "$p" 2>/dev/null || true; done sleep 1 fi left="$(matching_pids)" [ -z "$left" ] || { echo "!! 无法停止 $APP 的 myvpn 进程:$left;未删除安装目录。" >&2 exit 1 } if ! rm -rf "$TARGET_APP"; then echo "!! 删除安装目录失败:$TARGET_APP;凭据或程序可能仍然存在。" >&2 exit 1 fi [ ! -e "$TARGET_APP" ] && [ ! -L "$TARGET_APP" ] || { echo "!! 删除后安装目录仍然存在:$TARGET_APP;卸载不完整。" >&2 exit 1 } echo "myvpn 已从 $APP 完整卸载。" [ "$logical_app_is_link" -eq 0 ] || echo "提醒:$REAL_APP 是用户提供的符号链接,物理安装内容已删除,但该失效链接已保留。" echo "⚠️ 请到 DSM「控制面板 → 任务计划」手工删除指向 $APP/myvpn-run.sh 的启动任务。" echo "提醒:控制台中的设备记录仍然保留,需要管理员另行删除。"