Skip to content

Прогон CI: дерево с исправлением - #4

Closed
ksv47 wants to merge 157 commits into
mainfrom
ci-check-fixed
Closed

ksv47 wants to merge 157 commits into
mainfrom
ci-check-fixed

Conversation

@ksv47

@ksv47 ksv47 commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Технический PR внутри форка. Будет закрыт.

ksv47 and others added 30 commits August 22, 2026 01:41
Апстрим под Linux не собирался ни в одном коммите. Закрыто:
- недостающие using Avalonia / Controls.Primitives / Controls.Presenters в 15 файлах окон
- GetContainerForItemOverride -> CreateContainerForItemOverride (Avalonia 11)
- ToggleButton.OnToggle -> Toggle
- PasswordBox: своя реализация поверх TextBox, в Avalonia такого контрола нет
- using Configuration_Management.Services для IDialogService
- снята неоднозначность Path в MainWindow

Осталось 90 ошибок, требующих портирования по существу.
ToolTip: в Avalonia это присоединённое свойство, заменено на ToolTip.SetTip
в MainWindow, CacheCleanWindow, GroupEditWindow (14 мест).
ModalWindowBase.ShowDialogSync: protected -> public, метод вызывается
у экземпляра соседнего окна, а не через наследование (12 мест).
ThemeManager: добавлены using Avalonia.Styling (ThemeVariant),
Avalonia.VisualTree (GetVisualChildren), Avalonia.Controls.ApplicationLifetimes;
Application.Current.Windows заменено на desktop.Windows из
IClassicDesktopStyleApplicationLifetime.
App: добавлен приватный Shutdown, выход выполняет desktop-lifetime,
у Avalonia Application такого метода нет.
CacheCleanWindow: CapturePointer/ReleasePointerCapture заменены на
e.Pointer.Capture(grip) и e.Pointer.Capture(null).
GroupEditWindow: алиас Path -> Avalonia.Controls.Shapes.Path,
MainWindow: Path.Combine квалифицирован как System.IO.Path.

Осталось 45 ошибок из 90.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2c1c43d95b11481d8a1316b7e5601e359978dce2)
InverseBoolToVisibilityConverter переписан на bool: в Avalonia видимостью
управляет IsVisible, перечисления Visibility нет. Тот же приём автор уже
применил в соседнем GroupVisibilityConverter.Avalonia.cs.
Background, BorderBrush, BorderThickness, Padding объявлены на
TemplatedControl, а не на Control: заменены ссылки на свойства
в TemplateBinding, в Setter стиля и в ThemeBrushes.Bind.
Свойства шрифта в ThemeManager.ApplyFont ставятся через присоединённые
TextElement.SetFontFamily/SetFontSize/SetFontWeight/SetFontStyle,
они принимают Control и наследуются потомками.
У Grid и StackPanel нет Padding: отступ статус-бара перенесён на его
внешний Border, отступ содержимого диалога заменён на Margin.
Кисти hover-состояния поля поиска объявлены как IBrush: var выводил
IImmutableSolidColorBrush, а ресурс темы отдаёт IBrush.
GetObservable это метод расширения, добавлен явный получатель this.
IsCheckedProperty имеет тип bool?, добавлена проекция v => v == true.
ThemeBrushes: добавлен using Avalonia.Controls для GetResourceObservable.

Осталось 15 ошибок из 90.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 5b148b151d5aac039d6be88ffeba804c879eb514)
ControlTheme: в Avalonia 11 у него нет свойства Template, шаблон задаётся
сеттером TemplatedControl.TemplateProperty в коллекции Setters.
Замысел автора сохранён: свой шаблон Border + ContentPresenter вместо
хрома Fluent у PanelButton и SegmentButton.
FuncTreeDataTemplate: у конструктора три параметра, а не два. Добавлен
первый аргумент typeof(object) (совпадение по типу элемента), функция
построения обёрнута в (item, _) => BuildTreeRow(item), так как она
принимает ещё и INameScope. Четыре места: MainWindow, GroupPickerWindow,
GroupSettingsWindow, PlatformVersionPickerWindow.
Window.Owner в Avalonia имеет protected set, снаружи не ставится.
В AvaloniaDialogService владелец задаётся перегрузкой Show(owner),
она же выставляет Owner внутри.
TrayIcon.SetIcons принимает Application, а не окно: передаётся
Application.Current, иконка трея в Avalonia живёт на уровне приложения.
RelayCommand: метод без параметров вместе с предикатом _ => ... не даёт
подходящей перегрузки, метод обёрнут в _ => Method(). Семь команд.

Ошибок компиляции 0, предупреждений 29, все они авторские и были до правок.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit a5fc6f1de2152cf13d99ca1a198d69bf1d5ff6bc)
Два дефекта, вскрывшихся на первом исполнении.

UiMetrics.AddBrushTransition и AddOpacityTransition падали с
NullReferenceException: Animatable.Transitions в Avalonia по умолчанию
null, коллекцию нужно создать. Добавлено target.Transitions ??= new Transitions().
Компилятор предупреждал об этом (CS8602), автор предупреждение не видел.

TemplateBinding внутри шаблонов PanelButton и SegmentButton падал с
NotSupportedException: у Bind приоритет по умолчанию LocalValue, а
у выражения TemplateBinding приоритет Template, и они конфликтуют.
Тринадцать привязок переведены на индексаторную форму
control[!Property] = new TemplateBinding(...), она берёт приоритет
из самой привязки.

Окно открывается и отрисовывается. Логи обоих падений в _port.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7b0cde251656fb7cbc9f2973b5a0fbfd868e7203)
App.axaml не подключал Themes/LightTheme.axaml, только Icons.axaml.
Из-за этого тринадцать ключей ресурсов, которые запрашивает код
(AccentBrush, TextPrimaryBrush, CardBackgroundBrush, ItemHoverBrush,
SecondaryButton*Brush, TextOnAccentBrush, FavoriteBrush и другие),
не разрешались: иконки на кнопках оставались без Fill и были невидимы,
акцентный фон primary-кнопок не применялся. ThemeManager.ApplyColors
публикует другое семейство имён (ключ схемы плюс Brush), поэтому
работали только AccentColorBrush, BorderColorBrush и ещё пять.
Словарь темы подключён, кисти в нём заданы через DynamicResource на
цвета, которые ApplyColors перекрывает, поэтому смена схемы работает.
Скриншоты до и после в _port.

App.Shutdown: desktop.Shutdown нельзя вызывать из
OnFrameworkInitializationCompleted. Он гасит Dispatcher до входа
в цикл сообщений, и MainLoop падает с InvalidOperationException
«Cannot perform requested operation because the Dispatcher shut down».
Это видно в _port/run-crash-transitions.log. Оба вызова относятся
к этапу запуска (второй экземпляр и фатальная ошибка), поэтому выход
делается через Environment.Exit. Проверено исполнением: запуск второго
экземпляра при живом первом даёт код возврата 0 и пустой вывод.

Находка вскрыта встречным аудитом.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 03aec604b7a933b528709f7820b807a9856252db)
Эмуляция модальности была построена на цикле
while (window.IsVisible) { Dispatcher.UIThread.RunJobs(); Thread.Sleep(10); }.
RunJobs разбирает очередь задач диспетчера, но не качает события ввода
платформы: их качает только Dispatcher.MainLoop через PushFrame. Поэтому
после открытия любого диалога переставали отвечать и диалог, и главное
окно, окно уже не могло закрыться, цикл крутился вечно, а процесс не
реагировал на SIGTERM.

Проверено опытом. На свежем экземпляре переключатель темы меняет фон
с #FFFFFF на #1E293B. После вызова ShowInfo ни один клик и ни одна
клавиша не проходят ни в диалог, ни в главное окно.

Исправлено в трёх местах вложенным циклом сообщений: ModalWindowBase
ShowDialogSync, AvaloniaDialogService.ShowModalSync и RunSync
(файловые диалоги через StorageProvider страдали тем же). Окно
закрывает кадр через DispatcherFrame.Continue, а Dispatcher.PushFrame
качает события, как штатный цикл.

Заодно снято возражение встречного аудита про мнимую модальность:
при наличии владельца окно показывается через ShowDialog(owner),
то есть владелец на время диалога блокируется по-настоящему.

Проверено тем же сценарием: «Добавить» открывает диалог, клик по ОК
его закрывает, главное окно после этого снова отвечает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 59b82018ed0ed776843176c4605bb96e0f33fc2c)
HeaderBrush и HeaderTextBrush убраны из списка дефектов: это свойства
GroupNodeViewModel, привязанные через Binding с Source = group,
а не ключи ресурсов темы. Утверждение было ошибочным.

«Никогда не компилировался» заменено на проверяемое: ни один из 72
коммитов опубликованной истории не собирается под Linux. Запускал ли
автор компилятор локально, репозиторий не показывает.

Утверждение «кросс-сборки WPF не существует» неверно, есть
EnableWindowsTargeting. Но в этом проекте Windows-группы в csproj
включены по условию IsOSPlatform('Windows'), то есть по ОС сборки,
поэтому на Linux net10.0-windows не попадает в проект вовсе
(NETSDK1005). Формулировка переписана точно.

Снято внутреннее противоречие про непроверенные диалоги.
Doc-комментарии в ModalWindowBase и AvaloniaDialogService больше
не ссылаются на RunJobs: там теперь вложенный цикл через PushFrame.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Пути. Проверено на машине с установленной 1С 8.3.27 под Linux, что
платформа держит данные не там, где их ищет код. Правки строго внутри
веток #if LINUX, Windows-ветки не тронуты, прежние кандидаты оставлены
запасными на случай других раскладок дистрибутива.

