#!/bin/bash # ===================================================================== # Diagnostika Mac pered pokupkoy (tolko chtenie) # # Rabotaet v dva etapa: # etap 1 (~1 min) - harakteristiki, blokirovki, zdorovie diskov # -> otchet uhodit pokupatelyu srazu # etap 2 (~5 min) - nagruzka, temperatury, skorost diskov, pamyat # -> uhodit polnyy otchet # # Nichego ne ustanavlivaet, ne udalyaet, ne menyaet nastroyki, # ne trogaet lichnye fayly i paroli. # Ishodnik otkryt - eto tot zhe fayl, chto lezhit po ssylke. # ===================================================================== BASE_URL=${BASE_URL:-https://mac.sapcraft.net} UPLOAD_URL=${UPLOAD_URL:-$BASE_URL/up} LOAD_SECONDS=${LOAD_SECONDS:-120} MEM_TEST_MB=${MEM_TEST_MB:-4096} DISK_TEST_MB=${DISK_TEST_MB:-1024} WORK=$(mktemp -d /tmp/maccheck.XXXXXX) SMARTCTL="$WORK/smartctl" CPUTEMP="$WORK/osx-cpu-temp" STAMP=$(date +%Y%m%d-%H%M%S) REPORT=${REPORT:-$HOME/Desktop/mac-check-$STAMP.txt} P1="$WORK/part1.txt" P2="$WORK/part2.txt" WARN="" FAIL="" SUDO_OK=0 SELFTEST_DISKS="" CARD="" DISKCARD="" STARTED=$(date +%s) STEP=0 STEPS=10 # fd 3 - живой экран для продавца, туда идёт только прогресс. # Подробности пишутся в файл отчёта. # /dev/tty может существовать и «быть доступным для записи», но не открываться # (сессия без терминала) - поэтому пробуем открыть, а не смотрим на права. if ( : >/dev/tty ) 2>/dev/null; then exec 3>/dev/tty; else exec 3>&1; fi tell() { printf '%s\n' "$*" >&3; } step() { local pct=$(( STEP * 100 / STEPS )) local filled=$(( pct / 5 )) i=0 bar="" while [ $i -lt 20 ]; do if [ $i -lt $filled ]; then bar="${bar}#"; else bar="${bar}."; fi i=$((i+1)) done STEP=$((STEP+1)) tell "$(printf '[%s] %3d%% %d/%d %3dс %s' \ "$bar" "$pct" "$STEP" "$STEPS" "$(( $(date +%s) - STARTED ))" "$1")" printf '\n>>> ШАГ %d/%d: %s\n' "$STEP" "$STEPS" "$1" } hdr() { printf '\n\n===== %s =====\n' "$1"; } sub() { printf '\n--- %s ---\n' "$1"; } note() { printf ' %s\n' "$1"; } warn() { WARN="${WARN} [!] $1"; } fail() { FAIL="${FAIL} [X] $1"; } run_limited() { local secs="$1"; shift local out="$WORK/limited.$$" "$@" > "$out" 2>/dev/null & local p=$! i=0 while kill -0 "$p" 2>/dev/null && [ "$i" -lt "$secs" ]; do sleep 1; i=$((i+1)); done if kill -0 "$p" 2>/dev/null; then kill -9 "$p" 2>/dev/null echo " (журнал не ответил за ${secs}с - раздел пропущен)" fi wait "$p" 2>/dev/null cat "$out" 2>/dev/null rm -f "$out" } cleanup() { [ -n "$LOAD_PIDS" ] && kill $LOAD_PIDS 2>/dev/null [ -n "$SUDO_KEEPALIVE" ] && kill $SUDO_KEEPALIVE 2>/dev/null [ -n "$J_SHUT" ] && kill $J_SHUT 2>/dev/null [ -n "$J_IO" ] && kill $J_IO 2>/dev/null rm -rf "$WORK" 2>/dev/null } trap cleanup EXIT INT TERM ask_sudo() { cat >&3 <<'EOF' ============================================================ ПРОВЕРКА MAC ПЕРЕД ПОКУПКОЙ ============================================================ Скрипт только читает данные о железе. Он ничего не устанавливает, ничего не удаляет и не меняет настройки. Ваши файлы, почта и пароли не читаются. Сейчас macOS попросит пароль администратора - он нужен для проверки пароля прошивки, температур и здоровья диска. Пароль спрашивает сама система, скрипт его не видит. Идёт двумя этапами: сначала быстрая проверка (около минуты), потом нагрузочная (около пяти минут). Прервать в любой момент: Ctrl-C или закрыть окно. ============================================================ EOF if sudo -n true 2>/dev/null || sudo -v; then SUDO_OK=1 ( while sudo -n true 2>/dev/null; do sleep 30; done ) & SUDO_KEEPALIVE=$! else tell " Пароль не введён - часть проверок будет пропущена." fi } # --------------------------------------------------------- инструменты SHA_SMARTCTL=5b051204ef9e5db16c111c382f77ea19c48bf454a33bd3371633ccf1d7168784 SHA_CPUTEMP=c805db9e90487cfb4fbec88191b73fe9d4bae1b91652fb4414d9d6484606da0d SHA_DRIVEDB=7554f4e5d38256e4d5bc5dacf73d36c15f308c5dcd307bcf30c4c01234e8f329 fetch() { local url="$BASE_URL/$1" dst="$2" want="$3" got n=0 while [ $n -lt 3 ]; do n=$((n+1)) curl -fsSL --retry 2 --retry-delay 2 --connect-timeout 20 --max-time 120 \ "$url" -o "$dst" 2>/dev/null got=$(shasum -a 256 "$dst" 2>/dev/null | awk '{print $1}') [ "$got" = "$want" ] && return 0 sleep 2 done rm -f "$dst" return 1 } get_tools() { [ -z "$BASE_URL" ] && return 1 fetch bin/drivedb.h "$WORK/drivedb.h" "$SHA_DRIVEDB" || rm -f "$WORK/drivedb.h" fetch bin/osx-cpu-temp "$CPUTEMP" "$SHA_CPUTEMP" || CPUTEMP="" fetch bin/smartctl "$SMARTCTL" "$SHA_SMARTCTL" || return 1 chmod +x "$SMARTCTL" 2>/dev/null xattr -d com.apple.quarantine "$SMARTCTL" 2>/dev/null if [ -n "$CPUTEMP" ]; then chmod +x "$CPUTEMP" 2>/dev/null xattr -d com.apple.quarantine "$CPUTEMP" 2>/dev/null "$CPUTEMP" -c >/dev/null 2>&1 || CPUTEMP="" fi "$SMARTCTL" --version >/dev/null 2>&1 || return 1 return 0 } read_temp() { local t="" f="" o s if [ -n "$CPUTEMP" ] && [ -x "$CPUTEMP" ]; then o=$("$CPUTEMP" -c -f 2>/dev/null) t=$(echo "$o" | awk -F'[: ]+' '/^CPU:/ {print int($2); exit}') f=$(echo "$o" | awk '{for(i=1;i<=NF;i++) if($i=="RPM" && int($(i-1))>m) m=int($(i-1))} END {if(m) print m}') fi if [ -z "$t" ] || [ "$t" = "0" ]; then if [ "$SUDO_OK" = 1 ]; then s=$(sudo powermetrics --samplers smc -i 500 -n 1 2>/dev/null) t=$(echo "$s" | awk -F': *' '/CPU die temperature/ {print int($2); exit}') f=$(echo "$s" | awk -F': *' '/^Fan/ {print int($2); exit}') fi fi echo "${t:-0} ${f:-0}" } # ============================================================ 1. WHO sec_identity() { hdr "1. ЧТО ЭТО ЗА МАШИНА" sw_vers echo system_profiler SPHardwareDataType 2>/dev/null | sed 's/^ *//' sub "Процессор" sysctl -n machdep.cpu.brand_string 2>/dev/null echo "Ядер физических: $(sysctl -n hw.physicalcpu) логических: $(sysctl -n hw.ncpu)" sub "Board ID и серийник платы" ioreg -l 2>/dev/null | awk -F'"' '/board-id/ {print "board-id: "$4; exit}' ioreg -l 2>/dev/null | awk -F'"' '/IOPlatformSerialNumber/ {print "IOPlatformSerialNumber: "$4; exit}' sub "Память" system_profiler SPMemoryDataType 2>/dev/null | sed 's/^ *//' MODEL=$(sysctl -n hw.model 2>/dev/null) MNAME=$(system_profiler SPHardwareDataType 2>/dev/null | awk -F': *' '/Model Name/ {print $2; exit}') CPUB=$(sysctl -n machdep.cpu.brand_string 2>/dev/null) [ -z "$CPUB" ] && CPUB=$(system_profiler SPHardwareDataType 2>/dev/null | awk -F': *' '/Chip|Processor Name/ {print $2; exit}') SERIAL=$(ioreg -l 2>/dev/null | awk -F'"' '/IOPlatformSerialNumber/ {print $4; exit}') RAMGB=$(( $(sysctl -n hw.memsize) / 1073741824 )) CORES=$(sysctl -n hw.physicalcpu) OSV=$(sw_vers -productVersion 2>/dev/null) [ -z "$SERIAL" ] || [ "$SERIAL" = "Not Available" ] && \ fail "Серийный номер не читается - признак замены или перепрошивки платы." CARD="Машина: ${MNAME:-?} (${MODEL:-?}), macOS $OSV Процессор: ${CPUB:-?} Ядер: $CORES физических Память: ${RAMGB} ГБ Серийный: ${SERIAL:-НЕ ЧИТАЕТСЯ}" } # ==================================================== 2. BLOCKS/LOCKS sec_locks() { hdr "2. БЛОКИРОВКИ" sub "Пароль прошивки (EFI)" if [ "$SUDO_OK" = 1 ]; then FW=$(sudo firmwarepasswd -check 2>&1) echo "$FW" echo "$FW" | grep -qi "Password Enabled: Yes" && \ fail "УСТАНОВЛЕН ПАРОЛЬ ПРОШИВКИ. Без него Mac не переустановить и не загрузить с внешнего диска." echo "$FW" | grep -qi "Password Enabled: No" && note "-> Пароля прошивки нет." else echo "(пропущено - нет прав администратора)" warn "Пароль прошивки не проверен." fi sub "Activation Lock (для T2 и Apple Silicon)" AL=$(system_profiler SPHardwareDataType 2>/dev/null | awk -F': *' '/Activation Lock/ {print $2; exit}') if [ -n "$AL" ]; then echo " Activation Lock Status: $AL" echo "$AL" | grep -qi "Enabled" && \ fail "ВКЛЮЧЁН Activation Lock. Продавец обязан выйти из Apple ID до передачи, иначе Mac не активировать." else echo " (эта модель Activation Lock не показывает - смотрим Find My Mac ниже)" fi sub "Find My Mac (токен в NVRAM)" if nvram -p 2>/dev/null | grep -qi "fmm-mobileme-token-FMM"; then fail "Find My Mac ВКЛЮЧЁН. Продавец должен выйти из iCloud до передачи." else note "-> Токена Find My Mac в NVRAM нет." fi sub "iCloud текущего пользователя" defaults read MobileMeAccounts 2>/dev/null | grep -i "AccountID" || echo " (не найдено - из iCloud уже вышли)" sub "MDM / корпоративная привязка (DEP)" ENR=$(profiles status -type enrollment 2>&1) echo "$ENR" | sed 's/^/ /' echo "$ENR" | grep -qi "Enrolled via DEP: Yes" && \ fail "Машина в корпоративной программе Apple (DEP): после стирания вернётся под чужое управление. НЕ ПОКУПАТЬ." echo "$ENR" | grep -qi "MDM enrollment: Yes" && \ fail "Активна MDM-привязка к сторонней организации." sub "Профили конфигурации" [ "$SUDO_OK" = 1 ] && { sudo profiles show 2>/dev/null | head -30 || sudo profiles -P 2>/dev/null | head -30; } profiles -P 2>/dev/null | head -15 ls -1 "/Library/Managed Preferences" 2>/dev/null | sed 's/^/ managed: /' sub "SIP / FileVault / Recovery" csrutil status 2>&1 | sed 's/^/ /' fdesetup status 2>&1 | sed 's/^/ /' if diskutil list 2>/dev/null | grep -qi "Recovery"; then note "-> Раздел Recovery на месте." else warn "Раздела Recovery нет - переустановка потребует внешней флешки." fi sub "Локальные учётные записи" dscl . list /Users 2>/dev/null | grep -v '^_' | grep -vE '^(daemon|nobody|root)$' | sed 's/^/ /' } # ==================================================== 3. STORAGE/SMART sec_storage() { hdr "3. ДИСКИ И ЗДОРОВЬЕ НАКОПИТЕЛЯ" diskutil list 2>/dev/null sub "Контроллер и скорость линка" system_profiler SPSerialATADataType 2>/dev/null | sed 's/^ *//' system_profiler SPNVMeDataType 2>/dev/null | sed 's/^ *//' LINKS=$(system_profiler SPSerialATADataType 2>/dev/null | grep -i "Negotiated Link Speed") echo "$LINKS" | grep -qi "1.5 Gigabit" && warn "Диск на линке 1.5 Гбит/с - деградировавший шлейф или плохой контакт." echo "$LINKS" | grep -qi "3 Gigabit" && warn "Диск на линке 3 Гбит/с вместо 6 - изношенный SATA-шлейф или плохой контакт." PHYS=$(diskutil list 2>/dev/null | grep -E '^/dev/disk[0-9]+ \((internal|external), physical\)' | cut -d' ' -f1) NDISK=0 for d in $PHYS; do NDISK=$((NDISK+1)) sub "Физический диск $d" diskutil info "$d" 2>/dev/null | grep -E "Device / Media Name|Media Name|Disk Size|Solid State|SMART Status|TRIM|Device Location|Protocol" | sed 's/^ *//' SM=$(diskutil info "$d" 2>/dev/null | awk -F': *' '/SMART Status/ {print $2}' | sed 's/ *$//') case "$SM" in Verified) note "-> macOS S.M.A.R.T.: Verified" ;; "Not Supported") ;; "") ;; *) fail "$d: S.M.A.R.T. = $SM. Диск умирает." ;; esac SSD=$(diskutil info "$d" 2>/dev/null | awk -F': *' '/Solid State/ {print $2}' | sed 's/ *$//') SZ=$(diskutil info "$d" 2>/dev/null | awk -F': *' '/Disk Size/ {print $2}' | sed 's/ (.*//;s/ *$//') if [ "$SSD" = "No" ]; then DISKCARD="${DISKCARD} $d: HDD (механический), $SZ" else DISKCARD="${DISKCARD} $d: SSD, $SZ" fi if [ -x "$SMARTCTL" ] && [ "$SUDO_OK" = 1 ]; then analyze_smart "$d" fi done note "-> Физических дисков найдено: $NDISK" if [ ! -x "$SMARTCTL" ]; then warn "smartctl не загрузился - наработка и износ накопителя НЕ проверены." elif [ "$SUDO_OK" != 1 ]; then warn "Нет прав администратора - наработка и износ накопителя НЕ проверены." fi } smart() { local d="$1" out db="" [ -f "$WORK/drivedb.h" ] && db="-B $WORK/drivedb.h" out=$(sudo "$SMARTCTL" $db -x "$d" 2>&1) if echo "$out" | grep -qiE "Unknown USB bridge|please specify device type|Unsupported USB"; then out=$(sudo "$SMARTCTL" $db -d sat -x "$d" 2>&1) fi echo "$out" } analyze_smart() { local d="$1" local S A db="" S=$(smart "$d") # -x печатает таблицу атрибутов в 8 колонок, -A в классических 10. # Разбираем по -A, чтобы номера колонок не поехали. [ -f "$WORK/drivedb.h" ] && db="-B $WORK/drivedb.h" A=$(sudo "$SMARTCTL" $db -A "$d" 2>/dev/null) echo " -- S.M.A.R.T. ($d) --" echo "$S" | grep -E "^(Model Family|Device Model|Model Number|Serial Number|Firmware Version|User Capacity|Total NVM Capacity|Rotation Rate|Sector Size|SMART support|SMART overall)" | sed 's/^/ /' # ATA: таблица атрибутов. NVMe: секция Health Information. echo "$S" | sed -n '/ID# ATTRIBUTE_NAME/,/^$/p' | sed 's/^/ /' echo "$S" | sed -n '/SMART\/Health Information/,/^$/p' | sed 's/^/ /' echo "$S" | grep -E "^(Current Temperature|Lifetime *Min/Max Temperature|Specified Max Operating|Under/Over Temperature Limit Count)" | sed 's/^/ /' # значение атрибута ATA по имени / по номеру / строка NVMe по ключу araw() { echo "$A" | awk -v n="$1" '$2==n && NF>=10 {print $10; exit}'; } aid() { echo "$A" | awk -v i="$1" '$1==i && NF>=10 {print $10; exit}'; } anorm() { echo "$A" | awk -v n="$1" '$2==n && NF>=10 {print $4; exit}'; } nvme() { echo "$S" | awk -F':' -v k="$1" ' { key=$1; sub(/^[ \t]+/,"",key); sub(/[ \t]+$/,"",key) if (key==k) { v=$2; sub(/\[.*/,"",v); gsub(/[ ,%]/,"",v); print v; exit } }'; } echo "$S" | grep -qiE "self-assessment test result: FAILED|SMART Health Status: FAILED" && \ fail "$d: S.M.A.R.T. общая оценка FAILED - накопитель при смерти." local POH PC RSC PEND UNC CRC WEAR WNAME PU SPARE SPARET MERR UNSAFE DUW POH=$(araw Power_On_Hours); [ -z "$POH" ] && POH=$(araw Power_On_Hours_and_Msec) [ -z "$POH" ] && POH=$(aid 9) [ -z "$POH" ] && POH=$(nvme "Power On Hours") PC=$(araw Power_Cycle_Count); [ -z "$PC" ] && PC=$(aid 12) [ -z "$PC" ] && PC=$(nvme "Power Cycles") RSC=$(araw Reallocated_Sector_Ct); [ -z "$RSC" ] && RSC=$(araw Reallocated_NAND_Blk_Cnt) [ -z "$RSC" ] && RSC=$(aid 5) PEND=$(araw Current_Pending_Sector); [ -z "$PEND" ] && PEND=$(aid 197) UNC=$(araw Offline_Uncorrectable); [ -z "$UNC" ] && UNC=$(aid 198) CRC=$(araw UDMA_CRC_Error_Count); [ -z "$CRC" ] && CRC=$(aid 199) PU=$(nvme "Percentage Used") SPARE=$(nvme "Available Spare"); SPARET=$(nvme "Available Spare Threshold") MERR=$(nvme "Media and Data Integrity Errors") UNSAFE=$(nvme "Unsafe Shutdowns") DUW=$(nvme "Data Units Written") echo " == Ключевые цифры ==" if [ -n "$POH" ] && [ "$POH" -ge 0 ] 2>/dev/null; then echo " Наработка: $POH ч (~$((POH/24)) суток, ~$((POH/8760)) лет)" DISKCARD="${DISKCARD}, наработка ${POH} ч" [ "$POH" -gt 30000 ] 2>/dev/null && warn "$d: наработка $POH часов - накопитель много работал." [ "$POH" -lt 2000 ] 2>/dev/null && note " Наработка мала - накопитель почти новый." else warn "$d: наработка в часах не читается (модель её не отдаёт)." fi [ -n "$PC" ] && echo " Циклов включения: $PC" [ -n "$UNSAFE" ] && echo " Некорректных выключений: $UNSAFE" if [ -n "$RSC" ]; then echo " Переназначенных секторов: $RSC" [ "$RSC" -gt 50 ] 2>/dev/null && fail "$d: $RSC переназначенных секторов - диск сыплется." [ "$RSC" -gt 0 ] 2>/dev/null && [ "$RSC" -le 50 ] 2>/dev/null && warn "$d: есть переназначенные сектора ($RSC)." fi [ -n "$PEND" ] && [ "$PEND" -gt 0 ] 2>/dev/null && fail "$d: $PEND нечитаемых секторов ждут переназначения." [ -n "$UNC" ] && [ "$UNC" -gt 0 ] 2>/dev/null && fail "$d: $UNC неисправимых ошибок чтения." if [ -n "$CRC" ]; then echo " Ошибок CRC на интерфейсе: $CRC" [ "$CRC" -gt 0 ] 2>/dev/null && warn "$d: $CRC ошибок CRC - плохой шлейф или контакт." fi [ -n "$MERR" ] && { echo " Ошибок целостности данных: $MERR" [ "$MERR" -gt 0 ] 2>/dev/null && fail "$d: $MERR ошибок целостности данных - накопитель теряет данные." } # износ: ATA-атрибуты вендоров for a in Wear_Leveling_Count Media_Wearout_Indicator SSD_Life_Left Percent_Lifetime_Remain Percent_Life_Remaining Remaining_Lifetime_Perc; do local v; v=$(anorm "$a") if [ -n "$v" ]; then WEAR="$v"; WNAME="$a"; break; fi done if [ -n "$WEAR" ]; then echo " Ресурс ячеек ($WNAME): $WEAR% осталось" DISKCARD="${DISKCARD}, ресурс ${WEAR}%" [ "$WEAR" -lt 10 ] 2>/dev/null && fail "$d: ресурс ячеек $WEAR% - SSD почти исчерпан." [ "$WEAR" -ge 10 ] 2>/dev/null && [ "$WEAR" -lt 40 ] 2>/dev/null && warn "$d: ресурс ячеек $WEAR% - изношен заметно." fi # износ: NVMe if [ -n "$PU" ]; then echo " Израсходовано ресурса (NVMe Percentage Used): $PU%" DISKCARD="${DISKCARD}, износ ${PU}%" [ "$PU" -gt 80 ] 2>/dev/null && fail "$d: износ $PU% - ресурс на исходе." [ "$PU" -gt 40 ] 2>/dev/null && [ "$PU" -le 80 ] 2>/dev/null && warn "$d: износ $PU%." [ "$PU" -le 10 ] 2>/dev/null && note " Ресурс практически не тронут." fi if [ -n "$SPARE" ]; then echo " Резервных блоков: $SPARE% (порог ${SPARET:-?}%)" [ -n "$SPARET" ] && [ "$SPARE" -lt "$SPARET" ] 2>/dev/null && \ fail "$d: резервных блоков $SPARE% при пороге $SPARET% - накопитель исчерпал подмену." fi if [ -n "$DUW" ] && [ "$DUW" -gt 0 ] 2>/dev/null; then echo " Записано за жизнь: $((DUW / 1953125)) ТБ (Data Units Written: $DUW)" fi local LBAW; LBAW=$(araw Total_LBAs_Written) [ -n "$LBAW" ] && [ "$LBAW" -gt 0 ] 2>/dev/null && \ echo " Записано за жизнь: ~$((LBAW/2097152)) ГБ (Total_LBAs_Written: $LBAW)" if sudo "$SMARTCTL" -t short "$d" >/dev/null 2>&1; then SELFTEST_DISKS="$SELFTEST_DISKS $d" note " Запущен короткий самотест, результат будет в конце отчёта." fi } # ================================================= 4. PANICS (быстро) sec_panics() { hdr "4. ПАНИКИ ЯДРА" PANICS=$(ls -1 /Library/Logs/DiagnosticReports/*.panic 2>/dev/null | wc -l | tr -d ' ') echo " Файлов .panic: $PANICS" ls -lt /Library/Logs/DiagnosticReports/*.panic 2>/dev/null | head -5 | sed 's/^/ /' [ "$PANICS" -gt 3 ] 2>/dev/null && warn "Найдено $PANICS kernel panic - нестабильное железо." } # ================================================= 5. HISTORY # log show распаковывает и декодирует ВЕСЬ журнал за окно, а фильтрует потом, # поэтому пустой результат стоит столько же, сколько полный: минуты на HDD. # Запускаем оба запроса фоном в начале этапа 2 и читаем после прогрева, # когда мы всё равно две минуты просто ждём. start_journal() { log show --last 7d --style compact \ --predicate 'eventMessage CONTAINS "Previous shutdown cause"' \ > "$WORK/j-shutdown.txt" 2>/dev/null & J_SHUT=$!; disown $J_SHUT 2>/dev/null log show --last 3d --style compact \ --predicate 'eventMessage CONTAINS[c] "I/O error" OR eventMessage CONTAINS[c] "media is not present"' \ > "$WORK/j-io.txt" 2>/dev/null & J_IO=$!; disown $J_IO 2>/dev/null } wait_limited() { local secs="$1" p="$2" i=0 [ -z "$p" ] && return 1 while kill -0 "$p" 2>/dev/null && [ "$i" -lt "$secs" ]; do sleep 1; i=$((i+1)); done if kill -0 "$p" 2>/dev/null; then kill -9 "$p" 2>/dev/null; return 1; fi return 0 } sec_history() { hdr "9. ИСТОРИЯ В СИСТЕМНОМ ЖУРНАЛЕ" sub "Причины последних выключений (7 дней)" echo " (-3 / -60 / -62 / -128 = аварийные: перегрев, питание, паника)" if wait_limited 60 "$J_SHUT"; then tail -15 "$WORK/j-shutdown.txt" 2>/dev/null | sed 's/^/ /' else echo " (журнал не успел разобраться - раздел пропущен)" fi sub "Ошибки ввода-вывода (3 дня)" if wait_limited 30 "$J_IO"; then tail -10 "$WORK/j-io.txt" 2>/dev/null | sed 's/^/ /' else echo " (журнал не успел разобраться - раздел пропущен)" fi sub "Аптайм" uptime } # ====================================================== 6. THERMALS sec_thermal() { hdr "5. ТЕМПЕРАТУРЫ ПОД НАГРУЗКОЙ (${LOAD_SECONDS}с)" sub "В покое" set -- $(read_temp) T0=$1; F0=$2 echo " CPU ${T0} C, вентилятор ${F0} rpm" [ "$T0" = "0" ] && warn "Датчик температуры не отвечает - тепловое состояние не оценено." NC=$(sysctl -n hw.ncpu) i=0; LOAD_PIDS="" while [ $i -lt "$NC" ]; do yes > /dev/null 2>&1 & LOAD_PIDS="$LOAD_PIDS $!" disown $! 2>/dev/null i=$((i+1)) done echo " Загружены все $NC потоков." MAXT=0; MAXF=0; t=0 while [ $t -lt "$LOAD_SECONDS" ]; do sleep 10; t=$((t+10)) set -- $(read_temp) CT=$1; FN=$2 SL=$(pmset -g therm 2>/dev/null | awk -F'= *' '/CPU_Speed_Limit/ {print $2}') printf ' %3dс из %dс: CPU %s C вентилятор %s rpm лимит частоты %s%%\n' \ "$t" "$LOAD_SECONDS" "$CT" "$FN" "${SL:-?}" tell " прогрев ${t}/${LOAD_SECONDS}с — CPU ${CT} C, вентилятор ${FN} rpm" [ "$CT" -gt "$MAXT" ] 2>/dev/null && MAXT=$CT [ "$FN" -gt "$MAXF" ] 2>/dev/null && MAXF=$FN [ -n "$SL" ] && [ "$SL" -lt 100 ] 2>/dev/null && warn "Троттлинг: частота срезана до ${SL}% (забитый радиатор или высохшая термопаста)." done kill $LOAD_PIDS 2>/dev/null; LOAD_PIDS="" note "-> Максимум: CPU ${MAXT} C, вентилятор ${MAXF} rpm" [ "$MAXT" -ge 95 ] 2>/dev/null && fail "CPU разогрелся до ${MAXT} C - перегрев, нужна чистка и термопаста." [ "$MAXT" -ge 88 ] 2>/dev/null && [ "$MAXT" -lt 95 ] 2>/dev/null && warn "CPU ${MAXT} C - высоковато, желательна профилактика." [ "$MAXT" -gt 0 ] 2>/dev/null && [ "$MAXT" -lt 88 ] 2>/dev/null && note "-> Температура в норме." [ "$MAXF" = "0" ] && [ "$MAXT" -gt 80 ] 2>/dev/null && fail "Вентилятор не крутится при ${MAXT} C - возможно, мёртв." sub "Термальное состояние" pmset -g therm 2>&1 | sed 's/^/ /' } # ======================================================== 7. DISK SPEED sec_speed() { hdr "6. СКОРОСТЬ ДИСКОВ (${DISK_TEST_MB} МБ)" CANDS="$HOME" for v in /Volumes/*; do [ -d "$v" ] || continue [ -L "$v" ] && continue CANDS="$CANDS $v" done SEEN="" OLDIFS=$IFS; IFS=' ' for MP in $CANDS; do IFS=$OLDIFS [ -d "$MP" ] || continue [ -w "$MP" ] || continue case "$MP" in /Volumes/com.apple.TimeMachine*|/Volumes/Recovery*) continue ;; esac DEV=$(df "$MP" 2>/dev/null | awk 'END{print $1}') case "$DEV" in /dev/*) ;; *) continue ;; esac INFO=$(diskutil info "$DEV" 2>/dev/null) || continue echo "$INFO" | grep -qi "Read-Only Volume: *Yes" && continue WHOLE=$(echo "$INFO" | awk -F': *' '/APFS Physical Store/ {print $2}' | sed 's/ *$//' | head -1) [ -z "$WHOLE" ] && WHOLE=$(echo "$INFO" | awk -F': *' '/Part of Whole/ {print $2}' | sed 's/ *$//' | head -1) case " $SEEN " in *" $WHOLE "*) continue ;; esac SEEN="$SEEN $WHOLE" NAME=$(echo "$INFO" | awk -F': *' '/Volume Name/ {print $2}' | head -1) sub "Том \"$NAME\" ($MP, физически $WHOLE)" F="$MP/.mcspeed.$$" W=$(dd if=/dev/zero of="$F" bs=1048576 count="$DISK_TEST_MB" conv=sync 2>&1 | tail -1) echo " запись: $W" sudo purge 2>/dev/null R=$(dd if="$F" of=/dev/null bs=1048576 2>&1 | tail -1) echo " чтение: $R" rm -f "$F" WS=$(echo "$W" | sed -n 's/.*(\([0-9]*\) bytes\/sec.*/\1/p') if [ -n "$WS" ]; then note "-> запись $((WS/1000000)) МБ/с" [ "$WS" -lt 100000000 ] && \ warn "Том \"$NAME\": запись $((WS/1000000)) МБ/с - для SSD мало. HDD, износ или деградировавший линк." fi done IFS=$OLDIFS [ -z "$SEEN" ] && echo " (не нашлось тома, доступного для записи)" } # ========================================================= 8. MEMORY sec_memory() { hdr "7. БЫСТРЫЙ ТЕСТ ПАМЯТИ (${MEM_TEST_MB} МБ)" echo " Это не замена memtest86: из работающей системы доступна не вся память." # python3 без Xcode CLT - заглушка, которая выбрасывает продавцу диалог # установки инструментов разработчика, поэтому трогаем его аккуратно. # python2 удалён начиная с Monterey, а perl есть везде. if xcode-select -p >/dev/null 2>&1 && /usr/bin/python3 -c '' >/dev/null 2>&1; then echo " (интерпретатор: python3)" RES=$(/usr/bin/python3 - "$MEM_TEST_MB" <<'PYEOF' 2>&1 import sys mb = int(sys.argv[1]); chunk = 64*1024*1024 pat = b'\xa5\x5a\x3c\xc3' * (chunk // 4) blocks = []; errs = 0 try: for _ in range(max(1, mb*1024*1024//chunk)): blocks.append(bytearray(pat)) except MemoryError: pass for b in blocks: if bytes(b) != pat: errs += 1 print("blokov po 64MB: %d oshibok: %d" % (len(blocks), errs)) print("RESULT_ERRORS=%d" % errs) PYEOF ) elif /usr/bin/perl -e 'exit 0' >/dev/null 2>&1; then echo " (интерпретатор: perl)" RES=$(/usr/bin/perl -e ' my $mb = $ARGV[0]; my $chunk = 64*1024*1024; my $pat = "\xa5\x5a\x3c\xc3" x ($chunk/4); my @b; my $n = int($mb*1024*1024/$chunk); $n = 1 if $n < 1; for (1..$n) { push @b, ("\xa5\x5a\x3c\xc3" x ($chunk/4)); } my $e = 0; for my $x (@b) { $e++ if $x ne $pat; } printf("blokov po 64MB: %d oshibok: %d\n", scalar(@b), $e); printf("RESULT_ERRORS=%d\n", $e); ' "$MEM_TEST_MB" 2>&1) else echo " (пропущено - в системе нет ни python3, ни perl)" warn "Тест памяти не выполнен." RES="" fi [ -n "$RES" ] && echo "$RES" | sed 's/^/ /' E=$(echo "$RES" | sed -n 's/RESULT_ERRORS=//p') [ -n "$E" ] && [ "$E" -gt 0 ] 2>/dev/null && fail "Тест памяти нашёл $E сбойных блоков - битая планка RAM." [ -n "$E" ] && [ "$E" = "0" ] && note "-> Грубых сбоев памяти нет." sub "Давление на память" vm_stat 2>/dev/null | head -6 | sed 's/^/ /' } # ====================================================== 9. PERIPHERALS sec_periph() { hdr "8. ПЕРИФЕРИЯ, ПОРТЫ, ПИТАНИЕ" for t in SPPowerDataType SPUSBDataType SPThunderboltDataType SPEthernetDataType SPAirPortDataType SPBluetoothDataType SPDisplaysDataType SPAudioDataType; do sub "$t" system_profiler "$t" 2>/dev/null | sed 's/^ *//' | head -50 done sub "Батарея (для ноутбуков)" CYC=$(system_profiler SPPowerDataType 2>/dev/null | awk -F': *' '/Cycle Count/ {print $2; exit}') COND=$(system_profiler SPPowerDataType 2>/dev/null | awk -F': *' '/Condition/ {print $2; exit}') MAXP=$(system_profiler SPPowerDataType 2>/dev/null | awk -F': *' '/Maximum Capacity/ {print $2; exit}') DES=$(ioreg -rc AppleSmartBattery -w0 2>/dev/null | tr ',' '\n' | awk -F'=' '/"DesignCapacity"/ {gsub(/[^0-9]/,"",$2); print $2; exit}') NOM=$(ioreg -rc AppleSmartBattery -w0 2>/dev/null | tr ',' '\n' | awk -F'=' '/"NominalChargeCapacity"/ {gsub(/[^0-9]/,"",$2); print $2; exit}') if [ -n "$CYC" ]; then echo " Циклов заряда: $CYC" echo " Состояние: ${COND:-?}" echo " Ёмкость: ${MAXP:-?}${DES:+ ($NOM из $DES мАч)}" BATCARD="Батарея: циклов $CYC, состояние ${COND:-?}, ёмкость ${MAXP:-?}" [ "$CYC" -gt 1000 ] 2>/dev/null && warn "Батарея: $CYC циклов - ресурс выработан, скоро замена." [ "$CYC" -gt 500 ] 2>/dev/null && [ "$CYC" -le 1000 ] 2>/dev/null && warn "Батарея: $CYC циклов - изношена заметно." echo "$COND" | grep -qiE "Service|Replace" && fail "Батарея требует обслуживания: $COND" if [ -n "$DES" ] && [ -n "$NOM" ] && [ "$DES" -gt 0 ] 2>/dev/null; then PCT=$(( NOM * 100 / DES )) echo " Здоровье по ёмкости: ${PCT}%" [ "$PCT" -lt 80 ] && warn "Батарея держит ${PCT}% от заводской ёмкости." fi else echo " (батареи нет - это десктоп)" fi } sec_selftest() { [ -z "$SELFTEST_DISKS" ] && return hdr "10. РЕЗУЛЬТАТ САМОТЕСТА ДИСКОВ" for d in $SELFTEST_DISKS; do sub "$d" local L; L=$(sudo "$SMARTCTL" -l selftest "$d" 2>&1) echo "$L" | sed 's/^/ /' echo "$L" | grep -qiE "in progress" && note "-> Самотест ещё идёт." echo "$L" | grep -qiE "Completed without error" && note "-> Самотест пройден." echo "$L" | grep -qiE "Completed: read failure|Completed: unknown failure|Completed: electrical failure|Completed: servo" && \ fail "$d: самотест ЗАВЕРШИЛСЯ ОШИБКОЙ." done } # ========================================================== ВЕРДИКТ verdict() { hdr "$1" echo "ЧТО ЭТО ЗА МАШИНА - сверьте с объявлением:" echo "$CARD" | sed 's/^/ /' [ -n "$BATCARD" ] && echo " $BATCARD" [ -n "$DISKCARD" ] && echo "Накопители:$DISKCARD" echo [ -n "$FAIL" ] && echo "СТОП-ФАКТОРЫ:$FAIL" [ -n "$WARN" ] && { echo; echo "ЗАМЕЧАНИЯ:$WARN"; } [ -z "$FAIL" ] && [ -z "$WARN" ] && echo " Автоматических претензий нет." } upload() { local file="$1" stage="$2" [ -z "$UPLOAD_URL" ] && return curl -fsS --retry 2 --max-time 90 -X POST --data-binary @"$file" \ -H "Content-Type: text/plain" "${UPLOAD_URL}?stage=${stage}" >/dev/null 2>&1 && return # молчим, когда всё хорошо; говорим только если не отправилось tell " Отправить отчёт не удалось - он сохранён на Рабочем столе." } # ============================================================== ХОД ask_sudo tell "" tell "ЭТАП 1 — быстрая проверка" exec >"$P1" 2>&1 echo "=====================================================" echo " Проверка Mac перед покупкой. Дата: $(date)" echo "=====================================================" if get_tools; then echo "Инструменты: $("$SMARTCTL" --version 2>/dev/null | head -1)" else echo "smartctl загрузить не удалось - здоровье накопителя проверено ограниченно." fi step "Характеристики: модель, процессор, память"; sec_identity step "Блокировки: прошивка, Apple ID, MDM"; sec_locks step "Диски и здоровье накопителя"; sec_storage step "Паники ядра"; sec_panics verdict "ИТОГ ЭТАПА 1" exec >&3 2>&3 cp "$P1" "$REPORT" tell "" sed -n '/===== ИТОГ ЭТАПА 1 =====/,$p' "$P1" >&3 tell "" upload "$P1" 1 tell "" tell "ЭТАП 2 — нагрузочная проверка, около пяти минут" exec >"$P2" 2>&1 start_journal # крутится фоном, пока идёт прогрев step "Прогрев процессора, ${LOAD_SECONDS}с"; sec_thermal step "Скорость дисков"; sec_speed step "Тест памяти"; sec_memory step "Периферия, порты, батарея"; sec_periph step "История в системном журнале"; sec_history step "Результат самотеста дисков"; sec_selftest verdict "ИТОГ" exec >&3 2>&3 cat "$P1" "$P2" > "$REPORT" upload "$REPORT" 2 tell "" tell "[####################] 100% проверка закончена, заняла $(( $(date +%s) - STARTED ))с" tell "" sed -n '/===== ИТОГ =====/,$p' "$REPORT" >&3 tell "" tell "Полный отчёт: $REPORT"