3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-09-07

はじめに

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

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

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

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

インストールアプリ実装

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

driverKitInstallSequence.png

インストールアプリを起動するとユーザーにインストールとアンインストール用のボタンを持った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をインポートする。

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プロトコルに準拠するクラス。
以下のようなプロトコルに準拠しているクラスを実装する。

SysExtManager.swift
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に追加する。
01_UI.png

インストールとアンインストールボタンをViewControllerのそれぞれのアクションと結び付ける。
02_UI2.png

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

ビルド

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

アプリのEntitlementファイルdriverProject.entitlementsSystemExtensionの設定を入れればビルドできる。
03_entitlement.png

試しにビルドしてみてインストールボタンを押下すると以下のダイアログが表示される。
04_sysextdialog.png

ドライバのインストールに管理者権限を持つユーザーが承認なため、そのことをダイアログでユーザーに通知している。
システム環境設定を開いてセキュリティ設定に移動してドライバのインストールを許可すれば無事にドライバがインストールされる。
05_acceptSysext.png

インストールの状況はターミナルからコマンド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

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?