카테고리 없음

macOS - XPC Service

jin_j_i_n 2022. 3. 15. 23:36

프로세스 통신을 위한 XPC

 

XPC 매커니즘은 IPC용 소켓 (또는 MIG를 사용하는 Mach Services)에 대한 대안을 제공한다.

예를들어 특정 프로세스의 API에 접근하거나, 서비스를 제공하기 위한 클라이언트를 기다리는 서버역할의 프로세스를가 있을 수 있다.

 

애플리케이션의 XPC 서비스

보통 XPC 서비스에 대해 얘기할때 XPC Service라고 불리는 번들(Bundle)에 대해 이야기 하는것을 의미한다.

 

애플 생태계에서 번들은 특정 디렉토리 구조로 표현되는 엔티티를 나타낸다. 가장 잘 알려진 번들은 애플리케이션 번들이다. 애플리케이션(예: Chess.app)을 마우스 오른쪽 버튼으로 클릭하고 콘텐츠 표시를 선택하면 디렉토리 구조가 표시된다.

 

XPC로 돌아와서, 애플리케이션은 XPC Service 번들을 가질 수 있다. 애플리케이션 번들 내 Contents/XPCServices/ 경로의 디렉터리에서 확인할 수 있다.

또한 Frameworks 내에서 XPC 서비스를 가질 수 있다.

XPC 서비스의 추가 이점

xpc 서비스를 이용한다면 별도의 모듈에서 몇가지 기능을 중단할 수있다.

비용은 많이 들지만 드물게 발생하는 작업을 실행하는 XPC 서비스를 만들 수 있다. 예를 들어 랜덤 수를 생성하는 암호화 작업이 있을 수 있다. 

 

또하나의 이점은 XPC 서비스는 자신의 프로세스에서 작동한다는것이다. 만약 해당 프로세스가 죽어도 우리가 개발하고 있는 메인 앱에 영향을 끼치지 않을것이다. 

만약 개발하고 있는 앱이 사용자 정의 플러그인을 지원하고 그 플러그인들은 XPC 서비스를 사용해 개발된 상황을 가정해보자,

플러그인들을 실행시키는 코드가 크래시가 나도 그것은 현재 메인 앱의 무결성에 영향을 주지 않는다.

 

또한 XPC는 자체 entitlements(서비스 또는 기술을 사용할 수 있는 실행 권한을 부여하는 키-값 쌍, 파일형태이다.)를 가진다. 

애플리케이션은 XPC 서비스에서 제공하는 서비스를 사용할때만 entitlement를 필요로한다.

위치를 사용하지만, 특정 기능에서만 위치서비스를 사용하는 앱이 있다 상상해보자, 이러한 기능을 XPC 서비스로 이동하고 해당 XPC 서비스에만 위치 권한을 추가할 수 있다. 사용자가 위치를 사용하는 기능이 필요하지 않은경우, 권한을 묻는 메시지가 표시되지 않으므로 앱 사용을 더욱 신뢰할 수 있다.

 

XPC와 우리의 친구 launchd

launchd는 시스템에서 실행되는 첫번째 프로세스이다.

활성상태에서 launchd 확인하기

launchd는 다른 프로세스, 서비스, 데몬들을 실행시키고 관리할 책임이 있다. 또한 작업들을 스케쥴링을 담당한다.  따라서 launchd는 XPC 서비스의 관리를 담당한다는 뜻도 된다.

 

XPC 서비스는 오랫동안 유휴 상태였거나 요청 시 생성된 경우 중지될 수 있다. 모든 관리는 launchd에 의해 행해지고 우리는 해당 작업 (XPC 작동)에 대해 어떠한 작업도 할 필요가 없다.
launchd는 시스템 전체의 리소스 가용성 및 메모리 부족에 대한 정보를 가지고 있다. 누가 launchd보다 시스템 리소스를 가장 효과적으로 사용하는지 결정한다.
 

+Keynote애플리케이션에서 사용된 XPC 서비스

keynote.app 에서 사용된 xpc 서비스

터미널에서 find [앱 경로] -name ".*xpc" 명령어로 앱에 사용된 xpc 서비스를 확인할 수 있다.

 

 

