"권한이 네 코드한테 거부된 게 아냐. 네 코드의 identity 한테 거부된 거고 — 벌거벗은 인터프리터엔 그게 거의 없어."
같은 Python, 다른 두 답
두 번째 전쟁 이야기고, 지난번이랑 사람 미치게 하는 모양이 같아. executor 는 마운트된 볼륨에서 영상 아카이브를 읽어야 하는데, 요즘 macOS 에서는 그러려면 유저가 full-disk 파일 접근을 승인해줘야 해. 승인해주고, SSH 로 들어가서 Python 을 돌리면 아카이브를 완벽하게 읽어. 그런데 서비스 매니저가 정확히 같은 인터프리터를 같은 기계에서 띄우면 운영체제가 파일 접근을 거부해.
이유는 권한 시스템이 코드한테 접근을 승인하는 게 아니기 때문이야. identity 한테 승인해. 코드 서명된 애플리케이션, 그러니까 책임을 지는 프로세스한테 귀속되는 identity. 인터랙티브 셸 세션은 시스템이 이미 신뢰하는 identity 를 달고 있고, 네 Python 이 그 컨텍스트를 물려받아. 반면 서비스 매니저가 띄운 맨 인터프리터 바이너리한테는 승인해줄 만한 identity 가 없어. 그래서 요청이 유저가 승인한 적 없는 무언가로 착지하고, 거부되는 거야.
게다가 놓치기 쉬운 방식으로 더 나빠져. 그 Python 바이너리한테 직접 접근을 승인해줘도, 평범한 환경 업데이트가 그 바이너리를 갈아끼울 수 있어. 그럼 그 바이너리에 붙어 있던 승인이 증발하고. 다음에 툴링을 업데이트하는 순간 조용히 만료되는 권한을 만들어둔 셈이지. 갈아끼울 수 있는 파일한테 준 승인은, 네가 고른 적도 없고 통보받지도 못할 만료일이 붙은 승인이야.
고침: 승인이 살 안정된 identity 를 줘
Recall 의 답은 OS 한테 교체 가능한 파일을 믿어달라고 하길 그만두는 거야. 서비스 매니저가 안정된 bundle identifier 를 가진 작고 기계 로컬인 코드 서명된 애플리케이션 번들을 띄워. 시스템이 보는 건 그 launcher 야. 책임을 지는 부모 프로세스고, 런타임 Python 을 자식으로 spawn 하지. full-disk 접근은 안정된 앱 identity 한테 한 번 승인돼. 다음 주에 conda 가 갈아끼울 수 있는 인터프리터가 아니라. 그래서 권한이 밑에 깔린 툴체인의 온갖 일상 업데이트보다 오래 살아남아. 흔들리지 않는 것에 붙어 있으니까.
여기서 가져갈 원리는 이거야. 권한 승인은 identity 에 붙으니까, identity 는 안정적이어야 하고 네가 일부러 소유해야 해. 네 승인이 일상적으로 재생성되거나 업그레이드되거나 재설치되는 것에 고정돼 있으면, 그건 접근을 승인받은 게 아니라 다음 업데이트 때까지 빌린 거야.
나중에 무는 디테일: 신호를 전달해
서비스 매니저와 진짜 작업 사이에 launcher 를 끼워 넣으면 새 의무가 하나 생겨. 그걸 건너뛰면 고약한 부류의 버그가 나오고. 서비스 매니저는 job 을 멈출 때 자기가 아는 프로세스, 그러니까 launcher 한테 신호를 보내. launcher 가 그냥 죽어버리면 걔가 spawn 한 Python 자식이 고아가 될 수 있어. 전사는 아직 돌고 있고, lease 도 아직 쥐고 있는데, 네가 통제한다고 믿는 lifecycle 에는 안 보이는 상태로. 그래서 launcher 는 종료 신호를 worker 프로세스 그룹 전체에 전달해. restart 는 restart 여야지, 'restart 에 아직 일하는 유령 덤' 이면 안 되니까. 진짜 작업 앞에 supervisor 를 세울 때마다, 그 supervisor 의 일은 시작하는 것만이 아냐. 멈춤이 자기가 시작한 전부를 실제로 멈추게 만드는 것까지야.