Skip to content

Commit 06f550c

Browse files
committed
Implement pipe/socket transport for language server, Improve starting, stopping, restarting LSP client
1 parent 1d16a11 commit 06f550c

File tree

18 files changed

+766
-435
lines changed

18 files changed

+766
-435
lines changed

.vscode/launch.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@
2828
"host": "localhost",
2929
"port": 5678
3030
},
31-
"justMyCode": false
31+
"justMyCode": false,
32+
"subProcess": true,
33+
"showReturnValue": true,
3234
},
3335
{
3436
"name": "Python: Attach Prompt",

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ All notable changes to the "robotcode" extension will be documented in this file
77
### added
88
- Big speed improvements
99
- introduce some classes for threadsafe asyncio
10+
- Implement pipe/socket transport for language server
11+
- default is now pipe transport
12+
- Improve starting, stopping, restarting language server client, if ie. python environment changed, arguments changed or server crashed
1013

1114
### added
1215

log.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
#keys: root, robotcode_utils
77
#keys: root, diagnostics, document, jsonrpc2, jsonrpc2_message
88
#keys: root, diagnostics, robot_diagnostics, robocop_diagnostics
9-
keys: root, discovering
10-
#keys: root
9+
#keys: root, discovering
10+
keys: root
1111

1212
[formatters]
1313
#keys: detailed,simple,colored_simple

package-lock.json

Lines changed: 206 additions & 190 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"id": "builtin",
7272
"description": "built in library, keyword or variable"
7373
}
74-
],
74+
],
7575
"semanticTokenScopes": [
7676
{
7777
"language": "robotframework",
@@ -118,20 +118,19 @@
118118
"variable": [
119119
"variable.other.readwrite.robotframework"
120120
],
121-
"keywordCall": [
121+
"keywordCall": [
122122
"variable.function.keyword-call.robotframework"
123123
],
124-
"keywordCallInner": [
124+
"keywordCallInner": [
125125
"variable.function.keyword-call.inner.robotframework"
126126
],
127-
"nameCall": [
127+
"nameCall": [
128128
"variable.function.keyword-call.robotframework"
129129
],
130130
"continuation": [
131131
"punctuation.separator.continuation.robotframework"
132132
],
133-
"separator": [
134-
],
133+
"separator": [],
135134
"terminator": [
136135
"punctuation.terminator.robotframework"
137136
],
@@ -199,10 +198,12 @@
199198
},
200199
"robotcode.languageServer.mode": {
201200
"type": "string",
202-
"default": "stdio",
201+
"default": "pipe",
203202
"description": "Specifies the mode the language server is started. Requires a VSCode restart to take effect.",
204203
"enum": [
205204
"stdio",
205+
"pipe",
206+
"socket",
206207
"tcp"
207208
],
208209
"scope": "resource"
@@ -628,18 +629,18 @@
628629
"dependencies": {
629630
"ansi-colors": "^4.1.1",
630631
"vscode-debugadapter": "^1.51.0",
631-
"vscode-languageclient": "^7.0.0"
632+
"vscode-languageclient": "^8.0.0-next"
632633
},
633634
"devDependencies": {
634635
"@types/glob": "^7.2.0",
635636
"@types/mocha": "^9.0.0",
636637
"@types/node": "^14.16.0",
637638
"@types/vscode": "^1.61.0",
638-
"@typescript-eslint/eslint-plugin": "^5.8.0",
639-
"@typescript-eslint/parser": "^5.8.0",
640-
"eslint": "^8.5.0",
639+
"@typescript-eslint/eslint-plugin": "^5.9.0",
640+
"@typescript-eslint/parser": "^5.9.0",
641+
"eslint": "^8.6.0",
641642
"eslint-config-prettier": "^8.3.0",
642-
"eslint-plugin-import": "^2.25.3",
643+
"eslint-plugin-import": "^2.25.4",
643644
"eslint-plugin-jsx-a11y": "^6.5.1",
644645
"eslint-plugin-node": "^11.1.0",
645646
"eslint-plugin-prettier": "^4.0.0",
@@ -650,10 +651,10 @@
650651
"replace-in-file": "^6.3.2",
651652
"ts-loader": "^9.2.6",
652653
"typescript": "^4.5.4",
653-
"vsce": "^2.5.3",
654+
"vsce": "^2.6.3",
654655
"vscode-debugadapter-testsupport": "^1.51.0",
655656
"vscode-dts": "^0.3.3",
656-
"@vscode/test-electron": "^2.0.0",
657+
"@vscode/test-electron": "^2.0.1",
657658
"webpack": "^5.65.0",
658659
"webpack-cli": "^4.9.1"
659660
}

robotcode/jsonrpc2/protocol.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class RpcMethodEntry(NamedTuple):
143143
name: str
144144
method: Callable[..., Any]
145145
param_type: Optional[Type[Any]]
146+
cancelable: bool
146147

147148

148149
@runtime_checkable
@@ -159,12 +160,18 @@ def rpc_method(_func: _F) -> _F:
159160

160161

161162
@overload
162-
def rpc_method(*, name: Optional[str] = None, param_type: Optional[Type[Any]] = None) -> Callable[[_F], _F]:
163+
def rpc_method(
164+
*, name: Optional[str] = None, param_type: Optional[Type[Any]] = None, cancelable: bool = True
165+
) -> Callable[[_F], _F]:
163166
...
164167

165168

166169
def rpc_method(
167-
_func: Optional[_F] = None, *, name: Optional[str] = None, param_type: Optional[Type[Any]] = None
170+
_func: Optional[_F] = None,
171+
*,
172+
name: Optional[str] = None,
173+
param_type: Optional[Type[Any]] = None,
174+
cancelable: bool = True,
168175
) -> Callable[[_F], _F]:
169176
def _decorator(func: _F) -> Callable[[_F], _F]:
170177

@@ -182,7 +189,7 @@ def _decorator(func: _F) -> Callable[[_F], _F]:
182189
if real_name is None or not real_name:
183190
raise ValueError("name is empty.")
184191

185-
cast(RpcMethod, f).__rpc_method__ = RpcMethodEntry(real_name, f, param_type)
192+
cast(RpcMethod, f).__rpc_method__ = RpcMethodEntry(real_name, f, param_type, cancelable)
186193
return func
187194

188195
if _func is None:
@@ -248,6 +255,7 @@ def get_methods(obj: Any) -> Dict[str, RpcMethodEntry]:
248255
rpc_method.__rpc_method__.name,
249256
method,
250257
rpc_method.__rpc_method__.param_type,
258+
rpc_method.__rpc_method__.cancelable,
251259
)
252260
for method, rpc_method in map(
253261
lambda m1: (m1, cast(RpcMethod, m1)),
@@ -288,10 +296,12 @@ def methods(self) -> Dict[str, RpcMethodEntry]:
288296

289297
return self.__methods
290298

291-
def add_method(self, name: str, func: Callable[..., Any], param_type: Optional[Type[Any]] = None) -> None:
299+
def add_method(
300+
self, name: str, func: Callable[..., Any], param_type: Optional[Type[Any]] = None, cancelable: bool = True
301+
) -> None:
292302
self.__ensure_initialized()
293303

294-
self.__methods[name] = RpcMethodEntry(name, func, param_type)
304+
self.__methods[name] = RpcMethodEntry(name, func, param_type, cancelable)
295305

296306
def remove_method(self, name: str) -> Optional[RpcMethodEntry]:
297307
self.__ensure_initialized()
@@ -323,6 +333,7 @@ class ReceivedRequestEntry(NamedTuple):
323333
future: asyncio.Future[Any]
324334
request: Optional[Any]
325335
cancel_token: CancelationToken
336+
cancelable: bool
326337

327338

328339
class JsonRPCProtocolBase(asyncio.Protocol, ABC):
@@ -650,10 +661,13 @@ def handle_request(self, message: JsonRPCRequest) -> Optional[asyncio.Task[_T]]:
650661
cancel_token = CancelationToken()
651662

652663
task = create_sub_task(
653-
ensure_coroutine(e.method)(*params[0], cancel_token=cancel_token, **params[1]), name=message.method
664+
ensure_coroutine(e.method)(
665+
*params[0], **({"cancel_token": cancel_token} if e.cancelable else {}), **params[1]
666+
),
667+
name=message.method,
654668
)
655669
with self._received_request_lock:
656-
self._received_request[message.id] = ReceivedRequestEntry(task, message, cancel_token)
670+
self._received_request[message.id] = ReceivedRequestEntry(task, message, cancel_token, e.cancelable)
657671

658672
def done(t: asyncio.Task[Any]) -> None:
659673
try:
@@ -663,7 +677,6 @@ def done(t: asyncio.Task[Any]) -> None:
663677
except (SystemExit, KeyboardInterrupt):
664678
raise
665679
except JsonRPCErrorException as ex:
666-
self._logger.exception(ex)
667680
self.send_error(ex.code, ex.message, id=message.id, data=ex.data)
668681
except BaseException as e:
669682
self._logger.exception(e)
@@ -689,8 +702,9 @@ def cancel_request(self, id: Union[int, str, None]) -> None:
689702
@_logger.call
690703
async def cancel_all_received_request(self) -> None:
691704
for entry in self._received_request.values():
692-
if entry is not None and entry.future is not None and not entry.future.cancelled():
693-
entry.cancel_token.cancel()
705+
if entry is not None and entry.cancelable and entry.future is not None and not entry.future.cancelled():
706+
if entry.cancel_token:
707+
entry.cancel_token.cancel()
694708
entry.future.cancel()
695709

696710
@_logger.call

robotcode/jsonrpc2/server.py

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@
2020
from ..utils.logging import LoggingDescriptor
2121
from .protocol import JsonRPCException
2222

23-
__all__ = ["StdOutTransportAdapter", "JsonRpcServerMode", "TcpParams", "JsonRPCServer"]
23+
__all__ = ["JsonRpcServerMode", "TcpParams", "JsonRPCServer"]
2424

2525
TProtocol = TypeVar("TProtocol", bound=asyncio.Protocol)
2626

2727

28+
class NotSupportedError(Exception):
29+
pass
30+
31+
2832
class StdOutTransportAdapter(asyncio.Transport):
2933
def __init__(self, rfile: BinaryIO, wfile: BinaryIO) -> None:
3034
super().__init__()
@@ -43,6 +47,8 @@ def write(self, data: bytes) -> None:
4347
class JsonRpcServerMode(Enum):
4448
STDIO = "stdio"
4549
TCP = "tcp"
50+
SOCKET = "socket"
51+
PIPE = "pipe"
4652

4753

4854
class TcpParams(NamedTuple):
@@ -57,9 +63,11 @@ def __init__(
5763
self,
5864
mode: JsonRpcServerMode = JsonRpcServerMode.STDIO,
5965
tcp_params: TcpParams = TcpParams(None, 0),
66+
pipe_name: Optional[str] = None,
6067
):
6168
self.mode = mode
6269
self.tcp_params = tcp_params
70+
self.pipe_name = pipe_name
6371

6472
self._run_func: Optional[Callable[[], None]] = None
6573
self._server: Optional[asyncio.AbstractServer] = None
@@ -77,6 +85,10 @@ def start(self) -> None:
7785
self.start_stdio()
7886
elif self.mode == JsonRpcServerMode.TCP:
7987
self.start_tcp(self.tcp_params.host, self.tcp_params.port)
88+
elif self.mode == JsonRpcServerMode.PIPE:
89+
self.start_pipe(self.pipe_name)
90+
elif self.mode == JsonRpcServerMode.SOCKET:
91+
self.start_socket(self.tcp_params.port)
8092
else:
8193
raise JsonRPCException(f"Unknown server mode {self.mode}")
8294

@@ -105,11 +117,6 @@ def __exit__(
105117
def create_protocol(self) -> TProtocol:
106118
...
107119

108-
@_logger.call
109-
def shutdown_protocol(self, protocol: TProtocol) -> None:
110-
if self.mode == JsonRpcServerMode.STDIO and self._stdio_stop_event is not None:
111-
self._stdio_stop_event.set()
112-
113120
stdio_executor: Optional[ThreadPoolExecutor] = None
114121

115122
@_logger.call
@@ -135,11 +142,7 @@ async def aio_readline(rfile: BinaryIO, protocol: asyncio.Protocol) -> None:
135142
)
136143
protocol.data_received(data)
137144

138-
self._logger.debug("starting run_io_nonblocking")
139-
try:
140-
self.loop.run_until_complete(aio_readline(sys.__stdin__.buffer, protocol))
141-
finally:
142-
self._logger.debug("exiting run_io_nonblocking")
145+
self.loop.run_until_complete(aio_readline(transport.rfile, protocol))
143146

144147
self._run_func = run_io_nonblocking
145148

@@ -148,11 +151,38 @@ def start_tcp(self, host: Optional[str] = None, port: int = 0) -> None:
148151
self.mode = JsonRpcServerMode.TCP
149152

150153
self._server = self.loop.run_until_complete(
151-
self.loop.create_server(lambda: self.create_protocol(), host, port, reuse_address=True)
154+
self.loop.create_server(self.create_protocol, host, port, reuse_address=True)
152155
)
153156

154157
self._run_func = self.loop.run_forever
155158

159+
@_logger.call
160+
def start_pipe(self, pipe_name: Optional[str]) -> None:
161+
if pipe_name is None:
162+
raise ValueError("pipe name missing.")
163+
164+
self.mode = JsonRpcServerMode.PIPE
165+
166+
try:
167+
if sys.platform == "win32" and getattr(self.loop, "create_pipe_connection", None):
168+
self.loop.run_until_complete(
169+
self.loop.create_pipe_connection(self.create_protocol, pipe_name) # type: ignore
170+
)
171+
else:
172+
self.loop.run_until_complete(self.loop.create_unix_connection(self.create_protocol, pipe_name))
173+
except NotImplementedError:
174+
raise NotSupportedError("Pipe transport is not supported on this platform.")
175+
176+
self._run_func = self.loop.run_forever
177+
178+
@_logger.call
179+
def start_socket(self, port: int) -> None:
180+
self.mode = JsonRpcServerMode.SOCKET
181+
182+
self.loop.run_until_complete(self.loop.create_connection(self.create_protocol, port=port))
183+
184+
self._run_func = self.loop.run_forever
185+
156186
@_logger.call
157187
def run(self) -> None:
158188
if self._run_func is None:

0 commit comments

Comments
 (0)