Skip to content

LanguageServerManager.from_languages leaks the subprocess of a language server that raises after spawning it #1949

Description

@AmirF194

Preconditions:

  • I have made sure it's an actual issue, not a question.
  • I have consulted the user guide and verified that the issue cannot be resolved by adjusting configuration.
  • I have looked for similar issues and discussions, including closed ones.

Issue details:

  • I have provided a meaningful title and description.
  • I have explained how the issue arose and added instructions on how to reproduce it.
  • I have added details on my setup.

Description

LanguageServerManager.from_languages starts one language server per requested
language in a StartLSThread. Each thread sets self.language_server to the object
returned by factory.create_language_server(...) and then calls .start() on it. If
.start() raises after the underlying OS subprocess is already running (a capability
assertion, an initialize() timeout, a malformed LSP response during startup; several
of the bundled language servers can raise at that point), the collection loop only adds
a thread's server to language_servers when thread.exception is None:

for thread in threads:
    thread.join()
    if thread.exception is not None:
        exceptions[thread.ls_id] = thread.exception
    elif thread.language_server is not None:
        language_servers[thread.ls_id] = thread.language_server

The failure-path cleanup then only stops what made it into language_servers:

if exceptions:
    for ls in language_servers.values():
        ls.stop()
    ...

so the server that raised is never in that dict and .stop() is never called on it.
Its OS subprocess keeps running for the lifetime of the process (or until the OS reaps
it), even though the manager itself reports the whole startup as failed.

Reproduction

Using two duck-typed fake language servers (from_languages only calls
.start()/.is_running()/.stop(), never isinstance-checks the object), one that
starts a real subprocess and returns normally, one that starts a real subprocess and
then raises (mirroring a post-initialize() capability assert):

raised as expected: Failed to start 1 language server(s):
rust: simulated: capability assertion failed after initialize() (rust)
spawned pids: {'python': 10, 'rust': 9}
python pid=10 alive=False   # stopped by the cleanup loop
rust pid=9 alive=True       # leaked

Setup

  • Serena: current main (7fcbca7e)
  • OS: Linux (reproduced in a container; not OS-specific, from_languages has no
    platform-specific code on this path)
  • Not tied to a specific language server; any adapter that can raise after spawning its
    subprocess during startup triggers this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions