连接组件

本文档演示了如何使用功能和其他工具将父级组件与子级组件搭配使用,以便父级组件管理其子级。

概念

在继续阅读本指南之前,您应先了解以下概念:

  • 用于描述组件正常运行所需的capabilities组件向其他组件公开的功能会组装到公开目录中。
  • 每个组件都会收到一个 Directory 通道服务器端的句柄,该句柄称为“传出目录”。 组件的程序会使其通过此目录提供的所有功能可检测到。
  • 在运行时,组件实例描述了组件实例之间的父子关系。组件实例树和该树上的功能路由统称为组件拓扑
  • 或使用组件集合以动态方式声明子组件。集合是动态子项的容器,这些子项可能会在运行时使用 fuchsia.component.Realm 框架协议创建和销毁。

如需详细了解这些概念,请参阅令牌功能

通过路由实现连接功能

组件通过 capability 进行互动。组件提供的功能需要在该组件的清单中声明,并且为了让其他组件能够使用这些功能,需要通过 exposeoffer 声明将这些功能路由到父/子组件。使用该 capability 的其他组件也需要在其清单中声明其使用情况。为了让其程序能够在运行时使用该 capability,所使用的 capability 必须路由到该组件(由父级提供或由子级公开)。

capability 路由是指组件管理器执行的递归过程,该过程会遵循清单文件中所述的各个路由步骤来识别服务组件。在以下情况下,系统会发起 capability 路由:

  • 组件的程序会在其
  • 组件的程序会在另一个组件的目录中打开路径
  • 开发者调用 ffx component route
  • 启动依赖于尚未路由其后备解析器或运行器功能的解析器或运行器的组件。

路由是延迟执行的:虽然某项 capability 可以配置为由父级或子级提供(直接或间接通过进一步委托),但在启动路由操作时,目标组件可能尚未解析完毕。实际上,这意味着在尝试路由之前,可能无法知道从请求组件到最终投放组件的完整路线。

提供 capability 实现

提供 capability 的组件必须在其组件清单中使用 capabilities 声明来声明该 capability。

请参阅以下示例,其中在提供方组件的清单中声明了 FIDL 协议 capability

{
    include: [
        "inspect/client.shard.cml",
        "syslog/client.shard.cml",
    ],

    // Information about the program to run.
    program: {
        // Use the built-in ELF runner.
        runner: "elf",

        // The binary to run for this component.
        binary: "bin/echo_server_rust",
    },

    // Capabilities provided by this component.
    capabilities: [
        { protocol: "fidl.examples.routing.echo.Echo" },
    ],
    expose: [
        {
            protocol: "fidl.examples.routing.echo.Echo",
            from: "self",
        },
    ],
}

在运行时,服务器组件使用 fuchsia.io 协议在其传出目录中提供该 capability 的实现。生成的 FIDL 绑定会封装此句柄,并使提供程序能够开始接收传入请求:

Rust

// Wrap protocol requests being served.
enum IncomingRequest {
    Echo(EchoRequestStream),
}

#[fuchsia::main(logging = false)]
async fn main() -> Result<(), anyhow::Error> {
    let mut service_fs = ServiceFs::new_local();

    // Initialize inspect
    component::health().set_starting_up();
    let _inspect_server_task = inspect_runtime::publish(
        component::inspector(),
        inspect_runtime::PublishOptions::default(),
    );

    // Serve the Echo protocol
    service_fs.dir("svc").add_fidl_service(IncomingRequest::Echo);
    service_fs.take_and_serve_directory_handle().context("failed to serve outgoing namespace")?;

    // Component is serving and ready to handle incoming requests
    component::health().set_ok();

    // Attach request handler for incoming requests
    service_fs
        .for_each_concurrent(None, |request: IncomingRequest| async move {
            match request {
                IncomingRequest::Echo(stream) => handle_echo_request(stream).await,
            }
        })
        .await;

    Ok(())
}

// Handler for incoming service requests
async fn handle_echo_request(mut stream: EchoRequestStream) {
    while let Some(event) = stream.try_next().await.expect("failed to serve echo service") {
        let EchoRequest::EchoString { value, responder } = event;
        responder.send(value.as_ref().map(|s| &**s)).expect("failed to send echo response");
    }
}

C++

// Handler for incoming service requests
class EchoImplementation : public fidl::examples::routing::echo::Echo {
 public:
  void EchoString(fidl::StringPtr value, EchoStringCallback callback) override { callback(value); }
  fidl::examples::routing::echo::Echo_EventSender* event_sender_;
};

