fix: start nothing from a page newer than its ./ksim web
./ksim web reads web/index.html from disk on every request but runs the code it started with. After the Teach a domain update, a server from before it got the page's new start request, did not know "teach", and started its default instead: a full 20-task exam. Report "stale" in /api/session: whether ksim or sim/*.py changed on disk since the server started. The page treats a server that sends no "stale" as stale too, since it predates the check. While the server is stale, the lobby says to restart it and launch() starts no session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
accb26159e
commit
95f9d1b7ee
10
CHANGELOG.md
10
CHANGELOG.md
@ -4,6 +4,16 @@ Notable changes to kubestronaut-sim, newest first. A version is the `VERSION` st
|
||||
(year.month.day.n); `./ksim help` and the top bar of the web page show the one you are running.
|
||||
Every entry names the commit it came in.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Fixed
|
||||
|
||||
- Choosing *Teach a domain* could start a full exam. `./ksim web` reads `web/index.html` afresh on
|
||||
every request but keeps running the code it started with, so a server from before the update
|
||||
read the new request as its default, a full exam. The server now reports when its code has
|
||||
changed on disk since it started (a server from before this check counts as changed). The
|
||||
lobby then asks you to restart it and starts nothing until you do.
|
||||
|
||||
## 2026.10.26.2 — 2026-10-26
|
||||
|
||||
Everything below came in `e852419`.
|
||||
|
||||
13
ksim
13
ksim
@ -2165,9 +2165,17 @@ def cmd_reset(args):
|
||||
print(("rebuilt " if not problem else "problem rebuilding ") + f"Q{q['id']:02d}: {q['title']}")
|
||||
|
||||
# --------------------------------------------------------------- web ----
|
||||
def code_stamp():
|
||||
"""When the simulator's own code (ksim, sim/*.py) last changed on disk."""
|
||||
sim_dir = os.path.join(ROOT, "sim")
|
||||
return max(os.path.getmtime(f) for f in [os.path.join(ROOT, "ksim")]
|
||||
+ [os.path.join(sim_dir, n) for n in os.listdir(sim_dir) if n.endswith(".py")])
|
||||
|
||||
|
||||
class Api(http.server.BaseHTTPRequestHandler):
|
||||
"""Serves web/index.html plus a small JSON API. State lives in the same files the CLI uses."""
|
||||
lock = threading.Lock()
|
||||
started_code = None # code_stamp() when ./ksim web started
|
||||
log_lines = []
|
||||
seeding = False
|
||||
seed_error = None
|
||||
@ -2345,6 +2353,10 @@ class Api(http.server.BaseHTTPRequestHandler):
|
||||
"k8s": k8s_version(), "pending": len(PENDING), "drilled": drilled_domains(), "sets": {k: {"name": v, "count": len(set_questions(k))} for k, v in SETS.items()},
|
||||
"seen": sorted(last_seen()),
|
||||
"seeding": Api.seeding, "seed_error": Api.seed_error,
|
||||
# index.html is read from disk on every request, the server's code only when it started:
|
||||
# after an update the page may ask for what this server does not know (an old one read
|
||||
# "teach a domain" as a full exam), so the page starts nothing until it is restarted
|
||||
"stale": Api.started_code is not None and code_stamp() > Api.started_code,
|
||||
"session": None,
|
||||
}
|
||||
payload.update(self.path_payload())
|
||||
@ -2444,6 +2456,7 @@ def cmd_web(args):
|
||||
except envs.EnvError as e:
|
||||
Api.desktop_error = str(e)
|
||||
print(yellow(f" no desktop: {e}"))
|
||||
Api.started_code = code_stamp()
|
||||
http.server.ThreadingHTTPServer.allow_reuse_address = True
|
||||
srv = http.server.ThreadingHTTPServer(("127.0.0.1", port), Api) # type: ignore[arg-type]
|
||||
print(bold(f"\n {TRACK_ID.upper()} exam UI: http://127.0.0.1:{port} (ksim {VERSION})\n")
|
||||
|
||||
@ -397,6 +397,7 @@ try { const t = localStorage.getItem('ksim.theme'); if (t === 'light' || t === '
|
||||
<div class="scrollhint">SCROLL FOR YOUR PATH AND OBJECTIVES</div>
|
||||
</section>
|
||||
<main class="wrap">
|
||||
<div class="alert alert-warning hidden" id="staleNote"></div>
|
||||
<div class="alert alert-danger hidden" id="lobbyErr"></div>
|
||||
<div class="alert alert-info hidden" id="lobbyNote"></div>
|
||||
<section class="card" id="pathCard">
|
||||
@ -596,6 +597,9 @@ try { const t = localStorage.getItem('ksim.theme'); if (t === 'light' || t === '
|
||||
const fmtHMS = s => { s = Math.max(0, Math.floor(s)); return [s / 3600, s % 3600 / 60, s % 60].map(pad).join(':'); };
|
||||
const esc = s => String(s).replace(/[&<>"]/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"'}[c]));
|
||||
const oldServer = what => `This ./ksim web was started before "${what}" existed. Stop it (Ctrl-C) and start it again.`;
|
||||
// a server that sends no "stale" predates the check, so it is older than this page as well
|
||||
const STALE = 'The simulator was updated after this ./ksim web started, and the server still runs the old code: it would misread what this page asks for (an old one starts a full exam instead of a domain\'s lessons). Stop it (Ctrl-C), start it again, and reload this page.';
|
||||
const stale = () => S && S.stale !== false;
|
||||
const openModal = id => $(id).classList.remove('hidden');
|
||||
const closeModal = id => $(id).classList.add('hidden');
|
||||
const live = () => !!(S && S.session && !S.session.ended_at);
|
||||
@ -683,6 +687,7 @@ try { const t = localStorage.getItem('ksim.theme'); if (t === 'light' || t === '
|
||||
$$('#trackPick [data-track]').forEach(el => el.onclick = () => switchTrack(el.dataset.track));
|
||||
$('#ver').textContent = 'ksim ' + (S.version || '?') + (S.pending ? ` · ${S.pending} task(s) held back` : '');
|
||||
$('#foot').textContent = `ksim ${S.version || '?'} · Apache-2.0 · not affiliated with the CNCF or the Linux Foundation`;
|
||||
$('#staleNote').textContent = STALE; $('#staleNote').classList.toggle('hidden', !stale());
|
||||
$('#heroTitle').textContent = T.name;
|
||||
$('#heroSub').textContent = `Kubernetes ${T.k8s}. Every task runs on its own cluster that you reach with ssh from a remote desktop, and every session runs against the clock.`;
|
||||
$('#hardHero').checked = hard;
|
||||
@ -891,6 +896,7 @@ try { const t = localStorage.getItem('ksim.theme'); if (t === 'light' || t === '
|
||||
}
|
||||
}
|
||||
async function launch(body) {
|
||||
if (stale()) { closeModal('#mPicker'); return lobbyError(STALE); }
|
||||
closeModal('#mPicker'); $('#lobbyErr').classList.add('hidden'); $('#lobbyNote').classList.add('hidden');
|
||||
$('#prepMsg').textContent = 'starting…'; $('#prepLog').textContent = ''; openModal('#mPrep'); polling = true;
|
||||
try { await api('/api/start', body); await waitSeeded(); ready(true); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user