Merge current master and reconcile completed member-files fixes

This commit is contained in:
evgeny committed 2026-10-24 19:38:40 +02:00
commit 897cc007e1
16 files changed
+512 -91

No files matched your search

+8
View File
@@ -3,6 +3,14 @@ ETCP: независимая обратная связь каналов и до
Статус
24.10.2026, объединение с актуальным master: удаление чужого member_files temp
уже исправлено0f42f92f, ранняя отмена —20b4975d; повторный фикс не требуется.
Описание ниже185/186 относится к прежней базе a198cfb9, до этих исправлений.
Актуальный аудит и проверки: doc/member_files_audit_findings.txt.
Небольшой недобор до условных85% — диагностическая метрика; он сам по себе
не оправдывает усложнения BBR. Дальше приоритет STCP backpressure, смена RTT/BW
нескольких путей и повторный отказ с проверкой доставки, пауз и роста очередей.
24.10.2026, продолжение после push: отдельно исследован одиночный дуплекс2Мбит/с.
Все прежние15 локальных коммитов вошли в origin/master после rebase; патчи сохранены.
Из-за параллельных изменений основного каталога продолжение изолировано в ветке
+30 -6
View File
@@ -25,7 +25,8 @@
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
enabled проверяется на каждом серверном запросе; клиент доступа к другим storage остаётся доступен.
Лимит одинаков для каждого UID отдельно,
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
@@ -86,7 +87,7 @@ Wire, версия 4
Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются
в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX.
Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты,
компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы,
компоненты с ведущей '.', завершающей точкой/пробелом, ASCII C0/DEL,
Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь.
POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются.
Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points.
@@ -168,11 +169,19 @@ Windows удерживает родительские каталоги без SH
Статусы (0..14): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT,
DIRECTORY_NOT_EMPTY.
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ до истечения 120 секунд от начала обработки
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним
request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока
мутация возвращает BUSY до истечения старых. В таком ответе cursor содержит
остаток до ближайшего освобождения квитанции в timebase 0.1мс (1..1200000).
Нулевой cursor означает BUSY другой причины с обычной задержкой 5 секунд.
Клиент COMMIT/MKDIR/RENAME/REMOVE сохраняет pending и прежний request_id/параметры,
ставит отменяемый таймер на этот срок и повторяет команду; промежуточный BUSY не завершает callback.
При потере ответа обычный повтор идёт через 5 секунд. Общий срок от создания
запроса остаётся 120 секунд и не продлевается даже при BUSY; pump не отправляет
команду после этого срока. Поэтому повторы остаются внутри серверного окна
квитанции, не требуя её досрочного вытеснения. Остальные команды возвращают BUSY caller.
Повторы после этого срока
не дают гарантии того же результата; перед новой мутацией требуется STAT.
Потоки, таймауты и остановка
@@ -224,6 +233,8 @@ quiesce запрещает новые команды, отменяет тайм
вне callbacks дожидается только своих работ/completions и закрывает дисковый
поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O.
Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion.
Немедленная отмена после start снимает starting: поздний CM UP не запускает STAT/подготовку заново.
После перехода к финализации публикации повторный cancel не меняет уже выбранный результат.
Desktop
-------
@@ -262,7 +273,14 @@ Stop сервиса/группы завершает все принадлежа
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
Один локальный файл назначения не может одновременно принадлежать двум download.
Два download с одной строкой local одновременно не допускаются; физические aliases
и подмену локальных путей исключает вызывающий. Upload читает стабильный regular source;
изменение снимка возвращает STALE. На POSIX O_NONBLOCK позволяет отвергнуть FIFO
до блокирующего open/read единственного дискового worker.
Download создаёт соседний temp эксклюзивно и получает право cleanup только после успеха open.
Отказ из-за существующего temp, max_bytes, STALE или ранняя отмена не удаляют чужой путь.
После проверки SHA256 выполняются sync/close и заменяющий rename; до публикации прежний local сохраняется.
POSIX-публикация локального download не делает fsync родительского каталога, в отличие от серверного COMMIT.
Аватарки пользователей
---------------------
@@ -350,12 +368,18 @@ tests/test_member_files: wire/UTF-8, произвольные блоки, пов
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
fd idle и принадлежность work/done потокам.
На POSIX отдельный процесс с границей 2с проверяет неблокирующее открытие FIFO
и последующий отказ проверки типа; fork выполняется до создания pthread.
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
проверка, отмена одного handle, restart router и destroy с активным server I/O.
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
Проверены сохранность чужого temp/прежнего назначения при EXISTS, max_bytes и немедленном cancel,
поздний CM UP после отмены, 76 созданий и 76 удалений с исчерпанием квитанций.
Один успешный ответ отброшен перед клиентским handler: повтор с прежним ID возвращает квитанцию,
счётчики остаются точными (76 каталогов после создания, 0 после удаления).
test_services — stop/start общего чат-сервиса.
Диагностика файлового сервиса — категория media, маршрутизатора — etcp_route.
Настройка для полного трасса этих модулей:
+128
View File
@@ -0,0 +1,128 @@
Аудит C-модуля member_files — 2026-10-24
Статус: полностью прочитаны шесть реализаций и шесть заголовков src/member_files/.
Заголовки сверены; четыре подтверждённых дефекта исправлены и проверены.
Серверная/клиентская части doc/member_files.txt согласованы с кодом. Общий документ
остаётся [~]: его desktop/AvatarManager/Android-подсистемы целиком в этот аудит не входили.
Ниже результаты относятся к C-сервису, а не к полному аудиту всех файлов GUI/аватарок.
1. Удаление чужого локального temp
До исправления TRANSFER_FINISH безусловно удалял вычисленный путь temp при ошибке,
включая отказ эксклюзивного open из-за существующего файла и отказ до открытия.
Настоящий test_member_files_network: preexisting target/temp по37 проверенных байт,
скачивание с подписанного storage, MF_EXISTS; target=1, foreign_temp=0, FAIL phase23.
Корректное воспроизведение: /tmp/member-files-temp-repro5.log, строки391..395.
Исправление: один temp_owned у handle, устанавливается только после успешного
эксклюзивного создания. Finish закрывает fd, публикует/удаляет только свой temp.
После успешного rename владение именем temp снято; source/target не подменяются caller.
Новый FILE-слой или второй файловый владелец не добавлены: disk API работает с native fd.
Обычный и sanitizer-прогоны: EXISTS, max_bytes, немедленная отмена сохраняют
прежние37 байт target и чужого temp, finished ровно один раз.
2. Ранняя отмена возобновляла передачу и приводила к UAF
После cancel сразу вслед за start поздний CM UP вызывал wake. Starting оставался1,
поэтому pump после начала cleanup снова посылал STAT. Finish освобождал handle,
а ответ STAT вызывал transfer_response с уже освобождённым arg.
Лог: /tmp/member-files-temp-fixed.log; GDB: /tmp/member-files-cancel-gdb.log,
SIGSEGV в transfer_response → mf_pending_finish → mf_dispatch.
Переход stopping теперь снимает starting; wake без готового финального ответа
ничего не планирует. Обязательная cleanup-задача остаётся единственным продолжением,
затем отправляется финальный CANCEL/END и освобождается handle до callback.
Не добавлены новые ссылки/таймеры или зависимость от скорости ответа CM.
Регрессия early cancel прошла вместе с обычной отменой, параллельными потоками,
router restart и destroy при активном server I/O.
3. Заполнение64 квитанций обрывало массовые мутации
Прежнее воспроизведение в doc/tasks.md: из76 удалений выполнены64, затем MF_BUSY.
Серверный предел/окно нужны для идемпотентности и остаются: до64 успешных
COMMIT/MKDIR/RENAME/REMOVE, MF_REPLAY_TB=1200000 (120с) от начала обработки.
Квитанции не вытесняются досрочно. Полный кеш возвращает BUSY с cursor=остаток
до ближайшего освобождения в timebase0.1мс; это значение кодируется обычным LE-полем.
Нулевой/недопустимый hint использует обычную задержку5с; недопустимый логируется.
Клиент этих четырёх команд сохраняет тот же pending/ID/параметры и ожидает
отменяемый retry timer. Остальные BUSY возвращаются callback как раньше.
Общий срок запроса остаётся120с от создания; ни BUSY, ни потеря ответа его не продлевают.
Перед повторной отправкой pump также проверяет срок. Сервер начинает обработку
не раньше создания клиентского запроса, поэтому допустимые повторы не выходят
за окно первой успешной квитанции. Ошибка/timeout не обещают отката серверной мутации.
При постоянной конкуренции запрос может завершиться TIMEOUT; unbounded waiting нет.
Вариант с продлением срока клиента отклонён при архитектурной сверке до итогового
прогона: поздний BUSY и потеря результата могли вывести повтор за жизнь квитанции.
Один секундный tick также недостаточен для точного освобождения; введён отдельный
retry timer, который снимается до free/cancel/quiesce, с tick fallback при OOM.
Формат125-байтного заголовка не менялся; смысл mutation BUSY.cursor описан в protocol.h.
Новая сетевая регрессия проходит настоящий клиентский API/router/storage:
-76 MKDIR, включая исчерпание кеша; QUOTA подтверждает ровно76 каталогов.
-Первый успешный ответ намеренно отброшен до клиентского handler; повтор с прежним
ID возвращает успех, не создавая второй объект и не выдавая EXISTS.
-76 STAT/REMOVE, включая новое исчерпание квитанций; QUOTA подтверждает0 объектов/байт/резерва.
Qt recursiveDeletion(server-busy) вызывает storage напрямую через stub bridge:
его сырой MF_BUSY остаётся правильным серверным ответом. Поведение полного
клиентского ожидания и массового удаления проверено C-сетевой регрессией, не этим stub.
4. FIFO блокировал общий disk worker до проверки типа
POSIX mf_disk_local_open(source,0) использовал блокирующий open. FIFO без writer
зависал ещё до mf_disk_info, который должен отвергнуть не-regular источник.
Регрессия запускает отдельный процесс до создания pthread, alarm2с ограничивает
только диагностический child. До фикса он завершился SIGALRM (status14), failures=1:
/tmp/member-files-fifo-repro.log. Добавлен O_NONBLOCK, как у серверного open:
FIFO открывается без ожидания, проверка типа даёт INVALID_PATH, fd закрывается.
Обычный и sanitizer-прогоны: child status0, failures0; invalid mode/links имеют WARN.
Произвольный уже выполняющийся системный I/O/хеширование всё ещё не прерывается cancel.
Сверка архитектуры и описаний
-Uasync владеет запросами, streams, local FIFO, ACL, CM handles, waiter/pump/timers.
Единственный worker выполняет файловое I/O последовательно; work FIFO, порядок
done при синхронном drain не обещается. Данные живут до completion.
-Обычных работ до64; заранее зарезервированные cleanup jobs обходят лимит.
Quiesce запрещает producers; drain завершает I/O и cleanup, затем storage destroy
выполняется в worker, после него join/free, пока ядро/группы ещё живы.
-Storage принимает только уже проверенные членство/роли. Сам проверяет UID мутации
и токены temp. Квота общая для UID во всех группах; обычные операции не делают rescan.
-Server COMMIT и local download имеют разные гарантии: локальный POSIX rename
не синхронизирует каталог. Ошибка server sync после rename не откатывает публикацию.
-Пути disk API доверенные и уже проверены protocol/storage. Callback LIST получает
заимствованные name/info; порядок файловой системы, не сортировка. Source/target
aliases локальных download исключает caller; одинаковая строка local запрещена.
-Codec проверяет формат и размеры, не ACL и не UTF-8 policy пути. Ошибка decode
может изменить output; encode не проверяет семантику команды. Эти границы описаны.
-Все поля/функции шести .h получили соответствующие контракты и комментарии.
Проверки и границы
./build.sh --no-gui -j4 PASS: /tmp/member-files-audit-final-build.log.
Профиль7/7 PASS: member_files, member_files_network, services, encrypted_attachment,
dm_media, attachment_send, member_adapter. Результат: /tmp/member-files-audit-profile-results.log,
подробности: /tmp/member-files-audit-profile-logs/.
ASAN/UBSan/LSan4/4 PASS, detect_leaks=1, halt_on_error=1: member_files,
member_files_network, services, encrypted_attachment. Отдельная копия:
/home/user/utun4/.cache/member-files-asan-b87xwg24, все встроенные зависимости
инструментированы через CPPFLAGS; системные OpenSSL/FFmpeg не пересобирались.
Логи: /tmp/member-files-sanitizer-build.log, /tmp/member-files-sanitizer-tests-build.log,
/tmp/member-files-sanitizer-results.log, /tmp/member-files-sanitizer-test_*.log.
После этого прогона добавлены только уточнения описаний и WARN без изменения логики.
В sanitizer-сборке остаётся прежнее предупреждение snprintf в disk_parent_open:
strlen(component)<=MF_NAME_MAX проверяется до записи в leaf[MF_NAME_MAX+1];
воспроизведённого усечения здесь нет. Остальные предупреждения сборки относятся
к существующему коду других модулей/зависимостей, не скрывались фильтрами компилятора.
Первые диагностические прогоны до repro5 недействительны для temp: тест был
собран с новым utun_instance.h и старой libutun.a, в startup читались0xaaaaaaaa.
Общая сборка устранила несовпадение; ненужное изменение маршрутного условия отменено.
Один промежуточный старт остановился из-за полного /tmp; очищены только
воспроизводимые объекты предыдущей sanitizer-копии, исходники/журналы сохранены.
Новый полный make check, Windows/FreeBSD runtime и GUI/Android проверки не выполнялись.
Четыре подтверждённых дефекта закрыты; новых подтверждённых незакрытых в этом объёме нет.
+20 -14
View File
@@ -4,16 +4,16 @@
## Текущие задачи
[ ] **member_files: cleanup удаляет чужой temp при отказе создания загрузки** — воспроизведено 24.10.2026 на a198cfb9.
test_member_files_network падает в фазе23: локальный O_EXCL open возвращает MF_EXISTS,
затем TRANSFER_FINISH без проверки владения вызывает mf_disk_local_remove(t->temp).
Прежнее назначение37байт сохраняется, заранее созданный чужой .part исчезает.
Проверены общий check.sh и отдельный повтор: одинаковый FAIL preservation case=0, target=1, foreign_temp=0;
отсутствие .part дополнительно проверено в обоих оставшихся тестовых каталогах.
Нужен учёт успешного создания собственного temp, включая отмену до PREPARE и отказ max_bytes;
проверять/удалять можно только созданный этой передачей файл. Код этого модуля в ETCP-ветке не менялся.
Логи: /tmp/etcp-solo-native-full-check.log, /tmp/etcp-solo-member-network-repeat.log;
подробный первый лог: /tmp/etcp-solo-member-network-suite-failed.log.
[+] **member_files: сверить контракты и исправить ошибки владения/повторов** — завершено 24.10.2026.
Прочитаны все6 реализаций и6 заголовков C-модуля. Исправлены удаление чужого temp,
повторный запуск после раннего cancel с UAF, обрыв массовых мутаций при64 квитанциях
и блокирующий open FIFO до проверки типа. Заголовки и серверная/клиентская документация согласованы.
Подтверждены сохранность файлов при трёх отказах, 76 созданий/76 удалений и потеря успешного ответа без повторной мутации.
Сборка PASS; профиль7/7, ASAN/UBSan/LSan4/4 PASS с detect_leaks=1. Общий GUI/avatar документ остаётся [~].
Отчёт и границы: [member_files_audit_findings.txt](member_files_audit_findings.txt).
Логи: /tmp/member-files-audit-profile-results.log, /tmp/member-files-sanitizer-results.log.
Прежний FAIL удаления чужого temp из ETCP-проверки закрыт коммитом0f42f92f;
защита от возобновления после раннего cancel —20b4975d. Повторный фикс не требуется.
[+] **media_async/media_delivery: завершить сверку контрактов и исправлений** — завершено 24.10.2026.
Проверены все11 заголовков двух каталогов по полным реализациям; исправлены alias файлов,
@@ -113,6 +113,9 @@
Windows/FreeBSD runtime не проверялся.
[ ] **ETCP: полезная утилизация нескольких каналов и доставка при зависании части линков** — общая задача открыта.
Небольшой недобор до условного порога85% сохраняется как результат измерений;
сам по себе не оправдывает изменения/усложнения BBR. Далее по приоритету — STCP backpressure,
смена RTT нескольких путей/BW и повторный отказ; строгие критерии — доставка, паузы и рост очередей.
После push исследован одиночный дуплекс2Мбит/с на изолированной базе a198cfb9.
Точные7763 ACK показывают задержку обратной очереди: пример DATA15.909мс, hold0.029мс,
обратный ACK32.219мс; DOWN продолжает ждать flight7644 при цели5915байт.
@@ -805,13 +808,16 @@
нескольких ретрансляторов, нескольких автоматических источников/групп и mobile standby на устройстве
(doc/stream.txt, раздел 16). Сохранённые проверки отдельных этапов не закрывают всю матрицу.
[ ] **Member files: исчерпание кеша результатов массовых изменений** — 10.10.2026:
[+] **Member files: исчерпание кеша результатов массовых изменений** — исправлено 24.10.2026.
Воспроизведено test_member_files_browser::recursiveDeletion(server-busy): из 76 объектов удаляются 64,
следующая команда получает MF_BUSY, остаются 12. storage.c: STORE_RECEIPTS=64, RECEIPT_TB=1200000 (120 секунд).
Лимит общий для успешных COMMIT/MKDIR/RENAME/REMOVE; затрагивает также рекурсивное копирование.
Нужен отдельный этап управления временем жизни receipts с сохранением идемпотентности повторных запросов;
простое вытеснение старых записей нарушает гарантию повторного ответа. После исправления изменить регрессию
на успешное удаление более 64 объектов и проверить повторные запросы/потерю ответа.
Сохранены64 квитанции/120с без досрочного вытеснения. BUSY.cursor сообщает ближайшее освобождение,
клиент этих мутаций ждёт отменяемый таймер с прежним ID/параметрами и общим сроком120с без продления.
Новая сетевая регрессия через настоящий клиентский API:76 созданий, потеря успешного ответа,
затем76 удалений; QUOTA подтверждает76 и0 каталогов без дубликатов. Обычный и sanitizer-прогоны PASS.
Старый Qt-stub вызывает storage напрямую и по-прежнему видит допустимый сырой MF_BUSY;
он не проверяет новый клиентский retry. Подробности: [member_files_audit_findings.txt](member_files_audit_findings.txt).
[ ] **Desktop: провалы вероятности VAD при компрессии** — диагностика 09.10.2026:
Воспроизведение пользователя: длительная гласная, затем временная нечувствительность; ближе к концу лога несколько повторов.
+10 -3
View File
@@ -58,7 +58,10 @@ static int disk_win_error(const char* operation) {
static int disk_handle_info(HANDLE h, struct mf_disk_info* out) {
BY_HANDLE_FILE_INFORMATION i;
if (!GetFileInformationByHandle(h, &i)) return disk_win_error("GetFileInformationByHandle");
if ((i.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) || i.nNumberOfLinks > 1) return MF_INVALID_PATH;
if ((i.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) || i.nNumberOfLinks > 1) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: unsupported file attributes=%lu links=%lu", i.dwFileAttributes, i.nNumberOfLinks);
return MF_INVALID_PATH;
}
memset(out, 0, sizeof(*out));
out->type = i.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ? MF_DIRECTORY : MF_FILE;
out->size = out->type == MF_FILE ? ((uint64_t)i.nFileSizeHigh << 32) | i.nFileSizeLow : 0;
@@ -254,7 +257,11 @@ static int disk_parent_open(struct mf_disk_root* root, const char* path, struct
}
static int disk_posix_info(const struct stat* st, struct mf_disk_info* i) {
if ((!S_ISREG(st->st_mode) && !S_ISDIR(st->st_mode)) || (S_ISREG(st->st_mode) && st->st_nlink != 1)) return MF_INVALID_PATH;
if ((!S_ISREG(st->st_mode) && !S_ISDIR(st->st_mode)) || (S_ISREG(st->st_mode) && st->st_nlink != 1)) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: unsupported file mode=%lo links=%llu",
(unsigned long)st->st_mode, (unsigned long long)st->st_nlink);
return MF_INVALID_PATH;
}
memset(i, 0, sizeof(*i)); i->type = S_ISDIR(st->st_mode) ? MF_DIRECTORY : MF_FILE;
i->size = i->type == MF_FILE ? (uint64_t)st->st_size : 0; i->mtime = (uint64_t)st->st_mtime;
i->identity[0] = st->st_dev; i->identity[1] = st->st_ino;
@@ -352,7 +359,7 @@ int mf_disk_resize(int fd, uint64_t size) { return ftruncate(fd, (off_t)size) ?
int mf_disk_sync(int fd) { return fsync(fd) ? disk_error("sync", NULL) : MF_OK; }
int mf_disk_close(int fd) { return close(fd) ? disk_error("close", NULL) : MF_OK; }
int mf_disk_local_open(const char* path, int create, int* fd) {
*fd = open(path, O_CLOEXEC | O_NOFOLLOW | (create ? O_RDWR | O_CREAT | O_EXCL : O_RDONLY), 0600);
*fd = open(path, O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK | (create ? O_RDWR | O_CREAT | O_EXCL : O_RDONLY), 0600);
return *fd < 0 ? disk_error("local open", path) : MF_OK;
}
int mf_disk_local_publish(const char* from, const char* to) { return rename(from, to) ? disk_error("local publish", to) : MF_OK; }
+32 -7
View File
@@ -1,39 +1,64 @@
/* Примитивы диска для storage и клиентского worker. На POSIX пути разрешаются
/* Синхронные примитивы диска для storage и клиентского worker; не вызывать из uasync callbacks.
* Возврат — MF_OK либо положительный mf_status, не errno/-1. Аргументы/выходы обязательны, кроме отмеченных NULL.
* Серверные path/from/to — уже проверенные относительные пути; сам слой не запрещает '.'/'..'.
* На POSIX пути разрешаются
* относительно открытого каталога через *at/O_NOFOLLOW. Windows удерживает каталоги
* без FILE_SHARE_DELETE и отвергает reparse points; имена преобразуются UTF-8→UTF-16.
* fd принадлежит вызывающему; read/write работают по 64-битному смещению. */
* fd принадлежит вызывающему; read/write работают по 64-битному смещению.
* Локальные пути приложения не ограничены корнем storage; их родители/назначение остаются ответственностью caller. */
#ifndef MEMBER_FILES_DISK_H
#define MEMBER_FILES_DISK_H
#include "protocol.h"
#include "../../lib/platform_compat.h"
struct mf_disk_info { uint64_t size, mtime, identity[6]; uint8_t type; };
struct mf_disk_info {
uint64_t size; // Байты regular-файла; каталог — 0.
uint64_t mtime; // Unix seconds; для version используются более точные identity.
uint64_t identity[6]; // Непрозрачный снимок inode/file ID и времён; платформенно зависим, не wire.
uint8_t type; // MF_FILE/MF_DIRECTORY; другие типы и regular hardlink отклоняются.
};
struct mf_disk_root {
char path[2048];
char path[2048]; // Заимствованный при open путь копируется; предел2047 байт + NUL.
#ifdef _WIN32
HANDLE handle;
HANDLE handle; // Собственный handle корня, закрывается после всех операций.
#else
int fd;
int fd; // Собственный directory fd; операции разрешают имена относительно него.
#endif
};
/* Создать корень, если отсутствует (не создаёт родителей), и закрепить его. Close нужен только после успеха. */
int mf_disk_root_open(struct mf_disk_root* root, const char* path);
void mf_disk_root_close(struct mf_disk_root* root);
/* Открыть regular-файл без усечения. create!=0 — эксклюзивное создание, writable!=0 — read/write.
* fd использовать/закрывать только после MF_OK; отказ не гарантирует значение *fd. */
int mf_disk_open(struct mf_disk_root* root, const char* path, int create, int writable, int* fd);
/* Снимок без чтения содержимого: stat — по имени, info — уже открытого fd. Выход использовать только при успехе. */
int mf_disk_stat(struct mf_disk_root* root, const char* path, struct mf_disk_info* info);
int mf_disk_info(int fd, struct mf_disk_info* info);
/* created, если не NULL, показывает факт создания даже при последующей ошибке sync. */
/* Создать один каталог. created, если не NULL, показывает факт создания даже при последующей ошибке sync. */
int mf_disk_mkdir(struct mf_disk_root* root, const char* path, int* created);
/* Удалить файл либо пустой каталог. Ошибка sync на POSIX возможна уже после удаления. */
int mf_disk_remove(struct mf_disk_root* root, const char* path, int directory);
/* Заменяющий rename: запрет замены при обычном RENAME проверяет storage. На POSIX sync обоих родителей;
* ошибка sync не означает откат уже выполненного rename. */
int mf_disk_rename(struct mf_disk_root* root, const char* from, const char* to);
/* name/info заимствованы на время callback; 0 — продолжить, >0 — успешная ранняя остановка, <0 — -mf_status. */
typedef int (*mf_disk_entry_fn)(void* arg, const char* name, const struct mf_disk_info* info);
/* Перечислить записи в порядке файловой системы, без '.'/'..'. Пустой path означает корень. */
int mf_disk_list(struct mf_disk_root* root, const char* path, mf_disk_entry_fn cb, void* arg);
/* Точный диапазон: 0 байт допустимо, короткий read — ошибка. Caller гарантирует offset+size <= INT64_MAX. */
int mf_disk_read(int fd, uint64_t offset, void* data, size_t size);
int mf_disk_write(int fd, uint64_t offset, const void* data, size_t size);
/* Изменить длину (size<=INT64_MAX), синхронизировать содержимое либо закрыть fd; каждый вызов независим. */
int mf_disk_resize(int fd, uint64_t size);
int mf_disk_sync(int fd);
int mf_disk_close(int fd);
/* Локальный source (create=0) либо эксклюзивный read/write temp (create!=0), без усечения.
* Тип проверяет caller через info; POSIX O_NONBLOCK позволяет отвергнуть FIFO до блокирующего чтения. */
int mf_disk_local_open(const char* path, int create, int* fd);
/* Заменить local назначение: POSIX rename без sync каталога; Windows MOVEFILE_WRITE_THROUGH. */
int mf_disk_local_publish(const char* from, const char* to);
/* Удалить собственный local temp; отсутствие самого файла допустимо. Владение проверяет caller. */
int mf_disk_local_remove(const char* path);
/* SHA256 ровно size байт от нуля, включая пустой файл; ошибка делает hash непригодным. fd остаётся у caller. */
int mf_disk_hash(int fd, uint64_t size, uint8_t hash[32]);
#endif
+41 -7
View File
@@ -16,16 +16,17 @@
#define MF_MAX_PENDING 16
#define MF_MAX_JOBS 32
#define MF_REQUEST_TB 1200000ULL
struct mf_pending {
struct mf_pending* next;
struct member_files* files;
uint64_t group, server, started, sent;
uint64_t group, server, started, sent, retry_at;
int busy; // Уже залогировано ожидание квитанции; общий срок запроса не продлевается.
struct mf_message request;
mf_response_fn callback;
void* arg;
void* pump;
void* retry;
struct CONN_MGR_HANDLE* cm;
struct queue_waiter_handle waiter;
};
@@ -170,6 +171,7 @@ int mf_send(struct member_files* f, uint64_t group, uint64_t peer, const struct
static void mf_pending_finish(struct mf_pending* p, const struct mf_message* response, int status) {
struct member_files* f = p->files;
if (p->pump) uasync_call_soon_cancel(f->inst->ua, p->pump);
if (p->retry) uasync_cancel_timeout(f->inst->ua, p->retry);
etcp_router_cancel_send_ready(f->inst, p->group, p->server, ETCP_RT_ID_MEMBER_FILES, &p->waiter);
if (p->cm) conn_mgr_close(p->cm);
struct mf_pending** link = &f->pending;
@@ -188,20 +190,32 @@ static void mf_pending_finish(struct mf_pending* p, const struct mf_message* res
static void mf_pending_pump(void* arg) {
struct mf_pending* p = arg; p->pump = NULL;
if (p->files->closing) return;
if (p->retry) { uasync_cancel_timeout(p->files->inst->ua, p->retry); p->retry = NULL; }
if (get_time_tb() - p->started >= MF_REPLAY_TB) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: request expired before send id=%llu cmd=%s",
(unsigned long long)p->request.request_id, mf_command_text(p->request.command));
mf_pending_finish(p, NULL, MF_TIMEOUT); return;
}
int rc = mf_send(p->files, p->group, p->server, &p->request);
if (!rc) p->sent = get_time_tb();
if (!rc) { p->sent = get_time_tb(); p->retry_at = 0; }
else if (rc > 0) etcp_router_on_send_ready(p->files->inst, p->group, p->server, ETCP_RT_ID_MEMBER_FILES,
&p->waiter, mf_client_ready, p);
}
static void mf_pending_wake(struct mf_pending* p) {
if (p->pump || p->files->closing) return;
if (p->pump || p->files->closing || get_time_tb() < p->retry_at) return;
p->pump = uasync_call_soon(p->files->inst->ua, p, mf_pending_pump);
if (!p->pump) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "member_files: client pump allocation failed");
}
static void mf_client_ready(struct ll_queue* q, void* arg) { (void)q; mf_pending_wake(arg); }
/* Отдельный таймер не округляет срок освобождения квитанции до секундного sweep. */
static void mf_pending_retry(void* arg) {
struct mf_pending* p = arg; p->retry = NULL; p->retry_at = 0;
mf_pending_wake(p);
}
static void mf_connected(struct CONN_MGR_HANDLE* h, uint64_t peer, uint64_t group, enum conn_mgr_event event, void* arg) {
(void)h; (void)peer; (void)group;
struct mf_pending* p = arg;
@@ -385,18 +399,18 @@ static void mf_tick(void* arg) {
mf_receive_resume(f);
for (;;) {
struct mf_pending* expired = NULL;
for (struct mf_pending* p = f->pending; p; p = p->next) if (now - p->started >= MF_REQUEST_TB) { expired = p; break; }
for (struct mf_pending* p = f->pending; p; p = p->next) if (now - p->started >= MF_REPLAY_TB) { expired = p; break; }
if (!expired) break;
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: timeout id=%llu cmd=%u",
(unsigned long long)expired->request.request_id, expired->request.command);
mf_pending_finish(expired, NULL, MF_TIMEOUT);
}
for (struct mf_pending* p = f->pending; p; p = p->next)
if (!p->sent || now - p->sent >= 50000) mf_pending_wake(p);
if (p->retry_at ? now >= p->retry_at : !p->sent || now - p->sent >= 50000) mf_pending_wake(p);
struct mf_job* j = f->jobs;
while (j) {
struct mf_job* next = j->next;
if (j != f->worker && j->created && now - j->created >= MF_REQUEST_TB) {
if (j != f->worker && j->created && now - j->created >= MF_REPLAY_TB) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: server reply/queue timeout peer=%llu", (unsigned long long)j->peer); mf_job_free(j);
}
j = next;
@@ -427,6 +441,26 @@ static void mf_dispatch(struct member_files* f, uint64_t group, uint64_t peer, c
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: mismatched response id=%llu", (unsigned long long)m.request_id); return;
}
if (!member_files_member(f->inst, group, peer, 1)) { m.status = MF_ACCESS_DENIED; m.data_len = 0; }
if (m.status == MF_BUSY && (m.command == MF_COMMIT || (m.command >= MF_MKDIR && m.command <= MF_REMOVE_PATH))) {
if (m.cursor > MF_REPLAY_TB) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: invalid busy hint id=%llu delay_tb=%llu; using5s",
(unsigned long long)m.request_id, (unsigned long long)m.cursor);
uint64_t now = get_time_tb(), delay = m.cursor && m.cursor <= MF_REPLAY_TB ? m.cursor : 50000;
p->retry_at = now + delay;
p->sent = now;
if (p->pump) { uasync_call_soon_cancel(f->inst->ua, p->pump); p->pump = NULL; }
if (p->retry) uasync_cancel_timeout(f->inst->ua, p->retry);
p->retry = uasync_set_timeout(f->inst->ua, (int)delay, p, mf_pending_retry, "member_files_retry");
if (!p->retry) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "member_files: busy retry timer failed id=%llu; tick will retry",
(unsigned long long)m.request_id);
etcp_router_cancel_send_ready(f->inst, group, peer, ETCP_RT_ID_MEMBER_FILES, &p->waiter);
if (!p->busy) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: mutation busy; retaining request id=%llu cmd=%s limit=120s",
(unsigned long long)m.request_id, mf_command_text(m.command));
p->busy = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: busy retry id=%llu elapsed_ms=%llu delay_tb=%llu retry_at=%llu",
(unsigned long long)m.request_id, (unsigned long long)((now - p->started) / 10),
(unsigned long long)delay, (unsigned long long)p->retry_at);
return;
}
mf_pending_finish(p, &m, 0); break;
}
return;
+28 -9
View File
@@ -17,7 +17,8 @@ typedef void (*mf_response_fn)(void* arg, const struct mf_message* response);
// ── Жизненный цикл модуля ──
// Создать состояние сервера/клиента и запустить дисковый worker; 0/-1.
// Создать состояние сервера/клиента и worker; 0/-1. Требует живые ua/config/router/chat/SQLite, повторный init — ошибка.
// Стартовый scan асинхронный: init=0 ещё не означает готовность локального storage; remote client остаётся доступен.
int member_files_init(struct UTUN_INSTANCE* inst);
// Запретить новые команды и начать отмену запросов/передач; запущенный дисковый I/O завершается отдельно.
@@ -26,17 +27,20 @@ void member_files_quiesce(struct UTUN_INSTANCE* inst);
// Дождаться дискового worker и освободить контекст; вне callbacks, до удаления CHAT-групп.
void member_files_destroy(struct UTUN_INSTANCE* inst);
// Отменить запросы и передачи одной группы перед её удалением.
// Отменить свои запросы/передачи одной группы перед её удалением; дерево storage этим не удаляется.
void member_files_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group);
// ── Команды файлового сервера ──
/* Отправить одну команду; request_id=0 выбирает случайный ID. NULL — отказ запуска, callback не вызывается.
* При успехе callback ровно один, включая отмену; handle освобождён до callback, arg принадлежит вызывающему. */
* При успехе callback ровно один, включая отмену; handle освобождён до callback, arg принадлежит вызывающему.
* Общий timeout120с. BUSY для COMMIT/MKDIR/RENAME/REMOVE сохраняет ID и ждёт подсказанный сервером срок
* освобождения квитанции (иначе5с), не продлевая общий timeout. Остальные BUSY передаются callback. */
struct mf_pending* member_files_request(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t server,
const struct mf_message* request, mf_response_fn callback, void* arg);
// Синхронно завершить свой запрос ответом MF_CANCELLED; handle после вызова недействителен, NULL допустим.
// Это отмена локального ожидания; уже запущенная серверная мутация не откатывается.
void member_files_cancel(struct mf_pending* pending);
// Отменить pending-запрос с этим request_id; не отменяет потоковую передачу файла.
@@ -44,14 +48,20 @@ void member_files_cancel_request_id(struct UTUN_INSTANCE* inst, uint64_t id);
// ── Проверка доступа ──
// 1 — действующий мембер; storage!=0 дополнительно требует подписанную роль хранителя.
// 1 — полная source=0/deleted=0 запись мембера в локальной CHAT-таблице, иначе0.
// storage!=0 требует adm_tags.storage=yes и сохранённую64-байтную подпись; peers.storage не даёт полномочий.
int member_files_member(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t node, int storage);
// ── Команда интерфейса ──
struct mf_gui_request { struct UTUN_INSTANCE* inst; uint64_t group, server; struct mf_message message; };
struct mf_gui_request {
struct UTUN_INSTANCE* inst; // Заимствованное ядро до ответа.
uint64_t group, server; // CHAT-группа и явно выбранный storage, в том числе self.
struct mf_message message; // Команда; для корреляции GUI выбирает ненулевой request_id.
};
// arg — u_malloc/u_calloc mf_gui_request; освобождается после ответа CHAT_EVT_MEMBER_FILES_RESPONSE.
// arg — u_malloc/u_calloc mf_gui_request; владение переходит при вызове trampoline, освобождается после ответа.
// CHAT_EVT_MEMBER_FILES_RESPONSE: group:8LE/server:8LE, затем mf_encode(response). Отказ запуска даёт ACCESS_DENIED.
void member_files_gui_request(void* arg);
// ── Потоковая передача файлов ──
@@ -63,12 +73,17 @@ struct mf_transfer_request {
// download: по ответу STAT, до открытия локального temp и передачи данных.
uint64_t max_bytes;
int upload; // ненулевой — загрузить local на сервер, 0 — скачать remote
uint8_t version[16]; // версия из STAT; нули при создании нового серверного файла
uint8_t version[16]; // Upload:ожидаемая версия/нули для создания; download:ожидаемая версия/нули для текущей.
char remote[MF_PATH_MAX + 1], local[MF_PATH_MAX + 1]; // путь в каталоге owner и локальный путь приложения
};
struct mf_transfer;
struct mf_transfer_progress { uint64_t id, done, total; uint16_t status; uint8_t finished; };
struct mf_transfer_progress {
uint64_t id; // Фактический ID handle, включая автоматически выбранный.
uint64_t done, total; // Upload:принятые router байты; download:записанные на диск, до финального sync/publish.
uint16_t status; // mf_status; результат окончательный только при finished.
uint8_t finished; // 0 progress (может отсутствовать),1 ровно одно завершение.
};
typedef void (*mf_transfer_fn)(void* arg, const struct mf_transfer_progress* progress);
// Один handle на файл; до 16 клиентских передач одновременно, включая upload/download к одному пиру.
@@ -76,9 +91,13 @@ typedef void (*mf_transfer_fn)(void* arg, const struct mf_transfer_progress* pro
// finished приходит ровно один раз после закрытия/публикации; handle уже освобождён до callback.
// Прогресс заимствован на время callback. Cancel асинхронно ждёт свой I/O и не затрагивает другие handles.
// Request копируется; исходную структуру можно освободить сразу, arg должен жить до finished.
// Download заменяет local только после проверки/закрытия своего temp; отказ/отмена до создания не удаляет чужой temp.
// Совпадение строк local двух download запрещено; aliases/подмену локальных файлов исключает caller.
// Upload требует стабильный regular source; изменение снимка даёт STALE. Пустой файл допустим.
struct mf_transfer* member_files_transfer_start(const struct mf_transfer_request* request, mf_transfer_fn callback, void* arg);
// Запросить отмену своей передачи; завершение приходит через finished после закрытия её файлового I/O.
// Допустимо сразу после start: поздний CM UP не запускает подготовку/STAT заново. Финализируемый результат уже не отменяется.
void member_files_transfer_cancel(struct mf_transfer* transfer);
// ── Команды интерфейса для передачи и отмены ──
@@ -87,7 +106,7 @@ void member_files_transfer_cancel(struct mf_transfer* transfer);
* state=1 progress, 2 finished. arg — u_malloc/u_calloc mf_transfer_request, освобождается после finished. */
void member_files_gui_transfer(void* arg);
struct mf_cancel_request { struct UTUN_INSTANCE* inst; uint64_t id; };
struct mf_cancel_request { struct UTUN_INSTANCE* inst; uint64_t id; }; // Заимствованное ядро и фактический ID команды/передачи.
// arg — u_malloc/u_calloc mf_cancel_request, освобождается здесь; отменяет передачу либо одиночный запрос по id.
void member_files_gui_cancel(void* arg);
+29 -17
View File
@@ -1,4 +1,6 @@
/* Внутреннее состояние owner, только для member_files/; storage принадлежит единственному worker. */
/* Внутреннее состояние member_files/, все списки/таймеры принадлежат uasync.
* Disk worker последовательно владеет storage и своими снимками I/O; completion возвращает управление uasync.
* Start: worker/reserved cleanup → storage → router/tick → initialize. Stop: quiesce → drain → storage cleanup → join/free. */
#ifndef MEMBER_FILES_PRIVATE_H
#define MEMBER_FILES_PRIVATE_H
#include "member_files.h"
@@ -9,31 +11,41 @@ struct mf_job;
struct mf_transfer;
struct mf_local;
struct member_files {
struct UTUN_INSTANCE* inst;
struct mf_storage* storage;
struct mf_worker* disk;
struct mf_work* cleanup;
struct mf_pending* pending;
struct mf_job* jobs;
struct mf_job* worker;
struct mf_transfer* transfers;
struct mf_local* locals;
void* tick;
void* pump;
unsigned pending_count, job_count, transfer_count;
unsigned server_transfer_count;
unsigned local_count;
int closing, used_storage, sweep_due;
int storage_status, init_result;
struct UTUN_INSTANCE* inst; // Заимствованное ядро; ua/router/группы живут до destroy.
struct mf_storage* storage; // Собственный storage; освобождается worker после всех stream cleanup.
struct mf_worker* disk; // Единственный FIFO дискового I/O; uasync владеет handle.
struct mf_work* cleanup; // Резерв освобождения storage, создан до начала I/O.
struct mf_pending* pending; // Клиентские команды, до16; handle удаляется до callback.
struct mf_job* jobs; // До32 серверных queued/worker/reply задач, включая sweep.
struct mf_job* worker; // Текущая серверная задача; не включает startup/клиентские stream I/O.
struct mf_transfer* transfers; // Собственные client/server streams и их отложенная очистка.
struct mf_local* locals; // До32 сообщений self→self, общий FIFO запросов/ответов.
void* tick; // Таймер1с: pending/stream timeout, повторы, sweep/resume.
void* pump; // Отменяемый call_soon серверного pump, не worker.
unsigned pending_count, job_count, transfer_count; // Числа pending/server jobs/client streams.
unsigned server_transfer_count; // Серверные streams, независимо от client limit16.
unsigned local_count; // Длина локального FIFO.
int closing, used_storage, sweep_due; // Quiesce, необходимость периодического sweep и ожидающий sweep.
int storage_status; // Uasync:BUSY до startup completion, затем MF_OK либо постоянная ошибка.
int init_result; // Worker пишет результат initialize; читать только после completion/drain.
};
/* Запросить остановку streams группы (0 — всех), дождаться их I/O через worker drain отдельно. */
void mf_transfers_quiesce(struct member_files* files, uint64_t group);
/* 1 — DATA помещается в кольцо либо относится к уже неактивному потоку;0 — router должен удержать пакет. */
int mf_transfer_receive_ready(struct member_files* files, uint64_t group, uint64_t peer, int kind, uint64_t id);
/* Обработать проверенный STREAM-пакет; group/peer — end-to-end identity router. */
void mf_transfer_dispatch(struct member_files* files, uint64_t group, uint64_t peer, const struct mf_message* message);
/* Завершить потоки пары с IO_ERROR при reset/close router; прочие пары не затрагиваются. */
void mf_transfers_peer_down(struct member_files* files, uint64_t group, uint64_t peer);
/* Планировать готовые pumps / обработать прогресс и таймауты; все функции вызываются в uasync. */
void mf_transfers_wake(struct member_files* files);
void mf_transfers_tick(struct member_files* files, uint64_t now);
/* Отправить signed+encrypted либо поставить local:0 принято,1 backpressure,-1 ошибка. message копируется. */
int mf_send(struct member_files* files, uint64_t group, uint64_t peer, const struct mf_message* message);
/* 1/0: enabled, signed storage-role self, действующие peer/owner, запись только своего UID. Без I/O. */
int mf_server_allowed(struct member_files* files, uint64_t group, uint64_t peer, const struct mf_message* message);
/* Пробудить заблокированный router receive и голову local FIFO, если появились места. */
void mf_receive_resume(struct member_files* files);
/* Поставить копию ответа с указанным статусом в ограниченную очередь; отказ логируется. */
void mf_reply(struct member_files* files, uint64_t group, uint64_t peer, const struct mf_message* request, int status);
#endif
+31 -9
View File
@@ -1,9 +1,12 @@
/* Файловый протокол участников CHAT-группы. Все числа на проводе little-endian.
/* Файловый протокол участников CHAT-группы. Codec не проверяет членство/подпись/шифрование: это делает сервис.
* Все числа на проводе little-endian.
* Группа и подписанный отправитель приходят от router; owner — UID каталога.
* version — непрозрачный снимок объекта, upload — случайный ID временного файла.
* Нулевая version в BEGIN означает создание; замена требует version из STAT.
* LIST: cursor — число уже выданных записей; version фиксирует снимок каталога.
* READ/WRITE: offset и size задают диапазон; WRITE.data_len обязан равняться size.
* COMMIT/MKDIR/RENAME/REMOVE: RESPONSE BUSY.cursor — срок до освобождения квитанции в0.1мс;
* 0 означает обычный повтор через5с. Общий клиентский срок120с не продлевается.
* Данные LIST — последовательность entry: name_len:2,type:1,size:8,mtime:8,version:16,name.
* Токены меняются при перезапуске сервера. Никаких ABI-зависимых структур в wire. */
#ifndef MEMBER_FILES_PROTOCOL_H
@@ -25,10 +28,13 @@ extern "C" {
#define MF_AVATAR_MAX_BYTES (1024 * 1024)
#define MF_AVATAR_ORIGINAL_MAX_BYTES (32 * 1024 * 1024)
#define MF_AVATAR_MANIFEST_MAX_BYTES 4096
/* Имя объекта аватарки:64 lowercase hex и .png/.jpg/.webp/.json; вход — NUL-строка, NULL даёт0.
* valid/manifest_valid возвращают1/0; file_limit — лимит32MiB для изображения,4096 для JSON,0 при неверном имени. */
int mf_avatar_file_valid(const char* file);
int mf_avatar_manifest_valid(const char* file);
uint64_t mf_avatar_file_limit(const char* file);
#define MF_IDLE_TB 100000ULL /* 10 секунд */
#define MF_REPLAY_TB 1200000ULL /* Окно повторов запроса/успешной квитанции:120с. */
#define MF_REQUEST 1
#define MF_RESPONSE 2
#define MF_FILE 1
@@ -45,22 +51,38 @@ enum mf_status {
};
struct mf_message {
uint8_t kind, command, type, more;
uint16_t status;
uint64_t request_id, owner, offset, size, cursor, mtime;
uint8_t version[16], upload[16], hash[32];
char path[MF_PATH_MAX + 1], target[MF_PATH_MAX + 1];
uint32_t data_len;
uint8_t data[MF_BLOCK_SIZE];
uint8_t kind, command; // MF_REQUEST/RESPONSE и mf_command; group/src берутся отдельно из router.
uint8_t type, more; // MF_FILE/DIRECTORY и признак продолжения LIST/READ (0/1).
uint16_t status; // mf_status; у запроса всегда MF_OK.
uint64_t request_id; // Ненулевой ID одной неизменной команды; у DATA/END/CANCEL — ID потока.
uint64_t owner; // UID дерева;0 только для виртуального LIST корня группы.
uint64_t offset, size; // Диапазон READ/WRITE/DATA либо полный размер BEGIN/END/STAT.
uint64_t cursor; // LIST: число выданных записей; открытие STREAM:ID потока; mutation BUSY:retry_after_tb.
uint64_t mtime; // Unix seconds в ответах/записях каталога.
uint8_t version[16]; // Непрозрачный снимок объекта; нули BEGIN означают создание.
uint8_t upload[16]; // Случайный токен temp/потока; проверяется вместе с group/peer/owner/path.
uint8_t hash[32]; // SHA256 полного файла для BEGIN/COMMIT/END.
char path[MF_PATH_MAX + 1], target[MF_PATH_MAX + 1]; // NUL-строки; target используется RENAME.
uint32_t data_len; // Длина data; фактический предел MF_BLOCK_SIZE, wire использует u16.
uint8_t data[MF_BLOCK_SIZE]; // Байты файла, LIST entries или QUOTA, по команде.
};
/* Прочитать/записать ровно8 байт LE; буферы обязательны и имеют достаточный размер. */
uint64_t mf_get64(const uint8_t* p);
void mf_put64(uint8_t* p, uint64_t value);
/* 1 — все n байт нулевые, иначе0. */
int mf_zero(const uint8_t* p, size_t n);
/* Проверка UTF-8 и переносимых компонентов. Пустой путь допустим только для корня. */
/* 1/0: строгий UTF-8, путь<=1024 байт, до60 компонентов по1..255 байт; root_allowed разрешает пустую строку.
* Отвергает leading '.', trailing '.'/' ', ASCII C0/DEL и Windows device names.
* Запрещены обратная косая черта, двоеточие, '*', '?', '"', '<', '>', '|'. */
int mf_path_valid(const char* path, int root_allowed);
/* Записать wire: длина>0 либо-1 при переполнении; выход не меняется при отказе длины.
* Проверяет размеры, но не семантику command/ID/path. message/wire обязательны и не перекрываются. */
int mf_encode(const struct mf_message* message, uint8_t* wire, size_t capacity);
/* 0/-1: проверить формат/точную длину/общие поля и декодировать. При ошибке message может быть изменён.
* Невалидный UTF-8/путь и смысл полей отдельных команд проверяет вызывающий сервис/storage. */
int mf_decode(const uint8_t* wire, size_t length, struct mf_message* message);
/* Заимствованные статические строки; неизвестное значение возвращает Unknown error/UNKNOWN. */
const char* mf_status_text(int status);
const char* mf_command_text(int command);
#ifdef __cplusplus
+9 -3
View File
@@ -12,7 +12,6 @@
#define STORE_FILES 64
#define STORE_UPLOADS 16
#define STORE_RECEIPTS 64
#define RECEIPT_TB 1200000ULL
#define STORE_USERS 256
// Windows сравнивает имена без регистра; алиас не должен обходить неизменяемость avatars.
@@ -578,6 +577,8 @@ void mf_storage_execute(struct mf_storage* s, uint64_t group, uint64_t peer, con
return;
}
int mutates = q->command == MF_COMMIT || q->command == MF_MKDIR || q->command == MF_RENAME || q->command == MF_REMOVE_PATH;
uint64_t first_expiry = UINT64_MAX;
if (mutates) r->cursor = 0;
struct store_receipt* slot = NULL;
uint8_t wire[MF_WIRE_MAX], fingerprint[32];
int n = mf_encode(q, wire, sizeof(wire));
@@ -586,6 +587,7 @@ void mf_storage_execute(struct mf_storage* s, uint64_t group, uint64_t peer, con
for (unsigned i = 0; i < STORE_RECEIPTS; i++) {
struct store_receipt* p = &s->receipts[i];
if (p->until <= now) { if (!slot) slot = p; continue; }
if (p->until < first_expiry) first_expiry = p->until;
if (p->group != group || p->peer != peer) continue;
if (p->response.request_id == q->request_id) {
if (memcmp(p->fingerprint, fingerprint, 32)) r->status = MF_BAD_PACKET;
@@ -597,7 +599,11 @@ void mf_storage_execute(struct mf_storage* s, uint64_t group, uint64_t peer, con
*r = p->response; r->request_id = q->request_id; return;
}
}
if (mutates && !slot) r->status = MF_BUSY;
if (mutates && !slot) {
r->status = MF_BUSY; r->cursor = first_expiry - now;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: receipts full id=%llu retry_after_tb=%llu",
(unsigned long long)q->request_id, (unsigned long long)r->cursor);
}
else {
mf_storage_sweep(s, now); r->status = (uint16_t)store_command(s, group, peer, q, r);
/* Idle отсчитывается от завершения I/O, а не от начала медленного read/write. */
@@ -615,7 +621,7 @@ void mf_storage_execute(struct mf_storage* s, uint64_t group, uint64_t peer, con
}
}
if (!r->status && mutates) {
slot->group = group; slot->peer = peer; slot->until = now + RECEIPT_TB;
slot->group = group; slot->peer = peer; slot->until = now + MF_REPLAY_TB;
memcpy(slot->fingerprint, fingerprint, 32); slot->response = *r;
}
DEBUG_TRACE(DEBUG_CATEGORY_MEDIA, "member_files: storage result id=%llu cmd=%s status=%s bytes=%u more=%u cursor=%llu",
+20 -5
View File
@@ -1,17 +1,32 @@
/* Серверное хранилище. Вызывать execute/sweep/destroy последовательно, только без
* одновременно работающего worker. create не обращается к диску; initialize выполняет
* стартовый обход в worker до запросов. ACL проверяет member_files.c перед каждой работой. Файлы временных загрузок
/* Серверное хранилище. После create все initialize/execute/sweep/destroy выполняются
* последовательно одним disk worker; блокировки внутри storage нет. create не обращается к диску;
* ещё не инициализированный пустой owner допустимо destroy в потоке создателя.
* initialize выполняет стартовый обход до запросов. ACL проверяет member_files.c перед каждой работой.
* Storage сам проверяет group/peer/owner и принадлежность мутации UID, но не членство/подписанные роли.
* Файлы временных загрузок
* закрываются после 10 секунд простоя, удаляются после upload_ttl; restart отменяет их.
* Открытые regular-файлы кешируются, максимум 64. Родительские пути не следуют ссылкам. */
* Открытые regular-файлы кешируются, максимум64 (включая temp); активных upload до16.
* Квота/числа объектов общие для UID во всех группах и восстанавливаются только при initialize.
* Успешные COMMIT/MKDIR/RENAME/REMOVE занимают одну из64 квитанций на MF_REPLAY_TB от начала обработки.
* Родительские пути не следуют ссылкам. Файлы и счётчики не образуют откатываемую транзакцию. */
#ifndef MEMBER_FILES_STORAGE_H
#define MEMBER_FILES_STORAGE_H
#include "protocol.h"
struct mf_storage;
/* root — NUL-путь1..2047 байт, quota/TTL>0 (байты и timebase0.1мс); копирует root, возвращает owner/NULL. */
struct mf_storage* mf_storage_create(const char* root, uint64_t quota, uint64_t upload_ttl_tb);
/* Однократный стартовый обход. До его завершения execute возвращает BUSY, после ошибки — её код. */
/* MF_OK/положительный mf_status: создать/открыть root, удалить старые .uploads, восстановить квоты.
* Повтор после успеха — no-op; после ошибки возвращает тот же код, нужен новый owner/restart.
* До завершения execute возвращает BUSY, после ошибки — её код, частичные счётчики не выдаются. */
int mf_storage_initialize(struct mf_storage* storage);
/* Закрыть fd/корень, удалить активные temp и освободить owner; опубликованные файлы сохраняются, NULL допустим. */
void mf_storage_destroy(struct mf_storage* storage);
/* Закрыть idle fd и удалить просроченные uploads; now — монотонный timebase0.1мс, NULL допустим. */
void mf_storage_sweep(struct mf_storage* storage, uint64_t now);
/* Выполнить LIST..REMOVE либо QUOTA; STREAM преобразует transfer.c. Аргументы обязательны и не перекрываются.
* request не меняется; response получает копию запроса и результат, status/data_len определяют пригодность данных.
* Одинаковый ID/содержимое мутации возвращает квитанцию; конфликт — BAD_PACKET. После окна гарантий повтора нет.
* Полный кеш: BUSY.cursor — остаток до ближайшего освобождения в timebase; квитанции не вытесняются. */
void mf_storage_execute(struct mf_storage* storage, uint64_t group, uint64_t peer,
const struct mf_message* request, struct mf_message* response);
/* Диагностика/тест: число файловых дескрипторов, исключая каталог корня. */
+18 -4
View File
@@ -36,6 +36,7 @@ struct mf_transfer {
EVP_MD_CTX* digest;
int fd, server, sending, starting, active, opened, busy, stopping, finished_io, peer_down;
int status, work_status, reply_ready, reply_final, verified, committed, end_sent, end_received;
int temp_owned; // Только свой эксклюзивно созданный локальный temp можно публиковать/удалять.
enum transfer_work work;
uint64_t size, offset, received, read_offset, io_offset, io_bytes, io_size, progress_tb, last_event, last_log, logged_offset;
unsigned head, count, io_head, io_count;
@@ -59,6 +60,7 @@ static void transfer_event(struct mf_transfer* t, int finished) {
}
static void transfer_wake(struct mf_transfer* t) {
if (t->stopping && !t->reply_ready) return;
if (t->pump || t->waiter.internal || t->waiter.call_soon_id || t->files->closing) return;
t->pump = uasync_call_soon(t->files->inst->ua, t, transfer_pump);
if (!t->pump) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "member_files: stream pump allocation failed id=%llu", (unsigned long long)t->request.id);
@@ -129,7 +131,12 @@ static void transfer_work(void* arg) {
struct mf_disk_info after;
if (!rc) rc = mf_disk_info(t->fd, &after);
if (!rc && memcmp(&after, &t->source, sizeof(after))) rc = MF_STALE;
} else rc = mf_disk_local_open(t->temp, 1, &t->fd);
} else {
rc = mf_disk_local_open(t->temp, 1, &t->fd);
if (!rc) t->temp_owned = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: download temp id=%llu created=%d status=%s path=%s",
(unsigned long long)t->request.id, t->temp_owned, mf_status_text(rc), t->temp);
}
if (!rc && t->request.upload == 0) {
t->digest = EVP_MD_CTX_new();
if (!t->digest || EVP_DigestInit_ex(t->digest, EVP_sha256(), NULL) != 1) rc = MF_IO_ERROR;
@@ -196,8 +203,13 @@ static void transfer_work(void* arg) {
if (!rc) rc = closed;
}
if (!t->server && !t->sending) {
if (!t->status && !rc) rc = mf_disk_local_publish(t->temp, t->request.local);
if (t->status || rc) mf_disk_local_remove(t->temp);
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: download cleanup id=%llu owned=%d status=%s io=%s",
(unsigned long long)t->request.id, t->temp_owned, mf_status_text(t->status), mf_status_text(rc));
if (!t->status && !rc) {
rc = mf_disk_local_publish(t->temp, t->request.local);
if (!rc) t->temp_owned = 0;
}
if ((t->status || rc) && t->temp_owned && !mf_disk_local_remove(t->temp)) t->temp_owned = 0;
}
EVP_MD_CTX_free(t->digest); t->digest = NULL;
}
@@ -259,7 +271,9 @@ static void transfer_stop(struct mf_transfer* t, int status) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "member_files: stream stopped id=%llu peer=%llx reason=%s",
(unsigned long long)t->request.id, (unsigned long long)t->request.server, mf_status_text(status));
}
t->stopping = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "member_files: stream stopping id=%llu starting=%d active=%d io=%d pending=%d",
(unsigned long long)t->request.id, t->starting, t->active, t->busy, t->pending != NULL);
t->stopping = 1; t->starting = 0;
if (t->pump) { uasync_call_soon_cancel(t->files->inst->ua, t->pump); t->pump = NULL; }
etcp_router_cancel_send_ready(t->files->inst, t->request.group, t->request.server, ETCP_RT_ID_MEMBER_FILES, &t->waiter);
if (t->pending) { struct mf_pending* p = t->pending; t->pending = NULL; member_files_cancel(p); }
+12 -1
View File
@@ -1,21 +1,32 @@
/* Единственный дисковый поток модуля. submit/done выполняются в uasync, work —
* в pthread. Очередь ограничена; completion резервируется до передачи работы.
* Для обязательной очистки reserve выполняется до открытия файлов. destroy вне
* callbacks ждёт работ и доставляет done, включая поставленные ими cleanup jobs. */
* callbacks ждёт работ и доставляет done, включая поставленные ими cleanup jobs.
* data/arg заимствованы до done; worker освобождает только собственную оболочку работы.
* ua/work/done и worker обязательны; ua живёт до конца destroy. Останавливать worker из done нельзя. */
#ifndef MEMBER_FILES_WORKER_H
#define MEMBER_FILES_WORKER_H
struct UASYNC;
struct mf_worker;
struct mf_work;
typedef void (*mf_work_fn)(void* arg);
/* error=0 — work завершён, результат операции находится в data; -1 — обычная задача не принята (синхронно). */
typedef void (*mf_done_fn)(void* arg, int error);
/* Создать pthread/mutex/condition; возвращает owner либо NULL, без дискового I/O. */
struct mf_worker* mf_worker_create(struct UASYNC* ua);
/* Резерв оболочки и completion, NULL при OOM. Сам по себе не занимает очередь/не задаёт worker. */
struct mf_work* mf_worker_reserve(void);
/* Освободить только ещё не переданный в submit_reserved резерв; NULL допустим. */
void mf_worker_unreserve(struct mf_work* work);
/* До64 обычных задач, включая ожидающие done. При отказе done(-1) синхронно, work не вызывается. */
void mf_worker_submit(struct mf_worker* worker, mf_work_fn work, void* data, mf_done_fn done, void* arg);
/* Передать резерв worker без проверки лимита64: только обязательные cleanup continuations.
* После вызова резерв нельзя освобождать/использовать повторно. Work выполняется FIFO, done ровно один раз. */
void mf_worker_submit_reserved(struct mf_worker* worker, struct mf_work* reserved,
mf_work_fn work, void* data, mf_done_fn done, void* arg);
// После quiesce владельца, вне callbacks: дождаться работ и их cleanup continuations, не останавливая pthread.
// Не запускает uasync рекурсивно; порядок done при drain не обещается. NULL допустим.
void mf_worker_drain(struct mf_worker* worker);
/* Drain, затем join и освобождение. Работы не прерываются и получают обычный done(0); NULL допустим. */
void mf_worker_destroy(struct mf_worker* worker);
#endif
+29 -2
View File
@@ -14,6 +14,10 @@
#include <string.h>
#include <stdio.h>
#include <pthread.h>
#ifndef _WIN32
#include <signal.h>
#include <sys/wait.h>
#endif
static int failures;
#define EXPECT(condition, name) do { if (!(condition)) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "FAIL: %s line=%d", name, __LINE__); failures++; } } while (0)
@@ -179,6 +183,28 @@ static void storage_test(const char* directory) {
}
#ifndef _WIN32
/* Ошибочный local source не должен блокировать единственный disk worker до проверки типа. */
static void local_fifo_test(const char* directory) {
char path[1200]; snprintf(path, sizeof(path), "%s/local-fifo", directory);
EXPECT(!mkfifo(path, 0600), "FIFO source fixture");
pid_t child = fork();
EXPECT(child >= 0, "FIFO source child");
if (!child) {
alarm(2);
int fd = -1; struct mf_disk_info info;
int opened = mf_disk_local_open(path, 0, &fd);
int valid = opened == MF_OK && mf_disk_info(fd, &info) == MF_INVALID_PATH;
if (fd >= 0 && mf_disk_close(fd)) valid = 0;
_exit(valid ? 0 : 1);
}
if (child > 0) {
int status = 0;
EXPECT(waitpid(child, &status, 0) == child && WIFEXITED(status) && !WEXITSTATUS(status), "FIFO open returns before type rejection");
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "member_files: FIFO source child status=%d; expected normal exit before2s", status);
}
test_unlink(path);
}
static void large_offset_test(const char* directory) {
char path[1200]; snprintf(path, sizeof(path), "%s/large-offset", directory);
int fd = -1; uint64_t end = (1ULL << 33) + 4;
@@ -393,10 +419,11 @@ int main(void) {
char directory[1024] = "/tmp/utun_member_files_XXXXXX";
if (test_mkdtemp(directory)) return 1;
protocol_test(); storage_test(directory); avatar_storage_test(directory); quota_test(directory);
count_test(directory); quota_scan_failure_test(directory); worker_test();
count_test(directory); quota_scan_failure_test(directory);
#ifndef _WIN32
large_offset_test(directory);
local_fifo_test(directory); large_offset_test(directory);
#endif
worker_test();
printf("member_files: %s failures=%d directory=%s\n", failures ? "FAIL" : "PASS", failures, directory);
return failures ? 1 : 0;
}
+67 -4
View File
@@ -51,6 +51,8 @@ struct network_test {
unsigned restart_tick;
unsigned preservation_case;
char preserved_temp[864];
unsigned mutation_index, busy_responses, dropped_response;
etcp_recv_fn reader_receive;
struct api_case api[8];
void* timer;
};
@@ -163,6 +165,30 @@ static void response(void* arg, const struct mf_message* r) {
struct network_test* t = arg; t->response = *r; t->answered = 1;
}
/* Потеря первого результата проверяет повтор с тем же ID без повторного mkdir. */
static void mutation_receive(struct ETCP_CONN* conn, struct ll_entry* e) {
struct network_test* t = active;
struct mf_message m;
if (e && e->dgram && e->len > ROUTER_SVC_PAYLOAD_OFF &&
!mf_decode(e->dgram + ROUTER_SVC_PAYLOAD_OFF, e->len - ROUTER_SVC_PAYLOAD_OFF, &m) &&
m.kind == MF_RESPONSE && m.command == MF_MKDIR && m.request_id >= 1000 && m.request_id < 1076) {
if (m.status == MF_BUSY) t->busy_responses++;
if (!m.status && m.request_id == 1000 && !t->dropped_response) {
t->dropped_response = 1;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "member_files test: dropped successful mutation response id=1000; waiting for replay");
queue_dgram_free(e); queue_entry_free(e); return;
}
}
t->reader_receive(conn, e);
}
static void mutation_start(struct network_test* t) {
struct mf_message q = { .command = t->phase == 26 ? MF_STAT : MF_MKDIR, .owner = t->ids[READER],
.request_id = t->phase == 26 ? 2000 + 2 * t->mutation_index : 1000 + t->mutation_index };
snprintf(q.path, sizeof(q.path), "bulk_%02u", t->mutation_index); t->answered = 0;
if (!member_files_request(t->nodes[READER], GROUP, t->ids[SERVER], &q, response, t)) fail(t, "bulk mutation start");
}
static int put_member(struct network_test* t, int destination, int member, int deleted) {
uint8_t msg[512], join[64], tags_sig[64], tree[64];
uint64_t ts = 100000;
@@ -238,7 +264,7 @@ static int downloaded(struct network_test* t) {
static void tick(void* arg) {
struct network_test* t = arg; t->timer = NULL;
if (++t->ticks > 6000) { fail(t, "60s deadline"); return; }
if (++t->ticks > 30000) { fail(t, "300s deadline including receipt expiry"); return; }
// Отсчитываем полные 6с от фактической блокировки worker, не от постановки задания.
pthread_mutex_lock(&t->disk_mutex);
if (t->disk_started_tb && !t->disk_released && get_time_tb() - t->disk_started_tb >= 60000) {
@@ -251,10 +277,14 @@ static void tick(void* arg) {
struct ETCP_ROUTER_CONN* route = etcp_router_conn_get(t->nodes[AUTHOR], GROUP, t->ids[SERVER], ETCP_RT_ID_MEMBER_FILES);
if (route->inflight_q->count > t->peak_inflight) t->peak_inflight = route->inflight_q->count;
if (t->phase == 0) {
if (t->nodes[SERVER]->member_files->storage_status == MF_OK) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "member_files test: storage ready; upload creates its own CM route request");
t->phase = 1; transfer(t, 1, 1);
if (t->ticks == 1) {
struct member_files* files = t->nodes[SERVER]->member_files;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "member_files test: startup tick=%u storage_status=%d closing=%d server=%llx",
t->ticks, files->storage_status, files->closing, (unsigned long long)t->ids[SERVER]);
}
int a = etcp_router_get_path(t->nodes[AUTHOR], GROUP, t->ids[SERVER], ETCP_RT_ID_MEMBER_FILES).next_hop_node_id != 0;
int b = etcp_router_get_path(t->nodes[READER], GROUP, t->ids[SERVER], ETCP_RT_ID_MEMBER_FILES).next_hop_node_id != 0;
if (a && b && t->nodes[SERVER]->member_files->storage_status == MF_OK) { t->phase = 1; transfer(t, 1, 1); }
} else if (t->phase == 1 && t->finished) {
if (t->transfer_status) { fail(t, mf_status_text(t->transfer_status)); return; }
t->phase = 2; request(t, MF_STAT, t->ids[AUTHOR]);
@@ -332,6 +362,39 @@ static void tick(void* arg) {
fail(t, "download refusal/cancel removed unowned temp or changed target"); return;
}
if (++t->preservation_case < 3) { preservation_start(t); goto again; }
t->phase = 24;
t->reader_receive = t->nodes[READER]->router_bindings.callbacks[ETCP_RT_ID_MEMBER_FILES];
t->nodes[READER]->router_bindings.callbacks[ETCP_RT_ID_MEMBER_FILES] = mutation_receive;
mutation_start(t);
} else if (t->phase == 24 && t->answered) {
if (t->response.status) { fail(t, "bulk mutation did not survive busy/replay"); return; }
if (++t->mutation_index < 76) { mutation_start(t); goto again; }
t->nodes[READER]->router_bindings.callbacks[ETCP_RT_ID_MEMBER_FILES] = t->reader_receive;
if (!t->dropped_response || !t->busy_responses) { fail(t, "bulk mutation did not exercise lost result and busy"); return; }
t->phase = 25; request(t, MF_QUOTA, t->ids[READER]);
} else if (t->phase == 25 && t->answered) {
if (t->response.status || t->response.data_len != MF_QUOTA_BYTES || mf_get64(t->response.data) ||
mf_get64(t->response.data + 16) || mf_get64(t->response.data + 24) || mf_get64(t->response.data + 32) != 76) {
fail(t, "bulk mutation duplicated or lost directories"); return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "member_files test: PASS76 mutations, lost response replayed once, busy_responses=%u directories=76",
t->busy_responses);
t->phase = 26; t->mutation_index = 0; mutation_start(t);
} else if (t->phase == 26 && t->answered) {
if (t->response.status) { fail(t, "bulk removal did not survive busy"); return; }
if (t->response.command == MF_STAT) {
struct mf_message q = t->response; q.command = MF_REMOVE_PATH; q.request_id++; t->answered = 0;
if (!member_files_request(t->nodes[READER], GROUP, t->ids[SERVER], &q, response, t)) fail(t, "bulk remove start");
goto again;
}
if (++t->mutation_index < 76) { mutation_start(t); goto again; }
t->phase = 27; request(t, MF_QUOTA, t->ids[READER]);
} else if (t->phase == 27 && t->answered) {
if (t->response.status || t->response.data_len != MF_QUOTA_BYTES || mf_get64(t->response.data) ||
mf_get64(t->response.data + 16) || mf_get64(t->response.data + 24) || mf_get64(t->response.data + 32)) {
fail(t, "bulk removal left directories or quota"); return;
}
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "member_files test: PASS76 removals, directories=0, used=0, reserved=0");
t->phase = 10; t->answered = 0;
struct mf_message q = { .command = MF_STAT, .owner = t->ids[AUTHOR] }; snprintf(q.path, sizeof(q.path), "cancelled.bin");
if (!member_files_request(t->nodes[READER], GROUP, t->ids[SERVER], &q, response, t)) { fail(t, "cancelled STAT"); return; }