From 77c9809e79bde443c0515a18a9e1af5e09b93941 Mon Sep 17 00:00:00 2001 From: MatejKastak Date: Sun, 5 Jul 2020 23:39:46 +0200 Subject: [PATCH 1/8] server: Add WebSocket server option --- pygls/protocol.py | 17 +++++++++------ pygls/server.py | 55 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 8 deletions(-) diff --git a/pygls/protocol.py b/pygls/protocol.py index 64313e1a..c1aa310c 100644 --- a/pygls/protocol.py +++ b/pygls/protocol.py @@ -188,6 +188,8 @@ def __init__(self, server): self.transport = None self._message_buf = [] + self._send_only_data = False + def __call__(self): return self @@ -370,16 +372,19 @@ def _send_data(self, data): try: body = data.json(by_alias=True, exclude_unset=True) - logger.info('Sending data: %s', body) body = body.encode(self.CHARSET) - header = ( - f'Content-Length: {len(body)}\r\n' - f'Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n' - ).encode(self.CHARSET) - self.transport.write(header + body) + if not self._send_only_data: + header = ( + f'Content-Length: {len(body)}\r\n' + f'Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n' + ).encode(self.CHARSET) + + self.transport.write(header + body) + else: + self.transport.write(body.decode('utf-8')) except Exception: logger.error(traceback.format_exc()) diff --git a/pygls/server.py b/pygls/server.py index 9fa3247f..f9dc9906 100644 --- a/pygls/server.py +++ b/pygls/server.py @@ -15,6 +15,7 @@ # limitations under the License. # ############################################################################ import asyncio +import json import logging import re import sys @@ -23,6 +24,8 @@ from threading import Event from typing import Any, Callable, List, Optional, TypeVar +import websockets + from pygls import IS_WIN from pygls.lsp.types import (ApplyWorkspaceEditResponse, ClientCapabilities, ConfigCallbackType, ConfigurationParams, Diagnostic, MessageType, RegistrationParams, @@ -30,7 +33,7 @@ WorkspaceEdit) from pygls.lsp.types.window import ShowDocumentCallbackType, ShowDocumentParams from pygls.progress import Progress -from pygls.protocol import LanguageServerProtocol +from pygls.protocol import LanguageServerProtocol, deserialize_message from pygls.workspace import Workspace logger = logging.getLogger(__name__) @@ -97,6 +100,25 @@ def write(self, data): self.wfile.flush() +class WebSocketTransportAdapter: + """Protocol adapter which calls write method. + + Write method sends data via the WebSocket interface. + """ + + def __init__(self, ws: websockets.WebSocketServerProtocol, loop): + self._ws = ws + self._loop = loop + + def close(self) -> None: + """Stop the WebSocket server.""" + self._ws.close() + + def write(self, data: Any) -> None: + """Create a task to write specified data into a WebSocket.""" + asyncio.create_task(self._ws.send(data)) + + class Server: """Class that represents async server. It can be started using TCP or IO. @@ -196,7 +218,7 @@ def start_io(self, stdin=None, stdout=None): def start_tcp(self, host, port): """Starts TCP server.""" - logger.info('Starting server on %s:%s', host, port) + logger.info('Starting TCP server on {}:{}'.format(host, port)) self._stop_event = Event() self._server = self.loop.run_until_complete( @@ -209,6 +231,35 @@ def start_tcp(self, host, port): finally: self.shutdown() + def start_websocket(self, host, port, path='/'): + """Starts WebSocket server.""" + logger.info('Starting WebSocket server on {}:{}'.format(host, port)) + + # TODO: Handle the path + self._stop_event = Event() + self.lsp._send_only_data = True # Don't send headers within the payload + + async def connection_made(websocket, cur_path): + """Handle new connection wrapped in the WebSocket.""" + # TODO: How to handle the path argument? + self.lsp.transport = WebSocketTransportAdapter(websocket, self.loop) + async for message in websocket: + self.lsp._procedure_handler( + json.loads(message, object_hook=deserialize_message) + ) + + start_server = websockets.serve(connection_made, host, port) + self._server = start_server.ws_server + self.loop.run_until_complete(start_server) + + try: + self.loop.run_forever() + except (KeyboardInterrupt, SystemExit): + pass + finally: + self._stop_event.set() + self.shutdown() + @property def thread_pool(self) -> ThreadPool: """Returns thread pool instance (lazy initialization).""" From 157d08b674a19bfc529de89bb353d7e8eb7cd51a Mon Sep 17 00:00:00 2001 From: MatejKastak Date: Sun, 5 Jul 2020 23:52:05 +0200 Subject: [PATCH 2/8] CONTRIBUTORS: Add myself to contributors --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 2d7a00e8..f78a6f3c 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -7,6 +7,7 @@ - [DeathAxe](https://github.com/deathaxe) - [Denis Loginov](https://github.com/dinvlad) - [Jérome Perrin](https://github.com/perrinjerome) +- [Matej Kašťák](https://github.com/MatejKastak) - [Max O'Cull](https://github.com/Maxattax97) - [Samuel Roeca](https://github.com/pappasam) - [Tomoya Tanjo](https://github.com/tom-tan) From 20ee648975a693fff4c40b56c0236e770e018b17 Mon Sep 17 00:00:00 2001 From: MatejKastak Date: Sun, 19 Jul 2020 00:14:35 +0200 Subject: [PATCH 3/8] docs: Update standalone docs with websocket server --- docs/source/pages/advanced_usage.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/pages/advanced_usage.rst b/docs/source/pages/advanced_usage.rst index 00a4e230..17c58ca5 100644 --- a/docs/source/pages/advanced_usage.rst +++ b/docs/source/pages/advanced_usage.rst @@ -50,6 +50,22 @@ The code snippet below shows how to start the server in *STDIO* mode. server.start_io() +WEBSOCKET +^^^^^^^^^ + +WEBSOCKET connections are used when you want to expose language server to +browser based editors. + +The code snippet below shows how to start the server in *WEBSOCKET* mode. + +.. code:: python + + from pygls.server import LanguageServer + + server = LanguageServer() + + server.start_websocket('0.0.0.0', 1234) + Logging ~~~~~~~ From 2190b5ec2ef58097c0a412bd04c0792bc2c15167 Mon Sep 17 00:00:00 2001 From: MatejKastak Date: Sun, 19 Jul 2020 00:16:51 +0200 Subject: [PATCH 4/8] CHANGELOG: Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e5b4b28..63ba69cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,15 @@ and this project adheres to [Semantic Versioning][semver]. ### Added - Testing against Python 3.9 ([#186]) +- Websocket server implementation `start_websocket` for LSP ([#129]) ### Changed ### Fixed +[#186]: https://github.com/openlawlibrary/pygls/pull/186 +[#129]: https://github.com/openlawlibrary/pygls/pull/129 + ## [0.10.3] - 05/05/2021 ### Added From 6068321653fc1aae32f7d4fba04f3652cbf55b04 Mon Sep 17 00:00:00 2001 From: Daniel Elero Date: Tue, 15 Jun 2021 17:28:13 +0200 Subject: [PATCH 5/8] Rebase and minor updates --- examples/json-extension/.vscode/launch.json | 13 +++++++++++++ examples/json-extension/server/__main__.py | 8 +++++++- pygls/protocol.py | 5 ++--- pygls/server.py | 18 ++++++++++-------- setup.cfg | 2 ++ 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/examples/json-extension/.vscode/launch.json b/examples/json-extension/.vscode/launch.json index 1cb5440e..7ac28d73 100644 --- a/examples/json-extension/.vscode/launch.json +++ b/examples/json-extension/.vscode/launch.json @@ -29,6 +29,19 @@ "env": { "PYTHONPATH": "${workspaceFolder}" } + }, + { + "name": "Launch Server [WebSockets]", + "type": "python", + "request": "launch", + "module": "server", + "args": ["--ws"], + "justMyCode": false, + "python": "${command:python.interpreterPath}", + "cwd": "${workspaceFolder}", + "env": { + "PYTHONPATH": "${workspaceFolder}" + } } ], "compounds": [ diff --git a/examples/json-extension/server/__main__.py b/examples/json-extension/server/__main__.py index 08d40845..69a1e2ab 100644 --- a/examples/json-extension/server/__main__.py +++ b/examples/json-extension/server/__main__.py @@ -27,7 +27,11 @@ def add_arguments(parser): parser.add_argument( "--tcp", action="store_true", - help="Use TCP server instead of stdio" + help="Use TCP server" + ) + parser.add_argument( + "--ws", action="store_true", + help="Use WebSocket server" ) parser.add_argument( "--host", default="127.0.0.1", @@ -46,6 +50,8 @@ def main(): if args.tcp: json_server.start_tcp(args.host, args.port) + elif args.ws: + json_server.start_ws(args.host, args.port) else: json_server.start_io() diff --git a/pygls/protocol.py b/pygls/protocol.py index c1aa310c..f757ef4b 100644 --- a/pygls/protocol.py +++ b/pygls/protocol.py @@ -188,7 +188,7 @@ def __init__(self, server): self.transport = None self._message_buf = [] - self._send_only_data = False + self._send_only_body = False def __call__(self): return self @@ -375,8 +375,7 @@ def _send_data(self, data): logger.info('Sending data: %s', body) body = body.encode(self.CHARSET) - - if not self._send_only_data: + if not self._send_only_body: header = ( f'Content-Length: {len(body)}\r\n' f'Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n' diff --git a/pygls/server.py b/pygls/server.py index f9dc9906..43f94fa7 100644 --- a/pygls/server.py +++ b/pygls/server.py @@ -24,8 +24,6 @@ from threading import Event from typing import Any, Callable, List, Optional, TypeVar -import websockets - from pygls import IS_WIN from pygls.lsp.types import (ApplyWorkspaceEditResponse, ClientCapabilities, ConfigCallbackType, ConfigurationParams, Diagnostic, MessageType, RegistrationParams, @@ -106,7 +104,7 @@ class WebSocketTransportAdapter: Write method sends data via the WebSocket interface. """ - def __init__(self, ws: websockets.WebSocketServerProtocol, loop): + def __init__(self, ws, loop): self._ws = ws self._loop = loop @@ -231,17 +229,21 @@ def start_tcp(self, host, port): finally: self.shutdown() - def start_websocket(self, host, port, path='/'): + def start_ws(self, host, port): """Starts WebSocket server.""" + try: + import websockets + except ImportError: + logger.error('Run `pip install pygls[ws]` to install `websockets`.') + sys.exit(1) + logger.info('Starting WebSocket server on {}:{}'.format(host, port)) - # TODO: Handle the path self._stop_event = Event() - self.lsp._send_only_data = True # Don't send headers within the payload + self.lsp._send_only_body = True # Don't send headers within the payload - async def connection_made(websocket, cur_path): + async def connection_made(websocket, _): """Handle new connection wrapped in the WebSocket.""" - # TODO: How to handle the path argument? self.lsp.transport = WebSocketTransportAdapter(websocket, self.loop) async for message in websocket: self.lsp._procedure_handler( diff --git a/setup.cfg b/setup.cfg index 2d720ca5..8b86fb8c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,8 @@ exclude = tests.* [options.extras_require] +ws = + websockets==9.* dev = bandit==1.6.0 flake8==3.7.7 From 19923723e300ce007584bd5406ce9f547ff573cc Mon Sep 17 00:00:00 2001 From: Daniel Elero Date: Tue, 15 Jun 2021 17:31:47 +0200 Subject: [PATCH 6/8] Fix logger --- pygls/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygls/server.py b/pygls/server.py index 43f94fa7..dcb92296 100644 --- a/pygls/server.py +++ b/pygls/server.py @@ -216,7 +216,7 @@ def start_io(self, stdin=None, stdout=None): def start_tcp(self, host, port): """Starts TCP server.""" - logger.info('Starting TCP server on {}:{}'.format(host, port)) + logger.info('Starting TCP server on %s:%s', host, port) self._stop_event = Event() self._server = self.loop.run_until_complete( From 45758769f68b9bbcc189fafdab788c5ecd384a93 Mon Sep 17 00:00:00 2001 From: Daniel Elero Date: Tue, 15 Jun 2021 17:39:48 +0200 Subject: [PATCH 7/8] Fix support python 3.6 --- pygls/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pygls/server.py b/pygls/server.py index dcb92296..8999c4f9 100644 --- a/pygls/server.py +++ b/pygls/server.py @@ -114,7 +114,7 @@ def close(self) -> None: def write(self, data: Any) -> None: """Create a task to write specified data into a WebSocket.""" - asyncio.create_task(self._ws.send(data)) + asyncio.ensure_future(self._ws.send(data)) class Server: From f2d2be7c1a69d5707204cb72343ba8fc55f04274 Mon Sep 17 00:00:00 2001 From: Daniel Elero Date: Tue, 15 Jun 2021 17:42:42 +0200 Subject: [PATCH 8/8] Fix tox --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index b368b6ca..b1531016 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = py{36,37,38,39} [testenv] extras = + ws test dev commands =