feat: RAM operations run behind a blocking modal (v3.6.34)
A CAP install is a long chain of LOAD commands whose progress and results were only visible at the bottom of a large form - and an interrupted install can leave a half-loaded package. - Server: _RAM_PROGRESS (+ _ram_progress_begin/step/end) records the live step of the running RAM operation; /api/ram-install and /api/ram-install-app wrap their chains in it (total = steps + 1 for the format check) and /api/status carries it as ram_progress - lock-free, so the 2 s poll works while the request holds _CARD_LOCK. - PWA: #ram-op-modal (the STK panel pattern) blocks everything while a RAM operation runs: title, spinner, status line, progress bar, per-step result lines, Close disabled until it finishes; a beforeunload guard protects the window during the run. install-cap, install-app / make-selectable and Explore all use the shared helpers (ramOpModalBegin/Status/Finish); the install results stay in the form area as before once the modal is closed. The status poll feeds the modal via ramOpProgress (headline from the pure ramOpProgressText). - Tests: ramOpProgressText (JS) + the progress lifecycle (Python); the card-state and capmem suites stub the DOM-bound hooks. - Stylesheet rebuilt; version 3.6.34; sw simple-v307.
This commit is contained in:
+103
-4
@@ -1710,11 +1710,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ram-op-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 max-w-lg w-full max-h-[85vh] flex flex-col">
|
||||
<div class="flex justify-between items-center mb-2 gap-3">
|
||||
<span id="ram-op-title" class="font-bold text-sm flex-1"></span>
|
||||
<button id="ram-op-close" onclick="ramOpModalClose()" disabled class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Close">Close</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-slate-400">
|
||||
<div id="ram-op-spin" class="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
|
||||
<span id="ram-op-status" data-l10n="Working...">Working...</span>
|
||||
</div>
|
||||
<div class="mt-2 w-full bg-gray-200 dark:bg-slate-700 rounded h-1.5">
|
||||
<div id="ram-op-bar" class="bg-blue-600 dark:bg-blue-500 h-1.5 rounded" style="width:0%"></div>
|
||||
</div>
|
||||
<div id="ram-op-steps" class="mt-2 text-xs font-mono whitespace-pre-wrap overflow-auto text-gray-700 dark:text-slate-300"></div>
|
||||
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400" id="ram-op-warn" data-l10n="Do not close or reload this window while the operation is running.">Do not close or reload this window while the operation is running.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ===== 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.6.33';
|
||||
const SIMPLE_VERSION = '3.6.34';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -8267,6 +8285,72 @@ function ramHideProgress() {
|
||||
document.getElementById('ram-progress').classList.add('hidden');
|
||||
}
|
||||
|
||||
// ===== RAM operation modal (v3.6.34) =====
|
||||
// Long RAM operations (install chains, Explore) run behind a modal that blocks
|
||||
// everything else - an interrupted install can leave a half-loaded package.
|
||||
// The step headline comes from /api/status.ram_progress (polled every 2 s),
|
||||
// so it works while the install request holds the card lock.
|
||||
let _ramOpActive = false;
|
||||
|
||||
function ramOpBeforeUnload(e) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
|
||||
function ramOpModalBegin(title) {
|
||||
_ramOpActive = true;
|
||||
document.getElementById('ram-op-title').textContent = title || '';
|
||||
document.getElementById('ram-op-status').textContent = t('Working...');
|
||||
document.getElementById('ram-op-status').className = 'text-sm text-gray-600 dark:text-slate-400';
|
||||
document.getElementById('ram-op-bar').style.width = '0%';
|
||||
document.getElementById('ram-op-steps').textContent = '';
|
||||
document.getElementById('ram-op-close').disabled = true;
|
||||
document.getElementById('ram-op-spin').classList.remove('hidden');
|
||||
document.getElementById('ram-op-warn').classList.remove('hidden');
|
||||
document.getElementById('ram-op-modal').classList.remove('hidden');
|
||||
window.addEventListener('beforeunload', ramOpBeforeUnload);
|
||||
}
|
||||
|
||||
function ramOpModalStatus(text) {
|
||||
if (!_ramOpActive) return;
|
||||
document.getElementById('ram-op-status').textContent = text || '';
|
||||
}
|
||||
|
||||
// Headline of the modal from a /api/status.ram_progress payload (pure).
|
||||
function ramOpProgressText(p) {
|
||||
if (!p) return '';
|
||||
const parts = [];
|
||||
parts.push(p.total ? t('Step') + ' ' + p.step + '/' + p.total : t('Working...'));
|
||||
if (p.name) parts.push(p.name);
|
||||
if (p.elapsed) parts.push(p.elapsed + ' s');
|
||||
return parts.join(' \u2014 ');
|
||||
}
|
||||
|
||||
function ramOpProgress(p) {
|
||||
if (!_ramOpActive || !p || !p.active) return;
|
||||
document.getElementById('ram-op-status').textContent = ramOpProgressText(p);
|
||||
document.getElementById('ram-op-bar').style.width =
|
||||
(p.total ? Math.min(100, Math.round(p.step * 100 / p.total)) : 10) + '%';
|
||||
}
|
||||
|
||||
function ramOpModalFinish(summary, ok, stepsText) {
|
||||
_ramOpActive = false;
|
||||
window.removeEventListener('beforeunload', ramOpBeforeUnload);
|
||||
document.getElementById('ram-op-spin').classList.add('hidden');
|
||||
document.getElementById('ram-op-bar').style.width = '100%';
|
||||
document.getElementById('ram-op-status').textContent = summary || '';
|
||||
document.getElementById('ram-op-status').className = 'text-sm font-semibold ' +
|
||||
(ok ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400');
|
||||
if (stepsText !== undefined) document.getElementById('ram-op-steps').textContent = stepsText || '';
|
||||
document.getElementById('ram-op-warn').classList.add('hidden');
|
||||
document.getElementById('ram-op-close').disabled = false;
|
||||
}
|
||||
|
||||
function ramOpModalClose() {
|
||||
if (_ramOpActive) return;
|
||||
document.getElementById('ram-op-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function ramClearResults() {
|
||||
_ramExplorerData = null;
|
||||
ramHideProgress();
|
||||
@@ -8992,6 +9076,8 @@ async function ramExplore(sp) {
|
||||
const mem = { appCount: null, freeNV: null, freeV: null };
|
||||
const isd = [], apps = [], elfs = [];
|
||||
|
||||
ramOpModalBegin(t('Explore Card (all GP data)'));
|
||||
|
||||
async function paginate(p1, collector, parser, label, quiet) {
|
||||
// Compact listing format (GP 11.4.2.2: P2.b2=0) with the chained GET
|
||||
// RESPONSE - see ramGetStatusApdu(). The card's SCP80 layer runs both
|
||||
@@ -9005,6 +9091,7 @@ async function ramExplore(sp) {
|
||||
while (guard++ < 32) {
|
||||
const apdu = ramGetStatusApdu(p1, p2);
|
||||
ramShowProgress(label + ' P1=' + p1 + ' P2=' + p2 + '...');
|
||||
ramOpModalStatus(label + ' P1=' + p1 + ' P2=' + p2);
|
||||
const res = await ramSendOta(apdu, Object.assign({}, sp, { cntr, spi2 }));
|
||||
// Reuse the format detected for the first packet of this run
|
||||
if (res.ram_format) sp.ram_format = res.ram_format;
|
||||
@@ -9089,6 +9176,7 @@ async function ramExplore(sp) {
|
||||
}
|
||||
|
||||
ramShowProgress(t('Card resources (GET DATA FF21)') + '...');
|
||||
ramOpModalStatus(t('Card resources (GET DATA FF21)'));
|
||||
try {
|
||||
const memRes = await ramSendOta('80CAFF2100', Object.assign({}, sp, { spi2: '01' }));
|
||||
if (memRes.ram_format) sp.ram_format = memRes.ram_format;
|
||||
@@ -9144,6 +9232,7 @@ async function ramExplore(sp) {
|
||||
|
||||
_ramExplorerData = { mem: mem, isd: isd, apps: apps, elfs: elfs, fmt: sp.ram_format || '' };
|
||||
ramRenderExplorer();
|
||||
ramOpModalFinish(resultEl.textContent, errors.length === 0, errors.join('\n'));
|
||||
}
|
||||
|
||||
// GP DELETE (Card Spec v2.3.1 11.2, Tables 11-20/11-23): P1=00 (last/only
|
||||
@@ -9292,9 +9381,11 @@ async function ramInstallCap(sp) {
|
||||
if (!file) { alert(t('Select a .cap file')); return; }
|
||||
if (file.size > 48 * 1024) { alert(t('CAP file exceeds 48 kB limit')); return; }
|
||||
|
||||
ramShowProgress(t('Reading CAP file...'));
|
||||
const capPkg = (_capAnalysis.ram.memory && _capAnalysis.ram.memory.package_aid) || '';
|
||||
ramOpModalBegin(t('Installing package') + (capPkg ? ' \u2014 ' + capPkg : ''));
|
||||
ramOpModalStatus(t('Reading CAP file...'));
|
||||
const capHex = await ramReadFileHex(file);
|
||||
ramShowProgress(t('Sending to server for install...'));
|
||||
ramOpModalStatus(t('Sending to server for install...'));
|
||||
|
||||
// The STK hex is regenerated on every toolkit field edit; recompute it
|
||||
// once more at send time unless the user pasted a custom hex directly
|
||||
@@ -9373,7 +9464,9 @@ async function ramInstallApp(sp, op) {
|
||||
spi1: sp.spi1, spi2: '01', kic: sp.kic, kid: sp.kid,
|
||||
tar: sp.tar, cntr: sp.cntr, kicKey: sp.kicKey, kidKey: sp.kidKey,
|
||||
};
|
||||
ramShowProgress(t('Sending to card...'));
|
||||
ramOpModalBegin(t(mode === 'make_selectable' ? 'INSTALL [for make selectable]' : 'INSTALL [for install]')
|
||||
+ ((instance || module) ? ' \u2014 ' + (instance || module) : ''));
|
||||
ramOpModalStatus(t('Sending to card...'));
|
||||
const data = await pysimFetch('/api/ram-install-app', body);
|
||||
ramHideProgress();
|
||||
ramShowInstallResult(data, t('OK'));
|
||||
@@ -9399,6 +9492,7 @@ function ramShowInstallResult(data, okText) {
|
||||
resultEl.textContent = t('Install FAILED at step:') + ' ' + data.failed_step + (data.error ? ' (' + data.error + ')' : '');
|
||||
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
|
||||
}
|
||||
ramOpModalFinish(resultEl.textContent, !!data.success, txt);
|
||||
}
|
||||
|
||||
async function ramExecute() {
|
||||
@@ -9427,6 +9521,7 @@ async function ramExecute() {
|
||||
resultEl.textContent = t('Error') + ': ' + e.message;
|
||||
resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600');
|
||||
ramHideProgress();
|
||||
if (_ramOpActive) ramOpModalFinish(resultEl.textContent, false, '');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11661,6 +11756,7 @@ async function pysimResetCardData(refreshStatus) {
|
||||
|
||||
function pysimCardStateUpdate(status) {
|
||||
if (!status || typeof status.connected !== 'boolean') return;
|
||||
ramOpProgress(status.ram_progress);
|
||||
_pysimServerAvailable = true;
|
||||
_pysimLastStatus = status;
|
||||
_pysimCardEquipped = !!status.connected;
|
||||
@@ -17593,6 +17689,9 @@ const LANG_RU = {
|
||||
'Start': 'Запустить',
|
||||
'Clear': 'Очистить',
|
||||
'Loading...': 'Загрузка...',
|
||||
'Close': 'Закрыть',
|
||||
'Installing package': 'Установка пакета',
|
||||
'Do not close or reload this window while the operation is running.': 'Не закрывайте и не перезагружайте окно, пока операция выполняется.',
|
||||
'Working...': 'Выполняется...',
|
||||
'APDU hex': 'APDU (hex)',
|
||||
'CAP file (max 48 kB)': 'CAP-файл (не более 48 кБ)',
|
||||
|
||||
@@ -819,6 +819,10 @@ video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.h-1\.5 {
|
||||
height: 0.375rem;
|
||||
}
|
||||
|
||||
.h-16 {
|
||||
height: 4rem;
|
||||
}
|
||||
@@ -992,6 +996,10 @@ video {
|
||||
max-width: 80rem;
|
||||
}
|
||||
|
||||
.max-w-lg {
|
||||
max-width: 32rem;
|
||||
}
|
||||
|
||||
.max-w-md {
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v306';
|
||||
const CACHE = 'simple-v307';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -31,6 +31,9 @@ let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n'
|
||||
+ 'var _pysimHeaderIccid = undefined;\nvar _pysimHeaderScp80 = undefined;\nvar _pysimHeaderScp81 = undefined;\n'
|
||||
+ 'var _cardsAutoIccid = null;\nvar _pysimCardIccid = null;\n';
|
||||
code += extractFunc(html, 'pysimCardStateUpdate') + '\n';
|
||||
// the status poll also feeds the RAM operation modal (DOM-bound, covered in
|
||||
// ram.test.js) - stub it for these state tests
|
||||
code += 'var ramOpProgress = function() {};\n';
|
||||
code += extractFunc(html, 'pysimRefresh', true) + '\n';
|
||||
code += extractFunc(html, 'pysimAvailabilityState') + '\n';
|
||||
code += extractFunc(html, 'pysimControlDisabled') + '\n';
|
||||
|
||||
@@ -25,7 +25,7 @@ function extractFunc(src, name) {
|
||||
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
|
||||
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
|
||||
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode',
|
||||
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode', 'ramOpProgressText',
|
||||
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
|
||||
let code = '';
|
||||
for (const f of FNS) {
|
||||
@@ -274,6 +274,16 @@ test('ramFmtPrivileges decodes the GP privilege bytes (live ISD values)', () =>
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('ramOpProgressText renders the step headline', () => {
|
||||
global.t = s => s;
|
||||
assert.strictEqual(
|
||||
ramOpProgressText({ step: 3, total: 11, name: 'LOAD (2/9)', elapsed: 4.2 }),
|
||||
'Step 3/11 \u2014 LOAD (2/9) \u2014 4.2 s');
|
||||
assert.strictEqual(ramOpProgressText({ step: 0, total: 0, name: '' }), 'Working...');
|
||||
assert.strictEqual(ramOpProgressText(null), '');
|
||||
delete global.t;
|
||||
});
|
||||
|
||||
test('ramCapToolkitMode picks CA/EA from the CAP linked libraries', () => {
|
||||
// sim.toolkit / sim.access (A0000000090003...) -> SIM Toolkit (CA field)
|
||||
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090003FFFFFFFF8910710002' }]), 'ca');
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "pysim-simple-server"
|
||||
version = "3.6.33"
|
||||
version = "3.6.34"
|
||||
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.
|
||||
|
||||
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
|
||||
from osmocom.utils import rpad
|
||||
|
||||
|
||||
VERSION = '3.6.33'
|
||||
VERSION = '3.6.34'
|
||||
|
||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||
|
||||
@@ -1208,6 +1208,37 @@ def _ram_detect_format(server, scc, sp, state):
|
||||
return 'compact'
|
||||
|
||||
|
||||
# Live progress of the RAM operation currently running. The long install
|
||||
# chains hold _CARD_LOCK for the whole request, so the PWA cannot learn the
|
||||
# step from the response - /api/status (lock-free cached state, polled every
|
||||
# 2 s) carries this dict instead (v3.6.34).
|
||||
_RAM_PROGRESS = {'active': False, 'kind': '', 'step': 0, 'total': 0,
|
||||
'name': '', 'started': 0.0}
|
||||
|
||||
|
||||
def _ram_progress_begin(kind, total):
|
||||
_RAM_PROGRESS.update({'active': True, 'kind': str(kind),
|
||||
'step': 0, 'total': int(total or 0), 'name': '',
|
||||
'started': time.time()})
|
||||
|
||||
|
||||
def _ram_progress_step(step, name):
|
||||
_RAM_PROGRESS.update({'step': int(step), 'name': str(name or '')})
|
||||
|
||||
|
||||
def _ram_progress_end():
|
||||
_RAM_PROGRESS['active'] = False
|
||||
|
||||
|
||||
def _ram_progress_payload():
|
||||
pr = _RAM_PROGRESS
|
||||
started = pr.get('started') or 0
|
||||
return {'active': bool(pr.get('active')), 'kind': pr.get('kind', ''),
|
||||
'step': int(pr.get('step') or 0), 'total': int(pr.get('total') or 0),
|
||||
'name': pr.get('name', ''),
|
||||
'elapsed': round(time.time() - started, 1) if started else 0}
|
||||
|
||||
|
||||
def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
|
||||
"""Assemble a RAM-install step record and its failure reason.
|
||||
|
||||
@@ -5161,6 +5192,7 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'card_session': int(getattr(self.server, 'card_session', 0)),
|
||||
'proactive_seq': _PROACTIVE_ENTRY_ID,
|
||||
'equipping': bool(getattr(self.server, 'equipping', False)),
|
||||
'ram_progress': _ram_progress_payload(),
|
||||
'auto_equip': bool(_AUTO_EQUIP),
|
||||
'card': card.name if card else None,
|
||||
'profile': str(rs.profile) if rs and rs.profile else None,
|
||||
@@ -6326,41 +6358,50 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'tar': tar, 'kic_key': kic_key, 'kid_key': kid_key,
|
||||
'include_cpi': body.get('includeCpi', True)}
|
||||
|
||||
# RAM command format: detected per operation (a read-only probe
|
||||
# step) unless the caller pinned compact/expanded (v3.6.24).
|
||||
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||
if ram_format == 'auto':
|
||||
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||
|
||||
# INSTALL [for load] -> LOAD blocks -> INSTALL [for install]
|
||||
seq = _cap_apdu_sequence(
|
||||
loadfile_aid, module_aid, loadfile_data, sd_aid=sd_aid,
|
||||
privileges=privileges_hex,
|
||||
install_params=install_params_hex, stk_params=stk_params_hex,
|
||||
make_selectable=make_selectable, block_size=block_size)
|
||||
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL, %s) loadfile_aid=%s\n' % (
|
||||
len(seq), len(seq) - 2, ram_format, loadfile_aid))
|
||||
for apdu_idx, gp_apdu in enumerate(seq):
|
||||
if apdu_idx == 0:
|
||||
step_name = 'INSTALL [for load]'
|
||||
elif apdu_idx == len(seq) - 1:
|
||||
step_name = 'INSTALL [for install]'
|
||||
else:
|
||||
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
||||
if not _ram_send_gp_apdu(self.server, scc, sp_state, state,
|
||||
step_name,
|
||||
_ram_format_apdu(gp_apdu, ram_format)):
|
||||
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
||||
'error': (state['encode_error'] or state['failure'].get('error')
|
||||
or ('%s failed' % step_name)),
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format,
|
||||
'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
||||
'load_block_size': block_size,
|
||||
'load_block_size_requested': block_size_req,
|
||||
'load_block_size_auto': not block_size_req}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
|
||||
# Live progress for the PWA's operation modal (the request holds
|
||||
# _CARD_LOCK until the chain is done; /api/status carries this)
|
||||
_ram_progress_begin('install-cap', len(seq) + 1)
|
||||
try:
|
||||
# RAM command format: detected per operation (a read-only
|
||||
# probe step) unless the caller pinned compact/expanded.
|
||||
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||
if ram_format == 'auto':
|
||||
_ram_progress_step(0, 'FORMAT CHECK')
|
||||
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||
sys.stderr.write('RAM-INSTALL: %d APDUs (INSTALL / %d x LOAD / INSTALL, %s) loadfile_aid=%s\n' % (
|
||||
len(seq), len(seq) - 2, ram_format, loadfile_aid))
|
||||
|
||||
for apdu_idx, gp_apdu in enumerate(seq):
|
||||
if apdu_idx == 0:
|
||||
step_name = 'INSTALL [for load]'
|
||||
elif apdu_idx == len(seq) - 1:
|
||||
step_name = 'INSTALL [for install]'
|
||||
else:
|
||||
step_name = 'LOAD (%d/%d)' % (apdu_idx, len(seq) - 2)
|
||||
_ram_progress_step(apdu_idx + 1, step_name)
|
||||
if not _ram_send_gp_apdu(self.server, scc, sp_state, state,
|
||||
step_name,
|
||||
_ram_format_apdu(gp_apdu, ram_format)):
|
||||
resp = {'success': False, 'steps': steps, 'failed_step': len(steps),
|
||||
'error': (state['encode_error'] or state['failure'].get('error')
|
||||
or ('%s failed' % step_name)),
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format,
|
||||
'load_file_aid': loadfile_aid, 'module_aid': module_aid,
|
||||
'load_block_size': block_size,
|
||||
'load_block_size_requested': block_size_req,
|
||||
'load_block_size_auto': not block_size_req}
|
||||
self._send_json(resp)
|
||||
self._log_resp(resp)
|
||||
return
|
||||
finally:
|
||||
_ram_progress_end()
|
||||
|
||||
resp = {'success': True, 'steps': steps, 'load_file_aid': loadfile_aid,
|
||||
'module_aid': module_aid, 'final_cntr': state['cntr'],
|
||||
@@ -6425,15 +6466,22 @@ class PysimHandler(BaseHTTPRequestHandler):
|
||||
'include_cpi': body.get('includeCpi', True)}
|
||||
state = {'steps': [], 'encode_error': None, 'failure': {},
|
||||
'cntr': sp_state['cntr']}
|
||||
# RAM command format: detected per operation (a read-only probe
|
||||
# step) unless the caller pinned compact/expanded (v3.6.24).
|
||||
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||
if ram_format == 'auto':
|
||||
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||
apdu = _ram_format_apdu(apdu, ram_format)
|
||||
sys.stderr.write('RAM-INSTALL-APP: %s (%s, %s) cntr=%s\n' % (
|
||||
apdu, step_name, ram_format, sp_state['cntr']))
|
||||
ok = _ram_send_gp_apdu(self.server, scc, sp_state, state, step_name, apdu)
|
||||
# Live progress for the PWA's operation modal (see /api/ram-install)
|
||||
_ram_progress_begin('install-app', 2)
|
||||
try:
|
||||
# RAM command format: detected per operation (a read-only
|
||||
# probe step) unless the caller pinned compact/expanded.
|
||||
ram_format = _ram_normalize_format(body.get('ram_format'))
|
||||
if ram_format == 'auto':
|
||||
_ram_progress_step(0, 'FORMAT CHECK')
|
||||
ram_format = _ram_detect_format(self.server, scc, sp_state, state)
|
||||
apdu = _ram_format_apdu(apdu, ram_format)
|
||||
_ram_progress_step(1, step_name)
|
||||
sys.stderr.write('RAM-INSTALL-APP: %s (%s, %s) cntr=%s\n' % (
|
||||
apdu, step_name, ram_format, sp_state['cntr']))
|
||||
ok = _ram_send_gp_apdu(self.server, scc, sp_state, state, step_name, apdu)
|
||||
finally:
|
||||
_ram_progress_end()
|
||||
resp = {'success': bool(ok), 'steps': state['steps'],
|
||||
'final_cntr': state['cntr'], 'ram_format': ram_format}
|
||||
if not ok:
|
||||
|
||||
@@ -1588,3 +1588,20 @@ class RamCommandFormatTests(unittest.TestCase):
|
||||
# only the accepted (expanded) probe advanced the 5-byte counter
|
||||
self.assertEqual(state['cntr'], '10000AAAC9')
|
||||
self.assertEqual(len(state['steps']), 2)
|
||||
|
||||
|
||||
class RamProgressTests(unittest.TestCase):
|
||||
"""Live RAM-operation progress exposed in /api/status (v3.6.34)."""
|
||||
|
||||
def test_progress_lifecycle_and_payload(self):
|
||||
from pysim_simple_server import server as srv
|
||||
srv._ram_progress_begin('install-cap', 5)
|
||||
p = srv._ram_progress_payload()
|
||||
self.assertTrue(p['active'])
|
||||
self.assertEqual((p['kind'], p['step'], p['total']), ('install-cap', 0, 5))
|
||||
srv._ram_progress_step(3, 'LOAD (2/3)')
|
||||
p = srv._ram_progress_payload()
|
||||
self.assertEqual((p['step'], p['name']), (3, 'LOAD (2/3)'))
|
||||
self.assertGreaterEqual(p['elapsed'], 0)
|
||||
srv._ram_progress_end()
|
||||
self.assertFalse(srv._ram_progress_payload()['active'])
|
||||
|
||||
Reference in New Issue
Block a user