连接组件

本文档演示了如何使用功能和附加工具将父组件及其子组件连接在一起,以便父组件管理其子组件。

概念

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

  • 组件框架组装了命名空间对于使用组件声明描述的是能力该组件需要满足以下条件才能正常工作。组件向其他方公开的功能会组装到公开目录中。
  • 每个组件都会收到一个指向 Directory 通道服务器端的句柄,称为出站目录。组件的程序会通过此目录公开其提供的任何功能。
  • 在运行时,组件实例树描述了组件实例之间的父子关系。组件实例树和该树上的功能路由统称为组件拓扑
  • 父组件可以在其 组件清单 中静态声明子组件,也可以使用 组件集合 动态声明子组件。集合是动态子项的容器,这些子项可在运行时使用 fuchsia.component.Realm 框架协议创建和销毁。

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

通过路由连接功能

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

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

  • 组件的程序打开了其 命名空间中的路径。 <0x0
  • 一个组件的程序在另一个组件的公开目录中打开了一个路径。 <0
  • 开发者调用 ffx component route
  • 启动依赖于解析器或运行器的组件,但该解析器或运行器的后备解析器或运行器功能尚未路由。

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

提供功能实现

提供某种功能的组件必须使用 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::Server<fidl_examples_routing_echo::Echo> {
 public:
  void EchoString(EchoStringRequest& request, EchoStringCompleter::Sync& completer) override {
    completer.Reply({{.response = request.value()}});
  }
};

int main(int argc, const char** argv) {
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

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

  component::OutgoingDirectory outgoing_directory = component::OutgoingDirectory(dispatcher);
  zx::result result = outgoing_directory.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  result = outgoing_directory.AddUnmanagedProtocol<fidl_examples_routing_echo::Echo>(
      [dispatcher](fidl::ServerEnd<fidl_examples_routing_echo::Echo> server_end) {
        fidl::BindServer(dispatcher, std::move(server_end), std::make_unique<EchoImplementation>());
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Echo protocol: " << result.status_string();
    return -1;
  }

  // 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");
        log::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
  zx::result client_end = component::Connect<fidl_examples_routing_echo::Echo>();
  if (client_end.is_error()) {
    FX_LOGS(ERROR) << "Failed to connect to Echo protocol: " << client_end.status_string();
    return EXIT_FAILURE;
  }
  fidl::SyncClient client(std::move(client_end.value()));

  // Send messages over FIDL interface for each argument
  fidl::StringPtr response = nullptr;
  for (int i = 1; i < argc; i++) {
    fidl::Result response = client->EchoString({argv[i]});

    if (response.is_error()) {
      FX_LOGS(ERROR) << "echo_string failed: " << response.error_value();
      return EXIT_FAILURE;
    }
    if (!response->response().has_value()) {
      FX_LOGS(ERROR) << "echo_string got empty result";
      return EXIT_FAILURE;
    }
    const std::string& response_value = response->response().value();
    FX_LOG_KV(INFO, "Server response", FX_KV("response", response_value));
  }

  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 形式提供该功能,则静态功能分析器将生成错误,并且运行时连接尝试将始终失败。

使用可选功能

如果组件的父级已 offer 具有 availability: "optional" 的功能,则该功能可能在运行时无法使用。

无论功能是否可用,组件的任何尝试打开相应功能的路径都会导致提供给 Directory.Open() 调用的句柄被关闭,并带有 ZX_ERR_NOT_FOUND 墓志铭。

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

路由功能

组件只能访问路由到它们的功能。只要存在有效的功能路由(即从功能提供方到任何消费者的以下声明链),功能就可以源自组件拓扑中的任何位置:

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

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

  1. 向功能提供方组件添加 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.
            {
                dictionary: "diagnostics",
                from: "parent",
                to: [
                    "#echo_client",
                    "#echo_server",
                ],
            },
        ],
    }
    

可选依赖项

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

  • optional:优惠的目标必须通过将其 use 声明标记为 optional 来声明其能够处理缺少此功能的情况。如果目标无法执行此操作(即目标对此功能的支持情况为 required),则路由该功能会导致错误。
  • required:目标必须接收此功能。如果提供方来源为 parent,并且组件的父级(目标的祖父级)将此功能作为可选功能提供,那么路由该功能将导致错误,因为父级无法保证该功能的可用性。
  • 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",
    },
]

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

过渡性依赖项

组件框架允许组件软过渡到使用和提供可选功能和必需功能。使用过渡可用性标记后,使用功能的组件不会导致 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: [
        {
            dictionary: "diagnostics",
            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. 使用公开的目录句柄作为根,连接到子进程的公开功能:

    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 Runner 使用 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. 使用 runner 提供的启动句柄注册生命周期处理程序:

    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 控制组件的终止政策。如果设置了“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. 将组件的 moniker 添加到组件管理器的安全政策许可名单中(位于 //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() 请求来启动路由时,它会传递 Zircon 对象的服务器端句柄,该句柄在路由完成后提供相应功能。如果出现错误,相应句柄的对象将关闭并显示墓志铭。墓志铭载荷始终是 Zircon 状态代码。

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

注意:一旦路由成功,服务组件可以使用所选的状态代码关闭同一对象。客户端无法辨别对象是由组件管理器、服务组件还是随后委托的另一方关闭的。

对于 libc 类调用(例如 open()),系统会使用相同的错误信号发送机制。

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

错误状态代码

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

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

只要平台上的软件未更新,NOT_FOUNDACCESS_DENIEDINTERNAL 错误就会针对同一功能重现。即使是单个组件的软件更新,也可能会更改 capability 的路由,并可能会影响该 capability 的可用性。

路由错误语义原则

  • 极简性:由于错误信号传递路径在组件管理器和服务组件之间共享,因此组件管理器将大部分错误空间留给服务组件使用。
  • 客户端视角:虽然路由取决于许多单独的子操作,而每个子操作都可能因各种原因(包括其他组件作者的错误)而失败,但错误语义是根据请求客户端和请求客户端的需求量身定制的。例如,即使中间组件作者出现用户错误,请求客户端仍会返回 NOT_FOUND

问题排查

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

功能路由失败

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

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

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

    # 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 component capability 可查找使用或公开特定功能的全部组件。这有助于您验证组件拓扑中是否提供了某项功能。

    ffx component capability fuchsia.example.Echo
  • 使用 ffx component explore 在组件的命名空间内打开交互式 shell。您可以列出 /svc(或其他已路由的目录)的内容,以查看该功能是否确实在命名空间中可用。

    在交互式 shell 中,您可以检查相应功能:

    ls /svc
  • 使用 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`
    
  • 查看已关闭频道上是否有墓志铭。为最常见的路由失败设置的墓志铭是 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

从路由接收错误

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

组件未能启动

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

  • 对于 ELF Runner,请使用 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"
    
  • 对于其他 runner,请检查 runner 组件的日志。为此,您可以运行以下命令:

    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 连接流量是否存在错误或意外行为。