본문 바로가기
C.W.K.
Stream
Lesson 01 of 06 · published

LaunchServices, 그리고 /Applications의 칸 하나

~15 min · macos-citizen, launchservices, install, open, verification

Level 0번들 열어본 사람
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"신원을 봐, 시간 말고. 수정 시각으로 보면 엉뚱한 바이너리가 더 새 거였어."

모든 실행 뒤에 있는 데이터베이스

LaunchServices는 "com.example.spark가 어떤 앱이야", "spark://는 누가 받아", ".md 파일은 뭘로 열어" 같은 질문에 대답하는 macOS 데이터베이스야. Finder 더블클릭, open -a, open -b, 브라우저에서 누른 URL, NSWorkspace.openApplication이 전부 여길 거쳐. 그래서 가족 빌드 스크립트는 전부 설치한 번들을 lsregister -f로 등록하는 걸로 마무리해. 이 도구는 PATH가 아니라 LaunchServices.framework/Support 안에 있어. 데이터베이스가 새 빌드를 바로 알아보게 하려는 거야. 앞 트랙에서 open으로 띄우라고 고집한 이유도 이거야. LaunchServices로 띄운 앱은 자기가 책임 프로세스고, 열기 이벤트를 받고, 앱답게 활성화돼. 셸에서 실행 파일을 바로 돌리면 그냥 그 셸의 자식 프로세스야.

open엔 가끔 발목을 잡는 세부 사항이 두 가지 있어. open --env는 프로세스를 새로 띄울 때만 환경 변수를 넣어. 그래서 이미 돌고 있는 앱에 대고 쓰면 "already running and so the additional environment variables could not be set"이라며 거절해. 새 인스턴스를 띄우려면 -n을 붙이거나, 먼저 옛 걸 종료해. 그리고 프로세스 이름은 번들 이름이 아니라 CFBundleExecutable을 따라. 번들 이름으로 pkill -x를 부른 스모크 스크립트는 아무것도 못 맞혔어. 안에 든 실행 파일 이름이 달랐거든. 이름은 Info.plist에서 읽어.

/Applications는 모두가 같이 쓰는 칸 하나야

한 레포를 여러 벌 체크아웃해 두고 세션 여럿이 거기서 일하는 Mac에선, /Applications/Spark.app은 모두가 덮어쓰는 칸 하나야. 가족 클라이언트의 설치 앱 관문이 세 번 연속 실패했어. 설치된 바이너리에 테스트하려던 코드가 없었거든. 실행 도중에 다른 세션이 다른 체크아웃에서 빌드해서 설치해버린 거야. 있던 검사는 전부 통과했어. 번들 식별자, 제품 버전, 서명, designated requirement, CFBundleVersion까지. 근데 그건 그 앱의 어떤 빌드에도 다 맞는 사실이야. 환경 변수 전달이며 인자 확장이며 한참 헤매다가, 설치된 바이너리에 strings를 돌려보고서야 끝났어. 새로 넣은 표시 문자열이 아예 없었거든.

고치려면 신원이 실제로 드러나는 곳을 봐야 해. 체크아웃에 빌드 산출물이 있으면 설치된 실행 파일의 SHA-256이 그거랑 같아야 해. 이 검사는 더 흔한 실수도 잡아. 다시 빌드해놓고 설치를 까먹는 거. 여기선 시각이 아무것도 증명 못 해. 엉뚱한 바이너리가 더 새 파일이었으니까.

통하는 진단 순서

설치된 앱이 네가 고친 게 반영 안 된 것처럼 굴면, 먼저 설치된 바이너리에 strings -a를 돌려서 방금 넣은 문자열을 찾아(Swift 보간 문자열은 글자 조각으로 나뉘어 들어가니까 앞부분으로 찾아). 그러고 나서 설치된 실행 파일 해시를 기계에 있는 빌드 산출물 전부와 비교해서 어느 트리에서 왔는지 알아내. 돌고 있는 프로세스가 받은 환경은 맨 마지막에, 그 pid에 ps eww를 돌려서 확인해.

Code

설치된 앱이 이 체크아웃의 빌드인지 증명하고, LaunchServices로 띄우기·bash
app=/Applications/Spark.app
exe=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Contents/Info.plist")
installed="$app/Contents/MacOS/$exe"
built=".build/spark-app/Spark.app/Contents/MacOS/$exe"

if [[ -x $built ]]; then
  [[ $(shasum -a 256 "$installed" | cut -d' ' -f1) == $(shasum -a 256 "$built" | cut -d' ' -f1) ]] \
    || { echo "HARD_FAIL installed Spark is not this checkout's build"; exit 1; }
fi
strings -a "$installed" | grep -q 'spark-smoke-marker' || echo "marker missing: not your code"

# Launch through LaunchServices by full path. -n starts a new instance so --env applies.
open -n --env SPARK_SMOKE=1 -a "$app"
sleep 2
pid=$(pgrep -x "$exe") && ps eww "$pid" | tr ' ' '\n' | grep '^SPARK_SMOKE='
pkill -x "$exe"   # the executable name, which need not equal the bundle name

External links

Exercise

Spark 실행 로그 줄에 spark-smoke-marker 같은 고유 문자열을 넣고 앱을 설치해. scripts/test-installed-app.sh를 써. 설치된 실행 파일 해시가 빌드 산출물과 같지 않으면 실패하고, open -n --env SPARK_SMOKE=1로 띄워서 변수가 프로세스까지 전달됐는지 확인하게. 이제 설치 없이 다시 빌드하고 관문을 또 돌려. 반드시 실패해야 하고, 메시지에 어느 검사에서 실패했는지 나와야 해.
Hint
ps eww <pid>는 인자 뒤에 프로세스 환경을 공백으로 나눠서 찍어. 값에 공백이 없는 변수라면 공백 단위로 쪼갠 다음 줄 앞부분만 맞춰봐도 충분해.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.