赞
踩
https://chromium.googlesource.com/chromium/src/+/312b6bf/services/service_manager/README.md
Service Manager是一个组件,像 Chromium 这样的大型应用程序可以使用它来支持跨平台、多进程、面向服务、连字符形容词负载的体系结构。
本文档介绍了如何将Service Manager嵌入到应用程序中,以及如何定义和注册服务以供其管理。如果您只想阅读有关定义服务和使用公共服务 API 的内容,请跳至主要服务部分。
要嵌入Service Manager,应用程序应链接到//services/service_manager/embedder
. service_manager::MainDelegate这为大多数平台定义了一个主要入口点,并为应用程序实现提供了一个相对较小的接口。特别是,应用程序至少应实现GetServiceManifests提供有关包含应用程序的全套服务的元数据。
GetServiceManifests
服务管理器的生产用途。这是因为一堆流程启动和管理逻辑仍然存在于内容层。随着更多此类代码进入 Service Manager 内部,Chromium 将开始看起来更像任何其他 Service Manager 嵌入器。
TODO:在此处改进嵌入程序文档,并在 MainDelegate 支持后包括对进程内服务启动的支持。
此上下文中的服务可以定义为满足以下所有约束的任何独立的应用程序逻辑主体:
Service Manager负责管理各个Services实例的创建和互连,无论它们是嵌入在现有进程中还是每个都隔离在专用进程中。托管服务进程可以使用各种受支持的沙箱配置中的任何一种进行沙箱化。
本节将介绍整个服务开发过程中的重要概念和 API,并在此过程中构建一个小型工作示例服务。
许多开发人员担心一个服务或一组服务的正确“大小”或粒度是多少。这是有道理的,在选择更简单且可能更高效的整体实现与选择更模块化但通常更复杂的实现之间总会存在一些设计张力。
这种矛盾的一个典型例子是 Chromium 的device
service。该服务托管了许多独立的设备接口子系统,例如 USB、蓝牙、HID、电池状态等。您可以很容易地想象为这些功能中的每一个提供单独的服务,但最终决定将它们合并为一个服务与硬件设备能力相关。影响这一决定的一些因素:
考虑到上述所有条件,选择数量较少的服务似乎是正确的决定。
自己做出此类决定时,请运用您的最佳判断。如有疑问,请在services-dev@chromium.org上启动一个 bike-shedding centithread 。
任何服务实现中的中心固定装置就是它的Service实现。这是一个很小的接口,实际上只有三个实际感兴趣的虚方法,都可以选择实现:
- class Service {
- public:
- virtual void OnStart();
- virtual void OnBindInterface(const BindSourceInfo& source,
- const std::string& interface_name,
- mojo::ScopedMessagePipeHandle interface_pipe);
- virtual void OnDisconnected();
- };
每一个服务都实现这样的service接口以成为service的一个子类,以便service manager可以使用生命周期事件和来自其他服务的接口请求调用该服务。
Service
。
通过本文档的其余部分,我们将构建一个基本的工作服务实现,完成一个清单和简单的测试。我们称它为storage
service
constants.mojom
文件中:
- // src/services/storage/public/mojom/constants.mojom
- module storage.mojom;
-
- // This string will identify our service to the Service Manager. It will be used
- // in our manifest when registering the service, and clients can use it when
- // sending interface requests to the Service Manager if they want to reach our
- // service.
- const string kServiceName = "storage";
-
- // We'll use this later, in service manifest definitions.
- const string kAllocationCapability = "allocation";
以及一些有用的接口定义:
// src/services/storage/public/mojom/block.mojom module storage.mojom; interface BlockAllocator { // Allocates a new block of persistent storage for the client. If allocation // fails, |receiver| is discarded. Allocate(uint64 num_bytes, pending_receiver<Block> receiver); }; interface Block { // Reads and returns a small range of bytes from the block. Read(uint64 byte_offset, uint16 num_bytes) => (array<uint8> bytes); // Writes a small range of bytes to the block. Write(uint64 byte_offset, array<uint8> bytes); };
最后我们将定义我们的基本Service
子类:
// src/services/storage/storage_service.h #include "base/macros.h" #include "services/service_manager/public/cpp/service.h" #include "services/service_manager/public/cpp/service_binding.h" #include "services/storage/public/mojom/block.mojom.h" namespace storage { class StorageService : public service_manager::Service, public mojom::BlockAllocator { public: explicit StorageService(service_manager::mojom::ServiceRequest request) : service_binding_(this, std::move(request)) {} ~StorageService() override = default; private: // service_manager::Service: void OnBindInterface(const service_manager::BindSourceInfo& source, const std::string& interface_name, mojo::ScopedMessagePipeHandle interface_pipe) override { if (interface_name == mojom::BlockAllocator::Name_) { // If the Service Manager sends us a request with BlockAllocator's // interface name, we should treat |interface_pipe| as a // PendingReceiver<BlockAllocator> that we can bind. allocator_receivers_.Add( this, mojo::PendingReceiver<mojom::BlockAllocator>(std::move(interface_pipe))); } } // mojom::BlockAllocator: void Allocate(uint64_t num_bytes, mojo::PendingReceiver<mojom::Block> receiver) override { // This space intentionally left blank. } service_manager::ServiceBinding service_binding_; mojo::ReceiverSet<mojom::BlockAllocator> allocator_receivers_; DISALLOW_COPY_AND_ASSIGN(StorageService); }; } // namespace storage
这是一个基本的服务实现:
首先,请注意StorageService
构造函数接service_manager::mojom::ServiceRequest
并立即将其传递给service_binding_
构造函数。这是服务实现中几乎通用的惯例,您的服务可能也会这样做。是ServiceRequest
服务管理器用来驱动您的服务的接口管道,是ServiceBinding
一个帮助程序类,它将来自服务管理器的消息转换为Service
您已实现的类的更简单的接口方法。
StorageService
还实现了OnBindInterface
,这是服务管理器ServiceBinding
在决定将另一个服务的接口请求路由到您的服务实例时调用的(通过您的)。请注意,因为这是旨在支持任意接口的通用 API,所以请求以接口名称和原始消息管道句柄的形式出现。检查名称并决定如何(甚至是否)绑定管道是服务的责任。在这里我们只识别传入的BlockAllocator
请求并丢弃其他任何东西。
我们需要放下的最后一项服务是它的清单。
服务的清单是一个简单的静态数据结构,在其初始化过程的早期提供给服务管理器。服务管理器将它拥有的所有清单数据组合在一起,以形成它正在协调的系统的完整画面。它使用所有这些信息来做出如下决定:
所有这些元数据都包含在Manifest类的不同实例中。
A Basic Manifest
定义服务清单的最常见方法是将其放在服务的 C++ 客户端库中自己的源目标中。为了将内联一次性初始化的便利与避免静态初始化器结合起来,通常这意味着使用函数局部静态和base::NoDestructor
如下service_manager::ManifestBuilder
所示。首先是标题:
- // src/services/storage/public/cpp/manifest.h
-
- #include "services/service_manager/public/cpp/manifest.h"
-
- namespace storage {
-
- const service_manager::Manifest& GetManifest();
-
- } // namespace storage
对于实际实施:
// src/services/storage/public/cpp/manifest.cc #include "services/storage/public/cpp/manifest.h" #include "base/no_destructor.h" #include "services/storage/public/mojom/constants.mojom.h" #include "services/service_manager/public/cpp/manifest_builder.h" namespace storage { const service_manager::Manifest& GetManifest() { static base::NoDestructor<service_manager::Manifest> manifest{ service_manager::ManifestBuilder() .WithServiceName(mojom::kServiceName) .Build()}; return *manifest; }; } // namespace storage
这里我们只指定了服务名称,匹配中定义的常量,constants.mojom
以便其他服务无需硬编码字符串即可轻松找到我们。
有了这个清单定义,我们的服务就无法到达其他服务,其他服务也无法到达我们;这是因为我们既不公开也不要求任何功能,因此服务管理器将始终阻止来自我们或针对我们的任何接口请求。
Exposing Interfaces
让我们公开一个授予绑定管道权限的“分配器”功能BlockAllocator
。我们可以如下扩充上面的清单定义:
- ...
- #include "services/storage/public/mojom/block.mojom.h"
- ...
-
- ...
- .WithServiceName(mojom::kServiceName)
- .ExposeCapability(
- mojom::kAllocatorCapability,
- service_manager::Manifest::InterfaceList<mojom::BlockAllocator>())
- .Build()
- ...
这声明了我们的服务公开的能力的存在"allocator"
,并指定授予客户端此能力意味着授予它发送我们的服务storage.mojom.BlockAllocator
接口请求的特权。
您可以为每个公开的功能列出任意数量的接口,并且多个功能可以列出相同的接口。
注意:如果您希望其他服务能够通过服务管理器(请参阅连接器)请求它,则只需要通过功能公开接口——也就是说,如果您在实现中处理对它的请求Service::OnBindInterface
。
将此与如上所述的可传递获取的接口进行对比Block
。服务管理器不调解现有接口连接的行为,因此一旦客户端拥有一个服务管理器,BlockAllocator
他们就可以使用它们BlockAllocator.Allocate
发送任意数量的Block
请求。此类请求直接转到BlockAllocator
管道绑定到的服务端实现,因此清单内容与其行为无关。
Getting Access to Interfaces
我们不需要向我们的storage
manifest,添加任何其他内容,但如果其他服务想要访问,他们需要在他们的清单中声明他们需要我们的"allocation"
能力。为了便于维护,他们会利用我们公开定义的常量来执行此操作。这很简单:
- // src/services/some_other_pretty_cool_service/public/cpp/manifest.cc
-
- ... // Somewhere along the chain of ManifestBuilder calls...
- .RequireCapability(storage::mojom::kServiceName,
- storage::mojom::kAllocationCapability)
- ...
现在some_other_pretty_cool_service
可以使用它的连接器 Connector连接器向Service Manager请求BlockAllocator
我们的服务,如下所示:
- mojo::Remote<storage::mojom::BlockAllocator> allocator;
- connector->Connect(storage::mojom::kServiceName,
- allocator.BindNewPipeAndPassReceiver());
-
- mojo::Remote<storage::mojom::Block> block;
- allocator->Allocate(42, block.BindNewPipeAndPassReceiver());
-
- // etc..
Other Manifest Elements
结构中还有一些其他可选元素Manifest
可以影响您的服务在运行时的行为方式。请参阅当前Manifest定义和注释以及ManifestBuilder最完整和最新的信息,但清单指定的一些更常见的属性是:
ManifestOptions
字段中。其中包括沙盒类型(请参阅沙盒配置)、实例共享策略以及用于控制一些特殊功能的各种行为标志。连接服务使其可以在生产环境中运行实际上目前不在本文档的范围内,只是因为它仍然在很大程度上取决于嵌入服务管理器的环境。现在,如果你想让你的小服务连接到 Chromium 中,你应该查看以 Chromium 为中心的Mojo 和服务简介和/或Servicifying Chromium 功能文档中的相关部分。
出于本文档的目的,我们将重点关注在进程内和进程外服务的测试环境中运行服务。
测试服务时使用三种主要方法,以不同的组合应用:
标准单元测试
这非常适合涵盖服务内部组件的详细信息并确保它们按预期运行。关于服务,这里没有什么特别之处。代码就是代码,您可以对其进行单元测试。
进程外端到端测试
这些有利于尽可能接近地模拟生产环境,将您的服务实现与测试(客户端)代码隔离在一个单独的进程中。
这种方法的主要缺点是它限制了您的测试查看或观察内部服务状态的能力,这有时在测试环境中很有用(例如,以可预测的方式伪造某些行为)。通常,支持此类控件意味着向您的服务添加仅测试接口。
帮助程序TestServiceManager和service_executableGN 目标类型使这很容易完成。您只需为您的服务定义一个新的入口点:
- // src/services/storage/service_main.cc
-
- #include "base/message_loop.h"
- #include "services/service_manager/public/cpp/service_executable/main.h"
- #include "services/storage/storage_service.h"
-
- void ServiceMain(service_manager::ServiceRequest request) {
- base::SingleThreadTaskExecutor main_task_executor;
- storage::StorageService(std::move(request)).RunUntilTermination();
- }
和一个 GN 目标:
import "services/service_manager/public/cpp/service_executable.gni" service_executable("storage") { sources = [ "service_main.cc", ] deps = [ # The ":impl" target would be the target that defines our StorageService # implementation. ":impl", "//base", "//services/service_manager/public/cpp", ] } test("whatever_unittests") { ... # Include the executable target as data_deps for your test target data_deps = [ ":storage" ] }
最后在您的测试代码中,用于TestServiceManager
在您的测试环境中创建一个真正的 Service Manager 实例,配置为了解您的storage
服务。
TestServiceManager
允许您注入人工服务实例以将您的测试套件视为实际服务实例。您可以为您的测试提供一个清单,以模拟需要(或不需要)各种功能,并获得一个Connector
用于访问您的被测服务的清单。这看起来像:
#include "services/service_manager/public/cpp/manifest_builder.h" #include "services/service_manager/public/cpp/test/test_service.h" #include "services/service_manager/public/cpp/test/test_service_manager.h" #include "services/storage/public/cpp/manifest.h" #include "services/storage/public/mojom/constants.mojom.h" #include "services/storage/public/mojom/block.mojom.h" ... TEST(StorageServiceTest, AllocateBlock) { const char kTestServiceName[] = "my_inconsequentially_named_test_service"; service_manager::TestServiceManager service_manager( // Make sure the Service Manager knows about the storage service. {storage::GetManifest, // Also make sure it has a manifest for our test service, which this // test will effectively act as an instance of. service_manager::ManifestBuilder() .WithServiceName(kTestServiceName) .RequireCapability(storage::mojom::kServiceName, storage::mojom::kAllocationCapability) .Build()}); service_manager::TestService test_service( service_manager.RegisterTestInstance(kTestServiceName)); mojo::Remote<storage::mojom::BlockAllocator> allocator; // This Connector belongs to the test service instance and can reach the // storage service through the Service Manager by virtue of the required // capability above. test_service.connector()->Connect(storage::mojom::kServiceName, allocator.BindNewPipeAndPassReceiver()); // Verify that we can request a small block of storage. mojo::Remote<storage::mojom::Block> block; allocator->Allocate(64, block.BindNewPipeAndPassReceiver()); // Do some stuff with the block, etc... }
进程内服务 API 测试
有时您希望主要通过其客户端 API 访问您的服务,但您也希望能够——无论是为了方便还是出于必要——在测试代码中观察或操作其内部状态。在这种情况下,在进程内运行服务是理想的,在这种情况下,涉及服务管理器或处理清单没有多大用处。
相反,您可以使用 aTestConnectorFactory为自己提供一个工作Connector
对象,该对象将接口请求直接路由到您直接连接的特定服务实例。举个简单的例子,假设我们有一些客户端库辅助函数,用于在给定 a 时分配一个存储块Connector
:
// src/services/storage/public/cpp/allocate_block.h namespace storage { // This helper function can be used by any service which is granted the // |kAllocationCapability| capability. mojo::Remote<mojom::Block> AllocateBlock(service_manager::Connector* connector, uint64_t size) { mojo::Remote<mojom::BlockAllocator> allocator; connector->Connect(mojom::kServiceName, allocator.BindNewPipeAndPassReceiver()); mojo::Remote<mojom::Block> block; allocator->Allocate(size, block.BindNewPipeAndPassReceiver()); return block; } } // namespace storage
我们的测试可能类似于:
- EST(StorageTest, AllocateBlock) {
- service_manager::TestConnectorFactory test_connector_factory;
- storage::StorageService service(
- test_connector_factory.RegisterInstance(storage::mojom::kServiceName));
-
- constexpr uint64_t kTestBlockSize = 64;
- mojo::Remote<storage::mojom::Block> block = storage::AllocateBlock(
- test_connector_factory.GetDefaultConnector(), kTestBlockSize);
- block.FlushForTesting();
-
- // Verify that we have the expected number of bytes allocated within the
- // service implementation.
- EXPECT_EQ(kTestBlockSize, service.GetTotalAllocationSizeForTesting());
- }
services实例用Connector来向Service manager发送请求
Sending Interface Receivers 发送接口接收器
到目前为止,最常见和最有用的方法Connector
是Connect
,它允许您的服务将接口接收器发送到系统中的另一个服务,配置允许。
假设该storage
服务实际上依赖于更低级别的存储服务来访问其磁盘,您可以想象其块分配代码执行如下操作:
- mojo::Remote<real_storage::mojom::ReallyRealStorage> storage;
- service_binding_.GetConnector()->Connect(
- real_storage::mojom::kServiceName, storage.BindNewPipeAndPassReceiver());
- storage->AllocateBytes(...);
请注意,这个特定重载的第一个参数Connect
是一个字符串,但更通用的形式Connect
是一个ServiceFilter
. 在有关服务过滤器的部分中查看有关这些的更多信息。
Registering Service Instances 注册服务实例
可以授予的超能力服务之一是强制将新服务实例注入服务管理器世界的能力。这是通过完成的Connector::ServiceInstance,并且仍然被 Chromium 的浏览器进程大量使用。大多数服务不需要接触此 API。
在多线程环境中的使用
连接器不是线程安全的,但它们支持克隆。有两种有用的方法可以将新连接器与不同线程上的现有连接器相关联。
您可以在自己的Clone
线程Connector
上,然后将克隆传递给另一个线程:
- std::unique_ptr<service_manager::Connector> new_connector = connector->Clone();
- base::PostTask(...[elsewhere]...,
- base::BindOnce(..., std::move(new_connector)));
Connector
或者你可以从你站着的地方创建一个全新的,然后异步地将它与另一个线程上的一个相关联:
- mojo::PendingReceiver<service_manager::mojom::Connector> receiver;
- std::unique_ptr<service_manager::Connector> new_connector =
- service_manager::Connector::Create(&receiver);
-
- // |new_connector| can be used to start issuing calls immediately, despite not
- // yet being associated with the establshed Connector. The calls will queue as
- // long as necessary.
-
- base::PostTask(
- ...[over to the correct thread]...,
- base::BindOnce([](
- mojo::PendingReceiver<service_manager::Connector> receiver) {
- service_manager::Connector* connector = GetMyConnectorForThisThread();
- connector->BindConnectorReceiver(std::move(receiver));
- }));
服务管理器启动的每个服务实例都被分配了一个全局唯一的(跨空间和时间)标识,由Identity类型封装。该值被传递给服务,并在调用之前立即保留和公开。ServiceBinding
Service::OnStart
有四个组成部分Identity
:
您已经非常熟悉service name:这是服务在其清单中声明的任何内容,例如, "storage"
。
Instance ID 实例编号
Instance ID是一个base::Token
限定符,如果出于任意原因需要多个实例,它仅用于区分服务的多个实例。默认情况下,实例在启动时获得的实例 ID 为零,除非连接的客户端明确请求特定的实例 ID。这样做需要Additional Capabilities涵盖的特殊清单声明功能。
"unzip"
Chrome 中的服务用于安全地解压不受信任的 Chrome 扩展程序 (CRX) 存档,但我们不希望同一过程解压多个扩展程序。base::Token
为了支持这一点,Chrome为其连接到服务时使用的实例 ID生成一个随机数"unzip"
,这会在每个此类连接的新隔离进程中创建一个新服务实例。有关如何完成此操作的信息,请参阅服务过滤器。
Instance ID 实例组 ID
所有创建的服务实例都隐含地属于一个实例组,该实例组也由 标识base::Token
。除非被Additional Capabilities特殊授权,或者目标服务是单例或跨组共享,否则发出接口请求的服务只能到达同一实例组中的其他服务实例。有关详细信息,请参阅实例组。
Globally Unique ID全球唯一 ID
最后,全球唯一 ID是一个加密安全的、不可猜测的随机base::Token
值,可以被认为在所有时间和空间都是唯一的。这永远无法由实例甚至高特权服务控制,其唯一目的是确保Identity
其自身在时间和空间上都被视为唯一的。请参阅服务过滤器和观察服务实例,了解为什么这种唯一性属性很有用,有时甚至是必要的。
假设服务管理器由于足够的能力要求而决定允许接口请求,它必须考虑许多因素来决定将请求路由到哪里。第一个因素是目标服务的实例共享策略,在其清单中声明。支持的策略有以下三种:
ServiceFilter
以及由ServiceFilter
源实例组提供或继承的实例组。ServiceFilter
,但实例组ServiceFilter
和源实例都被完全忽略。基于上述策略之一,服务管理器确定现有服务实例是否与给定指定的参数ServiceFilter
以及源实例自己的身份相匹配。如果是这样,该服务管理器将通过 将接口请求转发到该实例Service::OnBindInterface
。否则,它将生成一个与约束充分匹配的新实例,并将请求转发到该新实例。
服务实例被组织成实例组。这些是实例的任意分区,主机应用程序可以使用它们来施加各种安全边界。
系统中的大多数服务都没有在传递给时指定它们要连接到的实例组的特权ServiceFilter
(Connector::Connect
请参阅其他功能)。因此,大多数调用隐式继承调用者的组 ID,并且仅在针对在其清单中采用单例或跨组共享策略Connect
的服务时跨出调用者的实例组。
单例和跨组共享服务本身总是在它们自己的隔离组中运行。
最常见的Connect
调用形式传递一个简单的字符串作为第一个参数。这实际上是在告诉service manager ,调用者不关心有关目标实例身份的任何细节——它只关心与指定服务的某个实例交谈。
当客户确实关心其他细节时,他们可以显式构造并传递一个ServiceFilter
对象,该对象实质上提供所需目标实例的总和的某个子集Identity
。
在 a 中指定实例组或实例 IDServiceFilter
需要服务在其清单选项中声明其他功能。
AServiceFilter
还可以包装一个完整的Identity
值,包括全局唯一 ID。此过滤器始终只匹配在空间和时间上唯一的特定实例。因此,如果已识别的实例已经死亡并被具有相同服务名称、相同实例 ID 和相同实例组的新实例替换,请求仍然会失败,因为全局唯一 ID 组件永远不会匹配这个或任何未来的实例。
以特定为目标的一个有用属性Identity
是客户端可以连接而不会引发新的目标实例创建的任何风险:目标存在并且可以路由请求,或者目标不存在并且请求将被丢弃。
服务清单可用于ManifestOptionsBuilder
设置一些额外的布尔选项来控制其服务管理器权限:
CanRegisterOtherServiceInstances
- 如果这是true
服务可以调用RegisterServiceInstance
它来Connector
强制将新的服务实例引入到环境中。CanConnectToInstancesWithAnyId
- 如果这是true
服务可以在ServiceFilter
它传递给的任何实例中指定一个实例 ID Connect
。CanConnectToInstancesInAnyGroup
- 如果这是true
该服务可以在ServiceFilter
它传递给的任何对象中指定一个实例组 ID Connect
。一项服务可以通过将另一项服务的清单嵌套在自己的清单中来声明它打包了另一项服务。
这向 Service Manager发出信号,表明它在需要打包服务的新实例时应该遵从打包服务。例如,如果我们提供清单:
- service_manager::ManifestBuilder()
- .WithServiceName("fruit_vendor")
- ...
- .PackageService(service_manager::ManifestBuilder()
- .WithServiceName("banana_stand")
- .Build())
- .Build()
如果有人想连接到服务的一个新实例"banana_stand"
(,服务管理器会请求一个合适的"fruit_vendor"
实例代表它做这件事。
"fruit_vendor"
尚未运行——正如上面实例共享中描述的规则所确定的——一个将首先由服务管理器生成。
为了支持此操作,必须fruit_vendor
公开一个准确命名的功能"service_manager:service_factory"
,其中包括"service_manager.mojom.ServiceFactory"
接口。service_manager.mojom.ServiceFactory
然后它必须在其实现中处理对接口的请求Service::OnBindInterface
。ServiceFactory
服务提供的实现必须处理CreateService
将由服务管理器发送的。此调用将包括服务的名称和ServiceRequest
需要绑定的新服务实例。
服务可以使用它,例如,如果在某些运行时环境中,他们想要与另一个服务共享他们的进程。
content_packaged_services
服务,它打包了系统中几乎所有其他已注册的服务,因此服务管理器将ServiceFactory
几乎所有服务实例创建操作(通过)推迟到内容。
服务清单支持为在进程外运行时启动的服务指定固定的沙箱配置。目前这些值是字符串,必须与此处定义的常量之一匹配。
最常见的默认值是"utility"
,这是一个限制性的沙箱配置,通常是一个安全的选择。对于必须在非沙盒环境下运行的服务,请使用值"none"
. 其他沙箱配置的使用应在 Chrome 安全审查员的建议下进行。
需要"service_manager:service_manager
服务能力的服务"service_manager"
可以连接到"service_manager"
服务以请求ServiceManager接口。这又可以用于注册一个新的ServiceManagerListener以观察与服务管理器托管的所有服务实例相关的生命周期事件。
整个树中有几个这样的例子。
如果本文档在某些方面没有帮助,请向您友好的services-dev@chromium.org邮件列表发送消息。
也不要忘记查看树中的其他Mojo 和服务文档。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。