WebHost: Better host room (#3496)
* add Range= to log, making responses a lot smaller for massive rooms * switch xhr to fetch * post the form using fetch if possible * also refresh log faster while waiting for command echo / response * do not follow redirect, saving a request * do not post empty body * smooth-scroll the log view * paste the log into the div when loading the HTML (up to 1MB, rest will be `fetch`ed) * fix duplicate charset in display_log response
This commit is contained in:
parent
52a13d38e9
commit
e95bb5ea56
|
@ -1,6 +1,6 @@
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
from typing import List, Dict, Union
|
from typing import Dict, Iterator, List, Tuple, Union
|
||||||
|
|
||||||
import jinja2.exceptions
|
import jinja2.exceptions
|
||||||
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
|
||||||
|
@ -97,25 +97,36 @@ def new_room(seed: UUID):
|
||||||
return redirect(url_for("host_room", room=room.id))
|
return redirect(url_for("host_room", room=room.id))
|
||||||
|
|
||||||
|
|
||||||
def _read_log(path: str):
|
def _read_log(path: str, offset: int = 0) -> Iterator[bytes]:
|
||||||
if os.path.exists(path):
|
with open(path, "rb") as log:
|
||||||
with open(path, encoding="utf-8-sig") as log:
|
marker = log.read(3) # skip optional BOM
|
||||||
|
if marker != b'\xEF\xBB\xBF':
|
||||||
|
log.seek(0, os.SEEK_SET)
|
||||||
|
log.seek(offset, os.SEEK_CUR)
|
||||||
yield from log
|
yield from log
|
||||||
else:
|
|
||||||
yield f"Logfile {path} does not exist. " \
|
|
||||||
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/log/<suuid:room>')
|
@app.route('/log/<suuid:room>')
|
||||||
def display_log(room: UUID):
|
def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
|
||||||
room = Room.get(id=room)
|
room = Room.get(id=room)
|
||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
if room.owner == session["_id"]:
|
if room.owner == session["_id"]:
|
||||||
file_path = os.path.join("logs", str(room.id) + ".txt")
|
file_path = os.path.join("logs", str(room.id) + ".txt")
|
||||||
if os.path.exists(file_path):
|
try:
|
||||||
return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8")
|
range_header = request.headers.get("Range")
|
||||||
return "Log File does not exist."
|
if range_header:
|
||||||
|
range_type, range_values = range_header.split('=')
|
||||||
|
start, end = map(str.strip, range_values.split('-', 1))
|
||||||
|
if range_type != "bytes" or end != "":
|
||||||
|
return "Unsupported range", 500
|
||||||
|
# NOTE: we skip Content-Range in the response here, which isn't great but works for our JS
|
||||||
|
return Response(_read_log(file_path, int(start)), mimetype="text/plain", status=206)
|
||||||
|
return Response(_read_log(file_path), mimetype="text/plain")
|
||||||
|
except FileNotFoundError:
|
||||||
|
return Response(f"Logfile {file_path} does not exist. "
|
||||||
|
f"Likely a crash during spinup of multiworld instance or it is still spinning up.",
|
||||||
|
mimetype="text/plain")
|
||||||
|
|
||||||
return "Access Denied", 403
|
return "Access Denied", 403
|
||||||
|
|
||||||
|
@ -139,7 +150,21 @@ def host_room(room: UUID):
|
||||||
with db_session:
|
with db_session:
|
||||||
room.last_activity = now # will trigger a spinup, if it's not already running
|
room.last_activity = now # will trigger a spinup, if it's not already running
|
||||||
|
|
||||||
return render_template("hostRoom.html", room=room, should_refresh=should_refresh)
|
def get_log(max_size: int = 1024000) -> str:
|
||||||
|
try:
|
||||||
|
raw_size = 0
|
||||||
|
fragments: List[str] = []
|
||||||
|
for block in _read_log(os.path.join("logs", str(room.id) + ".txt")):
|
||||||
|
if raw_size + len(block) > max_size:
|
||||||
|
fragments.append("…")
|
||||||
|
break
|
||||||
|
raw_size += len(block)
|
||||||
|
fragments.append(block.decode("utf-8"))
|
||||||
|
return "".join(fragments)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/favicon.ico')
|
@app.route('/favicon.ico')
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
{{ macros.list_patches_room(room) }}
|
{{ macros.list_patches_room(room) }}
|
||||||
{% if room.owner == session["_id"] %}
|
{% if room.owner == session["_id"] %}
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: flex; align-items: center;">
|
||||||
<form method=post style="flex-grow: 1; margin-right: 1em;">
|
<form method="post" id="command-form" style="flex-grow: 1; margin-right: 1em;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="cmd"></label>
|
<label for="cmd"></label>
|
||||||
<input class="form-control" type="text" id="cmd" name="cmd"
|
<input class="form-control" type="text" id="cmd" name="cmd"
|
||||||
|
@ -55,24 +55,89 @@
|
||||||
Open Log File...
|
Open Log File...
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="logger"></div>
|
{% set log = get_log() -%}
|
||||||
<script type="application/ecmascript">
|
{%- set log_len = log | length - 1 if log.endswith("…") else log | length -%}
|
||||||
let xmlhttp = new XMLHttpRequest();
|
<div id="logger" style="white-space: pre">{{ log }}</div>
|
||||||
|
<script>
|
||||||
let url = '{{ url_for('display_log', room = room.id) }}';
|
let url = '{{ url_for('display_log', room = room.id) }}';
|
||||||
|
let bytesReceived = {{ log_len }};
|
||||||
|
let updateLogTimeout;
|
||||||
|
let awaitingCommandResponse = false;
|
||||||
|
let logger = document.getElementById("logger");
|
||||||
|
|
||||||
xmlhttp.onreadystatechange = function () {
|
function scrollToBottom(el) {
|
||||||
if (this.readyState === 4 && this.status === 200) {
|
let bot = el.scrollHeight - el.clientHeight;
|
||||||
document.getElementById("logger").innerText = this.responseText;
|
el.scrollTop += Math.ceil((bot - el.scrollTop)/10);
|
||||||
|
if (bot - el.scrollTop >= 1) {
|
||||||
|
window.clearTimeout(el.scrollTimer);
|
||||||
|
el.scrollTimer = window.setTimeout(() => {
|
||||||
|
scrollToBottom(el)
|
||||||
|
}, 16);
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function request_new() {
|
|
||||||
xmlhttp.open("GET", url, true);
|
|
||||||
xmlhttp.send();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setTimeout(request_new, 1000);
|
async function updateLog() {
|
||||||
window.setInterval(request_new, 10000);
|
try {
|
||||||
|
let res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'Range': `bytes=${bytesReceived}-`,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
let text = await res.text();
|
||||||
|
if (text.length > 0) {
|
||||||
|
awaitingCommandResponse = false;
|
||||||
|
if (bytesReceived === 0 || res.status !== 206) {
|
||||||
|
logger.innerHTML = '';
|
||||||
|
}
|
||||||
|
if (res.status !== 206) {
|
||||||
|
bytesReceived = 0;
|
||||||
|
} else {
|
||||||
|
bytesReceived += new Blob([text]).size;
|
||||||
|
}
|
||||||
|
if (logger.innerHTML.endsWith('…')) {
|
||||||
|
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
|
||||||
|
}
|
||||||
|
logger.appendChild(document.createTextNode(text));
|
||||||
|
scrollToBottom(logger);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
window.clearTimeout(updateLogTimeout);
|
||||||
|
updateLogTimeout = window.setTimeout(updateLog, awaitingCommandResponse ? 500 : 10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postForm(ev) {
|
||||||
|
/** @type {HTMLInputElement} */
|
||||||
|
let cmd = document.getElementById("cmd");
|
||||||
|
if (cmd.value === "") {
|
||||||
|
ev.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/** @type {HTMLFormElement} */
|
||||||
|
let form = document.getElementById("command-form");
|
||||||
|
let req = fetch(form.action || window.location.href, {
|
||||||
|
method: form.method,
|
||||||
|
body: new FormData(form),
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
ev.preventDefault(); // has to happen before first await
|
||||||
|
form.reset();
|
||||||
|
let res = await req;
|
||||||
|
if (res.ok || res.type === 'opaqueredirect') {
|
||||||
|
awaitingCommandResponse = true;
|
||||||
|
window.clearTimeout(updateLogTimeout);
|
||||||
|
updateLogTimeout = window.setTimeout(updateLog, 100);
|
||||||
|
} else {
|
||||||
|
window.alert(res.statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("command-form").addEventListener("submit", postForm);
|
||||||
|
updateLogTimeout = window.setTimeout(updateLog, 1000);
|
||||||
|
logger.scrollTop = logger.scrollHeight;
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue