/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "ThreadCapture.h"
#include <fcntl.h>
#include <pthread.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <algorithm>
#include <functional>
#include <memory>
#include <thread>
#include <gtest/gtest.h>
#include "Allocator.h"
#include "ScopedDisableMalloc.h"
#include "ScopedPipe.h"
using namespace std::chrono_literals;
class ThreadListTest : public ::testing::TestWithParam<int> {
public:
ThreadListTest() : stop_(false) {}
~ThreadListTest() {
// pthread_join may return before the entry in /proc/pid/task/ is gone,
// loop until ListThreads only finds the main thread so the next test
// doesn't fail.
WaitForThreads();
}
virtual void TearDown() {
ASSERT_TRUE(heap.empty());
}
protected:
template<class Function>
void StartThreads(unsigned int threads, Function&& func) {
threads_.reserve(threads);
tids_.reserve(threads);
for (unsigned int i = 0; i < threads; i++) {
threads_.emplace_back([&, i, threads, this]() {
{
std::lock_guard<std::mutex> lk(m_);
tids_.push_back(gettid());
if (tids_.size() == threads) {
cv_start_.notify_one();
}
}
func();
{
std::unique_lock<std::mutex> lk(m_);
cv_stop_.wait(lk, [&] {return stop_;});
}
});
}
{
std::unique_lock<std::mutex> lk(m_);
cv_start_.wait(lk, [&]{ return tids_.size() == threads; });
}
}
void StopThreads() {
{
std::lock_guard<std::mutex> lk(m_);
stop_ = true;
}
cv_stop_.notify_all();
for (auto i = threads_.begin(); i != threads_.end(); i++) {
i->join();
}
threads_.clear();
tids_.clear();
}
std::vector<pid_t>& tids() {
return tids_;
}
Heap heap;
private:
void WaitForThreads() {
auto tids = TidList{heap};
ThreadCapture thread_capture{getpid(), heap};
for (unsigned int i = 0; i < 100; i++) {
EXPECT_TRUE(thread_capture.ListThreads(tids));
if (tids.size() == 1) {
break;
}
std::this_thread::sleep_for(10ms);
}
EXPECT_EQ(1U, tids.size());
}
std::mutex m_;
std::condition_variable cv_start_;
std::condition_variable cv_stop_;
bool stop_;
std::vector<pid_t> tids_;
std::vector<std::thread> threads_;
};
TEST_F(ThreadListTest, list_one) {
ScopedDisableMallocTimeout disable_malloc;
ThreadCapture thread_capture(getpid(), heap);
auto expected_tids = allocator::vector<pid_t>(1, getpid(), heap);
auto list_tids = allocator::vector<pid_t>(heap);
ASSERT_TRUE(thread_capture.ListThreads(list_tids));
ASSERT_EQ(expected_tids, list_tids);
if (!HasFailure()) {
ASSERT_FALSE(disable_malloc.timed_out());
}
}
TEST_P(ThreadListTest, list_some) {
const unsigned int threads = GetParam() - 1;
StartThreads(threads, [](){});
std::vector<pid_t> expected_tids = tids();
expected_tids.push_back(getpid());
auto list_tids = allocator::vector<pid_t>(heap);
{
ScopedDisableMallocTimeout disable_malloc;
ThreadCapture thread_capture(getpid(), heap);
ASSERT_TRUE(thread_capture.ListThreads(list_tids));
if (!HasFailure()) {
ASSERT_FALSE(disable_malloc.timed_out());
}
}
StopThreads();
std::sort(list_tids.begin(), list_tids.end());
std::sort(expected_tids.begin(), expected_tids.end());
ASSERT_EQ(expected_tids.size(), list_tids.size());
EXPECT_TRUE(std::equal(expected_tids.begin(), expected_tids.end(), list_tids.begin()));
}
INSTANTIATE_TEST_CASE_P(ThreadListTest, ThreadListTest, ::testing::Values(1, 2, 10, 1024));
class ThreadCaptureTest : public ThreadListTest {
public:
ThreadCaptureTest() {}
~ThreadCaptureTest() {}
void Fork(std::function<void()>&& child_init,
std::function<void()>&& child_cleanup,
std::function<void(pid_t)>&& parent) {
ScopedPipe start_pipe;
ScopedPipe stop_pipe;
int pid = fork();
if (pid == 0) {
// child
child_init();
EXPECT_EQ(1, TEMP_FAILURE_RETRY(write(start_pipe.Sender(), "+", 1))) << strerror(errno);
char buf;
EXPECT_EQ(1, TEMP_FAILURE_RETRY(read(stop_pipe.Receiver(), &buf, 1))) << strerror(errno);
child_cleanup();
_exit(0);
} else {
// parent
ASSERT_GT(pid, 0);
char buf;
ASSERT_EQ(1, TEMP_FAILURE_RETRY(read(start_pipe.Receiver(), &buf, 1))) << strerror(errno);
parent(pid);
ASSERT_EQ(1, TEMP_FAILURE_RETRY(write(stop_pipe.Sender(), "+", 1))) << strerror(errno);
siginfo_t info{};
ASSERT_EQ(0, TEMP_FAILURE_RETRY(waitid(P_PID, pid, &info, WEXITED))) << strerror(errno);
}
}
};
TEST_P(ThreadCaptureTest, capture_some) {
const unsigned int threads = GetParam();
Fork([&](){
// child init
StartThreads(threads - 1, [](){});
},
[&](){
// child cleanup
StopThreads();
},
[&](pid_t child){
// parent
ASSERT_GT(child, 0);
{
ScopedDisableMallocTimeout disable_malloc;
ThreadCapture thread_capture(child, heap);
auto list_tids = allocator::vector<pid_t>(heap);
ASSERT_TRUE(thread_capture.ListThreads(list_tids));
ASSERT_EQ(threads, list_tids.size());
ASSERT_TRUE(thread_capture.CaptureThreads());
auto thread_info = allocator::vector<ThreadInfo>(heap);
ASSERT_TRUE(thread_capture.CapturedThreadInfo(thread_info));
ASSERT_EQ(threads, thread_info.size());
ASSERT_TRUE(thread_capture.ReleaseThreads());
if (!HasFailure()) {
ASSERT_FALSE(disable_malloc.timed_out());
}
}
});
}
INSTANTIATE_TEST_CASE_P(ThreadCaptureTest, ThreadCaptureTest, ::testing::Values(1, 2, 10, 1024));
TEST_F(ThreadCaptureTest, capture_kill) {
int ret = fork();
if (ret == 0) {
// child
sleep(10);
} else {
// parent
ASSERT_GT(ret, 0);
{
ScopedDisableMallocTimeout disable_malloc;
ThreadCapture thread_capture(ret, heap);
thread_capture.InjectTestFunc([&](pid_t tid){
syscall(SYS_tgkill, ret, tid, SIGKILL);
usleep(10000);
});
auto list_tids = allocator::vector<pid_t>(heap);
ASSERT_TRUE(thread_capture.ListThreads(list_tids));
ASSERT_EQ(1U, list_tids.size());
ASSERT_FALSE(thread_capture.CaptureThreads());
if (!HasFailure()) {
ASSERT_FALSE(disable_malloc.timed_out());
}
}
}
}
TEST_F(ThreadCaptureTest, capture_signal) {
const int sig = SIGUSR1;
ScopedPipe pipe;
// For signal handler
static ScopedPipe* g_pipe;
Fork([&](){
// child init
pipe.CloseReceiver();
g_pipe = &pipe;
struct sigaction act{};
act.sa_handler = [](int){
char buf = '+';
write(g_pipe->Sender(), &buf, 1);
g_pipe->CloseSender();
};
sigaction(sig, &act, NULL);
sigset_t set;
sigemptyset(&set);
sigaddset(&set, sig);
pthread_sigmask(SIG_UNBLOCK, &set, NULL);
},
[&](){
// child cleanup
g_pipe = nullptr;
pipe.Close();
},
[&](pid_t child){
// parent
ASSERT_GT(child, 0);
pipe.CloseSender();
{
ScopedDisableMallocTimeout disable_malloc;
ThreadCapture thread_capture(child, heap);
thread_capture.InjectTestFunc([&](pid_t tid){
syscall(SYS_tgkill, child, tid, sig);
usleep(10000);
});
auto list_tids = allocator::vector<pid_t>(heap);
ASSERT_TRUE(thread_capture.ListThreads(list_tids));
ASSERT_EQ(1U, list_tids.size());
ASSERT_TRUE(thread_capture.CaptureThreads());
auto thread_info = allocator::vector<ThreadInfo>(heap);
ASSERT_TRUE(thread_capture.CapturedThreadInfo(thread_info));
ASSERT_EQ(1U, thread_info.size());
ASSERT_TRUE(thread_capture.ReleaseThreads());
usleep(100000);
char buf;
ASSERT_EQ(1, TEMP_FAILURE_RETRY(read(pipe.Receiver(), &buf, 1)));
ASSERT_EQ(buf, '+');
if (!HasFailure()) {
ASSERT_FALSE(disable_malloc.timed_out());
}
}
});
}