#!/bin/bash
# Снять PC Remote с этого Mac-а начисто.
#
#     sudo ./uninstall.sh          — снять всё, про настройки спросить
#     sudo ./uninstall.sh --keep-settings   — настройки оставить, не спрашивая
#     sudo ./uninstall.sh --purge  — снять всё вместе с настройками, не спрашивая
#
# Без sudo скрипт тоже работает: он снимет задание launchd и, если спросить,
# настройки, — но папку из «Программ» удалить не сможет и честно об этом скажет.
# Половинчатая уборка молча была бы хуже: человек бы решил, что программы больше
# нет, а она осталась бы в /Applications и поднималась двойным щелчком.
#
# Убираем у ВСЕХ учётных записей машины, а не только у той, из-под которой
# запущены. Почему — сказано над all_accounts.
#
# Порядок шагов важен ровно в одном месте: сначала задание, потом приложение.
# Наоборот — и launchd при следующем входе в систему пытается поднять файл,
# которого нет.

set -u

AGENT_LABEL="com.vareyko.pcremote.mac"
PKG_ID="com.vareyko.pcremote.mac.pkg"

# Переменные PCREMOTE_* подменяют пути и пусты у всех, кроме проверки в
# песочнице (mac-client/pkg/README.md, «Как это проверялось»). Уборщик, который
# нельзя прогнать ни разу, не проверив его на живой машине, — это уборщик,
# который однажды сотрёт не то.
#
# От root подмены не принимаются вовсе — тот же замок, что в scripts/lib.sh, и
# по той же причине: суперпользователь, которому окружение говорит, ЧТО стирать,
# сотрёт указанное. Молча взять настоящие пути тоже нельзя: песочница пошла бы
# сносить живую установку. Поэтому отказ вслух.
if [ "$(/usr/bin/id -u)" = 0 ] &&
	[ -n "${PCREMOTE_APPS:-}${PCREMOTE_HOME:-}${PCREMOTE_SYSTEM_AGENTS:-}${PCREMOTE_BIN:-}" ]; then
	echo "PC Remote: в окружении заданы PCREMOTE_*, а мы работаем от root." >&2
	echo "PC Remote: это подмены путей для проверки в песочнице, и от root им веры нет." >&2
	echo "PC Remote: проверять надо от обычного пользователя, без sudo. Прерываем." >&2
	exit 1
fi

APPS_DIR="${PCREMOTE_APPS:-/Applications}"
APP_PATH="$APPS_DIR/PC Remote.app"
SYSTEM_AGENTS="${PCREMOTE_SYSTEM_AGENTS:-/Library/LaunchAgents}"

# Свой PATH, а не унаследованный: скрипт запускают через sudo, и подсунутый в
# PATH «launchctl» получил бы права суперпользователя.
PATH="${PCREMOTE_BIN:+$PCREMOTE_BIN:}/usr/bin:/bin:/usr/sbin:/sbin"
export PATH

say() { echo "PC Remote: $*"; }

mode="ask"
for a in "$@"; do
	case "$a" in
	--purge) mode="purge" ;;
	--keep-settings) mode="keep" ;;
	-h | --help)
		# Печатаем шапку до первой не-комментарной строки, а не «строки со второй
		# по двадцатую»: закреплённые номера строк устаревают на первой же правке
		# комментария, и справка начинает показывать кусок кода.
		awk 'NR>1 && /^#/ { sub(/^# ?/, ""); print; next } NR>1 { exit }' "$0"
		exit 0
		;;
	*)
		echo "не знаю ключа $a; см. $0 --help" >&2
		exit 2
		;;
	esac
done

# ---------------------------------------------------------------------------
# Кто на этой машине живёт.
# ---------------------------------------------------------------------------

