为 Windows 设备门户编写自定义插件

了解如何编写使用 Windows 设备门户(WDP)托管网页并提供诊断信息的 UWP 应用。

自 Windows 10 创意者更新(版本 1703,内部版本 15063)开始,你可以使用设备门户托管你的应用诊断接口。 本文介绍为应用创建 DevicePortalProvider 所需的三个部分 - 应用程序包清单 更改、设置应用与 Device Portal 服务的连接以及处理传入请求。

创建新的 UWP 应用项目

在 Microsoft Visual Studio 中,创建新的 UWP 应用项目。 转到“文件”>“新建 > 项目”,然后选择“空白应用”(Windows Universal)用于 C#,然后单击 “下一步”。 在“配置新项目”对话框中。 将项目命名为“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">
    ...

若要声明应用是设备门户提供程序,需要创建应用服务和使用该应用的新设备门户提供程序扩展。 在 Extensions下的 Application 元素中添加 windows.appService 扩展和 windows.devicePortalProvider 扩展。 确保每个扩展中的 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/api”提供程序,而不是匹配“/MyNamespace”提供程序。

此功能需要两项新功能。 它们还必须添加到 package.appxmanifest 文件中。

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

注释

“devicePortalProvider”功能受到限制(“rescap”),这意味着必须先获得应用商店的批准,然后才能将应用发布到该应用商店。 但是,这不会阻止你在本地通过旁加载测试你的应用。 有关受限功能的详细信息,请参阅 应用功能声明

设置后台任务和 WinRT 组件

若要设置 Device Portal 连接,应用必须从 Device Portal 服务将应用服务连接与应用内运行的 Device Portal 实例挂钩。 为此,请在应用程序中添加一个新的 WinRT 组件,该组件包含一个实现 IBackgroundTask的类。

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;
}

应用程序必须处理两个事件以完成请求处理循环:Closed,即每当设备门户服务关闭时,以及 RequestReceived,后者用于显示传入的 HTTP 请求并提供设备门户提供者的主要功能。

处理 RequestReceived 事件

每当在插件指定的处理程序路由上发出一个 HTTP 请求时,将引发一次 RequestReceived 事件。 Device Portal 提供程序的请求处理循环与 NodeJS Express 中的请求处理循环类似:请求和响应对象与事件一起提供,处理程序通过填写响应对象进行响应。 在设备门户提供程序中,RequestReceived 事件及其处理程序使用 Windows.Web.Http.HttpRequestMessageHttpResponseMessage 对象。

// 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 实例被添加到 Response 对象中。 还允许其他 IHttpContent 类,例如“String”和“Buffer”。

然后,响应设置为 HTTP 响应,并给定 200 (确定) 状态代码。 它应在进行原始调用的浏览器中按预期呈现。 请注意,当 RequestReceived 事件处理程序返回时,响应消息会自动返回到用户代理:不需要其他“send”方法。

设备门户响应消息

提供静态内容

可以直接从包中的文件夹中提供静态内容,因此可以轻松地将 UI 添加到提供程序。 提供静态内容的最简单方法是在项目中创建可映射到 URL 的内容文件夹。

设备门户静态内容文件夹

然后,在 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 包时文件位于 AppX 包中。

配置静态内容文件复制

使用现有的设备门户资源和应用程序接口

设备门户提供程序提供的静态内容与核心设备门户服务位于同一端口上。 这意味着,可以使用 HTML 中的简单 <link><script> 标记引用设备门户随附的现有 JS 和 CSS。 一般情况下,我们建议使用 rest.js,它将所有核心设备门户 REST API 包装在方便的 webbRest 对象中,以及 common.css 文件,这使你可以设置内容样式以适应设备门户的 UI 的其余部分。 可以在示例中包含的 index.html 页中看到此示例。 它使用 rest.js 从设备门户检索设备名称和正在运行的进程。

设备门户插件输出

重要的是,在 webbRest 上使用 HttpPost/DeleteExpect200 方法会自动为你处理 CSRF 事务,从而允许你的网页调用更改状态的 REST API。

注释

设备门户附带的静态内容不保证不会有破坏性的更改。 虽然 API 预计不会经常更改,但它们可能会更改,尤其是在供应商不应使用的 common.jscontrols.js 文件中。

调试设备门户连接

若要调试后台任务,必须更改 Visual Studio 运行代码的方式。 按照以下步骤调试应用服务连接,检查提供程序如何处理 HTTP 请求:

  1. 从“调试”菜单中选择 DevicePortalProvider 属性。
  2. 在“调试”选项卡的“开始”操作部分中,选择“不启动,但在启动时调试我的代码”。
    将插件置于调试模式
  3. 在 RequestReceived 处理程序函数中设置断点。 在 的 requestreceived 处理程序上有一个断点

注释

确保生成体系结构与目标的体系结构完全匹配。 如果使用 64 位电脑,则必须使用 AMD64 内部版本进行部署。 4. 按 F5 部署你的应用 5. 关闭 Device Portal,然后重新打开它以找到你的应用(仅在更改你的应用清单时需要执行此操作——其他时候你只需重新部署即可,无需执行此步骤)。 6. 在浏览器中访问提供程序的命名空间,断点应该会被命中。