Volkov: добавить Sprinter Commander

Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места.

Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
This commit is contained in:
2026-09-10 10:45:30 +03:00
parent 05bcd8197e
commit 8e389c03f8
870 changed files with 21310 additions and 25 deletions
@@ -0,0 +1,215 @@
# Sprinter Commander: результаты стабилизации P5
Дата: 8 сентября 2026 года.
Статус: `PASS` в MAME 0.288 / BIOS 3.06 / DSS 1.71.57 на HDD `D:`.
Release gate PoC 0.1 закрыт в эмуляторе: пройдены граничные размеры, серия из
20 копирований, 100 refresh, повторный lifecycle, расширенная матрица ошибок,
смена носителя и запуск реальной внешней программы. Проверка на настоящем
Sprinter остаётся отдельным аппаратным smoke-тестом.
## 20 copy и граничные размеры
`tests/p5_fixture.py` создаёт двадцать файлов `F00.BIN..F19.BIN`. В набор
обязательно входят размеры:
```text
0, 1, 4095, 4096, 65535, 65536, 1048613 байт
```
Остальные тринадцать файлов проверяют повторное использование copy job,
временных имён и файловых дескрипторов. Lua-сценарий последовательно вызывает
F5 двадцать раз, не меняя активную панель. После остановки MAME
`tests/check_p5_hdd.sh` извлекает оба дерева и сравнивает каждую пару
побайтно:
```text
PASS: 20/20 файлов совпали; все граничные размеры и >1 МБ пройдены.
```
Первый вариант расписания успел выполнить только 17 команд: кадр показывал
активный прогресс `F16.BIN`, когда Lua уже отпустил следующие клавиши. Это не
трактовалось как ошибка Commander. Финальный сценарий разносит команды с
учётом числа 4-КБ блоков и получает все двадцать результатов на CHD.
## Refresh и EMM lifecycle
После copy тот же процесс получает 100 отдельных `Ctrl+R` с интервалом 0,5
секунды. Затем Commander штатно завершается и ещё дважды запускается и
завершается на том же HDD.
`sc_app_cleanup()` сравнивает `mem_info()` до выделения и после возврата общего
четырёхстраничного EMM-блока. При несовпадении программа печатает
`SPRCMD cleanup did not restore EMM free pages.` и возвращает код 4. Во всех
трёх контрольных кадрах присутствует только чистый prompt `D:\>`; повторные
инициализации также состоялись.
Артефакты:
- [прогресс файла больше 1 МБ](../artifacts/mame-p5/stability/copy-large-progress.png);
- [20 файлов после ста refresh](../artifacts/mame-p5/stability/twenty-copy-hundred-refresh.png);
- [cleanup-цикл 1](../artifacts/mame-p5/stability/cleanup-cycle-1.png);
- [cleanup-цикл 2](../artifacts/mame-p5/stability/cleanup-cycle-2.png);
- [cleanup-цикл 3](../artifacts/mame-p5/stability/cleanup-cycle-3.png).
## Реальный ENOSPC
Отдельный образ содержит `SOURCE/FULL.BIN` размером 262181 байт и filler
размером 32850000 байт. До запуска копирования на FAT16 свободно 104448 байт:
temp успевает создаться и частично вырасти, но не может вместить исходник.
Commander показал `Copy failed (errno 10); temporary file removed.` и остался
управляемым. После выхода валидатор подтвердил:
```text
PASS: ENOSPC не изменил source/filler и не оставил target/temp.
```
То есть проверен именно путь ошибки записи после создания temp, а не ранний
отказ `open()`.
Артефакты:
- [начало копирования на почти полном диске](../artifacts/mame-p5/enospc/copy-progress.png);
- [управляемый errno 10 и пустая target-панель](../artifacts/mame-p5/enospc/enospc-cleanup.png);
- [диалог после ошибки](../artifacts/mame-p5/enospc/quit-after-error.png);
- [чистый возврат в DSS](../artifacts/mame-p5/enospc/clean-dss-return.png).
## Точная граница видимой области 27/28
`N27` содержит 26 физических файлов плюс синтетический `..`, а `N28` — 27
файлов плюс `..`. Первый кадр одновременно показывает `027 entries` и
`028 entries`. После `End` панель N28 имеет `cursor=27`, первая видимая запись
становится `B0000000.TXT`, то есть `top=1`. Для N27 `End` даёт `cursor=26`,
`A0000000.TXT` остаётся первой строкой, то есть `top=0`.
- [обе точные границы](../artifacts/mame-p5/panel-edges/counts-27-28.png);
- [28-я запись включает прокрутку](../artifacts/mame-p5/panel-edges/count-28-scrolls.png);
- [27 записей помещаются без прокрутки](../artifacts/mame-p5/panel-edges/count-27-fits.png);
- [cleanup после проверки](../artifacts/mame-p5/panel-edges/clean-dss-return.png).
## Детерминированная матрица отказов copy/EXEC
`CPERR.EXE` вызывает то же банковое ядро `sc_copy_job.c`, но останавливает его
в точно известных состояниях. Это позволило проверить случаи, которые нельзя
надёжно поймать Lua-клавишей между двумя соседними 4-КБ блоками:
- отсутствующий EXE возвращает `ENOENT` (`errno 3`);
- отсутствующий source не оставляет уже созданный temp;
- отмена до первого read не создаёт target;
- после записи последнего блока job останавливается в отдельной точке
pre-commit, и отмена удаляет temp;
- принудительная ошибка read удаляет temp;
- temp, повторно открытый с `O_RDONLY`, даёт реальный отказ write
`EROFS` (`errno 8`) и удаляется;
- исчезновение temp непосредственно перед rename даёт управляемую ошибку;
- target, появившийся между begin и commit, сохраняет содержимое `KEEP`;
- после всей матрицы число свободных EMM-страниц полностью восстановлено.
Проверка выявила ошибку общего libc: исторические Sprinter-флаги имеют
значения `O_WRONLY=1`, `O_RDONLY=2`, `O_RDWR=3`, а `open()` трактовал младшие
биты как POSIX-нумерацию. Исправлены `libc/include/fcntl.h` и `libc/io/open.c`,
добавлен постоянный режимный тест в `tests/openenv`. После пересборки fast и
safe libc матрица прошла целиком, а основной F5-сценарий был повторён.
Результат внутри CHD проверяется не только по экрану: `RESULT.TXT` обязан
содержать все строки `PASS`, в `TARGET` разрешён только неизменённый
`RACE.BIN`, временные `~SC*.TMP` запрещены.
- [вся fault-matrix: ALL PASS](../artifacts/mame-p5/copy-faults/fault-matrix-all-pass.png);
- [возврат CPERR в DSS](../artifacts/mame-p5/copy-faults/return-to-dss.png).
## Смена HDD во время копирования
Lua физически выгружает только тестовый `hard2` во время первого F5 и через
несколько секунд подключает тот же CHD обратно. На DSS 1.71 активный системный
вызов ожидает возврата устройства; после подключения Commander получает
управляемый `errno 3`, удаляет temp и продолжает принимать команды. Повторный
F5 без перезапуска приложения успешно копирует `DATA.BIN` размером 262181 байт.
Посмертная проверка CHD подтверждает точное совпадение source/target и
отсутствие temp:
```text
PASS: HDD removal дал управляемый отказ; после возврата DATA совпал, temp нет.
```
- [управляемый отказ первого F5](../artifacts/mame-p5/media-change/managed-error.png);
- [прогресс повторного F5](../artifacts/mame-p5/media-change/retry-progress.png);
- [восстановленный target](../artifacts/mame-p5/media-change/recovered-copy.png);
- [чистый возврат в DSS](../artifacts/mame-p5/media-change/clean-dss-return.png).
## Реальный viewer и восстановление окружения
Внешним приложением служит не специальный test child, а полноэкранный
`examples/mdview2`. Commander перед EXEC устанавливает CWD активной панели,
поэтому viewer без передачи аргументов открывает лежащий рядом `README.MD`.
Проверены обычный вид, raw-режим, PageDown, выход и возврат в Commander с
восстановленными режимом, палитрой, CWD и обеими панелями.
Тест обнаружил в самом `mdview2` утечку scratch-блока EMM. Viewer переведён
на явное владение блоком: результат `mem_alloc_pages()` проверяется, а блок
освобождается в `unload_file()`. После исправления Commander штатно завершает
собственную lifecycle-проверку EMM.
- [главный экран mdview2](../artifacts/mame-p5/viewer/mdview-main.png);
- [mdview2 после PageDown](../artifacts/mame-p5/viewer/mdview-pagedown.png);
- [Commander после возврата](../artifacts/mame-p5/viewer/commander-restored.png);
- [чистый возврат в DSS](../artifacts/mame-p5/viewer/clean-dss-return.png).
## Финальный размер PoC
Конфигурация: `--memory big --safe --max-allocs 3000`.
| Область | Размер |
|---|---:|
| `_CODE` W2 | 8870 байт |
| данные W2 | 3240 байт |
| свободная куча W2 | 2738 байт |
| стек | 1279 байт |
| BANK1: scan/sort/copy job | 4242 / 16384 байт |
| BANK2: draw/copy/EXEC UI | 5973 / 16384 байт |
| `SPRCMD.EXE` | 42447 байт |
Проверка межбанковых вызовов чистая. Общий `make size-check` ожидаемо сообщает
рост `openenv` на 188 байт: это не скрытый рост libc, а добавленный постоянный
тест `write()` через `O_RDONLY` вместе с исправленной веткой mode mapping.
Эталон размеров автоматически не обновлялся, поскольку в общем дереве также
присутствует посторонний новый `hello3`.
## Команды воспроизведения
```sh
make hdd-p5 ALLOCS=3000
tests/run_mame_hdd.sh build/hdd/p5_stability.chd \
tests/mame_p5_stability.lua artifacts/mame-p5/local 205
tests/check_p5_hdd.sh build/hdd/p5_stability.chd
make hdd-p5-enospc ALLOCS=3000
tests/run_mame_hdd.sh build/hdd/p5_enospc.chd \
tests/mame_p5_enospc.lua artifacts/mame-p5/enospc-local 55
tests/check_p5_enospc_hdd.sh build/hdd/p5_enospc.chd
make hdd-p5-panels ALLOCS=3000
tests/run_mame_hdd.sh build/hdd/p5_panel_edges.chd \
tests/mame_p5_panel_edges.lua artifacts/mame-p5/panels-local 50
make hdd-p5-copy-faults ALLOCS=3000
tests/run_mame_hdd.sh build/hdd/p5_copy_faults.chd \
tests/mame_p5_copy_faults.lua artifacts/mame-p5/faults-local 35
tests/check_p5_copy_faults_hdd.sh build/hdd/p5_copy_faults.chd
make hdd-p5-viewer ALLOCS=3000
tests/run_mame_hdd.sh build/hdd/p5_viewer.chd \
tests/mame_p5_viewer.lua artifacts/mame-p5/viewer-local 65
make hdd-p5-media ALLOCS=3000
tests/run_mame_hdd.sh build/hdd/p5_media.chd \
tests/mame_p5_media_change.lua artifacts/mame-p5/media-local 50
tests/check_p5_media_hdd.sh build/hdd/p5_media.chd
```
Перед каждым запуском должен отсутствовать другой процесс MAME Sprinter.
`run_mame_hdd.sh` проверяет это сам. Единственный незакрытый пункт за пределами
эмуляторного release gate — повторить короткий smoke-сценарий на реальном
Sprinter Sp2000.