wip: attempt to replace websockets by SSE

Websockets were unnecessary, as we only need 1-way communication.
Instead, we switched to server-sent events. But there are a couple of
problems:

1. We still can't stop the server while a websocket connection is open
2. SSE make it difficult to format messages

So this is not a big win for now...
This commit is contained in:
Régis Behmo 2024-12-18 12:29:19 +01:00
parent a2e3018de4
commit 0565781c15
4 changed files with 60 additions and 44 deletions

View File

@ -9,9 +9,9 @@ import typing as t
import aiofiles
from quart import (
Quart,
make_response,
render_template,
request,
websocket,
redirect,
url_for,
)
@ -118,10 +118,7 @@ class TutorCli:
"""
Sets the stop flag, whic is monitored by all subprocess.Popen commands.
"""
app.logger.info(
"Stopping Tutor command: %s...",
self.command,
)
app.logger.info("Stopping Tutor command: %s...", self.command)
self._stop_flag.set()
async def iter_logs(self) -> t.AsyncGenerator[str, None]:
@ -273,11 +270,9 @@ class TutorCliPool:
replaced by another one, previous logs are not deleted. New ones are simply
appended.
"""
while True:
if cls.INSTANCE:
async for log in cls.INSTANCE.iter_logs():
yield log
await asyncio.sleep(SHORT_SLEEP_SECONDS)
while cls.INSTANCE:
async for log in cls.INSTANCE.iter_logs():
yield log
app = Quart(
@ -341,27 +336,46 @@ async def tutor_cli() -> WerkzeugResponse:
# ["config", "printvalue", "POUAC"],
# ["local", "launch", "--non-interactive"],
)
return redirect(url_for("tutor_logs"))
return redirect(url_for("tutor_cli_logs"))
@app.post("/tutor/cli/stop")
async def tutor_cli_stop() -> WerkzeugResponse:
TutorCliPool.stop()
return redirect(url_for("tutor_logs"))
return redirect(url_for("tutor_cli_logs"))
@app.get("/tutor/logs")
async def tutor_logs() -> str:
return await render_template("tutor_logs.html", **shared_template_context())
async def tutor_cli_logs() -> str:
return await render_template("tutor_cli_logs.html", **shared_template_context())
@app.websocket("/tutor/logs/stream")
async def tutor_logs_stream() -> None:
async for content in TutorCliPool.iter_logs():
try:
await websocket.send(content)
except asyncio.CancelledError:
return
@app.get("/tutor/cli/logs/stream")
async def tutor_cli_logs_stream() -> None:
# Websockets were not working for us in dev mode, we were unable to stop the server
# as long as there were open connection. We only need single-direction
# communication, so we use server-sent events
# https://github.com/pallets/quart/issues/333
# https://quart.palletsprojects.com/en/latest/how_to_guides/server_sent_events.html
async def send_events():
while True:
# TODO this is again causing the stream to never stop...
async for data in TutorCliPool.iter_logs():
event = f"data: {data}\nevent: logs\n"
# TODO encode one way or another to be able to send EOL characters and other weird chars
yield event.encode()
await asyncio.sleep(SHORT_SLEEP_SECONDS)
response = await make_response(
send_events(),
{
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Transfer-Encoding": "chunked",
},
)
response.timeout = None
return response
def shared_template_context() -> dict[str, t.Any]:

View File

@ -17,8 +17,8 @@
<link href="{{ url_for('static', filename='/css/dash.css') }}" rel="stylesheet">
<!-- TODO self-host -->
<script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js"></script>
<!-- WS extension https://htmx.org/extensions/ws/ TODO self host -->
<script src="https://unpkg.com/htmx-ext-ws@2.0.1/ws.js"></script>
<!-- SSE extension https://htmx.org/extensions/sse/ TODO self host and move to dedicated page-->
<script src="https://unpkg.com/htmx-ext-sse@2.2.2/sse.js"></script>
</head>
<body>
@ -31,7 +31,7 @@
<div class="content">
<ul>
<li>Configuration</li>
<li><a href="{{ url_for('tutor_logs') }}">Command logs</a></li>
<li><a href="{{ url_for('tutor_cli_logs') }}">Command logs</a></li>
</ul>
</div>
</div>

View File

@ -0,0 +1,22 @@
{% extends 'index.html' %}
{% block workspace_header %}Tutor command logs{% endblock %}
{% block workspace_content %}
<pre id="tutor-log" hx-ext="sse" sse-connect="{{ url_for('tutor_cli_logs_stream') }}" sse-swap="logs"></pre>
{% endblock %}
{% block scripts %}
<script>
// TODO fix me
htmx.on("htmx:sseBeforeMessage", function(evt) {
// Don't swap content, we want to append
evt.preventDefault();
// Note that HTML is automatically escaped
const text = document.createTextNode(evt.detail.data);
evt.detail.elt.appendChild(text);
evt.detail.elt.scrollTop = evt.detail.elt.scrollHeight;
});
</script>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends 'index.html' %}
{% block workspace_header %}Tutor command logs{% endblock %}
{% block workspace_content %}
<pre id="tutor-log" hx-ext="ws" ws-connect="{{ url_for('tutor_logs_stream') }}"></pre>
{% endblock %}
{% block scripts %}
<script>
htmx.on("htmx:wsAfterMessage", function(evt) {
const logs = document.getElementById("tutor-log");
// Note that HTML is automatically escaped
const text = document.createTextNode(evt.detail.message);
logs.appendChild(text);
logs.scrollTop = logs.scrollHeight;
});
</script>
{% endblock %}