# 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 180 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.