v3.28.7: the batch panel freeze
ramBatchFetch's re-entrancy guard (_ramBatchFetching) is only cleared when the fetch settles, and ramBatchApplyStatus returned without rendering whenever it decided a fetch was due - so one stalled /api/ram-batch/status request (the browser's fetch has no timeout) froze the whole panel (counts, phase, notice, throbber) at the last successful render while the server kept installing (live 2026-10-14: 31 installs, the panel stuck at "Installed: 12"; the log showed all 31 attempts and a healthy /api/status cadence, so the data was there). - The summary render now runs on every poll before the fetch decision: the headline (phase, current card, counts, notice) always follows the 2 s /api/status poll; the detailed fetch only adds the attempts table. - A fetch guard older than RAM_BATCH_FETCH_STALE_MS (10 s) is released, so a stalled fetch cannot block the report forever - the next poll retries and the attempts table recovers. Reproduced with the page's real functions: 12 good fetches, the 13th hung -> the panel stuck at 12 (the reported number); with the fix it follows the summary to 31 and the stale guard retries. Tests: a hung fetch keeps the counts following the summary; a stale guard is released and retried (a fresh one is kept). 919 frontend + 928 Python; sw cache simple-v445.
This commit is contained in:
+15
-3
@@ -1910,7 +1910,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.28.6';
|
||||
const SIMPLE_VERSION = '3.28.7';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== view history (the browser Back/Forward buttons) =====
|
||||
@@ -10155,8 +10155,11 @@ function ramHideProgress() {
|
||||
// (the attempts list) is fetched only when the summary says it changed.
|
||||
let _ramBatchLast = null; // the last detailed /api/ram-batch/status
|
||||
let _ramBatchFetching = false;
|
||||
let _ramBatchFetchAt = 0; // when the in-flight fetch started (the watchdog)
|
||||
let _ramBatchSummary = null; // /api/status.ram_batch
|
||||
let _ramBatchProgress = null; // /api/status.ram_progress
|
||||
// A fetch that never settled must not block the report forever (v3.28.7).
|
||||
const RAM_BATCH_FETCH_STALE_MS = 10000;
|
||||
|
||||
function ramBatchPanelVisible() {
|
||||
const op = document.getElementById('ram-op');
|
||||
@@ -10167,6 +10170,7 @@ function ramBatchPanelVisible() {
|
||||
async function ramBatchFetch() {
|
||||
if (_ramBatchFetching) return;
|
||||
_ramBatchFetching = true;
|
||||
_ramBatchFetchAt = Date.now();
|
||||
try {
|
||||
_ramBatchLast = await pysimFetch('/api/ram-batch/status');
|
||||
} catch (e) { /* the next poll retries */ }
|
||||
@@ -10181,14 +10185,22 @@ function ramBatchApplyStatus(status) {
|
||||
_ramBatchSummary = status.ram_batch || null;
|
||||
_ramBatchProgress = status.ram_progress || null;
|
||||
if (!ramBatchPanelVisible()) return;
|
||||
// The summary carries the headline (the phase, the current card, the
|
||||
// counts, the notice): render it on every poll, even when a detailed fetch
|
||||
// is due or stuck - a hung fetch used to freeze the whole panel at the
|
||||
// last successful render (v3.28.7).
|
||||
ramBatchRender();
|
||||
if (_ramBatchFetching && Date.now() - _ramBatchFetchAt > RAM_BATCH_FETCH_STALE_MS) {
|
||||
// the in-flight fetch is stale (a stalled connection): release the
|
||||
// guard so this poll retries and the report can recover
|
||||
_ramBatchFetching = false;
|
||||
}
|
||||
const total = _ramBatchSummary ? (_ramBatchSummary.attempts_total || 0) : 0;
|
||||
const active = _ramBatchSummary ? !!_ramBatchSummary.active : false;
|
||||
if (!_ramBatchLast || (_ramBatchLast.attempts_total || 0) !== total
|
||||
|| !!_ramBatchLast.active !== active) {
|
||||
ramBatchFetch();
|
||||
return;
|
||||
}
|
||||
ramBatchRender();
|
||||
}
|
||||
|
||||
function ramBatchRender() {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v444';
|
||||
const CACHE = 'simple-v445';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -29,6 +29,9 @@ for (const fn of ['ramBatchPanelVisible', 'ramBatchAttemptsHtml', 'ramBatchRende
|
||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||
}
|
||||
|
||||
const RAM_BATCH_FETCH_STALE_MS = parseInt(
|
||||
/const RAM_BATCH_FETCH_STALE_MS = (\d+);/.exec(html)[1], 10);
|
||||
|
||||
const els = {};
|
||||
function setEl(id, value) {
|
||||
els[id] = {
|
||||
@@ -54,7 +57,11 @@ const app = new Function('els', `
|
||||
var t = k => k;
|
||||
var isViewVisible = () => sandbox.visibility;
|
||||
var alert = m => sandbox.alerts.push(String(m));
|
||||
var pysimFetch = async (p) => { sandbox.fetched.push(p); return sandbox.replies[p] || null; };
|
||||
var pysimFetch = async (p) => {
|
||||
sandbox.fetched.push(p);
|
||||
if (sandbox.hangs) return new Promise(() => {}); // a stalled connection
|
||||
return sandbox.replies[p] || null;
|
||||
};
|
||||
var pysimFetchOk = async (p, body) => {
|
||||
sandbox.fetched.push(p);
|
||||
sandbox.bodies = sandbox.bodies || {};
|
||||
@@ -68,12 +75,17 @@ const app = new Function('els', `
|
||||
var pysimApplyAvailability = () => {};
|
||||
var _ramBatchLast = null;
|
||||
var _ramBatchFetching = false;
|
||||
var _ramBatchFetchAt = 0;
|
||||
var _ramBatchSummary = null;
|
||||
var _ramBatchProgress = null;
|
||||
var RAM_BATCH_FETCH_STALE_MS = ${RAM_BATCH_FETCH_STALE_MS};
|
||||
${code}
|
||||
sandbox.getLast = () => _ramBatchLast;
|
||||
sandbox.setLast = v => { _ramBatchLast = v; };
|
||||
sandbox.getSummary = () => _ramBatchSummary;
|
||||
sandbox.isFetching = () => _ramBatchFetching;
|
||||
sandbox.setFetchingStale = ms => { _ramBatchFetching = true; _ramBatchFetchAt = Date.now() - ms; };
|
||||
sandbox.resetFetching = () => { _ramBatchFetching = false; };
|
||||
sandbox.getProgress = () => _ramBatchProgress;
|
||||
sandbox.ramBatchPanelVisible = ramBatchPanelVisible;
|
||||
sandbox.ramBatchAttemptsHtml = ramBatchAttemptsHtml;
|
||||
@@ -265,6 +277,65 @@ test('the panel headline follows the live summary (phase and step)', () => {
|
||||
assert.strictEqual(els['ram-batch-state'].textContent, 'Insert next card. Waiting for a card...');
|
||||
});
|
||||
|
||||
test('a hung detailed fetch does not freeze the counts (v3.28.7)', async () => {
|
||||
clearEls();
|
||||
setEl('ram-op', 'batch');
|
||||
setEl('ram-batch-panel', '');
|
||||
setEl('ram-batch-start', '');
|
||||
setEl('ram-batch-stop', '');
|
||||
setEl('ram-batch-clear', '');
|
||||
setEl('ram-batch-spin', '');
|
||||
setEl('ram-batch-state', '');
|
||||
setEl('ram-batch-notice', '');
|
||||
setEl('ram-batch-counts', '');
|
||||
setEl('ram-batch-attempts', '');
|
||||
app.hangs = true; // every detailed fetch stalls
|
||||
const sum = n => ({ ram_batch: { active: true, phase: 'waiting_card', attempts_total: n,
|
||||
counts: { installed: n, failed: 0, skipped: 0 } } });
|
||||
app.ramBatchApplyStatus(sum(12));
|
||||
await new Promise(r => setImmediate(r));
|
||||
assert.match(els['ram-batch-counts'].textContent, /Installed: 12/);
|
||||
// the fetch never settles, but the headline must follow the summary
|
||||
app.ramBatchApplyStatus(sum(31));
|
||||
await new Promise(r => setImmediate(r));
|
||||
assert.match(els['ram-batch-counts'].textContent, /Installed: 31/);
|
||||
app.hangs = false;
|
||||
});
|
||||
|
||||
test('a stale fetch guard is released so the report recovers (v3.28.7)', async () => {
|
||||
clearEls();
|
||||
setEl('ram-op', 'batch');
|
||||
setEl('ram-batch-panel', '');
|
||||
setEl('ram-batch-start', '');
|
||||
setEl('ram-batch-stop', '');
|
||||
setEl('ram-batch-clear', '');
|
||||
setEl('ram-batch-spin', '');
|
||||
setEl('ram-batch-state', '');
|
||||
setEl('ram-batch-notice', '');
|
||||
setEl('ram-batch-counts', '');
|
||||
setEl('ram-batch-attempts', '');
|
||||
app.fetched = [];
|
||||
app.replies['/api/ram-batch/status'] = { active: true, phase: 'waiting_card', attempts: [],
|
||||
attempts_total: 31, counts: { installed: 31, failed: 0, skipped: 0 } };
|
||||
// the guard stuck for 11 s: the poll releases it and retries
|
||||
app.setFetchingStale(11000);
|
||||
app.ramBatchApplyStatus({ ram_batch: { active: true, phase: 'waiting_card', attempts_total: 31,
|
||||
counts: { installed: 31, failed: 0, skipped: 0 } } });
|
||||
// the stale guard is released and this poll retries (the fetch is issued)
|
||||
await new Promise(r => setImmediate(r));
|
||||
assert.deepStrictEqual(app.fetched, ['/api/ram-batch/status']);
|
||||
assert.ok(!app.isFetching(), 'the guard must be clear again after the fetch');
|
||||
assert.match(els['ram-batch-counts'].textContent, /Installed: 31/);
|
||||
// a fresh guard is kept (no duplicate fetch)
|
||||
app.fetched = [];
|
||||
app.setFetchingStale(1000);
|
||||
app.ramBatchApplyStatus({ ram_batch: { active: true, phase: 'waiting_card', attempts_total: 31,
|
||||
counts: { installed: 31, failed: 0, skipped: 0 } } });
|
||||
await new Promise(r => setImmediate(r));
|
||||
assert.deepStrictEqual(app.fetched, []);
|
||||
app.resetFetching();
|
||||
});
|
||||
|
||||
test('the stop and clear calls hit their endpoints', async () => {
|
||||
clearEls();
|
||||
app.fetched = [];
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.28.6"
|
||||
version = "3.28.7"
|
||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||
requires-python = ">=3.8"
|
||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
||||
|
||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.28.6'
|
||||
VERSION = '3.28.7'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
|
||||
Reference in New Issue
Block a user