Вики IT-KB

Пошаговые руководства, шпаргалки, полезные ссылки...

Инструменты пользователя

Инструменты сайта


apple-mac-os:macos-sequoia:automated-os-deployment-to-apple-mac-and-iphone-or-ipad-mobile-devices

Различия

Показаны различия между двумя версиями страницы.

Ссылка на это сравнение

Предыдущая версия справа и слеваПредыдущая версия
Следующая версия
Предыдущая версия
apple-mac-os:macos-sequoia:automated-os-deployment-to-apple-mac-and-iphone-or-ipad-mobile-devices [10.12.2024 07:40] – удалено - внешнее изменение (Дата неизвестна) 127.0.0.1apple-mac-os:macos-sequoia:automated-os-deployment-to-apple-mac-and-iphone-or-ipad-mobile-devices [10.12.2024 07:45] (текущий) – ↷ Операцией перемещения обновлены ссылки Алексей Максимов
Строка 1: Строка 1:
 +===== Автоматизированное развёртывание OC на Apple Mac и мобильные устройства iPhone или iPad =====
  
 +Идея подсмотрена на Российской ежегодной [[https://mac.shortcut.ru|конференции]] системных администраторов Apple.
 +
 +Реализация в целом похожа, в представленном варианте используется **cfgutil**, который запускает скрипт **AppleScript** с логикой обработки подключенных **Mac**, **iPhone** или **iPad**. Последний доступный образ восстановления при необходимости скачивается автоматически. Так же выполняется логирование для понимания процесса. Для перевода Mac в DFU при условии правильного подключения используется **macvdmtool**. iPhone и iPad, само собой, переводим в DFU в ручном режиме.
 +
 +\\
 +==== Основная логика ====
 +
 +Скрипт ''Restore.scpt'' разместим здесь ''/Library/Application Support/Custom/RestoreDevices/''
 +
 +<code AppleScript>
 +-- В скрипте используется утилита jq, которая встроена в macOS начиная с Sequoia.
 +-- При использовании более ранних версий, jq нужно установить дополнительно.
 +-- Перед использованием с AppStore установить Apple Configurator 2, и выполнить установку средств автоматизации из меню.
 +
 +--Объявляем глобальную переменную для работы в обработчиках
 +global LogPath
 +set LogPath to "/tmp/DevsRestore/logs/"
 +
 +
 +-- Получаем список подключенных устройств с помощью cfgutil
 +set cfgutilResult to my cfgutilDevList()
 +-- Получаем список подключенных устройств с помощью system_profiler
 +set SPResult to my systemprofilerDevList()
 +-- Объединяем списки для получения требуемой инф. в одной списке
 +set MergedList to my MergeLists(cfgutilResult, SPResult)
 +--return MergedList
 +
 +-- Проходим по каждому устройству в подписке списка
 +repeat with i from 1 to count MergedList
 +  -- Берём первое устройство
 +  set thisDevice to item i of MergedList
 +  -- Записываем атрибуты устройства
 +  set DeviceType to item 1 of thisDevice
 +  set DeviceClass to item 2 of thisDevice
 +  set SerialNumber to item 3 of thisDevice
 +  set ECID to item 4 of thisDevice
 +  set LocationID to item 5 of thisDevice
 +  -- Формируем имя Log-файла
 +  set LogFileName to ECID & ".log"
 +  
 +  --Логируем найденное устройство
 +  my WriteLog("Найдено устройство:" & space & DeviceType & space & "серийный номер:" & space & SerialNumber & "," & space ¬
 +    & "ECID:" & space & ECID & space & "в порте:" & space & LocationID, LogFileName)
 +  
 +  -- Выполняем подготовку  
 +  -- Формируем ссылку для получения доступных IPSW
 +  set ChooseUrl to "https://api.ipsw.me/v4/device/" & DeviceType & "?type=ipsw"
 +  --Логируем
 +  my WriteLog("Получаем информацию о последней версии ОС по ссылке:" & space & ChooseUrl, LogFileName)
 +  -- Получаем информацию о последней доступной версии ОС
 +  set BaseJSON to my GetJSON(ChooseUrl, LogFileName)
 +  -- Парсим JSON, если он был получен
 +  if BaseJSON is false then return
 +  -- Получаем контрольную сумму из JSON
 +  set CloudCheckSum to do shell script "echo" & space & quoted form of BaseJSON & space & "| jq -r '.sha1sum'"
 +  -- Получаем ссылку для скачивания "образа" IPSW из JSON
 +  set IPSWUrl to do shell script "echo" & space & quoted form of BaseJSON & space & "| jq -r '.url'"
 +  -- Получаем имя файла из JSON
 +  set CloudIPSWName to do shell script "basename" & space & quoted form of IPSWUrl
 +  -- Указываем родительский каталог для сохранения "образов" IPSW
 +  set IPSWRootPath to (POSIX path of "/Library/Application Support/Custom/RestoreDevices/IPSWs/")
 +  -- Формируем путь до файла "образа" IPSW 
 +  set CurrentIPSWPath to (IPSWRootPath & DeviceType & "/" & CloudIPSWName)
 +  -- Если файл существует, сверяем контрольные суммы.
 +  -- Если файла нет -- скачиваем. Скачивание файла может занимать длительное время.
 +  set SelectIPSWStatus to my SelectIPSW(CloudCheckSum, CurrentIPSWPath, IPSWRootPath, DeviceType, IPSWUrl, LogFileName)
 +  -- При возникновении ошибки процесс останавливается
 +  if SelectIPSWStatus is not true then return
 +  -- запускаем восстановление
 +  my Restore(LocationID, ECID, DeviceClass, CurrentIPSWPath, LogFileName)
 +  
 +end repeat
 +
 +
 +
 +--===================
 +--Обработчики
 +--===================
 +
 +-- Проверить или скачать IPSW
 +--===================
 +on SelectIPSW(TrustedCheckSum, thisFile, RootFolder, SubFolder, thisURL, LogFileName)
 +  -- Создаём каталоги для загрузки IPSW, если необходимо
 +  my CreateFolder(RootFolder, SubFolder, LogFileName)
 +  -- Проверяем контрольную сумму существующего файла
 +  set StatusCheckSum to my CheckSum(TrustedCheckSum, thisFile, LogFileName)
 +  if StatusCheckSum is true then
 +    return true
 +  else
 +    -- Скачиваем
 +    set GetIPSWStatus to my GetIPSW(thisFile, thisURL, TrustedCheckSum, LogFileName)
 +    if GetIPSWStatus is true then
 +      return true
 +    else
 +      return false
 +    end if
 +  end if
 +end SelectIPSW
 +--===================
 +
 +
 +
 +-- Получить JSON
 +--===================
 +on GetJSON(thisURL, LogFileName)
 +  -- Запрос всех доступных IPSW для подключенной модели  
 +  set thisJSON to do shell script "curl" & space & thisURL & space & "| jq '.firmwares'"
 +  -- Если возврат "null", то нет доступных IPSW
 +  if thisJSON is "null" then
 +    my WriteLog("Нет доступных IPSW. Остановка процесса.", LogFileName)
 +    return false
 +  else
 +    -- JSON получен, сортируем элементы массива по дате релиза и берём последний
 +    set thisJSON to do shell script "echo" & space & quoted form of thisJSON & space & "| jq -r 'sort_by(.releasedate) | last'"
 +    my WriteLog("Ответ сервера в формате JSON" & linefeed & thisJSON, LogFileName)
 +    
 +    return thisJSON
 +    
 +  end if
 +  
 +end GetJSON
 +--===================
 +
 +
 +-- Скачать IPSW
 +--===================
 +on GetIPSW(thisFile, thisURL, TrustedCheckSum, LogFileName)
 +  -- Поиск параллельных процессов скачивания IPSW образов
 +  set UserName to short user name of (system info)
 +  set PSShellCommand to "ps -u" & space & UserName & space & "| grep '[c]url' | grep" & space & quoted form of thisURL & space & "> /dev/null 2>&1; echo $?"
 +  set CurrentDownloads to (do shell script PSShellCommand) as integer
 +  
 +  -- Если процесс curl был найден, возврат 0
 +  if CurrentDownloads is 0 then
 +    -- Устанавливаем счётчик повторных запусков на 0
 +    set Counter to 0
 +    -- Повторяем цикл 5 раз
 +    repeat while Counter < 6
 +      my WriteLog("Обнаружено параллельное скачивание файла по ссылке" & space & thisURL & linefeed & "Проверим ещё раз через 10 минут.", LogFileName)
 +      -- пауза в секундах
 +      delay 600
 +      -- Добавляем к счётчику 1 выполненный проход
 +      set Counter to Counter + 1
 +      -- Проверяем ещё раз
 +      set CurrentDownloads to (do shell script PSShellCommand) as integer
 +      -- Если очередная проверка не обнаружила параллельный процесс, считаем что он завершён
 +      if CurrentDownloads is 1 then
 +        -- Процесс завершён, можно продолжать
 +        my WriteLog("Параллельное скачивание завершено. Повторно выполним проверку контрольной суммы.", LogFileName)
 +        set StatusCheckSum to my CheckSum(TrustedCheckSum, thisFile, LogFileName)
 +        if StatusCheckSum is true then
 +          return true
 +        else
 +          my WriteLog("Возможно, параллельный процесс загрузки был завершён некорректно", LogFileName)
 +          my WriteLog("Попробуйте в ручную загрузить файл по ссылке" & linefeed & thisURL, LogFileName)
 +          my WriteLog("Остановка процесса.", LogFileName)
 +          return false
 +        end if
 +        exit repeat
 +      end if
 +      -- При достижении счётчика, останавливаем процесс
 +      if Counter is 5 then
 +        my WriteLog("За последний час процесс загрузки образа не был завершён или есть зависший процесс curl.", LogFileName)
 +        my WriteLog("Остановка процесса.", LogFileName)
 +        return false
 +      end if
 +      
 +    end repeat
 +    
 +    -- Если процесс curl не был найден, возврат 1
 +  else if CurrentDownloads is 1 then
 +    set curlShellCommand to "curl -L --retry 5 --retry-delay 10 --retry-all-errors -J -o" & space & quoted form of thisFile & space & thisURL
 +    my WriteLog("Запускаем скачивание:" & linefeed & curlShellCommand, LogFileName)
 +    try
 +      do shell script curlShellCommand
 +      my WriteLog("Файл скачен", LogFileName)
 +      return true
 +    on error errormsg number errNumber
 +      my WriteLog("Проблема скачивания файла. Curl вернул ошибку:" & space & errNumber, LogFileName)
 +      my WriteLog("Коды возврата можно посмотреть в \"man curl\" в разделе \"EXIT CODES\"", LogFileName)
 +      my WriteLog("Остановка процесса.", LogFileName)
 +      return false
 +    end try
 +  end if
 +end GetIPSW
 +--===================
 +
 +
 +--===================
 +-- Записать лог
 +on WriteLog(message, logFile)
 +  
 +  --do shell script "mkdir -p" & space & quoted form of POSIX path of (do shell script "dirname" & space & quoted form of LogPath)
 +  do shell script "mkdir -p" & space & LogPath
 +  set timeStamp to (do shell script "date +'%Y-%m-%d %H:%M:%S'")
 +  set logMessage to timeStamp & space & "-" & space & message & linefeed
 +  do shell script "echo" & space & quoted form of logMessage & space & ">>" & space & quoted form of (LogPath & logFile)
 +  
 +end WriteLog
 +--===================
 +
 +
 +--===================
 +--Создать каталог для хранения IPSW этой модели
 +on CreateFolder(RootFolder, SubFolder, LogFileName)
 +  
 +  -- Формируем полный путь к каталогу
 +  set targetFolder to RootFolder & SubFolder
 +  -- Проверяем существование каталога
 +  try
 +    -- Классические методы проверки существования каталога или файла используя вызов "System Events"
 +    -- не могут быть использованы, т.к. задача запускает от RootFolder, без GUI.
 +    do shell script "test -d" & space & quoted form of targetFolder
 +    my WriteLog("Каталог для IPSW файла уже существует:" & space & targetFolder, LogFileName)
 +  on error
 +    -- Создать каталог
 +    do shell script "mkdir -p" & space & quoted form of targetFolder
 +    my WriteLog("Создан каталог для сохранения IPSW файла:" & space & targetFolder, LogFileName)
 +  end try
 +  
 +end CreateFolder --===================
 +
 +
 +--===================
 +--Сверка контрольной суммы
 +on CheckSum(TrustedCheckSum, thisFile, LogFileName)
 +  
 +  -- Проверяем существование файла
 +  try
 +    -- Классические методы проверки существования каталога или файла используя вызов "System Events"
 +    -- не могут быть использованы, т.к. задача запускает от root, без GUI.
 +    do shell script "test -f" & space & quoted form of thisFile
 +  on error
 +    return false
 +  end try
 +  
 +  -- Файл существует, продолжаем проверку
 +  my WriteLog("Начинаем проверку контрольной суммы файла" & space & thisFile, LogFileName)
 +  
 +  -- Вычисляем контрольную сумму
 +  set thisCheckSum to word 1 of (do shell script "shasum -a 1" & space & quoted form of thisFile)
 +  if TrustedCheckSum is equal to thisCheckSum then
 +    my WriteLog("Контрольные суммы совпадают", LogFileName)
 +    return true
 +  else
 +    my WriteLog("Контрольные суммы не совпадают:" & space & TrustedCheckSum & space & "≠" & space & thisCheckSum, LogFileName)
 +    return false
 +  end if
 +  
 +end CheckSum
 +--===================
 +
 +
 +--===================
 +--Перезагрузка в DFU
 +on RebootToDFU(LocationID, DeviceCalss, LogFileName)
 +  
 +  -- Список DFU портов.
 +  -- Обычно DFU порт хоста имеет идентификатор "0x100000", но зная Apple, в других модeлях это может быть изменено.  
 +  set DFUPortList to {"0x100000"}
 +  set macvdmtool to "/usr/local/bin/macvdmtool"
 +  set macvdmtoolShellCommand to macvdmtool & space & "dfu"
 +  -- Проверяем существование macvdmtool 
 +  try
 +    -- Классические методы проверки существования каталога или файла используя вызов "System Events"
 +    -- не могут быть использованы, т.к. задача запускает от root, без GUI.
 +    do shell script "test -f" & space & macvdmtool
 +    
 +    -- Если подключенный Mac подключен в DFU порт хоста
 +    if LocationID is in DFUPortList then
 +      my WriteLog("Попытка перезагрузить подключенный" & space & DeviceCalss & space & "в DFU режим" & linefeed & macvdmtoolShellCommand, LogFileName)
 +      try
 +        set macvdmtoolresult to do shell script macvdmtoolShellCommand
 +        my WriteLog("Лог утилиты macvdmtool:" & linefeed & macvdmtoolresult, LogFileName)
 +        my WriteLog("Mac" & space & DeviceCalss & space & "перезагружен в DFU режим", LogFileName)
 +      on error errormsg
 +        my WriteLog("Этот" & space & DeviceCalss & space & "не может быть переведён в DFU режим." & linefeed ¬
 +          & "Убедитесь что это Mac с процессором Apple, кабель подключен в DFU порт и не используется кабель Thunderbol 3/4.", LogFileName)
 +        my WriteLog("Лог утилиты macvdmtool:" & linefeed & errormsg, LogFileName)
 +        my WriteLog("Остановка процесса.", LogFileName)
 +        
 +        return false
 +        
 +      end try
 +    end if
 +  on error
 +    my WriteLog("Утилита macvdmtool отсутствует. Остановка процесса.", LogFileName)
 +    
 +    return false
 +    
 +  end try
 +  
 +end RebootToDFU
 +--===================
 +
 +
 +--===================
 +--Восстановление с DFU
 +on Restore(LocationID, ECID, DeviceCalss, PathToIPSW, LogFileName)
 +  
 +  if (DeviceCalss contains "Mac") then
 +    -- Выполняем попытку перезагрузки в DFU режим
 +    set DFUStatus to my RebootToDFU(LocationID, DeviceCalss, LogFileName)
 +    if DFUStatus is false then return
 +  end if
 +  -- Запуск восстановления в фоновом режиме
 +  set cfgutilShellCommand to "/usr/local/bin/cfgutil --progress --verbose -e" & space & ECID & space & "restore -I" & space & quoted form of PathToIPSW & space & ">>" & space & LogPath & ECID & ".log 2>&1 &"
 +  WriteLog("Запуск процесса восстановления:" & linefeed & cfgutilShellCommand, LogFileName)
 +  WriteLog("Лог работы cfgutil:" & linefeed, LogFileName)
 +  do shell script cfgutilShellCommand
 +  
 +end Restore
 +--===================
 +
 +--===================
 +--Поиск подключенных устройств с помощью cfgutil
 +on cfgutilDevList()
 +  
 +  -- Получаем инф. о подключенных устройствах в формате JSON
 +  set BaseJSON to do shell script "/usr/local/bin/cfgutil --format JSON list | jq"
 +  -- Парсим JSON, выбирая нужные данные и сохраняем контент как CVS, разделитель ";"
 +  set ParsedCSV to do shell script "echo" & space & quoted form of BaseJSON & space & "| jq -r '.Output | to_entries[] | [.value.locationID, .value.ECID, .value.deviceType] | join(\";\")'"
 +  -- Разбиваем на строки
 +  set LineCSV to paragraphs of ParsedCSV
 +  
 +  -- Пустой итоговый список
 +  set ResultList to {}
 +  
 +  -- Цикл обработки строк
 +  repeat with thisDev in LineCSV
 +    -- Используем разделитель ";" для работы с CSV
 +    set text item delimiters to ";"
 +    -- Развиваем строку на части разделителем установленным разделителем
 +    set CSVParts to text items of thisDev
 +    -- Сбрасываем разделитель
 +    set text item delimiters to ""
 +    
 +    -- Извлекаем значения
 +    -- Первая часть Location ID
 +    set LocationID to item 1 of CSVParts
 +    -- Вторая часть ECID
 +    set ECID to item 2 of CSVParts
 +    -- Третья часть DeviceType
 +    set DeviceType to item 3 of CSVParts
 +    
 +    -- Модифицируем полученные данные приводя в нужный вид
 +    -- при использовании вывода инф. о девайсах в не текстовом формате, то используется целочисленное значение Location ID.
 +    -- Конвертируем в HEX 1048576 -> 0x100000
 +    set LocationID to "0x" & (do shell script "printf '%X' " & LocationID)
 +    
 +    -- Выполняем проверку существования log-файла с именем ECID устройства,
 +    -- Если лог есть, то считаем, что запускать повторно процедуру восстановления не нужно. Не добавляем в список результатов.
 +    try
 +      do shell script "test -f" & space & quoted form of (LogPath & ECID & ".log")
 +    on error
 +      -- Добавляем в результат
 +      set end of ResultList to {DeviceType, ECID, LocationID}
 +    end try
 +  end repeat
 +  
 +  return ResultList
 +  
 +  --Пример вывода
 +  --{{"iPhone8,4", "0x00480000000E4", "0x8320000"}, {"Macmini9,1", "0x928200000001E", "0x100000"}, {"iPhone7,2", "0x19610000000026", "0x8310000"}}
 +  
 +end cfgutilDevList
 +--===================
 +
 +--===================
 +--Поиск подключенных устройств с помощью system_profiler
 +on systemprofilerDevList()
 +  --Ищем среди подключенных USB устройств имена в которых содержится Mac, DFU или Recovery. 
 +  -- Отображаем найденные строки, а так же 5 и 8 строку для отображения Serial Number и 
 +  -- Location. Location необходим в качестве ключа для объединения со списком cfgutilDevList()
 +  set SPUSBInfo to do shell script "system_profiler SPUSBDataType | awk '{
 +          lines[NR] = $0
 +      } 
 +      /Mac|DFU|Recovery/
 +          mac_dfu_recovery_line = NR
 +           mac_dfu_recovery_found = 1
 +      } 
 +      /Serial Number:/ &&  mac_dfu_recovery_found && (NR == mac_dfu_recovery_line + 5) { 
 +          serial_number_line = NR 
 +      } 
 +      /Location ID:/ &&  mac_dfu_recovery_found && (NR == mac_dfu_recovery_line + 8) {
 +          if ( mac_dfu_recovery_found && serial_number_line && NR == mac_dfu_recovery_line + 8) {
 +              sub(/^ +/, \"\", lines[mac_dfu_recovery_line])
 +              sub(/^ +/, \"\", lines[serial_number_line])
 +              sub(/^ +/, \"\", $0)
 +              
 +              print lines[mac_dfu_recovery_line]
 +              print lines[serial_number_line]
 +              print $0
 +      
 +               mac_dfu_recovery_found = 0
 +              serial_number_line = 0
 +          }
 +      }'"
 +  
 +  -- Разделяем вывод на строки
 +  set SPUSBInfoLines to paragraphs of SPUSBInfo
 +  
 +  -- Пустой итоговый список
 +  set ResultList to {}
 +  
 +  -- Извлекаем значения проходя по каждой строке с шагом 3
 +  repeat with i from 1 to count of SPUSBInfoLines by 3
 +    set Model to item i of SPUSBInfoLines
 +    set Serial to item (i + 1) of SPUSBInfoLines
 +    set LocationID to item (i + 2) of SPUSBInfoLines
 +    
 +    -- Модифицируем полученные данные приводя в нужный вид
 +    --Последний символ строки ":", уберём это
 +    set Model to text 1 thru -2 of Model
 +    -- Серийный номер подключенный Mac отображается сразу, у iOS строка содержит множество другой информации
 +    -- Mac загруженный в DFU режиме не отображает серийный номер.
 +    set Serial to do shell script "echo" & space & Serial & space & "| awk '
 +                         /Serial Number:/ {
 +                      value = substr($0, index($0, \"Serial Number:\") + 15);
 +                      if (value ~ /^[0-9A-Za-z]{6,}/) {
 +                          print value;
 +                          found = 1;
 +                      }
 +                  }
 +                  /SRNM:/ {
 +                      split($0, arr, /SRNM:\\[|\\]/);
 +                      if (length(arr) > 1) {
 +                          print arr[2];
 +                          found = 1;
 +                      }
 +                      next;
 +                  }
 +                  END {
 +                      if (found == 0) print \"Serial Number not found\";
 +                  }
 +              '"
 +    
 +    (*
 +      Меняем формат Location, т.к. у system_profiler и cfgutil они отличаются
 +      Пример
 +      0x00100000 --> 0x100000
 +      0x08310000 --> 0x8310000
 +      Поэтому конвертируем 0x08310000 в 8310000 и добавляем 0x спереди, чтобы значение соответствовало формату cfgutil
 +  *)
 +    --Вырезаем значение из строки
 +    set LocationID to text 14 thru 23 of LocationID
 +    set LocationID to "0x" & (do shell script "printf '%X' " & LocationID)
 +    
 +    -- Добавляем инф о каждом устройстве в подсписке в список
 +    set end of ResultList to {Model, Serial, LocationID}
 +  end repeat
 +  
 +  -- Выводим итоговый список
 +  return ResultList
 +  
 +  --Пример вывода
 +  --{{"Mac mini", "H2000000Q6NY", "0x100000"}, {"Apple Mobile Device (Recovery Mode)", "DX6000000TVL", "0x8320000"}, {"Apple Mobile Device (Recovery Mode)", "F170000005MR", "0x8310000"}}
 +  
 +end systemprofilerDevList
 +--===================
 +
 +-- Обработчик для объединения списков
 +--https://www.macscripter.net/t/merge-2-arrays-by-key/76457/10
 +--===================
 +on MergeLists(List1, List2)
 +  -- Создаём пустой итоговый список
 +  set ResultList to {}
 +  
 +  -- Проходим по первому списку
 +  repeat with subList1 in List1
 +    -- Извлекаем значения подсписков из первого списка "List1" в подсписок "subList1"
 +    set {Value01, Value02, ID01} to subList1
 +    
 +    -- Проходим по второму списку
 +    repeat with subList2 in List2
 +      -- Извлекаем значения подсписков из второго списка "List2" в подсписок "subList2"
 +      set {Value11, Value12, ID11} to subList2
 +      --Если ключевые значения равны, создаём итоговый новый список
 +      if ID01 is equal to ID11 then
 +        set end of ResultList to {Value01, Value11, Value12, Value02, ID01}
 +        exit repeat
 +      end if
 +    end repeat
 +  end repeat
 +  
 +  -- Выводим итоговый список
 +  return ResultList
 +  
 +end MergeLists
 +--===================
 +</code>
 +
 +
 +\\
 +==== Создание LaunchDaemon ====
 +
 +Создадим Launch Daemon
 +
 +<code>sudo nano /Library/LaunchDaemons/com.custom.restoredevices.plist</code>
 +
 +<code XML>
 +<?xml version="1.0" encoding="UTF-8"?>
 +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 +<plist version="1.0">
 +<dict>
 + <key>Label</key>
 + <string>custom.restoredevices</string>
 + <key>ProgramArguments</key>
 + <array>
 + <string>/usr/local/bin/cfgutil</string>
 + <string>exec</string>
 + <string>--on-attach</string>
 + <string>osascript '/Library/Application Support/Custom/RestoreDevices/Restore.scpt'</string>
 + </array>
 + <key>RunAtLoad</key>
 + <true/>
 + <key>StandardErrorPath</key>
 + <string>/tmp/custom.restoredevices.stderr</string>
 + <key>StandardOutPath</key>
 + <string>/tmp/custom.restoredevices.stdout</string>
 +</dict>
 +</plist>
 +</code>
 +
 +Убедимся, что plist не содержит ошибок
 +
 +<code>plutil -lint /Library/LaunchDaemons/com.custom.restoredevices.plist</code>
 +
 +
 +Загрузим и запустим демон
 +
 +<code>sudo launchctl bootstrap system/ /Library/LaunchDaemons/com.custom.restoredevices.plist</code>
 +
 +
 +Убедимся, что демон запущен
 +
 +<code>sudo launchctl list | grep restoredev</code>
 +
 +
 +Найдём процесс
 +
 +<code>ps aux | grep '[c]fgutil'</code>
 +
 +
 +
 +Launch Daemon работает независимо от входа пользователя и будет готов к работе сразу после загрузки macOS.
 +Если что-то пошло не так, то необработанные ошибки будут сохранятся в файле ''custom.restoredevices.stderr'', прочие информационные сообщения ''/tmp/custom.restoredevices.stdout''
 +
 +При ненадёжном или медленном интернет соединении образы восстановления лучше скачать заблаговременно.
 +
 +\\
 +==== Пример логирования процесса ====
 +
 +Процесс восстановления логируется созданным обработчиком WriteLog(message, logFile), на выходе мы получаем примерно такие логи:
 +
 +Восстановление iPhone
 +
 +{{ apple-mac-os:macos-sequoia:iphone6-restore-log.png }}
 +
 +Восстановление Mac
 +
 +{{ apple-mac-os:macos-sequoia:macmini2020-restore-log.png }}
 +
 +\\
 +==== Другой вариант получения списка подключенных устройств ====
 +
 +Администраторы, которые уже погружались в автоматизацию Apple Configurator могут сказать, что лепнина из двух обработчиков systemprofilerDevList() и cfgutilDevList(), а затем ещё и объединение результатов обработчиком MergeLists() лишнее и ненужное звено, потому что производитель уже предоставил встроенные инструменты, получить информацию о подключенных устройствах можно так:
 +
 +<code AppleScript>
 +set DevicesList to my DevicesList()
 +
 +on DevicesList()
 +  
 +  try
 +    tell script "Configuration Utility"
 +      -- Вызываем необходимые свойства и присваиваем значения в переменные
 +      copy CNFGvaluesOfSpecifiedProperties("all", {"deviceType", "ECID", "locationID", "serialNumber", "bootedState"}, false) to ¬
 +        {deviceCount ¬
 +          , propertyTitles ¬
 +          , DeviceTypes ¬
 +          , deviceECIDs ¬
 +          , LocationIDs ¬
 +          , serialNumbers ¬
 +          , BootedStates}
 +    end tell
 +    
 +    -- Создаём пустой список
 +    set ResultList to {}
 +    --Получаем значения атрибутов для каждого устройства
 +    repeat with i from 1 to the deviceCount
 +      set DeviceType to item i of DeviceTypes
 +      set ECID to item i of deviceECIDs
 +      set LocationID to item i of LocationIDs
 +      set SerialNumber to item i of serialNumbers -- серийные номера отображаются нас устройствах в статусе Booted. Серийные номер Mac не отображаются вообще.
 +      set BootedState to item i of BootedStates -- значения Booted, Recovery или DFU
 +      
 +      -- Изменим формат Location ID integer на hex
 +      set LocationID to "0x" & (do shell script "printf '%X' " & LocationID)
 +      -- Добавляем подсписки с значениями устройства в список    
 +      set end of ResultList to {DeviceType, ECID, LocationID, SerialNumber, BootedState}
 +    end repeat
 +    
 +    return ResultList
 +    
 +    -- Пример вывода
 +    -- {{"iPhone8,4", "0x0048FC93999E4", "0x8310000", missing value, "Recovery"}, {"Macmini9,1", "0x9282C1440801E", "0x100000", missing value, "DFU"}, {"iPhone7,2", "0x19610E38E81026", "0x8320000", "F17R30000000", "Booted"}}
 +    
 +  on error
 +    return false
 +  end try
 +</code>
 +
 +Мне не понравился нюанс, который не позволяет получить серийный номер подключенных Mac, а так же мобильных устройств в режиме DFU. Если этот момент вам неважен, то проще использовать обработчик CNFGvaluesOfSpecifiedProperties(), доступный из коробки.
 +
 +Получить справку о наборе встроенных обработчиков для управления автоматизацией можно так:
 +
 +<code>tell script "Configuration Utility" to show help</code>
 +
 +Подводя итог, можно сказать, что подобная автоматизация может использоваться автономно, например, на стоящем на столе ИТ отдела Mac Mini 2020 года без монитора и периферии с торчащими Type-C кабелями для подключения как одиночных устройств, так и множества.
 +
 +
 +P.S.
 +В моём ведении нет сотен Mac, iPhone или iPad, которые нужно перезаливать перед выдачей, только несколько личных старых iPhone и Mac Mini которые можно перезаливать каждый день. Попробуйте этот вариант и сообщите в комментариях как это работает на множестве подключенных устройств.
 +
 +
 +----
 +Проверено на следующих конфигурациях:
 +^ Версия ОС ^
 +| Apple macOS Sequoia (15.0)  |
 +
 +----
 +{{:user:vyakob.png?50&nolink |}} Автор первичной редакции:\\ [[user:vyakob|Виталий Якоб]] \\ Время публикации: 05.12.2024 16:57
 +{{tag>Apple macOS Sequoia AppleScript iPhone iPad}}
 +
 +~~DISCUSSION~~

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki