MacOSでドライバ開発：DriverKit入門② ～システム拡張インストールアプリ～


# はじめに
[前回](https://qiita.com/spc_gmorimoto/items/cd070354dfa92b38a4d8)の記事ではMacOS用ドライバ開発プロジェクトの骨組みを作ったので、今回はプロジェクトの肉付けをしていこう。
前回も少し触れたがDriverKitフレームワークを使用するドライバはカーネル拡張（Kernel Extension)ではなくシステム拡張（System Extension)としてインストールする必要がある。
カーネル拡張とシステム拡張ではインストール方式が全く違う。カーネル拡張では、`System/Library/Extension`または`/Library/Extension`配下に配置すればよかったが、システム拡張は`SystemExtension`フレームワークで提供されているインストール用のAPIを**アプリケーション**から呼ぶ必要がある。

前回紹介したドライバのプロジェクト作成手順でアプリケーションから作成したのは、ドライバのインストールを行う**ドライバインストールアプリ**とするためである。

本記事ではドライバインストールアプリとして紹介するが、そのアプリ専用のデバイスである場合などはアプリにインストール機能を持たせることもできる。

今回ドライバインストールに必要なアプリ側の実装を紹介する。

# インストールアプリ実装

まずインストールアプリがどのような動作をするのかを説明して、それを基にどのような機能を実装する必要があるかを見ていこう。

![driverKitInstallSequence.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685466/8c86efad-db33-5060-7563-215932bdfa35.png)

インストールアプリを起動するとユーザーにインストールとアンインストール用のボタンを持ったUIが表示される。
ユーザーがUIのインストール・アンインストールボタンを押下する (図の1)。

そのUIのボタンに結びついているViewControllerのメソッドが呼ばれて、ドライバのインストール・アンインストール用の以下の処理が実行される (図の2)。

* インストール・アンインストール用のリクエストを作成 (図の3)
    * リクエストの初期化 (図の3.1)
    * リクエストが受理された時などに呼ばれるメソッドを実装したデリゲートクラスをインスタンスの初期化 (図の3.2)
    * リクエストにデリゲートの登録 (図の3.3)
* リクエストを送信 (図の4)

リクエストが成功または失敗した際にデリゲートクラスの特定のメソッドが呼ばれるので、デリゲートクラスで実装したメソッドでリクエストのステータスが分かる。

デリゲートクラスではリクエストが成功、失敗した時に呼ばれるメソッドの他に既にドライバがインストールされている場合ドライバを更新するか判断するメソッドなども実装する必要がある。

まとめるとインストールアプリで必要な実装は以下になる。

* UIのボタンが押下された時に実行されるメソッド
* インストール・アンインストールリクエストのデリゲートクラス
* インストール・アンインストールボタンを持つUI

この３つの実装をそれぞれ順番に解説していこう。

## UIのボタンが押下された時に実行されるメソッド
自動で生成されている`ViewController.swift`内の`ViewController`クラスでボタンが押下された時に呼び出される`IBAction`メソッドを実装する。
`IBAction`メソッドとUI上のボタンの結び付けはUIを実装した時に行う。

先ほども言ったようにこのメソッドでは３つの処理を実装する必要がある。

* インストール・アンインストール用のリクエストを作成
* リクエストが受理された時などに呼ばれるメソッドを実装したデリゲートクラスを指定
* リクエストを送信

この３つの処理を順番に実装していこう。

### リクエストの作成

インストール・アンインストールの要求は、それぞれ、`activationRequest`メソッドと`deactivationRequest`メソッドを使用して、リクエストの作成を行う。

インストール用メソッド

```swift
class func activationRequest(forExtensionWithIdentifier identifier: String, 
                             queue: DispatchQueue) -> Self
```

アンインストール用メソッド

```swift
class func deactivationRequest(forExtensionWithIdentifier identifier: String, 
                               queue: DispatchQueue) -> Self
```

