2017 年 10 月

第 32 卷,第 10 期

此文章由机器翻译

C# - 编写 Windows Device Portal 封装插件

通过Scott Jones

Windows 10 版本的渲染,之间一个功能进行了 quiet 第一次出现:Windows 设备门户 (WDP)。WDP 是内置于 Windows 10 的基于 Web 的诊断系统。之后的渐进式推出 WDP 现已推出 HoloLens、 IoT、 移动、 桌面和 Xbox 上。它应该包括在新版本的 Windows 中,发布的。除了 Windows 桌面上,WDP 是现成立即可用。在桌面上,WDP 时使用的下载和安装可选的 Windows 更新开发人员模式包。在本文中,我将向你展示如何使用 WDP API 实现打包 WDP 插件来扩展使用自定义的 REST Api 对 Windows 应用商店应用程序。在 Windows 桌面上本文的重点时,概念和技术也适用于其他 Windows 版本。

若要创建和执行打包 WDP 插件,你将需要更新你的系统到至少是 Windows 10 创建者更新 (10.0.15063.0),并安装相应的 Windows 10 SDK (bit.ly/2tx5Bnk)。有关详细说明,请参阅文章,"更新您工具为 Windows 10 创建者更新"(bit.ly/2tx3FeD)。可能有必要重新启动 Visual Studio 来检测并支持新的 SDK 中; 的计算机否则项目可能无法加载。

如果你不熟悉 WDP,我建议你以阅读该文章,"Windows 设备门户概述"(bit.ly/2uG9rco)。这将提供的 WDP,功能的简介,还将确保你有在继续进行编写打包之前安装插件。 

简言之,你将需要下载并安装可选的开发人员模式运行包。从设置应用程序 (按 Windows + I),导航到更新和安全 |开发人员页上,然后选择开发人员模式单选按钮。开发人员模式运行包安装完成后,选中启用设备门户复选框,所需的凭据集,并浏览到提供的 URL 以验证功能。接受 WDP 自签名证书,并输入凭据,浏览器应显示 WDP Web UI 中中, 所示后图 1

Windows 设备门户 Web UI

图 1 Windows 设备门户 Web UI

WDP 体系结构

中的 WDP UI 中显示的工具图 1通过与 WDP 服务中托管的 REST Api 进行通信的 JavaScript 控制实现。为 REST Api,这些通用的无状态 Web 请求是适用于详细上下文比只 WDP UI。例如,可以从 Windows PowerShell 脚本或自定义用户代理调用 WDP REST API。WindowsDevicePortalWrapper (bit.ly/2tx2NXF) 开放源代码项目提供了有关在开发 C# 中的自定义 WDP 用户代理库。  在本文中,我将使用浏览器和可用的命令行实用工具 curl (curl.haxx.se) 执行我自定义的 REST Api。

WDP 旨在使用记住的扩展性。例如,WDP 可自定义每个版本的 Windows 通过内置的插件。与 Windows 10 创建者 Update,现第三方需通过创建打包的插件来扩展 WDP 可能。打包插件提供自定义 REST 终结点,可选基于 Web 的用户界面时,实现和部署 Windows 应用商店应用中。图 2阐释了系统的工作原理。

Windows 设备门户体系结构

图 2 Windows 设备门户体系结构

熟悉的 Microsoft 的 IIS 将内部读取器识别 WDP 的设计。与 IIS,WDP 是基于 HTTP 服务器 API (也称为 HTTP。SYS) 除以 HTTP 控制器和 HTTP 辅助角色之间的职责。WDP 服务实现的控制器,本地系统帐户下运行。每个 WDP 打包插件实现工作线程,在 AppContainer 沙盒中用户的安全上下文内运行。 

它是可以承载从头情况下,使用 HTTP 服务器 API 的应用商店应用中的 Web 服务器。但是,实现 WDP 打包插件提供几个优点。WDP 提供所有内容和 REST Api,包括在打包插件的加密、 身份验证和安全的服务。这包括从跨网站请求伪造和跨站点 WebSocket 劫持攻击的保护。此外,WDP 管理的生命周期内每打包插件,这可能会执行从前景 UI 可见应用或作为后台任务。简单地说,它是更简单、 更安全,实现 REST Api 操作,与打包插件。中的序列关系图图 3显示了执行有关打包的插件的流程。

