v3.28.0: the batch CAP install

The bulk workflow: one .cap and one set of install parameters applied to
card after card - swap cards and each one installs as soon as it is
equipped.  The server owns the loop, so closing the tab or reloading the
page does not interrupt it.

The install chain is now shared: _ram_install_plan(body) builds the
card-independent part (the parsed .cap, the INSTALL/LOAD sequence, the
block size, the transport knobs) and _ram_install_run(server, body, plan,
scc) runs one chain (the per-card preset/keyset/counter resolution
included); the /api/ram-install handler is the plan + run + response, and
the batch worker calls the same pair.  The plan is built once at Start,
so a bad .cap or parameter combination is a 400 there (a malformed
archive included - a zip parse error is the caller's, never a dropped
connection), not a per-card failure.  _op_keyset_resolve() is the module
form of the handler's _op_keyset() (the dead _keyset_guard method is
gone).

The worker (_ram_batch_worker, state _RAM_BATCH) polls the card state
every 0.5 s and acts on a new card session while connected; a card
already equipped at Start installs immediately.  Per card the preset is
matched by ICCID and its ISD role TAR supplies the keyset (v3.26.0) and
the counter seed (v3.20.0); a card with no readable ICCID, no matching
preset or no usable ISD keyset is recorded as skipped with its reason.
A card is attempted once per run (any outcome): re-equipping it records
"already processed in this batch (<outcome>)" and never installs again -
restarting the batch is the retry.  Failures carry the failing step and
the remote SW, are never retried and never stop the run; an unexpected
card-handling error is recorded and the loop keeps waiting.

Endpoints: POST /api/ram-batch/start (no card needed; 400/409), GET
/api/ram-batch/status (card-free - the panel polls it during an install),
POST /api/ram-batch/stop (the first _CARD_FREE_POST: it only sets a flag,
so the button works while the worker holds the card lock; a running
install always finishes first) and POST /api/ram-batch/clear (409 while
active).  /api/status carries the ram_batch summary (the detailed state
minus the attempts list), so the panel's headline - the live phase, the
current card, the counts - rides the 2 s poll; the attempts report (the
last 500 kept, the total counted) is fetched only when it changed.  The
batch and a test run exclude each other (409 both ways); the state is in
memory - a server restart ends a run.

PWA: the Batch install (.cap file, per card) RAM operation shares the
install form (ramInstallCapFields(); the single install uses it too and
now sends ram_format, and the block-size validation moved before the
modal), hides the preset selector + Execute and the preset-mismatch
banner, and renders Start/Stop/Clear, the phase headline (equip the next
card / installing card + live step / stopping after the current card),
the counts and the attempts table (newest first).  The panel only works
inside the visible SCP80 RAM pill.

