Podsumowanie

Jeden obraz jest wart więcej niż tysiąc słów 🙂 Nagrałem więc krótki filmik „promocyjny” w ramach podsumowania i umieściłem na vimeo. Poniżej znajdziecie linki do poprzednich postów konkursowych, dokumentujących moje poczynania, do przeczytania których serdecznie zapraszam. A na githubie świeżutkie repo, abyście mogli sami się pobawić aplikacją. Wystarczy uruchomić skrypt build, a następnie run i gotowe.

 13.03 Daj Się Poznać 2017
 19.03 Plan
 21.03 Don’t Repeat Yourself
 26.03 Orkiestra
 29.03 Interakcja
 02.04 Kolejka
 04.04 Konferencja
 09.04 Odłamkowy ładuj
 11.04 Code Jam
 15.04 Logika
 18.04 Podziel się
 23.04 Error
 26.04 Special cases
 29.04 Remote Debug
 04.05 Sprache
 07.05 Persystencja
 10.05 Frontend
 14.05 Węzły
 17.05 Bądź na bieżąco
 21.05 Kontakt
 24.05 Don’t Stop Me Now

Te jedenaście tygodni zleciało dość szybko. Efekt końcowy pozostawiam do waszej oceny. Mam nadzieję, że się podobało.

Ja jestem zadowolony, ponieważ konkurs Daj Się Poznać spełnił swoją podstawową funkcję. Pozwolił mi podzielić się swoimi zainteresowaniami i zmotywował do zaimplementowania aplikacji, która od dłuższego czasu chodziła mi po głowie i z której przynajmniej ja będę na co dzień korzystał.

Czuję również, że poprawiłem swoje umiejętności piśmiennicze, nieszlifowane od czasów pisania pracy magisterskiej. Widać to szczególnie po ostatnich postach, z których jestem bardziej zadowolony niż z tych z połowy marca.

tumblr_nhfv2ksaAq1r3c6kio1_500Dziękuję wszystkim za uwagę. Zapraszam jeszcze raz do czytania, oglądania, uruchamiania i… oczywiście głosowania, o ile takie przewidują organizatorzy konkursu 🙂

Don’t Stop Me Now

dontstop

Tak jak pisałem w poprzednim poście, projekt Scrap The World jest już na finishu. Wszystko wskazuje na to, że uda się go zrealizować przed końcem maja, kiedy to konkurs Daj Się Poznać dobiegnie końca. W tym momencie spełnia już zakładaną na etapie planowania funkcjonalność. Można w sposób zdalny sterować grupą workerów, wykonujących skrypty js w oparciu o zdefiniowany przez użytkownika flow, co było jego głównym założeniem.

Dziś będzie wisienka na torcie w postaci możliwości zatrzymania flow w celu zadania użytkownikowi pytania i uzyskania odpowiedzi. Mechanizm może być wykorzystany zarówno w celu pobrania dodatkowych danych potrzebnych do wykonania skryptu, jak również w przypadku sytuacji wyjątkowych, jak choćby konieczności rozwiązania zabezpieczenia typu CAPTCHA, o które na stronach www nie trudno.

Zadanie wydaje się proste. Ot zwykły popup. Ale jak się zastanowić trzeba wziąć pod uwagę, że mamy do czynienia z aplikacją webową, gdzie komunikacja z użytkownikiem odbywa się w sposób asynchroniczny z wykorzystaniem SignalR. To w połączeniu z koniecznością zatrzymania aktualnego flow do momentu otrzymania odpowiedzi i możliwość powrotu do tego samego wątku, do kolejnej linii kodu sprawia, że zadanie się komplikuje.

freddy-mercury

MethodHandler i ReturnWaiter, to dwie klasy, które w połączeniu umożliwiają wywołanie dowolnej metody (callbackowej) i zatrzymanie przetwarzania do czasu otrzymania wartości. Działają one w oparciu o ConcurrentDictionary<Guid, ReturnWaiter> i ManualResetEvent. Na początku generowany jest jednorazowy Guid i wstawiany obiekt typu ReturnWaiter do słownika. Następnie wywoływana jest metoda przekazana jako delegat Action, a tuż po niej WaitOne (ustawiony na 30s) na evencie wewnątrz ReturnWaitera. W momencie kiedy będziemy mieli odpowiedź, ReturnWaiter może zostać usunięty ze słownika, a po wywoływaniu na nim metody Set, przetwarzanie może być kontynuowane. Właściwą implementację znajdziecie w repozytorium projektu.

Wywołanie po stronie flontendu sprowadza się do poniższej metody w NancyModule:

