/* 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 "OpenFileFinder.h"

#include "mozilla/FileUtils.h"
#include "nsPrintfCString.h"

#include <sys/stat.h>
#include <errno.h>

#undef USE_DEBUG
#define USE_DEBUG 0

#undef LOG
#undef LOGW
#undef ERR
#define LOG(args...)  __android_log_print(ANDROID_LOG_INFO,  "OpenFileFinder", ## args)
#define LOGW(args...) __android_log_print(ANDROID_LOG_WARN,  "OpenFileFinder", ## args)
#define ERR(args...)  __android_log_print(ANDROID_LOG_ERROR, "OpenFileFinder", ## args)

#undef DBG
#if USE_DEBUG
#define DBG(args...)  __android_log_print(ANDROID_LOG_DEBUG, "OpenFileFinder" , ## args)
#else
#define DBG(args...)
#endif

namespace mozilla {
namespace system {

OpenFileFinder::OpenFileFinder(const nsACString& aPath,
                               bool aCheckIsB2gOrDescendant /* = true */)
  : mPath(aPath),
    mProcDir(nullptr),
    mFdDir(nullptr),
    mPid(0),
    mCheckIsB2gOrDescendant(aCheckIsB2gOrDescendant)
{
  // We assume that we're running in the parent process
  mMyPid = getpid();
}

OpenFileFinder::~OpenFileFinder()
{
  Close();
}

bool
OpenFileFinder::First(OpenFileFinder::Info* aInfo)
{
  Close();

  mProcDir = opendir("/proc");
  if (!mProcDir) {
    return false;
  }
  mState = NEXT_PID;
  return Next(aInfo);
}

bool
OpenFileFinder::Next(OpenFileFinder::Info* aInfo)
{
  // NOTE: This function calls readdir and readlink, neither of which should
  //       block since we're using the proc filesystem, which is a purely
  // kernel in-memory filesystem and doesn't depend on external driver
  // behaviour.
  while (mState != DONE) {
    switch (mState) {
      case NEXT_PID: {
        struct dirent *pidEntry;
        pidEntry = readdir(mProcDir);
        if (!pidEntry) {
          mState = DONE;
          break;
        }
        char *endPtr;
        mPid = strtol(pidEntry->d_name, &endPtr, 10);
        if (mPid == 0 || *endPtr != '\0') {
          // Not a +ve number - ignore
          continue;
        }
        // We've found a /proc/PID directory. Scan open file descriptors.
        if (mFdDir) {
          closedir(mFdDir);
        }
        nsPrintfCString fdDirPath("/proc/%d/fd", mPid);
        mFdDir = opendir(fdDirPath.get());
        if (!mFdDir) {
          continue;
        }
        mState = CHECK_FDS;
      }
      // Fall through
      case CHECK_FDS: {
        struct dirent *fdEntry;
        while((fdEntry = readdir(mFdDir))) {
          if (!strcmp(fdEntry->d_name, ".") ||
              !strcmp(fdEntry->d_name, "..")) {
            continue;
          }
          nsPrintfCString fdSymLink("/proc/%d/fd/%s", mPid, fdEntry->d_name);
          nsCString resolvedPath;
          if (ReadSymLink(fdSymLink, resolvedPath) && PathMatches(resolvedPath)) {
            // We found an open file contained within the directory tree passed
            // into the constructor.
            FillInfo(aInfo, resolvedPath);
            // If sCheckIsB2gOrDescendant is set false, the caller cares about
            // all processes which have open files. If sCheckIsB2gOrDescendant
            // is set false, we only care about the b2g proccess or its descendants.
            if (!mCheckIsB2gOrDescendant || aInfo->mIsB2gOrDescendant) {
              return true;
            }
            LOG("Ignore process(%d), not a b2g process or its descendant.",
                aInfo->mPid);
          }
        }
        // We've checked all of the files for this pid, move onto the next one.
        mState = NEXT_PID;
        continue;
      }
      case DONE:
      default:
        mState = DONE;  // covers the default case
        break;
    }
  }
  return false;
}