XPC 서비스 구조 & 원리   

XPC 서비스를 사용한다는 것은 C API (XPC Services)를 사용하는지, Objective - C API( NSXPCConnection)를 사용하는지에 대해 영향을 받는다.

Objective-C의 NSXPCConnection API는 다른 프로세스에서 한 프로세스의 개채에 대한 메서드를 호출할 수 있는 상위 수준 원격 프로시저 호출 인터페이스를 제공한다.

 

NSXPCConnection API는 전송을 위해 데이터 구조와 개체를 자동으로 직렬화하고, 다른 끝 쪽에서 역직렬화를 한다.

결과적으로, 원격 개체에서 메서드를 호출하는것은 로컬 개체에서 메서드를  호출하는 것과 매우 유사하게 작동하는것이다.

 

NSXPCConnection 을 사용하기 위해서 아래와 같이 구성을 해야한다.

 

- 인터페이스 : 대부분 프로토콜로 이루어져있고, 이 프로토콜은 원격 프로세스에서 호출할 수있는 메서드를 가지고 있다. 

호출 어플리케이션과 서비스간의 프로그래밍적 인터페이스를 정의한다. 연결의 반대쪽에서는 호출하려는 모든 인스턴스 메서드는 프로토콜로 명시적으로 정의해ㅐ야한다.

 

 

- 양쪽에서의 connection 객체 : 서비스(XPC 서비스) 쪽에서의 connection과 클라이언트 쪽에서의 connection이 필요하다.

 

- listener : 리스너는 XPC 서비스 커넥션안에 있다. 

 

전체적인 구조

 

XPC 서비스 구현하기

XPC 서비스는 기본 애플리케이션 번들의 Contents/XPCServices 디렉토리에 있는 번들이다. XPC 서비스 번들은 실행파일, info.plst, 서비스에 필요한 리소스들을 포함한다.

XPC 서비스는 메인함수에서 xpc_main(3) Mac OS X 개발자 도구 매뉴얼 페이지를 호출하여 서비스가 메세지를 수신할 때 호출할 함수를 나타낸다. (이부분은 무슨뜻인지 정확히 모르겠음)

 

Xcode에서 XPC 서비스의 간단한 예시 (swift)

(해당 예시는 유튜브 AppleProgramming 채널의 Cocoa 프로그래밍 L74-XPC 서비스 영상을 참고하였습니다)

 

이 예시에서 다룰 XPC 서비스는 영어 문장을 입력으로 소문자를 모두 대문자로 바꿔주는 XPC 서비스를 구현한다.

 

1. XPC Service 탬플릿을 사용하여 프로젝트에 새 대상을 추가한다.

 

 

2. 입력과 출력을 담당할 label, textfield, button을 추가한다.

 

3. XPC 서비스를 swift로 작성하기 때문에, 기존 TextServices 파일을 swift로 변환해준다.

XPC 서비스를 추가하고 나면 구성되어있는 파일은 위와 같다.

- TextServiceProtocol : 외부에서 해당 XPC 서비스를 사용하기위한 인터페이스 역할.

 

- TextService.m, TextService.h : Service 자체 코드

 

- main.m : 서비스의 메인 진입점, delegate를 설정한 다음 xpc 리스너를 설정하고, delegate를 할당한 다음 listener를 시작하여 연결을 한다. 연결을 한 이후엔 애플리케이션이 최초로 호출을 보낸 다음 실제로 서비스를 실행할 수 있게 한다. 

 

위 파일들은 오브젝트 씨 기준으로 생성된 파일이기 때문에 swift로 변환해주는 작업을 진행한다.

TextServiceProtocol.h -> TextServiceProtocol.swift 

TextService.h -> TextServiceDelegate.swift

TextService.m -> TextService.swift

main.m -> main.swift

 

이때 .h는 오브젝트 C의 헤더파일인데, 타겟 설정이 안되어있을 수 있으니 타겟설정을 해주고 넘어가야한다.

헤더파일이였던 파일을 눌러 왼쪽 바에서 보면 TargetMembership 박스 안에 TextService체크박스가 체크 안되어있는것을 확인할 수 있다.