int main(int argc, const char** argv) {
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();

  // Initialize inspect
  inspect::ComponentInspector inspector(loop.dispatcher(), inspect::PublishOptions{});
  inspector.Health().StartingUp();

  // Serve the Echo protocol
  EchoImplementation echo_instance;
  fidl::Binding<fidl::examples::routing::echo::Echo> binding(&echo_instance);
  echo_instance.event_sender_ = &binding.events();
  fidl::InterfaceRequestHandler<fidl::examples::routing::echo::Echo> handler =
      [&](fidl::InterfaceRequest<fidl::examples::routing::echo::Echo> request) {
        binding.Bind(std::move(request));
      };
  context->outgoing()->AddPublicService(std::move(handler));

  // Component is serving and ready to handle incoming requests
  inspector.Health().Ok();

  return loop.Run();
}

连接到路由功能

客户端组件使用 use 声明在其组件清单中声明可能请求的功能。

请参阅以下示例,了解使用上一个组件提供的 FIDL 协议的客户端组件的清单:

{
    include: [
        // Enable logging on stdout
        "syslog/client.shard.cml",
    ],

    // Information about the program to run.
    program: {
        // Use the built-in ELF runner.
        runner: "elf",

        // The binary to run for this component.
        binary: "bin/echo_client_rust",

        // Program arguments
        args: [ "Hello Fuchsia!" ],
    },


    // Capabilities used by this component.
    use: [
        { protocol: "fidl.examples.routing.echo.Echo" },
    ],
}

在运行时,客户端组件使用 fuchsia.io 协议打开其命名空间中填充的路径,以获取其他组件提供的功能。Fuchsia 组件库可与生成的 FIDL 绑定搭配使用,以提供用于通过通道进行通信的结构化接口:

Rust

#[fuchsia::main]
async fn main() -> Result<(), anyhow::Error> {
    // Parse arguments, removing binary name
    let mut args: Vec<String> = std::env::args().collect();
    args.remove(0);

    // Connect to FIDL protocol
    let echo = connect_to_protocol::<EchoMarker>().expect("error connecting to echo");

    // Send messages over FIDL interface
    for message in args {
        let out = echo.echo_string(Some(&message)).await.expect("echo_string failed");
        tracing::info!("Server response: {}", out.as_ref().expect("echo_string got empty result"));
    }

    Ok(())
}

C++

int main(int argc, const char* argv[], char* envp[]) {
  // Set tags for logging.
  fuchsia_logging::LogSettingsBuilder builder;
  builder.WithTags({"echo_client"}).BuildAndInitialize();

  // Connect to FIDL protocol
  fidl::examples::routing::echo::EchoSyncPtr echo_proxy;
  auto context = sys::ComponentContext::Create();
  context->svc()->Connect(echo_proxy.NewRequest());

  // Send messages over FIDL interface for each argument
  fidl::StringPtr response = nullptr;
  for (int i = 1; i < argc; i++) {
    ZX_ASSERT(echo_proxy->EchoString(argv[i], &response) == ZX_OK);
    if (!response.has_value()) {
      FX_LOG_KV(INFO, "echo_string got empty result");
    } else {
      FX_LOG_KV(INFO, "Server response", FX_KV("response", response->c_str()));
    }
  }

  return 0;
}

组件的父级负责将所有必要的功能路由到该组件。

将部分已使用的功能标记为可选

组件成功运行所需的功能并非全部由组件使用。有时,即使缺少某项 capability,组件仍可正常执行,而该 capability 的存在只会启用一些额外或替代行为。

如需让组件框架了解组件需要哪些 capability 以及哪些 capability 对组件而言是可选的,请使用 availability 字段。

use: [
    {
        // It is ok if this protocol is unavailable
        protocol: "fuchsia.examples.Echo1",
        availability: "optional",
    },
    {
        // This protocol MUST be provided for the component to function correctly.
        protocol: "fuchsia.examples.Echo2",
        availability: "required",
    },
]

如果某个组件对某项 capability 使用了 required 声明(默认),但其父级以 optional 的形式提供该 capability,则静态 capability 分析器将生成错误,并且运行时连接尝试始终会失败。

使用可选功能

如果组件的父级使用 availability: "optional" offer 了某项 capability,则该 capability 在运行时可能无法使用。

中的一个条目。无论相应 capability 是否可用,该条目都会存在。任何尝试打开该 capability 的路径的操作都会导致向 Directory.Open() 调用提供的句柄关闭并显示 ZX_ERR_NOT_FOUND 墓志铭。

使用 libc 方法(例如 open()stat())将返回 ENOENT

路由功能

