Skip to content

Commit

Permalink
Add WebSocket server (#129)
Browse files Browse the repository at this point in the history
* server: Add WebSocket server option

* CONTRIBUTORS: Add myself to contributors

* docs: Update standalone docs with websocket server
  • Loading branch information
MatejKastak authored Jun 16, 2021
1 parent 4cb8dcc commit 75c9b15
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 9 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions docs/source/pages/advanced_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~

Expand Down
13 changes: 13 additions & 0 deletions examples/json-extension/.vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down
8 changes: 7 additions & 1 deletion examples/json-extension/server/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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()

Expand Down
16 changes: 10 additions & 6 deletions pygls/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ def __init__(self, server):
self.transport = None
self._message_buf = []

self._send_only_body = False

def __call__(self):
return self

Expand Down Expand Up @@ -370,16 +372,18 @@ 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)
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'
).encode(self.CHARSET)

self.transport.write(header + body)
self.transport.write(header + body)
else:
self.transport.write(body.decode('utf-8'))
except Exception:
logger.error(traceback.format_exc())

Expand Down
57 changes: 55 additions & 2 deletions pygls/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
# limitations under the License. #
############################################################################
import asyncio
import json
import logging
import re
import sys
Expand All @@ -30,7 +31,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__)
Expand Down Expand Up @@ -97,6 +98,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, 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.ensure_future(self._ws.send(data))


class Server:
"""Class that represents async server. It can be started using TCP or IO.
Expand Down Expand Up @@ -196,7 +216,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 %s:%s', host, port)

self._stop_event = Event()
self._server = self.loop.run_until_complete(
Expand All @@ -209,6 +229,39 @@ def start_tcp(self, host, port):
finally:
self.shutdown()

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))

self._stop_event = Event()
self.lsp._send_only_body = True # Don't send headers within the payload

async def connection_made(websocket, _):
"""Handle new connection wrapped in the WebSocket."""
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)."""
Expand Down
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ exclude =
tests.*

[options.extras_require]
ws =
websockets==9.*
dev =
bandit==1.6.0
flake8==3.7.7
Expand Down
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ envlist = py{36,37,38,39}

[testenv]
extras =
ws
test
dev
commands =
Expand Down

0 comments on commit 75c9b15

Please sign in to comment.