激活和执行的打包的插件

图 3 激活和执行的打包的插件

在其包应用程序清单,每个插件将自身标识 windows.devicePortalProvider 应用扩展和关联应用程序服务而声明感兴趣的其路由 (Url)。内容的路由、 REST API 路由或两者,可以注册的插件。在包安装时,清单数据已注册到系统。

在启动时,WDP 服务扫描到系统的已注册 WDP 打包的插件,如由 windows.devicePortalProvider 应用扩展名标识。每个插件已发现,WDP 服务中读取的包清单的路由信息。集路由请求的打包的插件,称作 URLGroup,是注册 HTTP。若要创建按需 HTTP 请求队列的 SYS。WDP 服务然后监视传入的请求每个已打包的插件请求队列。

在为插件的第一个路由请求,WDP 服务将启动 WDP 发起人在用户的安全上下文中。WDP 发起人反过来激活打包插件,将 HTTP 请求队列传输到该插件。  WDP 服务激活,并与打包通信在清单中的应用程序服务通过插件所述。应用服务连接功能与充当客户端的此连接,并打包插件充当服务器 WDP 发起人类似命名的管道。此赞助设计可确保长时间运行的请求不会中断由系统的资源管理策略。WDP 运行时,则开始为代表该插件的请求提供服务。  REST API 请求被分派给该打包即插即用项的已注册 RequestReceived 事件处理程序时,将自动处理内容请求。插件的生存期由 WDP 服务和进程生命期管理器 (PLM) 管理。有关当挂起后台任务时,管理插件状态的详细信息,请参阅文章,"启动、 正在恢复和后台任务"(bit.ly/2u0N7fO)。作业对象进一步可确保 WDP 服务断开已完成,终止任何正在运行 WDP 发起人和其关联打包插件。

编写打包插件

创建项目在创建之前你打包插件,你将需要决定您 Web 请求的处理程序是否可以运行作为后台任务或它必须运行在前台应用程序的过程中。前景执行如果是必需的例如,该处理程序需要访问应用程序的内部数据结构。在执行这种灵活性被通过基础 AppService,可以将其配置为背景或前景操作。有关 AppServices 的更详细的讨论,请参阅的文章,"创建和使用应用程序服务"(bit.ly/2uZsSfz),并且,"将转换服务运行在相同进程作为其主机应用"(bit.ly/2u0G8n1)。

在此示例中,我将实现前台处理程序和后台处理程序,并演示如何将静态内容和 REST Api 相集成。首先使用空白应用 (通用 Windows) C# 项目模板在 Visual Studio 中创建一个新的解决方案。名称 MyPackagedPlugin 解决方案和项目 MyApp。应用程序将承载前台处理程序。  出现提示时,面向至少创建者的更新 SDK (15063),以确保 WDP API 的可用性。接下来,添加到解决方案中,使用 Windows 运行时组件 Visual C# 模板的运行时组件库。将此项目 MyComponent。此库将承载后台处理程序。 

若要确保该组件包含在应用包,请在应用程序项目添加对它的引用。在解决方案资源管理器,展开应用程序项目节点,右键单击引用节点中,并选择添加引用。在引用管理器对话框中,展开项目节点,选择解决方案,然后检查 MyComponent 项目。

继续之前,设置要与您的计算机的体系结构匹配的解决方案平台。这是一项要求打包插件,因为不支持 WoW64。请注意,在这种情况下,我将部署到本地计算机,但建议也适用于时将部署到辅助目标设备。

编辑清单因为存在大量的清单更改需要进行,我将直接编辑 Package.appxmanifest 文件而不是使用设计器。在解决方案资源管理器中的下的应用节点中,右击 Package.appxmanifest 文件节点,并选取查看代码,若要编辑的 XML。

首先,将添加 uap4 rescap 命名空间声明并将对是必需的后续元素的别名:

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