OneCCacheCleaner: кеш баз лежит в ~/.1cv8/1C/1cv8, каталоги баз по GUID
прямо в нём (26 штук на проверенной машине). Код искал ~/.1cv8/1cv8,
которого не существует. Каталоги ~/.cache/1cv8 и ~/.local/share/1cv8,
которые код тоже проверяет, содержат данные встроенного браузера WebKit,
а не кеш баз. Это единственная из трёх правок с живым эффектом:
чистка кеша в Linux-сборке подключена и работает.

IbasesV8iImporter.FindDefaultPath: список баз лежит в
~/.1C/1cestart/ibases.v8i, ни один из четырёх прежних кандидатов
на диске не существует. В Linux-сборке дефект пока спящий: сюда ведёт
только FindId из ConnectionSettingsWindow, а оно не создаётся нигде.
Выстрелит, когда окно подключат.

OneCTemplateService: то же для 1cestart.cfg.

Читаемость тёмной темы. Кнопки на вторичном фоне брали текст и иконки
из TextPrimaryBrush. В тёмной схеме он становится почти белым (#F1F5F9),
а фон вторичной кнопки остаётся кремовым (#FFF3CD, тот же, что и в
светлой), поэтому надписи были нечитаемы. В схеме для этого есть
ButtonTextBrush, чёрный в обеих темах, но Avalonia-код его не
использовал вовсе. Переведены пять мест: две кнопки верхней панели,
две правой панели и глиф стрелки у кнопки очистки кеша, который вообще
не имел заданного цвета и наследовал светлый.

Проверено исполнением: текст на кнопке в тёмной теме теперь #292721
на фоне #FEF2CC. Акцентные кисти из словаря темы работают в обеих
схемах (#FDBF00 в светлой, #FFB300 в тёмной), то есть DynamicResource
внутри объединённого словаря перечитывает цвет из Application.Resources.

Сквозная проверка чистки кеша не делалась осознанно: чтобы включить
команду, нужна база в списке, а с синтетической базой режим очистки
осиротевшего кеша посчитал бы все реальные каталоги чужими.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit f1925fc)
…алогов

Находка встречного аудита, блокирующая. Добавив ~/.1cv8/1C/1cv8 в список
корней кеша, предыдущий коммит попутно оживил режим очистки осиротевшего
кеша, который до этого на Linux был мёртвым: корня не существовало.

Режим работает от обратного, он удаляет всё, что не принадлежит базам
из списка приложения. А в этом корне рядом с кешем баз лежат служебные
каталоги платформы: conf, logs, ExtCompT, STT, standalone-server.
Ни один не проходит IsVersionDirName и ни один не совпадает с именем
базы, то есть все были бы удалены. Цена такой ошибки не кеш, а настройки
платформы.

Отдельно: каталоги кеша серверных баз называются
Srvr__<сервер>__Ref__<база>__, а BuildProtectedNames кладёт в защищённые
имена только имя базы из GetCacheName. То есть даже настоящие каталоги
кеша живых баз этот проход счёл бы остатками. Фильтр по форме GUID
проблему не решает по той же причине.

У GetCacheRoots появился параметр forOrphanScan. Читается он только
внутри #if LINUX, на Windows принимается и игнорируется, поведение
не меняется. Точечная очистка по конкретной базе новый корень
по-прежнему использует, она удаляет только каталог выбранной базы.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit d1eaa76)
PlatformVersionService.Linux требовал бинарники в подкаталоге bin каталога
версии и другого варианта не допускал. На штатной установке их там нет:
1cv8 лежит прямо в каталоге версии, подкаталога bin не существует.
Обе ветки перечисления отсекали такую установку, поэтому сервис не находил
ни одной версии, и запуск 1С на Linux был невозможен.

Раскладка различается между дистрибутивами и версиями платформы, поэтому
предположение не заменено, а расширено. Добавлен ResolveBinDirectory:
берёт bin, если бинарники там, иначе сам каталог версии. Через него
проведены семь мест, включая определение разрядности через readelf.

Проверено исполнением через зонд, вызывающий сервис из собранной сборки:
FindInstalledVersionInfos возвращает обе установленные версии,
ResolveVersionBinDirectory отдаёт каталог с существующим 1cv8.

Лаунчер править не потребовалось: GetBinaryNames для тонкого клиента уже
возвращает 1cv8c и только потом 1cv8, что важно для неполных установок.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Добавлен LinuxDesktopEnvironment: семейство рабочего стола, тип сессии,
признак риска с треем, подбор файловых менеджеров и проверка наличия
в PATH. Отдельной библиотеки для этого не нужно, и XDG, и сами среды
публикуют себя через переменные окружения, обёртка читала бы то же самое.
Учитывается, что XDG_CURRENT_DESKTOP бывает списком через двоеточие
(например ubuntu:GNOME), есть запасные XDG_SESSION_DESKTOP,
DESKTOP_SESSION, KDE_FULL_SESSION и GNOME_DESKTOP_SESSION_ID.

SelectFileInManager перебирал nautilus, потом dolphin, без оглядки
на среду. Process.Start считается успехом, если бинарник просто нашёлся
в PATH, поэтому первый установленный менеджер выигрывал, даже будучи
чужим для среды, а запасные варианты не срабатывали никогда. Теперь
порядок задаёт текущее окружение, список отфильтрован по наличию в PATH,
и мгновенное падение процесса (код возврата за 700 мс) переводит
на следующий кандидат. Добавлены nemo, thunar, caja, dde-file-manager;
thunar получает путь без --select, он этого ключа не понимает.

SetupTray: на GNOME Shell без расширения AppIndicator иконка трея
не появляется молча, без ошибки. Теперь это пишется в журнал, чтобы
не выглядело поломкой приложения.

DetectArchViaReadelf читает разрядность с любого доступного бинарника,
а не только с 1cv8: установка бывает неполной, у версии с одним тонким
клиентом файла 1cv8 нет. Замечание встречного аудита.

Проверено исполнением: окружение определяется как Kde, сессия x11,
трей не под вопросом, в списке менеджеров только dolphin с --select,
потому что nautilus на машине не установлен.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 1698e0b)
Обе находки встречного аудита, обе последствия предыдущей правки.
Добавив плоскую раскладку, я изменил смысл binDir: теперь это либо
<версия>/bin, либо сам каталог версии. Два потребителя продолжали
считать, что это всегда bin, и брали каталог уровнем выше.

FindOneCStart искал common/1cestart в родителе binDir. На плоской
раскладке это уводит на уровень выше каталога версии: для
/opt/1cv8/x86_64/8.3.27.2214 проверялось /opt/1cv8/x86_64/common.
На этой машине промах маскировался запасным списком, где есть
/opt/1cv8/common/1cestart, но установка со стартером в <версия>/common
не нашлась бы. Теперь проверяются оба варианта.

TryAddUsrBinSymlink вычислял каталог версии как родителя каталога цели
симлинка. На плоской раскладке каталог цели сам является каталогом
версии, поэтому LooksLikeVersion не срабатывал и установка через
/usr/bin/1cv8 пропускалась молча. Теперь каталог цели сначала
проверяется сам, и только потом берётся родитель.

Проверено исполнением: FindPlatformVersionDirs отдаёт обе версии
с binDir, равным каталогу версии, FindOneCStart возвращает
существующий /opt/1cv8/common/1cestart.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6d4fd47)
Три находки встречного аудита, все блокирующие. Я задал ключ выделения
одним правилом «--select всем, кроме thunar», и оно неверно для трёх
менеджеров из шести, то есть заявленная поддержка nemo, thunar
и dde-file-manager была фиктивной.

thunar от пути к файлу открывает сам файл приложением по умолчанию,
а не каталог с выделением. Для базы это означало бы попытку открыть
многогигабайтный 1Cv8.1CD чем придётся. Ему передаётся каталог,
выделения при этом не будет.

nemo ключа --select не понимает вовсе: он выделяет файл по одному
пути. С чужим ключом родной менеджер Cinnamon отваливался всегда.

У dde-file-manager ключ выделения свой, --show-item.

Способ показа вынесен в таблицу SelectSyntax: ключ и признак того,
что менеджеру нужен путь к файлу, а не к каталогу. Менеджер без записи
в таблице в список не попадает, поэтому чужой синтаксис подставиться
больше не может.

Заодно освежены doc-комментарии, описывавшие прежний жёсткий порядок
nautilus, затем dolphin.

Проверено исполнением: таблица отдаёт каждому менеджеру свой синтаксис,
на этой машине в списке остаётся только dolphin с --select.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit d8b5727)
Четыре связанных дефекта, найденных исполнением. До этого центральный
список не показывал ни одной базы никогда, а добавление было заглушкой.

1. Дерево не отрисовывалось вовсе. LeveledTreeView и LeveledTreeViewItem
   не переопределяли StyleKeyOverride. В Avalonia тема оформления ищется
   по ключу стиля, а для наследников TreeView и TreeViewItem её в Fluent
   нет, поэтому шаблон не находился и контрол не рисовался. Это же
   объясняет, почему список выглядел пустым при непустых данных.

2. Селектор стиля контейнеров перестал совпадать после пункта 1: он
   сопоставляется по ключу стиля, а тот теперь TreeViewItem. Селектор
   исправлен, иначе отключение стандартной подсветки перестало бы
   действовать.

3. RebuildTree строил только группы и никогда не раскладывал базы
   по узлам. Добавлен DistributeInfobases по эталону WPF-версии:
   разбор по полному пути группы, отдельные узлы «закреплённые»
   и «без группы», подавление уведомлений на время наполнения,
   PopulateItems в конце.

