/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsDumpUtils.h"

#include <errno.h>

#include "SpecialSystemDirectory.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Services.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsIObserverService.h"
#include "prenv.h"

#if defined(XP_UNIX) && !defined(XP_IOS)  // {
#  include <fcntl.h>
#  include <signal.h>
#  include <sys/stat.h>
#  include <unistd.h>

#  include "mozilla/Preferences.h"

using namespace mozilla;

/*
 * The following code supports triggering a registered callback upon
 * receiving a specific signal.
 *
 * Take about:memory for example, we register
 * 1. doGCCCDump for doMemoryReport
 * 2. doMemoryReport for sDumpAboutMemorySignum(SIGRTMIN)
 *                       and sDumpAboutMemoryAfterMMUSignum(SIGRTMIN+1).
 *
 * When we receive one of these signals, we write the signal number to a pipe.
 * The IO thread then notices that the pipe has been written to, and kicks off
 * the appropriate task on the main thread.
 *
 * This scheme is similar to using signalfd(), except it's portable and it
 * doesn't require the use of sigprocmask, which is problematic because it
 * masks signals received by child processes.
 */

// This is the write-end of a pipe that we use to notice when a
// specific signal occurs.
static Atomic<int> sDumpPipeWriteFd(-1);

const char FifoWatcher::kPrefName[] = "memory_info_dumper.watch_fifo.enabled";

static void DumpSignalHandler(int aSignum) {
  // This is a signal handler, so everything in here needs to be
  // async-signal-safe.  Be careful!

  if (sDumpPipeWriteFd != -1) {
    uint8_t signum = static_cast<int>(aSignum);
    [[maybe_unused]] ssize_t r =
        write(sDumpPipeWriteFd, &signum, sizeof(signum));
  }
}

NS_IMPL_ISUPPORTS(FdWatcher, nsIObserver);

void FdWatcher::Init() {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIObserverService> os = services::GetObserverService();
  os->AddObserver(this, "xpcom-shutdown", /* ownsWeak = */ false);

  XRE_GetAsyncIOEventTarget()->Dispatch(NewRunnableMethod(
      "FdWatcher::StartWatching", this, &FdWatcher::StartWatching));
}

// Implementations may call this function multiple times if they ensure that
// it's safe to call OpenFd() multiple times and they call StopWatching()
// first.
void FdWatcher::StartWatching() {
  MOZ_ASSERT(XRE_GetAsyncIOEventTarget()->IsOnCurrentThread());
  MOZ_ASSERT(mFd == -1);

  mFd = OpenFd();
  if (mFd == -1) {
    LOG("FdWatcher: OpenFd failed.");
    return;
  }

  MessageLoopForIO::current()->WatchFileDescriptor(mFd, /* persistent = */ true,
                                                   MessageLoopForIO::WATCH_READ,
                                                   &mReadWatcher, this);
}

// Since implementations can call StartWatching() multiple times, they can of
// course call StopWatching() multiple times.
void FdWatcher::StopWatching() {
  MOZ_ASSERT(XRE_GetAsyncIOEventTarget()->IsOnCurrentThread());

  mReadWatcher.StopWatchingFileDescriptor();
  if (mFd != -1) {
    close(mFd);
    mFd = -1;
  }
}

StaticRefPtr<SignalPipeWatcher> SignalPipeWatcher::sSingleton;

/* static */
SignalPipeWatcher* SignalPipeWatcher::GetSingleton() {
  if (!sSingleton) {
    sSingleton = new SignalPipeWatcher();
    sSingleton->Init();
    ClearOnShutdown(&sSingleton);
  }
  return sSingleton;
}

void SignalPipeWatcher::RegisterCallback(uint8_t aSignal,
                                         PipeCallback aCallback) {
  MutexAutoLock lock(mSignalInfoLock);

  for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); ++i) {
    if (mSignalInfo[i].mSignal == aSignal) {
      LOG("Register Signal(%d) callback failed! (DUPLICATE)", aSignal);
      return;
    }
  }
  SignalInfo signalInfo = {aSignal, aCallback};
  mSignalInfo.AppendElement(signalInfo);
  RegisterSignalHandler(signalInfo.mSignal);
}

void SignalPipeWatcher::RegisterSignalHandler(uint8_t aSignal) {
  struct sigaction action;
  memset(&action, 0, sizeof(action));
  sigemptyset(&action.sa_mask);
  action.sa_handler = DumpSignalHandler;

  if (aSignal) {
    if (sigaction(aSignal, &action, nullptr)) {
      LOG("SignalPipeWatcher failed to register sig %d.", aSignal);
    }
  } else {
    MutexAutoLock lock(mSignalInfoLock);
    for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); i++) {
      if (sigaction(mSignalInfo[i].mSignal, &action, nullptr)) {
        LOG("SignalPipeWatcher failed to register signal(%d) "
            "dump signal handler.",
            mSignalInfo[i].mSignal);
      }
    }
  }
}

SignalPipeWatcher::~SignalPipeWatcher() {
  if (sDumpPipeWriteFd != -1) {
    StopWatching();
  }
}

