Preconditions:
Issue details:
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.
Preconditions:
Issue details:
Description
LanguageServerManager.from_languagesstarts one language server per requestedlanguage in a
StartLSThread. Each thread setsself.language_serverto the objectreturned by
factory.create_language_server(...)and then calls.start()on it. If.start()raises after the underlying OS subprocess is already running (a capabilityassertion, an
initialize()timeout, a malformed LSP response during startup; severalof the bundled language servers can raise at that point), the collection loop only adds
a thread's server to
language_serverswhenthread.exception is None:The failure-path cleanup then only stops what made it into
language_servers: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_languagesonly calls.start()/.is_running()/.stop(), never isinstance-checks the object), one thatstarts a real subprocess and returns normally, one that starts a real subprocess and
then raises (mirroring a post-
initialize()capability assert):Setup
main(7fcbca7e)from_languageshas noplatform-specific code on this path)
subprocess during startup triggers this.