BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Niewidoczna dla użytkownika aktualizacja aplikacji.

11-02-2004 16:46 | User 79209
Czyli ClickOnce bez Whidbey :)

Wstęp

Kiedyś na jakiejś stronie przeczytałem, że obecnie dobrym programistą nie jest osoba, która sama potrafi pisać kod, ale taka, która potrafi znaleźć odpowiednie rozwiązanie i umiejętnie je „wkleić”. Autor argumentował swoją tezę tym, iż prawie wszystko zostało już wymyślone i przetestowane, tak więc nie ma sensu wymyślać koła. Idąc jego tropem ;) postanowiłem przy tworzeniu aplikacji wykorzystać framework (zbiór klas) stworzony w ramach Microsoft Application Blocks for .NET. Strona domowa tego projektu to: http://www.gotdotnet.com/community/user/newuser.aspx?returl=%2fCommunity%2fWorkspaces%2fworkspace.aspx. W tym miejscu warto zwrócić uwagę na fakt, że Microsoft, sam lub we współpracy z kimś, tworzy wiele rozwiązań, które nie są oficjalnie reklamowane, ale które za darmo można ściągnąć z ich strony. Przykładami takich innych rozwiązań są klasy ODBC oraz kontrolki IE Controls dla ASP.NET.

Wymagania

Aby móc przetestować przedstawione poniżej rozwiązanie trzeba mieć podstawową wiedzę z zakresu C# oraz zainstalowany serwer IIS wraz z frameworkiem w wersji 1.1.

Schemat działania

Microsoft Application Blocks for .NET można ze względu na architekturę podzielić na cztery główne elementy: Updater, Downloader, Validator i Post processor. Poniżej na rysunku 1 przedstawiłem jak te elementy współpracują ze sobą przy realizacji procesu aktualizacji.

Rysunek 1. Schemat działania programu – diagram czynności

Mam nadzieję, że powyższy rysunek jest w stanie wyjaśnić sposób działania procesu aktualizacji. Dla pewności opiszę ten proces także słownie. I tak, użytkownik uruchamia aplikację. Jednakże nie uruchamia jej poprzez bezpośrednie wywołanie jej kodu, a poprzez wywołanie aplikacji będącej loaderem (AppStart.exe). Aplikacja ta sprawdza w pliku AppStart.exe.config, gdzie znajduje się bieżąca wersja naszej aplikacji. Dopiero po jej zlokalizowaniu loader jest automatycznie zamykany, a otwierana jest nasza aplikacja. W tym miejscu zaczyna się proces aktualizacji. Od razu chcę zaznaczyć, że jest wiele możliwych scenariuszy rozpoczęcie aktualizacji. Przykładowo użytkownik może uruchomić aktualizację poprzez wybór opcji w menu lub może się ona samoczynnie rozpoczynać co jakiś okres czasu.

W naszym przypadku aplikacja sprawdza, czy nie ma jakichś aktualizacji za każdym razem, gdy jest uruchamiana. Proces aktualizacji obsługuje Updater (klasa ApplicationUpdateManager i pochodne). Updater przy pomocy Downloadera (klasa BITSDownloader i inne implementujące IDownloader) pobiera z serwera plik manifestu, który określa, jaka wersja aplikacji jest dostępna na serwerze oraz jakie pliki wchodzą w jej skład. Z każdym plikiem skojarzony jest jego podpis cyfrowy. Jeżeli chcesz dokładnie się dowiedzieć, jak działa podpis cyfrowy to polecam artykuł Marcina Kosieradzkiego pt. Zabezpieczanie aplikacji. Jeżeli wersja aplikacji na serwerze jest wyższa od tej obecnej na maszynie użytkownika, to przy pomocy Downloadera do tymczasowego katalogu są kopiowane pliki z serwera. W następnym kroku Validator (klasy implementujące IValidator) sprawdza sygnatury ściągniętych plików. Do weryfikacji używany jest klucz publiczny serwera zawarty w pliku SelfUpdater.exe.config. Jeżeli weryfikacja zostanie zakończona sukcesem, to następnie uruchamiany jest opcjonalny Post processor (klasy implementujące IPostProcessor). Po jego zakończeniu pliki są kopiowane do katalogu o nazwie takiej samej jak wersja oprogramowania(np. 2.0.0.0), a użytkownik jest pytany, czy chce zamknąć bieżącą wersję aplikacji i uruchomić nową, właśnie ściągniętą.

Jak to zainstalować ?

