The parser will need to move between proxies, so this adds the module it will sit on: pluggable sources, a pool that hands proxies out and learns from the outcome, three rotation strategies, and a page to drive it. Sources are IProxySource implementations. The public proxifly/free-proxy-list feed is fetched as the combined all/data.json through jsDelivr and filtered locally — one conditional request beats four per-protocol ones that can disagree mid-publish — and cached for the five minutes upstream takes to regenerate. A feed that is down keeps serving its last payload rather than emptying the pool. The user's own list lives in proxies.custom.json beside the settings, takes a pasted blob, and names the lines it could not parse instead of quietly dropping them. Both knobs the pool exposes are settings, as asked: rotation is Sticky (default, the only one that keeps site sessions coherent), RoundRobin or WeightedRandom; liveness is either a parallel sweep of the whole pool or a probe at hand-out time. Free lists are a few percent alive, so skipping verification entirely means mostly waiting on timeouts. Two invariants worth keeping, both of which cost a bug to find: Availability is decided by the quarantine, never by Health. Excluding everything that has ever failed made the quarantine window dead code and discarded proxies permanently on their first hiccup, which is exactly wrong for addresses that flap constantly. Health only orders the candidates now. A probe verdict does not touch the success/failure counters. Those are about real requests, and letting a sweep over a few thousand proxies rewrite them would drown the evidence weighted selection reads. SOCKS needs no extra package — .NET resolves socks4/socks4a/socks5 in WebProxy — but a proxifly record with "protocol": "https" is still an HTTP proxy reached over http:// with CONNECT, not an https:// scheme. 115 new tests. Also fixes a pre-existing flake: a command gated on another command's IsExecuting cannot be driven straight after its Execute() completes, because IsExecuting is published on the output scheduler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
192 lines
11 KiB
Markdown
192 lines
11 KiB
Markdown
# AvParser
|
||
|
||
Каркас desktop-приложения на **Avalonia 12** с ReactiveUI-MVVM, адаптивным layout поверх
|
||
Semi.Avalonia, единым DI-контейнером и тремя уровнями тестов.
|
||
|
||
Доменная часть пока намеренно абстрактная: ядро — это pluggable-контракт
|
||
`IParser<TInput, TOutput>` и два демо-парсера, чтобы каркас был запускаемым и проверяемым
|
||
end-to-end до появления настоящей логики.
|
||
|
||
---
|
||
|
||
## Быстрый старт
|
||
|
||
Нужен .NET SDK **10.0.100** (закреплён в `global.json`).
|
||
|
||
Полный локальный гейт — восстановление, проверка форматирования, сборка, тесты:
|
||
|
||
```bash
|
||
./build.ps1
|
||
```
|
||
|
||
Запуск приложения (по умолчанию Debug):
|
||
|
||
```bash
|
||
./run.ps1
|
||
```
|
||
|
||
На Linux и macOS — `./build.sh` и `./run.sh`, аргументы те же.
|
||
|
||
Полезные флаги:
|
||
|
||
```bash
|
||
./build.ps1 -Fix -Configuration Debug
|
||
```
|
||
|
||
`-Fix` переформатирует код вместо того, чтобы падать на непрошедшей проверке; `-SkipTests`
|
||
собирает без прогона тестов. То же в bash: `--fix`, `-c Debug`, `--skip-tests`.
|
||
|
||
Если нужны отдельные шаги, скрипты ничего не прячут:
|
||
|
||
```bash
|
||
dotnet build AvParser.slnx -c Release
|
||
```
|
||
```bash
|
||
dotnet test AvParser.slnx -c Release
|
||
```
|
||
```bash
|
||
dotnet csharpier check .
|
||
```
|
||
|
||
---
|
||
|
||
## Структура
|
||
|
||
```
|
||
src/
|
||
AvParser.Core домен: IParser, IParserCatalog, модели, демо-парсеры
|
||
ноль зависимостей кроме DI.Abstractions — ни Avalonia, ни IO
|
||
AvParser.Infrastructure AppPaths, JSON-настройки с debounce, Serilog
|
||
AvParser.UI Avalonia class library: App-независимые View, ViewModel,
|
||
ResponsiveLayout, дизайн-токены, навигация
|
||
AvParser.Desktop WinExe-хост: Program.cs, App.axaml, composition root
|
||
tests/
|
||
AvParser.Core.Tests парсеры, реестр, отмена, прогресс, пул прокси и стратегии
|
||
AvParser.Infrastructure.Tests разбор фида прокси, локальный список, маппинг на WebProxy
|
||
AvParser.UI.Tests ViewModel'и без Avalonia
|
||
AvParser.UI.HeadlessTests реальное дерево контролов через [AvaloniaFact]
|
||
```
|
||
|
||
Ссылки идут строго в одну сторону: `Core ← Infrastructure ← UI ← Desktop`.
|
||
`UI` — библиотека, а не exe, именно чтобы headless-тесты собирали настоящие View, не подтягивая
|
||
`Program.cs`, Serilog и контейнер.
|
||
|
||
---
|
||
|
||
## Адаптивный layout
|
||
|
||
В Avalonia нет `AdaptiveTrigger`, `VisualStateManager` и media-queries. Есть три примитива:
|
||
наблюдаемый `Visual.Bounds`, псевдоклассы и `SplitView`. `ResponsiveLayout` связывает первое со
|
||
вторым — получается CSS-подобная реакция на ширину.
|
||
|
||
| Брейкпоинт | Ширина окна | Навигация |
|
||
|---|---|---|
|
||
| Compact | < 720 px | выезжающий drawer поверх контента |
|
||
| Medium | 720 – 1100 px | рельс из одних иконок (56 px) |
|
||
| Expanded | ≥ 1100 px | полный сайдбар с подписями (248 px) |
|
||
|
||
Переключение с гистерезисом в 24 px: без неё перетаскивание края окна заставляет layout
|
||
мигать между двумя состояниями на каждом пикселе дрожания.
|
||
|
||
Разделение обязанностей, которое важно не сломать:
|
||
|
||
- `SplitView.DisplayMode` и `IsPaneOpen` **биндятся во ViewModel**. Style-сеттер навсегда
|
||
проигрывает локальному значению, поэтому первый же клик по гамбургеру заморозил бы любой
|
||
стиль, который тоже пишет в эти свойства.
|
||
- Всё чисто визуальное — ширины панели, видимость подписей, паддинги — живёт в
|
||
`Styles/Shell.axaml`.
|
||
|
||
Селекторы там написаны как `:is(UserControl).shell`, а не `UserControl.shell`: селектор типа в
|
||
Avalonia матчит **точный** тип, а `ShellView` наследуется от `ReactiveUserControl<T>` — обычная
|
||
форма молча не сматчилась бы ни с чем. На это есть тест
|
||
(`ShellViewTests.The_shell_stylesheet_is_actually_applied`).
|
||
|
||
---
|
||
|
||
## Прокси
|
||
|
||
Пул прокси с ротацией — `AvParser.Core/Proxies`, источники и сетевая часть —
|
||
`AvParser.Infrastructure/Proxies`, управление — страница **Proxies**.
|
||
|
||
Источники:
|
||
|
||
- **[proxifly/free-proxy-list](https://github.com/proxifly/free-proxy-list)** — публичный список,
|
||
обновляется каждые 5 минут. Тянем сводный `all/data.json` через jsDelivr и фильтруем локально:
|
||
один условный запрос за весь список надёжнее четырёх по протоколам, которые могут разъехаться
|
||
между собой в момент публикации. Ответ кэшируется на 5 минут, недоступность фида не роняет
|
||
приложение — остаётся прошлый список.
|
||
- **Свой список** — `proxies.custom.json` рядом с настройками. Вставляется пачкой, по одной на
|
||
строку; поддерживаются `scheme://host:port`, голый `host:port` и `user:pass@`. Непонятые строки
|
||
не проглатываются молча, а называются в статусе.
|
||
|
||
Ротация выбирается в настройках:
|
||
|
||
| Стратегия | Поведение | Когда |
|
||
|---|---|---|
|
||
| Sticky | одна прокси, смена только по отказу | по умолчанию: не рвёт сессии и cookie |
|
||
| RoundRobin | новая на каждый запрос | размазывает рейт-лимиты, но ломает сессии |
|
||
| WeightedRandom | случайно, с весом по score и доле успехов | при сильном разбросе качества |
|
||
|
||
Проверка живости — тоже настройка, два режима: **Pool** прогоняет весь список параллельно один
|
||
раз, **Lazy** проверяет прокси в момент выдачи и перескакивает на следующую. У бесплатных списков
|
||
рабочих обычно единицы процентов, поэтому без проверки парсер будет в основном ждать таймауты.
|
||
|
||
Упавшая прокси уходит в карантин с экспоненциальным окном (30 с → 15 мин), но **не** удаляется
|
||
навсегда: бесплатные прокси постоянно мигают, и жёсткий бан терял бы их безвозвратно.
|
||
|
||
Использование из кода:
|
||
|
||
```csharp
|
||
var (http, lease) = await clientFactory.CreateFromPoolAsync();
|
||
using (http)
|
||
using (lease)
|
||
{
|
||
try { var response = await http.GetAsync(url); lease?.ReportSuccess(); }
|
||
catch { lease?.ReportFailure("request failed"); throw; }
|
||
}
|
||
```
|
||
|
||
Отчёт об исходе — не формальность: без него пул ничего не узнаёт о том, какие прокси работают.
|
||
Освобождение лизы без вердикта нейтрально — отменённая операция не вина прокси.
|
||
|
||
SOCKS работает штатно: .NET понимает схемы `socks4/socks4a/socks5` в `WebProxy`. Учтите, что
|
||
proxifly-запись с `"protocol": "https"` — это всё равно HTTP-прокси с CONNECT, а не схема `https://`.
|
||
|
||
## Дизайн-токены
|
||
|
||
Все цвета, отступы, радиусы и типографика — в `Styles/Tokens.axaml`, с отдельными словарями
|
||
для Light и Dark. В остальном XAML нет ни одного литерального цвета и ни одного «магического»
|
||
отступа, так что перекрасить тему или уплотнить интерфейс — это правка одного файла.
|
||
|
||
Semi.Avalonia даёт темы контролов; токены — это семантический слой приложения поверх них.
|
||
Кнопки `.primary` / `.destructive` описаны своими стилями, а не классами Semi, чтобы акцентный
|
||
цвет не разъезжался между двумя палитрами.
|
||
|
||
---
|
||
|
||
## Стек
|
||
|
||
| Пакет | Версия | Заметка |
|
||
|---|---|---|
|
||
| Avalonia | 12.1.1 | compiled bindings по умолчанию → `x:DataType` обязателен |
|
||
| ReactiveUI.Avalonia | 12.1.1 | `Avalonia.ReactiveUI` — deprecated, это его преемник |
|
||
| ReactiveUI | 24.1.0 | дистрибутив Primitives: `RxVoid` вместо `Unit`, `ISequencer` вместо `IScheduler` |
|
||
| Semi.Avalonia | 12.1.0.1 | темы контролов |
|
||
| xUnit | v3 (3.2.2) | `Avalonia.Headless.XUnit` 12.x требует именно v3 |
|
||
|
||
---
|
||
|
||
## Что проверить руками
|
||
|
||
1. Потянуть окно по ширине — сайдбар проходит путь
|
||
`полный → только иконки → выезжающий drawer`, без мигания на границах.
|
||
2. Переключить тему кнопкой в заголовке и в Settings; перезапустить — выбор сохранился.
|
||
3. На странице Parse нажать **50k rows**, затем **Parse** — виден прогресс; **Cancel**
|
||
останавливает на середине и пишет, сколько успело разобраться.
|
||
В Avalonia 12 инспектора «из коробки» больше нет: `Avalonia.Diagnostics` остановился на 11.3.x,
|
||
а DevTools вынесли в отдельный инструмент со своей установкой
|
||
(`AvaloniaUI.DiagnosticsSupport` + `.WithDeveloperTools()`). Поэтому `F12` здесь ничего не
|
||
открывает — зависимость намеренно не добавлена.
|
||
|
||
Настройки и логи лежат в `%APPDATA%/AvParser` (Windows) или `~/.config/AvParser` (Linux/macOS).
|