blob: d54926c3148ad68737012cb5b01825d16f8e11b2 [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 "src/developer/debug/debug_agent/limbo_provider.h"
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/binding_set.h>
#include <lib/sys/cpp/testing/service_directory_provider.h>
#include <zircon/status.h>
#include <atomic>
#include <thread>
#include <gtest/gtest.h>
#include "src/developer/debug/debug_agent/mock_object_provider.h"
#include "src/developer/debug/debug_agent/test_utils.h"
#include "src/lib/fxl/logging.h"
using namespace ::fuchsia::exception;
namespace debug_agent {
namespace {
// Stubs -------------------------------------------------------------------------------------------
class StubProcessLimbo : public fuchsia::exception::ProcessLimbo {
public:
void SetActive(bool active, SetActiveCallback cb) { FXL_NOTREACHED() << "Not needed for tests."; }
void WatchActive(WatchActiveCallback callback) override {
if (!reply_active_)
return;
callback(is_active_);
reply_active_ = false;
}
void WatchProcessesWaitingOnException(
ProcessLimbo::WatchProcessesWaitingOnExceptionCallback callback) override {
watch_count_++;
if (!reply_watch_processes_) {
watch_processes_callback_ = std::move(callback);
reply_watch_processes_ = true;
return;
}
callback(fit::ok(CreateExceptionList()));
reply_watch_processes_ = false;
}
void RetrieveException(zx_koid_t process_koid,
ProcessLimbo::RetrieveExceptionCallback callback) override {
auto it = processes_.find(process_koid);
if (it == processes_.end())
return callback(fit::error(ZX_ERR_NOT_FOUND));
// We cannot set any fake handles, as they will fail on the channel write.
ProcessException exception;
exception.set_info(it->second.info());
processes_.erase(it);
callback(fit::ok(std::move(exception)));
}
void ReleaseProcess(zx_koid_t process_koid, ProcessLimbo::ReleaseProcessCallback cb) override {
auto it = processes_.find(process_koid);
if (it == processes_.end())
return cb(fit::error(ZX_ERR_NOT_FOUND));
processes_.erase(it);
cb(fit::ok());
if (reply_watch_processes_ && watch_processes_callback_) {
watch_processes_callback_(fit::ok(CreateExceptionList()));
reply_watch_processes_ = false;
return;
}
}
void AppendException(const MockProcessObject& process, const MockThreadObject& thread,
ExceptionType exception_type) {
ExceptionInfo info = {};
info.process_koid = process.koid;
info.thread_koid = thread.koid;
info.type = exception_type;
// Track the metadata in the limbo.
ProcessExceptionMetadata metadata = {};
metadata.set_info(std::move(info));
// Sadly we cannot send bad handles over a channel, so we cannot actually send the "invented"
// handles for this test. Setting the info is enough though.
// metadata.set_process(process.GetHandle());
// metadata.set_thread(thread.GetHandle());
processes_[info.process_koid] = std::move(metadata);
// If there is a callback, only send the new exceptions over.
if (watch_processes_callback_) {
watch_processes_callback_(fit::ok(CreateExceptionList()));
watch_processes_callback_ = {};
reply_watch_processes_ = false;
}
}
std::vector<ProcessExceptionMetadata> CreateExceptionList() {
std::vector<ProcessExceptionMetadata> processes;
processes.reserve(processes_.size());
for (auto& [process_koid, metadata] : processes_) {
ProcessExceptionMetadata new_metadata = {};
new_metadata.set_info(metadata.info());
processes.push_back(std::move(new_metadata));
}
return processes;
}
// Not used for now.
void GetFilters(GetFiltersCallback) override {}
void AppendFilters(std::vector<std::string> filters, AppendFiltersCallback) override {}
void RemoveFilters(std::vector<std::string> filters, RemoveFiltersCallback) override {}
const std::map<zx_koid_t, ProcessExceptionMetadata>& processes() const { return processes_; }
// Boilerplate needed for getting a FIDL binding to work in unit tests.
fidl::InterfaceRequestHandler<ProcessLimbo> GetHandler() { return bindings_.GetHandler(this); }
void set_is_active(bool is_active) { is_active_ = is_active; }
void set_reply_active(bool reply) { reply_active_ = reply; }
void set_reply_watch_processes(bool reply) { reply_watch_processes_ = reply; }
bool has_watch_processes_callback() const { return !!watch_processes_callback_; }
int watch_count() const { return watch_count_.load(); }
private:
std::map<zx_koid_t, ProcessExceptionMetadata> processes_;
fidl::BindingSet<ProcessLimbo> bindings_;
bool is_active_ = true;
bool reply_active_ = true;
bool reply_watch_processes_ = true;
ProcessLimbo::WatchProcessesWaitingOnExceptionCallback watch_processes_callback_;
std::atomic<int> watch_count_ = 0;
};
std::pair<const MockProcessObject*, const MockThreadObject*> GetProcessThread(
const MockObjectProvider& object_provider, const std::string& process_name,
const std::string& thread_name) {
const MockProcessObject* process = object_provider.ProcessByName(process_name);
FXL_DCHECK(process);
const MockThreadObject* thread = process->GetThread(thread_name);
FXL_DCHECK(thread);
return {process, thread};
}
void RunUntil(async::Loop* loop, fit::function<bool()> condition,
zx::duration step = zx::msec(10)) {
while (!condition()) {
loop->Run(zx::deadline_after(step));
}
}
// Tests -------------------------------------------------------------------------------------------
TEST(LimboProvider, WatchProcessesOnException) {
// Set the process limbo.
auto object_provider = CreateDefaultMockObjectProvider();
auto [process1, thread1] = GetProcessThread(*object_provider, "root-p2", "initial-thread");
constexpr ExceptionType exception1 = ExceptionType::FATAL_PAGE_FAULT;
auto [process2, thread2] = GetProcessThread(*object_provider, "job121-p2", "third-thread");
constexpr ExceptionType exception2 = ExceptionType::UNALIGNED_ACCESS;
StubProcessLimbo process_limbo;
process_limbo.AppendException(*process1, *thread1, exception1);
process_limbo.AppendException(*process2, *thread2, exception2);
// Setup the async loop to respond to the async call.
async::Loop remote_loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sys::testing::ServiceDirectoryProvider services(remote_loop.dispatcher());
services.AddService(process_limbo.GetHandler());
ASSERT_ZX_EQ(remote_loop.StartThread("process-limbo-thread"), ZX_OK);
async::Loop local_loop(&kAsyncLoopConfigAttachToCurrentThread);
LimboProvider limbo_provider(services.service_directory());
ASSERT_ZX_EQ(limbo_provider.Init(), ZX_OK);
ASSERT_TRUE(limbo_provider.Valid());
process_limbo.set_reply_active(false);
local_loop.RunUntilIdle();
/* std::vector<ProcessExceptionMetadata> processes; */
auto& processes = limbo_provider.Limbo();
ASSERT_EQ(processes.size(), 2u);
auto it = processes.begin();
ASSERT_TRUE(it->second.has_info());
EXPECT_EQ(it->second.info().process_koid, process1->koid);
EXPECT_EQ(it->second.info().thread_koid, thread1->koid);
EXPECT_EQ(it->second.info().type, exception1);
it++;
ASSERT_TRUE(it->second.has_info());
EXPECT_EQ(it->second.info().process_koid, process2->koid);
EXPECT_EQ(it->second.info().thread_koid, thread2->koid);
EXPECT_EQ(it->second.info().type, exception2);
}
TEST(LimboProvider, RetriveException) {
// Set the process limbo.
auto object_provider = CreateDefaultMockObjectProvider();
auto [process1, thread1] = GetProcessThread(*object_provider, "root-p2", "initial-thread");
constexpr ExceptionType exception1 = ExceptionType::FATAL_PAGE_FAULT;
auto [process2, thread2] = GetProcessThread(*object_provider, "job121-p2", "third-thread");
constexpr ExceptionType exception2 = ExceptionType::UNALIGNED_ACCESS;
StubProcessLimbo process_limbo;
process_limbo.AppendException(*process1, *thread1, exception1);
process_limbo.AppendException(*process2, *thread2, exception2);
// Setup the async loop to respond to the async call.
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sys::testing::ServiceDirectoryProvider services(loop.dispatcher());
services.AddService(process_limbo.GetHandler());
ASSERT_ZX_EQ(loop.StartThread("process-limbo-thread"), ZX_OK);
LimboProvider limbo_provider(services.service_directory());
// Some random koid should fail.
fuchsia::exception::ProcessException exception;
ASSERT_ZX_EQ(limbo_provider.RetrieveException(-1, &exception), ZX_ERR_NOT_FOUND);
// Getting a valid one should work.
ASSERT_ZX_EQ(limbo_provider.RetrieveException(process1->koid, &exception), ZX_OK);
// We can only check the info in this test.
EXPECT_EQ(exception.info().process_koid, process1->koid);
EXPECT_EQ(exception.info().thread_koid, thread1->koid);
EXPECT_EQ(exception.info().type, exception1);
}
TEST(LimboProvider, ReleaseProcess) {
// Set the process limbo.
auto object_provider = CreateDefaultMockObjectProvider();
auto [process1, thread1] = GetProcessThread(*object_provider, "root-p2", "initial-thread");
constexpr ExceptionType exception1 = ExceptionType::FATAL_PAGE_FAULT;
auto [process2, thread2] = GetProcessThread(*object_provider, "job121-p2", "third-thread");
constexpr ExceptionType exception2 = ExceptionType::UNALIGNED_ACCESS;
StubProcessLimbo process_limbo;
process_limbo.AppendException(*process1, *thread1, exception1);
process_limbo.AppendException(*process2, *thread2, exception2);
// Setup the async loop to respond to the async call.
async::Loop loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sys::testing::ServiceDirectoryProvider services(loop.dispatcher());
services.AddService(process_limbo.GetHandler());
ASSERT_ZX_EQ(loop.StartThread("process-limbo-thread"), ZX_OK);
LimboProvider limbo_provider(services.service_directory());
// Some random koid should fail.
fuchsia::exception::ProcessException exception;
ASSERT_ZX_EQ(limbo_provider.ReleaseProcess(-1), ZX_ERR_NOT_FOUND);
// Getting a valid one should work.
ASSERT_ZX_EQ(limbo_provider.ReleaseProcess(process1->koid), ZX_OK);
// There should only be one process left on limbo.
ASSERT_EQ(process_limbo.processes().size(), 1u);
auto it = process_limbo.processes().begin();
EXPECT_EQ(it++->first, process2->koid);
}
TEST(LimboProvider, WatchProcessesCallback) {
// Set the process limbo.
auto object_provider = CreateDefaultMockObjectProvider();
auto [process1, thread1] = GetProcessThread(*object_provider, "root-p2", "initial-thread");
constexpr ExceptionType exception1 = ExceptionType::FATAL_PAGE_FAULT;
auto [process2, thread2] = GetProcessThread(*object_provider, "job121-p2", "third-thread");
constexpr ExceptionType exception2 = ExceptionType::UNALIGNED_ACCESS;
StubProcessLimbo process_limbo;
process_limbo.AppendException(*process1, *thread1, exception1);
// Setup the async loop to respond to the async call.
async::Loop remote_loop(&kAsyncLoopConfigNoAttachToCurrentThread);
sys::testing::ServiceDirectoryProvider services(remote_loop.dispatcher());
services.AddService(process_limbo.GetHandler());
ASSERT_ZX_EQ(remote_loop.StartThread("process-limbo-thread"), ZX_OK);
async::Loop local_loop(&kAsyncLoopConfigAttachToCurrentThread);
LimboProvider limbo_provider(services.service_directory());
ASSERT_ZX_EQ(limbo_provider.Init(), ZX_OK);
ASSERT_TRUE(limbo_provider.Valid());
local_loop.RunUntilIdle();
RunUntil(&local_loop,
[&process_limbo]() { return process_limbo.has_watch_processes_callback(); });
{
// There should be one exception on limbo.
auto& limbo = limbo_provider.Limbo();
ASSERT_EQ(limbo.size(), 1u);
auto it = limbo.find(process1->koid);
ASSERT_NE(it, limbo.end());
EXPECT_EQ(it->second.info().process_koid, process1->koid);
EXPECT_EQ(it->second.info().thread_koid, thread1->koid);
EXPECT_EQ(it->second.info().type, exception1);
}
// Set the callback.
bool called = false;
std::vector<ProcessExceptionMetadata> exceptions;
limbo_provider.set_on_enter_limbo(
[&called, &exceptions](std::vector<ProcessExceptionMetadata> new_exceptions) {
called = true;
exceptions = std::move(new_exceptions);
});
// The event should've not been signaled.
/* ASSERT_ZX_EQ(called.wait_one(ZX_USER_SIGNAL_0, zx::time(0), nullptr), ZX_ERR_TIMED_OUT); */
ASSERT_FALSE(called);
// We post an exception on the limbo's loop.
{
zx::event exception_posted;
ASSERT_ZX_EQ(zx::event::create(0, &exception_posted), ZX_OK);
async::PostTask(remote_loop.dispatcher(),
[p = process2, t = thread2, &process_limbo, &exception_posted]() {
// Add the new exception.
process_limbo.AppendException(*p, *t, exception2);
exception_posted.signal(0, ZX_USER_SIGNAL_0);
});
// Wait until it was posted.
ASSERT_ZX_EQ(exception_posted.wait_one(ZX_USER_SIGNAL_0, zx::time::infinite(), nullptr), ZX_OK);
}
// Process the callback.
RunUntil(&local_loop, [&called]() { return called; });
// Should've called the callback.
{
ASSERT_TRUE(called);
ASSERT_EQ(exceptions.size(), 1u);
EXPECT_EQ(exceptions[0].info().process_koid, process2->koid);
EXPECT_EQ(exceptions[0].info().thread_koid, thread2->koid);
EXPECT_EQ(exceptions[0].info().type, exception2);
// The limbo should be updated.
auto& limbo = limbo_provider.Limbo();
ASSERT_EQ(limbo.size(), 2u);
auto it = limbo.find(process1->koid);
ASSERT_NE(it, limbo.end());
EXPECT_EQ(it->second.info().process_koid, process1->koid);
EXPECT_EQ(it->second.info().thread_koid, thread1->koid);
EXPECT_EQ(it->second.info().type, exception1);
it = limbo.find(process2->koid);
ASSERT_NE(it, limbo.end());
EXPECT_EQ(it->second.info().process_koid, process2->koid);
EXPECT_EQ(it->second.info().thread_koid, thread2->koid);
EXPECT_EQ(it->second.info().type, exception2);
}
// Releasing an exception should also call the enter limbo callback.
called = false;
exceptions.clear();
{
zx::event release_event;
ASSERT_ZX_EQ(zx::event::create(0, &release_event), ZX_OK);
async::PostTask(remote_loop.dispatcher(),
[process_koid = process2->koid, &process_limbo, &release_event]() {
// Add the new exception.
process_limbo.ReleaseProcess(process_koid, [](auto a) {});
release_event.signal(0, ZX_USER_SIGNAL_0);
});
// Wait until it was posted.
ASSERT_ZX_EQ(release_event.wait_one(ZX_USER_SIGNAL_0, zx::time::infinite(), nullptr), ZX_OK);
}
// The enter limbo callback should not have been called.
ASSERT_FALSE(called);
// We wait until the limbo have had a time to issue the other watch, thus having processes the
// release callback.
RunUntil(&local_loop, [&process_limbo]() { return process_limbo.watch_count() == 4; });
// We should've received the call.
// Should've called the callback.
{
// The limbo should be updated.
auto& limbo = limbo_provider.Limbo();
ASSERT_EQ(limbo.size(), 1u);
auto it = limbo.find(process1->koid);
ASSERT_NE(it, limbo.end());
EXPECT_EQ(it->second.info().process_koid, process1->koid);
EXPECT_EQ(it->second.info().thread_koid, thread1->koid);
EXPECT_EQ(it->second.info().type, exception1);
}
}
} // namespace
} // namespace debug_agent