체크가 되어있지 않다면 체크해줘서 타겟 설정을 해준다.

 

4. Swift를 쓰기 때문에 Object C일때 쓰였던 헤더 설정을 해제해준다.

 

Target을 TextService로한 후,

- Swift Compiler - Language : Swift 5(최신버전)

- Install Objective-C Compatiblity Header : No

-  Objective-C Generated Interface HeaderName : 빈칸

의 설정으로 바꿔준다.

 

5. 코드 구현 

 

- TextServiceProtocol.swift

import Foundation

@objc func public protocol TextServiceProtocol {
    func uppercase(_ string: String, withReply reply: @escaping (String) -> Void)
}

- TextService.swift

import Foundation

class TextService: NSObject, TextServiceProtocol {
    func uppercase(_ string: String, withReply reply: @escaping (String) -> Void) {
        reply(string.uppercased())
    }
}

 

- TextServiceDelegate.swift

import Foundation

class TextServiceDelegate: NSObject, NSXPCListenerDelegate {
    func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool {
        
        newConnection.exportedInterface = NSXPCInterface(with: TextServiceProtocol.self)
        newConnection.exportedObject = TextService()
        newConnection.resume()
        
        return true
    }
}

 

- main.swift

import Foundation

let delegate = TextServiceDelegate()
let listener = NSXPCListener.service()

listener.delegate = delegate
listener.resume()

 

6. XPC 서비스를 캡슐화 할 Utility 코드 작성

 

 다시 기존에 개발하던 앱 코드로 돌아가 앞서 구현해주었던 XPC 서비스를 캡슐화하고, 유틸리티 역할을 해주는 코드를 작성한다.

UppercaseUtility.swift 라는 클래스를 생성한 후 아래와같은 코드를 작성한다.

 

- UppercaseUtility.swift 

import Foundation
import TextService

class UppercaseUtility {
    func uppercase(_ string: String, withReply reply: @escaping (String) -> Void) {
        
        let connection = NSXPCConnection(serviceName: "com.boostcamp.S046.TextService")
        connection.remoteObjectInterface = NSXPCInterface(with: TextServiceProtocol.self)
        connection.resume()
        
        let service = connection.remoteObjectProxyWithErrorHandler { error in
            print("Error: \(error)")
        } as? TextServiceProtocol
        
        service?.uppercase(string, withReply: reply)
        
    }
}

 

Connection을 생성해주고 현재 개발하고있는 서비스와 연결해준다. 

연결을 사용하려는 XPC의 번들아이디로 생성(NSXPCConnection)한 후 service 객체 생성한다.

생성한 service 객체를 통해 TextService XPC 서비스의 uppercase 메서드를 사용한다.

 

7. 원하는 곳에서 해당 기능 사용

텍스트필드로 텍스트를 입력받은 후 버튼을 누르면 대문자로 변환하는 앱이기 때문에 button Action에 해당 기능을 사용하는 코드를 작성한다.

 

- ViewController.swift

import Cocoa

class ViewController: NSViewController {

    @IBOutlet weak var outputLabel: NSTextField!
    @IBOutlet weak var textField: NSTextField!
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func changeUppercase(_ sender: Any) {
        UppercaseUtility.uppercase(textField.stringValue) { text in
            DispatchQueue.main.async {
                self.outputLabel.stringValue = text
            }
        }
    }
}

 

8. 결과

텍스트를 입력후 버튼을 누르면 최초 첫번째 연결은 시간이 걸리지만, 그 이후의 버튼 액션은 시간이 더 짧아지는것을 확인할 수 있다.

또한 활성상태 앱에서도 TextService 라는 XPC 서비스가 단독으로 실행되고 있음을 확인할 수 있다.

 

해당 글은

https://medium.com/dwarves-foundation/xpc-services-on-macos-app-using-swift-657922d425cd 

 

XPC services on macOS app using Swift

Before XPC we used to pick up Sockets and Mach Messages (Mach Ports).

medium.com

과 

https://www.youtube.com/watch?v=kEUEGOI2WO0 

를 번역하여 정리한 글입니다.