Steven
Steven5 분 소요

Mac 앱이 마이크 권한을 받고도 실행할 때마다 잊어버리는 이유

GeekBye가 마이크 권한을 요청했고, 당신은 허용했고, 잘 작동했습니다. 그런데 다음 실행: 사라졌습니다. 게다가 앱은 시스템 설정 → 마이크 목록에 아예 나타나지도 않았습니다. 범인은 사라지는 경로에서 앱을 조용히 실행하던 macOS 보안 기능이었습니다 — 여기 그 진단과 한 번의 프롬프트로 끝나는 해결책이 있습니다.

macOS
권한
엔지니어링
GeekBye 릴리스
Mac 앱이 마이크 권한을 받고도 실행할 때마다 잊어버리는 이유

당신 자신의 눈을 의심하게 만드는 버그가 여기 있습니다. GeekBye를 설치하면 마이크 권한을 요청하고, 당신은 허용을 클릭하고, 받아쓰기가 작동합니다. 좋습니다. 앱을 종료하고 다음 날 아침 다시 열면 — 마이크 권한을 요청합니다. 수동으로 고치려고 시스템 설정 → 개인정보 보호 및 보안 → 마이크를 확인하러 가면, GeekBye는 목록에 아예 없습니다. 거부된 것도 아닙니다. 허용된 것도 아닙니다. 그냥 없습니다, 마치 애초에 요청한 적도 없는 것처럼.

각각의 조각은 하나하나 다 올바르게 보였습니다. 프롬프트는 진짜였습니다. 권한은 그 순간에 작동했습니다. 앱은 올바른 사용 목적 문자열과 함께 제대로 서명되고 공증되었습니다. 그런데도 권한 부여는 실행할 때마다 증발했습니다. GeekBye v2.0.6이 이를 고쳤습니다 — 그리고 근본 원인은 macOS가 당신을 보호하기 위해 하는 가장 은밀한 일 중 하나입니다.

앱을 앱 자신으로부터 숨기고 있던 기능

범인은 Gatekeeper 보안 기능인 macOS App Translocation입니다. 앱을 다운로드하고 DMG나 ~/Downloads 폴더에서 곧바로 실행하면 — 아직 "격리(quarantine)"된 어디에서든 — macOS는 실제로 당신이 보는 그 위치에서 앱을 실행하지 않습니다. macOS는 그것을 /private/var/folders/.../AppTranslocation/… 깊숙한 곳의 무작위로 지정된 읽기 전용 경로로 투명하게 복사한 뒤, 복사본을 실행합니다. 좋은 방어책입니다: 악성 다운로드가 자기 옆에 있는 파일을 조작하는 것을 막아 줍니다.

하지만 여기서 충돌이 일어납니다. macOS의 권한 시스템(TCC — 누가 당신의 마이크, 카메라, 화면을 쓸 수 있는지 추적하는 그것)은 앱을 경로와 코드 신원으로 식별합니다. 앱이 이동(translocate)되면 그 경로는 무작위이고 일시적입니다. 그래서 당신이 마이크 권한을 부여하면, macOS는 성실하게 그 권한 부여를 기록합니다 — 다음 실행에는 존재하지 않을 경로를 향해서 말입니다. 앱을 다시 열면, macOS는 그것을 다른 무작위 경로로 이동시키고, 기록에 없는 앱을 보고, 다시 요청합니다. 그리고 그 유령 같은 경로는 결코 안정적인 위치가 아니기 때문에, 앱은 시스템 설정 → 마이크에 영구적인 항목을 결코 얻지 못합니다.

앱은 유령에게 권한을 부여하고 있었던 것입니다.

이것이 바로 일부 사람들만 이 문제를 겪은 이유이기도 합니다. 당신의 GeekBye 복사본이 이미 /Applications에 있었다면 — 직접 그리로 드래그했든, 자동 업데이트를 통해 그리로 갔든 — 격리도 없고, 이동도 없고, 안정적인 경로가 있으며, 모든 것이 완벽하게 유지됩니다. 이 버그는 우리에게도, 첫 설치를 지난 누구에게도 보이지 않았습니다 — 그리고 이것이야말로 가장 오래 살아남는 종류의 버그입니다.