Docs: help EN/RU, api.md (a section + the four table rows), the repo
AGENTS.  Tests: the state/report helpers, the per-card decisions, the
worker loop (a real thread + a fake install), the lifecycle and guards,
the HTTP endpoints (the malformed .cap, the stop under a held card lock,
the test-run refusal) and the PWA panel helpers/wiring.  914 frontend +
919 Python; sw cache simple-v438.
This commit is contained in:
2026-10-14 15:00:03 +03:00
parent 21d7f58f61
commit ab373aff30
13 changed files with 1527 additions and 236 deletions
+1
View File
@@ -323,6 +323,7 @@
<tbody>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Компактный листинг содержит только AID, состояние жизненного цикла и привилегии (версии в устаревшем формате нет); если карта отвергает его (<code class="font-mono text-sm">P2.b2=0</code>, устаревший по GP), запросы повторяются в расширенном формате <code class="font-mono text-sm">E3</code>, а флажок <strong>Запрашивать расширенные данные реестра (связи load-файла и SD)</strong> дополнительно запрашивает их и объединяет расширенные поля: версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей, пункты меню STK апплета (SCP Registry Data, TS 102 226 8.2.1.6) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) &mdash; он стоит несколько дополнительных защищённых пакетов на каждый обзор и <strong>включён по умолчанию</strong> (запоминается в браузере; снимите флажок, чтобы пропустить). Карты различаются в том, что принимают, поэтому расширенный запрос пробуется в трёх формах &mdash; эталонный список тегов (с <code class="font-mono text-sm">EA</code> по TS 102 226), тот же список без <code class="font-mono text-sm">EA</code> и вовсе без списка тегов (форма GlobalPlatformPro) &mdash; а обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Если строки ELF всё ещё без версий, формы со списком тегов повторяются для обоих P1=10 и P1=20, пропуская уже отклонённые комбинации, и обход останавливается, как только версия найдена. Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded).</td></tr>
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1&ndash;240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong>: <strong>требование NVRAM</strong> (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны <strong>библиотеки, с которыми слинкован CAP</strong> (его компонент Import: имя библиотеки, AID и минимальная версия <code class="font-mono text-sm">≥ x.y</code> — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> и <strong>Components</strong>. Строка <strong>Platform</strong> показывает минимальный уровень Java Card, который должна предоставить карта (выводится из импорта <code class="font-mono text-sm">javacard.framework</code>; флаг <code class="font-mono text-sm">int</code> в заголовке дополнительно требует поддержки int — Java Card 2.2+), и <strong>релиз ETSI</strong>, из которого пришли импорты UICC API (версия <code class="font-mono text-sm">uicc.toolkit</code> &rarr; REL-N по корпусу <code class="font-mono text-sm">sim-tools</code> в рабочем каталоге; основание показано в скобках): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента <strong>Import</strong> (в строке шага виден remote SW карты и компонент, который завершил блок, например <em>завершает Import</em>): подсказка при ошибке и попап предлагают <strong>тест совместимости CAP</strong> (в один клик — тот же <code class="font-mono text-sm">.cap</code> уже выбран), чтобы найти отсутствующий пакет и версию; меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый <strong>C6</strong> следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Пакетная установка (.cap, по картам)</td><td class="py-1 px-2">Массовый сценарий: один <code class="font-mono text-sm">.cap</code> и один набор параметров установки, применяемые карта за картой. <strong>Запустить пакетную установку</strong> не требует карты &mdash; цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты <strong>пресет находится по ICCID</strong> и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт &mdash; пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается <strong>один раз за запуск</strong>: повторная установка карты даёт «уже обработана» &mdash; для повтора перезапустите пакетную установку. <strong>Остановить</strong> завершает текущую карту (прерванная цепочка LOAD может оставить полузагруженный пакет), а затем завершает запуск; <strong>Очистить</strong> удаляет отчёт завершённого запуска. Цикл работает на сервере, поэтому закрытие вкладки, переключение представлений или перезагрузка страницы его не прерывают; перезапуск сервера завершает запуск. Пакетная установка и тест-скрипт исключают друг друга.</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Проверка совместимости CAP</td><td class="py-1 px-2">Отвечает на один вопрос, ничего не устанавливая: <strong>может ли эта карта удовлетворить список импортов CAP?</strong> Сервер выполняет проверку формата, INSTALL[for load] и блоки LOAD только до блока, завершающего компонент <strong>Import</strong> &mdash; именно там JCRE проверяет список импортов &mdash; и останавливается: без флага последнего блока и без INSTALL[for install], поэтому ничего не фиксируется и удалять после этого ничего не нужно. Список <strong>«Версии импортов для проверки»</strong> предзаполняется из анализа CAP: <code class="font-mono text-sm">AID=version</code> переписывает запрошенную версию импорта, <code class="font-mono text-sm">+AID=version</code> добавляет синтетический импорт (запрос возможностей карты: LOAD проходит, только если карта экспортирует этот пакет версии &ge; указанной), а <strong>«Все 0.0»</strong> снимает все требования. Использование в два шага: нажмите «Все 0.0» &mdash; если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только <strong>этап LOAD/импортов</strong>: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install]. Запуск и результат показываются в окне (как при установке): строки шагов, вердикт и блок деталей по каждому AID &mdash; при отказе он называет раздел CAP, который покрывает неудачный блок (диапазон байтов load-файла и компоненты, и завершает ли этот блок <strong>Import</strong>), SW команды с его значением, список импортов ровно в том виде, в каком он проверялся (версии CAP, правки пробы, добавленные синтетические записи, строки без совпадений) и что означает состояние пробы; при успехе — список импортов, который прошёл проверку.</td></tr>
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">INSTALL [for install] (загруженный пакет)</td><td class="py-1 px-2">Один APDU INSTALL [for install] для уже загруженного на карту пакета (его Load File AID и Module AID, привилегии, параметры установки/STK и необязательное <em>make selectable</em>) &mdash; параметры установки можно перебирать, не удаляя и не загружая <code class="font-mono text-sm">.cap</code> заново.</td></tr>
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">Один APDU INSTALL [for make selectable] (AID экземпляра) для установленного, но невыбираемого экземпляра апплета.</td></tr>