/* -*- 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 # include # include # include #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 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 mIOThread; Mutex mMutex{"PlatformPipeReader"}; UniqueFileHandle mHandle MOZ_GUARDED_BY(mMutex); const UniquePtr 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 mCallback MOZ_GUARDED_BY(mMutex); nsCOMPtr 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 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(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::TakePending() { RefPtr 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 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("PlatformPipeLink::Close", this, &PlatformPipeLink::Close, aStatus, /* aInternal */ true), NS_DISPATCH_EVENT_MAY_BLOCK); } void PlatformPipeLink::DispatchNotify() { nsCOMPtr callback = mCallback.forget(); nsCOMPtr 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(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 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 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