#!/system/bin/sh
# On-device one-shot updater. Pulls the latest agent + boot APK + scripts from
# the VPS, reinstalls persistence, and restarts — from a single command:
#
#   su -c 'busybox wget -O- http://tbremote.nozim-dev.uz:8088/device-update.sh | sh'
#
# `su` syntax varies by box and the wrong one fails in a way that looks like a
# missing binary: AOSP toolbox su takes "su <uid> <cmd>" and answers
# "su: invalid uid/gid '-c'" to the line above. On those (TB40 among them):
#   su 0 sh -c 'busybox wget -O- http://tbremote.nozim-dev.uz:8088/device-update.sh | sh'
# The script itself probes both forms — this note is for the human typing it.
#
# Installing the boot APK, writing /system hooks and userinit.sh all need root,
# so the root-only steps are escalated via su when the calling shell is not root
# (the NovaStar console's default 'admin' shell is not). Same plain-HTTP channel
# the agent itself is fetched over (busybox is HTTP-only).
# TB_BASE lets a DNS-less device fetch by IP (http://<ip>:8088); default = domain
BASE="${TB_BASE:-http://tbremote.nozim-dev.uz:8088}"
DIR=/data/local/tmp
cd "$DIR" || exit 1
say() { echo "[update] $*"; }

# Some RK ROMs ship with no working DNS (empty /etc/resolv.conf), so a clean
# hostname gives "wget: bad address" and EVERY fetch below fails — silently,
# because each one just says "kept old". The agent stays online through this
# because it is pinned to TB_SERVER_IP, which is exactly why the box looks
# healthy while no update ever lands.
#
# The hosts pin that fixes DNS is written by install-persistence.sh… which this
# script has to download first. Breaking that circle needs a known IP, so probe
# once and fall back to it. TB_SERVER_FALLBACK_IP overrides.
FALLBACK_IP="${TB_SERVER_FALLBACK_IP:-45.94.58.205}"
BASE_HOST="${BASE#*://}"; BASE_HOST="${BASE_HOST%%[:/]*}"
case "$BASE_HOST" in
  [0-9]*[0-9]) ;;  # numeric host — already IP-addressed, nothing to probe
  *)
    if ! busybox wget -q -O /dev/null "$BASE/device-update.sh" 2>/dev/null; then
      ALT="http://$FALLBACK_IP:8088"
      if busybox wget -q -O /dev/null "$ALT/device-update.sh" 2>/dev/null; then
        say "DNS ishlamadi ($BASE_HOST) — IP orqali davom etyapman: $FALLBACK_IP"
        BASE="$ALT"
      else
        say "⚠️ na DNS, na $FALLBACK_IP javob bermadi — tarmoqni tekshiring"
      fi
    fi ;;
esac

# Root detection: Android 5.1 toolbox `id` has no -u flag (so $(id -u) is empty
# even for uid 0!). Probe busybox first, then fall back to parsing plain `id`.
cur_uid() {
  u="$(busybox id -u 2>/dev/null)"
  [ -n "$u" ] || u="$(id -u 2>/dev/null)"
  case "$u" in
    ''|*[!0-9]*) case "$(id 2>/dev/null)" in "uid=0("*) u=0 ;; *) u="" ;; esac ;;
  esac
  echo "$u"
}
ROOT=0; [ "$(cur_uid)" = 0 ] && ROOT=1
# su syntax varies by box: AOSP toolbox is "su <uid> <cmd...>", others use "su -c".
# Detect a variant that actually yields uid 0, then reuse it for every root step.
SUOK=""
if [ "$ROOT" != 1 ]; then
  for cand in "su 0 sh -c" "su root sh -c" "su -c" "su 0 busybox sh -c"; do
    if [ "$($cand 'busybox id -u' 2>/dev/null | busybox tr -d ' \r\n')" = 0 ]; then SUOK="$cand"; break; fi
  done
fi
asroot() {  # run "$1" as root: directly if already root, via the detected su otherwise
  if [ "$ROOT" = 1 ]; then sh -c "$1"
  elif [ -n "$SUOK" ]; then $SUOK "$1"
  else return 1; fi
}
if [ "$ROOT" = 1 ]; then say "running as root ✓"
elif [ -n "$SUOK" ]; then say "root via: $SUOK ✓"
else say "⚠️ ROOT topilmadi — APK/system hooks o'rnatilmaydi (avval 'su' bilan root shell oling)"; fi

# fetch to .new then swap — overwriting a running binary gives "Text file busy".
# The -s check matters: busybox wget can exit 0 having written nothing, and
# swapping an empty file over a working agent bricks the device until the next run.
get() {
  if busybox wget -O "$2.new" "$BASE/$1" 2>/dev/null && [ -s "$2.new" ]; then
    busybox mv "$2.new" "$2"; chmod 755 "$2"; say "$2 ✓"
  else
    rm -f "$2.new"; say "$2 FAILED (kept old)"; return 1
  fi
}

# The agent ships per-ABI. Serving `tb-agent` (a copy of the 32-bit build) as the
# default name meant every arm64 box ran the 32-bit binary — fine on TB2, but
# arm64-only Android 11 images drop 32-bit support entirely and it simply will
# not exec. Ask for the native build, and fall back to the legacy name so a
# device can still update against a VPS that has not shipped arm64 yet.
ABI="$(getprop ro.product.cpu.abi 2>/dev/null | busybox tr -d ' \r\n')"
case "$ABI" in
  arm64*|aarch64) AGENT_SRC=tb-agent-arm64 ;;
  *)              AGENT_SRC=tb-agent-arm ;;
esac
say "abi=$ABI → $AGENT_SRC"
get "$AGENT_SRC" tb-agent || {
  say "$AGENT_SRC unavailable — trying legacy tb-agent"
  get tb-agent tb-agent
}
get tb-supervisor.sh          tb-supervisor.sh
get tb-install-persistence.sh tb-install-persistence.sh
# The modern-box boot hook is a SEPARATE, opt-in script (writes /system, can
# stop a box booting). Fetched so it is on the device, but NEVER run from here.
get tb-enable-boot-hook.sh    tb-enable-boot-hook.sh 2>/dev/null || true

# OTA-durable layer: the boot APK (pm install needs root/shell privileges).
# Judged by pm's own "Success", NOT by `pm path`: when an upgrade is rejected the
# OLD package is still installed, so pm path succeeds and the update silently
# reports ✓ while nothing changed.
if busybox wget -O tb-boot.apk "$BASE/tb-boot.apk" 2>/dev/null && [ -s tb-boot.apk ]; then
  chmod 644 tb-boot.apk  # the system installer must be able to read it

  # Keep a copy of what is installed BEFORE touching anything. The apk moved
  # into UID 2000 (sharedUserId=android.uid.shell), and a package may not change
  # its UID in place — the only way through is uninstall + install. If the new
  # apk then refuses to install (wrong signing key is the likely reason: that
  # UID is platform-signed), we must be able to put the working one back.
  # Removing the only boot layer and failing to replace it is how a healthy TB2
  # turns into a device that needs an on-site visit.
  OLDPATH="$(asroot "pm path uz.nozim.tbboot" 2>/dev/null | busybox sed -n 's/^package://p' | busybox tr -d ' \r\n')"
  rm -f "$DIR/tb-boot.old.apk"
  [ -n "$OLDPATH" ] && asroot "cp '$OLDPATH' $DIR/tb-boot.old.apk" >/dev/null 2>&1

  PMOUT="$(asroot "pm install -r $DIR/tb-boot.apk" 2>&1)"
  case "$PMOUT" in
    *Success*) say "boot APK installed ✓" ;;
    *SHARED_USER*|*UID_CHANGED*|*INCONSISTENT_CERTIFICATES*|*[Ss]ignature*)
      say "APK UID/imzo o'zgargan — tozalab qayta o'rnatilyapti"
      asroot "pm uninstall uz.nozim.tbboot" >/dev/null 2>&1
      PMOUT="$(asroot "pm install $DIR/tb-boot.apk" 2>&1)"
      case "$PMOUT" in
        *Success*) say "boot APK qayta o'rnatildi ✓" ;;
        *)
          say "boot APK install FAILED — pm chiqishi:"
          echo "$PMOUT"
          if [ -s "$DIR/tb-boot.old.apk" ] \
             && asroot "pm install $DIR/tb-boot.old.apk" 2>&1 | busybox grep -q Success; then
            say "↩️  eski APK tiklandi — boot qatlami saqlanib qoldi (yangisi o'rnatilmadi)"
          else
            say "⚠️ boot APK QATLAMI YO'Q — qurilma reboot'dan keyin qaytmasligi mumkin"
          fi ;;
      esac ;;
    *)
      say "boot APK install FAILED — pm chiqishi:"
      echo "$PMOUT" ;;
  esac
  asroot "am start -n uz.nozim.tbboot/.MainActivity" >/dev/null 2>&1  # clear stopped-state
