/*
 * 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([&, 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());
      }
    }
  });
}
