Bug Description:
After the frontend reconnects, a disposed RPCProtocolImpl keeps answering the plugin host, and the plugin host's own error reporting then amplifies each failure until the application stops responding.
Two defects combine.
A leaked subscription lets a disposed protocol win the reply race. When the connection closes, beforeSyncPlugins triggers toDisconnect.dispose(), which disposes the RPCProtocolImpl serving the plugin host. RPCProtocolImpl.dispose() clears locals but does not detach the channel, and HostedPluginSupport.createServerRpc discards the HostedPluginWatcher.onPostMessageEvent disposable entirely. That watcher is a singleton outliving every connection, and pluginHostId is the constant 'main' on each of them, so the subscription survives and keeps feeding plugin-host messages into the disposed protocol's channel.
On reconnect, afterStart's server.onDidOpenConnection(() => this.load()) re-runs the load and creates a second protocol with a second subscription. Emitter dispatch is FIFO, so the disposed protocol sees each message first and answers it from an empty locals map, beating the live protocol to the reply. The correct reply arrives second and is discarded as No reply handler for reply with id: N. Every main-side call from the plugin host fails from the first reconnect onward.
The failure then amplifies. setupPluginHostLogger routes the plugin host's console.* through PluginLogger. Its $log is dispatched as an RPC request — RPCProtocolImpl.isNotification looks for a notify/on prefix and $log has neither — and sendLog drops the returned promise. A failed log therefore becomes an unhandled rejection, which plugin-host.ts reports with two console.error calls, each of which sends another failing $log. In a harness against 1.74.1, one failed log became 3,279 log attempts within 8 seconds, growing geometrically until the event loop was saturated.
The visible result is a flood of paired messages and an unresponsive application:
plugin-host ERROR Promise rejection not handled in one second: Error: no local service handler with id LoggerMain , reason: Error: no local service handler with id LoggerMain
plugin-host ERROR With stack trace: Error: no local service handler with id LoggerMain
at RPCProtocolImpl.handleRequest (.../lib/frontend/bundle.js)
at ...requestHandler
at BatchingChannel.handleMessages
Note that the stack points into the frontend bundle: the error originates in the frontend's disposed protocol and is transported back to the plugin host as a rejected reply.
Steps to Reproduce:
- Start an Electron Theia application with the default
frontendConnectionTimeout and open a workspace so plugins activate.
- Suspend the machine, wait for it to sleep, then wake it. (Equivalently in a browser application: stop the backend, wait for the frontend to report a lost connection, then restart it.)
- The log fills with the paired messages above at an accelerating rate and the application stops responding.
Additional Information
The reachability of this depends on frontendConnectionTimeout. At the default of 0, handleSocketDisconnect calls closeConnection the instant the socket drops, so the frontend's channel is already gone from connectionsByFrontend when it returns and the reconnect cannot succeed (Reconnecting failed for <id> → creating connection for <id>). Every reconnect is then a full teardown and rebuild. With frontendConnectionTimeout: -1 the persistent channel is reused, nothing is torn down, and none of this occurs — verified across two sleep/wake cycles. That the framework default makes the reconnect path unreachable may be worth considering separately; theia-ide sets -1 for Electron, but nothing in the framework does.
Beyond this bug, that teardown also leaves the plugin session unusable even once the flood is stopped: the plugin host is re-forked, so extensions lose their in-process state (an extension-owned REPL terminal is gone, a language server client does not re-establish), while the main side keeps registrations in container singletons that are never reset, producing TestController already registered, View resolver already registered for <id>, and Tool '<id>' is already registered. I have not addressed that here.
Previously reported symptoms that appear to be this same bug: discussion #15396 (two independent reports, one on Electron after sleep/wake), and it is likely the mechanism behind the undisposed-RPCProtocol observations in #12823. Related open reports: #14671, #15520.
Bug Description:
After the frontend reconnects, a disposed
RPCProtocolImplkeeps answering the plugin host, and the plugin host's own error reporting then amplifies each failure until the application stops responding.Two defects combine.
A leaked subscription lets a disposed protocol win the reply race. When the connection closes,
beforeSyncPluginstriggerstoDisconnect.dispose(), which disposes theRPCProtocolImplserving the plugin host.RPCProtocolImpl.dispose()clearslocalsbut does not detach the channel, andHostedPluginSupport.createServerRpcdiscards theHostedPluginWatcher.onPostMessageEventdisposable entirely. That watcher is a singleton outliving every connection, andpluginHostIdis the constant'main'on each of them, so the subscription survives and keeps feeding plugin-host messages into the disposed protocol's channel.On reconnect,
afterStart'sserver.onDidOpenConnection(() => this.load())re-runs the load and creates a second protocol with a second subscription. Emitter dispatch is FIFO, so the disposed protocol sees each message first and answers it from an emptylocalsmap, beating the live protocol to the reply. The correct reply arrives second and is discarded asNo reply handler for reply with id: N. Every main-side call from the plugin host fails from the first reconnect onward.The failure then amplifies.
setupPluginHostLoggerroutes the plugin host'sconsole.*throughPluginLogger. Its$logis dispatched as an RPC request —RPCProtocolImpl.isNotificationlooks for anotify/onprefix and$loghas neither — andsendLogdrops the returned promise. A failed log therefore becomes an unhandled rejection, whichplugin-host.tsreports with twoconsole.errorcalls, each of which sends another failing$log. In a harness against 1.74.1, one failed log became 3,279 log attempts within 8 seconds, growing geometrically until the event loop was saturated.The visible result is a flood of paired messages and an unresponsive application:
Note that the stack points into the frontend bundle: the error originates in the frontend's disposed protocol and is transported back to the plugin host as a rejected reply.
Steps to Reproduce:
frontendConnectionTimeoutand open a workspace so plugins activate.Additional Information
The reachability of this depends on
frontendConnectionTimeout. At the default of0,handleSocketDisconnectcallscloseConnectionthe instant the socket drops, so the frontend's channel is already gone fromconnectionsByFrontendwhen it returns and the reconnect cannot succeed (Reconnecting failed for <id>→creating connection for <id>). Every reconnect is then a full teardown and rebuild. WithfrontendConnectionTimeout: -1the persistent channel is reused, nothing is torn down, and none of this occurs — verified across two sleep/wake cycles. That the framework default makes the reconnect path unreachable may be worth considering separately;theia-idesets-1for Electron, but nothing in the framework does.Beyond this bug, that teardown also leaves the plugin session unusable even once the flood is stopped: the plugin host is re-forked, so extensions lose their in-process state (an extension-owned REPL terminal is gone, a language server client does not re-establish), while the main side keeps registrations in container singletons that are never reset, producing
TestController already registered,View resolver already registered for <id>, andTool '<id>' is already registered. I have not addressed that here.Previously reported symptoms that appear to be this same bug: discussion #15396 (two independent reports, one on Electron after sleep/wake), and it is likely the mechanism behind the undisposed-
RPCProtocolobservations in #12823. Related open reports: #14671, #15520.masteras of 30844b5