Skip to content

Commit 2254156

Browse files
committed
feature(langserver): start the internal HTTP server only on demand
You can disable this behaviour with the new option `robotcode.documentationServer.startOnDemand`.
1 parent 726d5dc commit 2254156

File tree

5 files changed

+249
-202
lines changed

5 files changed

+249
-202
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,11 @@
746746
"default": 3199,
747747
"description": "Defines the endport for searching a free port for the documentation server."
748748
},
749+
"robotcode.documentationServer.startOnDemand": {
750+
"type": "boolean",
751+
"default": true,
752+
"description": "Starts the internal HTTP server only if needed."
753+
},
749754
"robotcode.completion.filterDefaultLanguage": {
750755
"type": "boolean",
751756
"default": false,

packages/language_server/src/robotcode/language_server/robotframework/configuration.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class WorkspaceConfig(ConfigBase):
4949
class DocumentationServerConfig(ConfigBase):
5050
start_port: int = 3100
5151
end_port: int = 3199
52+
start_on_demand: bool = True
5253

5354

5455
@config_section("robotcode.inlayHints")
Lines changed: 5 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,6 @@
1-
from __future__ import annotations
2-
3-
import contextlib
4-
import io
5-
import multiprocessing as mp
6-
import socket
7-
import threading
8-
import traceback
91
import urllib.parse
10-
from concurrent.futures import ProcessPoolExecutor
112
from dataclasses import dataclass
12-
from http import HTTPStatus
13-
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
14-
from os import PathLike
15-
from string import Template
16-
from threading import Thread
173
from typing import TYPE_CHECKING, Any, List, Optional, Tuple, Union, cast
18-
from urllib.parse import parse_qs, urlparse
194

205
from robot.parsing.lexer.tokens import Token
216

@@ -31,20 +16,14 @@
3116
from robotcode.core.uri import Uri
3217
from robotcode.core.utils.dataclasses import CamelSnakeMixin
3318
from robotcode.core.utils.logging import LoggingDescriptor
34-
from robotcode.core.utils.net import find_free_port
3519
from robotcode.jsonrpc2.protocol import rpc_method
3620
from robotcode.robot.diagnostics.entities import LibraryEntry
37-
from robotcode.robot.diagnostics.library_doc import (
38-
get_library_doc,
39-
get_robot_library_html_doc_str,
40-
resolve_robot_variables,
41-
)
21+
from robotcode.robot.diagnostics.library_doc import resolve_robot_variables
4222
from robotcode.robot.diagnostics.model_helper import ModelHelper
4323
from robotcode.robot.diagnostics.namespace import Namespace
4424
from robotcode.robot.utils.ast import get_node_at_position, range_from_token
4525

4626
from ...common.decorators import code_action_kinds
47-
from ..configuration import DocumentationServerConfig
4827
from .protocol_part import RobotLanguageServerProtocolPart
4928

5029
if TYPE_CHECKING:
@@ -56,190 +35,14 @@ class ConvertUriParams(CamelSnakeMixin):
5635
uri: str
5736

5837

59-
HTML_ERROR_TEMPLATE = Template(
60-
"""\n
61-
<!doctype html>
62-
<html>
63-
<head>
64-
<meta charset="utf-8"/>
65-
<title>${type}: ${message}</title>
66-
</head>
67-
<body>
68-
<div id="content">
69-
<h3>
70-
${type}: ${message}
71-
</h3>
72-
<pre>
73-
${stacktrace}
74-
</pre>
75-
</div>
76-
77-
</body>
78-
</html>
79-
"""
80-
)
81-
82-
MARKDOWN_TEMPLATE = Template(
83-
"""\
84-
<!doctype html>
85-
<html>
86-
<head>
87-
<meta charset="utf-8"/>
88-
<title>${name}</title>
89-
</head>
90-
<body>
91-
<template type="markdown" id="markdown-content">${content}</template>
92-
<div id="content"></div>
93-
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
94-
<script>
95-
document.getElementById('content').innerHTML =
96-
marked.parse(document.getElementById('markdown-content').content.textContent, {gfm: true});
97-
</script>
98-
</body>
99-
</html>
100-
"""
101-
)
102-
103-
104-
class LibDocRequestHandler(SimpleHTTPRequestHandler):
105-
_logger = LoggingDescriptor()
106-
107-
def log_message(self, format: str, *args: Any) -> None:
108-
self._logger.info(lambda: f"{self.address_string()} - {format % args}")
109-
110-
def log_error(self, format: str, *args: Any) -> None:
111-
self._logger.error(lambda: f"{self.address_string()} - {format % args}")
112-
113-
def list_directory(self, _path: Union[str, PathLike[str]]) -> io.BytesIO | None:
114-
self.send_error(
115-
HTTPStatus.FORBIDDEN,
116-
"You don't have permission to access this resource.",
117-
"Directory browsing is not allowed.",
118-
)
119-
return None
120-
121-
def do_GET(self) -> None: # noqa: N802
122-
query = parse_qs(urlparse(self.path).query)
123-
name = n[0] if (n := query.get("name", [])) else None
124-
args = n[0] if (n := query.get("args", [])) else None
125-
basedir = n[0] if (n := query.get("basedir", [])) else None
126-
type_ = n[0] if (n := query.get("type", [])) else None
127-
theme = n[0] if (n := query.get("theme", [])) else None
128-
129-
if name:
130-
try:
131-
if type_ in ["md", "markdown"]:
132-
libdoc = get_library_doc(
133-
name,
134-
tuple(args.split("::") if args else ()),
135-
base_dir=basedir if basedir else ".",
136-
)
137-
138-
def calc_md() -> str:
139-
tt = str.maketrans({"<": "&lt;", ">": "&gt;"})
140-
return libdoc.to_markdown(add_signature=False, only_doc=False, header_level=0).translate(tt)
141-
142-
data = MARKDOWN_TEMPLATE.substitute(content=calc_md(), name=name)
143-
144-
self.send_response(200)
145-
self.send_header("Content-type", "text/html")
146-
self.end_headers()
147-
148-
self.wfile.write(bytes(data, "utf-8"))
149-
else:
150-
with ProcessPoolExecutor(max_workers=1, mp_context=mp.get_context("spawn")) as executor:
151-
result = executor.submit(
152-
get_robot_library_html_doc_str,
153-
name,
154-
args,
155-
base_dir=basedir if basedir else ".",
156-
theme=theme,
157-
).result(600)
158-
159-
self.send_response(200)
160-
self.send_header("Content-type", "text/html")
161-
self.end_headers()
162-
163-
self.wfile.write(bytes(result, "utf-8"))
164-
except (SystemExit, KeyboardInterrupt):
165-
raise
166-
except BaseException as e:
167-
self.send_response(404)
168-
self.send_header("Content-type", "text/html")
169-
self.end_headers()
170-
171-
self.wfile.write(
172-
bytes(
173-
HTML_ERROR_TEMPLATE.substitute(
174-
type=type(e).__qualname__,
175-
message=str(e),
176-
stacktrace="".join(traceback.format_exc()),
177-
),
178-
"utf-8",
179-
)
180-
)
181-
182-
else:
183-
super().do_GET()
184-
185-
186-
class DualStackServer(ThreadingHTTPServer):
187-
def server_bind(self) -> None:
188-
# suppress exception when protocol is IPv4
189-
with contextlib.suppress(Exception):
190-
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
191-
return super().server_bind()
192-
193-
19438
class RobotCodeActionDocumentationProtocolPart(RobotLanguageServerProtocolPart, ModelHelper):
19539
_logger = LoggingDescriptor()
19640

197-
def __init__(self, parent: RobotLanguageServerProtocol) -> None:
41+
def __init__(self, parent: "RobotLanguageServerProtocol") -> None:
19842
super().__init__(parent)
199-
200-
parent.code_action.collect.add(self.collect)
201-
self.parent.on_initialized.add(self.server_initialized)
202-
self.parent.on_shutdown.add(self.server_shutdown)
203-
204-
self._documentation_server: Optional[ThreadingHTTPServer] = None
205-
self._documentation_server_lock = threading.RLock()
206-
self._documentation_server_port = 0
207-
20843
self.parent.commands.register_all(self)
20944

210-
def server_initialized(self, sender: Any) -> None:
211-
self._ensure_http_server_started()
212-
213-
def server_shutdown(self, sender: Any) -> None:
214-
with self._documentation_server_lock:
215-
if self._documentation_server is not None:
216-
self._documentation_server.shutdown()
217-
self._documentation_server = None
218-
219-
def _run_server(self) -> None:
220-
config = self.parent.workspace.get_configuration(DocumentationServerConfig)
221-
222-
self._documentation_server_port = find_free_port(config.start_port, config.end_port)
223-
224-
self._logger.debug(lambda: f"Start documentation server on port {self._documentation_server_port}")
225-
226-
with DualStackServer(("127.0.0.1", self._documentation_server_port), LibDocRequestHandler) as server:
227-
self._documentation_server = server
228-
try:
229-
server.serve_forever()
230-
except BaseException:
231-
self._documentation_server = None
232-
raise
233-
234-
def _ensure_http_server_started(self) -> None:
235-
with self._documentation_server_lock:
236-
if self._documentation_server is None:
237-
self._server_thread = Thread(
238-
name="documentation_server",
239-
target=self._run_server,
240-
daemon=True,
241-
)
242-
self._server_thread.start()
45+
parent.code_action.collect.add(self.collect)
24346

24447
@language_id("robotframework")
24548
@code_action_kinds([CodeActionKind.SOURCE])
@@ -406,7 +209,7 @@ def build_url(
406209

407210
url_args = "::".join(args) if args else ""
408211

409-
base_url = f"http://localhost:{self._documentation_server_port}"
212+
base_url = f"http://localhost:{self.parent.http_server.port}"
410213
params = urllib.parse.urlencode(
411214
{
412215
"name": name,
@@ -427,6 +230,6 @@ def _convert_uri(self, uri: str, *args: Any, **kwargs: Any) -> Optional[str]:
427230
if folder:
428231
path = real_uri.to_path().relative_to(folder.uri.to_path())
429232

430-
return f"http://localhost:{self._documentation_server_port}/{path.as_posix()}"
233+
return f"http://localhost:{self.parent.http_server.port}/{path.as_posix()}"
431234

432235
return None

0 commit comments

Comments
 (0)