はじめに
前回の記事ではMacOS用ドライバ開発プロジェクトの骨組みを作ったので、今回はプロジェクトの肉付けをしていこう。
前回も少し触れたがDriverKitフレームワークを使用するドライバはカーネル拡張(Kernel Extension)ではなくシステム拡張(System Extension)としてインストールする必要がある。
カーネル拡張とシステム拡張ではインストール方式が全く違う。カーネル拡張では、System/Library/Extension
または/Library/Extension
配下に配置すればよかったが、システム拡張はSystemExtension
フレームワークで提供されているインストール用のAPIをアプリケーションから呼ぶ必要がある。
前回紹介したドライバのプロジェクト作成手順でアプリケーションから作成したのは、ドライバのインストールを行うドライバインストールアプリとするためである。
本記事ではドライバインストールアプリとして紹介するが、そのアプリ専用のデバイスである場合などはアプリにインストール機能を持たせることもできる。
今回ドライバインストールに必要なアプリ側の実装を紹介する。
インストールアプリ実装
まずインストールアプリがどのような動作をするのかを説明して、それを基にどのような機能を実装する必要があるかを見ていこう。
インストールアプリを起動するとユーザーにインストールとアンインストール用のボタンを持ったUIが表示される。
ユーザーがUIのインストール・アンインストールボタンを押下する (図の1)。
そのUIのボタンに結びついているViewControllerのメソッドが呼ばれて、ドライバのインストール・アンインストール用の以下の処理が実行される (図の2)。
- インストール・アンインストール用のリクエストを作成 (図の3)
- リクエストの初期化 (図の3.1)
- リクエストが受理された時などに呼ばれるメソッドを実装したデリゲートクラスをインスタンスの初期化 (図の3.2)
- リクエストにデリゲートの登録 (図の3.3)
- リクエストを送信 (図の4)
リクエストが成功または失敗した際にデリゲートクラスの特定のメソッドが呼ばれるので、デリゲートクラスで実装したメソッドでリクエストのステータスが分かる。
デリゲートクラスではリクエストが成功、失敗した時に呼ばれるメソッドの他に既にドライバがインストールされている場合ドライバを更新するか判断するメソッドなども実装する必要がある。
まとめるとインストールアプリで必要な実装は以下になる。
- UIのボタンが押下された時に実行されるメソッド
- インストール・アンインストールリクエストのデリゲートクラス
- インストール・アンインストールボタンを持つUI
この3つの実装をそれぞれ順番に解説していこう。
UIのボタンが押下された時に実行されるメソッド
自動で生成されているViewController.swift
内のViewController
クラスでボタンが押下された時に呼び出されるIBAction
メソッドを実装する。
IBAction
メソッドとUI上のボタンの結び付けはUIを実装した時に行う。
先ほども言ったようにこのメソッドでは3つの処理を実装する必要がある。
- インストール・アンインストール用のリクエストを作成
- リクエストが受理された時などに呼ばれるメソッドを実装したデリゲートクラスを指定
- リクエストを送信
この3つの処理を順番に実装していこう。
リクエストの作成
インストール・アンインストールの要求は、それぞれ、activationRequest
メソッドとdeactivationRequest
メソッドを使用して、リクエストの作成を行う。
インストール用メソッド
class func activationRequest(forExtensionWithIdentifier identifier: String,
queue: DispatchQueue) -> Self
アンインストール用メソッド
class func deactivationRequest(forExtensionWithIdentifier identifier: String,
queue: DispatchQueue) -> Self
identifier
はドライバのバンドルIDを入れる。今回は"jp.co.sciencepark.sampleDriver"を指定する。
DispatchQueue
にはプロセスのメインスレッドに結びついているメインDispatchQueue
の.main
を指定する。
インストールリクエストを作成する場合は以下のようになる。アンインストールリクエストも同様に作成する。
let activationRequest = OSSytemExtensionRequest.activationRequest(forExtensionWithIdentifier: "jp.co.sciencepark.sampleDriver", queue: .main)
デリゲートの指定
デリゲートにはOSSytemExtensionRequestDelegate
プロトコルに準拠しているクラスのインスタンスを指定する。
デリゲートクラスの詳細については後述するが、OSSytemExtensionRequestDelegate
を継承したSysExtManager
クラスを指定する。
let delegate = SysExtManager()
activationRequest.delegate = delegate
リクエストの送信
最後にリクエストを以下のメソッドで送信する。
func submitRequest(_ request: OSSystemExtensionRequest)
IBActionメソッド
3つの処理を繋げてUIのボタンに結びつけるIBActionメソッド内で実装すればViewController
側の実装は終了だ。
OSSytemExtensionRequest
を使用するために必要なフレームワークSystemExtensions
をインポートする。
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
プロトコルに準拠するクラス。
以下のようなプロトコルに準拠しているクラスを実装する。
import Foundation
import SystemExtensions
import os.log
class SysExtManager : NSObject, OSSytemExtensionRequestDelegate {
...
}
Appleの資料を見てみると、4つ実装必須のメソッドがあることが分かる。
// リクエストが正常に終了した時に呼ばれる
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
上2つのメソッドはリクエスト処理の成否を知らせてくれる。ログを出したり、UIを変更してユーザーにリクエストのステータス更新を知らせるために使える。今回はシンプルにログだけを出す。
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を変更したりできる。
これもログだけを出す。
func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {
os_log("Request needs user approval")
}
最後のメソッドは同じドライバの異なるバージョンがすでに端末にインストールされている時に呼ばれる。
ここでドライバのバージョンを比較して更新するかを決めることができる。
戻り値はOSSystemExtensionRequest.ReplacementAction
enumの値を返す。
更新する場合.replace
、更新しない場合.cancel
を返す。
バージョンが新しくなった時だけドライバを更新するようにする。
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に追加する。
インストールとアンインストールボタンをViewController
のそれぞれのアクションと結び付ける。
これでUIの実装は終了する。
残るはビルドして実際にアプリを動かしてみることだけになる。
ビルド
ビルドする前にプロジェクトの設定とEntitlement
の追加が必要になる。
プロジェクトの設定でアプリとドライバの署名をSign to Run Locally
に変更する。
アプリのEntitlement
ファイルdriverProject.entitlements
にSystemExtension
の設定を入れればビルドできる。
試しにビルドしてみてインストールボタンを押下すると以下のダイアログが表示される。
ドライバのインストールに管理者権限を持つユーザーが承認なため、そのことをダイアログでユーザーに通知している。
システム環境設定を開いてセキュリティ設定に移動してドライバのインストールを許可すれば無事にドライバがインストールされる。
インストールの状況はターミナルからコマンドsystemextensionsclt list
で確認できる。
> 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