Get["/questionAnswer"] = o =>
{
    var question = this.Request.Query["q"];
    var guid = Guid.NewGuid();
    string answer = MethodHandler.GetValue<string>(guid, () => HelloHub.Question(guid.ToString(), question));
    return answer;
};

Jako delegat podałem metodę Question z huba signalrowego, która jest odpowiedzialna za wysłanie na frontend pytania przekazanego jako parametr typu Query. Po stronie frontendu pokazywany jest następnie $mdDialog z angular material z inputem na wprowadzenie odpowiedzi.

question

Po stronie logiki dodałem do naszego języka domenowego słowo kluczone question, które użyte zamiast nazwy skryptu do uruchomienia, odpowiada za wysłanie nieograniczonego czasowo żądania typu GET do frontendu (którego IP zostało przekazane w requeście), inicjując tym samym zadanie pytania. Odbywa się to za pomocą biblioteki RestSharp, z której już wcześniej korzystaliśmy.

if (node.AskQuestion)
{
    var client = new RestClient("http://" + node.Data.Frontend);
    var req = new RestRequest("/questionAnswer?q=" + node.Data.Question) { Timeout = int.MaxValue };
    var response = client.Execute(req);
    node.Data.Answer = response.Content;
}

Ustawienie maksymalnego czasu na requeście spowoduje, że RestSharp, nie rzuci nam wyjątkiem (domyślny timeout wynosił bowiem 100s) i zatrzyma jednocześnie flow po stronie logiki. Wtedy do gry wchodzi mechanizm po stronie frontendu, gdzie timeout jest krótszy, bo ustawiony na 30 sekund. Jest to czas przewidziany na odpowiedź użytkownika na pytanie z dialogu. Może on zwrócić odpowiedź lub nie. Domyślnie flow będzie tak czy siak kontynuowany.

Można jednak pomyśleć o rozwinięciu implementacji o opcję ponawiania lub możliwości całkowitego przerwania przetwarzania.

Za pytanie i odpowiedz po stronie huba odpowiadają poniższe metody:

public static void Question(string guid, string data)
{
    _hubContext.Clients.All.question(guid, data);
}

public void Answer(string guid, string answer)
{
    MethodHandler.GetValueResult(Guid.Parse(guid), answer);
}

I na koniec. Od strony UI, dodałem do przykładowych węzłów na ekranie do zarządzania flow, te związane z dzisiejszym postem: Question i Answer (zmodyfikowany set przepisujący wartość z Data.Answer) oraz zaktualizowałem przykład, na którym pracowaliśmy od początku pracy nad projektem. Umożliwia on teraz wysłanie do użytkownika pytania i wprowadzenie odpowiedzi (fill) do pola w wyszukiwarce DuckDuckGo. Nice 🙂

Następny post będzie podsumowaniem pracy nad projektem i pokazaniem jego możliwości. Zapraszam do… zobaczenia 🙂

Kontakt

contact-wallpaper

Projekt Scrap The World jest już na etapie, na którym można pomyśleć o jego wystawieniu w sieci publicznej. Udostępnia bowiem zakładaną funkcjonalność. Można zarówno edytować słownik z javascriptami oraz definiować w graficzny sposób flow, który może być następnie wykonywany przez workery. Workery mogą dzielić się pracą w sprawiedliwy sposób i kiedy to potrzebne zwracać otrzymane rezultaty do kolejki, z której wyciąga je następnie frontend i zwraca do użytkownika za pomocą SignalR.

Zanim udostępnimy aplikację w internecie, musimy najpierw poznać kilka adresów IP i portów: adresy IP i porty komputerów na których nasłuchuje frontend oraz nasza restowa baza, adres IP routera, nasz publiczny adres IP oraz porty, które muszą zostać otwarte na routerze dla naszych aplikacji. Porty zależą od nas, a adresy IP od naszego dostawcy internetu i protokołu DHCP 🙂 Ja przyjąłem, że lokalnie aplikacja będzie wystawiona na porcie 8080, a baza na 8081. Resztę można sprawdzić poleceniem ipconfig i na stronie whatismyip.

ImyrC

Mając te informacje zacznijmy najpierw od widoczność poszczególnych aplikacji między sobą. Zarówno frontend jak i workery muszą znać oczywiście lokalizację kolejki rabbitowej, z której wszystkie korzystają, dodatkowo frontend musi znać adres IP i port na którym pracuje baza danych.

Można by było dodać te wskazania jako ustawienie w konfiguracji. Ja jednak przygotowałem pomocniczą metodę FindServerInLocalNetwork, która tuż po uruchomieniu każdej z aplikacji wyszukuje w sieci lokalnej aktywne komputery a następnie z wykorzystaniem klasy Socket sprawdza czy mają otwarte porty TCP związane z rabbitem i naszą usługą WWW. Oczywiście w pierwszej kolejności sprawdza czy nie są przypadkiem uruchomione lokalnie.

Uwaga. W przypadku RabbitMQ uruchomionego na innym komputerze niż localhost, o czym już wcześniej pisałem, należy pamiętać, że ze względów bezpieczeństwa musimy użyć innego konta niż guest. Inaczej się z nim nie połączymy.

Aby frontend był dostępny z zewnątrz trzeba zadbać o odpowiednie mapowanie portów, tak aby publiczny adres i port na routerze wskazywał na lokalny adres IP i port na którym uruchomiona została aplikacja. Można oczywiście wprowadzić takie mapowanie ręcznie logując się do routera, jednak w przypadku kiedy byśmy coś zmienili, należałoby zadbać o ponowne przemapowanie. Jest na to lepsze rozwiązanie. Jeśli tylko wasz router wspiera protokół UPnP można przy pomocy biblioteki Open.NAT dostępnej na nugecie, utworzyć takie mapowania za pomocą dosłownie paru linijek kodu:

private static async Task MapPorts()
{
    var discoverer = new NatDiscoverer();
    var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));

    var device = await discoverer.DiscoverDeviceAsync(PortMapper.Upnp, cts);
    ExternalIP = device.GetExternalIPAsync().Result.ToString();

    await device.CreatePortMapAsync(new Mapping(Protocol.Tcp, Port, ExternalPort, "Scrap The World"));
    await device.CreatePortMapAsync(new Mapping(Protocol.Tcp, PersistencePort, PersistencePort, "Persistence"));
}

Ponieważ persystencja jest osobną aplikacją konsolową niż frontend, utworzyłem dwa mapowania. Dla frontendu zmieniłem dodatkowo numer portu na domyślny (80), aby nie trzeba go było wpisywać w przeglądarce. Dodałem również do aplikacji konsolowej hostującej frontend informacje o aktualnych adresach, aby nie trzeba było ich pamiętać.

public

Wracając jeszcze do poprzedniego posta. Aby być jeszcze bardziej na bieżąco wprowadziłem kilka usprawnień do warstwy prezentacji, np. wykorzystałem Notifications API i dodałem proste powiadomienia, które będą informowały o nowych wynikach w sytuacji, kiedy będziemy mieli otwartą inną zakładkę przeglądarki.

notification

O tym czy są włączone można decydować za pomocą dedykowanego przełącznika w menu. Menu, którego brakowało na naszej stronie głównej, właśnie się na niej znalazło. Znalazła się tu również nawigacja do skryptów i flow oraz możliwość czyszczenia listy wyników.

menu

To newsy z tego tygodnia i to jeszcze nie koniec. Zapraszam do śledzenia 🙂

 

Bądź na bieżąco

up to date

W jaki sposób być na bieżąco z tematami, które nas interesują? Bycie na bieżąco nie jest prostą sprawą, trzeba albo bardzo dużo czytać, albo umieć zadawać dobrze ukierunkowane pytania. Ważne, żeby wśród natłoku informacji wybrać to co jest nam na prawdę potrzebne i stanowi wartość dodaną.

Taki jest cel, który przyświeca powstaniu projektu Scrap The World. Jego główne zadanie to zwolnienie nas z konieczności małpiego przeczesywania internetu i umożliwienie definiowania powtarzalnych flow, które na co dzień świadomie lub nie uruchamiamy oraz możliwości zbierania wyników.

Wiele firm oraz osób prywatnych już dostrzegło ten problem i podjęło rękawicę. Nie mówię o facebookowej tablicy, czy różnego rodzaju agregatorach, ale o botach internetowych. Ruch związany z botami stanowi bowiem ponad połowę całego ruchu w internecie.

Do tego sprowadza się bycie na bieżąco w ujęciu globalnym, a jak to się ma do konfiguracji samych workerów? Oczywiście, musi również nadążać za flow wyklikanym przez użytkownika. Inaczej jest spora szansa, że otrzymamy niepoprawne wyniki, bo worker będzie pracował na nieaktualnych danych.

Istnieją różne sposoby, aby zapewnić synchronizację między wartościami zapisanymi w bazie danych, a tymi na których operują workery.

Gdyby nie to, że używany message broker ma jednowątkowe przetwarzanie, o czym pisałem w jednym z poprzednich postów, moglibyśmy np. całą konfigurację wysyłać do wszystkich podłączonych aplikacji za pomocą już istniejącej kolejki.

