Steven
Steven5 мин. чтения

Ваше Mac-приложение получает доступ к микрофону — и забывает его при каждом запуске

GeekBye запросил доступ к микрофону, вы его выдали, и всё заработало. Следующий запуск: доступа нет. А в Системных настройках → Микрофон приложение вообще не появилось. Виновником оказалась функция безопасности macOS, тихо запускающая приложение из пути, который исчезает — вот диагноз и исправление в одно нажатие.

macOS
Разрешения
Инженерия
Релизы GeekBye
Ваше Mac-приложение получает доступ к микрофону — и забывает его при каждом запуске

Вот баг, из-за которого начинаешь сомневаться в собственных глазах. Вы устанавливаете GeekBye, он просит доступ к микрофону, вы нажимаете Разрешить, и транскрипция работает. Отлично. Вы закрываете, открываете на следующее утро — а он просит доступ к микрофону снова. Вы идёте в Системные настройки → Конфиденциальность и безопасность → Микрофон, чтобы исправить это вручную, а GeekBye вообще нет в списке. Не запрещён. Не разрешён. Просто отсутствует, как будто он никогда и не спрашивал.

Каждый отдельный кусочек выглядел правильным. Запрос был настоящим. Разрешение работало в тот момент. Приложение было корректно подписано и нотаризовано с правильными строками использования. И всё же выданное разрешение испарялось при каждом запуске. GeekBye v2.0.6 это исправил — а первопричина в одной из самых коварных вещей, которые macOS делает, чтобы вас защитить.

Функция, которая прятала приложение от него самого

Виновник — macOS App Translocation, функция безопасности Gatekeeper. Когда вы скачиваете приложение и запускаете его прямо из DMG или из папки ~/Downloads — везде, где оно всё ещё «в карантине» — macOS на самом деле запускает его не оттуда, где вы его видите. Он прозрачно копирует его в случайный путь только для чтения глубоко в /private/var/folders/.../AppTranslocation/… и запускает эту копию. Это хорошая защита: она мешает вредоносной загрузке подменять файлы рядом с собой.

Но вот столкновение. Система разрешений macOS (TCC — то, что отслеживает, кто может использовать ваш микрофон, камеру, экран) идентифицирует приложение по его пути и идентичности кода. Когда приложение транслоцировано, этот путь случаен и временен. Так что когда вы выдаёте доступ к микрофону, macOS исправно записывает выдачу — к пути, которого не будет при следующем запуске. Открываете приложение снова, macOS транслоцирует его в другой случайный путь, видит приложение, о котором у него нет записи, и спрашивает опять. А поскольку этот фантомный путь никогда не бывает стабильным местом, приложение так и не получает постоянную строку в Системных настройках → Микрофон.

Приложение выдавало разрешение призраку.

Именно поэтому с этим сталкивались лишь некоторые люди. Если ваша копия GeekBye уже жила в /Applications — потому что вы перетащили её туда или потому что она попала туда через автообновление — нет карантина, нет транслокации, стабильный путь и всё сохраняется идеально. Баг был невидим для нас и для всех, кто прошёл первую установку, а это ровно тот тип бага, который выживает дольше всего.

Исправление: дайте приложению настоящий дом

Раз вся проблема — в нестабильном пути, исправление в том, чтобы переместить приложение на стабильный. v2.0.6 обнаруживает, когда GeekBye работает транслоцированным (или просто работает вне /Applications), и предлагает в одно нажатие «Move to Applications» — используя вызов перемещения macOS, который копирует бандл в /Applications и перезапускает его оттуда. С этого момента у приложения фиксированная идентичность: выдача микрофона держится, запись экрана держится, и GeekBye наконец появляется в Системных настройках там, где вы этого ожидаете.

Запрос вежлив на этот счёт. Он предлагает Move to Applications, Not Now и Don't Ask Again — и запоминает последний выбор, так что приложение никогда не докучает тому, у кого есть осознанная причина запускать его из другого места. Решение о том, показывать ли запрос вообще, изолировано в маленькие чистые функции (транслоцирована ли эта сборка? находится ли она вне /Applications? заглушил ли её пользователь?), так что логика покрыта юнит-тестами без запуска настоящей нотаризованной сборки на настоящем карантинном томе.

Тот же релиз упростил и сам опыт работы с разрешениями. GeekBye раньше выбрасывал кастомное, самописное встроенное окно разрешений — пару сотен строк интерфейса, пытающихся воспроизвести то, что ОС и так делает хорошо. v2.0.6 удалил его и опёрся на нативный запрос разрешений macOS, подкреплённый тихим неблокирующим баннером, который появляется только тогда, когда требуемого разрешения действительно не хватает. Меньше кода, и поведение, которое пользователи уже узнают, потому что любое другое Mac-приложение работает так же.