若要使包在调试期间更容易地发现,我将为意义的名称从生成的 GUID 更改包名称:

<Identity Name="MyPackagedPlugin" Publisher="CN=msdn" Version="1.0.0.0" />

接下来,我将添加到 package\applications\application\extensions 元素中,每个已打包插件处理程序所需的应用程序扩展,如中所示图 4。 

图 4 添加应用程序扩展

<Package ...><Applications><Application ...>

  <Extensions>
        
    <!--Foreground (in app) packaged plug-in handler app service and WDP provider-->
    <uap:Extension 
      Category="windows.appService">
      <uap:AppService Name="com.contoso.www.myapp" />
    </uap:Extension>
    <uap4:Extension 
      Category="windows.devicePortalProvider">
      <uap4:DevicePortalProvider 
        DisplayName="MyPluginApp" 
        AppServiceName="com.contoso.www.myapp"
        ContentRoute="/myapp/www/" 
        HandlerRoute="/myapp/API/" />
    </uap4:Extension>
            
    <!--Background packaged plug-in handler app service and WDP provider-->
    <uap:Extension 
      Category="windows.appService" 
      EntryPoint="MyComponent.MyBackgroundHandler">
      <uap:AppService Name="com.contoso.www.mycomponent" />
    </uap:Extension>
    <uap4:Extension 
      Category="windows.devicePortalProvider">
      <uap4:DevicePortalProvider 
        DisplayName="MyPluginComponent" 
        AppServiceName="com.contoso.www.mycomponent"
        HandlerRoute="/mycomponent/API/" />
    </uap4:Extension>
   
  </Extensions>

</Application></Applications></Package>

每个处理程序需要两个扩展。AppService 扩展提供了 WDP 服务和托管处理程序 WDP 运行时之间的激活机制和通信通道。按照约定,AppServices 使用反向域名称方案来确保唯一性。如果实现背景 AppService,EntryPoint 属性是必需的并指定开始执行。如果实现前景 AppService,执行开始应用程序的 OnBackgroundActivated 方法和省略 EntryPoint 属性。

DevicePortalProvider 扩展为承载该处理程序 DevicePortalConnection 提供配置数据。DevicePortalProvider 表示 AppService 连接的客户端向 DevicePortalConnection 提供 URL 处理程序。AppServiceName 属性必须与 AppService 元素 (例如,com.contoso.www.myapp) 的名称属性相对应。DevicePortalProvider 元素可能为提供静态 Web 内容; 指定 ContentRoute用于调度到 REST API 处理程序; 的请求 HandlerRoute和/或文件名。ContentRoute 和 HandlerRoute 必须是唯一的。如果与内置的 WDP 路由,或与以前注册的打包插件路由,请将路由冲突,该插件将无法加载,提供适当的诊断消息。此外,相对 URL 必须映射到包下的相对路径 ContentRoute 安装文件夹 (例如 \myapp\www)。有关更多详细信息,请参阅 DevicePortalProvider 扩展规范 (bit.ly/2u1aqG8)。

最后,我将在其中添加我打包所需的功能插件:

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

PrivateNetworkClientServer 功能启用 HTTP。在 AppContainer 内 SYS 功能。在此演示中,我将部署直接从 Visual Studio 的执行的本地计算机上的包。  但是,到上架应用商店中,你的应用将还需要获取 devicePortalProvider 功能,仅限于 Microsoft 合作伙伴。有关详细信息,请参阅文章,"应用功能声明"(bit.ly/2u7gHkt)。这些功能是 WDP 运行时主机打包到插件所需的最低。你即插即用接程序的处理程序代码可能需要其他功能,具体取决于通用 Windows 平台 Api 调用。 

添加静态内容下一步,让我们创建的插件的状态页。页上和它所引用的任何其他静态的内容文件应放在对应于为该插件保留的内容路由应用相对路径在此情况下,/myapp/www。在解决方案资源管理器,右键单击你的应用程序项目节点,选择添加 |新文件夹。名称的文件夹 p。右键单击新添加的文件夹节点并再次选择添加 |新文件夹以创建一个子文件夹名为 www。按 Ctrl + N,并在新文件的对话框中,选择常规 |HTML 页模板。将此文件保存为 index.html 中,解决方案的 MyPackagedPlugin\MyApp\myapp\www 路径下。然后将此文件添加到项目文件夹路径,以便它包含作为包内容。右键单击新添加的 index.html 文件节点,然后选择属性。确认生成操作的默认属性值:内容,并将复制到输出目录:不要复制。

