/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "AsyncPlatformPipes.h"

#include "base/eintr_wrapper.h"
#include "base/message_loop.h"
#include "mozilla/CondVar.h"
#include "mozilla/EventTargetCapability.h"
#include "mozilla/UniquePtr.h"
#include "nsStreamUtils.h"
#include "nsThreadUtils.h"
#include "nsXULAppAPI.h"

#ifndef XP_WIN
#  include <errno.h>
#  include <fcntl.h>
#  include <sys/stat.h>
#  include <unistd.h>
#endif

namespace mozilla {

namespace platform_pipe_detail {

class PlatformPipeLink
#ifdef XP_WIN
    : public MessageLoopForIO::IOHandler
#else
    : public MessageLoopForIO::Watcher
#endif
{
  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PlatformPipeLink)

 public:
  PlatformPipeLink(UniqueFileHandle aHandle, uint32_t aBufferSize);

  // Clears mPending to nullptr, returning the previous value. Also notifies
  // mPendingCV in case there are any threads synchronously waiting for pending
  // I/O to complete.
  already_AddRefed<PlatformPipeLink> TakePending() MOZ_REQUIRES(mMutex);

  // NOTE: This performs potentially-blocking I/O to close mHandle, so should
  // not be called on the IO thread!
  void Close(nsresult aStatus, bool aInternal) MOZ_EXCLUDES(mMutex)
      MOZ_EXCLUDES(mIOThread);

  // Dispatch an internal Close(...) call to be performed in a background task.
  // Called when background I/O operations fail.
  void DispatchPipeError(nsresult aStatus) MOZ_REQUIRES(mMutex, mIOThread);

  // Dispatch notification of mCallback to another thread.
  //
  // NOTE: We intentionally do not notify directly on the current thread, even
  // if no event target is provided, as we don't want user code running on the
  // IPC I/O thread as it is very latency sensitive.
  void DispatchNotify() MOZ_REQUIRES(mMutex);

  void AdvanceIO() MOZ_EXCLUDES(mMutex) MOZ_REQUIRES(mIOThread);

  void AdvanceIOLocked() MOZ_REQUIRES(mMutex, mIOThread);

#ifdef XP_WIN
  void OnIOCompleted(MessageLoopForIO::IOContext* aContext,
                     DWORD aBytesTransferred, DWORD aError) override;
#else
  void OnFileCanReadWithoutBlocking(int fd) override;
  void OnFileCanWriteWithoutBlocking(int fd) override;
#endif

  const EventTargetCapability<nsISerialEventTarget> mIOThread;
  Mutex mMutex{"PlatformPipeReader"};

  UniqueFileHandle mHandle MOZ_GUARDED_BY(mMutex);
  const UniquePtr<char[]> mBuffer;
  const uint32_t mBufferSize;

  bool mProcessingSegment MOZ_GUARDED_BY(mMutex) = false;
  bool mClosing MOZ_GUARDED_BY(mMutex) = false;

  nsresult mStatus MOZ_GUARDED_BY(mMutex) = NS_OK;

  // `mOffset` is the start of the readable region in the buffer, and
  // `mAvailable` is its size.
  uint32_t mOffset MOZ_GUARDED_BY(mMutex) = 0;
  uint32_t mAvailable MOZ_GUARDED_BY(mMutex) = 0;

  bool mCallbackClosureOnly MOZ_GUARDED_BY(mMutex) = false;
  nsCOMPtr<nsIRunnable> mCallback MOZ_GUARDED_BY(mMutex);
  nsCOMPtr<nsIEventTarget> mCallbackTarget MOZ_GUARDED_BY(mMutex);

  // A reference keeping `this` alive while I/O is in-flight.
  // This is particularly important on Windows where mBuffer needs to be kept
  // alive while Overlapped IO is ongoing.
  RefPtr<PlatformPipeLink> mPending MOZ_GUARDED_BY(mMutex);

  // CondVar which is notified whenever mPending is cleared to `nullptr`.
  // Used by Close() to wait for pending I/O to complete.
  CondVar mPendingCV{mMutex, "PlatformPipeReader::mPendingCV"};

#ifdef XP_WIN
  MessageLoopForIO::IOContext mIOContext MOZ_GUARDED_BY(mMutex) = {};
#else
  MessageLoopForIO::FileDescriptorWatcher mWatcher MOZ_GUARDED_BY(mIOThread);
#endif

 private:
  ~PlatformPipeLink() = default;
};

PlatformPipeLink::PlatformPipeLink(UniqueFileHandle aHandle,
                                   uint32_t aBufferSize)
    : mIOThread(XRE_GetAsyncIOEventTarget()),
      mHandle(std::move(aHandle)),
      mBuffer(MakeUnique<char[]>(aBufferSize)),
      mBufferSize(aBufferSize) {
  MOZ_ASSERT(aBufferSize > 1, "invalid buffer size");
  MOZ_ASSERT(mHandle, "invalid handle");

#if defined(DEBUG) && !defined(XP_WIN)
  struct stat st{};
  MOZ_ASSERT(fstat(mHandle.get(), &st) == 0 && !S_ISREG(st.st_mode),
             "PlatformPipeLink does not support regular files");
  MOZ_ASSERT(fcntl(mHandle.get(), F_GETFL) & O_NONBLOCK,
             "PlatformPipeLink requires non-blocking file descriptors");
#endif
}

already_AddRefed<PlatformPipeLink> PlatformPipeLink::TakePending() {
  RefPtr<PlatformPipeLink> pending = mPending.forget();
  if (pending) {
    mPendingCV.NotifyAll();
  }
  return pending.forget();
}

void PlatformPipeLink::Close(nsresult aStatus, bool aInternal) {
  MOZ_RELEASE_ASSERT(!mIOThread.IsOnCurrentThread(),
                     "Close may deadlock if called on the IO thread");

  MutexAutoLock lock(mMutex);
  MOZ_RELEASE_ASSERT(aInternal || !mProcessingSegment,
                     "Cannot close pipe during ReadSegments callback");
  if (NS_FAILED(mStatus)) {
    return;
  }

  // Ensure we're marked as closing before we cancel pending IO, this prevents
  // new pending IO from being started.
  mClosing = true;

  if (mPending) {
#ifdef XP_WIN
    // On Windows, if we still have pending IO, request that it is cancelled.
    // We can do that from any thread using CancelIoEx. The notification will be
    // delivered to the IO thread.
    CancelIoEx(mHandle.get(), &mIOContext.overlapped);
#else
    // On POSIX, request cancellation by dispatching a runnable to stop watching
    // our file descriptor, and clear mPending.
    MOZ_ALWAYS_SUCCEEDS(mIOThread.Dispatch(NS_NewRunnableFunction(
        "PlatformPipeLink::CancelIO", [self = RefPtr{this}] {
          self->mIOThread.AssertOnCurrentThread();
          RefPtr<PlatformPipeLink> pending;
          MutexAutoLock lock(self->mMutex);
          if (self->mPending) {
            self->mWatcher.StopWatchingFileDescriptor();
            pending = self->TakePending();
          }
        })));
#endif

    // Wait on the condvar until all pending operations are completed.
    // NOTE: This unlocks mMutex while we wait.
    while (mPending) {
      mPendingCV.Wait();
    }
  }
  MOZ_DIAGNOSTIC_ASSERT(!mPending, "How do we still have pending I/O?");

  // Someone else may have closed the link while we were unlocked.
  if (NS_FAILED(mStatus)) {
    return;
  }

  mStatus = NS_SUCCEEDED(aStatus) ? NS_BASE_STREAM_CLOSED : aStatus;
  DispatchNotify();

  // NOTE: Make sure we've closed the handle synchronously here, as callers rely
  // on the kernel file object being destroyed.
  mHandle = nullptr;
}

void PlatformPipeLink::DispatchPipeError(nsresult aStatus) {
  MOZ_ASSERT(!mPending,
             "Shouldn't be pending when closing due to a pipe error");

  // Ensure other threads don't start new I/O after this point.
  // Don't actually change mStatus until we're in the Close method.
  mClosing = true;

  // Perform the Close() operation on a background task which is allowed to
  // block. This operation may be blocking, as it closes the pipe, which could
  // do expensive I/O.
  NS_DispatchBackgroundTask(
      NewRunnableMethod<nsresult, bool>("PlatformPipeLink::Close", this,
                                        &PlatformPipeLink::Close, aStatus,
                                        /* aInternal */ true),
      NS_DISPATCH_EVENT_MAY_BLOCK);
}

void PlatformPipeLink::DispatchNotify() {
  nsCOMPtr<nsIRunnable> callback = mCallback.forget();
  nsCOMPtr<nsIEventTarget> target = mCallbackTarget.forget();
  if (!callback) {
    return;
  }
  if (target) {
    target->Dispatch(callback.forget());
  } else {
    NS_DispatchBackgroundTask(callback.forget());
  }
}

void PlatformPipeLink::AdvanceIO() {
  MutexAutoLock lock(mMutex);
  AdvanceIOLocked();
}

void PlatformPipeLink::AdvanceIOLocked() {
  // If we've closed, or are in the process of closing, our PlatformPipeLink,
  // don't start any new I/O operations.
  if (mClosing || !mHandle || NS_FAILED(mStatus)) {
    return;
  }

  // We still have outstanding I/O, or our buffer already has data waiting to
  // be consumed. Either way, don't start new I/O.
  if (mPending || mAvailable) {
    return;
  }

#ifdef XP_WIN
  // On Windows, we need to register the IO handler the first time we're on the
  // I/O thread.
  if (!mIOContext.handler) {
    MessageLoopForIO::current()->RegisterIOHandler(mHandle.get(), this);
    mIOContext.handler = this;
  }
  BOOL ok = ReadFile(mHandle.get(), mBuffer.get(), mBufferSize, nullptr,
                     &mIOContext.overlapped);
  if (!ok) {
    DWORD error = GetLastError();
    if (error == ERROR_IO_PENDING) {
      mPending = this;
      return;
    }
    if (error == ERROR_BROKEN_PIPE || error == ERROR_HANDLE_EOF) {
      DispatchPipeError(NS_BASE_STREAM_CLOSED);
    } else {
      DispatchPipeError(NS_ERROR_FAILURE);
    }
    return;
  }

  mPending = this;
#else
  ssize_t rv = HANDLE_EINTR(read(mHandle.get(), mBuffer.get(), mBufferSize));
  if (rv > 0) {
    mOffset = 0;
    mAvailable = static_cast<uint32_t>(rv);
    if (!mCallbackClosureOnly) {
      DispatchNotify();
    }
    return;
  }

  if (rv == 0) {
    DispatchPipeError(NS_BASE_STREAM_CLOSED);
    return;
  }

  if (errno == EAGAIN
#  if EWOULDBLOCK != EAGAIN
      || errno == EWOULDBLOCK
#  endif
  ) {
    if (MessageLoopForIO::current()->WatchFileDescriptor(
            mHandle.get(), false, MessageLoopForIO::WATCH_READ, &mWatcher,
            this)) {
      mPending = this;
      return;
    }
  }

  DispatchPipeError(NS_ERROR_FAILURE);
#endif
}

#ifdef XP_WIN
void PlatformPipeLink::OnIOCompleted(MessageLoopForIO::IOContext* aContext,
                                     DWORD aBytesTransferred, DWORD aError) {
  mIOThread.AssertOnCurrentThread();
  RefPtr<PlatformPipeLink> pending;
  MutexAutoLock lock(mMutex);
  if (aContext != &mIOContext) {
    return;
  }

  pending = TakePending();
  if (!pending) {
    return;
  }

  if (mClosing || NS_FAILED(mStatus)) {
    return;
  }

  if (aError != ERROR_SUCCESS) {
    if (aError == ERROR_BROKEN_PIPE || aError == ERROR_HANDLE_EOF ||
        aError == ERROR_OPERATION_ABORTED) {
      DispatchPipeError(NS_BASE_STREAM_CLOSED);
    } else {
      DispatchPipeError(NS_ERROR_FAILURE);
    }
    return;
  }

  if (aBytesTransferred == 0) {
    DispatchPipeError(NS_BASE_STREAM_CLOSED);
    return;
  }

  mOffset = 0;
  mAvailable = aBytesTransferred;
  if (!mCallbackClosureOnly) {
    DispatchNotify();
  }
}
#else
void PlatformPipeLink::OnFileCanReadWithoutBlocking(int fd) {
  mIOThread.AssertOnCurrentThread();
  RefPtr<PlatformPipeLink> pending;
  MutexAutoLock lock(mMutex);
  pending = TakePending();
  AdvanceIOLocked();
}

void PlatformPipeLink::OnFileCanWriteWithoutBlocking(int fd) {
  MOZ_ASSERT_UNREACHABLE();
}
#endif

}  // namespace platform_pipe_detail