# account_home — домашняя папка по имени учётной записи.
#
# ПОЧЕМУ не «dscl … | sed -n 's/^NFSHomeDirectory: //p'», как было раньше. Этот
# разбор молча теряет ровно те ответы, ради которых dscl тут и заведён. Настоящий
# dscl печатает значение С ПРОБЕЛОМ не на своей строке, а на следующей, с
# ведущим пробелом:
#
#     $ dscl . -read /Users/daemon RealName
#     RealName:
#      System Services
#
# То есть для человека, чья домашняя папка лежит на томе с пробелом в имени —
# /Volumes/Home Disk/anna, обычная раскладка на общих Mac-ах и там, где
# системный диск маленький, — sed возвращал ПУСТО. Уборщик отвечал «БЕДА: не
# нашли домашнюю папку» и выходил единицей, не сняв НИЧЕГО: ни задания, ни
# приложения, ни ключа. А совет в сообщении («войдите в эту учётную запись и
# повторите») не чинил ничего, потому что беда была не в имени.
#
# dscacheutil печатает значение с пробелами одной строкой и без выравнивания —
# берём его. Запасной путь — dscl в машинном виде (-plist): человекочитаемый
# вывод разбирать нельзя вовсе, у root он вдобавок отдаёт два значения на одной
# строке («/var/root /private/var/root»).
account_home() {
	local u="$1" h="" pl
	h="$(dscacheutil -q user -a name "$u" 2>/dev/null | sed -n 's/^dir: //p' | head -1)"
	if [ -z "$h" ]; then
		pl="$(mktemp -t pcremote-home 2>/dev/null || true)"
		if [ -n "$pl" ]; then
			dscl -plist . -read "/Users/$u" NFSHomeDirectory >"$pl" 2>/dev/null
			h="$(/usr/libexec/PlistBuddy -c 'Print :dsAttrTypeStandard\:NFSHomeDirectory:0' \
				"$pl" 2>/dev/null || true)"
			rm -f "$pl"
		fi
	fi
	printf '%s' "$h"
}

# account_uid — номер учётной записи. Через тот же dscacheutil, чтобы не
# зависеть от того, знает ли `id` это имя.
account_uid() {
	local u="$1" n=""
	n="$(dscacheutil -q user -a name "$u" 2>/dev/null | sed -n 's/^uid: //p' | head -1)"
	[ -z "$n" ] && n="$(id -u "$u" 2>/dev/null || true)"
	printf '%s' "$n"
}

# all_accounts — все настоящие учётные записи машины, по строке
# «имя<таб>uid<таб>домашняя папка».
#
# ПОЧЕМУ перебор, а не одна запись. Раньше уборщик брал ровно одного человека —
# того, кто сидит за экраном, или SUDO_USER, — и все пути строил из его домашней
# папки. Про остальных он не знал ничего, но общесистемные шаги делал всё равно:
# удалял /Applications/PC Remote.app и снимал отметку pkgutil. В конце печатал
# безусловное «Готово».
#
# Чем это кончалось. PC Remote стоял у Бориса; Анна, второй администратор,
# перед продажей Mac-а входит под собой и делает sudo ./uninstall.sh. У неё ни
# задания, ни настроек нет — вопрос про настройки даже не задаётся, — приложение
# удаляется, печатается «Готово». А у Бориса остаются: задание launchd,
# указывающее на только что удалённое приложение (launchd при каждом его входе
# в систему безуспешно поднимает несуществующий файл), журнал и, самое дорогое,
# pcremote.json с ПОСТОЯННЫМ ЗАКРЫТЫМ КЛЮЧОМ машины и хешем пароля на
# подключение. Анна уверена, что стёрла всё, — ей так и написали, — и отдаёт Mac
# вместе с ключом и паролем. Ни строчки про вторую учётную запись в выводе не
# было.
#
# Служебные записи отсеиваем по имени (_*, root, daemon, nobody, Guest) и по
# номеру: свои у людей начинаются с 500. Дальше требуем, чтобы домашняя папка
# существовала, — иначе это запись без своего угла, и убирать в ней нечего.
all_accounts() {
	local n u h
	for n in $(dscl . -list /Users 2>/dev/null); do
		case "$n" in
		_* | root | daemon | nobody | Guest) continue ;;
		esac
		u="$(account_uid "$n")"
		[ -n "$u" ] || continue
		[ "$u" -ge 500 ] 2>/dev/null || continue
		h="$(account_home "$n")"
		[ -n "$h" ] && [ -d "$h" ] || continue
		printf '%s\t%s\t%s\n' "$n" "$u" "$h"
	done
}