`identifier`はドライバのバンドルIDを入れる。今回は"jp.co.sciencepark.sampleDriver"を指定する。
`DispatchQueue`にはプロセスのメインスレッドに結びついているメイン`DispatchQueue`の`.main`を指定する。

インストールリクエストを作成する場合は以下のようになる。アンインストールリクエストも同様に作成する。

```swift
let activationRequest = OSSytemExtensionRequest.activationRequest(forExtensionWithIdentifier: "jp.co.sciencepark.sampleDriver", queue: .main)
```

### デリゲートの指定

デリゲートには`OSSytemExtensionRequestDelegate`プロトコルに準拠しているクラスのインスタンスを指定する。

デリゲートクラスの詳細については後述するが、`OSSytemExtensionRequestDelegate`を継承した`SysExtManager`クラスを指定する。

```swift
let delegate = SysExtManager()
activationRequest.delegate = delegate
```

### リクエストの送信
最後にリクエストを以下のメソッドで送信する。

```swift
func submitRequest(_ request: OSSystemExtensionRequest)
```

### IBActionメソッド
３つの処理を繋げてUIのボタンに結びつけるIBActionメソッド内で実装すれば`ViewController`側の実装は終了だ。
`OSSytemExtensionRequest`を使用するために必要なフレームワーク`SystemExtensions`をインポートする。

```swift:ViewController.swift
import Cocoa
import SystemExtensions

class ViewController: NSViewController {
    
    ...

    /// インストール
    @IBAction func installDriver(_ sender: Any) {
        let activationRequest = OSSytemExtensionRequest.activationRequest(forExtensionWithIdentifier: "jp.co.sciencepark.sampleDriver", queue: .main)

        let delegate = SysExtManager()
        actiationRequest.delegate = delegate
        OSSytemExtensionManager.shared.submitRequest(activationRequest)
    }

    /// アンインストール
    @IBAction func uninstallDriver(_ sender: Any) {
        let deactivationRequest = OSSytemExtensionRequest.deactivationRequest(forExtensionWithIdentifier: "jp.co.sciencepark.sampleDriver", queue: .main)

        let delegate = SysExtManager()
        deactivationRequest.delegate = delegate
        OSSytemExtensionManager.shared.submitRequest(deactivationRequest)
    }
}
```

これで1段階目の「UIのボタンが押下された時に実行されるメソッド」の実装は完了する。
次に「インストール・アンインストールリクエストのデリゲートクラス」の実装を見ていこう。


## インストール・アンインストールリクエストのデリゲートクラス

`OSSytemExtensionRequest`はデリゲートパターンを使ってリクエスト処理の過程に任意の処理を追加できます。
`OSSytemExtensionRequest`のデリゲートは`OSSytemExtensionRequestDelegate`プロトコルに準拠するクラス。
以下のようなプロトコルに準拠しているクラスを実装する。

```swift:SysExtManager.swift
import Foundation
import SystemExtensions
import os.log

class SysExtManager : NSObject, OSSytemExtensionRequestDelegate {
    ...
}
```

