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
Steven6 мин. чтения

Анатомия доведения ПО до совершенства: как код-ревью поймало то, что не смогли тесты

На протяжении всей серии GeekBye v2 повторяется одно и то же: исправление проходит каждый тест на машине разработчика, а потом код-ревью доказывает, что оно бы не сработало почти ни у кого. Вот рабочий процесс, стоящий за девятью релизами — ворота ревью, отловы на этапе исправления и дисциплина «протестируй до отгрузки», которая превращает «у меня работает» в «работает».

Инженерия
Процесс
Код-ревью
Одна CSS-переменная, пять раундов ревью и Swift-тулчейн, который солгал
Steven
Steven6 мин. чтения

Одна CSS-переменная, пять раундов ревью и Swift-тулчейн, который солгал

GeekBye v2.0.7 сделал весь оверлей регулируемо полупрозрачным — звучит как однострочное изменение в CSS и совершенно им не является. Настоящая история — в том, что поймало код-ревью: две поверхности, которые отказывались растворяться, панель записи, которая при той же прозрачности выглядела не так, и скомпилированный бинарник, который туда-сюда менял 672 байта, потому что наша же документация назвала ревьюеру не ту версию Swift.

Инженерия
Дизайн
Сборка
Спокойный софт: как мы убили мигающий переключатель и научили чат сообщать, в каком он режиме
Steven
Steven6 мин. чтения

Спокойный софт: как мы убили мигающий переключатель и научили чат сообщать, в каком он режиме

Два небольших релиза GeekBye без громких фич — просто переключатель в настройках перестал мигать, а чат-ассистент наконец сообщает, ответил ли он в режиме встречи или в режиме кодинга. Вот сколько на самом деле стоит «спокойный софт» — по одной детали за раз.

Продукт
Дизайн
Инженерия