해결책: 앱에게 진짜 집을 주기

문제 전체가 불안정한 경로이므로, 해결책은 앱을 안정적인 경로로 옮기는 것입니다. v2.0.6은 GeekBye가 이동된 상태로 실행 중일 때(또는 그냥 /Applications 밖에서 실행 중일 때)를 감지하고, 한 번의 클릭으로 **"Move to Applications"**를 제공합니다 — 번들을 /Applications로 복사하고 거기서 다시 실행하는 macOS 재배치 호출을 사용해서 말입니다. 그 시점부터 앱은 고정된 신원을 갖습니다: 마이크 권한이 유지되고, 화면 녹화 권한이 유지되며, GeekBye는 드디어 당신이 예상하는 곳인 시스템 설정에 나타납니다.

프롬프트는 그 점에서 정중합니다. Move to Applications, Not Now, Don't Ask Again을 제공하며 — 마지막 선택을 기억하므로, 다른 곳에서 실행할 의도적인 이유가 있는 사람을 앱이 결코 성가시게 하지 않습니다. 프롬프트를 아예 보여줄지 말지 를 결정하는 로직은 작고 순수한 함수들로 분리되어 있어(이 빌드가 이동됐는가? /Applications 밖에 있는가? 사용자가 억제했는가?), 실제 격리된 볼륨에서 실제 공증된 빌드를 띄우지 않고도 단위 테스트할 수 있습니다.

같은 릴리스는 권한 경험 자체도 단순화했습니다. GeekBye는 예전에 커스텀으로 만든 앱 내 권한 창을 띄웠습니다 — OS가 이미 잘하는 일을 재현하려는 수백 줄의 UI였죠. v2.0.6은 그것을 삭제하고 네이티브 macOS 권한 프롬프트에 기댔으며, 필요한 권한이 실제로 없을 때만 나타나는 조용하고 비차단적인 배너로 뒷받침했습니다. 코드는 줄고, 다른 모든 Mac 앱이 똑같이 동작하기 때문에 사용자가 이미 알아보는 동작입니다.

내가 가장 자랑스러운 부분: 우리는 믿기 전에 증명했다

이동을 추측 하고 해결책을 출시하기는 쉬웠을 것입니다. 대신, 이 릴리스는 시작 시 진단 기능을 먼저 출시했습니다: 실행 시 GeekBye는 이제 자신의 실행 파일 경로와, 이동된 상태인지, /Applications 안에 있는지, 그리고 현재 마이크 및 화면 권한 상태를 보고합니다. 그 텔레메트리는 그럴듯한 이론을 확인된 이론으로 바꿔 놓았습니다 — 프로덕션 데이터는 영향받은 세션이 실제로 /Applications 밖의 이동된 경로에서 실행 중이었음을, 예측한 그대로 보여 주었습니다.

그 순서가 중요합니다. "프롬프트는 뜨는데 권한이 유지되지 않는다"에는 여러 가능한 설명이 있습니다 — 서명 문제, 누락된 사용 목적 문자열, 자격(entitlement) 문제, TCC 데이터베이스의 특이 동작. 우리는 API 수준의 원인을 배제했고(요청 경로는 명백히 올바랐습니다), 그런 뒤 희망 섞인 해결책을 출시하고 지원 티켓이 멈추기만을 바라는 대신, 실세계 데이터가 신원/경로 계층을 가리키도록 두었습니다.

