/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */ /* vim: set ts=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 "BluetoothSocket.h" #include #include #include "base/message_loop.h" #include "BluetoothSocketObserver.h" #include "BluetoothInterface.h" #include "BluetoothUtils.h" #include "mozilla/FileUtils.h" #include "mozilla/RefPtr.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" using namespace mozilla::ipc; USING_BLUETOOTH_NAMESPACE static const size_t MAX_READ_SIZE = 1 << 16; static const uint8_t UUID_OBEX_OBJECT_PUSH[] = { 0x00, 0x00, 0x11, 0x05, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB }; static BluetoothSocketInterface* sBluetoothSocketInterface; // helper functions static bool EnsureBluetoothSocketHalLoad() { if (sBluetoothSocketInterface) { return true; } BluetoothInterface* btInf = BluetoothInterface::GetInstance(); NS_ENSURE_TRUE(btInf, false); sBluetoothSocketInterface = btInf->GetBluetoothSocketInterface(); NS_ENSURE_TRUE(sBluetoothSocketInterface, false); return true; } class mozilla::dom::bluetooth::DroidSocketImpl : public ipc::UnixFdWatcher , protected SocketIOBase { public: /* The connection status in DroidSocketImpl indicates the current * phase of the socket connection. The initial settign should always * be DISCONNECTED, when no connection is present. * * To establish a connection on the server, DroidSocketImpl moves * to LISTENING. It now waits for incoming connection attempts by * installing a read watcher on the I/O thread. When its socket file * descriptor becomes readable, DroidSocketImpl accepts the connection * and finally moves DroidSocketImpl to CONNECTED. DroidSocketImpl now * uses read and write watchers during data transfers. Any socket setup * is handled internally by the accept method. * * On the client side, DroidSocketImpl moves to CONNECTING and installs * a write watcher on the I/O thread to wait until the connection is * ready. The socket setup is handled internally by the connect method. * Installing the write handler makes the code compatible with POSIX * semantics for non-blocking connects and gives a clear signal when the * conncetion is ready. DroidSocketImpl then moves to CONNECTED and uses * read and write watchers during data transfers. */ enum ConnectionStatus { SOCKET_IS_DISCONNECTED = 0, SOCKET_IS_LISTENING, SOCKET_IS_CONNECTING, SOCKET_IS_CONNECTED }; DroidSocketImpl(MessageLoop* aIOLoop, BluetoothSocket* aConsumer) : ipc::UnixFdWatcher(aIOLoop) , SocketIOBase(MAX_READ_SIZE) , mConsumer(aConsumer) , mShuttingDownOnIOThread(false) , mConnectionStatus(SOCKET_IS_DISCONNECTED) { } ~DroidSocketImpl() { MOZ_ASSERT(NS_IsMainThread()); } void Send(UnixSocketRawData* aData) { EnqueueData(aData); AddWatchers(WRITE_WATCHER, false); } bool IsShutdownOnMainThread() { MOZ_ASSERT(NS_IsMainThread()); return mConsumer == nullptr; } bool IsShutdownOnIOThread() { return mShuttingDownOnIOThread; } void ShutdownOnMainThread() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!IsShutdownOnMainThread()); mConsumer = nullptr; } void ShutdownOnIOThread() { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!mShuttingDownOnIOThread); Close(); // will also remove fd from I/O loop mShuttingDownOnIOThread = true; } void Connect(int aFd); void Listen(int aFd); void Accept(int aFd); void ConnectClientFd() { // Stop current read watch RemoveWatchers(READ_WATCHER); mConnectionStatus = SOCKET_IS_CONNECTED; // Restart read & write watch on client fd AddWatchers(READ_WATCHER, true); AddWatchers(WRITE_WATCHER, false); } SocketConsumerBase* GetConsumer() { return mConsumer.get(); } SocketBase* GetSocketBase() { return GetConsumer(); } /** * Consumer pointer. Non-thread safe RefPtr, so should only be manipulated * directly from main thread. All non-main-thread accesses should happen with * mImpl as container. */ RefPtr mConsumer; private: /** * libevent triggered functions that reads data from socket when available and * guarenteed non-blocking. Only to be called on IO thread. * * @param aFd [in] File descriptor to read from */ virtual void OnFileCanReadWithoutBlocking(int aFd); /** * libevent or developer triggered functions that writes data to socket when * available and guarenteed non-blocking. Only to be called on IO thread. * * @param aFd [in] File descriptor to read from */ virtual void OnFileCanWriteWithoutBlocking(int aFd); void OnSocketCanReceiveWithoutBlocking(int aFd); void OnSocketCanAcceptWithoutBlocking(int aFd); void OnSocketCanSendWithoutBlocking(int aFd); void OnSocketCanConnectWithoutBlocking(int aFd); /** * If true, do not requeue whatever task we're running */ bool mShuttingDownOnIOThread; ConnectionStatus mConnectionStatus; }; class SocketConnectTask MOZ_FINAL : public SocketIOTask { public: SocketConnectTask(DroidSocketImpl* aDroidSocketImpl, int aFd) : SocketIOTask(aDroidSocketImpl) , mFd(aFd) { } void Run() MOZ_OVERRIDE { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsCanceled()); GetIO()->Connect(mFd); } private: int mFd; }; class SocketListenTask MOZ_FINAL : public SocketIOTask { public: SocketListenTask(DroidSocketImpl* aDroidSocketImpl, int aFd) : SocketIOTask(aDroidSocketImpl) , mFd(aFd) { } void Run() MOZ_OVERRIDE { MOZ_ASSERT(!NS_IsMainThread()); if (!IsCanceled()) { GetIO()->Listen(mFd); } } private: int mFd; }; class SocketConnectClientFdTask MOZ_FINAL : public SocketIOTask { SocketConnectClientFdTask(DroidSocketImpl* aImpl) : SocketIOTask(aImpl) { } void Run() MOZ_OVERRIDE { MOZ_ASSERT(!NS_IsMainThread()); GetIO()->ConnectClientFd(); } }; void DroidSocketImpl::Connect(int aFd) { MOZ_ASSERT(aFd >= 0); int flags = TEMP_FAILURE_RETRY(fcntl(aFd, F_GETFL)); NS_ENSURE_TRUE_VOID(flags >= 0); if (!(flags & O_NONBLOCK)) { int res = TEMP_FAILURE_RETRY(fcntl(aFd, F_SETFL, flags | O_NONBLOCK)); NS_ENSURE_TRUE_VOID(!res); } SetFd(aFd); mConnectionStatus = SOCKET_IS_CONNECTING; AddWatchers(WRITE_WATCHER, false); } void DroidSocketImpl::Listen(int aFd) { MOZ_ASSERT(aFd >= 0); int flags = TEMP_FAILURE_RETRY(fcntl(aFd, F_GETFL)); NS_ENSURE_TRUE_VOID(flags >= 0); if (!(flags & O_NONBLOCK)) { int res = TEMP_FAILURE_RETRY(fcntl(aFd, F_SETFL, flags | O_NONBLOCK)); NS_ENSURE_TRUE_VOID(!res); } SetFd(aFd); mConnectionStatus = SOCKET_IS_LISTENING; AddWatchers(READ_WATCHER, true); } void DroidSocketImpl::Accept(int aFd) { Close(); int flags = TEMP_FAILURE_RETRY(fcntl(aFd, F_GETFL)); NS_ENSURE_TRUE_VOID(flags >= 0); if (!(flags & O_NONBLOCK)) { int res = TEMP_FAILURE_RETRY(fcntl(aFd, F_SETFL, flags | O_NONBLOCK)); NS_ENSURE_TRUE_VOID(!res); } SetFd(aFd); mConnectionStatus = SOCKET_IS_CONNECTED; nsRefPtr r = new SocketIOEventRunnable( this, SocketIOEventRunnable::CONNECT_SUCCESS); NS_DispatchToMainThread(r); AddWatchers(READ_WATCHER, true); if (HasPendingData()) { AddWatchers(WRITE_WATCHER, false); } } void DroidSocketImpl::OnFileCanReadWithoutBlocking(int aFd) { if (mConnectionStatus == SOCKET_IS_CONNECTED) { OnSocketCanReceiveWithoutBlocking(aFd); } else if (mConnectionStatus == SOCKET_IS_LISTENING) { OnSocketCanAcceptWithoutBlocking(aFd); } else { NS_NOTREACHED("invalid connection state for reading"); } } void DroidSocketImpl::OnSocketCanReceiveWithoutBlocking(int aFd) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!mShuttingDownOnIOThread); ssize_t res = ReceiveData(aFd, this); if (res < 0) { /* I/O error */ RemoveWatchers(READ_WATCHER|WRITE_WATCHER); } else if (!res) { /* EOF or peer shutdown */ RemoveWatchers(READ_WATCHER); } } class AcceptTask MOZ_FINAL : public SocketIOTask { public: AcceptTask(DroidSocketImpl* aDroidSocketImpl, int aFd) : SocketIOTask(aDroidSocketImpl) , mFd(aFd) { } void Run() MOZ_OVERRIDE { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!IsCanceled()); GetIO()->Accept(mFd); } private: int mFd; }; class AcceptResultHandler MOZ_FINAL : public BluetoothSocketResultHandler { public: AcceptResultHandler(DroidSocketImpl* aImpl) : mImpl(aImpl) { MOZ_ASSERT(mImpl); } void Accept(int aFd, const nsAString& aBdAddress, int aConnectionStatus) MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); mozilla::ScopedClose fd(aFd); // Close received socket fd on error if (mImpl->IsShutdownOnMainThread()) { BT_LOGD("mConsumer is null, aborting receive!"); return; } if (aConnectionStatus != 0) { mImpl->mConsumer->NotifyError(); return; } mImpl->mConsumer->SetAddress(aBdAddress); XRE_GetIOMessageLoop()->PostTask(FROM_HERE, new AcceptTask(mImpl, fd.forget())); } void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); BT_LOGR("BluetoothSocketInterface::Accept failed: %d", (int)aStatus); if (!mImpl->IsShutdownOnMainThread()) { // Instead of NotifyError(), call NotifyDisconnect() to trigger // BluetoothOppManager::OnSocketDisconnect() as // DroidSocketImpl::OnFileCanReadWithoutBlocking() in Firefox OS 2.0 in // order to keep the same behavior and reduce regression risk. mImpl->mConsumer->NotifyDisconnect(); } } private: DroidSocketImpl* mImpl; }; class AcceptRunnable MOZ_FINAL : public SocketIORunnable { public: AcceptRunnable(DroidSocketImpl* aImpl, int aFd) : SocketIORunnable(aImpl) , mFd(aFd) { } NS_IMETHOD Run() MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(sBluetoothSocketInterface); BluetoothSocketResultHandler* res = new AcceptResultHandler(GetIO()); GetIO()->mConsumer->SetCurrentResultHandler(res); sBluetoothSocketInterface->Accept(mFd, res); return NS_OK; } private: int mFd; }; void DroidSocketImpl::OnSocketCanAcceptWithoutBlocking(int aFd) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!mShuttingDownOnIOThread); /* When a listening socket is ready for receiving data, * we can call |Accept| on it. */ RemoveWatchers(READ_WATCHER); nsRefPtr t = new AcceptRunnable(this, aFd); NS_DispatchToMainThread(t); } void DroidSocketImpl::OnFileCanWriteWithoutBlocking(int aFd) { if (mConnectionStatus == SOCKET_IS_CONNECTED) { OnSocketCanSendWithoutBlocking(aFd); } else if (mConnectionStatus == SOCKET_IS_CONNECTING) { OnSocketCanConnectWithoutBlocking(aFd); } else { NS_NOTREACHED("invalid connection state for writing"); } } void DroidSocketImpl::OnSocketCanSendWithoutBlocking(int aFd) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!mShuttingDownOnIOThread); MOZ_ASSERT(aFd >= 0); nsresult rv = SendPendingData(aFd, this); if (NS_FAILED(rv)) { return; } if (HasPendingData()) { AddWatchers(WRITE_WATCHER, false); } } void DroidSocketImpl::OnSocketCanConnectWithoutBlocking(int aFd) { MOZ_ASSERT(!NS_IsMainThread()); MOZ_ASSERT(!mShuttingDownOnIOThread); /* We follow Posix behaviour here: Connect operations are * complete once we can write to the connecting socket. */ mConnectionStatus = SOCKET_IS_CONNECTED; nsRefPtr r = new SocketIOEventRunnable( this, SocketIOEventRunnable::CONNECT_SUCCESS); NS_DispatchToMainThread(r); AddWatchers(READ_WATCHER, true); if (HasPendingData()) { AddWatchers(WRITE_WATCHER, false); } } BluetoothSocket::BluetoothSocket(BluetoothSocketObserver* aObserver, BluetoothSocketType aType, bool aAuth, bool aEncrypt) : mObserver(aObserver) , mCurrentRes(nullptr) , mImpl(nullptr) , mAuth(aAuth) , mEncrypt(aEncrypt) { MOZ_ASSERT(aObserver); EnsureBluetoothSocketHalLoad(); mDeviceAddress.AssignLiteral(BLUETOOTH_ADDRESS_NONE); } class ConnectSocketResultHandler MOZ_FINAL : public BluetoothSocketResultHandler { public: ConnectSocketResultHandler(DroidSocketImpl* aImpl) : mImpl(aImpl) { MOZ_ASSERT(mImpl); } void Connect(int aFd, const nsAString& aBdAddress, int aConnectionStatus) MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); if (mImpl->IsShutdownOnMainThread()) { BT_LOGD("mConsumer is null, aborting send!"); return; } if (aConnectionStatus != 0) { mImpl->mConsumer->NotifyError(); return; } mImpl->mConsumer->SetAddress(aBdAddress); XRE_GetIOMessageLoop()->PostTask(FROM_HERE, new SocketConnectTask(mImpl, aFd)); } void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); BT_WARNING("Connect failed: %d", (int)aStatus); if (!mImpl->IsShutdownOnMainThread()) { // Instead of NotifyError(), call NotifyDisconnect() to trigger // BluetoothOppManager::OnSocketDisconnect() as // DroidSocketImpl::OnFileCanReadWithoutBlocking() in Firefox OS 2.0 in // order to keep the same behavior and reduce regression risk. mImpl->mConsumer->NotifyDisconnect(); } } private: DroidSocketImpl* mImpl; }; bool BluetoothSocket::ConnectSocket(const nsAString& aDeviceAddress, int aChannel) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_FALSE(mImpl, false); SetConnectionStatus(SOCKET_CONNECTING); mImpl = new DroidSocketImpl(XRE_GetIOMessageLoop(), this); BluetoothSocketResultHandler* res = new ConnectSocketResultHandler(mImpl); SetCurrentResultHandler(res); // TODO: uuid as argument sBluetoothSocketInterface->Connect( aDeviceAddress, BluetoothSocketType::RFCOMM, UUID_OBEX_OBJECT_PUSH, aChannel, mEncrypt, mAuth, res); return true; } class ListenResultHandler MOZ_FINAL : public BluetoothSocketResultHandler { public: ListenResultHandler(DroidSocketImpl* aImpl) : mImpl(aImpl) { MOZ_ASSERT(mImpl); } void Listen(int aFd) MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); XRE_GetIOMessageLoop()->PostTask(FROM_HERE, new SocketListenTask(mImpl, aFd)); } void OnError(BluetoothStatus aStatus) MOZ_OVERRIDE { MOZ_ASSERT(NS_IsMainThread()); BT_WARNING("Listen failed: %d", (int)aStatus); } private: DroidSocketImpl* mImpl; }; bool BluetoothSocket::ListenSocket(int aChannel) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_FALSE(mImpl, false); SetConnectionStatus(SOCKET_LISTENING); mImpl = new DroidSocketImpl(XRE_GetIOMessageLoop(), this); BluetoothSocketResultHandler* res = new ListenResultHandler(mImpl); SetCurrentResultHandler(res); sBluetoothSocketInterface->Listen( BluetoothSocketType::RFCOMM, NS_LITERAL_STRING("OBEX Object Push"), UUID_OBEX_OBJECT_PUSH, aChannel, mEncrypt, mAuth, res); return true; } void BluetoothSocket::CloseSocket() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(sBluetoothSocketInterface); if (!mImpl) { return; } // Stop any watching |SocketMessageWatcher| if (mCurrentRes) { sBluetoothSocketInterface->Close(mCurrentRes); } // From this point on, we consider mImpl as being deleted. // We sever the relationship here so any future calls to listen or connect // will create a new implementation. mImpl->ShutdownOnMainThread(); XRE_GetIOMessageLoop()->PostTask( FROM_HERE, new SocketIOShutdownTask(mImpl)); mImpl = nullptr; NotifyDisconnect(); } bool BluetoothSocket::SendSocketData(UnixSocketRawData* aData) { MOZ_ASSERT(NS_IsMainThread()); NS_ENSURE_TRUE(mImpl, false); MOZ_ASSERT(!mImpl->IsShutdownOnMainThread()); XRE_GetIOMessageLoop()->PostTask( FROM_HERE, new SocketIOSendTask(mImpl, aData)); return true; } void BluetoothSocket::ReceiveSocketData(nsAutoPtr& aMessage) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mObserver); mObserver->ReceiveSocketData(this, aMessage); } void BluetoothSocket::OnConnectSuccess() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mObserver); SetCurrentResultHandler(nullptr); mObserver->OnSocketConnectSuccess(this); } void BluetoothSocket::OnConnectError() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mObserver); SetCurrentResultHandler(nullptr); mObserver->OnSocketConnectError(this); } void BluetoothSocket::OnDisconnect() { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(mObserver); mObserver->OnSocketDisconnect(this); }