4. Раскрытие узла в интерфейсе не было связано с моделью, поэтому
   состояние не переживало перестроение дерева, а команды «развернуть
   все» и «свернуть все» до интерфейса не доходили. Добавлена двусторонняя
   привязка TreeViewItem.IsExpanded к свойству узла.

Подключены потоки добавления и редактирования базы. AddInfobase
открывает AddEditWindow и по выбранному типу ведёт в CreateInfobaseWindow,
ConnectionSettingsWindow или GroupEditWindow, EditInfobase открывает
настройки выбранной базы. Логика повторяет WPF-версию MainViewModel.
Добавлены вспомогательные InstalledPlatformVersions, SaveGroupsSilently
и OwnerWindow.

Проверено исполнением полностью: диалог выбора типа открывается,
настройка подключения заполняется, база сохраняется в infobases.json
с верными данными и отображается карточкой в дереве под узлом
«Без группы».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2fba3dd)
Три блокирующих находки и три существенных.

PasswordBox тоже не переопределял StyleKeyOverride, та же болезнь, что
у дерева. Его три экземпляра создаёт окно настройки подключения, то есть
поля пароля оставались без шаблона. Ключ стиля задан на TextBox.

Раскладка баз по узлам не нормализовала путь группы: сравнивалась точная
строка. База с путём вида «Отдел\Подгруппа» или «Отдел / Подгруппа»
попадала в «Без группы», хотя группа существует. Добавлена
NormalizeGroupPath по эталону WPF, индексация ведётся и по исходному,
и по нормализованному пути.

Переключатель группировки ничего не менял: ApplyFilter в обычном режиме
заново показывал прежние узлы. ApplyFilter переписан по эталону WPF:
и при активном фильтре, и при отключённой группировке строится один
плоский узел. Это же закрывает прежний долг с FlatItems: дерево привязано
только к GroupNodes, поэтому поиск, «Избранное» и «Недавние» показывали
пустоту. Проверено исполнением: поиск даёт узел «Найдено (1)» с карточкой.

После добавления, создания и редактирования не вызывался экспорт
в ibases.v8i. Добавлен ExportToIbasesAfterLocalChange с теми же
предохранителями, что в WPF: работает только при явно включённом режиме
экспорта (по умолчанию IbasesSyncMode.None, файл платформы не трогается),
перед записью снимается резервная копия.

Проверено исполнением на изолированных настройках с целью в отдельном
каталоге: файл выгружается корректно, обе базы на месте, контрольная
сумма реального ibases.v8i пользователя не изменилась.

Мелкие: в окно настройки подключения передаются серверы и порты
из уже зарегистрированных клиент-серверных баз (автодополнение),
родителем новой группы берётся группа выбранной базы, а не только
выделенный узел дерева.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 89a956a)
EditInfobase портил пользовательские данные. Он заменял элемент списка
объектом из диалога, а туда переносятся только редактируемые поля.
Терялись история запусков (до 30 записей), SortOrder и номер горячей
клавиши избранного, причём всё это пишется в infobases.json, то есть
пропадало с диска после любой правки базы. Теперь изменения применяются
к существующему объекту, как в WPF-версии, где это оговорено
комментарием отдельно.

PasswordBox: убран дублирующий using System, оставшийся от предыдущей
правки.

Привязка IsExpanded перенесена со стиля на подготовку контейнера.
Стиль вешал её на все контейнеры, включая строки баз, у которых такого
свойства нет, и журнал заполнялся предупреждениями привязки при каждом
перестроении дерева. Теперь привязка ставится только узлам групп.
Проверено: предупреждений ноль.

Отключение стандартной подсветки не работало, а комментарий утверждал
обратное. Сеттер снимал только фон покоя, а выделение и наведение Fluent
задаёт вложенным стилем на части шаблона, поэтому синяя полоса рисовалась
за карточкой строки. Добавлены адресные стили на PART_LayoutRoot
для состояний :selected и :pointerover, комментарий исправлен.
Проверено исполнением: выделение рисует только карточка приложения.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 557c893)
Три долга, названные в предыдущем PR как незакрытые.

Свёрнутость групп теперь переживает перестроение дерева и перезапуск.
Раньше каждый RebuildTree создавал узлы заново и принудительно разворачивал
их, поэтому свёрнутые пользователем группы раскрывались после любого
добавления, правки или закрепления. Состояние хранится в AppSettings
по NodeKey: полный путь для групп, маркер для служебных узлов, не зависит
от языка интерфейса. Отслеживается само свойство IsExpanded, а не места
его изменения, потому что оно меняется и мышью через привязку контейнера,
и командами «развернуть все» и «свернуть все».

Сортировка групп применяется при раскладке дерева. Прежний SortGroups
сортировал узлы и тут же звал RebuildTree, который пересобирает всё
из _groups в исходном порядке, то есть был пустой операцией. Теперь
направление запоминается и применяется к корням и подгруппам.

Порядок узлов приведён к WPF-версии: закреплённые, без группы, затем
группы. Раньше узел «без группы» стоял в конце.

HotkeyBox получил StyleKeyOverride: он болел тем же, чем дерево
и PasswordBox. Сейчас в Linux-сборке не создаётся, правка на будущее.

Проверено исполнением: две группы отрисовываются по алфавиту, базы
разложены по ним верно, свёрнутая группа записывается в CollapsedGroups
и остаётся свёрнутой после перезапуска.

Оговорка: смену направления сортировки проверить нечем. Команды
SortGroupsAscending и SortGroupsDescending существуют во вьюмодели,
но в интерфейс Avalonia не выведены ни одним элементом, нажать их негде.
Это отдельный незакрытый долг.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit d06e75f)
…х узлов

Собственная находка при перечитывании: комментарий утверждал, что ключ
не зависит от языка интерфейса, но NodeKey у узла без группы и без маркера
это отображаемое имя, то есть локализованная строка. После смены языка она
перестала бы совпадать, а с реальной группой такого же имени столкнулась бы.

Сейчас такой узел один, плоский «Найдено» при активном фильтре, и он
не подписан на отслеживание, поэтому в настройки ничего не утекало.
Но ловушка была заложена на будущее.

Добавлен PersistableNodeKey: годятся только полный путь группы
и внутренний маркер служебного узла, остальные не сохраняются.
Теперь утверждение комментария соответствует коду.

Проверено: свёрнутая группа по-прежнему записывается и остаётся
свёрнутой после перезапуска.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3c90a9b)
Массовые команды «развернуть все» и «свернуть все» писали весь файл
настроек по разу на каждый изменившийся узел, синхронно на потоке
интерфейса. У автора в WPF массовая операция завершается одним
отложенным сохранением. Добавлено пакетное сохранение, и файл не
трогается вовсе, если набор свёрнутых групп не изменился: «развернуть
все» на уже развёрнутом дереве больше не идёт на диск.

Контейнеры дерева переиспользуются, а привязка IsExpanded вешалась
при каждой подготовке без освобождения прежней, то есть выражения
привязки копились на одном контейнере. Прежняя привязка теперь
освобождается, ссылки держатся в ConditionalWeakTable.

Проверено исполнением: ввод и очистка поиска пересоздают контейнеры,
после возврата дерево в прежнем виде, свёрнутая группа осталась
свёрнутой, CollapsedGroups не изменился.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 371ed1c)
Ревизия достижимости команд показала, что восемь из двадцати восьми
команд вьюмодели не вызываются ни одним элементом интерфейса. Две из них
про теги, и переключатель «теги» в верхней панели при этом существовал
и менял свойство, за которым ничего не стояло: кнопка обманывала.

Добавлена панель между верхней панелью и рабочей областью: кнопка на
каждый тег и кнопка сброса. Видимость подчинена переключателю и наличию
тегов, состав пересобирается при изменении набора.

HasActiveTagFilter не уведомлял об изменении, поэтому кнопка сброса
не появлялась бы. Уведомление добавлено в SearchByTag и ClearTagFilters.

SegmentButton рисовал иконку даже при пустом ключе, а IconHelper на пустой
ключ подставляет запасную папку, из-за чего теги получили иконки папок.
Теперь пустой ключ означает кнопку без иконки, а тегам передаётся IconTag.

Проверено исполнением: три тега отрисованы, клик по тегу выделяет его
и фильтрует список до одной базы, кнопка сброса появляется.

Осталось недостижимым: развернуть и свернуть все, сортировка по возрастанию
и убыванию, копирование строки подключения, обновление сведений
о конфигурациях.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit c316458)
По снимкам рабочей Windows-версии: панель не прячется, когда тегов нет,
а показывает подсказку «Теги, нажмите чтобы добавить в фильтр». Чипы тегов
идут второй строкой под подсказкой, кнопка сброса прижата к правому краю
и появляется только при выбранных тегах.

Прежняя раскладка ставила подсказку и чипы в одну строку, а панель
пряталась при отсутствии тегов, из-за чего переключатель «теги»
в верхней панели выглядел неработающим.

Проверено исполнением: подсказка на месте, клик по тегу выделяет его
и фильтрует список, кнопка сброса появляется справа.

Известный косметический дефект: у выбранного чипа подложка обрывается
на последней букве.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 3d709af)
Две блокирующие находки, обе мои.

Утечка подписок с квадратичным ростом. TagFilterItems пересобирается
очисткой и добавлением по одному, а подписка на изменение коллекции
пересобирала панель на каждом шаге: из N тегов создавалось N(N+1)/2
кнопок. Каждая кнопка подписывается на пять ресурсов темы, и ни одна
подписка не освобождалась.

Исправлено с двух сторон. Вьюмодель поднимает отдельное событие
TagFiltersRebuilt один раз по завершении пересборки, окно слушает его
вместо CollectionChanged. SegmentButton получил Dispose, освобождающий
подписки, и панель освобождает старые кнопки перед очисткой: очистка
коллекции детей сама по себе их не отпускает.

