Как проверить возобновление загрузки iOS на удалённом Mac

Как проверить возобновление загрузки iOS на удалённом Mac

При удалённой разработке iOS-приложения, которому нужно загружать большие ресурсы для офлайн-работы, движение индикатора прогресса с прежней отметки после обрыва сети ещё не доказывает, что клиент использовал уже полученные байты. Он мог начать загрузку заново, оставив на экране прежний прогресс. На облачном Mac симулятор может обращаться к локальному сервису на той же машине: журнал сервера и проверка файла позволяют воспроизводимо проверить результат без публичного файлового сервиса.

Сначала определите критерии успеха

В этом тесте используется один файл и один диапазон байтов; параллельная загрузка фрагментов не рассматривается. Подготовьте файл без конфиденциальных данных, достаточно большой, чтобы успеть прервать загрузку. Первый запрос должен получить 200. Если клиент сохранил пригодные данные для возобновления, следующий запрос может содержать Range: bytes=N-; тогда сервер вернёт 206 и точный Content-Range. Итоговая длина файла и его SHA-256 должны совпадать с исходными.

resumeData — это данные, с помощью которых клиент пытается возобновить загрузку, а не доказательство того, что сервер уже отдал частичный ответ. Отмена загрузки также не гарантирует получения пригодных resumeData.

Создайте файл в отдельном тестовом каталоге и зафиксируйте эталонные значения. Размер нужен лишь для того, чтобы оставалось время отменить загрузку вручную; он не отражает конфигурацию продукта или показатели производительности:

mkdir -p ~/range-lab && cd ~/range-lab
python3 - <<'PY'
with open("fixture.bin", "wb") as out:
    for _ in range(64):
        out.write(b"XCZone-range-check" * 65536)
PY
shasum -a 256 fixture.bin
stat -f '%z bytes' fixture.bin

Запустите локальный сервер с поддержкой Range

Минимальный сервер ниже отдаёт только fixture.bin из текущего каталога. Он создаёт для файла ETag и умеет передавать данные от указанного смещения до конца; на неподдерживаемые запросы Range отвечает 416. После записи каждого блока сервер делает короткую паузу, чтобы загрузку было удобно прервать в симуляторе. Этот сервис не предназначен для публичного развёртывания.

from hashlib import sha256
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from time import sleep
import re

path = Path("fixture.bin")
size = path.stat().st_size
etag = '"' + sha256(path.read_bytes()).hexdigest() + '"'

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/fixture.bin":
            self.send_error(404)
            return
        requested = self.headers.get("Range")
        if self.headers.get("If-Range") not in (None, etag):
            requested = None
        start = 0
        if requested:
            match = re.fullmatch(r"bytes=(\d+)-", requested)
            if not match or int(match.group(1)) >= size:
                self.send_response(416)
                self.send_header("Content-Range", f"bytes */{size}")
                self.end_headers()
                return
            start = int(match.group(1))
        self.send_response(206 if requested else 200)
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Accept-Ranges", "bytes")
        self.send_header("ETag", etag)
        self.send_header("Content-Length", str(size - start))
        if requested:
            self.send_header("Content-Range", f"bytes {start}-{size - 1}/{size}")
        self.end_headers()
        with path.open("rb") as source:
            source.seek(start)
            while chunk := source.read(65536):
                try:
                    self.wfile.write(chunk)
                    self.wfile.flush()
                except (BrokenPipeError, ConnectionResetError):
                    break
                sleep(0.01)

HTTPServer(("127.0.0.1", 8765), Handler).serve_forever()

Сохраните код в range_server.py и выполните в этом каталоге python3 range_server.py. В другом терминале сначала проверьте код ответа и заголовки командой curl -i -r 1024-2047 http://127.0.0.1:8765/fixture.bin -o /dev/null; вывод может показывать ход передачи, но по нему не следует судить о содержимом. Для явной проверки 206, Content-Range: bytes 1024-2047/… и Content-Length можно использовать curl -sD - -r 1024-2047 -o /dev/null http://127.0.0.1:8765/fixture.bin. По окончании теста остановите сервер с клавиатуры; не меняйте адрес прослушивания на адрес внешнего сетевого интерфейса.

Прервите и возобновите загрузку в симуляторе

Настройте тестовую сборку приложения на загрузку http://127.0.0.1:8765/fixture.bin через URLSessionDownloadTask. Симулятор и Mac, на котором он запущен, используют один и тот же локальный адрес. На физическом устройстве 127.0.0.1 указывает уже на телефон, поэтому переносить эту настройку без изменений нельзя. Если для обычного HTTP требуется исключение из правил сетевой безопасности, добавьте его только в отдельную тестовую конфигурацию и убедитесь, что его нет в рабочей сборке.

После начала загрузки вызовите cancel(byProducingResumeData:), сохраните данные из обратного вызова, затем создайте новую задачу с помощью downloadTask(withResumeData:). Сохраните вывод сервера в терминале и для каждого запроса запишите Range, статус ответа и итоговый размер файла. Сначала убедитесь, что обычная загрузка завершается успешно, и лишь затем проверяйте прерывание: иначе проблемы с соединением будет трудно отличить от проблем с возобновлением.

Отличайте возобновление от повторной загрузки

Если во втором запросе нет Range, а сервер возвращает 200, клиент, возможно, решил загрузить файл заново. Это не означает, что частичное возобновление сработало. Если запрос содержит Range, но из-за изменения ETag файла приходит полный ответ, записывать файл также нужно с начала. Ни при каком сценарии нельзя дописывать полный ответ в конец старого фрагмента. Тестовый код должен учитывать и случай, когда после отмены получить resumeData не удалось: тогда загрузку следует явно начать заново, а не оставлять задачу в состоянии «ожидание возобновления».

Завершите проверку сверкой файла

Когда задача загрузки завершится, скопируйте её временный файл в тестовый каталог и выполните shasum -a 256 下载后的文件路径. Сравните полученный хеш с хешем исходного файла символ в символ и сверьте длину. Статус 206 говорит лишь о том, что сервер вернул часть данных. Если хеши не совпадают, нужно проверить смещение, способ записи и то, не изменился ли файл между запросами.

Напоследок проверьте отрицательный сценарий: прервите загрузку, затем измените исходный файл и перезапустите тестовый сервер, чтобы ETag сменился. Клиент не должен принять старый фрагмент за первую часть нового файла. Если хранить запросы, коды ответов, длины и хеши в одном протоколе тестирования, найти причину регрессии будет проще, чем по одному скриншоту индикатора прогресса.

Часто задаваемые вопросы

Достаточно ли ответа 206, чтобы считать возобновление успешным?

Нет. Сверьте начальное смещение, итоговый размер файла и его SHA-256.

Что делать, если при повторном запросе сервер вернул 200?

Сохранить ответ как полный файл с начала, не добавляя его к уже загруженному фрагменту.

XCZone · облачный Mac

Нужен выделенный физический Mac с удалённым доступом?

Сравните конфигурации M4 Start, M4 Work и M4 Pro, расположение узлов и периоды оплаты, чтобы выбрать подходящий вариант.

Посмотреть тарифы на облачный Mac