Można również pomyśleć o rozwiązaniu typu pull i wykorzystać samą bazę restową, do której workery wysyłałyby requesty z pytaniem o konfigurację flow i javascripty, np. śledząc przy tym czasy poszczególnych zapisów, w celu ograniczenia pustych przebiegów. Rozwiązanie też nie wydaje się najszczęśliwsze.

dobromir

Wszystko staje się prostsze, gdy sobie uzmysłowimy dwie rzeczy.

Po pierwsze, nie wszystkie workery muszą mieć aktualną konfigurację i nie muszą jej mieć natychmiast po zapisie do bazy. Temat trochę z pogranicza eventual consistency. Wystarczy, że przed rozpoczęciem przetwarzania flow, worker, który wyciągnął żądanie z kolejki, będzie miał listę potrzebnych w danym momencie węzłów i skryptów.

A po drugie. Każdy worker obsługuje jednocześnie tylko jeden request, który inicjuje dany fragment flow i wywołuje sekwencyjnie skrypty z nim związane.

Możemy więc wszystko sprowadzić do tej samej statycznej klasy fabryki, z której korzystaliśmy do tej pory, z tą różnicą,  że lista węzłów i skryptów będzie wyciągana z requesta i ustawiana na początku logiki przetwarzającej.

private static void UpdateFlow(Node node)
{
    if ((node.Nodes ?? new List<Node>()).Any())
    {
        Factory.Nodes = node.Nodes.Clone();
        node.Nodes = null;
    }

    if ((node.Scripts ?? new Dictionary<string, string>()).Any())
    {
        Factory.Scripts = node.Scripts.Clone();
        node.Scripts = null;
    }
}

Za zakres przekazanej konfiguracji odpowiada frontend, który jako jedyny komunikuje się bazą i wyciąga z niej  potrzebne w danym momencie wartości kluczy.

var client = new RestClient("http://localhost:8081");
var req = new RestRequest("/api/Persistence?table=scripts");

node.Scripts = new Dictionary<string, string>();

foreach (var item in client.Execute<dynamic>(req).Data)
{
    node.Scripts.Add(item["Key"], item["Value"]);
}

req = new RestRequest("/api/Persistence?table=flows&key=" + flow);

var data = client.Execute<dynamic>(req).Data;
var json = data?["Value"] ?? "{}";
node.Nodes = GetNodes(json);

Rozbudowałem metodę przygotowującą request w oparciu o przekazane parametry o ww. zachowanie i dodałem do repozytorium.

Tak wygląda to przy pierwszym żądaniu, pochodzącym z frontendu. Należy jednak pamiętać, że w przypadku przesyłania requestów między workerami, czyli pod koniec naszej logiki, również musimy uzupełnić parametry konfiguracyjne, tym razem w oparciu o bieżące właściwości fabryki.

if (newNode.NextWorker)
{
    newNode.Nodes = Factory.Nodes;
    newNode.Scripts = Factory.Scripts;

    await onNext(newNode);
}

To tyle na dzisiaj. Tradycyjnie zapraszam do śledzenia 🙂

Węzły

knots

Dziś kontynuujemy prace nad frontendem, a konkretnie będziemy zajmowali się węzłami i możliwością ich wiązania. Tak jak węzły marynarskie umożliwiają połączenie dwóch lin na różne sposoby, tak samo łączenie węzłów z poziomu UI sprawi, że będziemy mogli testować połączenia między różnymi elementami naszego flow, bez konieczności zmian w kodzie.

Ekran do modelowania flow jest zatem podstawą funkcjonowania projektu Scrap The World, a zabraliśmy się za niego dopiero teraz, bo jak widać, projekt jest realizowany metodą bottom-up.

Zaczęliśmy od przygotowania kanału komunikacji, różnego rodzaju workerów, później zajęliśmy się logiką oraz obsługą błędów i sytuacji wyjątkowych. Ostatnio pokazywałem jak w prosty sposób możemy wszystko zapisać, tworząc restową bazę danych oraz że definiując swój własny język domenowy możemy wiele zyskać, m. in. upraszczając interfejs użytkownika.

A propos. Do jego przygotowania wykorzystałem bibliotekę jQuery o nazwie jquery.flowchart. Z początku sam byłem lekko zaskoczony, myślałem, że całość rozwiązania wykonam przy użyciu angulara. Jednak wśród bibliotek angularowych nie znalazłem o dziwo żadnej darmowej, która umożliwiłaby mi przygotowanie stosunkowo prostego ekranu na którym mógłbym dodawać węzły poprzez ich przeciągnięcie na kanwę, łączyć je oraz importować/eksportować do formatu JSON.

