-
Notifications
You must be signed in to change notification settings - Fork 5k
MessagePort: close a port whose peer closed, so it is rejected as a transferable #38066
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
6e339fb
fd863bc
bc7a4e0
f0af4bc
d2851de
5db54a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,6 +50,8 @@ Ref<MessagePort> MessagePort::create(ScriptExecutionContext& context, Ref<Messag | |
| { | ||
| auto messagePort = adoptRef(*new MessagePort(context, WTF::move(pipe), side)); | ||
| messagePort->suspendIfNeeded(); | ||
| // So the peer's close() reaches this port (peerClosed()) even if it never gets a listener. | ||
| messagePort->m_pipe->registerCloseContext(side, context.identifier(), ThreadSafeWeakPtr<MessagePort> { messagePort.get() }); | ||
| return messagePort; | ||
| } | ||
|
|
||
|
|
@@ -271,24 +273,33 @@ void MessagePort::dispatchCloseEvent() | |
|
|
||
| void MessagePort::peerClosed() | ||
| { | ||
| if (m_isDetached) | ||
| if (m_isDetached || m_isClosing) | ||
| return; | ||
| auto* context = scriptExecutionContext(); | ||
| if (!context || !context->globalObject()) | ||
| return; | ||
| Ref protectedThis { *this }; | ||
| // Deliver whatever the peer sent before it closed, then fire 'close'. Node orders | ||
| // them that way, and registerCloseContext()'s retroactive notify can land before any | ||
| // drain is scheduled -- e.g. on('close') registered before on('message'). | ||
| // Node delivers what the peer sent before closing first; this task can run before the drain. | ||
| if (m_started && hasMessageEventListener()) | ||
| flushQueuedMessagesBeforeClose(); | ||
| // Fire 'close' (guarded against a double dispatch) and release this side's loop refs | ||
| // so the loop can idle, matching node. | ||
| dispatchCloseEvent(); | ||
| // jsUnref() clears both the listener loop-ref (m_isRefd) and the onmessage/ref() | ||
| // keepalive (m_hasRef), so a listening transferred port stops pinning the loop. | ||
| auto* globalObject = defaultGlobalObject(context->globalObject()); | ||
| jsUnref(globalObject); | ||
| // A handler in that flush closed or transferred this port. | ||
| if (m_isDetached || m_isClosing) | ||
| return; | ||
| if (!m_pipe->isOtherSideClosedByRequest(m_side)) { | ||
| // Peer collected, not closed: node never closes a channel over that (see jsRef()), so | ||
| // only release the loop refs of a port that is started or listening for 'close'. An | ||
| // idle port keeps its one 'close' event for its own close(cb). | ||
| if (m_started || m_hasCloseEventListener.load(std::memory_order_relaxed)) { | ||
| dispatchCloseEvent(); | ||
| jsUnref(defaultGlobalObject(context->globalObject())); | ||
| } | ||
| return; | ||
| } | ||
| // Not receiving, messages still queued: node's close notification waits behind them. | ||
| // attach() notifies again once the port starts; tryTakeMessage() closes on the empty read. | ||
| if (MessagePortPipe::queuedCount(m_pipe->state(m_side)) > 0) | ||
| return; | ||
|
Comment on lines
+307
to
+311
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 The Extended reasoning...What the bug is
Step-by-step proofconst { port1, port2 } = new (require('worker_threads').MessageChannel)();
port1.ref();
const h = () => {};
port1.on('message', h);
port1.off('message', h);
port2.postMessage('queued');
port2.close();
setTimeout(() => { console.log('hung, hasRef=' + port1.hasRef()); process.exit(1); }, 3000).unref();
Result: The Why nothing recovers it
Why this is a regression
This is the remaining gap in the How to fix itTwo options that both match node's observable state (port stays open/transferable, but does not pin the loop): (a) Mirror node — call (b) Release the loop refs when deferring — in Either way, add a spawned-process test alongside the existing "a ref()'d port with no listeners…" one: |
||
| close(); | ||
| } | ||
|
robobun marked this conversation as resolved.
|
||
|
|
||
| TransferredMessagePort MessagePort::disentangle() | ||
|
|
@@ -381,8 +392,14 @@ JSValue MessagePort::tryTakeMessage(JSGlobalObject* lexicalGlobalObject, bool& h | |
| return jsUndefined(); | ||
|
|
||
| auto message = m_pipe->takeOne(m_side); | ||
| if (!message) | ||
| if (!message) { | ||
| // This read reached the peer's close: node closes the port here (see peerClosed()). | ||
| // Peer state before our queue, as in virtualHasPendingActivity(): a message the peer | ||
| // sent between takeOne() and its close() is then visible and must not be dropped. | ||
|
robobun marked this conversation as resolved.
|
||
| if (m_pipe->isOtherSideClosedByRequest(m_side) && !MessagePortPipe::queuedCount(m_pipe->state(m_side))) | ||
| close(); | ||
| return jsUndefined(); | ||
|
claude[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| hadMessage = true; | ||
| auto ports = MessagePort::entanglePorts(*context, WTF::move(message->transferredPorts)); | ||
|
|
@@ -525,12 +542,6 @@ bool MessagePort::addEventListener(const AtomString& eventType, Ref<EventListene | |
| } | ||
| } else if (eventType == eventNames().closeEvent) { | ||
| m_hasCloseEventListener.store(true, std::memory_order_release); | ||
| if (isEntangled()) { | ||
| // Record our context with the pipe so the peer's close() can deliver a | ||
| // 'close' event even if we never started (no 'message' listener). | ||
| if (auto* context = scriptExecutionContext()) | ||
| m_pipe->registerCloseContext(m_side, context->identifier(), ThreadSafeWeakPtr<MessagePort> { *this }); | ||
| } | ||
| } | ||
| return EventTarget::addEventListener(eventType, WTF::move(listener), options); | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.