组件只能访问路由到它们的功能。功能可以从组件拓扑中的任何位置发起,只要存在有效的功能路由,即从功能提供方到任何使用方的以下声明链:

  • expose:将 capability 路由到组件的父级。
  • offer:将 capability 路由到组件的某个子项。

如需将 capability 提供程序与请求这些 capability 的组件相关联,请执行以下操作:

  1. 向 capability 提供程序组件添加 offerexpose 声明:

    {
        include: [
            "inspect/client.shard.cml",
            "syslog/client.shard.cml",
        ],
    
        // Information about the program to run.
        program: {
            // Use the built-in ELF runner.
            runner: "elf",
    
            // The binary to run for this component.
            binary: "bin/echo_server_rust",
        },
    
        // Capabilities provided by this component.
        capabilities: [
            { protocol: "fidl.examples.routing.echo.Echo" },
        ],
        expose: [
            {
                protocol: "fidl.examples.routing.echo.Echo",
                from: "self",
            },
        ],
    }
    
  2. 对于组件实例树中的每个中间组件,请添加其他 exposeoffer 声明,直到您找到包含 use 声明的使用方组件:

    {
        // Two children: a server and client.
        children: [
            {
                name: "echo_server",
                url: "#meta/echo_server.cm",
            },
            {
                name: "echo_client",
                url: "#meta/echo_client.cm",
            },
        ],
        offer: [
            // Route Echo protocol from server to client.
            {
                protocol: "fidl.examples.routing.echo.Echo",
                from: "#echo_server",
                to: "#echo_client",
            },
    
            // Route diagnostics protocols to both children.
            {
                protocol: [
                    "fuchsia.inspect.InspectSink",
                    "fuchsia.logger.LogSink",
                ],
                from: "parent",
                to: [
                    "#echo_client",
                    "#echo_server",
                ],
            },
            {
                dictionary: "diagnostics",
                from: "parent",
                to: [
                    "#echo_client",
                    "#echo_server",
                ],
            },
        ],
    }
    

可选依赖项

当某个组件对某项 capability 具有可选依赖项时,则由该组件的父级决定该组件是否会收到该 capability。提供 capability 时,组件可以将 availability 字段设置为 optionalrequiredsame_as_target。每个值都有以下语义:

  • optional:商品的目标必须声明其能够处理缺少此功能的情况,方法是将其 use 声明标记为 optional。如果目标无法执行此操作(即目标对此 capability 的可用性为 required),则路由该 capability 将导致错误。
  • required:目标必须接收此 capability。如果 offer 来源为 parent,并且组件的父级(目标的祖父级)将其作为可选 capability 提供,则路由该 capability 会导致错误,因为父级无法保证该 capability 的可用性。
  • same_as_target:此功能的可用性取决于目标平台的预期。如果目标对此 capability 有可选依赖项,则此 offer 也将是可选的。如果目标对此 capability 有必需的依赖项,则此 offer 也必需。
offer: [
    {
        // child-1 MUST receive the protocol 'fuchsia.logger.LogSink'.
        protocol: "fuchsia.logger.LogSink",
        to: "#child-1",
        from: "#child-2",
        availability: "required",
    },
    {
        // child-1 MUST be able to handle the absence of the protocol
        // 'fuchsia.tracing.provider.Registry'.
        protocol: "fuchsia.tracing.provider.Registry",
        to: "#child-1",
        from: "parent",
        availability: "optional",
    },
    {
        // child-1 decides if it must receive the protocol
        // 'fuchsia.posix.socket.Provider', or if it must be able to support its
        // absence.
        protocol: "fuchsia.posix.socket.Provider",
        to: "#child-1",
        from: "parent",
        availability: "same_as_target",
    },
]

与使用声明一样,您可以省略 availability 字段,在这种情况下,它默认为 required

过渡依赖项

借助组件框架,组件可以平滑过渡到使用和提供可选功能和必需功能。使用过渡性可用性标记后,无论父级是必需的、可选的还是与目标相同,使用该 capability 的组件都不会导致 Scrutiny 验证错误。请注意,虽然此字段的存在是为了实现软过渡,但组件最终应设为可选或必需。

如需使用此功能,子组件将将其可用性标记为“过渡”:

use: [
    {
        // It is ok if this protocol is offered as "required" or "optional"
        protocol: "fuchsia.examples.Echo",
        availability: "transitional",
    },
]

管理子组件

只要组件之间存在有效的 capability 路线,组件就可以在组件拓扑中的任何位置通过 capability 相互交互。还有其他方法可让父级组件与其直接子级互动。

以下示例组件声明了一个名为 lifecycle 的单个静态子项和一个名为 echo 的集合,可以在运行时创建其他子组件:

{
    // ...

    // Statically declared child components
    children: [
        {
            name: "lifecycle",
            url: "lifecycle#meta/default.cm",
        },
    ],

    // Collection to hold dynamically created child components
    collections: [
        {
            name: "echo",
            durability: "transient",
        },
    ],

    // Capabilities required by this component
    use: [
        {
            protocol: "fuchsia.component.Binder",
            from: "#lifecycle",
        },
        {
            protocol: "fuchsia.component.Realm",
            from: "framework",
        },
    ],

    // Capabilities required by child components
    offer: [
        {
            protocol: [ "fuchsia.inspect.InspectSink" ],
            from: "parent",
            to: [
                "#echo",
                "#lifecycle",
            ],
        },
    ],
}

请注意,集合在父级组件的清单中的作用类似于静态子实例,您可以为其命名并为其提供特定功能。集合中的所有子组件都可以访问为其提供的一组功能。

启动子组件

组件框架提供了 fuchsia.component.Binder 协议,供父级组件明确启动可能不公开任何其他功能的子级。由于此功能由框架提供,因此子组件只需从其组件清单中公开此功能即可:

{
    // ...

    // Capabilities exposed from this component to parent.
    expose: [
        {
            // Expose this protocol so that parent component can start it
            // by binding to this capability.
            protocol: "fuchsia.component.Binder",
            from: "framework",
        },
    ],
}

创建动态子项

如需在运行时创建新的子组件,请使用 fuchsia.component.Realm 协议在现有集合中创建组件。使用以下参数调用 CreateChild 方法:

  • CollectionRef:描述将添加组件的集合。
  • Child:组件声明,包括其名称和组件网址。

Rust

use fidl_fuchsia_component::{BinderMarker, CreateChildArgs, RealmMarker};
use fidl_fuchsia_component_decl::{Child, ChildRef, CollectionRef, StartupMode};

// ...

// Use the fuchsia.component.Realm protocol to create a dynamic
// child instance in the collection.
async fn create_dynamic_child() {
    let realm = client::connect_to_protocol::<RealmMarker>()
        .expect("failed to connect to fuchsia.component.Realm");

    let collection_ref = CollectionRef { name: String::from("echo") };
    let child_decl = Child {
        name: Some(String::from("lifecycle_dynamic")),
        url: Some(String::from("echo_server#meta/default.cm")),
        startup: Some(StartupMode::Lazy),
        ..Default::default()
    };

    realm
        .create_child(&collection_ref, &child_decl, CreateChildArgs::default())
        .await
        .expect("create_child failed")
        .expect("failed to create child");
}

C++

#include <fuchsia/component/cpp/fidl.h>
#include <fuchsia/component/decl/cpp/fidl.h>

// ...

// Use the fuchsia.component.Realm protocol to create a dynamic
// child instance in the collection.
void CreateDynamicChild() {
  fuchsia::component::decl::CollectionRef collection_ref = {
      .name = "echo",
  };
  fuchsia::component::decl::Child child_decl;
  child_decl.set_name("lifecycle_dynamic");
  child_decl.set_url("echo_server#meta/default.cm");
  child_decl.set_startup(fuchsia::component::decl::StartupMode::LAZY);

  realm_proxy_->CreateChild(std::move(collection_ref), std::move(child_decl),
                            fuchsia::component::CreateChildArgs(),
                            [&](fuchsia::component::Realm_CreateChild_Result result) {
                              ZX_ASSERT(!result.is_err());
                              FX_LOGS(INFO) << "Dynamic child instance created.";

                              ConnectDynamicChild();
                            });
}

关联到子功能

由于动态组件的父级在构建时未知,因此无法在组件清单中表示的 capability 路线中为其公开的 capability 命名。