Сброс выбранных тегов. RebuildTagFilters создавал элементы всегда
невыбранными, а зовётся он при каждом перестроении дерева, то есть
действующий отбор слетал после любой правки базы. В WPF выбор
сохраняется через множество имён, сделано так же.

Косметика: шаблон SegmentButton не передавал Padding ни в Border,
ни в ContentPresenter, поэтому фон измерялся ровно по содержимому
и обрезал текст выбранного чипа. Тем же были затронуты сегменты
«Все», «Избранное», «Недавние».

Проверено исполнением: чип отрисован целиком, выбор тега переживает
перестроение дерева, кнопка сброса на месте.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 4e2191e)
…анение переключателя

Три находки второго встречного аудита.

Утечка была закрыта не полностью. Мой Dispose освобождал пять подписок
самой кнопки, но содержимое кнопки (иконка и текст) подписывается
на ресурсы темы отдельно, и эти подписки выбрасывались. UpdateContent
выполняется два-три раза уже при создании кнопки, поэтому каждая
пересборка панели оставляла порядка четырёх-шести подписок на тег,
живущих до конца процесса.

ThemeBrushes.Bind теперь возвращает подписку, IconHelper.MakeIcon умеет
складывать её в переданный приёмник. Обе правки добавочные, прежние
вызывающие ничего не замечают. SegmentButton держит подписки содержимого
отдельным списком и освобождает их в начале каждого UpdateContent
и в Dispose.

Панель пересобиралась дважды на одно перестроение дерева: вьюмодель
поднимала и HasActiveTagFilter, и TagFiltersRebuilt, а окно слушает оба.
Лишнее уведомление убрано.

ShowTagFilterPanel не сохранялся, поэтому панель после перезапуска всегда
возвращалась включённой. Настройка пишется, а кнопка «теги» подтягивает
состояние из вьюмодели: она строится до загрузки настроек и без этого
разошлась бы с реальной видимостью панели.

Проверено исполнением: переключатель гасит панель, настройка пишется,
после перезапуска панель скрыта и кнопка погашена.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 0c29a9f)
По снимкам рабочей Windows-версии: список это таблица с заголовками
колонок, а не карточки с текстовыми строками. Шесть настроек видимости
колонок и шесть значений ширины во вьюмодели существовали, но ни на что
не влияли, потому что колонок не было вовсе.

Добавлено общее описание колонок, из которого строятся и заголовок,
и строки, поэтому они не могут разъехаться по составу. Значения
переехали из вторичных текстовых строк в колонки, во вторичной строке
осталось то, чего в колонках нет.

Заголовок и список помещены в одну область горизонтальной прокрутки.
Это принципиально: колонок может не хватить по ширине, и если
прокручивать только список, заголовки перестанут совпадать со значениями.
По той же причине у дерева отключена собственная горизонтальная прокрутка.

Колонке имени задан минимум, иначе при шести видимых колонках она
схлопывалась и обрезала название базы. Ширины берутся из настроек,
а при нуле (настройка ещё не трогалась) применяется свой размер
под содержимое колонки.

Проверено исполнением: заголовки Название, Версия платформы,
Конфигурация, Режим запуска, Сервер/База, Последний запуск, Размер;
значения строк стоят под своими заголовками; при нехватке ширины
включается горизонтальная прокрутка и заголовок едет вместе со строками.

Изменение ширины колонок перетаскиванием пока не сделано, ширины берутся
из настроек только на чтение.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit eac300f)
Заголовок строился до загрузки настроек. RefreshColumnHeader звался
в конструкторе окна, когда у вьюмодели ещё настройки по умолчанию,
а Initialize грузит сохранённые позже, в Loaded. Строки читали
загруженные, заголовок оставался на дефолтных, и при любом отличии
settings.json они расходились с первого запуска. Заголовок теперь
обновляется по уведомлению о колонках. NotifyColumnSettings поднимает
шестнадцать уведомлений подряд, поэтому пересборка склеивается через
очередь диспетчера.

Колонка «Режим запуска» показывала сырое хранимое значение LaunchMode
вместо локализованного ParsedLaunchMode, который стоит в разметке автора.
В английском интерфейсе колонка показывала бы русский текст.

Колонка «Сервер/База» для веб-базы подставляла WebUrl. Это была моя
отсебятина, перенесённая из старой текстовой строки карточки: у автора
колонка привязана к ServerDatabaseDisplay. Приведено к эталону.

Ширины по умолчанию приближены к значениям автора (120, 160, 120, 200,
140, 90 вместо моих более узких).

Заголовок больше не прячется целиком при отсутствии дополнительных
колонок: «Название» есть всегда, как и в WPF.

Вертикальная прокрутка вынесена во внешний ScrollViewer. Прежде она
оставалась у дерева, и при переполнении по ширине ползунок оказывался
у правого края содержимого, то есть за пределами видимой области.
Основание для переноса: аудит проверил по декомпиляции, что у TreeView
в Avalonia 11.3.20 виртуализации нет вовсе, панель элементов обычный
StackPanel. Авторский комментарий рядом утверждал обратное и был причиной
убрать внешний ScrollViewer; комментарий переписан по факту.

Проверено исполнением на недефолтных настройках: скрытая колонка «Размер»
исчезает и из заголовка, заданная ширина версии применяется, значения
стоят под своими заголовками.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 81171a2)
Заголовок таблицы стал панелью инструментов, как в рабочей WPF-версии:
слева кнопки «развернуть все», «свернуть все» и две сортировки групп
(видны только при группировке), дальше колонки звезды и булавки, имя базы
и колонки значений. Четыре команды вьюмодели, до сих пор недостижимые
из интерфейса, наконец вызываются.

Клик по заголовку «Название» и «Последний запуск» меняет поле сортировки,
повторный клик разворачивает направление. Поле и направление хранятся
в настройках и восстанавливаются при следующем запуске. Порядок баз
внутри узла считает ApplyCurrentSort: закреплённые первыми, затем
выбранное поле, имя вторым ключом. В режиме «Недавние» порядок задаёт
дата запуска, как в WPF-версии.

В строке базы появились звезда и булавка: цвет показывает состояние,
клик переключает его для этой базы (команды ToggleFavoriteFor
и TogglePinFor). Прежние значки-маркеры после имени убраны, их роль
взяли кнопки. Настройки ShowFavoritesButton и ShowPinnedButton
управляют и колонкой заголовка, и кнопкой строки сразу, как написано
в их описании в AppSettings; в WPF они меняют только ширину колонки
заголовка, и там кнопки строки от них не зависят.

Отступ уровня дерева компенсируется колонкой HeaderOffsetColumn: её
ширина считается по самой левой видимой строке после раскладки, поэтому
заголовок стоит над значениями и при группировке, и без неё.

Иконки развёртывания и свёртывания взяты из авторского Themes/Icons.xaml,
иконки сортировки нарисованы здесь: в WPF они приходят из MaterialDesign,
которого в Linux-сборке нет.
Заголовок встал над значениями. У строки перед именем есть колонка иконки
подключения, а в заголовке её не было, поэтому подпись «Название» уезжала
влево на ширину иконки. Колонка добавлена, а блок кнопок групп лежит теперь
поверх пустых колонок компенсатора, звезды, булавки и иконки: своей колонкой
он сдвигал подписи вправо от значений, потому что шире отступа строки
в дереве. Пометка булавки в заголовке прячется, когда её место занял блок
кнопок.

Переключение закрепления больше не пересобирает дерево целиком.
UpdatePinnedSection точечно обновляет узел «Закреплённые», как ApplyPinToggle
в WPF-версии: полная пересборка стоила бы пересоздания всех строк и теряла
выделение. Переключение избранного пересобирает список только при активном
временном фильтре, там база может выпасть из выборки.

Строка перекрашивается по уведомлению модели, а не пересборкой: звезда
и булавка следят за IsFavorite и IsPinned самой базы, поэтому обе копии
закреплённой базы меняют вид одновременно. Счётчик в заголовке группы стал
привязкой к TotalInfobaseCount, иначе после точечного обновления узла
показывал бы прежнее число.

Подписки содержимого строки (кисти темы у иконок и подписей, уведомления
базы) держит карточка строки и освобождает при отсоединении от дерева.
Раньше они накапливались на каждую пересборку и держали выброшенное
визуальное дерево.

Не принято: возражение, что клик по звезде очищает список без фильтра.
Ветка ApplyFilter для нефильтрованного режима заново наполняет GroupNodes
из AllGroupNodes, и прогон это подтвердил.
Узел «Закреплённые» правится всегда, а в видимый список попадает, только
когда он там уместен. Раньше при активном фильтре или отключённой
группировке обновление пропускалось целиком, а AllGroupNodes переживает
и то, и другое: база, закреплённая во время поиска, после возврата
к группам оказалась бы не в том узле.

Закрепление переупорядочивает и свою группу, а не только узел
«Закреплённые»: закреплённые идут первыми по GroupSortOrder, и без этого
строка стояла бы на прежнем месте до следующей полной пересборки.

Подписки содержимого строки хранятся способом их создать, а не готовым
объектом: карточка подписывается при присоединении к дереву и освобождает
при отсоединении, поэтому повторное присоединение восстанавливает и цвета,
и слежение за состоянием базы.

Проверено исполнением: переключение темы перекрашивает звёзды, булавки,
иконки и подписи строк; закрепление и открепление меняют состав узла,
счётчик и порядок внутри группы.