现在添加中所示标记图 5到新创建的 index.html。此页演示几种方法。首先,请注意,内置 WDP 资源,如 jquery.js 和 rest.js 库中,打包即插即用的外接程序,也可用。这使打包插件要组合的内置 WDP 功能特定于域的功能。有关详细信息,请参阅"设备门户 API 参考"一文 (bit.ly/2uFTTFD)。其次,请注意对即插即用接程序的 REST API 调用,应用程序的 /myapp/API/status 和组件的 /mycomponent/API/status 的引用。  下面的示例演示如何可以轻松地组合的即插即用接静态和动态内容。 

图 5 状态页面标记

<html>
<head>
  <title>My Packaged Plug-in Page</title>
  <script src="/js/rest.js"></script>
  <script src="/js/jquery.js"></script>
  <script type="text/javascript">
    function InitPage() {
      var webb = new WebbRest();
      webb.httpGetExpect200("/myapp/API/status")
        .done(function (response) {
          $('#app_status')[0].innerText = response.status;
        });
      webb.httpGetExpect200("/mycomponent/API/status")
        .done(function (response) {
          $('#comp_status')[0].innerText = response.status;
        });
    }
  </script>
</head>
<body onload="InitPage();">
  <div style="font-size: x-large;">
    My Packaged Plug-in Page
    <br/><br/>
    App Status:&nbsp;<span id="app_status" style="color:green"></span>
    <br/>
    Component Status:&nbsp;<span id="comp_status" style="color:green"></span>
  </div>
</body>
</html>

添加 REST API 现在我将实现一个 REST API 处理程序。如前所述的入口点的选择取决于是否实现背景或前景 AppService 的方式。在如何获取传入的应用程序服务连接一个细微的差别,超出设备门户连接实现等同这两种情况。我将启动与应用程序的前景处理程序。

打开源文件 App.xaml.cs 并添加以下命名空间,所需的每个处理程序:

using Windows.ApplicationModel.AppService;
using Windows.ApplicationModel.Background;
using Windows.System.Diagnostics.DevicePortal;
using Windows.Web.Http;
using Windows.Web.Http.Headers;

MyApp.App 类定义中,我将添加几个成员来实现处理程序的状态。  BackgroundTaskDeferral 交由后台任务的完成处理程序中,该主机执行并 DevicePortalConnection 实现 WDP 服务本身的连接。

sealed partial class App : Application
{
  private BackgroundTaskDeferral taskDeferral;
  private DevicePortalConnection devicePortalConnection;
  private static Uri statusUri = new Uri("/myapp/API/status", UriKind.Relative);

接下来,重写应用程序,以实例化 DevicePortalConnection 并订阅其事件中所示的后台激活处理图 6

图 6 实现 DevicePortalConnection

// Implement background task handler with a DevicePortalConnection
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
  // Take a deferral to allow the background task to continue executing 
  var taskInstance = args.TaskInstance;
  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 handlers for RequestReceived and Closed events
  devicePortalConnection.RequestReceived += DevicePortalConnection_RequestReceived;
  devicePortalConnection.Closed += DevicePortalConnection_Closed;
}

对于此处理程序,我将只需查找到的 URI /myapp/API/status 和使用 JSON 结构进行响应的请求中所示图 7。更复杂实现无法支持多个路由,、 区分获取和文章,检查 URI 参数,依此类推。

图 7 处理请求