//-----------------------------------------------------------------------------
// PlatformPipeReader
//-----------------------------------------------------------------------------

NS_IMPL_ISUPPORTS(PlatformPipeReader, nsIInputStream, nsIAsyncInputStream)

PlatformPipeReader::PlatformPipeReader(UniqueFileHandle aHandle,
                                       uint32_t aBufferSize)
    : mLink(new platform_pipe_detail::PlatformPipeLink(std::move(aHandle),
                                                       aBufferSize)) {}

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

NS_IMETHODIMP PlatformPipeReader::Close() {
  return CloseWithStatus(NS_BASE_STREAM_CLOSED);
}

NS_IMETHODIMP PlatformPipeReader::Available(uint64_t* aAvailable) {
  MutexAutoLock lock(mLink->mMutex);
  if (NS_FAILED(mLink->mStatus)) {
    return mLink->mStatus;
  }
  *aAvailable = mLink->mAvailable;
  return NS_OK;
}

NS_IMETHODIMP PlatformPipeReader::StreamStatus() {
  MutexAutoLock lock(mLink->mMutex);
  return mLink->mStatus;
}

NS_IMETHODIMP PlatformPipeReader::Read(char* aBuf, uint32_t aCount,
                                       uint32_t* aReadCount) {
  return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, aReadCount);
}

