Skip to content

Disposed plugin host RPC protocol hijacks replies after a reconnect, and the plugin host logger amplifies the failure #17924

Description

@dr14-make

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 requestRPCProtocolImpl.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:

  1. Start an Electron Theia application with the default frontendConnectionTimeout and open a workspace so plugins activate.
  2. 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.)
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions