본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

빌렸다가 그대로 돌려놓는 찔러보기

~12 min · copy-probe, cgevent, changecount, reversible

Level 0식은 부싯돌
0 XP0/34 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"긁어놓은 걸 못 읽겠으면 앱한테 복사를 시켜. 그리고 클립보드는 찾은 그대로 돌려놓고."

클립보드를 빌렸다가 돌려주기

AX 가 긁어놓은 걸 못 볼 때, 모든 앱이 알아듣는 게 하나 있어. Cmd+C. Flint 가 복사를 흉내 내면 불투명한 앱도 긁어놓은 글을 클립보드에 올려주고, Flint 는 거기서 읽으면 돼. 문제는 뻔하지. 사용자가 복사해둔 게 날아가잖아. 그래서 이 찔러보기는 되돌려놓게 만들어져 있어. 클립보드를 통째로 떠놓고, 복사 한 번만 빌리고, 결과를 읽고, 원래 있던 걸 도로 넣어. 제대로 하면 사용자 클립보드는 그대로고, 빌렸다는 것도 몰라.

Cmd+C 흉내 내기

복사 자체는 가짜 키 입력이야. C 키에 Command 를 얹은 눌림과 뗌 이벤트를 만들어 이벤트 흐름에 밀어 넣어. 사용자가 진짜로 Cmd+C 를 누른 것처럼. 앞의 앱은 그걸 받고 늘 하던 대로 복사하지. Flint 가 남의 앱 속을 뜯어보는 게 아니야. 그 앱이 이미 갖고 있는 보편적인 동작 하나를 쓰고, 결과가 떨어지는 표준 자리를 읽는 거야.

복사는 바로 안 끝나. 클립보드를 즉시 읽지 마. 가짜 Cmd+C 를 밀어 넣고 나면 대상 앱이 자기 사정대로 몇 밀리초 뒤에 클립보드를 써. 밀어 넣자마자 NSPasteboard 를 읽으면 옛날 내용을 읽고는 찔러보기가 실패했다고 착각하게 돼. 맞는 방법은 페이스트보드의 changeCount 를 미리 적어두고 그게 올라갈 때까지 짧게 기다리는 거야. 그 숫자가 올라간 게 복사가 진짜로 일어났다는 증거니까.

되돌려놓는다는 건 진짜로 돌려놓는 것

이 찔러보기가 받아들여지는 이유 전부가 마지막 되돌려놓기에 있어. 복사하기 전에 Flint 가 지금 클립보드를 통째로 떠. 읽고 나면 떠뒀던 걸 도로 써넣고. 복사가 아무것도 안 만들어냈어도, 그러니까 changeCount 가 안 움직였어도 어쨌든 되돌려놓고 긁힌 게 없다고 보고해. 성공이든 실패든 클립보드를 빌린 채로 두는 법이 없어. 다음 레슨은 그 떠놓기와 되돌리기를 제대로 하는 얘기야. '클립보드 되돌려놓기' 가 생각보다 어렵거든.

Code

앱이 진짜랑 구분 못 하는 Cmd+C 만들기·swift
func synthesizeCopy() {
    let src = CGEventSource(stateID: .combinedSessionState)
    let cKey: CGKeyCode = 0x08                 // kVK_ANSI_C

    let down = CGEvent(keyboardEventSource: src, virtualKey: cKey, keyDown: true)
    down?.flags = .maskCommand                 // Cmd 를 누른 채로
    let up = CGEvent(keyboardEventSource: src, virtualKey: cKey, keyDown: false)
    up?.flags = .maskCommand

    down?.post(tap: .cghidEventTap)
    up?.post(tap: .cghidEventTap)
}
떠놓고, 복사하고, 기다리고, 읽고, 되돌려놓기·swift
func probeSelectionViaCopy() -> String? {
    let pb = NSPasteboard.general
    let before = pb.changeCount
    let saved = capturePasteboard(pb)   // 통째로 떠둔다 (다음 레슨)

    synthesizeCopy()

    // 앱이 실제로 클립보드를 쓸 때까지 기다린다. changeCount 가 올라간 게
    // 증거다. 바로 읽으면 옛날 내용을 읽게 된다.
    let deadline = Date().addingTimeInterval(0.4)
    while pb.changeCount == before, Date() < deadline { usleep(8_000) }

    let captured = pb.changeCount != before ? pb.string(forType: .string) : nil

    restorePasteboard(pb, from: saved)  // 어떤 경우든 도로 넣는다
    return captured
}

External links

Exercise

복사로 찔러보기가 실패하는 경우들과 각각의 올바른 대응을 적어봐. 하나, 앱이 복사를 아예 안 했을 때(changeCount 가 안 움직임). 둘, 복사는 했는데 글이 비어 있을 때. 셋, 찌르는 중간에 에러가 났을 때. 세 경우 모두에서 함수가 끝날 때 사용자 클립보드에 대해 반드시 지켜져야 할 게 뭐야? 코드 어디서 그걸 보장해?
Hint
복사가 안 일어났든, 빈손이든, 에러가 났든 지켜져야 할 건 하나야. 클립보드가 시작할 때 그대로 끝나는 것. 보장하는 방법은 찌르기 전에 떠두고 모든 출구에서 되돌려놓는 거고. defer 처럼 반드시 도는 자리에 두거나, 모든 return 앞에 한 번씩 두거나. 읽어낸 결과는 경우마다 달라져도 되돌려놓기는 안 달라져.

Progress

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

댓글 0

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

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