NS_IMETHODIMP PlatformPipeReader::ReadSegments(nsWriteSegmentFun aWriter,
                                               void* aClosure, uint32_t aCount,
                                               uint32_t* aReadCount) {
  *aReadCount = 0;

  MutexAutoLock lock(mLink->mMutex);
  if (NS_FAILED(mLink->mStatus)) {
    return mLink->mStatus == NS_BASE_STREAM_CLOSED ? NS_OK : mLink->mStatus;
  }

  if (!mLink->mAvailable) {
    return NS_BASE_STREAM_WOULD_BLOCK;
  }

  MOZ_RELEASE_ASSERT(!mLink->mProcessingSegment,
                     "Only one thread may be processing a segment at a time");

  char* start = mLink->mBuffer.get() + mLink->mOffset;
  uint32_t length = std::min(aCount, mLink->mAvailable);

  mLink->mProcessingSegment = true;
  {
    MutexAutoUnlock unlock(mLink->mMutex);
    nsresult rv = aWriter(this, aClosure, start, 0, length, aReadCount);
    if (NS_FAILED(rv)) {
      *aReadCount = 0;
    }
    MOZ_RELEASE_ASSERT(*aReadCount <= length);
  }
  mLink->mProcessingSegment = false;

  mLink->mOffset += *aReadCount;
  mLink->mAvailable -= *aReadCount;

  // If a closure-only callback is armed, the caller isn't listening for new
  // data, so only the IO thread is able to notice the peer closing. Re-kick
  // AdvanceIO once the buffer has drained so a subsequent close is observed.
  if (!mLink->mAvailable && mLink->mCallback && mLink->mCallbackClosureOnly) {
    mLink->mIOThread.Dispatch(
        NewRunnableMethod("PlatformPipeLink::AdvanceIO", mLink,
                          &platform_pipe_detail::PlatformPipeLink::AdvanceIO));
  }
  return NS_OK;
}

NS_IMETHODIMP PlatformPipeReader::IsNonBlocking(bool* aNonBlocking) {
  *aNonBlocking = true;
  return NS_OK;
}

NS_IMETHODIMP PlatformPipeReader::CloseWithStatus(nsresult aStatus) {
  mLink->Close(aStatus, /* aInternal */ false);
  return NS_OK;
}

NS_IMETHODIMP PlatformPipeReader::AsyncWait(nsIInputStreamCallback* aCallback,
                                            uint32_t aFlags,
                                            uint32_t aRequestedCount,
                                            nsIEventTarget* aTarget) {
  MutexAutoLock lock(mLink->mMutex);

  if (!aCallback) {
    mLink->mCallback = nullptr;
    mLink->mCallbackTarget = nullptr;
    return NS_OK;
  }

  mLink->mCallback = NS_NewRunnableFunction(
      "PlatformPipeReader::AsyncWait",
      [self = RefPtr{this}, callback = RefPtr{aCallback}] {
        callback->OnInputStreamReady(self);
      });
  mLink->mCallbackTarget = aTarget;
  mLink->mCallbackClosureOnly = aFlags & WAIT_CLOSURE_ONLY;

  if (NS_FAILED(mLink->mStatus) ||
      (!mLink->mCallbackClosureOnly && mLink->mAvailable)) {
    mLink->DispatchNotify();
  } else {
    mLink->mIOThread.Dispatch(
        NewRunnableMethod("PlatformPipeLink::AdvanceIO", mLink,
                          &platform_pipe_detail::PlatformPipeLink::AdvanceIO));
  }
  return NS_OK;
}

}  // namespace mozilla
