blob: 4fd848e65b07a23d702a7438d70c76e29cb3834a [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/ui/scenic/lib/utils/object_linker.h"
#include <lib/async/cpp/task.h>
#include <lib/async/cpp/wait.h>
#include <lib/async/default.h>
#include <lib/syslog/cpp/macros.h>
#include <algorithm>
#include <functional>
#include "src/lib/fsl/handles/object_info.h"
namespace utils {
void ObjectLinkerBase::Link::LinkInvalidatedLocked(bool on_destruction) {
if (link_invalidated_) {
// link_invalidated_ will be reset to nullptr; this way we can avoid
// assignment operators to |link_validated_|.
auto link_invalidated = std::move(link_invalidated_);
link_invalidated_ = [](auto) {};
link_invalidated(on_destruction);
}
}
void ObjectLinkerBase::Link::LinkUnresolvedLocked() {
if (link_invalidated_) {
// It's janky that we need to copy the closure before invoking it, but if we don't then there is
// a crash when the mutable closure from `flatland::LinkSystem::CreateLinkToChild/Parent()` is
// run a second time. For unknown reasons, this causes the `ChildView/ParentViewportWatcher` to
// be destroyed on the wrong dispatcher (as evidenced by a CHECK in the watcher's destructor).
auto link_invalidated = link_invalidated_;
link_invalidated(false);
}
}
size_t ObjectLinkerBase::UnresolvedExportCount() {
auto access = GetScopedAccess();
return std::count_if(exports_.begin(), exports_.end(),
[](const auto& iter) { return iter.second.IsUnresolved(); });
}
size_t ObjectLinkerBase::UnresolvedImportCount() {
auto access = GetScopedAccess();
return std::count_if(imports_.begin(), imports_.end(),
[](const auto& iter) { return iter.second.IsUnresolved(); });
}
zx_koid_t ObjectLinkerBase::CreateEndpoint(zx::handle token, ErrorReporter* error_reporter,
bool is_import) {
// Select imports or exports to operate on based on the flag.
auto& endpoints = is_import ? imports_ : exports_;
auto access = GetScopedAccess();
if (!token) {
error_reporter->ERROR() << "Token is invalid";
return ZX_KOID_INVALID;
}
zx_koid_t endpoint_id;
zx_koid_t peer_endpoint_id;
std::tie(endpoint_id, peer_endpoint_id) = fsl::GetKoids(token.get());
if (endpoint_id == ZX_KOID_INVALID || peer_endpoint_id == ZX_KOID_INVALID) {
error_reporter->ERROR() << "Token with ID " << token.get() << " refers to invalid objects";
return ZX_KOID_INVALID;
}
auto endpoint_iter = endpoints.find(endpoint_id);
if (endpoint_iter != endpoints.end()) {
error_reporter->ERROR() << "Endpoint with id " << endpoint_id
<< " is already in use by this ObjectLinker";
return ZX_KOID_INVALID;
}
// Create a new endpoint in an unresolved state. Full linking cannot occur
// until Initialize() is called on the endpoint to provide a link object and
// handler callbacks.
auto raw_token = token.get(); // Read handle before move()-ing into Endpoint.
auto dispatcher = async_get_default_dispatcher();
auto emplaced_endpoint = endpoints.emplace(
endpoint_id, Endpoint(peer_endpoint_id, std::move(token), dispatcher,
WaitForPeerDeath(dispatcher, raw_token, endpoint_id, is_import)));
FX_DCHECK(emplaced_endpoint.second);
return endpoint_id;
}
void ObjectLinkerBase::DestroyEndpoint(zx_koid_t endpoint_id, bool is_import, bool destroy_peer) {
auto& endpoints = is_import ? imports_ : exports_;
auto& peer_endpoints = is_import ? exports_ : imports_;
auto access = GetScopedAccess();
auto endpoint_iter = endpoints.find(endpoint_id);
if (endpoint_iter == endpoints.end()) {
FX_LOGS(ERROR) << "Attempted to remove an unknown endpoint " << endpoint_id
<< " from ObjectLinker";
return;
}
// If the object has a peer linked tell it about the object being removed,
// which will immediately invalidate the peer.
if (destroy_peer) {
zx_koid_t peer_endpoint_id = endpoint_iter->second.peer_endpoint_id;
auto peer_endpoint_iter = peer_endpoints.find(peer_endpoint_id);
if (peer_endpoint_iter != peer_endpoints.end()) {
Endpoint& peer_endpoint = peer_endpoint_iter->second;
// Invalidate the peer endpoint. If Initialize() has already been called on
// the peer endpoint, then close its connection which will destroy it.
// Otherwise, any future connection attempts will fail immediately with a
// link_failed callback, due to peer_endpoint_id being marked as
// invalid.
//
// This handles the case where the peer exists but Initialize() has not been
// called on it yet (so no callbacks exist).
peer_endpoint.peer_endpoint_id = ZX_KOID_INVALID;
if (peer_endpoint.link) {
if (peer_endpoint.peer_death_waiter) {
// `peer_endpoint` is still waiting for a link to `endpoint`. This is a problem,
// because, when `endpoint` is destroyed:
// 1. `endpoint.token` will be destroyed
// 2. `endpoint.token` is a `zx::handle` to the channel that
// `peer_endpoint.peer_death_waiter`
// uses to detect the disappearance of `endpoint`.
// 3. The kernel will then send a `PEER_CLOSED` signal to the peer's end of the channel,
// causing `peer_endpoint.peer_death_waiter` to execute.
// 4. There is a race between the execution of `peer_endpoint.peer_death_waiter`, and the
// call to `Invalidate()` below. (The latter will free the memory that the former
// references.)
FX_LOGS(ERROR)
<< "DestroyEndpoint() with |peer_endpoint.link && peer_endpoint.peer_death_waiter|";
}
peer_endpoint.link->Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
} else {
// It is safe to skip Invalidate. The peer_endpoint is in the map but its .link field is
// nullptr, which means the peer link had called CreateEndpoint but did not reach
// Initialize. Also, the .peer_endpoint_id field is set to ZX_KOID_INVALID just above, which
// lets a belated call to Initialize to avoid a CHECK fail.
}
}
}
// At this point it is safe to completely erase the endpoint for the object.
endpoints.erase(endpoint_iter);
}
void ObjectLinkerBase::InitializeEndpoint(ObjectLinkerBase::Link* link, zx_koid_t endpoint_id,
bool is_import) {
FX_DCHECK(link);
auto& endpoints = is_import ? imports_ : exports_;
auto access = GetScopedAccess();
// Update the endpoint with the connection information.
auto endpoint_iter = endpoints.find(endpoint_id);
FX_DCHECK(endpoint_iter != endpoints.end());
Endpoint& endpoint = endpoint_iter->second;
// If the endpoint is no longer valid (i.e. its peer no longer exists), then
// immediately signal a disconnection (which will destroy the endpoint)
// instead of linking.
//
// This edge-case happens if the endpoint's peer is destroyed after the
// endpoint is created, but before Initialize() is called on it.
zx_koid_t peer_endpoint_id = endpoint.peer_endpoint_id;
if (peer_endpoint_id == ZX_KOID_INVALID) {
link->Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
return;
}
if (!endpoint.link) {
endpoint.link = link;
// Attempt to locate and link with the endpoint's peer.
AttemptLinking(endpoint_id, peer_endpoint_id, is_import);
} else {
endpoint.link = link;
}
}
void ObjectLinkerBase::AttemptLinking(zx_koid_t endpoint_id, zx_koid_t peer_endpoint_id,
bool is_import) {
auto& endpoints = is_import ? imports_ : exports_;
auto& peer_endpoints = is_import ? exports_ : imports_;
auto access = GetScopedAccess();
auto endpoint_iter = endpoints.find(endpoint_id);
FX_DCHECK(endpoint_iter != endpoints.end());
auto peer_endpoint_iter = peer_endpoints.find(peer_endpoint_id);
if (peer_endpoint_iter == peer_endpoints.end()) {
return; // Peer endpoint hasn't even been created yet, bail.
}
Endpoint& endpoint = endpoint_iter->second;
Endpoint& peer_endpoint = peer_endpoint_iter->second;
if (!peer_endpoint.link) {
return; // Peer endpoint isn't connected yet, bail.
}
// Delete the peer waiters now that the endpoints are resolved.
endpoint.peer_death_waiter = nullptr;
peer_endpoint.peer_death_waiter = nullptr;
// Do linking last, so clients see a consistent view of the Linker.
// Always fire the callback for the Export first, so clients can rely on
// callbacks firing in a certain order.
if (is_import) {
peer_endpoint.link->LinkResolved(endpoint.link);
endpoint.link->LinkResolved(peer_endpoint.link);
} else {
endpoint.link->LinkResolved(peer_endpoint.link);
peer_endpoint.link->LinkResolved(endpoint.link);
}
}
std::unique_ptr<async::Wait> ObjectLinkerBase::WaitForPeerDeath(async_dispatcher_t* dispatcher,
zx_handle_t endpoint_handle,
zx_koid_t endpoint_id,
bool is_import) {
auto access = GetScopedAccess();
// Each endpoint must be removed from being considered for linking if its
// peer's handle is closed before the two entries are successfully linked.
// This communication happens via the link_failed callback.
//
// Once linking has occurred, this communication happens via UnregisterExport
// or UnregisterImport and the peer_destroyed callback.
// TODO(https://fxbug.dev/42098366): Follow up on __ZX_OBJECT_PEER_CLOSED with Zircon.
static_assert(ZX_CHANNEL_PEER_CLOSED == __ZX_OBJECT_PEER_CLOSED, "enum mismatch");
static_assert(ZX_EVENTPAIR_PEER_CLOSED == __ZX_OBJECT_PEER_CLOSED, "enum mismatch");
static_assert(ZX_FIFO_PEER_CLOSED == __ZX_OBJECT_PEER_CLOSED, "enum mismatch");
static_assert(ZX_SOCKET_PEER_CLOSED == __ZX_OBJECT_PEER_CLOSED, "enum mismatch");
auto waiter = std::make_unique<async::Wait>(
endpoint_handle, __ZX_OBJECT_PEER_CLOSED, 0,
[weak_this = weak_ptr_factory_.GetWeakPtr(), endpoint_id, is_import](
async_dispatcher_t*, async::Wait*, zx_status_t status, const zx_packet_signal_t*) {
if (status == ZX_ERR_CANCELED) {
// (1) Can happen if this callback's dispatcher is shutting down.
// GFX runs these callbacks on the main (render) thread, and Flatland runs it on the
// instance thread, so it is okay to exit early.
// (2) Can happen if the async::Wait object was itself destroyed as part of
// DestroyEndpoint. That path invalidates the peer eagerly, so it is okay
// to exit early.
return;
}
// If `this` has been destroyed, there isn't really anything we can do here, so skip
// entirely.
if (!weak_this.get()) {
FX_LOGS(ERROR) << "ObjectLinkerBase destroyed; skipping Endpoint cleanup";
return;
}
auto access = weak_this->GetScopedAccess();
auto& endpoints = is_import ? weak_this->imports_ : weak_this->exports_;
auto endpoint_iter = endpoints.find(endpoint_id);
if (endpoint_iter == endpoints.end()) {
// Can happen if this callback executes after the async::Wait object
// gets destroyed on another (Flatland) thread, and the cancel was lost.
return;
}
Endpoint& endpoint = endpoint_iter->second;
// Invalidate the endpoint. If Initialize() has
// already been called on the endpoint, then close
// its connection (which will cause it to be
// destroyed). Any future connection attempts will
// fail immediately with a link_failed call, due to
// peer_endpoint_id being marked as invalid.
endpoint.peer_endpoint_id = ZX_KOID_INVALID;
if (endpoint.link) {
endpoint.link->Invalidate(/*on_destruction=*/false, /*invalidate_peer=*/true);
}
});
zx_status_t status = waiter->Begin(dispatcher);
FX_DCHECK(status == ZX_OK);
return waiter;
}
zx::handle ObjectLinkerBase::ReleaseToken(zx_koid_t endpoint_id, bool is_import) {
auto& endpoints = is_import ? imports_ : exports_;
auto& peer_endpoints = is_import ? exports_ : imports_;
auto access = GetScopedAccess();
// If the endpoint was resolved, it will still be invalidated, but the peer endpoint must be
// unresolved first if it exists.
auto endpoint_iter = endpoints.find(endpoint_id);
FX_DCHECK(endpoint_iter != endpoints.end());
zx_koid_t peer_endpoint_id = endpoint_iter->second.peer_endpoint_id;
auto peer_endpoint_iter = peer_endpoints.find(peer_endpoint_id);
if (peer_endpoint_iter == peer_endpoints.end()) {
return std::move(endpoint_iter->second.token);
}
// Signal that the link is now unresolved, then re-create the peer death waiter to flag the
// endpoint as unresolved.
auto& peer_endpoint = peer_endpoint_iter->second;
if (peer_endpoint.link) {
peer_endpoint.link->LinkUnresolvedLocked();
}
peer_endpoint.peer_death_waiter = WaitForPeerDeath(
peer_endpoint.dispatcher, peer_endpoint.token.get(), peer_endpoint_id, !is_import);
return std::move(endpoint_iter->second.token);
}
} // namespace utils