连接组件

本文档演示了如何使用父组件管理其子组件的 capability 和其他工具将组件连接在一起。

概念

在继续学习本指南之前,您应该了解以下概念:

  • capabilities
  • Directory该组件的程序能让用户发现它通过此目录提供的任何功能。
  • 5
  • 集合是用于存储动态子项的容器,可使用 fuchsia.component.Realm 框架协议在运行时创建和销毁这些子项。

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

通过路由连接功能

组件通过功能相互交互。由一个组件提供的功能需要在该组件的清单中声明,并且为了让它们可供其他组件使用,请通过 exposeoffer 声明路由到父级/子级组件。使用该功能的其他组件也需要在其清单中声明它们的使用。为了让其程序在运行时使用该 capability,必须将所使用的 capability 路由到该组件(通过父级提供或由子级公开)。

功能路由是指由组件管理器执行的递归过程,该过程通过按照清单文件描述的各个路由步骤识别服务组件。在以下情况下,会启动功能路由:

  • 开发者调用 ffx component route
  • 启动依赖于尚未路由其后备解析器或运行程序功能的解析器或运行程序的组件。

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

提供 capability 实现

提供相应功能的组件必须使用 capabilities 声明在其组件清单中声明该功能。

请参阅以下示例,该示例在提供组件的清单中声明了 FIDL 协议功能

{
    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 协议在其传出目录中提供相应功能,从而实现了该功能。生成的 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::SetTags({"echo_client"});

  // 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_SLOG(INFO, "echo_string got empty result");
    } else {
      FX_SLOG(INFO, "Server response", FX_KV("response", response->c_str()));
    }
  }

  return 0;
}

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

将一些已使用的功能标记为可选

并非组件使用的所有功能都是成功运行所必需的。有时,如果某项功能缺失,组件仍能顺利执行,其存在只是启用一些其他或替代的行为。

如需使组件框架了解某个组件需要哪些功能,以及哪些功能对组件来说是可选功能,请使用 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",
    },
]

如果某个组件对某项功能具有 required 使用声明(默认),但其父级提供该功能作为 optional,则静态功能分析器将生成错误,并且在运行时尝试连接将始终失败。

使用可选功能

如果组件的父级已通过 availability: "optional" 对某项功能执行 offer 操作,则该功能在运行时可能无法使用。

如果尝试打开该 capability 的路径,则会导致向 Directory.Open() 调用提供的句柄以 ZX_ERR_NOT_FOUND 首字母缩写词关闭。

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

路由功能

组件只能使用传送到它们的功能。只要有效的功能路由以从 capability 提供程序到任何使用方的以下声明链的形式存在,相应功能便可来自组件拓扑中的任何位置:

  • expose:将功能向上路由到组件的父级。
  • offer:将功能向下路由到组件的一个子级。

如需将 capability 提供程序与请求这些功能的组件连接起来,请执行以下操作:

  1. offerexpose 声明添加到 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",
            },
        ],
    }
    
  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",
                ],
            },
        ],
    }
    

可选依赖项

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

  • optional:优惠的目标必须通过将它的 use 声明标记为 optional 来声明它能够处理没有此功能的情况。如果目标无法执行此操作(即目标对于此功能具有 required 可用性),则路由该 capability 会导致错误。
  • required:目标必须接收此功能。如果优惠来源为 parent,并且组件的父级(目标的祖父级)将此功能作为可选 capability 提供,则路由该 capability 将导致错误,因为父级无法保证该 capability 的可用性。
  • same_as_target:此功能的可用性取决于目标的预期。如果目标对此功能有可选依赖项,则此优惠也是可选的。如果目标必须依赖此功能,则也必须提供此优惠。
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",
    },
]

与 use 声明一样,availability 字段可以省略,在这种情况下,该字段默认为 required

过渡依赖项

组件框架允许组件软转换为使用并提供可选和必需的功能。使用过渡可用性标记时,无论父级是必需、可选还是与目标相同,使用某项功能的组件都不会导致审核验证错误。请注意,虽然此字段是为了启用软过渡,但组件最终应决定是可选还是必需。

为了使用此功能,子组件会将其可用性标记为“transitional”:

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

管理子组件

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

以下示例组件声明了一个名为 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",
                "fuchsia.logger.LogSink",
            ],
            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();
                            });
}

关联子功能

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

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

  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. 使用公开的目录句柄作为根目录连接到子项公开的 capability:

    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: fuchsia_zircon::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: Required protocol `fuchsia.component.CoreBinder` was not
    available for target component `/startup`:
    failed to resolve "fuchsia-pkg://fuchsia.com/your_component#meta/your_component.cm":
    package not found: remote resolver responded with PackageNotFound
    

如果标记为 eager 的组件不存在,可能会导致系统崩溃,前提是这些组件的祖先也被标记为 eager,直到再到根组件。这一点非常重要,因为许多 build 配置都会创建包含部分可用组件的系统映像。为避免此问题,请使用核心领域分片声明这些组件,以确保可以安全地将其从测试 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 控制组件的终止政策。设置了“reboot-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",
                ],
            },
        },
    }
    

错误

信号机制

Directory.Open()错误将导致该句柄的对象以书名关闭。epitaph 载荷始终是 Zircon 状态代码。

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

注意:路由成功后,服务组件还可以使用其选择的状态代码关闭同一对象。客户端无法辨别该对象是被组件管理器、传送组件关闭,还是被委托给其他方关闭。

类似的错误信号机制也用于类似 libc 的调用,例如 open()

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

错误状态代码

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

  • ZX_ERR_NOT_FOUND:由于以下某个原因,无法路由功能:
    • 此配置中未提供可选功能。
    • 路线路径沿途的任何一个组成部分存在配置错误。
    • 服务组件与此版本的 Fuchsia 不兼容。
    • 服务组件的程序错误。
    • 与完成路由操作相关的任何解析器运行程序中的 bug。
  • ZX_ERR_ACCESS_DENIED:无法路由该功能,因为发出请求的组件无权访问该功能。例如:
    • 相应功能存在政策允许列表,但不包含发出请求的组件。
    • 请求组件请求的权限大于提供给它的权限(即请求对以只读方式提供的目录进行读/写)。
  • ZX_ERR_TIMED_OUT:某个路由步骤超时。
  • ZX_ERR_INTERNAL:组件管理器本身遇到了意外错误,表示平台存在 bug。

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

路由错误语义的原则

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

问题排查

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

当功能路由失败时,底层 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) {
  // ...
});

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

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

功能路由失败

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

请执行以下操作,检查路由故障是否是通道关闭的原因:

  • 使用 ffx log 检查组件日志,查找以 Failed to route 开头的消息(说明路由链的失败位置)。例如:

    [echo_client][][W] Required protocol
    `fidl.examples.routing.echo.Echo` was not available for target component
    `/core/ffx-laboratory:echo_realm/echo_client`:
    A `use from parent` declaration was found at `/core/ffx-laboratory:echo_realm/echo_client`
    for `fidl.examples.routing.echo.Echo`, but no matching `offer` declaration was found in the parent
    
  • 检查已关闭的频道上是否有墓碑。 最常见路由故障的首字母缩写为 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"
    

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

如需查看失败的功能路由的独立示例,请参阅 //examples/components/routing_failed

组件未能启动

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

  • 对于 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 (anonymous) 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 连接流量,看看是否存在错误或意外行为。