[Appleの資料](https://developer.apple.com/documentation/systemextensions/ossystemextensionrequestdelegate)を見てみると、４つ実装必須のメソッドがあることが分かる。

```swift
// リクエストが正常に終了した時に呼ばれる
func request(_ request: OSSystemExtensionRequest, 
             didFinishWithResult result: OSSystemExtensionRequest.Result)

// リクエストがエラーで終了した時に呼ばれる
func request(_ request: OSSystemExtensionRequest, 
             didFailWithError error: Error)

// リクエストの受理にユーザー承認が必要な時呼ばれる
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest)

// 既にドライバがインストールされている場合呼ばれる
// 戻り値で更新するかを返す
func request(_ request: OSSystemExtensionRequest, 
             actionForReplacingExtension existing: OSSystemExtensionProperties, 
             withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction
```

上２つのメソッドはリクエスト処理の成否を知らせてくれる。ログを出したり、UIを変更してユーザーにリクエストのステータス更新を知らせるために使える。今回はシンプルにログだけを出す。

```swift
func request(_ request: OSSystemExtensionRequest, 
             didFinishWithResult result: OSSystemExtensionRequest.Result) {
    os_log("Request finished with result %@", result.rawValue)
}

func request(_ request: OSSystemExtensionRequest, 
             didFailWithError error: Error) {
    os_log("Request failed with error %@", error.localizedDescription)
}
```

3つ目のメソッドはリクエストの処理にユーザーの承認が必要な場合呼ばれる。
これもログを出したり、UIを変更したりできる。
これもログだけを出す。

```swift
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
    os_log("Request needs user approval")
}
```

最後のメソッドは同じドライバの異なるバージョンがすでに端末にインストールされている時に呼ばれる。
ここでドライバのバージョンを比較して更新するかを決めることができる。
戻り値は`OSSystemExtensionRequest.ReplacementAction` enumの値を返す。
更新する場合`.replace`、更新しない場合`.cancel`を返す。
バージョンが新しくなった時だけドライバを更新するようにする。

```swift
func request(_ request: OSSystemExtensionRequest, 
             actionForReplacingExtension existing: OSSystemExtensionProperties, 
             withExtension ext: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction {
    let compareResult = existing.bundleVersion.compare(ext.bundleVersion, options: .numeric)
    return (compareResult == .orderedAscending) ? .replace : .cancel
}
```

これでデリゲートクラスの実装は完了した。

# インストール・アンインストールボタンを持つUI
最後にユーザーに表示するインストールアプリのUIを作成する。
UIは非常にシンプルにインストールとアンインストール用のボタンのみを持ったものにする。

StoryBoardを使ってインストールとアンインストールのためのボタンをUIに追加する。
![01_UI.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685466/784e8f56-32c3-9d9e-febd-9c4ab52abae6.png)

インストールとアンインストールボタンを`ViewController`のそれぞれのアクションと結び付ける。
![02_UI2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685466/941d111b-5eae-2ad9-bfa6-a58db668bb53.png)

これでUIの実装は終了する。
残るはビルドして実際にアプリを動かしてみることだけになる。


# ビルド

ビルドする前にプロジェクトの設定と`Entitlement`の追加が必要になる。
プロジェクトの設定でアプリとドライバの署名を`Sign to Run Locally`に変更する。

アプリの`Entitlement`ファイル`driverProject.entitlements`に`SystemExtension`の設定を入れればビルドできる。
![03_entitlement.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685466/8980973e-dd6a-bca6-e2b0-2c1e9fa6fcb2.png)

試しにビルドしてみてインストールボタンを押下すると以下のダイアログが表示される。
![04_sysextdialog.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685466/4e4c665c-2673-c2be-3cb8-4ce5829843db.png)

ドライバのインストールに管理者権限を持つユーザーが承認なため、そのことをダイアログでユーザーに通知している。
システム環境設定を開いてセキュリティ設定に移動してドライバのインストールを許可すれば無事にドライバがインストールされる。
![05_acceptSysext.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/685466/a94501c2-2ef9-2fc6-94d9-fa635b1ccd2c.png)

インストールの状況はターミナルからコマンド`systemextensionsclt list`で確認できる。

```shell_session
> systemextensionsctl list
1 extension(s)
--- com.apple.system_extension.driver_extension
enabled	active	teamID	bundleID (version)	name	[state]
*	*	-	jp.co.sciencepark.sampleDriver (1.0/1)	jp.co.sciencepark.sampleDriver	[activated enabled]
```

ステータスが`[activated enabled]`になっているので今回のドライバがインストールされていることが分かる。

# 終わりに
アプリ側にインストール用のコードを実装して無事にドライバのインストールができた。今のところ空のドライバなので次回はドライバの処理を実装していこう。

# 参考資料
https://developer.apple.com/documentation/systemextensions/installing_system_extensions_and_drivers
https://developer.apple.com/documentation/systemextensions/ossystemextensionrequestdelegate