accounts=()
trouble=no  # было ли то, что мы НЕ смогли убрать
removed=""  # что убрали на самом деле — печатается в конце

note() {
	say "$1"
	removed="$removed  $1
"
}

if [ -n "${PCREMOTE_HOME:-}" ]; then
	# Песочница: ровно одна «учётная запись» с подменённой домашней папкой.
	# Перебирать настоящие записи здесь нельзя ни в коем случае — проверка
	# пошла бы стирать задания живых людей на этой самой машине.
	sbuser="$(stat -f%Su /dev/console 2>/dev/null || true)"
	case "$sbuser" in
	"" | root | _* | loginwindow | daemon | nobody) sbuser="${SUDO_USER:-$(id -un)}" ;;
	esac
	if [ -d "$PCREMOTE_HOME" ]; then
		accounts=("$sbuser"$'\t'"$(account_uid "$sbuser")"$'\t'"$PCREMOTE_HOME")
	fi
else
	while IFS= read -r line; do
		[ -n "$line" ] && accounts+=("$line")
	done <<ACCOUNTS
$(all_accounts)
ACCOUNTS
fi

if [ ${#accounts[@]} -eq 0 ]; then
	# Молчать нельзя, гадать нельзя, поэтому отказ. Уборщик, который ничего не
	# нашёл и отчитался об успехе, оставляет человеку работающую службу с ключом
	# и паролем — то есть худший исход из всех, ради которых он написан.
	say "БЕДА: не нашли домашнюю папку ни одной учётной записи — ничего не сняли."
	say "  Так бывает, когда каталог служб не отвечает. Проверьте:"
	say "  dscl . -list /Users   и   dscacheutil -q user -a name ВАШЕ_ИМЯ"
	exit 1
fi

say "нашли учётных записей: ${#accounts[@]}"

# ---------------------------------------------------------------------------
# 1. Задание launchd у каждого. bootout завершает и сам процесс службы.
# ---------------------------------------------------------------------------
for a in "${accounts[@]}"; do
	IFS=$'\t' read -r acc_name acc_uid acc_home <<<"$a"
	say "смотрим у $acc_name ($acc_home)"

	# Чужая ~/Library закрыта правами (0700), и без sudo мы в неё не заглянем.
	# Сказать об этом надо вслух: «не нашли» и «не смогли посмотреть» — разные
	# ответы, а выглядят одинаково.
	if [ -d "$acc_home/Library" ] && [ ! -r "$acc_home/Library" ]; then
		say "  не хватило прав заглянуть в $acc_home/Library — там могли остаться"
		say "  задание, журнал и ключ. Повторите под sudo."
		trouble=yes
		continue
	fi

	plist="$acc_home/Library/LaunchAgents/$AGENT_LABEL.plist"
	if [ -n "$acc_uid" ]; then
		launchctl bootout "gui/$acc_uid/$AGENT_LABEL" >/dev/null 2>&1 &&
			note "служба остановлена у $acc_name"
	fi
	if [ -f "$plist" ]; then
		# Отказ называем: оставшееся описание — это служба, воскресающая при
		# следующем входе в систему. Человек, прочитавший «Готово» и не увидевший
		# здесь ни строчки, был бы уверен в обратном.
		if rm -f "$plist" 2>/dev/null; then
			note "задание удалено: $plist"
		else
			say "  БЕДА: не вышло удалить $plist — служба вернётся при следующем входе."
			say "  Повторите под sudo или удалите файл руками."
			trouble=yes
		fi
	fi
done

# Системное — своё мы такого не ставим, но установщик его убирает, и уборка
# должна уметь то же самое: иначе после «снятия» служба воскресает при
# следующей загрузке.
if [ -f "$SYSTEM_AGENTS/$AGENT_LABEL.plist" ]; then
	launchctl bootout "system/$AGENT_LABEL" >/dev/null 2>&1 || true
	if rm -f "$SYSTEM_AGENTS/$AGENT_LABEL.plist" 2>/dev/null; then
		note "системное задание удалено"
	else
		say "системное задание $SYSTEM_AGENTS/$AGENT_LABEL.plist не удалить без sudo"
		trouble=yes
	fi
fi

# ---------------------------------------------------------------------------
# 2. Работающие экземпляры, запущенные руками.
#
# Пара к EXE_PATTERN из scripts/lib.sh, слово в слово, и по тем же причинам:
# ловим и голый двоичный файл (он держит те же пять портов), но не ловим строку
# запуска установщика с «pcremote-mac.pkg» внутри. Своей копией, а не общим
# файлом, потому что uninstall.sh кладётся рядом с пакетом и уезжает к человеку
# один — lib.sh рядом с ним не будет.
# ---------------------------------------------------------------------------
pattern='/pcremote-mac( |$)'
if pgrep -f "$pattern" >/dev/null 2>&1; then
	pkill -TERM -f "$pattern" >/dev/null 2>&1 || true
	waited=0
	while [ "$waited" -lt 50 ] && pgrep -f "$pattern" >/dev/null 2>&1; do
		sleep 0.1
		waited=$((waited + 1))
	done
	pgrep -f "$pattern" >/dev/null 2>&1 && pkill -KILL -f "$pattern" >/dev/null 2>&1
	# Сверяемся ПОСЛЕ, а не рапортуем вперёд дела. Без sudo мы вправе гасить
	# только свои процессы, и служба чужой учётной записи переживает и TERM, и
	# KILL. Сказать «закрыта» про живую программу — значит отправить человека
	# удалять папку, которую держит работающий процесс, и оставить его гадать,
	# почему после «полной уборки» Mac всё ещё виден в сети.
	if pgrep -f "$pattern" >/dev/null 2>&1; then
		say "БЕДА: программа не закрылась — не хватило прав. Повторите под sudo."
		trouble=yes
	else
		note "запущенная программа закрыта"
	fi
fi

# ---------------------------------------------------------------------------
# 3. Само приложение.
# ---------------------------------------------------------------------------
if [ -d "$APP_PATH" ]; then
	if rm -rf "$APP_PATH" 2>/dev/null; then
		note "приложение удалено: $APP_PATH"
	else
		say "не хватило прав удалить $APP_PATH — запустите: sudo $0"
		trouble=yes
	fi
fi

# ---------------------------------------------------------------------------
# 4. Отметка установщика. Без неё pkgutil до конца дней уверен, что пакет стоит,
# и `pkgutil --files` показывает файлы, которых нет.
# ---------------------------------------------------------------------------
if pkgutil --pkg-info "$PKG_ID" >/dev/null 2>&1; then
	if pkgutil --forget "$PKG_ID" >/dev/null 2>&1; then
		note "отметка установщика убрана"
	else
		say "отметку установщика не убрать без sudo (pkgutil --forget $PKG_ID)"
		trouble=yes
	fi
fi

# ---------------------------------------------------------------------------
# 5. Настройки — только по отдельному согласию.
#
# Спрашиваем отдельно, потому что теряется тут не «файл конфигурации», а
# личность компьютера: в pcremote.json лежит постоянный ключ, из которого выведен
# номер этого Mac-а. Удалить его — значит при следующей установке стать в чужих
# списках НОВЫМ, незнакомым компьютером: сохранённые пароли, свои имена и
# звёздочки на всех телефонах и ПК перестанут к нему относиться.
#
# Спрашиваем ОДИН раз, но показываем все найденные папки: чаще всего уборку
# делают перед тем, как отдать Mac другому, и человеку важно увидеть, что ключ
# лежит ещё и у второй учётной записи, про которую он забыл.
# ---------------------------------------------------------------------------
found_cfg=()
for a in "${accounts[@]}"; do
	IFS=$'\t' read -r acc_name acc_uid acc_home <<<"$a"
	if [ -d "$acc_home/Library/Application Support/PC Remote" ] ||
		[ -e "$acc_home/Library/Logs/PC Remote.log" ]; then
		found_cfg+=("$a")
	fi
done

remove_settings="no"
case "$mode" in
purge) remove_settings="yes" ;;
keep) remove_settings="no" ;;
ask)
	if [ ${#found_cfg[@]} -gt 0 ] && [ -t 0 ]; then
		echo
		echo "Удалить настройки, список компьютеров и журнал?"
		for a in "${found_cfg[@]}"; do
			IFS=$'\t' read -r acc_name acc_uid acc_home <<<"$a"
			echo "  $acc_name: $acc_home/Library/Application Support/PC Remote"
			echo "  $acc_name: $acc_home/Library/Logs/PC Remote.log"
		done
		echo "Там постоянный ключ этого компьютера: без него он станет для всех,"
		echo "кто его помнит, новым и незнакомым — с другим номером и другим отпечатком."
		printf "Удалить? [y/N] "
		read -r answer
		case "$answer" in y | Y | yes | да | д) remove_settings="yes" ;; esac
	elif [ ${#found_cfg[@]} -gt 0 ]; then
		# Не терминал — спросить некого, а молча удалить ключ нельзя.
		for a in "${found_cfg[@]}"; do
			IFS=$'\t' read -r acc_name acc_uid acc_home <<<"$a"
			say "настройки оставлены (спросить некого): $acc_home/Library/Application Support/PC Remote"
		done
		say "  удалить их: $0 --purge"
	fi
	;;
esac

if [ "$remove_settings" = yes ]; then
	for a in "${accounts[@]}"; do
		IFS=$'\t' read -r acc_name acc_uid acc_home <<<"$a"
		cfg_dir="$acc_home/Library/Application Support/PC Remote"
		logfile="$acc_home/Library/Logs/PC Remote.log"
		# Отказ называем вслух: человек, попросивший стереть ключ и пароль (чаще
		# всего — перед тем как отдать Mac другому), обязан узнать, что они
		# остались лежать. Молчание тут читается как «сделано».
		if [ -e "$cfg_dir" ]; then
			if rm -rf "$cfg_dir" 2>/dev/null; then
				note "настройки удалены: $cfg_dir"
			else
				say "БЕДА: не вышло удалить $cfg_dir — там ключ и пароль. Удалите руками."
				trouble=yes
			fi
		fi
		if [ -e "$logfile" ]; then
			if rm -f "$logfile" 2>/dev/null; then
				note "журнал удалён: $logfile"
			else
				say "не вышло удалить журнал $logfile"
				trouble=yes
			fi
		fi
	done
elif [ ${#found_cfg[@]} -gt 0 ] && [ "$mode" != ask ]; then
	for a in "${found_cfg[@]}"; do
		IFS=$'\t' read -r acc_name acc_uid acc_home <<<"$a"
		say "настройки оставлены: $acc_home/Library/Application Support/PC Remote"
	done
fi

# ---------------------------------------------------------------------------
# 6. Что вышло на самом деле.
#
# Список печатаем всегда: без него «Готово» одинаково выглядит и над убранной
# машиной, и над машиной, где не нашлось ровно ничего, — а это разные ответы, и
# второй значит, что искали не там.
# ---------------------------------------------------------------------------
say ""
if [ -n "$removed" ]; then
	say "Убрано:"
	printf '%s' "$removed"
else
	say "Убирать было нечего: ни задания, ни приложения, ни отметки установщика"
	say "не нашлось ни у одной из ${#accounts[@]} учётных записей."
fi

if [ "$trouble" = yes ]; then
	say ""
	say "НЕ ВСЁ: выше есть строки о том, что убрать не вышло. Повторите под sudo"
	say "и перечитайте их — оставшийся pcremote.json это ключ и пароль этой машины."
	exit 1
fi

say ""
say "Готово. Одно осталось, и из скрипта его не убрать: строки «PC Remote» в"
say "Системных настройках → Конфиденциальность и безопасность → Запись экрана и"
say "Универсальный доступ. Их убирает только человек — кнопкой «минус» под списком."