이 버그가 가르쳐 주는 세 가지

  1. 프롬프트는 뜨는데 유지되지 않는 권한은 API 문제가 아니라 신원 문제다. 요청 코드가 올바른데도 권한이 계속 사라진다면, 요청을 다시 짜는 것을 멈추세요. OS가 권한을 어떤 경로와 코드 신원에 묶고 있는지 — 그리고 그 신원이 실행 간에 안정적인지 — 를 물으세요.
  2. 진단 기능을 해결책과 함께(또는 먼저) 출시하라. 몇 개의 필드 — 나는 어디서 실행 중인가, 내 권한 상태는 무엇인가 — 가 근거 있는 추측을 검증된 근본 원인으로 바꿔 주었고, 어떤 사용자가 영향을 받았는지 정확히 알려 주었습니다. 패치하기 전에, 의심 가는 경계에 계측을 하세요.
  3. 개발자로부터 숨는 버그는 "잘못된" 환경에서 실행되는 버그다. 우리 버그는 모든 개발 머신의 /Applications에 살고 있었고, 그래서 그 버그는 우리에게는 구조적으로 보이지 않으면서 처음 쓰는 사용자를 때렸습니다. 보고된 문제가 재현되지 않을 때, 첫 번째 질문은 사용자가 틀렸는가 가 아니라 어디서 실행되는지가 무엇이 다른가 입니다.

GeekBye v2.0.6은 재배치 프롬프트와 진단 기능을 함께 출시했습니다. 이것이 자리한 더 넓은 신뢰성의 궤적에 대해서는, 버전 2에 실제로 무엇이 필요한가(v2.0.0)와 화면 녹화가 엉뚱한 모니터를 잡는 이유(v2.0.10) — 특정 환경에서만 드러난 또 하나의 버그 — 를 보세요. 바로 옆의 디테일 릴리스에 대해서는, 차분한 소프트웨어: 깜빡임 수정과 답변 모드 칩(v2.0.3 + v2.0.5)을 보세요.

관련 글

열려 있는 앱과 통화 중을 가려내다
Steven
Steven7 분 소요

열려 있는 앱과 통화 중을 가려내다

GeekBye는 당신이 화상 회의에 들어간 것을 알아채고 한 번의 클릭으로 녹화를 권할 수 있습니다. 감지는 알고 보니 쉬운 절반이었습니다 — 10초마다 창 제목을 읽는 Swift 바이너리 하나. 어려운 절반은 정밀도입니다: Zoom이 그저 열려만 있을 때 발화하지 않기, 이미 녹화 중인 회의에 대해 권하지 않기, 그리고 당신이 실제로 있는 통화의 마이크를 음소거하지 않기. 세 번의 릴리스, 그리고 그 하나하나가 자기 자신을 이기지 않는 법을 배워야 했던 가드입니다.

엔지니어링
macOS
데스크톱
침묵은 하중을 견디고 있었다
Steven
Steven6 분 소요

침묵은 하중을 견디고 있었다

GeekBye v1의 마지막 두 릴리스는 같은 불편한 진실에 관한 것입니다: 진짜 네트워크 위의 실시간 전사는 무손실이 아니며, 정직한 수는 그런 척하기를 멈추는 것이라는 진실. v1.8.20은 재연결 도중 오디오 청크를 드롭하기 전에 그 모든 사본을 디스크에 간직했고, 전사의 간극을 소리 내어 표시하기 시작했습니다. v1.9.0은 대역폭을 아끼려고 침묵을 보내기를 멈췄고 — 그 침묵이야말로 문장이 끝났음을 알기 위해 전사 엔진이 쓰던 바로 그 신호였음을 발견했습니다. 무언가를 던져 버리는 일의 대가에 관한 두 릴리스입니다.

엔지니어링
Audio
안정성
Web Audio를 살아 있게 하는 세 개의 동사
Steven
Steven7 분 소요

Web Audio를 살아 있게 하는 세 개의 동사

두 달 간격으로 서로 다른 파일에서 나온 두 번의 GeekBye 포인트 릴리스가, 우리의 오디오 코드에 같은 교훈을 정반대의 끝에서 가르쳤습니다: 브라우저의 AudioContext를 일회용처럼 다루기를 멈춰라. 한 릴리스는 macOS가 녹화 도중 조용히 suspend해 버린 컨텍스트를 resume()하는 법을 배웠고; 다른 하나는 연이은 세션들이 Chromium의 대략 여섯 컨텍스트 한계에 들이받는 것을 멈추도록 close() 대신 suspend()하는 법을 배웠습니다. resume, suspend, close — 그것이 줄거리 전부입니다.

엔지니어링
Audio
데스크톱