Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места. Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
13 KiB
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. В набор
обязательно входят размеры:
0, 1, 4095, 4096, 65535, 65536, 1048613 байт
Остальные тринадцать файлов проверяют повторное использование copy job,
временных имён и файловых дескрипторов. Lua-сценарий последовательно вызывает
F5 двадцать раз, не меняя активную панель. После остановки MAME
tests/check_p5_hdd.sh извлекает оба дерева и сравнивает каждую пару
побайтно:
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 МБ;
- 20 файлов после ста refresh;
- cleanup-цикл 1;
- cleanup-цикл 2;
- cleanup-цикл 3.
Реальный ENOSPC
Отдельный образ содержит SOURCE/FULL.BIN размером 262181 байт и filler
размером 32850000 байт. До запуска копирования на FAT16 свободно 104448 байт:
temp успевает создаться и частично вырасти, но не может вместить исходник.
Commander показал Copy failed (errno 10); temporary file removed. и остался
управляемым. После выхода валидатор подтвердил:
PASS: ENOSPC не изменил source/filler и не оставил target/temp.
То есть проверен именно путь ошибки записи после создания temp, а не ранний
отказ open().
Артефакты:
- начало копирования на почти полном диске;
- управляемый errno 10 и пустая target-панель;
- диалог после ошибки;
- чистый возврат в DSS.
Точная граница видимой области 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.
- обе точные границы;
- 28-я запись включает прокрутку;
- 27 записей помещаются без прокрутки;
- cleanup после проверки.
Детерминированная матрица отказов 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, даёт реальный отказ writeEROFS(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 запрещены.
Смена HDD во время копирования
Lua физически выгружает только тестовый hard2 во время первого F5 и через
несколько секунд подключает тот же CHD обратно. На DSS 1.71 активный системный
вызов ожидает возврата устройства; после подключения Commander получает
управляемый errno 3, удаляет temp и продолжает принимать команды. Повторный
F5 без перезапуска приложения успешно копирует DATA.BIN размером 262181 байт.
Посмертная проверка CHD подтверждает точное совпадение source/target и отсутствие temp:
PASS: HDD removal дал управляемый отказ; после возврата DATA совпал, temp нет.
Реальный 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.
Финальный размер 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.
Команды воспроизведения
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.