else
  say "boot APK download FAILED — did you run deploy/update.sh on the server first?"
fi

# reinstall every boot hook (root: /system remount + userinit.sh)
asroot "sh $DIR/tb-install-persistence.sh"

# restart cleanly: stop old supervisor via its lock pid, kill workers, clear the
# lock (dir + legacy pidfile) so the fresh supervisor isn't blocked, relaunch as root
asroot "[ -f $DIR/tb-supervisor.lock/pid ] && kill \$(cat $DIR/tb-supervisor.lock/pid) 2>/dev/null; busybox pkill tb-agent 2>/dev/null; busybox pkill minicap 2>/dev/null; rm -rf $DIR/tb-supervisor.lock $DIR/tb-supervisor.pid"
sleep 2
# Relaunch and VERIFY, retrying with different detach forms.
#
# The line above KILLS a working agent, and this used to have exactly one chance
# to bring it back — with no check that it did. A single failure here (a real
# one: `Done (127)` in the field, the shell mangling the backgrounded command)
# leaves a box that has no boot layer offline until somebody visits it. Every
# form below is one that works on some shell in this fleet; try them until `ps`
# actually shows the agent.
agent_up() { ps 2>/dev/null | busybox grep -v grep | busybox grep -q tb-agent; }
relaunch_form() {
  case "$1" in
    1) asroot "nohup sh $DIR/tb-supervisor.sh >/dev/null 2>&1 &" ;;
    2) asroot "( busybox setsid sh $DIR/tb-supervisor.sh </dev/null >/dev/null 2>&1 & )" ;;
    3) asroot "busybox nohup sh $DIR/tb-supervisor.sh >/dev/null 2>&1 &" ;;
    4) asroot "sh -c 'sh $DIR/tb-supervisor.sh >/dev/null 2>&1 &'" ;;
  esac
}
form=1
while [ "$form" -le 4 ]; do
  relaunch_form "$form" >/dev/null 2>&1
  tries=0
  while [ "$tries" -lt 5 ]; do
    sleep 2
    agent_up && break 2
    tries=$((tries + 1))
  done
  say "qayta ishga tushirish usuli $form ishlamadi — keyingisi sinalyapti"
  form=$((form + 1))
done

if agent_up; then
  say "✅ yangilandi va ishlayapti"
else
  say "❌ AGENT ISHGA TUSHMADI — bu quti HOZIR OFFLINE."
  say "   Qo'lda ko'taring va chiqishni tekshiring:"
  say "     sh $DIR/tb-supervisor.sh"
  say "   REBOOT QILMANG — boot qatlami bo'lmasa quti qaytmaydi."
fi
if [ "$ROOT" != 1 ] && [ -z "$SUOK" ]; then
  say "eslatma: root olinmadi — avval 'su' (yoki 'su 0') yozib root shell oling, so'ng qayta ishga tushiring"
fi

# --- Which boot layer will actually bring this device back? ---
# Worth printing on every run: "online now" says nothing about surviving a
# reboot, and that gap is exactly how TB40 looked healthy while being unable to
# restart. If every line below is ✗, the device WILL come back offline.
SDK="$(getprop ro.build.version.sdk 2>/dev/null | busybox tr -d ' \r\n')"
say "--- boot persistence (sdk=$SDK abi=$ABI) ---"
[ -f /data/adb/service.d/90tb-agent.sh ] \
  && say "  ✓ service.d hook (root at boot — Magisk/KernelSU)" \
  || say "  ✗ service.d hook (no Magisk/KernelSU)"
# Judge the file init ACTUALLY loads. /system/etc/init/tb-agent.rc is dropped as
# a fallback but init never reads a new file in that directory on these ROMs —
# reporting ✓ for it would be exactly the false reassurance that let a box be
# rebooted into oblivion.
if busybox grep -q "service tb-agent" /system/etc/init/hw/init.rc 2>/dev/null; then
  IWRAP="$(busybox sed -n 's/^service tb-agent [^ ]* //p' /system/etc/init/hw/init.rc 2>/dev/null | busybox head -1)"
  if [ -s "$IWRAP" ]; then
    say "  ✓ init service (root at boot — patched init.rc → $IWRAP)"
  else
    say "  ⛔ init service points at a MISSING script ($IWRAP) — init will loop"
  fi
