WebHost: Better host room v2 (#3948)

* WebHost: add spinner to room command

and show error message if fetch fails due to NetworkError

* WebHost: don't update room log while tab is inactive

* WebHost: don't include log for automated requests

* WebHost: refresh room also for re-spinups

and do that from javascript

* Test, WebHost: send fake user-agent where required

* WebHost: remove wrong comment in host room
This commit is contained in:
black-sliver 2024-09-18 00:47:26 +02:00 committed by GitHub
parent 6fac83b84c
commit f73c0d9894
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 156 additions and 62 deletions

View File

@ -132,12 +132,12 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
return "Access Denied", 403 return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST']) @app.post("/room/<suuid:room>")
def host_room(room: UUID): def host_room_command(room: UUID):
room: Room = Room.get(id=room) room: Room = Room.get(id=room)
if room is None: if room is None:
return abort(404) return abort(404)
if request.method == "POST":
if room.owner == session["_id"]: if room.owner == session["_id"]:
cmd = request.form["cmd"] cmd = request.form["cmd"]
if cmd: if cmd:
@ -145,13 +145,28 @@ def host_room(room: UUID):
commit() commit()
return redirect(url_for("host_room", room=room.id)) return redirect(url_for("host_room", room=room.id))
@app.get("/room/<suuid:room>")
def host_room(room: UUID):
room: Room = Room.get(id=room)
if room is None:
return abort(404)
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port # indicate that the page should reload to get the assigned port
should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3) should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
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
def get_log(max_size: int = 1024000) -> str: browser_tokens = "Mozilla", "Chrome", "Safari"
automated = ("update" in request.args
or "Discordbot" in request.user_agent.string
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
def get_log(max_size: int = 0 if automated else 1024000) -> str:
if max_size == 0:
return ""
try: try:
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log: with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
raw_size = 0 raw_size = 0

View File

@ -58,3 +58,28 @@
overflow-y: auto; overflow-y: auto;
max-height: 400px; max-height: 400px;
} }
.loader{
display: inline-block;
visibility: hidden;
margin-left: 5px;
width: 40px;
aspect-ratio: 4;
--_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0);
background:
var(--_g) 0 50%,
var(--_g) 50% 50%,
var(--_g) 100% 50%;
background-size: calc(100%/3) 100%;
animation: l7 1s infinite linear;
}
.loader.loading{
visibility: visible;
}
@keyframes l7{
33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%}
50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%}
66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 }
}

View File

@ -19,6 +19,7 @@
{% block body %} {% block body %}
{% include 'header/grassHeader.html' %} {% include 'header/grassHeader.html' %}
<div id="host-room"> <div id="host-room">
<span id="host-room-info">
{% if room.owner == session["_id"] %} {% if room.owner == session["_id"] %}
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a> Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
<br /> <br />
@ -41,6 +42,7 @@
</span> </span>
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br> in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
{% endif %} {% endif %}
</span>
{{ 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;">
@ -49,6 +51,7 @@
<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"
placeholder="Server Command. /help to list them, list gets appended to log."> placeholder="Server Command. /help to list them, list gets appended to log.">
<span class="loader"></span>
</div> </div>
</form> </form>
<a href="{{ url_for("display_log", room=room.id) }}"> <a href="{{ url_for("display_log", room=room.id) }}">
@ -62,6 +65,7 @@
let url = '{{ url_for('display_log', room = room.id) }}'; let url = '{{ url_for('display_log', room = room.id) }}';
let bytesReceived = {{ log_len }}; let bytesReceived = {{ log_len }};
let updateLogTimeout; let updateLogTimeout;
let updateLogImmediately = false;
let awaitingCommandResponse = false; let awaitingCommandResponse = false;
let logger = document.getElementById("logger"); let logger = document.getElementById("logger");
@ -78,6 +82,8 @@
async function updateLog() { async function updateLog() {
try { try {
if (!document.hidden) {
updateLogImmediately = false;
let res = await fetch(url, { let res = await fetch(url, {
headers: { headers: {
'Range': `bytes=${bytesReceived}-`, 'Range': `bytes=${bytesReceived}-`,
@ -100,8 +106,13 @@
} }
logger.appendChild(document.createTextNode(text)); logger.appendChild(document.createTextNode(text));
scrollToBottom(logger); scrollToBottom(logger);
let loader = document.getElementById("command-form").getElementsByClassName("loader")[0];
loader.classList.remove("loading");
} }
} }
} else {
updateLogImmediately = true;
}
} }
finally { finally {
window.clearTimeout(updateLogTimeout); window.clearTimeout(updateLogTimeout);
@ -125,20 +136,62 @@
}); });
ev.preventDefault(); // has to happen before first await ev.preventDefault(); // has to happen before first await
form.reset(); form.reset();
let loader = form.getElementsByClassName("loader")[0];
loader.classList.add("loading");
try {
let res = await req; let res = await req;
if (res.ok || res.type === 'opaqueredirect') { if (res.ok || res.type === 'opaqueredirect') {
awaitingCommandResponse = true; awaitingCommandResponse = true;
window.clearTimeout(updateLogTimeout); window.clearTimeout(updateLogTimeout);
updateLogTimeout = window.setTimeout(updateLog, 100); updateLogTimeout = window.setTimeout(updateLog, 100);
} else { } else {
loader.classList.remove("loading");
window.alert(res.statusText); window.alert(res.statusText);
} }
} catch (e) {
console.error(e);
loader.classList.remove("loading");
window.alert(e.message);
}
} }
document.getElementById("command-form").addEventListener("submit", postForm); document.getElementById("command-form").addEventListener("submit", postForm);
updateLogTimeout = window.setTimeout(updateLog, 1000); updateLogTimeout = window.setTimeout(updateLog, 1000);
logger.scrollTop = logger.scrollHeight; logger.scrollTop = logger.scrollHeight;
document.addEventListener("visibilitychange", () => {
if (!document.hidden && updateLogImmediately) {
updateLog();
}
})
</script> </script>
{% endif %} {% endif %}
<script>
function updateInfo() {
let url = new URL(window.location.href);
url.search = "?update";
fetch(url)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
return res.text()
})
.then(text => new DOMParser().parseFromString(text, 'text/html'))
.then(newDocument => {
let el = newDocument.getElementById("host-room-info");
document.getElementById("host-room-info").innerHTML = el.innerHTML;
});
}
if (document.querySelector("meta[http-equiv='refresh']")) {
console.log("Refresh!");
window.addEventListener('load', function () {
for (let i=0; i<3; i++) {
window.setTimeout(updateInfo, Math.pow(2, i) * 2000); // 2, 4, 8s
}
window.stop(); // cancel meta refresh
})
}
</script>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -131,7 +131,8 @@ class TestHostFakeRoom(TestBase):
f.write(text) f.write(text)
with self.app.app_context(), self.app.test_request_context(): with self.app.app_context(), self.app.test_request_context():
response = self.client.get(url_for("host_room", room=self.room_id)) response = self.client.get(url_for("host_room", room=self.room_id),
headers={"User-Agent": "Mozilla/5.0"})
response_text = response.get_data(True) response_text = response.get_data(True)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("href=\"/seed/", response_text) self.assertIn("href=\"/seed/", response_text)