Не сделано: подсветка выбранной строки после пересоздания строк узла
не возвращается. Пробовал три способа (SelectedItem у дерева, IsSelected
у контейнера, отложенная переустановка) — ни один не удержался. Сама
выборка сохраняется: правая панель, строка состояния и команды работают
с прежней базой. Поведение то же, что было до этой ветки работ, когда
закрепление пересобирало дерево целиком.
У правого края каждой колонки заголовка появилась зона захвата: тонкая линия
по центру и широкая невидимая полоса вокруг, иначе в разделитель трудно
попасть. Приём взят из CacheCleanWindow, где он уже работал.

Ширина ведётся сразу в двух сетках: в заголовке и в каждой построенной
строке, без пересборки дерева, поэтому список при перетаскивании не мигает.
В настройки ширина пишется один раз по отпусканию кнопки и только через
вьюмодель (UpdateColumnWidth), а не собственным циклом чтения и записи
файла: файл настроек и без того пишут двое.

Колонка имени до первого перетаскивания остаётся звёздной и занимает
остаток ширины, после становится заданной, как в WPF-версии через
NameColumnWidthConverter. Минимум колонки 40, как у автора.

Имя базы переехало из горизонтальной панели прямо в колонку: в панели оно
получало бесконечную ширину и при узкой колонке налезало на соседние
значения вместо обрезки многоточием.

В оснастку (_port/harness/ui.py, вне git) добавлена команда drag: нажатие,
плавное движение, отпускание. Без неё перетаскивание нечем проверить.
sivatorov and others added 28 commits August 25, 2026 07:47
Приложение молча умирало примерно через секунду после старта с кодом
0xC0000409, как только в списке была хотя бы одна файловая или
клиент-серверная база с незаполненными ConfigurationName/ConfigurationVersion.
С пустым списком запускалось нормально.

Причина. Конструктор MainViewModel вызывает RefreshConfigurationInfoAsync,
тот для каждой такой базы идёт в ConfigurationInfoService.TryRead ->
OneCComConnector.ReadConfigurationInfo -> ConnectCore, где грузится
comcntr.dll и вызывается его метод Connect. Под CoreCLR этот вызов
обрывает процесс нативным fast-fail, без управляемого исключения:
обработчики в App.xaml.cs не срабатывают, в журнал ничего не попадает.
Под .NET Framework 4.8 тот же вызов возвращает обычное исключение, то
есть дело в рантайме, а не в данных: обрывается даже подключение к
заведомо рабочей базе.

Проверено сведением к 30 строкам: консольное net10.0-windows приложение,
делающее GetTypeFromProgID -> CreateInstance -> InvokeMember("Connect"),
падает с 0xC0000409 на CreateInstance OK. Позднее связывание через
reflection и через dynamic ведут себя одинаково, значит биндер ни при чём.

Решение. COM-вызов вынесен в дочерний экземпляр этого же приложения,
запускаемый с ключом --read-config-com. Гибель дочернего процесса больше
не трогает основной, тот лишь видит ненулевой код возврата. После первого
такого отказа COM помечается недоступным на всю сессию, иначе на списке
из десятков баз мы бы плодили десятки заведомо гибнущих процессов.
Чтение версии по файлу базы (1Cv8.1CD) продолжает работать как прежде.

Проверено на 64 реальных базах: до правки 0xC0000409 через 0.65 с,
после правки приложение живёт, в журнале одна поясняющая строка.

Linux-цель не затронута: ComReadHost.cs закрыт #if WINDOWS и добавлен в
список Compile Remove, а App.xaml.cs и OneCComConnector.cs и так
исключались из сборки под Linux.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…аймаут

Правки по трём встречным аудитам предыдущего коммита. Каждый круг нашёл то,
что упустили другие: кодировку не увидел codex, пароль не увидел fable.

Блокеры.

1. Таймаут не работал вовсе. ReadToEnd() возвращается только когда дочерний
   процесс умер, поэтому WaitForExit(timeoutMs) всегда был истинен, а ветка
   таймаута и TryKill недостижимы; в агенте вдобавок стоял thread.Join() без
   таймаута вместо прежнего Join(timeoutMs). Недоступный сервер подвешивал
   фоновое обновление навсегда. Теперь ответ читается отдельной задачей с
   ожиданием по времени, при просрочке агент убивается; stderr дренируется
   через BeginErrorReadLine, иначе переполнение буфера подвесило бы обоих.

2. Кодировка портила данные. Агент писал через Console.Out, а у процесса без
   консоли это кодовая страница ANSI; родитель декодировал UTF-8. Кириллица в
   имени конфигурации приезжала как U+FFFD и записывалась в файл пользователя.
   Теперь обе стороны работают явными потоками UTF-8 без BOM. Проверено
   стендом из двух WinExe: прежний способ даёт FD-FF FD-FF, новый возвращает
   строку побайтово целой.

3. Пароль базы уходил в командную строку дочернего процесса, видимую другим
   процессам пользователя и журналам аудита запуска, — против существующего в
   этом же файле MaskCredentials. Теперь строка подключения передаётся через
   stdin, в командной строке остаётся только ключ.

Остальное.

* Агент стал постоянным: один процесс на сессию обслуживает запросы построчно
  вместо запуска на каждую базу. На списке из 64 баз при исправном COM это
  убирает 64 старта приложения.
* Точка входа написана вручную (Program.Main, StartupObject), App.xaml переведён
  в Page. Режим агента перехватывается до создания App, поэтому агент больше не
  поднимает WPF и словари тем; прежняя проверка стояла в OnStartup, то есть уже
  после InitializeComponent.
* Восстановлен перебор KnownProgIds: на машине с платформой 8.2 чтение снова
  работает, было жёстко зашито V83.
* Протокол получил коды отказа вместо готовых текстов. Тексты подбираются в
  родителе через LocalizationManager, английская локаль больше не видит русских
  сообщений. Переиспользованы осиротевшие ключи Com.TimeoutReadFormat,
  Com.MetadataPropertyFailed, Com.MetadataReadFailed; добавлены четыре новых.
* Защёлка недоступности ставится только на настоящий крах агента и на
  отсутствие коннектора, но не на таймаут, и снимается явными действиями
  пользователя: обновлением сведений и регистрацией коннектора.
* Статический LastError убран, Read возвращает ComReadResult.
* IOneCComConnector.Connect помечен Obsolete: он делает COM-вызов в текущем
  процессе, и первый же вызывающий вернул бы 0xC0000409 в основной процесс.
* Агент останавливается в App.OnExit.

Проверено на 64 реальных базах: приложение живёт, процесс один, после закрытия
остатков нет, в журнале одна локализованная строка.

Не проверено исполнением: срабатывание таймаута — на этой машине COM гибнет
мгновенно и зависание не воспроизводится. Правка структурная.

Linux-цель не затронута: ComReadHost.cs закрыт #if WINDOWS и в Compile Remove,
MainViewModel.cs и App.xaml.cs на Linux не компилируются, StartupObject и
перевод App.xaml в Page стоят под условием сборки на Windows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правки по трём встречным аудитам второй редакции. Все три круга подтвердили,
что блокеры первой редакции закрыты по-настоящему; ниже — новое.

Утечка пароля. Текст ошибки, приходящий от 1С, шёл от агента в журнал и в
диалог пользователя без всякой проверки, а сообщения 1С умеют цитировать
строку подключения вместе с Pwd. Существующий MaskCredentials этот путь
обходил, причём после выноса COM в агент он вообще перестал быть достижим:
единственный его вызов остался в ConnectCore, до которого больше никто не
доходит. Теперь маскировка стоит на выходе DescribeFailure, а сама функция
покрывает обе формы записи пароля — в кавычках и без.

Закрытие приложения. Shutdown вызывается из App.OnExit в потоке интерфейса и
брал тот же монитор, что удерживается всё время запроса, — окно замирало до
конца текущего чтения, до тринадцати секунд. Теперь агент забирается через
Interlocked.Exchange и убивается без монитора. Измерено: закрытие 0.12 с.

Защёлка недоступности. Прежде она ставилась только на закрытый канал. Круг с
измерениями показал, что на машинах с включённым Windows Error Reporting
упавший процесс удерживается и EOF приходит через двадцать-тридцать секунд,
то есть позже нашего бюджета: крах выглядел таймаутом, защёлка не срабатывала
и агент перезапускался на каждую базу. Теперь истечение ожидания считается
отказом агента: живой агент отвечает сам не позже собственного Join. Заодно
защёлка перестала срабатывать с первого раза — разовое убийство агента
антивирусом или диспетчером больше не глушит COM на сессию, нужны два отказа
подряд. Отсутствие коннектора защёлкивается сразу: оно само не изменится.

Два выключателя. После переноса на апстрим рядом оказались его кэш реестра и
наша защёлка, а команды обновления снимали только вторую — после установки
платформы приложение всё равно молча отказывало по устаревшему кэшу. Заведён
ResetComVerdicts, снимающий оба; сбросы перенесены за подтверждение, иначе
отмена диалога всё равно снимала защёлку.

Перебор ProgID. Получение типа и создание объекта отделены от подключения:
их отказ означает непригодность этой версии платформы, и надо пробовать
следующую. Прежде исключение из CreateInstance прекращало перебор, и на
машине, где V83 остался в реестре после сноса, рабочий V82 не пробовался.

Протокол. Полезная нагрузка едет в Base64. Прежняя «очистка» табуляций и
переводов строк молча портила пароль, содержащий такие символы, и давала
необъяснимый отказ входа. Заодно снят целый класс рассинхронизации
запрос-ответ. Убран комментарий, утверждавший, будто строку уже очищает
BuildComConnectString: он неверен.

Цикл агента обёрнут: смерть родителя посреди запроса больше не даёт
необработанное исключение и лишний отчёт WER, агент выходит тихо.

Мелочи: подсказка об установке платформы вернулась в текст «коннектор не
зарегистрирован»; подробность отказа обмена больше не теряется; код возврата
снимается до убийства процесса, иначе настоящий 0xC0000409 подменялся кодом
принудительного завершения; ResetAvailability берёт монитор.

Проверено на 64 реальных базах: приложение живёт, процесс один, закрытие
0.12 с, остаточных процессов нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правки по третьему кругу встречных аудитов. Два из четырёх блокеров —
регрессии, внесённые предыдущим коммитом при починке второго круга.

Маскировка переписана разбором параметров вместо регулярных выражений.
Три круга нашли шесть разных способов её пробить: пароль с кавычкой отдавал
хвост, удвоенные кавычки отдавали пароль целиком, незакрытая кавычка не
маскировалась вовсе, границы имени не было (NotPwd тоже маскировался),
жадный класс уничтожал диагностику после пароля. Разбирать баланс кавычек
нельзя: BuildComConnectString кавычку внутри пароля не экранирует. Поэтому
значение считается идущим до ближайшего разделителя или конца строки —
это грамматика строки подключения, и она устойчива ко всем шести формам.
Проверено стендом: восемь векторов утечки закрыты, пять вещей, которые
трогать нельзя, сохранены, включая диагностику после разделителя.

Заморозка интерфейса вернулась через другую дверь. В прошлом коммите
Shutdown был выведен из-под монитора, а ResetAvailability остался под ним —
и тем же коммитом добавлены три вызова с потока интерфейса. Пока фоновое
дочитывание держит монитор, ручное обновление вешало окно до десяти секунд
ровно тогда, когда пользователь пытается COM починить. Монитор там не нужен:
оба поля атомарны, взаимная согласованность не требуется.

Код возврата подменялся ровно в том сценарии, ради которого всё строилось.
Собственный комментарий предупреждал снимать код до убийства, а код делал
наоборот, и Kill давал 0xFFFFFFFF вместо 0xC0000409 — единственной приметы
дефекта. Теперь умирающему процессу даётся короткий срок завершиться самому,
а код убитого не берётся вовсе. Проверено: в журнале снова 0xC0000409.

Отказ создания объекта больше не выдаётся за отсутствие регистрации. Прежде
любое исключение CreateInstance оставляло вердикт NotRegistered, который
защёлкивает COM с первого раза, и пользователь получал самопротиворечивый
текст: коннектор не найден, ProgID виден в реестре, установите платформу.
Заведён отдельный разряд InstanceFailed с причиной и без защёлки.

Остальное: признак завершения работы (закрытие приложения посреди чтения
больше не пишет ложный крах и не приближает защёлку, а EnsureAgent не
поднимает агента, который пережил бы родителя); защита от исключения
HasExited на освобождённом процессе; наблюдатель на брошенной задаче чтения;
счётчик отказов сбрасывает только успех, иначе на чередующемся списке двух
подряд не набирается никогда; повреждённый Base64 в успешном ответе больше
не выдаётся за успех с пустыми полями; освобождается RCW metadata; текст
про отключение COM согласован с обоими путями защёлки.

Проверено на 64 реальных базах: приложение живёт, процесс один, закрытие
0.08 с, остаточных процессов нет, в журнале настоящий код возврата.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правки по четвёртому кругу встречных аудитов. Все три круга сошлись на одном
блокере и на его корне.

Пароль больше не ищется по образцу. Круги подряд пробивали маскировку: сперва
кавычкой внутри пароля, затем удвоенными кавычками, теперь символами ; CR и LF.
Исчерпывающий перебор одного из кругов дал точный список ломающих символов —
ровно эти три. Корень не в правиле разбора, а в том, что BuildComConnectString
строит строку без экранирования: она принципиально неоднозначна, и любое
правило ломается на очередном спецсимволе. Поэтому пароль теперь передаётся
агенту отдельным полем и вырезается буквальным вхождением, без разбора
грамматики вообще. Делает это агент, до отправки ответа: иначе секрет успевал
попасть в канал обмена, где Base64 маскировкой не является. Образцовая
маскировка осталась вторым рубежом и заодно перестала уничтожать прозу —
незакавыченное значение теперь обрывается пробелом.
Проверено стендом: девять паролей, включая с ; CR LF кавычкой пробелом и
односимвольный, вырезаны полностью; пять видов диагностики сохранены.

Номер запроса в протоколе. Прежде лишняя строка в потоке агента навсегда
сдвигала соответствие запрос-ответ, и сведения о конфигурации начинали молча
приписываться чужим базам — без единого признака ошибки. Теперь ответ с чужим
номером означает рассинхронизацию: агент останавливается, запрос считается
неудачным.

Подавление WER в агенте. Падение агента здесь штатное событие, а не авария:
SetErrorMode убирает и системное окно, и дамп, в памяти которого лежит строка
подключения вместе с паролем. Раньше такой дамп писался на диск при каждом
отказе.

Остальное по кругам: отказ запуска агента теперь считается отказом и участвует
в защёлке, иначе на списке из десятков баз выходило по попытке и по строке в
журнале на каждую; поколение сброса — отказ запроса, начатого до нажатия
пользователем, больше не защёлкивает недоступность заново; грация на снятие
кода возврата не тратится там, где агент заведомо жив, и увеличена до трёх
секунд, потому что полсекунды не покрывали удержание процесса службой отчётов;
неизвестный код возврата получил отдельный текст вместо «(код возврата )»;
успешный ответ требует ровно четырёх полей, а декодер стал строгим — прежде
недопустимая последовательность превращалась в U+FFFD и сходила за имя
конфигурации; после защёлки NotRegistered агент останавливается, а не висит
до выхода; восстановлен переход к следующему ProgID после ошибки Connect,
как делал прежний ConnectCore.

Проверено на 64 реальных базах: приложение живёт, процесс один, закрытие
0.14 с, остаточных процессов нет, в журнале настоящий код возврата.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…дним монитором

Правки по пятому кругу встречных аудитов.

Маскировка перестала быть декоративной. Круги показали, что заявленная
двухступенчатость была одноступенчатой: вырезание по значению сравнивает
побайтово и промахивается при любом несовпадении, а вторая ступень содержала
ровно тот дефект, из-за которого забраковали четвёртую редакцию, — обрывалась
на точке с запятой и переводе строки внутри значения. Более того, на одном
классе паролей она была хуже апстримной регулярки. Теперь закавыченное значение
тянется до последней кавычки, за которой идёт разделитель или конец текста,
причём поиск идёт по всему остатку, а не в пределах строки: перевод строки
может стоять внутри самого значения. Незакавыченное обрывается пробелом, но
пробелы после знака равенства не пропускаются — иначе в прозе «Pwd= is
required» маска съедала следующее слово. Проверено стендом: перебор всего BMP
по одному символу внутри пароля не нашёл ни одного ломающего символа, тогда как
прежняя версия ломалась ровно на трёх.

Секрет передаётся только когда он действительно в строке подключения.
BuildComConnectString вставляет Pwd не всегда: нужен режим «по логину и паролю»
и заполненный пользователь, а по умолчанию режим другой. Прежде пароль
передавался всегда и вырезался из ответа, даже если его там быть не могло.

Имя и версия конфигурации больше не режутся вовсе. Они приходят из Metadata и
строку подключения содержать не могут, а вырезание по совпадению их портило:
числовой пароль превращал версию 1.2.1 в ***.2.***, пароль из пробела убирал
из имени все пробелы, и это записывалось в данные пользователя как успех.

Состояние защёлки сведено под отдельный короткий монитор. Прежде поколение,
счётчик и признак менялись раздельными атомарными записями, и между проверкой
поколения и записью признака оставалась щель, в которую проваливалась команда
пользователя. Монитор отдельный от того, что удерживается на время запроса,
и во время ввода-вывода не держится, поэтому интерфейс не задевает.

Перебор ProgID после ошибки Connect снова прекращается. Восстановив его в
прошлой редакции, я получил худшее: на машине с двумя коннекторами один
неверный пароль давал две-три неудачные аутентификации, втрое быстрее набивая
счётчик блокировки учётной записи, а итоговый диагноз доставался от последнего
ProgID. Случай непригодного коннектора уже покрыт отдельной веткой, которая
срабатывает раньше.

Остальное: закрытие приложения больше не считается отказом COM и не пишет в
журнал ложную ошибку запуска; рассинхронизация протокола и сбой канала теперь
учитываются в защёлке, иначе систематический мусор в потоке агента давал
бесконечную череду перезапусков; агенту гасятся переменные среды, которыми
рантайм включает запись дампа; текст исключения во внешнем обработчике агента
тоже проходит вырезание; убран BOM, случайно добавленный в четыре файла.

Про дампы честно: SetErrorMode измеримо помогает — на этой машине LocalDumps
включён глобально, и без флагов крах оставлял дамп на полтора мегабайта, с
флагами дампа нет. Но для настоящего fast-fail это не доказано, поэтому канал
следует считать суженным, а не закрытым.

Проверено на 64 реальных базах: приложение живёт, процесс один, закрытие
0.09-0.11 с, остаточных процессов нет, дампов не появляется.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правки по шестому кругу встречных аудитов. Круги подтвердили, что утечки
пароля нет: исчерпывающий перебор паролей над алфавитом из кавычек, точек
с запятой, пробелов и переводов строк (1.8 млн вариантов) и два фаззинга по
миллиону дали ноль утечек. Но выяснилось, чем за это заплачено.

Широкое правило маскировки уничтожало диагностику. Тянуть значение до
последней кавычки во всём остатке текста безопасно, но в многострочном
сообщении 1С это съедает всё до последнего закавыченного токена, а 1С
цитирует в кавычках сплошь и рядом. Замер одного из кругов: текст теряется
примерно в 7-8 процентах входов. На живом примере из сообщения исчезали
строки «сервер не отвечает» и «проверьте службу» — то есть ровно причина
сбоя, и в интерфейсе, и в журнале.

Вырезание по значению било по чужим полям. Секрет передавался, если пароль
встречается в строке подключения как подстрока, а вырезался из всего текста.
Пароль, совпавший с портом сервера или фрагментом имени базы, вычищал их из
диагностики. Односимвольный пароль портил её до неузнаваемости.

Оба дефекта — следствие одной попытки: разобрать принципиально неоднозначный
текст. Строка подключения не экранирована, а сообщение может прийти усечённым
посреди значения, поэтому любое правило либо выпускает хвост пароля, либо
съедает лишнее. Шестая попытка подряд.

Поэтому свободный текст ошибки 1С больше не пересылается вовсе, когда у базы
есть пароль: вместо него возвращается код ошибки COM. Резать нечего, и утечки
нет по построению. Для баз без пароля, а это большинство, текст сохраняется
целиком. Сам пароль агенту больше не передаётся — довольно признака, что он
есть, поэтому секрет вообще перестал покидать родительский процесс отдельным
полем, а вычищение подстроки из чужих полей исчезло вместе с ним.

Маскировка вернулась к узкому правилу: первая кавычка, за которой разделитель
или конец строки, без выхода за границу строки. Она стала страховкой для
текста, в котором секрета нет по построению, и диагностику больше не портит.

Перебор ProgID после ошибки Connect продолжается только без учётных данных.
Так разрешается расхождение между кругами: с паролем повтор означал бы две-три
неудачные аутентификации за одно чтение и втрое более быстрое набивание
счётчика блокировки учётной записи, а без пароля этой цены нет, и перебор
нужен — база 8.2 через V83 даёт управляемое исключение о несовместимости,
коннектор при этом создаётся нормально, поэтому ветка InstanceFailed такой
случай не ловит, а рабочий V82 ловит.

Сбои канала получили отдельный, более мягкий порог защёлки: разовые обрывы
лечатся перезапуском агента, глушить из-за них COM на сессию незачем.
Переменные среды, включающие дамп, теперь удаляются, а не гасятся пустой
строкой.

Проверено на 64 реальных базах: приложение живёт, процесс один, закрытие
0.1 с, остаточных процессов нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правки по седьмому кругу встречных аудитов.

Блокер: признак наличия пароля вычислялся сравнением значения из настроек со
строкой подключения, и это ломало чтение баз. Клиент-серверная база с
аутентификацией ОС учётных данных в строку не кладёт, но если в карточке с
прошлых времён остался пароль, совпавший с подстрокой имени базы или сервера,
признак выставлялся, перебор ProgID обрывался на первой ошибке, и база 8.2,
которая читалась бы через V82, переставала читаться вовсе. Тот же признак
разъезжался со строкой при подмене Connection фоновой синхронизацией.

Схема заменена целиком, по предложению одного из кругов — оно лучше прежнего.
Агент отдаёт и текст ошибки, и опознавательный код, а решение принимает
родитель: только у него есть пароль. Если текст содержит пароль или его начало
достаточной длины — страховка на случай сообщения, усечённого посреди
значения, — текст отбрасывается целиком и остаётся код. Решение «всё или
ничего», поэтому чужие поля пострадать не могут, а для отказов, которые строку
подключения не цитируют (неверный пароль, база занята, сервер недоступен),
текст доходит до пользователя как есть. Прежняя схема глушила текст при любом
пароле, и для таких баз все причины отказа становились неразличимы.

Агенту теперь сообщается не наличие пароля, а наличие учётных данных, и только
чтобы не перебирать ProgID с повторными попытками входа: счётчик блокировки в
1С считает неудачные входы по пользователю, и вход с пустым паролем его тоже
набивает. Признак считается по самой собранной строке, а не по полям модели.

Пользователю, у которого текст скрыт, теперь объясняют почему: отдельный ключ
локализации вместо голого шестнадцатеричного кода. Опознавательные сведения
дополнены именем типа исключения, разворот идёт до самого глубокого вложенного
(прежде при двойной обёртке терялся настоящий код), нулевой код не показывается.

Счётчики отказов разведены на два. Общий с двумя порогами давал результат,
зависящий от порядка: один разовый сбой канала поднимал счётчик, и следующий
настоящий крах защёлкивал COM по жёсткому порогу. Таймауты теперь тоже
учитываются — прежде список недоступных клиент-серверных баз тратил полный
таймаут и перезапуск процесса на каждую, и защёлка это не останавливала никогда.

Остальное: ветка «Connect вернул null» больше не перебирает ProgID при наличии
учётных данных — прежде защита стояла только в ветке исключения; итоговый
диагноз берётся от первого ProgID, а не от последнего, иначе про базу 8.3
пользователь читал сообщение от V81; штатный выход агента с нулевым кодом
больше не выдаётся за аварийный; маскировка считает закрывающей кавычку перед
любым несловообразующим символом, а не только перед точкой с запятой, иначе
терялся остаток строки после запятой или пробела; [Obsolete] продублирован на
реализации Connect — на интерфейсе он не предупреждает того, кто возьмёт
конкретный тип.

Проверено на 64 реальных базах: приложение живёт, процесс один, закрытие
0.1 с, остаточных процессов нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правки по восьмому кругу встречных аудитов. Все три круга пробили решающее
правило прошлой редакции, и каждый — своим способом.

Правило проверяло, содержит ли текст ошибки начало пароля. Это закрывало
только усечение справа. Исчерпывающий перебор одного из кругов: из 105
подстрок четырнадцатисимвольного пароля правило пропускало наружу 94, самая
длинная — 13 символов подряд. Достаточно, чтобы 1С процитировала значение
со второго символа. Практический путь туда простой: пароль с кавычкой в
начале делает строку неоднозначной, 1С обрывает разбор на кавычке и цитирует
хвост как неопознанный параметр.

Обратная сторона того же правила — ложное подавление. Пароль из пробелов в
строку подключения не попадает вовсе, но глушил почти любой текст, причём
пользователю сообщалось «текст скрыт: он содержит пароль базы», что неправда
дважды. Для односимвольных паролей подавлялось до 83 процентов сообщений.

Теперь решение принимается по строке подключения, а не по содержимому текста:
если в строке нет параметра Pwd, то 1С пароля не видела и процитировать не
могла — текст безопасен по построению. Если есть — текст не выпускается вовсе.
Никаких порогов, окон и догадок о том, что именно 1С процитировала. Ложное
подавление исчезает вместе с догадкой: пароль из пробелов в строку не попадает,
Pwd отсутствует, текст показывается.

Отдельно исправлена моя же ошибка в обратную сторону. Ограничение перебора
ProgID я придумал сам, у апстрима его не было. В седьмой редакции оно ломало
чтение баз 8.2 с паролем, в восьмой я «починил» его признаком наличия учётных
данных — и сделал условие шире, потому что Pwd вставляется только вместе с Usr.
Круг показал таблицей, что регрессия не устранена, а распространена ещё и на
логин с пустым паролем. Поведение апстрима восстановлено, признак убран
совсем, вместе с ним из протокола ушло четвёртое поле запроса. Соображение
про счётчик блокировки учётной записи оставляю для описания PR как возможное
продолжение, а не как свершившееся решение: эта правка про то, чтобы
приложение не падало, и менять чужую семантику походя она не должна.

Остальное: диагноз выбирается по осмысленности разряда, а не по порядку —
прежде битая регистрация V83 заслоняла настоящую причину от работоспособного
V82, и пользователь читал «не удалось создать экземпляр» вместо «неправильное
имя или пароль»; текст ошибки берётся с того же уровня вложенности, что и код;
разряды Metadata больше не тащат чужую подробность от предыдущего ProgID;
повреждённый кадр ответа считается рассинхронизацией, а не молчаливым отказом,
и поля ошибки разбираются так же строго, как поля успеха; добавлены недостающие
переменные среды, включающие дамп; ConnectCore больше не пишет строку
подключения в журнал — маскировка на ней принципиально ненадёжна, а имени
ProgID для разбора достаточно.

Проверено на 64 реальных базах: приложение живёт, процесс один, закрытие
0.1 с, остаточных процессов нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…рядов

Три круга встречного аудита сошлись на трёх дефектах.

Признак наличия пароля отдаёт BuildComConnectString через out-параметр —
он единственный знает наверняка, положил ли Pwd в строку. Обратный разбор
готовой строки удалён: он зависел от списка секретных имён, который надо
было держать синхронным с SecretParameters, и ложно срабатывал на Pwd=
внутри пути к базе.

Соответствие разрядов отказа и токенов протокола сведено в одну таблицу.
Раньше отправка и разбор держали независимые списки, и неопознанный токен
отображался в Transport с сохранением текста — свободный текст 1С мог
доехать до журнала и диалога мимо решения о пароле. Теперь неизвестный
токен считается рассинхронизацией.

Promote при равном весе принимает более поздний диагноз. Это семантика
исходного ConnectCore, который переписывал LastError на каждой итерации
перебора: для базы 8.2 иначе побеждало «несовместимая версия» от V83,
а настоящая причина, которую называет только V82, терялась.

Прочее: повреждённая нагрузка успеха тоже считается рассинхронизацией;
запрос разбирается строго — ровно три поля и TryDecode, повреждённый
запрос отвечает отдельным токеном без текста вместо DBERR с английской
внутренней фразой; закрыта гонка, в которой обнуление каналов при выходе
выглядело отказом запуска агента; ConnectCore больше не передаёт
исключение логгеру, который дописывал Message без маскировки.

Формулировка «текст содержит пароль» смягчена до «может содержать»:
проверялась строка подключения, а не текст. Комментарии со ссылками на
внутреннюю историю ветки убраны.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…adRequest

Добавлен V85.COMConnector первым в перебор ProgID. У платформы 8.5 свой
ProgID и свой CLSID, прежний список её не находил вовсе — на машине, где
установлена только 8.5, сведения о конфигурации не читались бы совсем.
Порядок важен вдвойне: отдельные сборки коннектора 8.3.27 (проверены
8.3.27.2130 и 8.3.27.2214) обрывают процесс нативным fast-fail на вызове
Connect, поэтому исправный V85 должен опрашиваться раньше них. Замерено,
что коннектор 8.5 читает базы 8.3 с тем же результатом, что исправная
сборка 8.3.

Ошибки базы больше не вытесняют друг друга, а копятся все и отдаются
списком с пометкой ProgID. Выбор между двумя одинаково осмысленными
диагнозами проигрывает в любую сторону: оставишь ранний — для базы 8.2
победит «несовместимая версия» вместо настоящей причины от V82; оставишь
поздний — то же самое зеркально произойдёт с базой 8.3.

Отказ разбора запроса получил собственный разряд BadRequest вместо
общего с Transport. Transport печатает подробность пользователю дословно,
поэтому он не должен приезжать по протоколу: иначе подделанный кадр вынес
бы произвольный текст мимо решения о показе пароля. Разбор дополнительно
требует, чтобы у BADREQ не было ни кода, ни текста, и такой отказ теперь
учитывается счётчиком и перезапускает агента.

Прочее: подавлено предупреждение IL3000, появлявшееся при публикации
однофайлового образа; на выходе из приложения отказы больше не пишутся
в журнал — раньше закрытие окна во время фонового дочитывания добавляло
по строке на каждую оставшуюся базу; маскировка больше не трогает
незакавыченную форму, из-за которой у базы без пароля терялся хвост пути
вида «D:\Базы\Pwd=1\buh» вместе с диагностикой; ConnectCore не полагается
на маскировку и выпускает только тип исключения и HRESULT.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ации

Значения в строке подключения экранируются: кавычка внутри значения
удваивается, как того требует грамматика строки подключения 1С. Без этого
значение могло закрыть себя и дописать любой другой параметр — путь вида
C:\Base";Pwd=…;x=" добавлял в строку пароль, о котором вызывающий не знал.
Ломались сразу две вещи: подключение к базе, в пароле которой есть кавычка,
и признак наличия пароля, на котором держится решение о показе текста
ошибки 1С — признак оставался ложным при реально добавленном Pwd.

Проверено на живом коннекторе: без экранирования 1С разбирает внедрённый
Pwd как отдельный параметр и отвечает «Информационная база не обнаружена»,
с экранированием принимает всю строку за одно значение пути.

Проверка регистрации и диагностика реестра перестали смотреть только на
V83. Платформа 8.5 регистрирует собственный ProgID с собственным CLSID,
поэтому удачная регистрация объявлялась неудачной, кэш недоступности не
сбрасывался, а разрядность определялась по отсутствующему ключу.

Отложенное из предыдущего круга: окружение агента чистится ещё и от
профилировщика и стартовых перехватчиков; поля каналов объявлены volatile;
внутренние имена шагов убраны из диалога пользователя и оставлены в
журнале; формулировка про подавление дампов смягчена до «по возможности» —
настоящий fast-fail уходит в отчёт мимо обработчиков, и политику Windows
Error Reporting из падающего процесса не отключить.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Перебор ProgID идёт внутри агента, и коннектор, опрошенный раньше, успевает
назвать настоящую причину отказа — а следующий обрывает процесс нативным
fast-fail и уносит её с собой. Пользователь в этом случае получал голое
«вспомогательный процесс аварийно завершился» вместо, например, «неверные
имя или пароль», хотя причина уже была известна.

Агент теперь отправляет каждый полученный диагноз сразу, отдельным
промежуточным кадром PARTIAL, не дожидаясь конца перебора. Родитель
придерживает последний такой кадр и предъявляет его, если агент погиб или
замолчал. Крах при этом по-прежнему учитывается счётчиком и защёлкивает
COM — теряется только бесполезное сообщение, а не сам факт отказа.

Промежуточный кадр отличается от окончательного одной меткой и разбирается
тем же строгим декодером; запрос он не завершает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…истрации

Промежуточные кадры и окончательный ответ писались двумя потоками в один
поток вывода без синхронизации. По таймауту главный поток перестаёт ждать
COM-поток, но убить его нечем — тот остался внутри COM-вызова и мог
дописать промежуточный кадр уже после окончательного. Кадры перемешались
бы, и родитель принял бы ответ за повреждённый: точный диагноз потерян,
агент перезапущен. Запись сериализована, а после окончательного кадра
промежуточные запрещаются.

Успех регистрации коннектора снова определяется результатом самой команды.
Расширение проверки с V83 на весь список ProgID сделало возможным ложный
успех: при уже установленной платформе 8.5 регистрация выбранной 8.3 могла
провалиться целиком, а пользователю сообщили бы об успехе — и заодно сняли
бы вердикт о недоступности COM. Когда регистрировать нечего, отчёт
по-прежнему опирается на видимость ProgID: это состояние системы, а не
результат операции.

Тексты диагностики реестра перестали называть конкретный V83: проверка
смотрит весь список, и при установленной только 8.5 сообщение называло
не тот ProgID, который на самом деле найден.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Три круга аудита подряд находили не старые дефекты, а внесённые самими
исправлениями: ложный успех регистрации, гонку записи в поток агента,
потерю диагноза. У изменения не было страховки, и каждая правка
проверялась только следующим кругом.

Добавлен тестовый проект: 35 тестов на чистые функции, в которых эти
регрессии и возникали. Разбор кадров протокола (успех, ошибка,
промежуточный кадр, чужой seq, повреждённая нагрузка, неизвестный токен,
подделанный BADREQ с текстом), взаимная обратимость разрядов и токенов,
ранжирование диагнозов, сборка строки подключения с экранированием и
признаком пароля, маскировка учётных данных.

Отдельно закреплено то, на чём держится правило о пароле: значение поля
не может закрыть себя и дописать чужой параметр, а Transport не приезжает
по протоколу ни одним токеном.

Проверяемые функции открыты сборке тестов через InternalsVisibleTo;
публичными они не становятся — за пределами сборки смысла не имеют.

Целевая платформа тестов следует за основным проектом, а сами тесты
закрыты #if WINDOWS: на Linux проверяемый код исключён из компиляции,
поэтому проект там собирается пустым и решение не ломается.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Правило «свободный текст 1С проходит через решение о показе пароля»
держалось наполовину. Строгость была введена только для отказа разбора
запроса, а разряды TIMEOUT, NOINST и NOCONN принимали произвольный текст,
который затем подставлялся в сообщение пользователю мимо этого решения.
Утечки не возникало лишь потому, что агент таких строк туда не клал, —
то есть защита держалась на дисциплине отправителя, а не на разборе.

Теперь форма подробности проверяется для каждого разряда: свободный текст
разрешён единственному — ошибке самой базы; таймаут несёт только число
миллисекунд; отказ создания и отсутствие соединения — только имя ProgID
из известного списка; остальным подробность запрещена. Подробность
InstanceFailed сведена к ProgID: сообщение сторонней библиотеки формы
не имеет и опознавательной ценности к коду не добавляет.

Придержанный диагноз возвращён на две ветки, где он молча терялся: при
повреждённом окончательном кадре (агент мог погибнуть посреди записи)
и при таймауте (ранний коннектор уже назвал причину, следующий завис
на недоступном сервере). Отказ в обеих ветках по-прежнему учитывается.

Разряд BadRequest получил собственный текст: он заводился ради того,
чтобы отличаться от молчания агента, но выводился именно этой фразой.

Диагностика разрядности сравнивает один и тот же ProgID в обеих ветвях
реестра. После расширения проверки на весь список она отвечала бы
«зарегистрирован и там, и там» при 64-разрядной 8.5 и 32-разрядной 8.3,
то есть врала бы ровно в том случае, который должна различать.

Признак завершения записи выставляется через finally; кадр PARTIAL
собирается escape-последовательностями, а не настоящими символами
табуляции в литерале, которые молча пережило бы любое переформатирование.

Тесты: 53 вместо 35. Добавлено само решение о показе пароля, склейка
диагнозов и проверка формы подробности по каждому разряду.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Приложение молча завершается на старте с кодом 0xC0000409
Целевой фреймворк выбирается в csproj по ОС сборки, поэтому одной машиной
обе цели не проверить: на Windows не компилируется ни один файл *.Avalonia.cs,
на Linux не компилируется WPF-часть. Задачи по одной на систему.

Каждая вызывает тот же скрипт, которым проект собирается вручную, чтобы CI
и локальная сборка не разъезжались.

build.sh получил бит исполнения: README документирует запуск как ./build.sh,
но на свежем клоне это давало Permission denied. У соседних скриптов
упаковки бит стоит.
Две ошибки пришли из апстрима и ломали цель net10.0 целиком, 42 ошибки
компилятора. Windows-цель их не видит: оба файла в неё не входят.

* SettingsWindow.Avalonia.cs: record ColumnOrderItem объявлен внутри тела
  метода, а C# типы внутри методов не допускает. Вынесен вложенным типом
  класса, тело не менялось.
* MainWindow.Avalonia.cs: имя Path неоднозначно между Avalonia.Controls.Shapes
  и System.IO. Уточнено полным именем в одной строке.

После правок: 0 ошибок, 32 предупреждения, как и до обновления апстрима.
@ksv47

ksv47 commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

Прогон выполнен, роль исчерпана.

@ksv47 ksv47 closed this Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants