#!/bin/sh # Life OS Bridge — установка одной командой. # # curl -fsSL https://mcp.archik.tech/devices/install.sh | sh # # Скачивает мост для этой системы, сверяет контрольную сумму, кладёт его в # ~/.local/bin и запускает подключение. Права администратора не нужны: # ничего не пишется вне домашней папки. # # Исключение — установка уровня «Администратор» на macOS: # # curl -fsSL https://mcp.archik.tech/devices/install.sh | LIFEOS_ADMIN=1 sh # # Тогда мост ставится системно и работает от root как LaunchDaemon, а sudo # спросит пароль в тот момент, когда права действительно понадобятся. Без # этого флага уровень «Администратор» на странице подтверждения просто не # предлагается: мост, запущенный от вашего пользователя, его не потянет. # # Пароль администратора сам по себе тут не помогает: macOS разрешает sudo не # тому, кто знает пароль, а тому, чья учётная запись состоит в группе admin. # С обычной учётки эту установку запускают из учётки администратора: # # su <администратор> -c 'curl -fsSL .../install.sh | LIFEOS_ADMIN=1 sh' # # Всё лежит внутри main() и вызывается последней строкой файла. Так задумано: # скрипт читают из трубы, и оборванная загрузка не должна выполнить половину # работы — недочитанный файл просто ничего не запустит. set -eu # Контрольные суммы сборок, которые сервер отдаёт прямо сейчас. Их подставили # при выдаче этого файла, поэтому они описывают ровно те двоичные файлы, # которые скачаются следующей командой. LIFEOS_SUMS='60df00dace4d9bfea1dcac1379c3fd598efb407b6d1de05263568d24666adf0a darwin-arm64 168b307d0354fb7a361010499cfe9650fc14fe651b89ffaababe75ae892d617a darwin-amd64 b12455387526aaae8dfcb3a37d128db51c1058a0bc4430c9b8c6dbf77211aea8 linux-amd64 eb2b6187bba07d14d362de773b90c95ba3e51e974d24d6314c95f9ab27cf1c6f linux-arm64' say() { printf ' %s\n' "$*"; } die() { printf '\n %s\n\n' "$*" >&2; exit 1; } detect_build() { case "$(uname -s)" in Darwin) os=darwin ;; Linux) os=linux ;; *) die "Эта команда работает на macOS и Linux. На Windows откройте PowerShell и выполните команду со страницы https://mcp.archik.tech/devices/download" ;; esac case "$(uname -m)" in x86_64 | amd64) arch=amd64 ;; arm64 | aarch64) arch=arm64 ;; *) die "Для процессора $(uname -m) сборки нет. Все сборки — https://mcp.archik.tech/devices/builds" ;; esac build="$os-$arch" } fetch() { # $1=url $2=файл if command -v curl >/dev/null 2>&1; then curl -fsSL "$1" -o "$2" elif command -v wget >/dev/null 2>&1; then wget -qO "$2" "$1" else die "Нужен curl или wget." fi } digest() { # $1=файл -> sha256 или пусто, если считать нечем if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1 fi } # Домашняя папка того, кто сидит за этой машиной. Не то же самое, что $HOME: # установку с правами запускают из учётки администратора, а прежний мост # остался у вошедшего в систему человека. console_home() { who="$(stat -f '%Su' /dev/console 2>/dev/null)" || return 0 case "${who}" in "" | root) return 0 ;; esac dscl . -read "/Users/${who}" NFSHomeDirectory 2>/dev/null \ | sed -n 's/^NFSHomeDirectory: //p' } # Имя какой-нибудь учётки администратора — чтобы в подсказке стояло не # «<администратор>», а то, что на этой машине действительно есть. an_admin_account() { dscl . -read /Groups/admin GroupMembership 2>/dev/null \ | awk '{ for (i = 2; i <= NF; i++) if ($i != "root") { print $i; exit } }' } # Ставит мост системно и поднимает его от root как LaunchDaemon. Вызывается # уже после того, как двоичный файл скачан и сверен, поэтому sudo спрашивает # пароль ровно один раз и ровно там, где права нужны. # # Каталог — /Library/Application Support: он всегда принадлежит root, в # отличие от /usr/local, который Homebrew на Intel делает доступным на запись # любому администратору. Подменяемый файл, который root запускает при каждой # загрузке, — ровно та дыра, ради закрытия которой всё и затевается. install_admin_darwin() { # $1=скачанный файл $2=сервер appdir="/Library/Application Support/LifeOS" binary="$appdir/lifeos-bridge" plist="/Library/LaunchDaemons/tech.archik.lifeos.bridge.plist" label="tech.archik.lifeos.bridge" say "Нужны права администратора — sudo спросит пароль." sudo -v || die "Без прав администратора этот уровень установить нельзя." sudo install -d -o root -g wheel -m 755 "$appdir" \ || die "Не удалось создать $appdir" sudo install -o root -g wheel -m 755 "$1" "$binary" \ || die "Не удалось положить мост в $binary" say "Мост установлен: ${binary}" echo # Мост, оставшийся от обычной установки, держал бы второй мост с другим # токеном на той же машине. Один компьютер — один мост, и платформа сама # снимет прежнюю запись устройства — а брошенный автозапуск останется # крутиться с отозванным токеном. Домашняя папка тут не одна: установку с # правами запускают из отдельной учётки администратора, а мост стоял у # того, кто вошёл в систему. for home in "$HOME" "$(console_home)"; do [ -n "$home" ] && [ -d "$home" ] || continue agent="$home/Library/LaunchAgents/${label}.plist" token="$home/.config/lifeos/bridge.json" [ -f "$agent" ] || [ -f "$token" ] || continue sudo launchctl bootout "gui/$(stat -f '%u' "$home")/${label}" 2>/dev/null || true sudo rm -f "$agent" "$token" say "Прежний пользовательский мост убран: ${home}" done # Подключение — от root, чтобы токен лёг туда, где его будет искать демон, # а не в домашнюю папку пользователя. -H тут обязателен: sudo на macOS # оставляет HOME вызывающего, а мост под root отдаёт предпочтение токену # в этом HOME, если он там уже есть, — и демон, стартующий с /var/root, # потом его не найдёт. С --no-autostart мост получает токен и сразу # отдаёт управление: запускать его будет launchd. say "Подключение (выберите на странице уровень «Администратор»):" echo sudo -H "$binary" --server "$2" --no-autostart enroll \ || die "Подключение не завершено — демон не создавался." sudo tee "$plist" >/dev/null < Label${label} ProgramArguments ${binary} --server $2 --no-autostart RunAtLoad KeepAlive PLIST sudo chown root:wheel "$plist" || die "Не удалось назначить владельца $plist" sudo chmod 644 "$plist" # bootout перед bootstrap: launchd отказывается загружать метку, которая # уже числится в системном домене, а переустановка — обычное дело. sudo launchctl bootout system "$plist" 2>/dev/null || true sudo launchctl bootstrap system "$plist" \ || die "launchd не принял $plist" echo say "Готово. Мост работает от имени root и запустится сам при загрузке." say "Проверить: sudo launchctl print system/${label}" } main() { server="${LIFEOS_SERVER:-https://mcp.archik.tech}" bindir="${LIFEOS_BIN_DIR:-$HOME/.local/bin}" admin=0 [ "${LIFEOS_ADMIN:-}" = "1" ] && admin=1 detect_build if [ "$admin" = "1" ] && [ "$os" != "darwin" ]; then die "LIFEOS_ADMIN поддержан только на macOS. На Windows — install.ps1; на Linux ставьте обычным способом и выбирайте уровень «Полный»." fi # Знать пароль администратора и быть администратором — на macOS разные # вещи: право повышаться даёт членство в группе admin, а не пароль. Без # него установка всё равно упрётся в отказ, но уже после скачивания и с # сообщением, из которого непонятно, что делать дальше. if [ "$admin" = "1" ] && [ "$(id -u)" != "0" ]; then case " $(id -Gn) " in *" admin "*) ;; *) echo say "Учётная запись $(id -un) — не администратор, и системный" say "мост от неё не поставить. Выполните в Терминале (пароль" say "спросит сама macOS):" echo account="$(an_admin_account)" say " su ${account:-<администратор>} -c 'curl -fsSL ${server}/devices/install.sh | LIFEOS_ADMIN=1 sh'" echo exit 1 ;; esac fi mkdir -p "$bindir" || die "Не удалось создать $bindir" # Временный файл в той же папке: перенос на место получается мгновенным и # не оставляет полускачанного моста там, куда указывает автозапуск. tmp="$(mktemp "$bindir/.lifeos-bridge.XXXXXX")" || die "Не удалось создать временный файл в $bindir" trap 'rm -f "$tmp"' EXIT INT TERM # Фигурные скобки не для красоты: bash на macOS в UTF-8 локали считает # первый байт многоточия частью имени переменной. Без скобок получается # имя, которого нет, и на нём set -u роняет установку. say "Скачиваем мост для ${build}…" fetch "$server/devices/bridge/$build" "$tmp" \ || die "Не удалось скачать $server/devices/bridge/$build" expected="$(printf '%s\n' "$LIFEOS_SUMS" | awk -v b="$build" '$2 == b { print $1 }')" actual="$(digest "$tmp")" if [ -n "$expected" ] && [ -n "$actual" ] && [ "$expected" != "$actual" ]; then die "Контрольная сумма не совпала. Файл не установлен — попробуйте ещё раз, а если повторится, напишите нам." fi if [ -z "$expected" ] || [ -z "$actual" ]; then say "Проверить контрольную сумму нечем — пропускаем." fi chmod 755 "$tmp" if [ "$admin" = "1" ]; then install_admin_darwin "$tmp" "$server" rm -f "$tmp" trap - EXIT INT TERM return fi target="$bindir/lifeos-bridge" mv -f "$tmp" "$target" || die "Не удалось положить мост в $target" trap - EXIT INT TERM say "Мост установлен: $target" case ":${PATH:-}:" in *":$bindir:"*) ;; *) say "Каталог $bindir не в PATH — запускать мост придётся по полному пути." ;; esac echo # Флаг стоит до команды: разбор аргументов в мосте останавливается на # первом слове без дефиса, и после «enroll» --server уже не увидят. exec "$target" --server "$server" enroll } main "$@"