#!/bin/sh # Стабильный URL для Joplin: http://<подсеть>.100:8080 # Primary IP *.100 через networksetup (Android видит primary, alias — часто нет). # # ./joplin-hotspot-alias.sh status # ./joplin-hotspot-alias.sh ensure|repair # ./joplin-hotspot-alias.sh dhcp # вернуть DHCP (перед домашним Wi‑Fi) # # Passwordless: ./joplin-hotspot-alias-install-sudo.sh set -e CONFIG="${JOPLIN_WEBDAV_CONFIG:-$HOME/.config/joplin-webdav/env}" WIFI_DEV="${WIFI_DEVICE:-en0}" WIFI_SERVICE="${WIFI_SERVICE:-Wi-Fi}" PORT="${JOPLIN_WEBDAV_PORT:-8080}" LOG="$HOME/Library/Logs/joplin-webdav/alias.log" NS="/usr/sbin/networksetup" ipv4_subnet() { printf '%s\n' "$1" | awk -F. '{print $1"."$2"."$3}' } ipv4_last_octet() { printf '%s\n' "$1" | awk -F. '{print $4}' } default_gateway() { route -n get default 2>/dev/null | awk '/gateway:/{print $2; exit}' } load_config() { if [ -f "$CONFIG" ]; then # shellcheck disable=SC1090 . "$CONFIG" fi PORT="${JOPLIN_WEBDAV_PORT:-$PORT}" } joplin_phone_ip() { load_config gw="$(default_gateway 2>/dev/null || true)" [ -n "$gw" ] || return 1 subnet="$(ipv4_subnet "$gw")" if [ -n "${JOPLIN_STATIC_IP:-}" ]; then last="$(ipv4_last_octet "$JOPLIN_STATIC_IP")" else last=100 fi printf '%s\n' "${subnet}.${last}" } phone_url() { ip="$(joplin_phone_ip 2>/dev/null || true)" [ -n "$ip" ] || return 1 printf 'http://%s:%s\n' "$ip" "$PORT" } has_ip_on_dev() { ip="$1" dev="$2" ifconfig "$dev" 2>/dev/null | grep -q "inet ${ip} " } # Убрать *.100 alias с других подсетей (после смены хотспота / Wi‑Fi) prune_stale_aliases() { target="$1" dev="$2" ifconfig "$dev" 2>/dev/null | awk '/inet / {print $2}' | while read -r ip; do case "$ip" in *.*.*.100) [ "$ip" = "$target" ] && continue ifconfig "$dev" -alias "$ip" 2>/dev/null || true log_msg "removed stale alias $ip" ;; esac done } is_android_hotspot_gw() { gw="$1" case "$gw" in 10.*|172.20.10.*|192.168.43.*|192.168.44.*|192.168.49.*) return 0 ;; *) return 1 ;; esac } log_msg() { mkdir -p "$(dirname "$LOG")" printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >>"$LOG" } write_phone_url_env() { url="$(phone_url 2>/dev/null || true)" [ -n "$url" ] && [ -f "$CONFIG" ] || return 0 if grep -q '^JOPLIN_PHONE_URL=' "$CONFIG" 2>/dev/null; then sed -i '' "s|^JOPLIN_PHONE_URL=.*|JOPLIN_PHONE_URL=$url|" "$CONFIG" 2>/dev/null \ || sed -i "s|^JOPLIN_PHONE_URL=.*|JOPLIN_PHONE_URL=$url|" "$CONFIG" else printf 'JOPLIN_PHONE_URL=%s\n' "$url" >>"$CONFIG" fi } cmd_status() { load_config target="$(joplin_phone_ip 2>/dev/null || true)" primary="$(ipconfig getifaddr "$WIFI_DEV" 2>/dev/null || true)" gw="$(default_gateway 2>/dev/null || true)" url="$(phone_url 2>/dev/null || true)" echo "mode: static primary *.100 ($WIFI_SERVICE)" echo "device: $WIFI_DEV" echo "primary: ${primary:-(none)}" echo "gateway: ${gw:-(none)}" echo "joplin IP: ${target:-(no gateway)}" if [ -n "$gw" ] && ! is_android_hotspot_gw "$gw"; then echo "network: домашний/роутер Wi‑Fi (не Android-хотспот)" echo " → телефон должен быть в той же Wi‑Fi сети, что Mac" else echo "network: похоже на Android-хотспот" fi if [ -n "$target" ]; then ifconfig "$WIFI_DEV" 2>/dev/null | awk -v t="$target" '/inet / { if ($2 ~ /\.100$/ && $2 != t) print "stale ifconfig: " $2 " (repair)" }' fi if [ -n "$target" ] && [ "$primary" = "$target" ]; then echo "static IP: OK (primary = $target)" elif [ -n "$target" ]; then echo "static IP: MISSING (primary $primary, нужен $target)" echo " fix: $0 repair" fi if [ -n "$url" ]; then echo "" echo "URL на телефоне:" echo " $url" echo "" echo "Проверка: браузер НА ТЕЛЕФОНЕ → URL → запрос логина за 2–3 сек" echo "Если браузер висит — телефон не видит Mac (не Joplin)" echo "" echo "⚠ http://*.local:8080 с телефона НЕ работает (Android не резолвит mDNS клиентов хотспота)" fi } cmd_ensure() { force=0 [ "${1:-}" = --force ] && force=1 target="$(joplin_phone_ip 2>/dev/null || true)" if [ -z "$target" ]; then log_msg "skip: no gateway" exit 0 fi primary="$(ipconfig getifaddr "$WIFI_DEV" 2>/dev/null || true)" if [ -z "$primary" ]; then log_msg "skip: no primary IP" exit 0 fi if [ "$primary" = "$target" ] && [ "$force" -eq 0 ]; then [ "$(id -u)" -ne 0 ] && write_phone_url_env exit 0 fi if [ "$(id -u)" -ne 0 ]; then log_msg "setmanual $target (was $primary)" sudo_arg=ensure [ "$force" -eq 1 ] && sudo_arg="ensure --force" # shellcheck disable=SC2086 if ! sudo -n "$0" $sudo_arg; then echo "static IP FAILED: нужен sudo." >&2 echo " один раз: ./docs/apps/joplin/scripts/joplin-hotspot-alias-install-sudo.sh" >&2 echo " затем: $0 repair" >&2 exit 1 fi write_phone_url_env echo "static IP OK: $target" echo "URL на телефоне: $(phone_url)" exit 0 fi gw="$(default_gateway 2>/dev/null || true)" [ -n "$gw" ] || { log_msg "skip: no gateway"; exit 1; } prune_stale_aliases "$target" "$WIFI_DEV" log_msg "networksetup -setmanual $target gw $gw [root]" "$NS" -setmanual "$WIFI_SERVICE" "$target" "255.255.255.255.0" "$gw" } cmd_dhcp() { if [ "$(id -u)" -ne 0 ]; then if ! sudo -n "$0" dhcp; then echo "Нужен sudo: sudo $NS -setdhcp \"$WIFI_SERVICE\"" >&2 exit 1 fi echo "Wi‑Fi → DHCP" exit 0 fi "$NS" -setdhcp "$WIFI_SERVICE" log_msg "setdhcp $WIFI_SERVICE" } usage() { echo "Usage: $(basename "$0") status|ensure|repair|dhcp" >&2 exit 1 } case "${1:-ensure}" in status) cmd_status ;; ensure) cmd_ensure "${2:-}" ;; repair) "$0" ensure --force ;; dhcp) cmd_dhcp ;; -h|--help) usage ;; *) usage ;; esac