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

#include "commonupdatedir.h"  // for GetInstallHash
#include "mozilla/UniquePtr.h"
#include "nsPrintfCString.h"
#include "nsPromiseFlatString.h"
#include "nsXULAppAPI.h"
#include "updatedefines.h"  // for NS_t* definitions

#ifdef XP_WIN
#  include <shlwapi.h>
#else
#  include <fcntl.h>
#  include <sys/stat.h>
#  include <sys/types.h>
#endif

#ifdef XP_WIN
#  include "WinUtils.h"
#endif

#ifdef MOZ_WIDGET_COCOA
#  include "nsILocalFileMac.h"
#endif

namespace mozilla {

bool GetMultiInstanceLockFileName(const char* nameToken,
                                  const char16_t* installPath,
                                  nsCString& filePath) {
#ifdef XP_WIN
  // On Windows, the lock file is placed at the path
  // [updateDirectory]\[nameToken]-[pathHash], so first we need to get the
  // update directory path and then append the file name.

  // Note: This will return something like
  //   C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38\updates\<hash>
  // But we actually are going to want to return the root update directory,
  // the grandparent of this directory, which will look something like this:
  //   C:\ProgramData\Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38
  mozilla::UniquePtr<wchar_t[]> updateDir;
  HRESULT hr = GetCommonUpdateDirectory(
      reinterpret_cast<const wchar_t*>(installPath), updateDir);
  if (FAILED(hr)) {
    return false;
  }

  // For the path manipulation that we are about to do, it is important that
  // the update directory have no trailing slash.
  size_t len = wcslen(updateDir.get());
  if (len == 0) {
    return false;
  }
  if (updateDir.get()[len - 1] == '/' || updateDir.get()[len - 1] == '\\') {
    updateDir.get()[len - 1] = '\0';
  }

  wchar_t* hashPtr = PathFindFileNameW(updateDir.get());
  // PathFindFileNameW returns a pointer to the beginning of the string on
  // failure.
  if (hashPtr == updateDir.get()) {
    return false;
  }

  // We need to make a copy of the hash before we modify updateDir to get the
  // root update dir.
  size_t hashSize = wcslen(hashPtr) + 1;
  mozilla::UniquePtr<wchar_t[]> hash = mozilla::MakeUnique<wchar_t[]>(hashSize);
  errno_t error = wcscpy_s(hash.get(), hashSize, hashPtr);
  if (error != 0) {
    return false;
  }

  // Get the root update dir from the update dir.
  BOOL success = PathRemoveFileSpecW(updateDir.get());
  if (!success) {
    return false;
  }
  success = PathRemoveFileSpecW(updateDir.get());
  if (!success) {
    return false;
  }

  filePath =
      nsPrintfCString("%s\\%s-%s", NS_ConvertUTF16toUTF8(updateDir.get()).get(),
                      nameToken, NS_ConvertUTF16toUTF8(hash.get()).get());

#else
  mozilla::UniquePtr<NS_tchar[]> pathHash;
  if (!GetInstallHash(installPath, pathHash)) {
    return false;
  }

  // On POSIX platforms the base path is /tmp/[vendor][nameToken]-[pathHash].
  filePath = nsPrintfCString("/tmp/%s%s-%s", MOZ_APP_VENDOR, nameToken,
                             pathHash.get());

#endif

  return true;
}

MultiInstLockHandle OpenMultiInstanceLock(const char* nameToken,
                                          const char16_t* installPath) {
  nsCString filePath;
  if (!GetMultiInstanceLockFileName(nameToken, installPath, filePath)) {
    return MULTI_INSTANCE_LOCK_HANDLE_ERROR;
  }

  // Open a file handle with full privileges and sharing, and then attempt to
  // take a shared (nonexclusive, read-only) lock on it.
#ifdef XP_WIN
  HANDLE h =
      ::CreateFileW(PromiseFlatString(NS_ConvertUTF8toUTF16(filePath)).get(),
                    GENERIC_READ | GENERIC_WRITE,
                    FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
                    nullptr, OPEN_ALWAYS, 0, nullptr);
  if (h != INVALID_HANDLE_VALUE) {
    // The LockFileEx functions always require an OVERLAPPED structure even
    // though we did not open the lock file for overlapped I/O.
    OVERLAPPED o = {0};
    if (!::LockFileEx(h, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) {
      CloseHandle(h);
      h = INVALID_HANDLE_VALUE;
    }
  }
  return h;

#else
  int fd = ::open(PromiseFlatCString(filePath).get(),
                  O_CLOEXEC | O_CREAT | O_NOFOLLOW,
                  S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
  if (fd != -1) {
    // We would like to ensure that the lock file is deleted when we are done
    // with it. The normal way to do that would be to call unlink on it right
    // now, but that would immediately delete the name from the file system, and
    // we need other instances to be able to open that name and get the same
    // inode, so we can't unlink the file before we're done with it. This means
    // we accept some unreliability in getting the file deleted, but it's a zero
    // byte file in the tmp directory, so having it stay around isn't the worst.
    struct flock l = {0};
    l.l_start = 0;
    l.l_len = 0;
    l.l_type = F_RDLCK;
    if (::fcntl(fd, F_SETLK, &l)) {
      ::close(fd);
      fd = -1;
    }
  }
  return fd;

#endif
}

void ReleaseMultiInstanceLock(MultiInstLockHandle lock) {
  if (lock != MULTI_INSTANCE_LOCK_HANDLE_ERROR) {
#ifdef XP_WIN
    OVERLAPPED o = {0};
    ::UnlockFileEx(lock, 0, 1, 0, &o);
    ::CloseHandle(lock);

#else
    // If we're the last instance, then unlink the lock file. There is a race
    // condition here that may cause an instance to fail to open the same inode
    // as another even though they use the same path, but there's no reasonable
    // way to avoid that without skipping deleting the file at all, so we accept
    // that risk.
    bool otherInstance = true;
    if (IsOtherInstanceRunning(lock, &otherInstance) && !otherInstance) {
      // Recover the file's path so we can unlink it.
      // There's no error checking in here because we're content to let the file
      // hang around if any of this fails (which can happen if for example we're
      // on a system where /proc/self/fd does not exist); this is a zero-byte
      // file in the tmp directory after all.
      UniquePtr<NS_tchar[]> linkPath = MakeUnique<NS_tchar[]>(MAXPATHLEN + 1);
      NS_tsnprintf(linkPath.get(), MAXPATHLEN + 1, "/proc/self/fd/%d", lock);
      UniquePtr<NS_tchar[]> lockFilePath =
          MakeUnique<NS_tchar[]>(MAXPATHLEN + 1);
      if (::readlink(linkPath.get(), lockFilePath.get(), MAXPATHLEN + 1) !=
          -1) {
        ::unlink(lockFilePath.get());
      }
    }
    // Now close the lock file, which will release the lock.
    ::close(lock);
#endif
  }
}

bool IsOtherInstanceRunning(MultiInstLockHandle lock, bool* aResult) {
  // Every running instance has opened a readonly lock, and read locks prevent
  // write locks from being opened, so to see if we are the only instance, we
  // attempt to take a write lock, and if it succeeds then that must mean there
  // are no other read locks open and therefore no other instances.
  if (lock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) {
    return false;
  }

#ifdef XP_WIN
  // We need to release the lock we're holding before we would be allowed to
  // take an exclusive lock, and if that succeeds we need to release it too
  // in order to get our shared lock back. This procedure is not atomic, so we
  // accept the risk of the scheduler deciding to ruin our day between these
  // operations; we'd get a false negative in a different instance's check.
  OVERLAPPED o = {0};
  // Release our current shared lock.
  if (!::UnlockFileEx(lock, 0, 1, 0, &o)) {
    return false;
  }
  // Attempt to take an exclusive lock. A failure with ERROR_LOCK_VIOLATION
  // normally means another instance is holding a shared lock, but it can also
  // be a false positive: an unrelated process (antivirus, the search indexer,
  // an EDR agent) briefly opening the just-created lock file for scanning holds
  // a transient shared handle that blocks our exclusive lock for a moment. To
  // avoid spuriously reporting another instance, retry a few times with a short
  // backoff so such transient handles get a chance to close.
  constexpr int kMaxAttempts = 5;
  bool rv = false;
  for (int attempt = 0; attempt < kMaxAttempts; ++attempt) {
    if (::LockFileEx(lock, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY,
                     0, 1, 0, &o)) {
      // We got the exclusive lock, so now release it.
      ::UnlockFileEx(lock, 0, 1, 0, &o);
      *aResult = false;
      rv = true;
      break;
    }
    if (::GetLastError() != ERROR_LOCK_VIOLATION) {
      // An unexpected failure; we can't determine whether another instance is
      // running, so leave rv false to report that.
      break;
    }
    // Outstanding shared lock: assume another instance, but keep retrying in
    // case it is a transient handle that will go away.
    *aResult = true;
    rv = true;
    if (attempt + 1 < kMaxAttempts) {
      ::Sleep(10);
    }
  }
  // Attempt to reclaim the shared lock we released at the beginning.
  if (!::LockFileEx(lock, LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &o)) {
    rv = false;
  }
  return rv;

#else
  // See if we would be allowed to set a write lock (no need to actually do so).
  struct flock l = {0};
  l.l_start = 0;
  l.l_len = 0;
  l.l_type = F_WRLCK;
  if (::fcntl(lock, F_GETLK, &l)) {
    return false;
  }
  *aResult = l.l_type != F_UNLCK;
  return true;

#endif
}

already_AddRefed<nsIFile> GetNormalizedAppFile(nsIFile* aAppFile) {
  // If we're given an app file, use it; otherwise, get it from the ambient
  // directory service.
  nsresult rv;
  nsCOMPtr<nsIFile> appFile;
  if (aAppFile) {
    rv = aAppFile->Clone(getter_AddRefs(appFile));
    NS_ENSURE_SUCCESS(rv, nullptr);
  } else {
    nsCOMPtr<nsIProperties> dirSvc =
        do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
    NS_ENSURE_TRUE(dirSvc, nullptr);

    rv = dirSvc->Get(XRE_EXECUTABLE_FILE, NS_GET_IID(nsIFile),
                     getter_AddRefs(appFile));
    NS_ENSURE_SUCCESS(rv, nullptr);
  }

  // It is possible that the path we have is on a case insensitive
  // filesystem in which case the path may vary depending on how the
  // application is called. We want to normalize the case somehow.
  // On Linux XRE_EXECUTABLE_FILE already seems to be set to the correct path.
  //
  // See similar nsXREDirProvider::GetInstallHash. The main difference here is
  // to allow lookup to fail on OSX, because some tests use a nonexistent
  // appFile.
#ifdef XP_WIN
  // Windows provides a way to get the correct case.
  if (!mozilla::widget::WinUtils::ResolveJunctionPointsAndSymLinks(appFile)) {
    NS_WARNING("Failed to resolve install directory.");
  }
#elif defined(MOZ_WIDGET_COCOA)
  // On OSX roundtripping through an FSRef fixes the case.
  FSRef ref;
  nsCOMPtr<nsILocalFileMac> macFile = do_QueryInterface(appFile);
  if (macFile && NS_SUCCEEDED(macFile->GetFSRef(&ref)) &&
      NS_SUCCEEDED(NS_NewLocalFileWithFSRef(&ref, getter_AddRefs(macFile)))) {
    appFile = macFile.forget();
  } else {
    NS_WARNING("Failed to resolve install directory.");
  }
#endif

  return appFile.forget();
}

};  // namespace mozilla
