/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim:set ts=2 sw=2 sts=2 et cindent: */
/* 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 "nsSocketTransportService2.h"
#include "PollableEvent.h"
#include "mozilla/Assertions.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Logging.h"
#include "prerror.h"
#include "prio.h"
#include "private/pprio.h"
#include "prnetdb.h"

#ifdef XP_WIN
#include "ShutdownLayer.h"
#else
#include <fcntl.h>
#define USEPIPE 1
#endif

namespace mozilla {
namespace net {

#ifndef USEPIPE
static PRDescIdentity sPollableEventLayerIdentity;
static PRIOMethods    sPollableEventLayerMethods;
static PRIOMethods   *sPollableEventLayerMethodsPtr = nullptr;

static void LazyInitSocket()
{
  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
  if (sPollableEventLayerMethodsPtr) {
    return;
  }
  sPollableEventLayerIdentity = PR_GetUniqueIdentity("PollableEvent Layer");
  sPollableEventLayerMethods = *PR_GetDefaultIOMethods();
  sPollableEventLayerMethodsPtr = &sPollableEventLayerMethods;
}

static bool NewTCPSocketPair(PRFileDesc *fd[], bool aSetRecvBuff)
{
  // this is a replacement for PR_NewTCPSocketPair that manually
  // sets the recv buffer to 64K. A windows bug (1248358)
  // can result in using an incompatible rwin and window
  // scale option on localhost pipes if not set before connect.

  SOCKET_LOG(("NewTCPSocketPair %s a recv buffer tuning\n", aSetRecvBuff ? "with" : "without"));

  PRFileDesc *listener = nullptr;
  PRFileDesc *writer = nullptr;
  PRFileDesc *reader = nullptr;
  PRSocketOptionData recvBufferOpt;
  recvBufferOpt.option = PR_SockOpt_RecvBufferSize;
  recvBufferOpt.value.recv_buffer_size = 65535;

  PRSocketOptionData nodelayOpt;
  nodelayOpt.option = PR_SockOpt_NoDelay;
  nodelayOpt.value.no_delay = true;

  PRSocketOptionData noblockOpt;
  noblockOpt.option = PR_SockOpt_Nonblocking;
  noblockOpt.value.non_blocking = true;

  listener = PR_OpenTCPSocket(PR_AF_INET);
  if (!listener) {
    goto failed;
  }

  if (aSetRecvBuff) {
    PR_SetSocketOption(listener, &recvBufferOpt);
  }
  PR_SetSocketOption(listener, &nodelayOpt);

  PRNetAddr listenAddr;
  memset(&listenAddr, 0, sizeof(listenAddr));
  if ((PR_InitializeNetAddr(PR_IpAddrLoopback, 0, &listenAddr) == PR_FAILURE) ||
      (PR_Bind(listener, &listenAddr) == PR_FAILURE) ||
      (PR_GetSockName(listener, &listenAddr) == PR_FAILURE) || // learn the dynamic port
      (PR_Listen(listener, 5) == PR_FAILURE)) {
    goto failed;
  }

  writer = PR_OpenTCPSocket(PR_AF_INET);
  if (!writer) {
    goto failed;
  }
  if (aSetRecvBuff) {
    PR_SetSocketOption(writer, &recvBufferOpt);
  }
  PR_SetSocketOption(writer, &nodelayOpt);
  PR_SetSocketOption(writer, &noblockOpt);
  PRNetAddr writerAddr;
  if (PR_InitializeNetAddr(PR_IpAddrLoopback, ntohs(listenAddr.inet.port), &writerAddr) == PR_FAILURE) {
    goto failed;
  }

  if (PR_Connect(writer, &writerAddr, PR_INTERVAL_NO_TIMEOUT) == PR_FAILURE) {
    if ((PR_GetError() != PR_IN_PROGRESS_ERROR) ||
        (PR_ConnectContinue(writer, PR_POLL_WRITE) == PR_FAILURE)) {
      goto failed;
    }
  }

  reader = PR_Accept(listener, &listenAddr, PR_MillisecondsToInterval(200));
  if (!reader) {
    goto failed;
  }
  if (aSetRecvBuff) {
    PR_SetSocketOption(reader, &recvBufferOpt);
  }
  PR_SetSocketOption(reader, &nodelayOpt);
  PR_SetSocketOption(reader, &noblockOpt);
  PR_Close(listener);

  fd[0] = reader;
  fd[1] = writer;
  return true;

failed:
  if (listener) {
    PR_Close(listener);
  }
  if (reader) {
    PR_Close(reader);
  }
  if (writer) {
    PR_Close(writer);
  }
  return false;
}

#endif

PollableEvent::PollableEvent()
  : mWriteFD(nullptr)
  , mReadFD(nullptr)
  , mSignaled(false)
{
  MOZ_COUNT_CTOR(PollableEvent);
  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);
  // create pair of prfiledesc that can be used as a poll()ble
  // signal. on windows use a localhost socket pair, and on
  // unix use a pipe.
#ifdef USEPIPE
  SOCKET_LOG(("PollableEvent() using pipe\n"));
  if (PR_CreatePipe(&mReadFD, &mWriteFD) == PR_SUCCESS) {
    // make the pipe non blocking. NSPR asserts at
    // trying to use SockOpt here
    PROsfd fd = PR_FileDesc2NativeHandle(mReadFD);
    int flags = fcntl(fd, F_GETFL, 0);
    (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
    fd = PR_FileDesc2NativeHandle(mWriteFD);
    flags = fcntl(fd, F_GETFL, 0);
    (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
  } else {
    mReadFD = nullptr;
    mWriteFD = nullptr;
    SOCKET_LOG(("PollableEvent() pipe failed\n"));
  }
#else
  SOCKET_LOG(("PollableEvent() using socket pair\n"));
  PRFileDesc *fd[2];
  LazyInitSocket();

  // Try with a increased recv buffer first (bug 1248358).
  if (NewTCPSocketPair(fd, true)) {
    mReadFD = fd[0];
    mWriteFD = fd[1];
  // If the previous fails try without recv buffer increase (bug 1305436).
  } else if (NewTCPSocketPair(fd, false)) {
    mReadFD = fd[0];
    mWriteFD = fd[1];
  // If both fail, try the old version.
  } else if (PR_NewTCPSocketPair(fd) == PR_SUCCESS) {
    mReadFD = fd[0];
    mWriteFD = fd[1];

    PRSocketOptionData socket_opt;
    DebugOnly<PRStatus> status;
    socket_opt.option = PR_SockOpt_NoDelay;
    socket_opt.value.no_delay = true;
    PR_SetSocketOption(mWriteFD, &socket_opt);
    PR_SetSocketOption(mReadFD, &socket_opt);
    socket_opt.option = PR_SockOpt_Nonblocking;
    socket_opt.value.non_blocking = true;
    status = PR_SetSocketOption(mWriteFD, &socket_opt);
    MOZ_ASSERT(status == PR_SUCCESS);
    status = PR_SetSocketOption(mReadFD, &socket_opt);
    MOZ_ASSERT(status == PR_SUCCESS);
  }

  if (mReadFD && mWriteFD) {
    // compatibility with LSPs such as McAfee that assume a NSPR
    // layer for read ala the nspr Pollable Event - Bug 698882. This layer is a nop.
    PRFileDesc *topLayer =
      PR_CreateIOLayerStub(sPollableEventLayerIdentity,
                           sPollableEventLayerMethodsPtr);
    if (topLayer) {
      if (PR_PushIOLayer(fd[0], PR_TOP_IO_LAYER, topLayer) == PR_FAILURE) {
        topLayer->dtor(topLayer);
      } else {
        SOCKET_LOG(("PollableEvent() nspr layer ok\n"));
        mReadFD = topLayer;
      }
    }

  } else {
    SOCKET_LOG(("PollableEvent() socketpair failed\n"));
  }
#endif

  if (mReadFD && mWriteFD) {
    // prime the system to deal with races invovled in [dc]tor cycle
    SOCKET_LOG(("PollableEvent() ctor ok\n"));
    mSignaled = true;
    PR_Write(mWriteFD, "I", 1);
  }
}

PollableEvent::~PollableEvent()
{
  MOZ_COUNT_DTOR(PollableEvent);
  if (mWriteFD) {
#if defined(XP_WIN)
    AttachShutdownLayer(mWriteFD);
#endif
    PR_Close(mWriteFD);
  }
  if (mReadFD) {
#if defined(XP_WIN)
    AttachShutdownLayer(mReadFD);
#endif
    PR_Close(mReadFD);
  }
}

// we do not record signals on the socket thread
// because the socket thread can reliably look at its
// own runnable queue before selecting a poll time
// this is the "service the network without blocking" comment in
// nsSocketTransportService2.cpp
bool
PollableEvent::Signal()
{
  SOCKET_LOG(("PollableEvent::Signal\n"));

  if (!mWriteFD) {
    SOCKET_LOG(("PollableEvent::Signal Failed on no FD\n"));
    return false;
  }
#ifndef XP_WIN
  // On windows poll can hang and this became worse when we introduced the
  // patch for bug 698882 (see also bug 1292181), therefore we reverted the
  // behavior on windows to be as before bug 698882, e.g. write to the socket
  // also if an event dispatch is on the socket thread and writing to the
  // socket for each event. See bug 1292181.
  if (PR_GetCurrentThread() == gSocketThread) {
    SOCKET_LOG(("PollableEvent::Signal OnSocketThread nop\n"));
    return true;
  }
#endif

#ifndef XP_WIN
  // To wake up the poll writing once is enough, but for Windows that can cause
  // hangs so we will write for every event.
  // For non-Windows systems it is enough to write just once.
  if (mSignaled) {
    return true;
  }
#endif

  mSignaled = true;
  int32_t status = PR_Write(mWriteFD, "M", 1);
  SOCKET_LOG(("PollableEvent::Signal PR_Write %d\n", status));
  if (status != 1) {
    NS_WARNING("PollableEvent::Signal Failed\n");
    SOCKET_LOG(("PollableEvent::Signal Failed\n"));
    mSignaled = false;
  }
  return (status == 1);
}

bool
PollableEvent::Clear()
{
  // necessary because of the "dont signal on socket thread" optimization
  MOZ_ASSERT(PR_GetCurrentThread() == gSocketThread);

  SOCKET_LOG(("PollableEvent::Clear\n"));
  mSignaled = false;
  if (!mReadFD) {
    SOCKET_LOG(("PollableEvent::Clear mReadFD is null\n"));
    return false;
  }
  char buf[2048];
  int32_t status;
#ifdef XP_WIN
  // On Windows we are writing to the socket for each event, to be sure that we
  // do not have any deadlock read from the socket as much as we can.
  while (true) {
    status = PR_Read(mReadFD, buf, 2048);
    SOCKET_LOG(("PollableEvent::Signal PR_Read %d\n", status));
    if (status == 0) {
      SOCKET_LOG(("PollableEvent::Clear EOF!\n"));
      return false;
    }
    if (status < 0) {
      PRErrorCode code = PR_GetError();
      if (code == PR_WOULD_BLOCK_ERROR) {
        return true;
      } else {
        SOCKET_LOG(("PollableEvent::Clear unexpected error %d\n", code));
        return false;
      }
    }
  }
#else
  status = PR_Read(mReadFD, buf, 2048);
  SOCKET_LOG(("PollableEvent::Signal PR_Read %d\n", status));

  if (status == 1) {
    return true;
  }
  if (status == 0) {
    SOCKET_LOG(("PollableEvent::Clear EOF!\n"));
    return false;
  }
  if (status > 1) {
    MOZ_ASSERT(false);
    SOCKET_LOG(("PollableEvent::Clear Unexpected events\n"));
    Clear();
    return true;
  }
  PRErrorCode code = PR_GetError();
  if (code == PR_WOULD_BLOCK_ERROR) {
    return true;
  }
  SOCKET_LOG(("PollableEvent::Clear unexpected error %d\n", code));
  return false;
#endif //XP_WIN

}
} // namespace net
} // namespace mozilla