W oryginale przedstawione rozwiązanie nie pozwala na stosowanie ścieżek względnych, co jest bardzo niewygodne i przeczy idei xcopy platformy .NET. Na szczęście autorzy udostępnili źródła, które zmieniłem i teraz ścieżki w plikach konfiguracyjnych są względne. Odnoszą się one do katalogu, w którym znajduje się plik wykonywalny aplikacji użytkownika lub plik wykonywalny loadera. Z powodu wprowadzonych przeze mnie zmian, opisane dalej formaty plików konfiguracyjnych są w pewnym sensie moimi własnymi. Opis oryginalnych formatów można przeczytać w oryginalnej dokumentacji, która zawarta jest w pliku MicrosoftUpdaterApplicationBlock_original.msi.

W MicrosoftUpdaterApplicationBlock_changed.zip znajdują się zmienione przeze mnie źródła. Miejsca, w których zmieniałem kod, można znaleźć poprzez wyszukanie wyrażenia

// Jest.

Zawarte w UserApp_VersionServer.zip pliki konfiguracyjne opierają się na tym, że serwer jest zainstalowany pod adresem http://localhost/VersionServer. Takiej też ścieżki będę używał przy opisie instalacji.

Pierwszym krokiem, jaki trzeba wykonać jest instalacja serwera. W tym przypadku instalacja to za duże słowo, ponieważ wystarczy stworzyć w IIS wirtualny katalog o nazwie VersionServer i przegrać do niego pliki z UserApp_VersionServer.zip, które znajdują się w katalogu o tej samej nazwie. W następnym kroku instalujemy aplikację użytkownika, czyli kopiujemy zawartość UserApp do dowolnego katalogu. Aplikację użytkownika uruchamiamy przez loadera, którym jest AppStart.exe. Jest to konieczne do prawidłowego działania procesu aktualizacji.

Opis plików konfiguracyjnych

W prezentowanym rozwiązaniu cały proces aktualizacji konfigurowany jest przy pomocy plików XML. Do dyspozycji mamy trzy rodzaje takich plików. Pierwszy z nich znajduje się po stronie serwera i w moim przykładzie nosi nazwę Manifest.xml. Zawiera on wersję znajdującego się na serwerze oprogramowania, sieciowy adres katalogu z tym oprogramowaniem oraz listę wchodzących w jego skład plików wraz z ich podpisami cyfrowymi. Przykładowa zawartość tego pliku znajduje się poniżej. Przy tworzenia tego pliku używamy narzędzia o nazwie ManifestUtility.exe.

<ServerApplicationInfo signature="Mmt...DUyGE=">
     <availableVersion>2.0.0.0</availableVersion>
     <updateLocation>http://localhost/VersionServer/2.0</updateLocation>
     <files>
          <!-- Plik wykonywalny aplikacji -->
          <file name="SelfUpdater.exe" signature="tLBy...asdf" />
          <!-- Plik konfiguracyjny dla pliku wykonywalnego aplikacji -->
          <file name="SelfUpdater.exe.config" signature="S...q" />
     </files>
</ServerApplicationInfo>

Następnym plikiem jest plik SelfUpdater.exe.config, który zawiera dane dotyczące danej wersji oprogramowania. Poniżej znajduje się wycinek tego pliku. Od razu uprzedzam, że dosyć często będzie pojawiać się słowo Katalog ;).

<validator (...)>
     <!-- Publiczny klucz serwera -->
     <key>
          <RSAKeyValue>                    
               <Modulus>
                    03Fp2VoJ71LoakocdpX...
               </Modulus>
               <Exponent>AQAB</Exponent>
          </RSAKeyValue>
     </key>
</validator>               
<application name="SelfUpdatingTest"useValidation="true">
     <client>
          <!-- Określa jak nazywa się plik z konfiguracją loadera.                Katalog, w którym jest ten plik, to katalog nadrzędny do                katalogu, w którym znajduje się plik wykonywalny aplikacji -->
          <xmlFile>AppStart.exe.config</xmlFile>
          <!-- Określa jak nazywa się katalog z tymczasowymi plikami.
          Katalog, w którym ten katalog się znajduje, to katalog, w                którym jest plik wykonywalny aplikacji-->
          <tempDir>TemporaryFiles</tempDir>
     </client>
     <server>
          <!-- Sieciowa lokalizacja manifestu -->               <xmlFile>http://localhost/VersionServer/Manifest.xml</xmlFile>
          <!-- Nazwa pliku, w którym będzie przechowywany ściągnięty z
               serwera manifest -->
          <xmlFileDest>Manifest.xml</xmlFileDest>
          <!-- Timeout limit w czasie ściągania manifestu -->
          <maxWaitXmlFile>60000</maxWaitXmlFile>
     </server>