如需连接到动态子实例公开的功能,请执行以下操作:

  1. 使用 fuchsia.component.Realm 协议打开子目录的公开目录。使用子组件的名称和集合名称调用 OpenExposedDir 方法:

    Rust

    use fidl_fuchsia_component::{BinderMarker, CreateChildArgs, RealmMarker};
    use fidl_fuchsia_component_decl::{Child, ChildRef, CollectionRef, StartupMode};
    
    // ...
    // Connect to the fidl.examples.routing.echo capability exposed by the child
    // instance, and send a request.
    async fn connect_dynamic_child(message: String) {
        // Open the child's exposed directory
        let exposed_dir = client::open_childs_exposed_directory(
            String::from("lifecycle_dynamic"),
            Some(String::from("echo")),
        )
        .await
        .expect("failed to open exposed directory");
    
        // ...
    }
    

    C++

    #include <fuchsia/component/cpp/fidl.h>
    #include <fuchsia/component/decl/cpp/fidl.h>
    
    // ...
    
    // Use the fuchsia.component.Realm protocol to open the exposed directory of
    // the dynamic child instance.
    void ConnectDynamicChild() {
      fuchsia::component::decl::ChildRef child_ref = {
          .name = "lifecycle_dynamic",
          .collection = "echo",
      };
    
      fidl::InterfaceHandle<fuchsia::io::Directory> exposed_dir;
      realm_proxy_->OpenExposedDir(
          child_ref, exposed_dir.NewRequest(),
          [this, exposed_dir = std::move(exposed_dir)](
              fuchsia::component::Realm_OpenExposedDir_Result result) mutable {
            ZX_ASSERT(!result.is_err());
            std::shared_ptr<sys::ServiceDirectory> svc = std::make_shared<sys::ServiceDirectory>(
                sys::ServiceDirectory(std::move(exposed_dir)));
    
            SendEchoRequest(svc);
          });
    }
    
  2. 使用公开目录句柄作为根目录连接到子项的公开功能:

    Rust

    // Access the fidl.examples.routing.echo capability provided there
    let echo = client::connect_to_protocol_at_dir_root::<EchoMarker>(&exposed_dir)
        .expect("failed to connect to fidl.examples.routing.echo");
    
    let response = echo
        .echo_string(Some(&message))
        .await
        .expect("echo_string failed")
        .expect("echo_string got empty result");
    info!("Server response: {}", response);
    

    C++

    // Connect to the fidl.examples.routing.echo capability exposed by the child's
    // service directory.
    void SendEchoRequest(std::shared_ptr<sys::ServiceDirectory> svc_directory) {
      // Connect to the protocol inside the child's exposed directory
      svc_directory->Connect(echo_proxy_.NewRequest());
    
      // Send a protocol request
      echo_proxy_->EchoString(message_, [&](fidl::StringPtr response) {
        FX_LOGS(INFO) << "Server response: " << response;
        DestroyDynamicChild();
      });
    }
    

销毁动态子项

当不再需要动态子项时,请使用 fuchsia.component.Realm 协议销毁组件实例。使用表示集合中子项的 ChildRef 调用 DestroyChild 方法。

Rust

use fidl_fuchsia_component::{BinderMarker, CreateChildArgs, RealmMarker};
use fidl_fuchsia_component_decl::{Child, ChildRef, CollectionRef, StartupMode};

// ...

// Use the fuchsia.component.Realm protocol to destroy the dynamic
// child instance running in the collection.
async fn destroy_dynamic_child() {
    let realm = client::connect_to_protocol::<RealmMarker>()
        .expect("failed to connect to fuchsia.component.Realm");

    let child_ref = ChildRef {
        name: String::from("lifecycle_dynamic"),
        collection: Some(String::from("echo")),
    };

    realm
        .destroy_child(&child_ref)
        .await
        .expect("destroy_child failed")
        .expect("failed to destroy child");
}

C++

#include <fuchsia/component/cpp/fidl.h>
#include <fuchsia/component/decl/cpp/fidl.h>

// ...

// Use the fuchsia.component.Realm protocol to destroy the dynamic
// child instance running in the collection.
void DestroyDynamicChild() {
  fuchsia::component::decl::ChildRef child_ref = {
      .name = "lifecycle_dynamic",
      .collection = "echo",
  };

  realm_proxy_->DestroyChild(std::move(child_ref),
                             [&](fuchsia::component::Realm_DestroyChild_Result result) {
                               ZX_ASSERT(!result.is_err());
                               FX_LOGS(INFO) << "Dynamic child instance destroyed.";

                               // Terminate the loop
                               loop_->Quit();
                             });
}

这会导致组件停止(如果当前正在运行)。如需在组件中处理此事件,请参阅监听停止事件

控制组件生命周期

组件框架提供了用于修改和与组件生命周期的各个部分交互的功能。

如需详细了解生命周期概念,请参阅组件生命周期

生命周期通知

ELF 运行程序使用 fuchsia.process.lifecycle.Lifecycle 协议通知组件生命周期事件。

