Windows デバイス ポータルのカスタム プラグインの作成

Web ページをホストして診断情報を提供する UWP アプリを、Windows Device Portal (WDP) を使用して作成する方法について説明します。

Windows 10 Creators Update (バージョン 1703、ビルド 15063) 以降では、Device Portal を使用してアプリの診断インターフェイスをホストすることができます。 この記事では、アプリ用の DevicePortalProvider の作成に必要な 3 つの要素である、アプリケーション パッケージ マニフェストの変更、デバイス ポータル サービスへのアプリの接続の設定、着信要求の処理について説明します。

新しい UWP アプリ プロジェクトを作成する

Microsoft Visual Studio で、新しい UWP アプリ プロジェクトを作成します。 [ファイル]>[新規]>[プロジェクト] に移動し、[Blank App (Windows Universal) for C#]\(C# 用空のアプリ (Windows ユニバーサル)\) を選択して [次へ] をクリックします。 [新しいプロジェクトの構成] ダイアログ ボックスで、 プロジェクトに "DevicePortalProvider" という名前を指定し、[作成] をクリックします。 これは、アプリ サービスを含むアプリです。 Visual Studio の更新、または最新の Windows SDK のインストールが必要になる場合があります。

アプリケーション パッケージ マニフェストに devicePortalProvider 拡張機能を追加する

アプリがデバイス ポータル プラグインとして機能するようにするには、package.appxmanifest ファイルにコードを追加する必要があります。 まず、ファイルの先頭に次の名前空間定義を追加します。 それらを IgnorableNamespaces 属性にも追加します。

<Package
    ... 
    xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
    xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4"
    IgnorableNamespaces="uap mp rescap uap4">
    ...

アプリをデバイス ポータル プロバイダーとして宣言するには、アプリ サービスと、それを使用する新しいデバイス ポータル プロバイダー拡張機能を作成する必要があります。 windows.appService 拡張機能と windows.devicePortalProvider 拡張機能の両方を、Application の下の Extensions 要素に追加します。 各拡張機能で AppServiceName 属性が一致していることを確認してください。 これにより、このアプリ サービスを起動してハンドラーの名前空間で要求を処理できることが、デバイス ポータル サービスに示されます。

...   
<Application 
    Id="App" 
    Executable="$targetnametoken$.exe"
    EntryPoint="DevicePortalProvider.App">
    ...
    <Extensions>
        <uap:Extension Category="windows.appService" EntryPoint="MySampleProvider.SampleProvider">
            <uap:AppService Name="com.sampleProvider.wdp" />
        </uap:Extension>
        <uap4:Extension Category="windows.devicePortalProvider">
            <uap4:DevicePortalProvider 
                DisplayName="My Device Portal Provider Sample App" 
                AppServiceName="com.sampleProvider.wdp" 
                HandlerRoute="/MyNamespace/api/" />
        </uap4:Extension>
    </Extensions>
</Application>
...

HandlerRoute 属性では、アプリによって要求される REST 名前空間が示されます。 デバイス ポータル サービスが受信した、その名前空間のすべての HTTP 要求は (暗黙的に後にワイルドカードが続く)、アプリに送信されて処理されます。 この場合は、<ip_address>/MyNamespace/api/* に対する、正常に認証されたすべての HTTP 要求がアプリに送信されます。 ハンドラー ルート間の競合は "最長一致" のチェックにより解決され、要求により多く一致するルートが選択されます。つまり、"/MyNamespace/api/foo" に対する要求は、"/MyNamespace" ではなく "/MyNamespace/api" のプロバイダーと一致します。

この機能には、2 つの新機能が必要です。 また、それらを package.appxmanifest ファイルに追加する必要があります。

...
<Capabilities>
    ...
    <Capability Name="privateNetworkClientServer" />
    <rescap:Capability Name="devicePortalProvider" />
</Capabilities>
...

Note

"devicePortalProvider" 機能は制限された機能 ("rescap") であり、ストアでアプリを公開する前に、ストアから事前に承認を受ける必要があります。 ただし、これにより、サイドローディングによってローカルでアプリをテストできなくなるわけではありません。 制限された機能について詳しくは、「アプリ機能の宣言」をご覧ください。

バックグラウンド タスクと WinRT コンポーネントを設定する

デバイス ポータルの接続を設定するために、アプリで、アプリ内で実行されているデバイス ポータルのインスタンスを使用して、デバイス ポータル サービスからアプリ サービスの接続をフックする必要があります。 これを行うには、IBackgroundTask を実装するクラスを使用して、アプリケーションに新しい WinRT コンポーネントを追加します。

using Windows.System.Diagnostics.DevicePortal;
using Windows.ApplicationModel.Background;

namespace MySampleProvider {
    // Implementing a DevicePortalConnection in a background task
    public sealed class SampleProvider : IBackgroundTask {
        BackgroundTaskDeferral taskDeferral;
        DevicePortalConnection devicePortalConnection;
        //...
    }

その名前は、AppService の EntryPoint で設定された名前空間およびクラスの名前 ("MySampleProvider.SampleProvider") と一致するようにしてください。 デバイス ポータル プロバイダーに初めて要求を発行すると、デバイス ポータルでその要求が一時退避され、アプリのバックグラウンド タスクが起動され、その Run メソッドが呼び出され、IBackgroundTaskInstance が渡されます。 その後、アプリでそれが使用されて DevicePortalConnection インスタンスが設定されます。

// Implement background task handler with a DevicePortalConnection
public void Run(IBackgroundTaskInstance taskInstance) {
    // Take a deferral to allow the background task to continue executing 
    this.taskDeferral = taskInstance.GetDeferral();
    taskInstance.Canceled += TaskInstance_Canceled;

    // Create a DevicePortal client from an AppServiceConnection 
    var details = taskInstance.TriggerDetails as AppServiceTriggerDetails;
    var appServiceConnection = details.AppServiceConnection;
    this.devicePortalConnection = DevicePortalConnection.GetForAppServiceConnection(appServiceConnection);

    // Add Closed, RequestReceived handlers 
    devicePortalConnection.Closed += DevicePortalConnection_Closed;
    devicePortalConnection.RequestReceived += DevicePortalConnection_RequestReceived;
}

要求処理ループを完了するには、次の 2 つのイベントがアプリによって処理される必要があります。デバイス ポータル サービスのシャットダウンのたびに発生する Closed、および着信 HTTP 要求を処理しデバイス ポータル プロバイダーのメイン機能を提供する RequestReceived です。

RequestReceived イベントを処理する

RequestReceived イベントは、プラグインの指定したハンドラー ルートで行われる HTTP 要求ごとに 1 回発生します。 デバイス ポータル プロバイダーの要求処理ループは、NodeJS Express での要求処理ループと似ています。つまり、イベントとともに要求オブジェクトと応答オブジェクトが提供され、ハンドラーが応答オブジェクトに入力して応答します。 デバイス ポータル プロバイダーでは、RequestReceived イベントとそのハンドラーで、Windows.Web.Http.HttpRequestMessage オブジェクトと HttpResponseMessage オブジェクトが使用されます。

// Sample RequestReceived echo handler: respond with an HTML page including the query and some additional process information. 
private void DevicePortalConnection_RequestReceived(DevicePortalConnection sender, DevicePortalConnectionRequestReceivedEventArgs args)
{
    var req = args.RequestMessage;
    var res = args.ResponseMessage;

    if (req.RequestUri.AbsolutePath.EndsWith("/echo"))
    {
        // construct an html response message
        string con = "<h1>" + req.RequestUri.AbsoluteUri + "</h1><br/>";
        var proc = Windows.System.Diagnostics.ProcessDiagnosticInfo.GetForCurrentProcess();
        con += String.Format("This process is consuming {0} bytes (Working Set)<br/>", proc.MemoryUsage.GetReport().WorkingSetSizeInBytes);
        con += String.Format("The process PID is {0}<br/>", proc.ProcessId);
        con += String.Format("The executable filename is {0}", proc.ExecutableFileName);
        res.Content = new Windows.Web.HttpStringContent(con);
        res.Content.Headers.ContentType = new Windows.Web.Http.Headers.HttpMediaTypeHeaderValue("text/html");
        res.StatusCode = Windows.Web.Http.HttpStatusCode.Ok;            
    }
    //...
}

このサンプル要求ハンドラーでは、まず argsパラメーターから要求オブジェクトと応答オブジェクトを取り出し、要求 URL やその他の HTML 書式設定で文字列を作成します。 これが、HttpStringContent インスタンスとして応答オブジェクトに追加されます。 その他の IHttpContent クラス ("String" や "Buffer" などのクラス) も使用できます。

その後、応答は HTTP 応答として設定され、200 (OK) 状態コードが指定されます。 元の呼び出しを行ったブラウザーでは、想定どおりにレンダリングされます。 なお、RequestReceived イベント ハンドラーに制御が戻ると、応答メッセージは自動的にユーザー エージェントに返されます。さらに "send" メソッドが必要になることはありません。

device portal response message

静的コンテンツを提供する

静的コンテンツはパッケージ内のフォルダーから直接提供できるため、プロバイダーに UI を追加するのは簡単です。 静的コンテンツの提供方法として最も簡単なのは、プロジェクト内で、URL にマップできるコンテンツ フォルダーを作成する方法です。

device portal static content folder

その後、RequestReceived イベント ハンドラーに、静的コンテンツのルートを検出し要求を適切にマップするルート ハンドラーを追加します。

if (req.RequestUri.LocalPath.ToLower().Contains("/www/")) {
    var filePath = req.RequestUri.AbsolutePath.Replace('/', '\\').ToLower();
    filePath = filePath.Replace("\\backgroundprovider", "")
    try {
        var fileStream = Windows.ApplicationModel.Package.Current.InstalledLocation.OpenStreamForReadAsync(filePath).GetAwaiter().GetResult();
        res.StatusCode = HttpStatusCode.Ok;
        res.Content = new HttpStreamContent(fileStream.AsInputStream());
        res.Content.Headers.ContentType = new HttpMediaTypeHeaderValue("text/html");
    } catch(FileNotFoundException e) {
        string con = String.Format("<h1>{0} - not found</h1>\r\n", filePath);
        con += "Exception: " + e.ToString();
        res.Content = new Windows.Web.Http.HttpStringContent(con);
        res.StatusCode = Windows.Web.Http.HttpStatusCode.NotFound;
        res.Content.Headers.ContentType = new Windows.Web.Http.Headers.HttpMediaTypeHeaderValue("text/html");
    }
}

コンテンツ フォルダー内のすべてのファイルが "コンテンツ" としてマークされており Visual Studio の [プロパティ] メニューで [新しい場合はコピーする] または [常にコピーする] に設定されていることを確認してください。 これにより、それらのファイルが、展開時に AppX パッケージに含まれるようになります。

configure static content file copying

既存の デバイス ポータル リソースおよび API を使用する

デバイス ポータル プロバイダーによって提供される静的コンテンツは、コア デバイス ポータル サービスと同じポートで提供されます。 つまり、デバイス ポータルに含まれている既存の JS および CSS を、HTML の単純な <link> および <script> タグで参照できます。 一般的には、すべてのコア デバイス ポータル REST API を便利な webbRest オブジェクトにラップする rest.js と、デバイス ポータルの UI の他の部分に合わせてコンテンツのスタイルを設定できる common.css ファイルを使用することをお勧めします。 この例については、サンプルに含まれている index.html ページをご覧ください。 この例では、rest.js を使用してデバイス ポータルからデバイス名と実行中プロセスを取得します。

device portal plugin output

重要なのは、webbRest で HttpPost/DeleteExpect200 メソッドを使用すると自動的に CSRF 処理が実行されるため、状態が変化する REST API を Web ページで呼び出せるということです。

Note

デバイス ポータルに含まれている静的コンテンツには、重大な変更に対する保証はありません。 これらの API は、頻繁な変更は想定されていませんが、変更される可能性もあるため (特に common.jscontrols.js ファイルにおいて)、プロバイダーでは使用しないでください。

デバイス ポータルの接続をデバッグする

バックグラウンド タスクをデバッグするには、Visual Studio でのコード実行方法を変更する必要があります。 アプリ サービスの接続をデバッグしてプロバイダーでの HTTP 要求の処理方法を調べるには、次の手順に従います。

  1. [デバッグ] メニューから [DevicePortalProvider のプロパティ] を選択します。
  2. [デバッグ] タブの [開始動作] で、[起動はしないが、開始時にコードをデバッグする] を選択します。
    put plugin in debug mode
  3. RequestReceived ハンドラー関数にブレークポイントを設定します。 break point at requestreceived handler

Note

ビルドのアーキテクチャがターゲットのアーキテクチャと正確に一致することを確認してください。 64 ビット PC を使用している場合は、AMD64 ビルドを使用して展開する必要があります。 4. F5 キーを押してアプリを展開します。5. デバイス ポータルをオフにし、再度オンにしてアプリを検出できるようにします (アプリ マニフェストを変更した場合にのみ必要。その他の場合は、単に再展開し、この手順を省略できます)。 6. ブラウザーで、プロバイダーの名前空間にアクセスすると、ブレークポイントにヒットします。