flowPrzypomniałem więc sobie czasy, w których programowało się aplikacje webowe bez wzorca MVVM z wykorzystaniem jQuery i przystąpiłem do działania.

W jednym z pierwszych postów wspominałem o tym, że w pracy programistycznej powinniśmy stronić od powtarzania się i wybierać sprawdzone komponenty, które są dobrze skrojone, a ich dodanie do projektu łatwe.

Tak było w przypadku tej biblioteki. Jej użycie jako podstawy działania ekranu było samą przyjemnością. Posiada minimalistyczne i bardzo dobrze opisane api, co pozwoliło mi na dostarczenie pożądanej funkcjonalności w szybki sposób. Podczas czytania dokumentacji oraz kodu, od razu pomyślałem, że osobie, która ją przygotowała po prostu zależało.

happy-dev

Całość ostylowałem Materialize sprawiając, że UI nie odbiega wizualnie od pozostałych ekranów angularowych. Może on stanowić samodzielne rozwiązanie  dlatego umieściłem go w osobnym projekcie VS, jednak dodałem akcję post build, kopiującą statyczne pliki do głównego projektu z frontendem.

Ekran tak jak wspomniałem umożliwia konfigurowanie węzłów, z których otrzymamy listę obiektów typu Node, przetwarzanych przez workery. Można to robić w dwojaki sposób, poprzez pisanie linii komend oraz wizualne wiązanie. Dodałem listę przykładowych elementów, aby można je było połączyć we flow, znany z poprzednich postów.

Całość może być wyeksportowana do jsona lub zapisana w naszej bazie sqlite. Do logiki dodałem metodę, która zamienia wspomnianego jsona na listę obiektów. W ciele metody umieściłem wywołanie klasy NodesParser, zamieniającej komendy na węzły, a tam gdzie było wizualne połączenie między elementami na UI, ustawiłem property NextNode ostatniego z otrzymanych węzłów na pierwszy węzeł połączonego elementu.

Teraz wystarczy jedynie rozpropagować konfigurację, aby workery miały aktualne informacje o strukturze flow i javascriptach i możemy już prawie otwierać szampana 😉

 

 

Frontend

opakowania

Mówią schludny wygląd to podstawa. Nawet najlepszy produkt bez dobrego opakowania ma często mniejsze szanse z przeciętnymi, a dobrze wyglądającymi produktami konkurencji. Przyszedł czas rozpocząć pracę nad warstwą prezentacji. Do tej pory wystarczał nam w zupełności interfejs konsolowy, który znacznie ułatwił testowanie aplikacji. Dążymy jednak do tego, aby móc pracować na odległość, a do tego potrzebujemy wystawić projekt Scrap The World w internecie albo przynajmniej intranecie.

Do prostych aplikacji webowych, hostowanych jako aplikacje konsolowe, w zupełności wystarczy nam biblioteka Nancy i angular. Nie ma moim zdaniem co angażować całego ASP.NET MVC, aby przygotować domowej klasy projekt. Nutkę elegancji doda z pewnością wykorzystanie angularowego material design i sprawi, że cały UI nabierze lekkości.

frontendInterfejs jest jak widać minimalistyczny. W polu wyszukiwania możemy podać nazwę pierwszego węzła do procesowania, a następnie oddzielone spacjami nazwy i wartości parametrów, które chcemy przekazywać w naszym słowniku Expando, między kolejnymi wywołaniami logiki.

To co najistotniejsze dzieje się po kliknięciu szukaj. To tzw. Real-time web.  Użycie biblioteki SignalR umożliwia na łatwe dodanie takiego mechanizmu do naszych .netowych aplikacji webowych. SignalR inicjuje kanał komunikacji do wysyłania requestów oraz nasłuchiwania na wyniki operacji. Piękno tej biblioteki polega właśnie na tej dwustronnej komunikacji między klientem a serwerem, gdzie z kodu C# możemy wywołać funkcję js po stronie przeglądarki.

010013438535Daje to możliwość wyświetlania wyników połączonym z tzw. hubem klientom, bez konieczności ciągłego odpytywania się przez nich, czy serwer je posiada. Zmienia to podejście z tradycyjnego pull na push, dbając również o dobór wspieranego przez klienta mechanizmu transportowego.

Dodanie SignalR do aplikacji angularowej ogranicza się do użycia odpowiedniego factory i skonfigurowania nazw metod po stronie serwera oraz implementacji funkcji klienckich.

SignalR posiada możliwość pisania do różnych grup użytkowników. Skupimy się na najprostszym sposobie identyfikacji, na podstawie connection id. Identyfikator można sprawdzić z poziomu js