// RequestReceived handler demonstrating response construction, based on request
private void DevicePortalConnection_RequestReceived(
  DevicePortalConnection sender, 
  DevicePortalConnectionRequestReceivedEventArgs args)
{
  if (args.RequestMessage.RequestUri.AbsolutePath.ToString() == 
    statusUri.ToString())
  {
    args.ResponseMessage.StatusCode = HttpStatusCode.Ok;
    args.ResponseMessage.Content = 
      new HttpStringContent("{ \"status\": \"good\" }");
    args.ResponseMessage.Content.Headers.ContentType = 
      new HttpMediaTypeHeaderValue("application/json");
  }
  else
  {
    args.ResponseMessage.StatusCode = HttpStatusCode.NotFound;
  }
}

最后,我处理 DevicePortalConnection,或直接使用其已关闭的事件,间接通过后台任务的取消关闭中所示图 8

图 8 关闭 DevicePortalConnection

// Complete the deferral if task is canceled or DevicePortal connection closed
private void Close()
{
  this.devicePortalConnection = null;
  this.taskDeferral.Complete();
}

private void TaskInstance_Canceled(IBackgroundTaskInstance sender, 
  BackgroundTaskCancellationReason reason)
{
  Close();
}

private void DevicePortalConnection_Closed(DevicePortalConnection sender, 
  DevicePortalConnectionClosedEventArgs args)
{
  Close();
}

实现组件项目的后台处理程序都与前台示例几乎完全相同。打开源文件 Class1.cs 并添加所需的命名空间,与前面。内的组件命名空间,将生成的类定义替换实现 IBackgroundTask 和其所需的运行方法的类: 

public sealed class MyBackgroundHandler : IBackgroundTask
{
  public void Run(IBackgroundTaskInstance taskInstance)
  {
    // Implement as for foreground handler's OnBackgroundActivated...
  }
}

Run 方法直接采用 IBackgroundTaskInstance 参数值。回想一下前台处理程序从 OnBackgroundActivated BackgroundActivatedEventArgs 参数 TaskInstance 成员获取此值。获取 taskInstance 这种差异,除了实现是相同的两个处理程序。从前台处理程序的 App.xaml.cs 文件中复制的剩余的实现。

测试打包插件

将部署到插件测试插件,通过 Visual Studio F5 部署或使用松散.appx 文件,则必须先部署它,(通过 Visual Studio 项目生成 |应用商店 |创建应用程序包菜单)。让我们使用前者。右键单击 MyApp 项目节点并选择属性。在项目属性表中,选择调试选项卡,然后启用与不启动复选框。

我建议设置断点,在 RequestReceived 事件处理程序,并可能在你即插即用接程序的入口点-你 BackgroundActivated 事件处理程序或运行方法重写。这将会确认插件已正确配置并激活已成功测试下一节中时。  现在,按 F5 部署和调试你的应用程序。

必须重新启动在调试模式后的包,WDP 部署的正在运行 WDP 才能对其进行检测。(以防你跳过 WDP 简介前面所述,你可能还需要安装开发人员模式包现在以确保 WDP 可用)。这可以通过切换从设置应用程序的更新和安全启用设备门户复选框完成 |开发人员页。但是,我要改为在作为控制台应用程序的调试模式下运行 WDP 服务。这将允许我看到 WDP 跟踪输出,它将帮助进行故障排除打包插件配置和执行问题时的输出。WDP 服务配置为使用系统帐户执行和在调试模式下运行 WDP 时,这是还必需。PsExec 实用程序,可用 PsTools 套件的一部分 (bit.ly/2tXiDf4),将帮助您执行该操作。

首先,创建的系统控制台中,在管理员控制台内运行从 PsExec:

> psexec -d -i -s -accepteula cmd /k title SYSTEM Console - Careful!

此命令将启动的系统帐户上下文中运行的第二个控制台窗口。通常情况下,此窗口使用旧的控制台中,和新的控制台功能,如文本换行重设大小、 文本选择增强功能和剪贴板快捷方式上不可用。如果你想要启用这些功能作为系统运行时,将以下内容保存到注册表脚本文件 (例如,EnableNewConsoleForSystem.reg) 并执行它:

[HKEY_USERS\.DEFAULT\Console]
"ForceV2"=dword:00000001

在系统控制台中,在调试模式下运行 WDP 启用清除和加密请求: 

> webmanagement -debug -clearandssl

