blob: f47ccb1e2d0a5b7b01553acd327ce03869d24d08 [file] [log] [blame]
// Copyright 2019 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 <lib/sysmem-connector/sysmem-connector.h>
#include <fbl/auto_lock.h>
#include <fbl/function.h>
#include <fbl/mutex.h>
#include <fbl/unique_fd.h>
#include <fuchsia/sysmem/c/fidl.h>
#include <lib/async/cpp/task.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/fdio/watcher.h>
#include <lib/fdio/fd.h>
#include <lib/fdio/fdio.h>
#include <lib/fdio/directory.h>
#include <lib/fzl/fdio.h>
#include <lib/zx/channel.h>
#include <fcntl.h>
#include <errno.h>
#include <queue>
#include <threads.h>
// The actual sysmem FIDL server is in the sysmem driver. The code that watches
// for the driver and sends sysmem service requests to the driver is in
// ulib/sysmem-connector. The code here just needs to queue requests to
// sysmem-connector.
// Clients interact with sysmem-connector using a C ABI, but we want to use C++
// for the implementation. We have SysmemConnector inherit from an empty
// sysmem_connector struct just to make the SysmemConnector class officially be
// a class not a struct, and to make the SysmemConnector name consistent with
// C++ coding conventions, and have member names with "_" at the end consistent
// with coding conventions, etc.
//
// Every instance of sysmem_connector is actually a SysmemConnector and vice
// versa.
struct sysmem_connector {
// Intentionally declared as empty; never instantiated; see SysmemConnector.
};
class SysmemConnector : public sysmem_connector {
// public in this case just means public to this file. The interface is via
// the functions declared in lib/sysmem-connector/sysmem-connector.h.
public:
SysmemConnector(const char* sysmem_device_path);
zx_status_t Start();
void QueueRequest(zx::channel allocator_request);
void Stop();
private:
void Post(fbl::Closure to_run);
static zx_status_t DeviceAddedShim(int dirfd, int event, const char* fn, void* cookie);
zx_status_t DeviceAdded(int dirfd, int event, const char* fn);
zx_status_t ConnectToSysmemDriver();
void ProcessQueue();
//
// Set once during construction + Start(), never set again.
//
const char* sysmem_device_path_{};
async::Loop process_queue_loop_;
thrd_t process_queue_thrd_{};
//
// Only touched from process_queue_loop_'s one thread.
//
fbl::unique_fd sysmem_dir_fd_;
zx::channel driver_connector_client_;
//
// Synchronized using lock_.
//
fbl::Mutex lock_;
std::queue<zx::channel> connection_requests_ __TA_GUARDED(lock_);
};
SysmemConnector::SysmemConnector(const char* sysmem_device_path)
: sysmem_device_path_(sysmem_device_path),
process_queue_loop_(&kAsyncLoopConfigNoAttachToThread) {
ZX_DEBUG_ASSERT(sysmem_device_path_);
}
zx_status_t SysmemConnector::Start() {
// The process_queue_thrd_ is filled out before any code that checks it runs on the thread, because the current
// thread is the only thread that triggers anything to happen on the thread being created here, and that is only
// done after process_queue_thrd_ is filled out by the current thread.
zx_status_t status = process_queue_loop_.StartThread("SysmemConnector-ProcessQueue", &process_queue_thrd_);
if (status != ZX_OK) {
printf("sysmem-connector: process_queue_loop_.StartThread() failed - status: %d\n", status);
return status;
}
// Establish initial connection to sysmem driver async.
Post([this]{ConnectToSysmemDriver();});
return ZX_OK;
}
void SysmemConnector::QueueRequest(zx::channel allocator_request) {
ZX_DEBUG_ASSERT(thrd_current() != process_queue_thrd_);
bool trigger_needed;
{ // scope lock
fbl::AutoLock lock(&lock_);
trigger_needed = connection_requests_.empty();
connection_requests_.emplace(std::move(allocator_request));
} // ~lock
if (trigger_needed) {
Post([this]{ProcessQueue();});
}
}
void SysmemConnector::Stop() {
ZX_DEBUG_ASSERT(thrd_current() != process_queue_thrd_);
process_queue_loop_.Quit();
process_queue_loop_.JoinThreads();
process_queue_loop_.Shutdown();
}
void SysmemConnector::Post(fbl::Closure to_run) {
zx_status_t post_status = async::PostTask(
process_queue_loop_.dispatcher(), std::move(to_run));
// We don't expect this post to ever fail.
ZX_ASSERT(post_status == ZX_OK);
}
zx_status_t SysmemConnector::DeviceAddedShim(int dirfd, int event, const char* fn, void* cookie) {
ZX_DEBUG_ASSERT(cookie);
SysmemConnector* connector = static_cast<SysmemConnector*>(cookie);
return connector->DeviceAdded(dirfd, event, fn);
}
zx_status_t SysmemConnector::DeviceAdded(int dirfd, int event, const char* filename) {
ZX_DEBUG_ASSERT(thrd_current() == process_queue_thrd_);
if (event != WATCH_EVENT_ADD_FILE) {
// Keep going on IDLE or REMOVE. There's nothing else useful that the
// current thread can do until a sysmem device instance is available,
// and we don't have any reason to attempt to directly handle any
// REMOVE(s) since we'll do fdio_watch_directory() again later from
// scratch instead.
return ZX_OK;
}
ZX_DEBUG_ASSERT(event == WATCH_EVENT_ADD_FILE);
zx::channel driver_connector_client;
zx::channel driver_connector_server;
zx_status_t status = zx::channel::create(0, &driver_connector_client, &driver_connector_server);
if (status != ZX_OK) {
printf("SysmemConnector::DeviceAdded() zx::channel::create() failed - status: %d\n", status);
ZX_DEBUG_ASSERT(status != ZX_ERR_STOP);
// If channel create fails, give up on this attempt to find a sysmem
// device instance. If another request arrives later, we'll try again
// later.
return status;
}
// We don't intend to close dirfd; all paths should release not close.
fbl::unique_fd unique_dirfd(dirfd);
fzl::FdioCaller caller(std::move(unique_dirfd));
status = fdio_service_connect_at(caller.borrow_channel(), filename, driver_connector_server.release());
// Never close dirfd.
caller.release().release();
if (status != ZX_OK) {
printf("SysmemConnector::DeviceAdded() fdio_service_connect_at() failed - status: %d\n", status);
ZX_DEBUG_ASSERT(status != ZX_ERR_STOP);
// If somehow fdio_service_connect_at() fails for this device instance,
// keep watching for another device instance.
return ZX_OK;
}
driver_connector_client_ = std::move(driver_connector_client);
ZX_DEBUG_ASSERT(driver_connector_client_);
return ZX_ERR_STOP;
}
zx_status_t SysmemConnector::ConnectToSysmemDriver() {
ZX_DEBUG_ASSERT(thrd_current() == process_queue_thrd_);
ZX_DEBUG_ASSERT(!driver_connector_client_);
ZX_DEBUG_ASSERT(!sysmem_dir_fd_);
{
int fd = open(sysmem_device_path_, O_DIRECTORY | O_RDONLY);
if (fd < 0) {
printf("sysmem-connector: Failed to open %s: %d\n", sysmem_device_path_, errno);
return ZX_ERR_INTERNAL;
}
sysmem_dir_fd_.reset(fd);
}
ZX_DEBUG_ASSERT(sysmem_dir_fd_);
// Returns ZX_ERR_STOP as soon as one of the 000, 001 device instances is
// found. We rely on those to go away if the corresponding sysmem instance
// is no longer operational, so that we don't find them when we call
// ConnectToSysmemDriver() again upon discovering that we can't send to a
// previous device instance.
//
// TODO(dustingreen): Currently if this watch never finds a sysmem device
// instance, then sysmem_connector_release() will block forever. This can
// be fixed once it's feasible to use DeviceWatcher (or similar) here
// instead (currently DeviceWatcher is in garnet not zircon).
zx_status_t watch_status = fdio_watch_directory(
sysmem_dir_fd_.get(), DeviceAddedShim, ZX_TIME_INFINITE, this);
if (watch_status != ZX_ERR_STOP) {
printf("sysmem-connector: Failed to find sysmem device - status: %d\n", watch_status);
return watch_status;
}
ZX_DEBUG_ASSERT(driver_connector_client_);
return ZX_OK;
}
void SysmemConnector::ProcessQueue() {
ZX_DEBUG_ASSERT(thrd_current() == process_queue_thrd_);
while (true) {
zx::channel allocator_request;
{ // scope lock
fbl::AutoLock lock(&lock_);
if (connection_requests_.empty()) {
return;
}
allocator_request = std::move(connection_requests_.front());
connection_requests_.pop();
} // ~lock
ZX_DEBUG_ASSERT(allocator_request);
// Poll for PEER_CLOSED just before we need the channel to be usable, to
// avoid routing a request to a stale no-longer-usable sysmem device
// instance. This doesn't eliminate the inherent race where a request
// can be sent to an instance that's already started failing - that race
// is fine. This check is just a best-effort way to avoid routing to a
// super-stale previous instance.
//
// TODO(dustingreen): When it becomes more convenient, switch to
// noticing PEER_CLOSED async. Currently it doesn't seem particularly
// safe to use sysmem_fdio_caller_.borrow_channel() to borrow for an
// async wait.
if (driver_connector_client_) {
zx_signals_t observed;
zx_status_t wait_status = zx_object_wait_one(
driver_connector_client_.get(),
ZX_CHANNEL_PEER_CLOSED,
ZX_TIME_INFINITE_PAST,
&observed);
if (wait_status == ZX_OK) {
ZX_DEBUG_ASSERT(observed & ZX_CHANNEL_PEER_CLOSED);
// This way, we'll call ConnectToSysmemDriver() below.
driver_connector_client_.reset();
} else {
// Any other failing status is unexpected.
ZX_DEBUG_ASSERT(ZX_ERR_TIMED_OUT);
}
}
if (!driver_connector_client_) {
zx_status_t connect_status = ConnectToSysmemDriver();
if (connect_status != ZX_OK) {
// ~allocator_request - we'll try again to connect to a sysmem
// instance next time a request comes in, but any given request
// gets a max of one attempt to connect to a sysmem device
// instance, in case attempts to find a sysmem device instance
// are just failing.
return;
}
}
ZX_DEBUG_ASSERT(driver_connector_client_);
zx_status_t send_connect_status = fuchsia_sysmem_DriverConnectorConnect(
driver_connector_client_.get(), allocator_request.release());
if (send_connect_status != ZX_OK) {
// The most likely failing send_connect_status is
// ZX_ERR_PEER_CLOSED, which can happen if the channel closed since
// we checked above. Since we don't really expect even
// ZX_ERR_PEER_CLOSED unless sysmem is having problems, complain
// about the error regardless of which error.
printf("SysmemConnector::ProcessQueue() DriverConnectorConnect() returned unexpected status: %d\n",
send_connect_status);
// Regardless of the specific error, we want to try
// ConnectToSysmemDriver() again for the _next_ request.
driver_connector_client_.reset();
// We don't retry this request (the window for getting
// ZX_ERR_PEER_CLOSED is short due to check above, and exists in any
// case due to possibility of close from other end at any time), but
// the next request will try ConnectToSysmemDriver() again.
//
// continue with next request
}
}
}
zx_status_t sysmem_connector_init(const char* sysmem_device_path, sysmem_connector_t** out_connector) {
SysmemConnector* connector = new SysmemConnector(sysmem_device_path);
zx_status_t status = connector->Start();
if (status != ZX_OK) {
printf("sysmem_connector_init() connector->Start() failed - status: %d\n", status);
return status;
}
*out_connector = connector;
return ZX_OK;
}
void sysmem_connector_queue_connection_request(
sysmem_connector_t* connector_param,
zx_handle_t allocator_request_param) {
zx::channel allocator_request(allocator_request_param);
ZX_DEBUG_ASSERT(connector_param);
ZX_DEBUG_ASSERT(allocator_request);
SysmemConnector* connector = static_cast<SysmemConnector*>(connector_param);
connector->QueueRequest(std::move(allocator_request));
}
void sysmem_connector_release(sysmem_connector_t* connector_param) {
ZX_DEBUG_ASSERT(connector_param);
SysmemConnector* connector = static_cast<SysmemConnector*>(connector_param);
connector->Stop();
delete connector;
}