Rozdzielenie UI i logiki biznesowej w aplikacjach desktopowych .NET przy użyciu gRPC i IPC
W aplikacjach desktopowych na WPF lub Avalonia UI logika biznesowa jest ściśle powiązana z wątkiem UI. Zawieszenie renderowania lub awaria XAML prowadzi do zatrzymania całego systemu: timery się resetują, zapytania API przerywają, stan jest tracony. Jest to krytyczne dla zadań w tle, takich jak zarządzanie inteligentnym domem, blokowanie procesów czy odtwarzacz multimedialny.
Rozwiązanie — rozdzielenie na headless backend (Worker Service) i thin client (UI). Komunikują się one przez gRPC z server-side streaming, zapewniając niezależność procesów.
Architektura: dwa procesy, bezpieczna komunikacja
Backend (Axorith.Host) — .NET Worker Service, kompilowany jako WinExe. Odpowiada za:
- Przechowywanie stanu sesji.
- Timery i orkiestrację modułów.
- Integrację z Home Assistant, Spotify, monitorowanie procesów przez ETW.
- Zarządzanie przeglądarką przez Named Pipes.
Zużycie w stanie spoczynku: 20–30 MB RAM. Działa autonomicznie, niezależnie od UI.
Frontend (Axorith.Client) na Avalonia UI z ReactiveUI. Tylko wizualizacja i wprowadzanie danych. UI można zakończyć w Menedżerze zadań — backend będzie kontynuował pracę.
Zależności warstw:
- Client i Host odwołują się do Axorith.Contracts (gRPC proto).
- Wspólny Core dla silnika sesyjnego.
- Shared dla narzędzi i kodu zależnego od platformy.
- Moduły (wtyczki) zależą od Sdk.
gRPC z server-side streaming dla reaktywności
gRPC wybrano ze względu na kontrakty protobuf i streaming. Proto dla aktualizacji ustawień:
syntax = "proto3";
package axorith.contracts;
service ModulesService {
rpc StreamSettingUpdates (StreamSettingUpdatesRequest) returns (stream SettingUpdate);
}
message SettingUpdate {
string module_instance_id = 1;
string setting_key = 2;
SettingProperty property = 3;
oneof value {
string string_value = 4;
bool bool_value = 5;
int32 int_value = 6;
double number_value = 7;
ChoiceList choice_list = 8;
}
}
Na host: SettingUpdateBroadcaster z Rx.NET Subject. Zmiany w modułach (np. przełączenie utworu) są wypychane do strumienia.
Na client: przy starcie otwierany jest strumień, aktualizacje trafiają do ViewModel. ReactiveUI zapewnia natychmiastowe przerysowanie bez opóźnień.
Zalety nad pollingiem:
- Brak opóźnień (push zamiast odpytywania).
- Efektywne wykorzystanie ruchu sieciowego.
- Ścisła typizacja protobuf zapobiega desynchronizacji.
Bezpieczny IPC zamiast TCP
Lokalne porty są podatne na ataki: konflikty, dostęp wirusów, wyskakujące okienka firewalla. Przejście na IPC:
| Platforma | Transport | ACL |
|-----------|-----------|-----|
| Windows | Named Pipes (ListenNamedPipe) | Tylko bieżący użytkownik |
| Linux/macOS | Unix Domain Sockets (ListenUnixSocket) | Uprawnienia plikowe |
Host zapisuje endpoint w %AppData%/Axorith/host-info.json. Client odczytuje i łączy się. Pełna izolacja procesów.
Cykl życia i UX
Podejście Client-first:
- Uruchomienie Client.exe.
- Sprawdzenie host-info.json, automatyczne uruchomienie Host w tle.
- Zamknięcie okna — minimalizacja do zasobnika (--tray mode).
- Ikona w zasobniku do restartu/zatrzymania Host lub rozwinięcia UI.
Wykrywanie zerwania połączenia: HTTP/2 Keep-Alive rejestruje przerwanie strumienia. Host wysyła powiadomienie systemowe (Windows Toast / D-Bus).
Test niezniszczalności:
- Sesja "Work": Spotify gra, YouTube zablokowany.
- Zakończ Client w Menedżerze zadań.
- Wynik: backend żyje, sesja kontynuowana.
Trudności wdrożenia
- Kontrakty: Każda zmiana — regeneracja proto, aktualizacja obu stron.
- Wersjonowanie: gRPC Interceptor sprawdza
x-axorith-version. Niespójność — FailedPrecondition + automatyczna aktualizacja. - Asynchroniczność: Wszystkie wywołania Task, obsługa timeoutów, błędów sieciowych na localhost.
Co jest ważne
- Rozdzielenie procesów eliminuje single point of failure w wątku UI.
- gRPC streaming + Rx.NET dają reaktywny UI bez polling.
- IPC (Named Pipes/UDS) zapewnia bezpieczeństwo bez portów.
- Client-first UX ukrywa złożoność przed użytkownikiem.
- Nadaje się dla aplikacji 24/7 z zarządzaniem systemowym.
— Editorial Team
Brak komentarzy.