var getConnectionId = function() {
    return hub.connection.id;
}

jak również wyciągnąć z kontekstu w C#

var connectionId = Context.ConnectionId;
node.Data = new { Guid = connectionId };

Przekazując ww. identyfikator jako Guid mamy możliwość powiązania węzłów z danym połączeniem signalrowym oraz zwrócenia wyników właściwemu klientowi.

private static IHubContext _hubContext = GlobalHost.ConnectionManager.GetHubContext<HelloHub>();
_hubContext.Clients.All.addResult(data, image);
_hubContext.Clients.Client(connectionId).addResult(data, image);

To jeszcze nie koniec frontendu. Nie mamy jeszcze najważniejszego, możliwości łączenia węzłów we flow.

Tradycyjnie. Zapraszam do śledzenia 🙂

Persystencja

Persistence creative green sign

W poście kolejka i odłamkowy ładuj pokazałem jak można w prosty sposób przygotować przenośną wersję brokera RabbitMQ i jak za pomocą dosłownie dwóch linijek kodu umożliwić komunikację między różnymi elementami systemu. Dziś będzie mowa o persystencji.

Do tej pory w logice projektu Scrap The World korzystaliśmy z prostej fabryki, w której zahardkodowane były węzły oraz skrypty przetwarzane przez workery. Zanim przejdziemy do budowania frontendu naszej aplikacji i umożliwimy łączenie węzłów w postaci wizualnej, przygotujmy przenośną bazę danych typu klucz-wartość do której będziemy mogli zapisywać nasze javascripty.

Jak w prosty sposób można podejść do tematu? Można na przykład wykorzystać dwie paczki nugetowe, LiteDB i Swashbuckle, które w połączeniu z microsoftowym Web API idealnie się do tego nadadzą. Umożliwią przygotowanie restowego słownika, z którego będzie mógł korzystać zarówno frontend aplikacji jak i same workery.

LiteDB to proste i szybkie rozwiązanie NoSQL’owe. Tak piszą twórcy i rzeczywiście tak jest. Cała biblioteka mieści się w jednej dll’ce, nie wymaga instalacji serwera, ma bardzo proste API i szereg możliwości. Pozwala przy minimalnym narzucie na implementację zapisywać klasy POCO w jednym fizycznym pliku na dysku. Ciekawostką jest to, że wspiera format Bson. LiteDB będzie miejscem do którego będziemy zapisywali pary KeyValue. Oczywiście u was może to być dowolna, inna baza typu NoSQL, np. SQLite.

Swashbuckle, czy Swagger ogólnie, to biblioteka, która pozwala na samo-dokumentowanie restowego api, np. WebApi. Dodatkowo daje nam interfejs Swagger UI, przypominający rozszerzenie Postman do Chrome, umożliwiający jego testowanie. Wystarczy wprowadzić parametry i nacisnąć przycisk „Try it out!” i gotowe. Nie dość, że zwróci nam wyniki to jeszcze pokaże wysłany request, czy polecenie curl, jakie należy wpisać w konsoli, aby uzyskać ten sam efekt.

swaggerNajlepsze w tym jest to, że konfiguracja w podstawowej formie, polega na jednej linii kodu, a zyskujemy na prawdę wiele. Dla rozwiązań domowych można by się pokusić i całkiem zrezygnować z warstwy ui i wykorzystać interfejs oferowany przez swaggera. Oczywiście w ramach konkursu to nie przystoi, dlatego przygotowałem prosty widok angularowy, umożliwiający CRUD na naszych skryptach.

persistence

Zachęcam do zapoznania się z implementacją. Jest ona bardzo krótka i treściwa, a dzięki zastosowaniu interfejsu restowego można ją wykorzystać w niemal każdym projekcie.

Jeszcze dwa słowa wyjaśnienia. W przypadku Swaggera czy nawet Postmana Cross-Origin Resource Sharing, czyli tzw. CORS nie jest problemem. Należy jednak pamiętać, że jeśli będziemy próbowali wykonać request ajaxowy z poziomu innej aplikacji webowej, dostaniemy komunikat „Origin is not allowed by Access-Control-Allow-Origin”. Całe szczęście w WebApi można to w banalnie prosty sposób dodać do konfiguracji.

Odnośnie trybu administratora. Pisałem, że najbardziej lubię aplikacje w wersji portable, w których nie musimy niczego instalować w systemie i które działają bez dodatkowych uprawnień. Czasem nie można od tego uciec. Chcąc uruchomić własną aplikację webową musimy poprosić system o możliwość jej zarejestrowania na wybranym porcie.

netsh http add urlacl url=http://+:8081/ user=%USERNAME%

