/* -*- 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 "mozilla/dom/SerialPortParent.h" #include "SerialLogging.h" #include "SerialPortPumps.h" #include "mozilla/Services.h" #include "mozilla/dom/SerialManagerParent.h" #include "mozilla/dom/SerialPlatformService.h" #include "mozilla/dom/SerialPortIPCTypes.h" #include "mozilla/ipc/DataPipe.h" #include "mozilla/ipc/Endpoint.h" #include "nsHashPropertyBag.h" #include "nsIObserverService.h" #include "nsIPipe.h" #include "nsNetUtil.h" #include "nsStreamUtils.h" namespace mozilla::dom { SerialPortParent::SerialPortParent(const nsString& aPortId, uint64_t aBrowserId, SerialDeviceChangeProxy* aProxy) : mPortId(aPortId), mBrowserId(aBrowserId), mDeviceChangeProxy(aProxy) { MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPortParent[%p] created for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); } SerialPortParent::~SerialPortParent() { MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPortParent[%p] destroyed for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); } mozilla::ipc::IPCResult SerialPortParent::RecvOpen( const IPCSerialOptions& aOptions, OpenResolver&& aResolver) { if (mIsOpen) { MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("SerialPortParent[%p]::RecvOpen failed: port '%s' already open", this, NS_ConvertUTF16toUTF8(mPortId).get())); aResolver(NS_ERROR_ALREADY_INITIALIZED); return IPC_OK(); } if (aOptions.bufferSize() > kMaxSerialBufferSize) { MOZ_LOG( gWebSerialLog, LogLevel::Warning, ("SerialPortParent[%p]::RecvOpen rejecting oversized bufferSize " "%u for port '%s'", this, aOptions.bufferSize(), NS_ConvertUTF16toUTF8(mPortId).get())); return IPC_FAIL(this, "bufferSize exceeds maximum"); } MOZ_LOG( gWebSerialLog, LogLevel::Info, ("SerialPortParent[%p]::RecvOpen opening port '%s' (baudRate=%u, " "dataBits=%u, stopBits=%u, parity=%u, bufferSize=%u, flowControl=%u)", this, NS_ConvertUTF16toUTF8(mPortId).get(), aOptions.baudRate(), aOptions.dataBits(), aOptions.stopBits(), static_cast(aOptions.parity()), aOptions.bufferSize(), static_cast(aOptions.flowControl()))); RefPtr service = SerialPlatformService::GetInstance(); nsresult rv = NS_ERROR_FAILURE; if (service) { service->AssertIsOnIOThread(); // Validate portId against enumerated ports before opening. bool portFound = false; SerialPortList ports; nsresult enumRv = service->EnumeratePorts(ports); if (NS_SUCCEEDED(enumRv)) { for (const auto& portInfo : ports) { if (portInfo.id() == mPortId) { portFound = true; break; } } } if (portFound) { rv = service->Open(mPortId, aOptions); } else { MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("SerialPortParent[%p]::RecvOpen portId '%s' not found in " "enumerated ports, rejecting open", this, NS_ConvertUTF16toUTF8(mPortId).get())); rv = NS_ERROR_NOT_AVAILABLE; } } if (NS_FAILED(rv)) { MOZ_LOG( gWebSerialLog, LogLevel::Error, ("SerialPortParent[%p]::RecvOpen failed for port '%s': 0x%08x", this, NS_ConvertUTF16toUTF8(mPortId).get(), static_cast(rv))); aResolver(rv); return IPC_OK(); } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPortParent[%p]::RecvOpen succeeded for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); mIsOpen = true; mPipeCapacity = std::max(aOptions.bufferSize(), kMinSerialPortPumpSize); aResolver(NS_OK); return IPC_OK(); } mozilla::ipc::IPCResult SerialPortParent::RecvClose(CloseResolver&& aResolver) { if (!mIsOpen) { MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPortParent[%p]::RecvClose: port '%s' already closed", this, NS_ConvertUTF16toUTF8(mPortId).get())); aResolver(NS_OK); return IPC_OK(); } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPortParent[%p]::RecvClose closing port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); StopPumps(); RefPtr service = SerialPlatformService::GetInstance(); nsresult rv = NS_ERROR_FAILURE; if (service) { service->AssertIsOnIOThread(); rv = service->Close(mPortId); } if (NS_SUCCEEDED(rv)) { MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPortParent[%p]::RecvClose succeeded for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); } else { MOZ_LOG( gWebSerialLog, LogLevel::Error, ("SerialPortParent[%p]::RecvClose failed for port '%s': 0x%08x", this, NS_ConvertUTF16toUTF8(mPortId).get(), static_cast(rv))); } mIsOpen = false; aResolver(rv); return IPC_OK(); } void SerialPortParent::StopReadPump() { if (mReadPump) { mReadPump->Stop(); mReadPump = nullptr; } if (mReadPipeSender) { mReadPipeSender->CloseWithStatus(NS_BASE_STREAM_CLOSED); mReadPipeSender = nullptr; } } void SerialPortParent::StopWritePump() { if (mWritePump) { mWritePump->Stop(); mWritePump = nullptr; } if (mWritePipeReceiver) { mWritePipeReceiver->CloseWithStatus(NS_BASE_STREAM_CLOSED); mWritePipeReceiver = nullptr; } } void SerialPortParent::StartReadPump( already_AddRefed aReadPipeSender) { mReadPipeSender = aReadPipeSender; RefPtr service = SerialPlatformService::GetInstance(); if (!service) { return; } mReadPump = MakeRefPtr(mPortId, mReadPipeSender); service->IOThread()->Dispatch(do_AddRef(mReadPump)); } void SerialPortParent::StartWritePump( already_AddRefed aWritePipeReceiver) { mWritePipeReceiver = aWritePipeReceiver; mWritePump = MakeRefPtr(mPortId, mWritePipeReceiver); mWritePump->Start(); } mozilla::ipc::IPCResult SerialPortParent::RecvAttachReadPipe( const RefPtr& aReadPipeSender) { if (!mIsOpen || !aReadPipeSender) { MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("SerialPortParent[%p]::RecvAttachReadPipe: port '%s' not open", this, NS_ConvertUTF16toUTF8(mPortId).get())); if (aReadPipeSender) { aReadPipeSender->CloseWithStatus(NS_ERROR_NOT_AVAILABLE); } return IPC_OK(); } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPortParent[%p]::RecvAttachReadPipe for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); // Clean up any existing read pipe/pump from a previous readable stream. StopReadPump(); RefPtr sender = aReadPipeSender; StartReadPump(sender.forget()); return IPC_OK(); } mozilla::ipc::IPCResult SerialPortParent::RecvAttachWritePipe( const RefPtr& aWritePipeReceiver) { if (!mIsOpen || !aWritePipeReceiver) { MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("SerialPortParent[%p]::RecvAttachWritePipe: port '%s' not open", this, NS_ConvertUTF16toUTF8(mPortId).get())); if (aWritePipeReceiver) { aWritePipeReceiver->CloseWithStatus(NS_ERROR_NOT_AVAILABLE); } return IPC_OK(); } MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPortParent[%p]::RecvAttachWritePipe for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); // Clean up any existing write pipe/pump from a previous writable stream. StopWritePump(); RefPtr receiver = aWritePipeReceiver; StartWritePump(receiver.forget()); return IPC_OK(); } mozilla::ipc::IPCResult SerialPortParent::RecvSetSignals( const IPCSerialOutputSignals& aSignals, SetSignalsResolver&& aResolver) { if (!mIsOpen) { aResolver(NS_ERROR_NOT_AVAILABLE); return IPC_OK(); } RefPtr service = SerialPlatformService::GetInstance(); nsresult rv = NS_ERROR_FAILURE; if (service) { service->AssertIsOnIOThread(); rv = service->SetSignals(mPortId, aSignals); } aResolver(rv); return IPC_OK(); } mozilla::ipc::IPCResult SerialPortParent::RecvGetSignals( GetSignalsResolver&& aResolver) { IPCSerialInputSignals signals( /* dataCarrierDetect */ false, /* clearToSend */ false, /* ringIndicator */ false, /* dataSetReady */ false); if (!mIsOpen) { aResolver(std::tuple(NS_ERROR_NOT_AVAILABLE, signals)); return IPC_OK(); } RefPtr service = SerialPlatformService::GetInstance(); nsresult rv = NS_ERROR_FAILURE; if (service) { service->AssertIsOnIOThread(); rv = service->GetSignals(mPortId, signals); } aResolver(std::tuple(rv, signals)); return IPC_OK(); } mozilla::ipc::IPCResult SerialPortParent::RecvDrain(DrainResolver&& aResolver) { if (!mIsOpen) { MOZ_LOG(gWebSerialLog, LogLevel::Error, ("SerialPortParent[%p]::RecvDrain failed: port '%s' not open", this, NS_ConvertUTF16toUTF8(mPortId).get())); aResolver(NS_ERROR_NOT_AVAILABLE); return IPC_OK(); } MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPortParent[%p]::RecvDrain draining buffers for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); // There is no ordering guarantee between DataPipe notifications and IPC // messages. The child closes its DataPipeSender before sending Drain, but // the DataPipe closure notification may not have reached the parent yet, // meaning the write pump may still have unconsumed data. We must wait for // the write pipe to be fully closed (all data written to the device) // before draining OS transmit buffers. auto completeDrain = [portId = mPortId, aResolver]() { RefPtr service = SerialPlatformService::GetInstance(); nsresult rv = NS_ERROR_FAILURE; if (service) { service->AssertIsOnIOThread(); rv = service->Drain(portId); } aResolver(rv); }; if (mWritePump) { mWritePump->OnPipeClosed( NS_NewRunnableFunction("DrainAfterPipeClosed", completeDrain)); return IPC_OK(); } completeDrain(); return IPC_OK(); } mozilla::ipc::IPCResult SerialPortParent::RecvFlush(bool aReceive, FlushResolver&& aResolver) { if (!mIsOpen) { MOZ_LOG(gWebSerialLog, LogLevel::Error, ("SerialPortParent[%p]::RecvFlush failed: port '%s' not open", this, NS_ConvertUTF16toUTF8(mPortId).get())); aResolver(NS_ERROR_NOT_AVAILABLE); return IPC_OK(); } MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPortParent[%p]::RecvFlush flushing %s buffers for port '%s'", this, aReceive ? "receive" : "transmit", NS_ConvertUTF16toUTF8(mPortId).get())); if (aReceive) { StopReadPump(); } else { StopWritePump(); } RefPtr service = SerialPlatformService::GetInstance(); nsresult rv = NS_ERROR_FAILURE; if (service) { service->AssertIsOnIOThread(); rv = service->Flush(mPortId, aReceive); } aResolver(rv); return IPC_OK(); } void SerialPortParent::NotifySharingStateChanged(bool aConnected) { mSharingConnected = aConnected; NS_DispatchToMainThread(NS_NewRunnableFunction( "SerialPortParent::NotifySharingStateChanged", [browserId = mBrowserId, aConnected]() { nsCOMPtr obs = mozilla::services::GetObserverService(); if (!obs) { return; } auto props = MakeRefPtr(); props->SetPropertyAsUint64(u"browserId"_ns, browserId); props->SetPropertyAsBool(u"connected"_ns, aConnected); obs->NotifyObservers(static_cast(props), "serial-device-state-changed", nullptr); })); } mozilla::ipc::IPCResult SerialPortParent::RecvUpdateSharingState( bool aConnected) { if (aConnected != mSharingConnected) { NotifySharingStateChanged(aConnected); } else { MOZ_LOG(gWebSerialLog, LogLevel::Warning, ("SerialPortParent[%p]::RecvUpdateSharingState got same state of " "%d for port '%s'", this, aConnected ? 1 : 0, NS_ConvertUTF16toUTF8(mPortId).get())); } return IPC_OK(); } mozilla::ipc::IPCResult SerialPortParent::RecvClone( mozilla::ipc::Endpoint&& aEndpoint) { if (!aEndpoint.IsValid()) { return IPC_FAIL(this, "Invalid endpoint in RecvClone"); } auto actor = MakeRefPtr(mPortId, mBrowserId); if (!aEndpoint.Bind(actor)) { MOZ_LOG(gWebSerialLog, LogLevel::Error, ("SerialPortParent[%p]::RecvClone failed to bind for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); return IPC_OK(); } mClones.AppendElement(actor); return IPC_OK(); } void SerialPortParent::NotifyConnected() { if (CanSend()) { (void)SendConnected(); } for (const auto& clone : mClones) { clone->NotifyConnected(); } } void SerialPortParent::NotifyDisconnected() { MOZ_LOG(gWebSerialLog, LogLevel::Info, ("SerialPortParent[%p]::NotifyDisconnected for port '%s'", this, NS_ConvertUTF16toUTF8(mPortId).get())); if (mIsOpen) { StopPumps(); mIsOpen = false; RefPtr service = SerialPlatformService::GetInstance(); if (service) { service->Close(mPortId); } } if (CanSend()) { (void)SendDisconnected(); } for (const auto& clone : mClones) { clone->NotifyDisconnected(); } } void SerialPortParent::StopPumps() { StopReadPump(); StopWritePump(); } void SerialPortParent::ActorDestroy(ActorDestroyReason aWhy) { MOZ_LOG(gWebSerialLog, LogLevel::Debug, ("SerialPortParent[%p]::ActorDestroy for port '%s' (reason: %d)", this, NS_ConvertUTF16toUTF8(mPortId).get(), (int)aWhy)); if (mDeviceChangeProxy) { mDeviceChangeProxy->RemovePortActor(this); mDeviceChangeProxy = nullptr; } StopPumps(); if (mIsOpen) { RefPtr service = SerialPlatformService::GetInstance(); if (service) { service->Close(mPortId); } mIsOpen = false; } // If the child sent connected=true but never sent connected=false (e.g. // content process crash or iframe navigation), send the disconnect // notification so the browser sharing indicator count stays in sync. if (mSharingConnected) { NotifySharingStateChanged(false); } nsTArray> clones = std::move(mClones); for (const auto& clone : clones) { if (clone->CanSend()) { clone->Close(); } } } } // namespace mozilla::dom