"너무 짧은 대기는 실패를 보고하는 게 아니라 실패를 지어내."
로그인할 때 켜지는 방법 두 가지
사용자가 직접 켜는 Mac 앱(설정의 "로그인 시 실행")은 SMAppService.mainApp.register()를 써. 시스템이 로그인 항목에 올려주고, 사용자가 거기서 끌 수 있어. 가족의 상시 유틸리티(텍스트 변환 도구, 받아쓰기 도구, 음성 런처)는 달라. 스크립트로 모든 Mac에 배포하는 데다, 로그인 항목만으로는 모자라. 죽으면 다시 켜줘야 하고, 로그 파일이 있어야 하고, 배포 스크립트가 이름으로 재시작할 수 있는 작업이어야 해. 그래서 이런 유틸리티는 LaunchAgent로 배포해. ~/Library/LaunchAgents에 두는 속성 목록 파일이고, launchd가 사용자 GUI 세션에서 돌려.
중요한 키들
RunAtLoad는 에이전트가 올라올 때 작업을 시작해. GUI로 로그인할 때마다 그래. LimitLoadToSessionType = Aqua는 에이전트를 그래픽 세션에 묶어. 메뉴 막대 앱엔 이게 필요해. ProcessType = Interactive는 이 프로세스가 사용자를 직접 상대한다고 launchd에 알려. 그러면 분류 안 된 작업이 받는 CPU, I/O 조절 대신 앱 수준의 자원 한도로 돌아. StandardOutPath와 StandardErrorPath는 로그 코드가 돌기 전부터 쓸 수 있는 로그 파일을 만들어줘. 두 번 읽어야 할 키는 KeepAlive = { SuccessfulExit = false }야. launchd는 작업이 실패로 끝났을 때만 다시 켜. 연습용 에이전트로 재보니 exit 3은 다시 떴고(state = spawn scheduled였다가 다시 running), exit 0은 not running으로 남았어. 사용자가 종료를 누르면 exit 0이니까 앱은 꺼진 채로 있고, 죽으면 다시 살아나. 그냥 KeepAlive = true면 사용자가 끌 때마다 앱이 되살아나.
한 가족에서 나온 배포 함정 셋
먼저 죽이면 옛 바이너리가 다시 떠. 설치 스크립트가 swift build 전에 pkill을 불렀어. 앱을 쥐고 있는 건 launchd라서, 빌드가 도는 동안 아직 남아 있던 옛 번들로 바로 다시 띄웠어. 나중에 번들을 바꿔도 디스크만 바뀌고 돌고 있는 이미지는 안 바뀌었지. 제대로 된 순서는 이래. 빌드하고 서명하는 동안엔 지금 프로세스를 그대로 살려둬. 서명까지 끝난 번들을 이름 바꾸기로 설치하고 검증해. 그제야 올라가 있는 작업을 launchctl kickstart -k로 다시 켜.
짧은 대기가 실패를 지어내. 번들을 바꿔 끼운 직후엔 launchd가 작업을 띄웠다가 코드 서명 평가에서 한 번 실패하고(last exit reason = OS_REASON_CODESIGNING) 다시 시도할 수 있어. 5초만 확인한 스크립트가 멀쩡한 설치에 HARD_FAIL을 찍었어. 그걸 보면 누구나 설치를 다시 돌리고 싶어지는데, 그러면 이미 멀쩡한 시스템을 괜히 흔들게 돼. 지금은 20초를 기다리고, 실패하더라도 메시지에서 번들은 설치됐다고 알려줘.
KeepAlive는 실패하는 전제 조건을 무한 반복으로 바꿔. 어떤 서비스의 실행 스크립트가 set -e를 켠 채로 서비스 바깥 경로가 있는지 확인했어. 며칠 뒤 그 경로가 없어졌어. 돌고 있던 프로세스는 검사가 깨지기 전에 시작됐으니까 아무도 몰랐지. 다음 재시작부터는 검사에서 실패해서 끝나고, 다시 켜지고, 또 실패하고, 끝이 없어. 실행 스크립트는 인터프리터나 자기 데이터 폴더처럼 서비스 자신에게 필요한 것만 확인해야 해. 그리고 고쳤다고 말할 수 있는 건 지금 서비스 중인 pid가 수정 뒤에 시작됐을 때뿐이야.