</application>

Ostatnim plikiem jest plik AppStart.exe.config, który zawiera ustawienia dla loadera. Poniżej znajduje się wycinek tego pliku.

<configuration>
     <appStart>
          <ClientApplicationInfo>          
               <!-- Nazwa pliku wykonywalnego aplikacji. Katalogiem,
w którym jest ten plik, jest katalog o nazwie takiej jak wersja aplikacji. Z kolei ten katalog znajduje się w katalogu, w którym jest loader -->
               <appExeName>SelfUpdater.exe</appExeName>
               <!-- Wersja aplikacji do uruchomienia -->
               <installedVersion>1.0.0.0</installedVersion>
               <!-- Czas ostatniej aktualizacji -->
               <lastUpdated>2004-02 07T17:56:16.0150...</lastUpdated>
          </ClientApplicationInfo>
     </appStart>
</configuration>

Aplikacje pomocnicze

Pierwszą aplikacją pomocniczą jest loader, który uruchamia właściwą wersję naszego programu. Ścieżkę do aplikacji tworzy on poprzez złączenie ścieżki do swojego bieżącego katalogu, nazwy wersji oraz nazwy pliku wykonywalnego. Przykładowo, jeżeli loader jest w C:\Temp to uruchomi on aplikację z C:\Temp\1.0.0.0\SelfUpdater.exe.

Drugą aplikacją jest wspomniany już wcześniej ManifestUtility.exe. Program ten potrafi wygenerować do plików XML klucze potrzebne w algorytmie RSA. Co więcej, bardzo ułatwia on życie, ponieważ automatycznie tworzy plik manifestu wraz z potrzebnymi sygnaturami. Ścieżki z pól Update location i Update files folder powinny wskazywać na te same pliki.

Rysunek 2. Interfejs programu do generowania manifestów

Aplikacja użytkownika

Stworzona przeze mnie aplikacja użytkownika nie posiada żadnej funkcjonalności, ale mimo tego znajduje się w niej kilka interesujących linii kodu.

Poniżej znajduje się kod konstruktora, w którym odbywa się inicjalizacja procesu aktualizacji oraz jego rozpoczęcie.

public SelfUpdater()
{
     InitializeComponent();
     // Obsługa tego zdarzenia zapewnia nam możliwość posprzątania
     // po sobie nawet w sytuacji, gdy aplikacja będzie zamknięta
     // bez pytania nas o zgodę.
     AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomainProcessExit);
     // Obsługa tego zdarzenia zapewnia nam możliwość posprzątania
     // po sobie, gdy zostanie wywołane zdarzenie Closed.
     Closed +=new EventHandler(SelfUpdaterClosed);
     // Obiekt klasy ApplicationUpdateManager zarządza całym
     // procesem aktualizacji.
     updater = new ApplicationUpdateManager();
     // Podpinamy się pod zdarzenia występujące w procesie aktualizacji
     // Zdarzenie informujące o rozpoczęciu pobierania plików
     updater.DownloadStarted +=new UpdaterActionEventHandler(OnUpdaterDownloadStarted);
     // Zdarzenie informujące o pomyślnej walidacji plików
     updater.FilesValidated +=new UpdaterActionEventHandler(OnUpdaterFilesValidated);
     // Zdarzenie informujące o nowej wersji programu na serwerze
     updater.UpdateAvailable +=new UpdaterActionEventHandler(OnUpdaterUpdateAvailable);
     // Zdarzenie informujące o zakończeniu pobierania plików
     updater.DownloadCompleted +=new UpdaterActionEventHandler(OnUpdaterDownloadCompleted);
     // Proces aktualizacji będzie wykonywany w osobnym wątku.
     // Chodzi o to, żeby interfejs się nie "zamroził".
     updaterThread = new Thread(newThreadStart(updater.StartUpdater));
     updaterThread.Start();
     // Ustawiamy informację na temat bieżącej wersji programu.
versionLabel.Text = "Bieżąca wersja: " +ConfigurationSettings.AppSettings["version"];
     Text = "SelfUpdater " + ConfigurationSettings.AppSettings["version"];
}