else
  say "  ✗ init service (system partition not writable / overlay not enabled)"
  # Pre-flight the modern boot hook so the operator knows, BEFORE touching a
  # thing, whether THIS box can safely take it. The overlay disables verity;
  # doing that on a locked `user` build is what bricks the next boot, so only a
  # debuggable/userdebug box is a candidate.
  if [ "${SDK:-0}" -ge 29 ] 2>/dev/null; then
    DBG="$(getprop ro.debuggable 2>/dev/null | busybox tr -d ' \r\n')"
    BT="$(getprop ro.build.type 2>/dev/null | busybox tr -d ' \r\n')"
    if [ "$DBG" = 1 ]; then
      say "    → overlay boot hook MUMKIN (debuggable=$DBG build=$BT). Jismoniy kirish bilan, bitta qutida:"
      say "      su 0 sh -c 'TB_I_HAVE_PHYSICAL_ACCESS=1 sh $DIR/tb-enable-boot-hook.sh'"
    else
      say "    → overlay boot hook XAVFLI (debuggable=$DBG build=$BT) — verity o'chirish bootni buzishi mumkin"
    fi
  fi
fi
if asroot "pm path uz.nozim.tbboot" >/dev/null 2>&1; then
  VC="$(asroot "dumpsys package uz.nozim.tbboot" 2>/dev/null | busybox grep -m1 versionName | busybox tr -d ' \r\n')"
  TBUID="$(asroot "pm list packages -U" 2>/dev/null | busybox grep 'uz.nozim.tbboot ' | busybox sed 's/.*uid://' | busybox tr -d ' \r\n')"
  say "  ✓ boot APK installed (${VC:-version?}, uid=${TBUID:-?})"
  # The whole point of sharedUserId: uid 2000 is group shell, which is the only
  # non-root way to execute a 04750 root:shell vendor su.
  if [ "${TBUID:-0}" = 2000 ]; then
    say "    uid 2000 (shell) → vendor /system/xbin/su is executable ✓"
  else
    say "    ⚠️ uid ${TBUID:-?} is an app uid — a 04750 root:shell su stays unreachable"
  fi
else
  say "  ✗ boot APK NOT installed"
fi
A11Y="$(asroot 'settings get secure enabled_accessibility_services' 2>/dev/null | busybox tr -d ' \r\n')"
case "$A11Y" in
  *uz.nozim.tbboot*) say "  ✓ accessibility autostart enabled" ;;
  *) [ "${SDK:-0}" -ge 24 ] 2>/dev/null && say "  ✗ accessibility autostart NOT enabled" ;;
esac
[ -f /system/bin/install-recovery.sh ] && busybox grep -q tb-agent-hook /system/bin/install-recovery.sh 2>/dev/null \
  && say "  ✓ /system install-recovery hook (pre-Android 10 boxes)" \
  || say "  · /system hook absent (expected on Android 10+: read-only system)"
# A root-side layer is the only thing that reliably survives a reboot on
# Android 10+: either Magisk's service.d or Android's own init service. With
# neither, the box depends on the APK reaching su — which it cannot do where su
# is 04750 root:shell.
# The one line an operator actually needs. Rebooting a box with no root-side
# layer is what strands it: it goes down and nothing brings it back. Judge the
# init hook by the file init really reads (the patched init.rc), not by the
# standalone .rc, which init never loads on these ROMs.
INITRC_OK=0
busybox grep -q "service tb-agent" /system/etc/init/hw/init.rc 2>/dev/null && INITRC_OK=1
if [ -f /data/adb/service.d/90tb-agent.sh ] || [ "$INITRC_OK" = 1 ] || [ "${SDK:-0}" -lt 29 ] 2>/dev/null; then
  say "  ✅ REBOOT XAVFSIZ — boot qatlami bor, quti o'zi qaytadi."
else
  say "  ⛔ REBOOT QILMANG — bu qutida boot qatlami YO'Q, u qaytmaydi."
  say "     Quti hozir ishlayapti; uni shu holda qoldiring."
fi