您应看到类似于在输出图 9。  若要在调试模式下,安全地终止 WDP,只需按 Ctrl + C。这将维护你的系统控制台,因此你可以循环访问插件开发。 

在调试模式下的 Windows 设备门户

在调试模式下的图 9 Windows 设备门户

请注意,跟踪输出使用各个术语: webmanagement,WebB 和 WDP。这些存在出于历史原因并引用的其他子系统,但可以忽略区别。另请注意,在调试模式下执行使用从作为服务执行不同的配置。例如,在调试模式下,默认授权是默认禁用。这就无需输入凭据,可以防止自动 HTTPS 重定向,从而简化了从浏览器或命令行实用工具测试。此外,端口均已分配值 54321 和 54684。通常情况下,桌面 WDP 服务使用端口 50080 和 50443 (尽管如果这些都将使用,将动态分配随机端口)。这将允许 WDP 以在调试模式下执行而不会干扰 WDP 生产模式下作为服务运行。但是,它可能成为 irksome 若要切换到 Url 的端口号,在测试时,根据执行的 WDP 模式。如果是这样,你可以使用-httpPort 和-httpsPort 命令行选项来显式设置以匹配生产模式下的端口号。在这种情况下,你将需要确保 WDP 服务处于关闭以防止冲突。若要查看所有 WDP 命令行选项,请键入:

> WebManagement.exe-?

跟踪输出中所示,几个内置的即插即用单元会自动加载 WDP 启动的一部分。WDP 然后发现的部署打包插件使用的报表"找到 2 包"(更确切地说,一个包中的使用两个提供程序)。第一个提供程序描述前台处理程序,并确认的静态 ContentRoute URL 保留和包文件路径和 REST HandlerRoute 其相应的映射。此时,WDP 已创建按需 HTTP 请求队列服务这些路由,并且正在等待请求。第二个提供程序描述的后台处理程序,指定仅 REST HandlerRoute。它,也可供业务。

执行插件建议开发和测试你的插件使用极好的命令行实用工具我以前所述,curl。对于更简单的方案中,浏览器已足够,但 curl 还是提供了对标头、 身份验证等细化的控制。这可以更容易地使用时启用 WDP 加密、 身份验证和 CSRF 保护选项。在这些情况下,curl"-verbose"选项也是用于故障排除。

图 10演示请求前台处理程序的资源: 其状态 REST API 和网页。 

使用 Curl 测试

图 10 使用 Curl 测试

图 11使用浏览器请求应用程序的状态页所示,反过来演示嵌入 REST API 调用。

使用浏览器进行测试

图 11 使用浏览器进行测试

为路由,您即插即用接发出任何请求时,应在调试器中命中条件中的入口点设置的断点。和 REST API 请求具体而言,在 RequestReceived 事件处理程序应命中断点。您还会看到确认在 WDP 诊断输出打包插件已激活。

疑难解答值得几个防止处理程序的正确执行的常见错误是:

  • 清单不匹配,例如 AppService 名称或入口点
  • 会添加到应用的后台处理程序组件项目引用
  • 部署使用包和服务之间体系结构不匹配
  • 在应用程序项目中,而不作为 Windows 运行时组件实现 IBackgroundTask

在调试模式下运行时,WDP 将为这些错误,提供特定的诊断消息,在可能的情况。三个失败尝试激活插件后,WDP 将禁用该插件该会话与诊断所示:

WDP Packaged Plugin: Disabling request queue after 3 failed attempts

包清单或代码中进行更正,之后,通常需要重启 WDP,重置已禁用的计数器和包配置更改重新扫描。

总结

Windows 设备门户 API 提供一个安全且简单的机制,来扩展的打包的即插即用签入的 Windows 应用商店应用的诊断功能。此 API 的未来增强功能将启用的打包插件在 WDP UI 中的集成,有关安装和控制打包的插件引入 REST Api 并展开可能为打包插件的应用程序。


Scott Jones适用于 Microsoft 的软件工程师作为应用程序平台和工具团队。

衷心感谢以下 Microsoft 技术专家对本文的审阅:  Hirsch Singhal


在 MSDN 杂志论坛讨论这篇文章