Informacje o woluminach logicznych w systemie z wykorzystaniem C++ i WinAPI Drukuj
Ocena użytkowników: / 0
SłabyŚwietny 
Wpisany przez Tomasz Stasiak   
środa, 10 sierpnia 2011 22:38
    W tym tutorialu postaram się pokazać, jak w C/C++ uzyskać podstawowe informacje o podłączonych do systemu woluminach logicznych. Ale od początku - co to są te woluminy logiczne? Inaczej zwane partycjami lub dyskami logicznymi są wydzielonymi obszarami fizycznego nośnika danych (takiego jak dysk twardy, pendrive, płyta CD czy DVD) i służą do przechowywania informacji. W systemie Windows (bo tego systemu ten tekst dotyczy) każdy dysk logiczny posiada przypisaną do niego literę alfabetu. Pierwsze dwie (A i B) są zarezerwowane dla dyskietek, litera C jest pierwszą dostępną dla dysków twardych i reprezentuje zwykle partycję systemową (na której jest umieszczony system). Należy uważać, żeby nie pomylić fizycznego nośnika z partycją, bo, mimo iż popularnie mówi się np. "dysk c" mowa tu o wydzielonej partycji na którymś z nośników (jeden dysk może być podzielony na wiele woluminów/partycji, każdą reprezentowaną inną literą). Także tutaj będzie pokazane, w jaki sposób można uzyskać dane o partycjach (między innymi o numerze seryjnym woluminu, nie wolno mylić z serialem HDD!).
    Ktoś może się zapytać - ale po co nam takie informacje? No cóż, mogą się przydać w wypadku, gdy chcemy zabezpieczyć swój program (aby utrudnić jego rozpowszechnianie bez licencji), lub (jak to jest wykonane np. w GG v8) użyć np. numeru seryjnego jednego z woluminów do szyfrowania danych (w tym wypadku to było szyfrowanie plików komunikatora zawierających dane użytkownika). Następne narzucające się pytanie to: co to jest to WinAPI? Jest to zbiór funkcji, stałych, zmiennych czy struktur umożliwiający działanie programu w systemie Windows. Udostępnia ono wiele nowych możliwości, często niemożliwych do osiągnięcia korzystając z elementów udostępnianych przez język.
    Tyle teorii powinno wystarczyć - czas na praktykę! Co nam będzie potrzebne:
        - Edytor (w którym będziemy pisali kod), np. Code::Blocks, Microsoft Visual Studio, Notepad++ etc.
        - Kompilator, np. MinGW
        - System Windows, dzięki któremu będziemy mogli zobaczyć efekty naszej pracy
        - Podstawowa wiedza o języku C++
        - Trochę cierpliwości (bo mam zwyczaj strasznie lać wodę :P)
    Na początku musimy określić, co nam będzie potrzebne do wykonania zadania, czyli jakich funkcji i bibliotek będziemy musieli użyć. Do uzyskania podstawowych informacji o partycji służą 2 funkcje WinAPI - GetDriveType() oraz GetVolumeInformation(). Pierwsza służy do określenia typu nośnika danych, druga udostępnia już pożądane przez nas informacje (np. nazwa woluminu, system plików czy serial). Obie funkcje są udostępnione w pliku nagłówkowym "windows.h", dodatkowo będziemy potrzebowali funkcji umożliwiających wypisanie wyniku na konsolę, czyli standardowe cout z biblioteki "iostream" oraz biblioteki string, która powinna nam ułatwić później zadanie:     
        #define ARRAYSIZE(a) (sizeof(a)/sizeof(a[0])) // Makro, dzięki któremu możemy obliczyć wielkość tablicy z elementami stałej wielkości
        #include <windows.h>
        #include <iostream>
        #include <sstream> // Przyda się później
        #include <string>
        /**
         * Główna funkcja programu
         */
         int main(void)
        {
            return 0;
        } // End main()
    
    No dobrze, mamy już szkielet programu, możemy przejść dalej - trzeba ustalić jakie zmienne będą nam potrzebne (w nich będziemy przechowywać informacje o woluminach). Jak możemy zobaczyć na stronach msdn funkcja GetDriveType() zwraca żądaną wartość, więć nie musimy jej nigdzie zapisywać (choć możemy, nikt nam przecież nie broni :)), a jako parametr przyjmuje główny katalog partycji. Nnatomiast GetVolumeInformation() zwraca kod błędu, a argumenty przyjmuje jako pointery (wskaźniki); są to kolejno (w nawiasie typ): głowny katalog partycji (char*), bufor na nazwę partycji (char*), jej wielkość (DWORD), numer seryjny (DWORD*), maksymalna długość elementu ścieżki (DWORD*), flagi systemu plików (DWORD*), bufor na nazwę systemu plików (char*), jego wielkość (DWORD).
    Dlaczego DWORD? Dlaczego nie można użyć zwykłego inta? Ma to związek z architekturą komputera PC i Asemblerem, mianowicie int (w językach wysokiego poziomu) ma MINIMUM 16 bitów, czyli 2 bajty, natomiast DWORD (skrót o double WORD, czyli podwójne słowo) ma zawsze 32 bity, czyli 4 bajty. Coś takiego jak DWORD istnieje nawet w Asemblerze, dzięki czemu możliwe jest użycie WinAPI w języku dowolnego poziomu (w c/c++ dword jest zadeklarowany następująco: typedef unsigned long DWORD;).
    Wracając do tematu: tworząc wszystkie wymienione powyżej zmienne można od razu stworzyć tablicę zawierającą listę możliwych rodzajów nośników na podstawie wartości zwracanej przez GetDriveType(), czyli (w nawiasie wartość z GetDriveType()): nieznany typ nośnika (0), katalog główny jest błędny (1), napęd wymienny (2), napęd stały (3), zdalny napęd (4), CDROM (5), dysk RAM (6).
    Tak więc dopisujemy ten kod do ciała funkcji main programu:     
        // Zmienne winapi do wyciągania dokładniejszych info o dysku
        char fileSystemName[MAX_PATH*5 + 1] = {0}; // Nazwa systemu plików
        char volumeName[MAX_PATH*5 + 1]     = {0}; // Nazwa partycji
        DWORD maxComponentLen               = 0;   // Maksymalna długość elementu ścieżki
        DWORD fileSystemFlags               = 0;   // Flagi systemu plików (tu pominięte)
        DWORD serialNumber                  = 0;   // Numer seryjny woluminu
        // Typy dysków
        std::string drive_types[] = {"nieznany typ",
                                     "katalog glowny jest bledny",
                                     "naped wymienny (np. pendrive)",
                                     "naped staly (np. dysk twardy lub dysk flash)",
                                     "zdalny naped",
                                     "CDROM",
                                     "dysk RAM"
                                    };
    
    Pozostaje jeszcze to, o co nam chodziło, czyli uzyskanie informacji o wszystkich woluminach zamontowanych w systemie (jak już wspominałem, każda partycja jest reprezentowana przez literę alfabetu). Tak więc musimy "przejść" przez wszystkie możliwe partycje, sprawdzić, czy są one prawidłowe (czyli nie jest to nieznany typ lub z błędnym katalogiem głównym) i jeśli tak, to wyświetlić o nich informacje:
    
        /// Rendering outputu
         for(char i = 'C'; i <= 'Z'; ++i) // Sprawdzanie woluminów
        {
             if(GetDriveType((to_string(i)+":/").c_str()) != 0 && GetDriveType((to_string(i)+":/").c_str()) != 1) // Jeśli wolumin istnieje
            {
                /// Wyciąganie info o woluminie
                 if ( GetVolumeInformation((to_string(i)+":/").c_str(), volumeName, ARRAYSIZE(volumeName), &serialNumber, &maxComponentLen, &fileSystemFlags, fileSystemName, ARRAYSIZE(fileSystemName)) )
                {
                    /// Dane dotyczące woluminów logicznych
                    // Etykieta woluminu
                    std::cout << "Etykieta woluminu.....................: ";
                    std::cout << volumeName << "\n";
                    // Numer seryjny
                    std::cout << "Numer seryjny woluminu................: ";
                    std::cout << serialNumber;
                    // System plików
                    std::cout << "System plikow.........................: ";
                    std::cout << fileSystemName << "\n";
                    // Maksymana długość komponentu
                    std::cout << "Maksymalna dlugosc komponentu sciezki.: ";
                    std::cout << maxComponentLen << "\n";
                    // Typ nośnika
                    std::cout << "Typ nosnika...........................: ";
                    std::cout << drive_types[GetDriveType((to_string(i)+":/").c_str())] << "\n";

                    std::cout << "\n";
                } // End if
            } // End if
        } // End for
    
    Funkcja to_string użyta tu kilkukrotnie zamienia typ char na std::string i ma następującą postać (tutaj przydaje się właśnie biblioteka sstream):     
        /**
         * Konwersja znaku (char) na std::string
         *
         * @param  const char text znak do konwersji
         * @return std::string
         */
         std::string to_string(const char text)
        {
            std::stringstream ss;
            std::string       str;
            ss << text;
            ss >> str;

            return str;
        } // End to_string()
    
    No dobrze, ktoś powie, ale co to jest niby za numer seryjny, jak jest on 10 cyfrową liczbą, a polecenie dir (które twierdzi, że też pokazuje serial woluminu) zwraca dziewięcioznakowy ciąg znaków z myślnikiem na piątej pozycji? Rzeczywiście, oba wyniki się od siebie całkowicie różnią, ale tylko na pozór. Pamiętacie, jak mówiłem, że typ DWORD ma 32 bity? Te 8 znaków z wyniku polecenia dir nieprzypadkowo ZAWSZE ma 8 znaków z zakresu od 0 do 9 lub od A do F(nie licząc separatora w postaci myślnika). Już wyjaśniam: w systemie szesnastkowym każda cyfra jest reprezentowana kolejno jako jeden z tych znaków: [0123456789ABCDEF] czyli wartośći od 0 do 15 (szesnaście cyfr). Każdą cyfrę tego systemu można zakodować na 4 bitach (0 - 0000. F - 1111), dzięki czemu 32 bity (wielkość typu DWORD) podzielone przez 4 bity (wielkość potrzebna na jedną cyfrę szesnastkową) daje właśnie te 8 znaków (myślnik pełni rolę ozdobną i pomaga w ewentualnym przepisywaniu). Aby zamienić liczbę z wartości dziesiętnej (którą dostaliśmy z funkcji GetVolumeInformation()) na wartość jak ta z polecenia dir wykorzystamy następującą funkcję:     
        /**
         * Zmiana DWORD'a (32 bity) na liczbę szesnastkową po 16 bitach rozdzieloną myślnikiem zawartą w std::string
         *
         * @param  DWORD data 32-bitowa liczba do konwersji
         * @return std::string
         */
         std::string to_string_upper_hex(DWORD data)
        {
            /// Konwersja na stringa (z równoczesną zmianą podstawy systemu liczenia)
            std::stringstream ss;
            std::string       str;
            ss << std::hex << data;
            ss >> str;
            /// Zmiana wielkości liter na wielkie
             for(unsigned short i = 0; i < str.length(); ++i)
                str[i] = toupper(str[i]);
            /// Podział na 2 połowy i rozdzielenie ich średnikiem
            str = str.substr(0, 4) + "-" + str.substr(4, 4);

            return str;
        } // End to_string_upper_hex()
    
    Musimy jeszcza zamienić linijkę odpowiedzialną za wypisanie numeru seryjnego, będzie ona miała taką postać:     
        std::cout << serialNumber << "(dec)/" << to_string_upper_hex(serialNumber) << "(hex)" << "\n";
    
    No dobrze, osiągnęliśmy już zamierzony efekt, więc czy coś jeszcze można zrobić? Skoro już rozpoczęliśmy zabawę z WinAPI, to czemu nie? Na razie program wygląda monotonnie i jednokolorowo, więc czemu by go trochę nie ożywić? Do zmiany koloru tekstu/tła (UWAGA! tło zmienia się tylko pod tekstem, nie dla całej konsoli) służy funkcja SetConsoleTextAttribute() (także udostępniana przez nagłówek "windows.h"). Przyjmuje ona 2 argumenty - uchwyt do bufora ekranu konsoli oraz atrybuty (opisane dokładniej tutaj ). Aby uzyskać wymagany uchwyt skorzystamy z funkcji GetStdHandle() która zwraca pożądany uchwyt, a jako jedyny argument wymaga podania informacji, do czego chcemy uzyskać dostęp (stdin, stdout, stderr). Jako iż chcemy zmienić kolor tekstu wyświetlanego na stdout podajemy do niej argument STD_OUTPUT_HANDLE:     
        HANDLE console_out = GetStdHandle(STD_OUTPUT_HANDLE); // Uchwyt do konsoli (aby można było zmieniać kolory)
    
    Dzięki temu uchwyt znajduje się w zmiennej console_out i możemy go już używać. Zmieńmy np. kolor etykiet na intensywny czerwony (atrybuty FOREGROUND_RED i FOREGROUND_INTENSITY), natomiast odczytane wartości na kolor zielony (atrybut FOREGROUND_GREEN):     
        /// Dane dotyczące woluminów logicznych
        // Etykieta woluminu
        SetConsoleTextAttribute(console_out, FOREGROUND_RED | FOREGROUND_INTENSITY);
        std::cout << "Etykieta woluminu.....................: ";
        SetConsoleTextAttribute(console_out, FOREGROUND_GREEN);
        std::cout << volumeName << "\n";
        // Numer seryjny
        SetConsoleTextAttribute(console_out, FOREGROUND_RED | FOREGROUND_INTENSITY);
        std::cout << "Numer seryjny woluminu................: ";
        SetConsoleTextAttribute(console_out, FOREGROUND_GREEN);
        std::cout << serialNumber << "(dec)/" << to_string_upper_hex(serialNumber) << "(hex)" << "\n";
        // System plików
        SetConsoleTextAttribute(console_out, FOREGROUND_RED | FOREGROUND_INTENSITY);
        std::cout << "System plikow.........................: ";
        SetConsoleTextAttribute(console_out, FOREGROUND_GREEN);
        std::cout << fileSystemName << "\n";
        // Maksymana długość komponentu
        SetConsoleTextAttribute(console_out, FOREGROUND_RED | FOREGROUND_INTENSITY);
        std::cout << "Maksymalna dlugosc komponentu sciezki.: ";
        SetConsoleTextAttribute(console_out, FOREGROUND_GREEN);
        std::cout << maxComponentLen << "\n";
        // Typ nośnika
        SetConsoleTextAttribute(console_out, FOREGROUND_RED | FOREGROUND_INTENSITY);
        std::cout << "Typ nosnika...........................: ";
        SetConsoleTextAttribute(console_out, FOREGROUND_GREEN);
        std::cout << drive_types[GetDriveType((to_string(i)+":/").c_str())] << "\n";
    
    Czy wszystko jest ok? Nie do końca - jeśli przyjrzymy się bliżej naszemu kodowi, okaże się, że jedyne, co się w tym fragmencie zmienia, to etykieta (np. "Numer seryjny woluminu......:") oraz wartość parametru. Mając to na względzie można stworzyć dodatkową funkcję (nazwijmy ją np. print_data()), która będzie wyświetlała te dane:     
        /**
         * Wyświetlenie kolejnych parametrów woluminu
         *
         * @param  std::string heading nagłówek do wyświetlenia
         * @param  std::string val     wartość do wyświetlenia
         * @return void
         */
         void print_data(std::string heading, std::string val)
        {
            HANDLE console_out = GetStdHandle(STD_OUTPUT_HANDLE); // Uchwyt do konsoli (aby można było zmieniać kolory)

            SetConsoleTextAttribute(console_out, FOREGROUND_RED | FOREGROUND_INTENSITY);
            std::cout << heading;
            SetConsoleTextAttribute(console_out, FOREGROUND_GREEN);
            std::cout << val << "\n";
        } // End print_data()
    
    Jak widać także zmienna HANDLE console_out została tu przeniesiona z funkcji main, gdzie nie będzie już dłużej potrzebna.     I tu pojawia się problem: napisaliśmy, że wartość do wyświetlenia ma typ std::string, a przecież zmienne mają typy std::string, char* lub DWORD! Na szczęście w C++ zostały stworzone szablony (ang. templates), dzięki którym możemy stworzyć funkcję operującą na dowolnym typie danych. Jak to będzie wyglądać? Możemy np. zmodyfikować wcześniejszą funkcję to_string(), aby konwertowała nie tylko typ char, lecz dowolny (bo konwersja dowolnego typu prostego na std::string wygląda tak samo):     
        /**
         * Konwersja danych na std::string
         *
         * @param  T data znak do konwersji
         * @return std::string
         */
        template<typename T>
         std::string to_string(T data)
        {
            std::stringstream ss;
            std::string       str;
            ss << data;
            ss >> str;

            return str;
        } // End to_string()
    
    I jej definicja:     
        /// Konwersja danych na zmienną typu string
        template<typename T>
         std::string to_string(T data);
    
    Jak widać, zmiany ograniczyły się jedynie do dodania w 2 miejscach linii "template" oraz zmiany "char text" na "T data" (przy okazji zmieniłem nazwę zmiennej, aby nie wprowadzała w błąd). Dzięki temu kod wyświetlający dane nie zawiera powtórzeń (zgodnie z regułą DRY) i wygląda przejrzyściej (dodatkowe nowe linie dodane dla przejrzystości):     
        print_data("Etykieta woluminu.....................: ",
                   to_string<char*>(volumeName));
        print_data("Numer seryjny woluminu................: ",
                   to_string<DWORD>(serialNumber) + "(dec)/" + to_string_upper_hex(serialNumber) + "(hex)");
        print_data("System plikow.........................: ",
                   to_string<char*>(fileSystemName));
        print_data("Maksymalna dlugosc komponentu sciezki.: ",
                   to_string<DWORD>(maxComponentLen));
        print_data("Typ nosnika...........................: ",
                   drive_types[GetDriveType((to_string<char>(i)+":/").c_str())] );
    
    Jak widać, kodu trochę przybyło, ale program zaczął się wyróżniać :). Jeszcze tylko dodajmy łapanie wyjątków (żeby progrm się brzydko nie wysypał):     
    // ...
         int main(void)
        {
             try
            {
            } // End try
             catch(const std::exception &e)
            {
                MessageBox(NULL, e.what(), "Podczas dzialania wystapil blad!", MB_OK);
            } // End catch
            
            return 0;
        } // End main()
    // ...
    
    Użyta tu funkcja MessageBox(), będąca także elementem WinAPI odpowiada za wyświetlenie małego okienka z komunikatem. Jej parametry to, kolejno: okno rodzica (NULL oznacza brak rodzica), treść komunikatu, tekst na pasku tytułu i dodatkowe parametry (MB_OK oznacza wyświetlenie przycisku OK). Zbierzmy teraz wszystko razem i powstanie taki kod:     
/************************************************************************************
 * Name:      hddinfo.cpp                                                           *
 * Purpose:   Extracting basic information about partitions installed in the system *
 * Author:    Tomasz Stasiak                                                        *
 * Created:   2011-08-10                                                            *
 * Copyright: Tomasz Stasiak                                                        *
 * License:   http://creativecommons.org/licenses/by/2.5/pl/                        *
 ***********************************************************************************/
#define ARRAYSIZE(a) (sizeof(a)/sizeof(a[0]))
#include <windows.h>
#include <iostream>
#include <sstream>
#include <string>

/// Konwersja danych na zmienną typu string
template<typename T>
 std::string to_string(T data);
/// Konwersja numeru seryjnego z liczby dziesiętnej na format zwracany przez np. dir
 std::string to_string_upper_hex(DWORD data);
/// Wyświetlenie pokolorowanego tekstu
 void print_data(std::string heading, std::string val);

/**
 * Główna funkcja programu
 */
 int main(void)
{
     try
    {
        // Zmienne winapi do wyciągania dokładniejszych info o dysku
        char fileSystemName[MAX_PATH*5 + 1] = {0}, volumeName[MAX_PATH*5 + 1] = {0};
        DWORD maxComponentLen = 0, fileSystemFlags = 0, serialNumber = 0;
        // Typy dysków
        std::string drive_types[] = {"nieznany typ",
                                     "katalog glowny jest bledny",
                                     "naped wymienny (np. pendrive)",
                                     "naped staly (np. dysk twardy lub dysk flash)",
                                     "zdalny naped",
                                     "CDROM",
                                     "RAM"
                                    };
        /// Rendering outputu
         for(char i = 'C'; i <= 'Z'; ++i) // Sprawdzanie woluminów
        {
             if(GetDriveType((to_string(i)+":/").c_str()) != 0 && GetDriveType((to_string(i)+":/").c_str()) != 1) // Jeśli wolumin istnieje
            {
                /// Wyciąganie info o woluminie
                 if ( GetVolumeInformation((to_string(i)+":/").c_str(), volumeName, ARRAYSIZE(volumeName), &serialNumber, &maxComponentLen, &fileSystemFlags, fileSystemName, ARRAYSIZE(fileSystemName)) )
                {
                    /// Dane dotyczące woluminów logicznych
                    print_data("Etykieta woluminu.....................: ",
                               to_string<char*>(volumeName));
                    print_data("Numer seryjny woluminu................: ",
                               to_string<DWORD>(serialNumber) + "(dec)/" + to_string_upper_hex(serialNumber) + "(hex)");
                    print_data("System plikow.........................: ",
                               to_string<char*>(fileSystemName));
                    print_data("Maksymalna dlugosc komponentu sciezki.: ",
                               to_string<DWORD>(maxComponentLen));
                    print_data("Typ nosnika...........................: ",
                               drive_types[GetDriveType((to_string<char>(i)+":/").c_str())] );

                    std::cout << "\n";
                } // End if
            } // End if
        } // End for
    } // End try
     catch(const std::exception &e)
    {
        MessageBox(NULL, e.what(), "Podczas dzialania wystapil blad!", MB_OK);
    } // End catch

    return 0;
} // End main()

/**
 * Konwersja danych na std::string
 *
 * @param  T data znak do konwersji
 * @return std::string
 */
template<typename T>
 std::string to_string(T data)
{
    std::stringstream ss;
    std::string       str;
    ss << data;
    ss >> str;

    return str;
} // End to_string()

/**
 * Zmiana DWORD'a (32 bity) na liczbę szesnastkową po 16 bitach rozdzieloną myślnikiem zawartą w std::string
 *
 * @param  DWORD  data 32-bitowa liczba do konwersji
 * @return std::string
 */
 std::string to_string_upper_hex(DWORD data)
{
    /// Konwersja na stringa (z równoczesną zmianą podstawą systemu liczenia)
    std::stringstream ss;
    std::string       str;
    ss << std::hex << data;
    ss >> str;
    /// Zmiana wielkości liter na wielkie
     for(unsigned short i = 0; i < str.length(); ++i)
        str[i] = toupper(str[i]);
    /// Podział na 2 połowy i rozdzielenie ich średnikiem
    str = str.substr(0, 4) + "-" + str.substr(4, 4);

    return str;
} // End to_string_upper_hex()

/**
 * Wyświetlenie kolejnych parametrów woluminu
 *
 * @param  std::string heading nagłówek do wyświetlenia
 * @param  std::string val     wartość do wyświetlenia
 * @return void
 */
 void print_data(std::string heading, std::string val)
{
    HANDLE console_out = GetStdHandle(STD_OUTPUT_HANDLE); // Uchwyt do konsoli (aby można było zmieniać kolory)

    SetConsoleTextAttribute(console_out, FOREGROUND_RED | FOREGROUND_INTENSITY);
    std::cout << heading;
    SetConsoleTextAttribute(console_out, FOREGROUND_GREEN);
    std::cout << val << "\n";
} // End print_data()
    
    Program powinien działać na wszystkich systemach od Windows XP (włącznie) w górę. Dlaczego nie na starszych? Winowajcą jest funkcja GetDriveType() udostępniona dopiero w tymże systemie.

    Jeszcze tylko paczka z kodem + skompilowany program.