int SignalPipeWatcher::OpenFd() {
  MOZ_ASSERT(XRE_GetAsyncIOEventTarget()->IsOnCurrentThread());

  // Create a pipe.  When we receive a signal in our signal handler, we'll
  // write the signum to the write-end of this pipe.
  int pipeFds[2];
  if (pipe(pipeFds)) {
    LOG("SignalPipeWatcher failed to create pipe.");
    return -1;
  }

  // Close this pipe on calls to exec().
  fcntl(pipeFds[0], F_SETFD, FD_CLOEXEC);
  fcntl(pipeFds[1], F_SETFD, FD_CLOEXEC);

  int readFd = pipeFds[0];
  sDumpPipeWriteFd = pipeFds[1];

  RegisterSignalHandler();
  return readFd;
}

void SignalPipeWatcher::StopWatching() {
  MOZ_ASSERT(XRE_GetAsyncIOEventTarget()->IsOnCurrentThread());

  // Close sDumpPipeWriteFd /after/ setting the fd to -1.
  // Otherwise we have the (admittedly far-fetched) race where we
  //
  //  1) close sDumpPipeWriteFd
  //  2) open a new fd with the same number as sDumpPipeWriteFd
  //     had.
  //  3) receive a signal, then write to the fd.
  int pipeWriteFd = sDumpPipeWriteFd.exchange(-1);
  close(pipeWriteFd);

  FdWatcher::StopWatching();
}

void SignalPipeWatcher::OnFileCanReadWithoutBlocking(int aFd) {
  MOZ_ASSERT(XRE_GetAsyncIOEventTarget()->IsOnCurrentThread());

  uint8_t signum;
  ssize_t numReceived = read(aFd, &signum, sizeof(signum));
  if (numReceived != sizeof(signum)) {
    LOG("Error reading from buffer in "
        "SignalPipeWatcher::OnFileCanReadWithoutBlocking.");
    return;
  }

  {
    MutexAutoLock lock(mSignalInfoLock);
    for (SignalInfoArray::index_type i = 0; i < mSignalInfo.Length(); i++) {
      if (signum == mSignalInfo[i].mSignal) {
        mSignalInfo[i].mCallback(signum);
        return;
      }
    }
  }
  LOG("SignalPipeWatcher got unexpected signum.");
}

StaticRefPtr<FifoWatcher> FifoWatcher::sSingleton;

/* static */
FifoWatcher* FifoWatcher::GetSingleton() {
  if (!sSingleton) {
    nsAutoCString dirPath;
    Preferences::GetCString("memory_info_dumper.watch_fifo.directory", dirPath);
    sSingleton = new FifoWatcher(std::move(dirPath));
    sSingleton->Init();
    ClearOnShutdown(&sSingleton);
  }
  return sSingleton;
}

/* static */
bool FifoWatcher::MaybeCreate() {
  MOZ_ASSERT(NS_IsMainThread());

  if (!XRE_IsParentProcess()) {
    // We want this to be main-process only, since two processes can't listen
    // to the same fifo.
    return false;
  }

  if (!Preferences::GetBool(kPrefName, false)) {
    LOG("Fifo watcher disabled via pref.");
    return false;
  }

  // The FifoWatcher is held alive by the observer service.
  if (!sSingleton) {
    GetSingleton();
  }
  return true;
}

void FifoWatcher::RegisterCallback(const nsCString& aCommand,
                                   FifoCallback aCallback) {
  MutexAutoLock lock(mFifoInfoLock);

  for (FifoInfoArray::index_type i = 0; i < mFifoInfo.Length(); ++i) {
    if (mFifoInfo[i].mCommand.Equals(aCommand)) {
      LOG("Register command(%s) callback failed! (DUPLICATE)", aCommand.get());
      return;
    }
  }
  FifoInfo aFifoInfo = {aCommand, aCallback};
  mFifoInfo.AppendElement(aFifoInfo);
}

FifoWatcher::~FifoWatcher() = default;

int FifoWatcher::OpenFd() {
  // If the memory_info_dumper.directory pref is specified, put the fifo
  // there.  Otherwise, put it into the system's tmp directory.

  nsCOMPtr<nsIFile> file;

  nsresult rv;
  if (mDirPath.Length() > 0) {
    rv = XRE_GetFileFromPath(mDirPath.get(), getter_AddRefs(file));
    if (NS_FAILED(rv)) {
      LOG("FifoWatcher failed to open file \"%s\"", mDirPath.get());
      return -1;
    }
  } else {
    rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file));
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return -1;
    }
  }

  rv = file->AppendNative("debug_info_trigger"_ns);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return -1;
  }

  nsAutoCString path;
  rv = file->GetNativePath(path);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return -1;
  }

  // unlink might fail because the file doesn't exist, or for other reasons.
  // But we don't care it fails; any problems will be detected later, when we
  // try to mkfifo or open the file.
  if (unlink(path.get())) {
    LOG("FifoWatcher::OpenFifo unlink failed; errno=%d.  "
        "Continuing despite error.",
        errno);
  }

  if (mkfifo(path.get(), 0766)) {
    LOG("FifoWatcher::OpenFifo mkfifo failed; errno=%d", errno);
    return -1;
  }

#  ifdef ANDROID
  // Android runs with a umask, so we need to chmod our fifo to make it
  // world-writable.
  chmod(path.get(), 0666);
#  endif

  int fd;
  do {
    // The fifo will block until someone else has written to it.  In
    // particular, open() will block until someone else has opened it for
    // writing!  We want open() to succeed and read() to block, so we open
    // with NONBLOCK and then fcntl that away.
    fd = open(path.get(), O_RDONLY | O_NONBLOCK);
  } while (fd == -1 && errno == EINTR);

  if (fd == -1) {
    LOG("FifoWatcher::OpenFifo open failed; errno=%d", errno);
    return -1;
  }

  // Make fd blocking now that we've opened it.
  if (fcntl(fd, F_SETFL, 0)) {
    close(fd);
    return -1;
  }

  return fd;
}

void FifoWatcher::OnFileCanReadWithoutBlocking(int aFd) {
  MOZ_ASSERT(XRE_GetAsyncIOEventTarget()->IsOnCurrentThread());

  char buf[1024];
  int nread;
  do {
    // sizeof(buf) - 1 to leave space for the null-terminator.
    nread = read(aFd, buf, sizeof(buf));
  } while (nread == -1 && errno == EINTR);

  if (nread == -1) {
    // We want to avoid getting into a situation where
    // OnFileCanReadWithoutBlocking is called in an infinite loop, so when
    // something goes wrong, stop watching the fifo altogether.
    LOG("FifoWatcher hit an error (%d) and is quitting.", errno);
    StopWatching();
    return;
  }

  if (nread == 0) {
    // If we get EOF, that means that the other side closed the fifo.  We need
    // to close and re-open the fifo; if we don't,
    // OnFileCanWriteWithoutBlocking will be called in an infinite loop.

    LOG("FifoWatcher closing and re-opening fifo.");
    StopWatching();
    StartWatching();
    return;
  }

  nsAutoCString inputStr;
  inputStr.Append(buf, nread);

  // Trimming whitespace is important because if you do
  //   |echo "foo" >> debug_info_trigger|,
  // it'll actually write "foo\n" to the fifo.
  inputStr.Trim("\b\t\r\n");

  {
    MutexAutoLock lock(mFifoInfoLock);

    for (FifoInfoArray::index_type i = 0; i < mFifoInfo.Length(); i++) {
      const nsCString commandStr = mFifoInfo[i].mCommand;
      if (inputStr == commandStr.get()) {
        mFifoInfo[i].mCallback(inputStr);
        return;
      }
    }
  }
  LOG("Got unexpected value from fifo; ignoring it.");
}

#endif  // XP_UNIX && !XP_IOS }

// In Android case, this function will open a file named aFilename under
// /data/local/tmp/"aFoldername".
// Otherwise, it will open a file named aFilename under "NS_OS_TEMP_DIR".
/* static */
nsresult nsDumpUtils::OpenTempFile(const nsACString& aFilename, nsIFile** aFile,
                                   const nsACString& aFoldername, Mode aMode) {
#ifdef ANDROID
  // For Android, first try the downloads directory which is world-readable
  // rather than the temp directory which is not.
  if (!*aFile) {
    if (char* env = PR_GetEnv("DOWNLOADS_DIRECTORY")) {
      (void)NS_WARN_IF(NS_FAILED(NS_NewNativeLocalFile(nsCString(env), aFile)));
    }
  }
#endif
  nsresult rv;
  if (!*aFile) {
    if (NS_IsMainThread()) {
      // This allows tests to override, but isn't safe off-mainthread.
      rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, aFile);
    } else {
      rv = GetSpecialSystemDirectory(OS_TemporaryDirectory, aFile);
    }
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }
  }

#ifdef ANDROID
  // /data/local/tmp is a true tmp directory; anyone can create a file there,
  // but only the user which created the file can remove it.  We want non-root
  // users to be able to remove these files, so we write them into a
  // subdirectory of the temp directory and chmod 777 that directory.
  if (!aFoldername.IsEmpty()) {
    rv = (*aFile)->AppendNative(aFoldername);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    // It's OK if this fails; that probably just means that the directory
    // already exists.
    (void)(*aFile)->Create(nsIFile::DIRECTORY_TYPE, 0777);

    nsAutoCString dirPath;
    rv = (*aFile)->GetNativePath(dirPath);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      return rv;
    }

    while (chmod(dirPath.get(), 0777) == -1 && errno == EINTR) {
    }
  }
#endif

  nsCOMPtr<nsIFile> file(*aFile);

  rv = file->AppendNative(aFilename);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  if (aMode == CREATE_UNIQUE) {
    rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0666);
  } else {
    rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0666);
  }
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

#ifdef ANDROID
  // Make this file world-read/writable; the permissions passed to the
  // CreateUnique call above are not sufficient on Android, which runs with a
  // umask.
  nsAutoCString path;
  rv = file->GetNativePath(path);
  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  while (chmod(path.get(), 0666) == -1 && errno == EINTR) {
  }
#endif

  return NS_OK;
}