如需在子组件中监听停止通知,请执行以下操作:

  1. 在组件清单中订阅生命周期事件

    Rust

    // Information about the program to run.
    program: {
        // Use the built-in ELF runner.
        runner: "elf",
    
        // The binary to run for this component.
        binary: "bin/lifecycle_example_rust",
    
        // Subscribe to component lifecycle events
        lifecycle: { stop_event: "notify" },
    },
    

    C++

    // Information about the program to run.
    program: {
        // Use the built-in ELF runner.
        runner: "elf",
    
        // The binary to run for this component.
        binary: "bin/lifecycle_example_cpp",
    
        // Subscribe to component lifecycle events
        lifecycle: { stop_event: "notify" },
    },
    
  2. 使用运行程序提供的启动句柄注册生命周期处理程序:

    Rust

    use fidl_fuchsia_process_lifecycle::{LifecycleRequest, LifecycleRequestStream};
    
    // ...
    
    #[fuchsia::main(logging_tags = ["lifecycle", "example"])]
    async fn main() {
        // Take the lifecycle handle provided by the runner
        match fuchsia_runtime::take_startup_handle(HandleInfo::new(HandleType::Lifecycle, 0)) {
            Some(lifecycle_handle) => {
                info!("Lifecycle channel received.");
                // Begin listening for lifecycle requests on this channel
                let x: zx::Channel = lifecycle_handle.into();
                let async_x = AsyncChannel::from(fuchsia_async::Channel::from_channel(x));
                let mut req_stream = LifecycleRequestStream::from_channel(async_x);
                info!("Awaiting request to close...");
                if let Some(request) =
                    req_stream.try_next().await.expect("Failure receiving lifecycle FIDL message")
                {
                    match request {
                        LifecycleRequest::Stop { control_handle: c } => {
                            info!("Received request to stop. Shutting down.");
                            c.shutdown();
                            process::exit(0);
                        }
                    }
                }
    
                // We only arrive here if the lifecycle channel closed without
                // first sending the shutdown event, which is unexpected.
                process::abort();
            }
            None => {
                // We did not receive a lifecycle channel, exit abnormally.
                error!("No lifecycle channel received, exiting.");
                process::abort();
            }
        }
    }
    

    C++

    #include <fuchsia/process/lifecycle/cpp/fidl.h>
    
    // ...
    
    // Implementation of the fuchsia.process.lifecycle FIDL protocol
    class LifecycleHandler : public fuchsia::process::lifecycle::Lifecycle {
     public:
      explicit LifecycleHandler(async::Loop* loop) : loop_(loop) {
        // Get the PA_LIFECYCLE handle, and instantiate the channel with it
        zx::channel channel = zx::channel(zx_take_startup_handle(PA_LIFECYCLE));
        // Bind to the channel and start listening for events
        bindings_.AddBinding(
            this, fidl::InterfaceRequest<fuchsia::process::lifecycle::Lifecycle>(std::move(channel)),
            loop_->dispatcher());
        FX_LOGS(INFO) << "Lifecycle channel received.";
      }
    
      // This is the Stop event we must override - see the pure virtual function we
      // need to override at the declaration of fuchsia::process::lifecycle::Lifecycle
      void Stop() override {
        FX_LOGS(INFO) << "Received request to stop. Shutting down.";
        // Shut down our loop - it's important to call Shutdown() here vs. Quit()
        loop_->Shutdown();
        // Close the binding
        bindings_.CloseAll();
      }
    
     private:
      async::Loop* loop_;
      fidl::BindingSet<fuchsia::process::lifecycle::Lifecycle> bindings_;
    };
    

从父级开始

借助组件清单,您可以将子项标记为 eager,这会导致组件框架隐式地通过父项启动该子项。

