add lsp_bridge.py: proper LSP-aware WebSocket bridge replacing websocat
websocat reads stdin line-by-line and waits for \n before sending each chunk. LSP JSON bodies have no trailing \n, causing a deadlock: websocat holds the body, the server waits for the body, LSP4IJ waits for the response. lsp_bridge.py reads complete Content-Length-framed messages before sending, then sends each as a single binary WebSocket frame. This fixes autocomplete in IntelliJ IDEA via LSP4IJ.
This commit is contained in:
93
scripts/lsp_bridge.py
Executable file
93
scripts/lsp_bridge.py
Executable file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
LSP WebSocket bridge: stdin/stdout (LSP Content-Length framed) <-> WebSocket.
|
||||||
|
|
||||||
|
websocat reads stdin line-by-line and holds the JSON body until a trailing \\n
|
||||||
|
arrives -- causing a deadlock on the first LSP message. This script solves that
|
||||||
|
by reading complete Content-Length-framed messages from stdin before sending.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
lsp_bridge.py <wss://host/path>
|
||||||
|
|
||||||
|
The bridge is transparent: it reads exactly one LSP message from stdin (header +
|
||||||
|
body), sends it as a single binary WebSocket frame, and writes any incoming
|
||||||
|
frames straight to stdout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_header(buf: bytes) -> tuple[int, int]:
|
||||||
|
"""Return (content_length, body_start_offset) or (-1, -1) if header incomplete."""
|
||||||
|
end = buf.find(b"\r\n\r\n")
|
||||||
|
if end == -1:
|
||||||
|
return -1, -1
|
||||||
|
content_length = 0
|
||||||
|
for line in buf[:end].split(b"\r\n"):
|
||||||
|
if line.lower().startswith(b"content-length:"):
|
||||||
|
content_length = int(line.split(b":")[1].strip())
|
||||||
|
return content_length, end + 4
|
||||||
|
|
||||||
|
|
||||||
|
async def _read_stdin_lsp(reader: asyncio.StreamReader) -> bytes | None:
|
||||||
|
"""Read one complete LSP-framed message from the async stdin reader."""
|
||||||
|
buf = b""
|
||||||
|
content_length = -1
|
||||||
|
body_start = -1
|
||||||
|
|
||||||
|
while True:
|
||||||
|
chunk = await reader.read(4096)
|
||||||
|
if not chunk:
|
||||||
|
return None
|
||||||
|
buf += chunk
|
||||||
|
|
||||||
|
if content_length == -1:
|
||||||
|
content_length, body_start = _parse_header(buf)
|
||||||
|
|
||||||
|
if content_length != -1 and len(buf) >= body_start + content_length:
|
||||||
|
return buf[: body_start + content_length]
|
||||||
|
|
||||||
|
|
||||||
|
async def main(uri: str) -> None:
|
||||||
|
ssl_ctx = ssl.create_default_context() if uri.startswith("wss://") else None
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
stdin_reader = asyncio.StreamReader()
|
||||||
|
await loop.connect_read_pipe(
|
||||||
|
lambda: asyncio.StreamReaderProtocol(stdin_reader), sys.stdin.buffer
|
||||||
|
)
|
||||||
|
|
||||||
|
async with websockets.connect(uri, ssl=ssl_ctx) as ws:
|
||||||
|
|
||||||
|
async def stdin_to_ws() -> None:
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
msg = await _read_stdin_lsp(stdin_reader)
|
||||||
|
if msg is None:
|
||||||
|
break
|
||||||
|
await ws.send(msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def ws_to_stdout() -> None:
|
||||||
|
try:
|
||||||
|
async for frame in ws:
|
||||||
|
data = frame if isinstance(frame, bytes) else frame.encode()
|
||||||
|
sys.stdout.buffer.write(data)
|
||||||
|
sys.stdout.buffer.flush()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
await asyncio.gather(stdin_to_ws(), ws_to_stdout())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: lsp_bridge.py <wss://...>", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
asyncio.run(main(sys.argv[1]))
|
||||||
Reference in New Issue
Block a user