Obsługa każdego zdarzenia wywołanego przez wątek updaterThread musi być rozbita na dwa etapy. W pierwszym z nich - np. OnUpdaterDownloadStarted - zdarzenie jest obsługiwane bezpośrednio przez wątek updaterThread. W drugim etapie - np. OnUpdaterDownloadStartedHandler - zdarzenie obsługiwane jest przez wątek, który stworzył kontrolkę. W naszym przypadku jest to główny wątek aplikacji. Musimy tak postąpić ze względu na to, że kontrolki w Win Forms mogą być obsługiwane TYLKO przez wątek, który je stworzył. Dlatego też właściwą obsługę zdarzenia musimy oddelegować do głównego wątku. Czynność tą wykonujemy przy pomocy metody Invoke klasy Control. Drugi etap nie jest konieczny, jeżeli w obsłudze zdarzenia nie zmieniamy stanu kontrolki.

// Obsługa zdarzenia powiadamiającego o rozpoczęciu ściągania plików.
// Obsługa zmienia stan formularza, tak więc musi być wykonana przez wątek,
// który stworzył formularz
private void OnUpdaterDownloadStartedHandler( object sender, UpdaterActionEventArgs e )
{          
     logTextBox.Text += "Rozpoczęto pobieranie plików." + Environment.NewLine;
}
          
// "Wskaźnik" na funkcję przyjmującą dwa argumenty.
// Taką postać będą miały funkcje obsługujące zdarzenia
// pochodzące z procesu aktualizacji
private delegate void MarshalEventDelegate(object sender, UpdaterActionEventArgs e);      
     
// Obsługa zdarzenia powiadamiającego o rozpoczęciu ściągania plików.
// Ten etap obsługi zdarzenia jest wykonywany przez wątek updateThread
// a co za tym idzie nie można w tej metodzie zmieniać stanu formularza.
private void OnUpdaterDownloadStarted( object sender, UpdaterActionEventArgs e )
{
     // Przekazanie obsługi do głównego wątku aplikacji.
     Invoke(new MarshalEventDelegate(OnUpdaterDownloadStartedHandler), new object[]{sender, e});
}

Podsumowanie

Główną zaletą przedstawionego przeze mnie rozwiązania jest to, iż jest ono gotowe do użycia i nie wymaga pisania kodu, ponieważ cała konfiguracja odbywa się przy pomocy plików XML. Co więcej, Microsoft Application Blocks for .NET jest dostarczany z kodem źródłowym, tak więc każdy może go modyfikować wedle potrzeb. Ja zmieniłem ścieżki bezwzględne na względne, ale ktoś inny może zaimplementować własnego Validatora, własny Post-processing itd.

Microsoft Application Blocks for .NET nie jest pozbawione wad. Przykładowo, można byłoby zrezygnować z pewnych tagów w plikach konfiguracyjnych i odczytywać potrzebne informacje bezpośrednio z pakietu (mechanizm refleksji). Przydatne byłoby to np. w przypadku określania wersji aplikacji.

Dla tych, których ten artykuł raczej zniechęcił do zawierania mechanizmu aktualizacji we własnych aplikacjach, mam dobra wiadomość. W Visual Studio 2004 (Whidbey) będzie dostępna technologia o nazwie ClickOnce. Dzięki niej będzie można przy pomocy kreatora skonfigurować cały proces aktualizacji wraz z przeniesieniem plików aplikacji na serwer.

Paweł Pabich

Załączniki:

Podobne artykuły

Komentarze 6

User 79543
User 79543
30 pkt.
Poczatkujacy
21-01-2010
oceń pozytywnie 0
Trochę błędów, ale artykuł ciekawy.
User 79130
User 79130
9 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
rysunki poszły w kosmos?
User 79453
User 79453
2 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
Bardzo ciekawy temat i dużo własnej pracy (nie jest to jedynie tłumaczenie dokumentacji), ale artykuł bardzo nieczytelny.
ja_raz141
ja_raz141
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
praktyczny i nowatorski
cwi_mich3536
cwi_mich3536
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
dobry artykuł, tyle że (przynajmniej u mnie) jego wizualna forma pozostawia wiele do życzenia (tu do adminów CG: 800x600, ie6, 24b i tekst nie mieści mi się na stronie a rysunki.. ! proszę się postarać;)
m_i_k_e
m_i_k_e
0 pkt.
Nowicjusz
21-01-2010
oceń pozytywnie 0
No wreszcie cos ciekawego i potrzebnego.
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.