如果有任何原因(例如缺少组件)导致提前启动的子项无法启动,组件管理器会表现出以下行为:

  • 如果父级不是根组件,则父级将启动,但绑定到它的组件将观察到连接中断(就像任何其他绑定失败一样)。
  • 如果父级是根组件,组件管理器将崩溃,并显示如下错误消息:

    [component_manager] ERROR: protocol `fuchsia.component.CoreBinder` was not available for target `startup`:
        failed to resolve `fuchsia-pkg://fuchsia.com/your_component#meta/your_component.cm`:
        package not found: remote resolver responded with PackageNotFound
        For more, run `ffx component doctor `startup`
    

如果标记为 eager 的组件不存在,并且其祖先(一直到根组件)也标记为 eager,则可能会导致系统崩溃。这一点非常重要,因为许多 build 配置都会创建包含可用组件子集的系统映像。为避免此问题,请使用核心 Realm 分片声明这些组件,以确保可以将它们安全地从测试 build 和产品映像中排除。

eager 组件也应与其父级位于同一软件包集中,因为该组件将与其父级同时启动。通常,eager 组件应位于产品的基础软件包集中。

如需确定您的软件包是否位于基础软件包集中,请运行以下命令:

fx list-packages --verbose my-package

此命令会输出包含匹配软件包的软件包集的列表。例如,system-update-checker 位于 baseuniverse 软件包集中:

$ fx list-packages --verbose system-update-checker
system-update-checker [base universe]

您还可以使用 --base 选项查看基础软件包集中的所有软件包:

fx list-packages --base

终止时重新启动

借助组件清单,您可以使用 on_terminate 控制组件的终止政策。如果组件因成功退出以外的任何原因而终止,设置了“terminate-on-terminate”政策的组件会导致系统优雅重启。

如需启用此功能,请执行以下操作:

  1. 在父级的组件清单中将子项标记为 on_terminate: "reboot"

    // core.cml
    {
        children: [
            ...
            {
                name: "system-update-checker",
                url: "fuchsia-pkg://fuchsia.com/system-update-checker#meta/system-update-checker.cm",
                startup: "eager",
                on_terminate: "reboot",
            },
        ],
    }
    
  2. 将组件的标识符添加到组件管理器的安全政策许可名单 //src/security/policy/component_manager_policy.json5

    // //src/security/policy/component_manager_policy.json5
    {
        security_policy: {
            ...
            child_policy: {
                reboot_on_terminate: [
                    ...
                    "/core/system-update-checker",
                ],
            },
        },
    }
    

错误

信号机制

请求客户端通过对其命名空间或某个 zircon 对象执行 Directory.Open() 请求来启动路由时,会传递该 zircon 对象的服务器端句柄,该句柄会在路由后提供相应功能。错误会导致相应句柄的对象关闭并显示墓志铭。epitaph 载荷始终是 Zircon 状态代码。

由于路由是延迟且异步的,因此此消息可能会在路由操作启动后随时到达。

注意:路由成功后,广告投放组件可以使用其选择的状态代码关闭同一对象。客户端无法区分对象是由组件管理器、投放组件还是之后委托的其他方关闭的。

open() 等类似 libc 的调用使用相同的错误信号机制。

如需查看实用示例,请参阅问题排查部分。

错误状态代码

组件管理器可能会发送以下错误代码来指示路由操作失败:

  • ZX_ERR_NOT_FOUND:由于以下原因之一,无法路由该 capability:
    • 此配置中未提供可选功能。
    • 路由路径中的任一组件存在配置错误。
    • 分发组件与此版本的 Fuchsia 不兼容。
    • 广告投放组件的程序中存在 bug。
    • 完成路由操作所涉及的任何解析器运行程序中的 bug。
  • ZX_ERR_ACCESS_DENIED:由于请求功能的组件无权访问该功能,因此无法路由该功能。例如:
    • 存在相应功能的政策许可名单,但不包含发出请求的组件。
    • 请求组件请求的权限超出了为其提供的权限(即请求对作为只读提供的目录执行读写操作)。
  • ZX_ERR_TIMED_OUT:路由步骤之一超时。
  • ZX_ERR_INTERNAL:组件管理器本身遇到了意外错误,表明平台存在 bug。

只要平台上的软件没有更新,相同功能就会重现 NOT_FOUNDACCESS_DENIEDINTERNAL 错误。软件更新(即使是单个组件的更新)可能会更改 capability 的路由,并可能会影响该 capability 的可用性。

路由错误语义原则

  • 最小化:由于错误信号路径由组件管理器和服务组件共享,因此组件管理器会将大部分错误空间留给服务组件使用。
  • 客户端视角:虽然路由依赖于许多单独的子操作,而这些操作都可能会因各种原因(包括其他组件作者的错误)而失败,但错误语义会根据发出请求的客户端和其需求进行量身定制。例如,中间组件作者的用户错误仍会为请求客户端返回 NOT_FOUND

问题排查

本部分介绍了您在尝试 use 和连接到组件功能时可能遇到的常见问题,并提供了建议的解决方案。

功能路由失败

当您的组件尝试访问某项功能时,组件管理器会执行capability routing 来查找给定 capability 的来源。如果路由路径中的某个组件清单配置不正确,路由可能会失败。例如,路径中的某个组件缺少 offerexpose 声明,或者链中的某个组件无法解析。

请执行以下操作,检查是否是路由失败导致了渠道关闭:

  • 使用 ffx component route 检查组件的路由。此 ID 可与组件的标识名或组件的网址搭配使用。例如:

    # check with the moniker
    ffx component route /core/ffx-laboratory:echo_realm/echo_client
    
    # check with a partial URL
    ffx component route echo_client.cm
    
  • 使用 ffx log 检查组件日志,查找以 Failed to route 开头的消息,其中说明了路由链失败的位置。例如:

    [echo_client][][W] protocol `fidl.examples.routing.echo.Echo` was not available for target `/core/ffx-laboratory:echo_realm/echo_client`:
        `fidl.examples.routing.echo.Echo` was not offered to `/core/ffx-laboratory:echo_realm/echo_client` by parent
        For more, run `ffx component doctor /core/ffx-laboratory:echo_realm/echo_client`
    
  • 检查已关闭的频道上的墓志铭。为最常见的路由故障设置的 epitaph 是 ZX_ERR_NOT_FOUND

    [echo_client][][I] Connecting to Echo protocol failed with error
    "A FIDL client's channel to the service fidl.examples.routing.echo.Echo was closed: NOT_FOUND"
    

    如需了解详情,请参阅路由错误

如需查看失败的 capability 路由的自包含示例,请参阅 //examples/components/routing_failed

收到路由错误

当 capability 路由失败时,底层 FIDL 通道会关闭。如果通道已关闭,FIDL 协议绑定会返回错误状态。请考虑以下示例:

Rust

let echo = connect_to_protocol::<EchoMarker>()
    .context("Failed to connect to echo service")?;
let res = echo.echo_string(Some("Hippos rule!")).await;
match res {
    Ok(_) => { info!("Call succeeded!"); }
    Err(fidl::Error::ClientChannelClosed { status, service_name } => { 
        error!("Channel to service {} was closed with status: {}", service_name, status); 
    } 
    Err(e) => {
        error!("Unexpected error: {}", e);
    }
};

C++

fuchsia::examples::EchoPtr echo_proxy;
auto context = sys::ComponentContext::Create();
context->svc()->Connect(echo_proxy.NewRequest());

// Sets an error handler that will be called if an error causes the underlying 
// channel to be closed. 
echo_proxy.set_error_handler([&loop](zx_status_t status) { 
  printf("Channel was closed with status: %d\n", status); 
  // ... 
}); 

echo_proxy->EchoString("Hippos rule!", [&](std::string response) {
  // ...
});

如需确定频道关闭的根本原因,您可以检查频道上设置的可选墓志铭。如需检索已关闭渠道的墓志铭,请执行以下操作:

Rust

let stream = echo.take_event_stream();
match stream.next().await {
    Some(Err(fidl::Error::ClientChannelClosed { status, .. })) => {
        info!("Channel was closed with epitaph: {}", status);
    }
    Some(m) => {
        info!("Received message other than epitaph or peer closed: {:?}", m);
    }
    None => {
        info!("Component failed to start or channel was closed by server");
    }
}

C++

echo_proxy.set_error_handler([&loop](zx_status_t status) {
  // If an Epitaph was present on the channel, its error value will be passed as
  // the parameter.
  printf("Channel was closed with epitaph: %d\n", status);
});

组件未能启动

如果功能路由成功,但在解析或启动组件时出现问题,您可能会遇到错误。错误消息的形式取决于组件运行程序

  • 对于 ELF 运行程序,请使用 ffx log --filter component_manager 检查组件管理器日志。查找以 Failed to start component 开头的消息。例如:

    [component_manager] WARN: Failed to start component
    `fuchsia-pkg://fuchsia.com/components-routing-failed-example#meta/echo_server_bad.cm`:
    unable to load component with url "fuchsia-pkg://fuchsia.com/components-routing-failed-example#meta/echo_server_bad.cm":
    error loading executable: "reading object at \"bin/routing_failed_echo_server_oops\" failed:
    A FIDL client's channel to the service fuchsia.io.File was closed: PEER_CLOSED"
    
  • 对于其他运行程序,请查看运行程序组件的日志。为此,请运行以下命令:

    ffx log --tags runner-name

如需解决此问题,请验证以下各项:

  • 组件清单中的 program 声明已正确配置。例如,验证二进制文件的路径拼写是否正确。
  • 二进制文件本身以及启动组件所需的所有其他资源都包含在软件包中。

如需查看因组件清单配置错误而无法启动的组件的示例,请参阅 //examples/components/routing_failed

组件终止或关闭了频道

如果您已验证路由成功且组件已成功启动,则可能遇到了来源组件关闭了通道本身的问题。这可能会在组件运行时发生,也可能是组件终止的副作用。

如果组件因崩溃而终止,您可以在 ffx log 中查找包含该转储中组件名称的崩溃报告:

[33860.645][klog][klog][I] crashsvc: exception received, processing
[33860.645][klog][klog][I] <== fatal : process echo_client.cm[21090] thread initial-thread[21092]
<stack trace follows...>

如果来源组件自行关闭了通道,请参考以下提示进一步排查原因:

  • 如需查看错误消息,请参阅源组件的日志
  • 使用 ffx debug fidl 通过 fidlcat 检查 FIDL 连接流量,以查看是否存在错误或意外行为。