Часть, которой я горжусь больше всего: мы доказали это, прежде чем поверили

Было бы легко угадать транслокацию и выпустить исправление. Вместо этого релиз сначала выпустил стартовую диагностику: при запуске GeekBye теперь сообщает собственный путь исполняемого файла и то, транслоцирован ли он, находится ли внутри /Applications, а также текущий статус разрешений на микрофон и экран. Эта телеметрия превратила правдоподобную теорию в подтверждённую — производственные данные показали, что затронутые сессии действительно работали из транслоцированных путей вне /Applications, ровно как и предсказывалось.

Этот порядок важен. У «запрос появляется, но разрешение не держится» есть несколько возможных объяснений — проблема с подписью, отсутствующая строка использования, вопрос с entitlement, причуда базы TCC. Мы исключили причины на уровне API (путь запроса был очевидно корректным), а затем позволили реальным данным указать на слой идентичности/пути, вместо того чтобы выпускать исправление на авось и надеяться, что обращения в поддержку прекратятся.

Три вещи, которым учит этот баг

  1. Разрешение, которое запрашивается, но не сохраняется — это проблема идентичности, а не API. Если код запроса корректен, а выдача всё равно исчезает, перестаньте переписывать запрос. Спросите, к какому пути и идентичности кода ОС привязывает выдачу — и стабильна ли эта идентичность между запусками.
  2. Выпускайте диагностику вместе с (или до) исправлением. Несколько полей — откуда я работаю, каков мой статус разрешений — превратили обоснованную догадку в проверенную первопричину и сказали нам точно, какие пользователи были затронуты. Оснастите инструментами границу, которую подозреваете, прежде чем её латать.
  3. Баги, которые прячутся от разработчиков — это те, что работают в «неправильной» среде. Наш жил в /Applications на каждой машине разработчика, так что был структурно невидим для нас, попадая при этом в новых пользователей. Когда обращение не воспроизводится, первый вопрос — что отличается в месте, где оно работает, а не не ошибается ли пользователь.

GeekBye v2.0.6 выпустил запрос перемещения и диагностику вместе. О более широкой дуге надёжности, в которую это вписывается, см. сколько на самом деле стоит версия 2 (v2.0.0) и почему запись экрана захватывает не тот монитор (v2.0.10) — ещё один баг, всплывший только в конкретной среде. О релизе с мелкими деталями по соседству — спокойное ПО: исправление мерцания и чип режима ответа (v2.0.3 + v2.0.5).

Похожие статьи

Отличить звонок от открытого приложения
Steven
Steven8 мин. чтения

Отличить звонок от открытого приложения

GeekBye умеет заметить, что ты присоединился к видеовстрече, и предложить её записать. Обнаружение оказывается лёгкой половиной — нативный Swift-бинарник читает заголовки окон каждые десять секунд. Трудная половина — точность: не срабатывать, когда Zoom просто открыт, не предлагать записать встречу, которую ты уже записываешь, и не заглушать микрофон в звонке, в котором ты на самом деле сидишь. Три релиза, и каждый из них — предохранитель, которому пришлось научиться не побеждать самого себя.

Инженерия
macOS
Десктоп
Тишина была несущей
Steven
Steven7 мин. чтения

Тишина была несущей

Два последних релиза GeekBye v1 — об одной и той же неудобной правде: транскрипция в реальном времени по настоящей сети не бесстратна, и честный ход — перестать притворяться, что это так. v1.8.20 хранил копию каждого фрагмента звука на диске, прежде чем отбросить его при переподключении, и начал вслух отмечать пропуски в транскрипте. v1.9.0 перестал отправлять тишину, чтобы сэкономить трафик — и обнаружил, что тишина была ровно тем сигналом, по которому транскрайбер понимал, что предложение закончилось. Два релиза о цене выбрасывания вещей.

Инженерия
Аудио
Надёжность
Три глагола, что держат Web Audio живым
Steven
Steven8 мин. чтения

Три глагола, что держат Web Audio живым

Два точечных релиза GeekBye, разделённые двумя месяцами и в двух разных файлах, преподали нашему аудиокоду один и тот же урок с противоположных концов: перестань обращаться с браузерным AudioContext как с одноразовым. Один релиз научился делать resume() контексту, который macOS тихо приостановил посреди записи; другой научился suspend() вместо close(), чтобы идущие подряд сессии перестали врезаться в навязанный Chromium потолок примерно в шесть контекстов. Resume, suspend, close — вот и весь сюжет.

Инженерия
Аудио
Десктоп