mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 16:15:33 +00:00
Fix: 1397 Expert deadlock by sending didOpen(mix.exs) before readiness wait (#1405)
* Fix: Expert deadlock by sending didOpen(mix.exs) before readiness wait Expert's build pipeline only starts after receiving a textDocument/didOpen notification. Without it, Serena waited 300 s for $/progress signals that Expert never emitted, causing a deadlock on every cold start.
This commit is contained in:
1 parent
18e7e63070
commit
fc27a5204e
3 files changed
+131
No files matched your search
@@ -45,6 +45,7 @@ Status of the `main` branch. Changes prior to the next official version change w
|
||||
- Fix: `rename_symbol` for Vue files now correctly propagates edits to the TypeScript server, enabling cross-file renames in `.vue` files
|
||||
- Fix: Lean4 stale cache — empty document symbol responses (returned before `lake build` completes) are no longer persisted, preventing symbols from being permanently hidden #1356
|
||||
- Add JSON language server support via `vscode-json-languageserver` (experimental) #1391
|
||||
- Fix: Elixir/Expert deadlock on startup — Expert's build pipeline requires a `textDocument/didOpen` notification to start; Serena now opens `mix.exs` immediately after `initialized` so Expert begins compiling instead of waiting indefinitely #1397
|
||||
|
||||
Dashboard:
|
||||
- Add configurable dashboard interface mode (new global configuration setting `web_dashboard_interface`):
|
||||
|
||||
@@ -360,6 +360,28 @@ class ElixirTools(SolidLanguageServer):
|
||||
|
||||
self.server.notify.initialized({})
|
||||
|
||||
# Expert's build pipeline is triggered by a textDocument/didOpen notification.
|
||||
# Without it Expert sits idle and never emits the $/progress signals we wait for,
|
||||
# causing a deadlock. Opening mix.exs is the minimal trigger; we close it again
|
||||
# once the server is ready so it does not linger in the open-file set.
|
||||
mix_exs_path = os.path.join(self.repository_root_path, "mix.exs")
|
||||
mix_exs_uri = pathlib.Path(mix_exs_path).as_uri() if os.path.exists(mix_exs_path) else None
|
||||
|
||||
if mix_exs_uri is not None:
|
||||
with open(mix_exs_path, encoding="utf-8") as f:
|
||||
mix_exs_content = f.read()
|
||||
self.server.notify.did_open_text_document(
|
||||
{
|
||||
"textDocument": {
|
||||
"uri": mix_exs_uri,
|
||||
"languageId": "elixir",
|
||||
"version": 1,
|
||||
"text": mix_exs_content,
|
||||
}
|
||||
}
|
||||
)
|
||||
log.debug("Opened mix.exs to trigger Expert's build pipeline")
|
||||
|
||||
# Expert needs time to compile the project and build indexes on first run.
|
||||
# This can take 2-3+ minutes for mid-sized codebases.
|
||||
# After the first run, subsequent startups are much faster.
|
||||
@@ -370,3 +392,7 @@ class ElixirTools(SolidLanguageServer):
|
||||
else:
|
||||
log.warning(f"Expert did not signal readiness within {ready_timeout}s. Proceeding with requests anyway.")
|
||||
self.server_ready.set() # Mark as ready anyway to allow requests
|
||||
|
||||
if mix_exs_uri is not None:
|
||||
self.server.notify.did_close_text_document({"textDocument": {"uri": mix_exs_uri}})
|
||||
log.debug("Closed mix.exs after Expert is ready")
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Unit tests for Elixir Expert language server startup behavior."""
|
||||
|
||||
import threading
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from solidlsp.language_servers.elixir_tools.elixir_tools import ElixirTools
|
||||
|
||||
|
||||
def _make_elixir_tools(tmp_path):
|
||||
language_server = object.__new__(ElixirTools)
|
||||
language_server.repository_root_path = str(tmp_path)
|
||||
language_server.server_ready = threading.Event()
|
||||
language_server.request_id = 0
|
||||
return language_server
|
||||
|
||||
|
||||
def _make_mock_server():
|
||||
server = MagicMock()
|
||||
server.send.initialize.return_value = {"capabilities": {"textDocumentSync": {}}}
|
||||
return server
|
||||
|
||||
|
||||
class TestElixirToolsStartup:
|
||||
"""Tests for ElixirTools._start_server() startup behavior — the Expert deadlock fix."""
|
||||
|
||||
def test_sends_did_open_for_mix_exs_after_initialized(self, tmp_path):
|
||||
"""Expert blocks waiting for a didOpen notification to trigger its build pipeline."""
|
||||
(tmp_path / "mix.exs").write_text("defmodule App.MixProject do\n use Mix.Project\nend\n", encoding="utf-8")
|
||||
|
||||
language_server = _make_elixir_tools(tmp_path)
|
||||
server = _make_mock_server()
|
||||
language_server.server = server
|
||||
language_server.server_ready.set()
|
||||
|
||||
language_server._start_server()
|
||||
|
||||
calls = server.notify.did_open_text_document.call_args_list
|
||||
assert len(calls) == 1, f"Expected one didOpen notification for mix.exs, got {len(calls)}"
|
||||
text_doc = calls[0][0][0]["textDocument"]
|
||||
assert text_doc["uri"].endswith("mix.exs"), f"Expected mix.exs URI, got {text_doc['uri']}"
|
||||
assert text_doc["languageId"] == "elixir"
|
||||
|
||||
def test_did_open_contains_mix_exs_content(self, tmp_path):
|
||||
"""The didOpen notification must include the file content so Expert can parse it."""
|
||||
content = "defmodule App.MixProject do\n use Mix.Project\nend\n"
|
||||
(tmp_path / "mix.exs").write_text(content, encoding="utf-8")
|
||||
|
||||
language_server = _make_elixir_tools(tmp_path)
|
||||
server = _make_mock_server()
|
||||
language_server.server = server
|
||||
language_server.server_ready.set()
|
||||
|
||||
language_server._start_server()
|
||||
|
||||
calls = server.notify.did_open_text_document.call_args_list
|
||||
assert len(calls) == 1
|
||||
text_doc = calls[0][0][0]["textDocument"]
|
||||
assert text_doc["text"] == content
|
||||
|
||||
def test_closes_mix_exs_after_server_ready(self, tmp_path):
|
||||
"""mix.exs is closed after Expert signals readiness so it does not linger open."""
|
||||
(tmp_path / "mix.exs").write_text("defmodule App.MixProject do\n use Mix.Project\nend\n", encoding="utf-8")
|
||||
|
||||
language_server = _make_elixir_tools(tmp_path)
|
||||
server = _make_mock_server()
|
||||
language_server.server = server
|
||||
language_server.server_ready.set()
|
||||
|
||||
language_server._start_server()
|
||||
|
||||
close_calls = server.notify.did_close_text_document.call_args_list
|
||||
assert len(close_calls) == 1, f"Expected one didClose for mix.exs, got {len(close_calls)}"
|
||||
text_doc = close_calls[0][0][0]["textDocument"]
|
||||
assert text_doc["uri"].endswith("mix.exs"), f"Expected mix.exs URI, got {text_doc['uri']}"
|
||||
|
||||
def test_startup_event_order_initialized_then_didopen_then_didclose(self, tmp_path):
|
||||
"""Initialized → didOpen(mix.exs) → [ready] → didClose(mix.exs) is the required sequence."""
|
||||
(tmp_path / "mix.exs").write_text("defmodule App.MixProject do\n use Mix.Project\nend\n", encoding="utf-8")
|
||||
|
||||
events: list[str] = []
|
||||
|
||||
language_server = _make_elixir_tools(tmp_path)
|
||||
server = _make_mock_server()
|
||||
language_server.server = server
|
||||
language_server.server_ready.set()
|
||||
|
||||
server.notify.initialized.side_effect = lambda *_: events.append("initialized")
|
||||
server.notify.did_open_text_document.side_effect = lambda *_: events.append("didOpen")
|
||||
server.notify.did_close_text_document.side_effect = lambda *_: events.append("didClose")
|
||||
|
||||
language_server._start_server()
|
||||
|
||||
assert events == ["initialized", "didOpen", "didClose"], f"Unexpected startup event order: {events}"
|
||||
|
||||
def test_no_crash_when_mix_exs_missing(self, tmp_path):
|
||||
"""_start_server must not crash if mix.exs is absent (non-standard project layout)."""
|
||||
language_server = _make_elixir_tools(tmp_path)
|
||||
server = _make_mock_server()
|
||||
language_server.server = server
|
||||
language_server.server_ready.set()
|
||||
|
||||
language_server._start_server()
|
||||
|
||||
server.notify.did_open_text_document.assert_not_called()
|
||||
Reference in new issue
Block a user