Można to uzyskać uruchamiając całą aplikację w trybie administratora, wówczas wiązanie stworzy się automatycznie lub jednorazowo wywołując powyższą komendę. Aplikacja po tej operacji może już dalej pracować w trybie zwykłego użytkownika.

W następnych postach zaczniemy po mału przygotowywać frontend i spinać całe rozwiązanie. Zapraszam do śledzenia 🙂

Sprache

sprache

Dziś będzie mowa o DSL i nie chodzi mi tu o szerokopasmowy internet. DSL, czyli Domain-specific Language, to z mojego doświadczenia kolejna z rzeczy, którą programiści znają, ale rzadko kiedy wykorzystują. A jest to bardzo użyteczny koncept, dający możliwość opisania naszej domeny w sposób bardziej naturalny, za pomocą własnego, skrojonego na miarę języka.

Zbudujmy więc prosty język domenowy do opisu węzłów. Dla uproszczenia, każdą linię takiego opisu będziemy traktowali niezależnie i nazywali komendą. Komenda będzie stanowiła tekstową reprezentacją węzła przetwarzanego przez workery, natomiast każdy wizualny blok (składający się na flow), opisany za pomocą jednej lub wielu takich komend.

Dzięki opisaniu węzłów za pomocą języka domenowego mamy możliwość odciążenia użytkowników od znajomości wewnętrznej struktury klasy Node oraz każdorazowego wyświetlania nieobligatoryjnych pól na interfejsie użytkownika. Dodatkowo możliwość zdefiniowania w ramach jednego wizualnego bloku wielu komend, a tym samym wielu węzłów, pozwoli również na zwiększenie czytelności podczas konfiguracji samego flow.

public override string ToString()
{
    return $"{(NextWorker ? "worker " : "")}{(WaitTime > 0 ? "wait " + WaitTime + " " : "")}{(Open ? "open" : Script)}{ExpandoToString(Data)}{(ReturnResults ? " return" : "")}";
}

Jeśli chodzi o składnię. Każdy obiekt typu Node zawiera albo otwarcie wybranej strony (open), albo uruchomienie skryptu ze słownika, albo jedno i drugie. Po nazwie skryptu lub słowa open, w komendzie opcjonalnie mogłyby się pojawić pary klucz-wartość słownika, obiektu expando, opisującego stan węzła. Dodatkowo należałoby zdefiniować jeszcze dwa słowa kluczowe identyfikujące te obiekty Node, które mogą zostać uruchomione w innych workerach (worker) oraz te, które zwracają wyniki (return).

Niewątpliwym plusem jest to, że w komendach możemy pominąć już nazwy węzłów. Wystarczy, że pierwszy z nich będzie nazwany, aby flow przez nie opisany mógł zostać uruchomiony.

Poniżej przykład opisujący ten, na którym pracowaliśmy w poprzednich postach:

var commands = new StringBuilder();
commands.Append("open Url \"https://duckduckgo.com/\"");
commands.AppendLine();
commands.Append("fill QuerySelector \"#search_form_input_homepage\" Value \"Test\"");
commands.AppendLine();
commands.Append("click QuerySelector \"#search_button_homepage\"");
commands.AppendLine();
commands.Append("--wait 1000");
commands.AppendLine();
commands.Append("urls");
commands.AppendLine();
commands.Append("limit Limit \"3\"");
commands.AppendLine();
commands.Append("split Limit \"1\"");
commands.AppendLine();
commands.Append("set From \"self.LastResults[0]\" To \"Url\"");
commands.AppendLine();
commands.Append("worker open");
commands.AppendLine();
commands.Append("title return");

var nodes = NodesParser.ParseString("flow2", commands.ToString());

Dzięki wykorzystaniu biblioteki Sprache, mogłem w prosty sposób przełożyć linie komend na generyczną listę węzłów. Odbywa się to za pomocą kilku obiektów klasy Parser, odpowiadających za poszczególne części składowe komend, które łączy się następnie w jednym zbiorczym zapytaniu LINQ, inicjującym właściwości obiektów typu Node. Implementacja jest dość prosta i bardzo czytelna.

Sami twórcy Sprache podkreślają, że biblioteka jest czymś pomiędzy wyrażeniami regularnymi, a generatorami parserów, takimi jak ANTLR. Znakomicie się sprawdza właśnie w takich sytuacjach, kiedy chcemy zdefiniować prosty język domenowy dla naszej aplikacji, a nie przypuśćmy kolejny język programowania. Zachęcam do przejrzenia artykułu wprowadzającego, opisującego podstawowe możliwości Sprache, jak również do powyższego przykładu znajdującego się w repozytorium projektu 🙂