void
OpenFileFinder::Close()
{
  if (mFdDir) {
    closedir(mFdDir);
  }
  if (mProcDir) {
    closedir(mProcDir);
  }
}

void
OpenFileFinder::FillInfo(OpenFileFinder::Info* aInfo, const nsACString& aPath)
{
  aInfo->mFileName = aPath;
  aInfo->mPid = mPid;
  nsPrintfCString exePath("/proc/%d/exe", mPid);
  ReadSymLink(exePath, aInfo->mExe);
  aInfo->mComm.Truncate();
  aInfo->mAppName.Truncate();
  nsPrintfCString statPath("/proc/%d/stat", mPid);
  nsCString statString;
  statString.SetLength(200);
  char *stat = statString.BeginWriting();
  if (!stat) {
    return;
  }
  ReadSysFile(statPath.get(), stat, statString.Length());
  // The stat line includes the comm field, surrounded by parenthesis.
  // However, the contents of the comm field itself is arbitrary and
  // and can include ')', so we search for the rightmost ) as being
  // the end of the comm field.
  char *closeParen = strrchr(stat, ')');
  if (!closeParen) {
    return;
  }
  char *openParen = strchr(stat, '(');
  if (!openParen) {
    return;
  }
  if (openParen >= closeParen) {
    return;
  }
  nsDependentCSubstring comm(&openParen[1], closeParen - openParen - 1);
  aInfo->mComm = comm;
  // There is a single character field after the comm and then
  // the parent pid (the field we're interested in).
  // ) X ppid
  // 01234
  int ppid = atoi(&closeParen[4]);

  if (mPid == mMyPid) {
    // This is chrome process
    aInfo->mIsB2gOrDescendant = true;
    DBG("Chrome process has open file(s)");
    return;
  }
  // For the rest (non-chrome process), we recursively check the ppid to know
  // it is a descendant of b2g or not. See bug 931456.
  while (ppid != mMyPid && ppid != 1) {
    DBG("Process(%d) is not forked from b2g(%d) or Init(1), keep looking",
        ppid, mMyPid);
    nsPrintfCString ppStatPath("/proc/%d/stat", ppid);
    ReadSysFile(ppStatPath.get(), stat, statString.Length());
    closeParen = strrchr(stat, ')');
    if (!closeParen) {
      return;
    }
    ppid = atoi(&closeParen[4]);
  }
  if (ppid == 1) {
    // This is a not a b2g process.
    DBG("Non-b2g process has open file(s)");
    aInfo->mIsB2gOrDescendant = false;
    return;
  }
  if (ppid == mMyPid) {
    // This is a descendant of b2g.
    DBG("Child process of chrome process has open file(s)");
    aInfo->mIsB2gOrDescendant = true;
  }

  // This looks like a content process. The comm field will be the
  // app name.
  aInfo->mAppName = aInfo->mComm;
}

bool
OpenFileFinder::ReadSymLink(const nsACString& aSymLink, nsACString& aOutPath)
{
  aOutPath.Truncate();
  const char *symLink = aSymLink.BeginReading();

  // Verify that we actually have a symlink.
  struct stat st;
  if (lstat(symLink, &st)) {
      return false;
  }
  if ((st.st_mode & S_IFMT) != S_IFLNK) {
      return false;
  }

  // Contrary to the documentation st.st_size doesn't seem to be a reliable
  // indication of the length when reading from /proc, so we use a fixed
  // size buffer instead.

  char resolvedSymLink[PATH_MAX];
  ssize_t pathLength = readlink(symLink, resolvedSymLink,
                                sizeof(resolvedSymLink) - 1);
  if (pathLength <= 0) {
      return false;
  }
  resolvedSymLink[pathLength] = '\0';
  aOutPath.Assign(resolvedSymLink);
  return true;
}

} // system
} // mozilla