Remote Debug

remote

Przeglądając ustawienia CefSharpa natknąłem się na jedno, które wzbudziło moją ciekawość. A mianowicie RemoteDebuggingPort.

var settings = new CefSettings
{
    RemoteDebuggingPort = 9222
};

Na stronie CefSharp API Doc jest napisane, że port może być dowolną liczbą z przedziału 1024-65535, a jego ustawienie umożliwia zdalne debugowanie z dowolnej przeglądarki CEF lub Chrome. Od razu wpisałem wskazany w dokumentacji adres i ukazała mi się poniższa strona:

cef_remote

A klikając w link narzędzia developerskie. W konsoli można było testować kod javascript uruchamiany w  cefsharpowej przeglądarce, w ten sam sposób, jak gdybyśmy robili to w niej natywnie.

interia_remote_debug

Okazuje się, że to nic nadzwyczajnego, a za wszystkim stoi nic innego jak Chrome Debugging Protocol. Po przejrzeniu krótkiej dokumentacji na stronie Google można dowiedzieć się, że jest to protokół web socketowy, który bazuje na przesłaniu prostego jsona z polem expression, z kodem javascript do wykonania. W odpowiedzi dostaniemy również json, tyle że z informacjami widocznymi w konsoli.

Myślę, że nikt z nas specjalnie nie wykorzystywał tego protokołu, chyba że na co dzień zajmuje się stronami www wyświetlanymi na urządzeniach Android. Protokół bowiem znacznie ułatwia pracę z mobilną wersją Chrome. Uruchamiając Chrome Developer Tools na komputerze oraz zezwalając na zdalny dostęp na telefonie, możemy np. stylować elementy strony na odległość. Co, nie oszukujmy się, byłoby trudne, gdybyśmy nie mieli takiego mechanizmu.

android

Jak Chrome Debugging Protocol ma się do projektu Scrap The World. Okazuje się, że jest on podstawą funkcjonowania takich bibliotek jak chociażby Selenium. Pytanie jednak, czy potrzebujemy aż tylu funkcji, które oferuje nam ta biblioteka? Najbardziej zależy nam na otwieraniu stron, wywoływaniu skryptów javascript oraz czekaniu na pełne załadowanie strony. Spróbujmy więc zaimplementować obsługę tych trzech rzeczy samemu.

Zacznijmy od uruchomienia Chrome w trybie zdalnego debugowania. Można wykorzystać CefSharpa z ustawionym RemoteDebuggingPort, albo Chrome, np. w wersji portable, z parametrem remote-debugging-port:

GoogleChromePortable.exe –remote-debugging-port=9222 http://www.google.pl

Otwierając stronę http://localhost:9222/json/list otrzymamy listę zakładek z adresami webSocketDebuggerUrl:

json_listWykorzystując np. bibliotekę RestSharp, można otrzymać powyższe adresy w kodzie C#.

Dodałem do repozytorium workera wykorzystującego prostą implementację MyClientWebSocket, wrappera klasy ClientWebSocket, umożliwiającej komunikację web socketową w środowisku .NET. W logice będziemy wykorzystywali trzy metody: Open, EvaluateWithReturnWaitForDocumentReady.

Po nawiązaniu połączenia z adresem:

ws://localhost:9222/devtools/page/5a0eabc7-d1ae-4f8b-befb-7465f9b765d8

klasa przygotowuje request, przesyła go do przeglądarki i czeka na informację zwrotną.

Metoda WaitForDocumentReady, bazuje na sprawdzaniu stanu document.readyState i jeśli będzie różny od complete, zaczeka sekundę i spróbuje ponownie. Wszystkie zapytania wywoływane są w osobnych sesjach, zamykanych tuż po odebraniu wyników. Same jsony są natomiast postaci:

{"method":"Runtime.evaluate","params":{"expression":"skrypt_do_wykonania","objectGroup":"console","includeCommandLineAPI":true,"doNotPauseOnExceptions":false,"returnByValue":true},"id":1}

Dla zwrócenia tytuły strony, expression będzie równe – document.title.

Tak to wygląda w środowisku chrome. I tak, nie wygląda to na skomplikowane 🙂 Potraktujmy ten POC jako ostatnią alternatywę implementacji workera, który będzie dostępny do wyboru z poziomu naszego frontendu.

Przy okazji znalazłem jeszcze jedną stronę, opisującą inicjatywę mającą na celu wypracowanie wspólnego protokołu RemoteDebug dla współczesnych przeglądarek. Więcej informacji można uzyskać na stronie.

Tradycyjnie. Zapraszam do śledzenia 🙂