Счетчик копий программы или сбор статистики об использовании

Некоторое время назад выполнял я заказ для одной конторы. Суть проекта сейчас не важна (это был некий довесок к их корпоративной системе, который они ставили на компы своим клиентам), одним из требований было что бы приложение отправляло отчет об своем использовании. А попросту говоря, ребята хотели знать насколько их программа востребована среди клиентов.
И вот на этой волне, возник у меня вопрос —, а действительно, написал ты программу, отдал |продал в добрые руки или просто выложил ее в интернет. И что дальше? Сколько реальных пользователей ее увидели?

Если программа продается, то количество покупателей можно легко посчитать по проданным лицензиям или ключам — это у кого как. А вот если она бесплатная, то тут возникнут проблемы. Считать количество загрузок с офсайта (если такой вообще есть) бессмысленно, т.к. если что-то попало в интернет начнет плодиться и расползаться по варезным ресурсам да торрент-трекерам.
Прилагаемое тут решение дает не такой подробный отчет, и собирает не так много данных (мне это было просто не нужно) как например, Software Statistics Service (но она, если мне не изменяет память, и платная). Но есть и плюс: систему всегда можно допилить до своей необходимости.
Не претендую на оригинальность, наверняка что-то подобное уже есть, я особо не искал. Старался писать детально, стало даже похоже на курсовую работу студента. В общем, возможно кому-то пригодится и будет интересно.

Индивидуальность


Итак, наша программа расползлась по всему миру, и теперь мы хотим знать насколько это глобально. Сначала надо определиться, как будем отличать одну копию от другой. Первое, что приходит на ум это GUID. Например, генерировать его при установке или при первом запуске приложения, и сохранять в файл или в реестр. Плюсом такова варианта является простота, а минусом, то что GUID при каждом запуске установщика будет новый. Т.е. стоит бедолаге переустановить систему (или, того проще, нашу программу) мы получим завышенный результат. Такое решение больше подходит для подсчета количества установок программы, а не реального количества пользователей, но его вполне допустимо использовать для приблизительной оценки. GUID можно получить, например, так:

string guid = Guid.NewGuid().ToString();
Console.WriteLine("guid: {0}", guid);


Более точный результат даст привязка копии программы к аппаратному обеспечению компьютера. Но, тут надо иметь ввиду, что при апгрейде (например, замене HDD) поменяется и HardwareID. Поэтому, лучше за источник данных брать то что дольше всего «живет». Это может быть материнская плата или процессор, т.к. они как правило, меняются при полной замене компьютера. Ниже приведен код, который извлекает CPU ID и MotherBoard ID, и вычисляет из них md5-хеш. Полученный результат можно уже использовать для идентификации.

GetHID
private static string GetHID()
        {
            string CPUid = string.Empty;
            string MtbId = string.Empty;
            string DiskId = string.Empty;
            string HID = string.Empty;

            ManagementObjectSearcher mos = new ManagementObjectSearcher();
            // Процессор
            mos.Query = new ObjectQuery("Select * From Win32_processor");
            foreach (ManagementObject mo in mos.Get())
            {
                try
                {
                    CPUid = mo["ProcessorID"].ToString();
                }
                catch { }
            }
            // Материнская плата            
            mos.Query = new ObjectQuery("SELECT * FROM Win32_BaseBoard");
            foreach (ManagementObject mo in mos.Get())
            {
                try
                {
                    MtbId = mo["SerialNumber"].ToString();
                }
                catch { }
            }
            // Жесткий диск
            ManagementObject dsk = new ManagementObject(@"win32_logicaldisk.deviceid=""C:""");
            try
            {
                DiskId = dsk["VolumeSerialNumber"].ToString();
            }
            catch { }

            Byte[] Bytes = Encoding.ASCII.GetBytes(CPUid + MtbId + DiskId);
            if (Bytes.Length == 0)
                return "";
            MD5 md5 = MD5.Create();
            Byte[] HidBytes = md5.ComputeHash(Bytes);
            foreach (Byte b in HidBytes)
                HID += b.ToString("X2");
            return HID;

        }


В методе добавлен еще Volume Serial жесткого диска, т.к. в виртуальных средах CpuID и серийный номер материнской платы могут не определятся (у меня, по крайней мере, прилетали нули либо исключения)
Для получения информацию о железе используется WMI. Не забываем подключить соответствующие пространство имен:

using System.Management;


И в References добавить одноименную сборку
WMI содержит огромное количество классов, позволяющих вытащить практически любую информацию о системе, поэтому при желании можно расширить список.

Как это работает


Теперь настало время задать алгоритм работы, и определить какие данные будут принимать участие в жизненном цикле нашей системы. Дабы не усложнять решение для обмена данными будем использовать протокол HTTP. Ведь WEB — это самый распространенный сервис в сети Интернет, у многих сейчас есть арендованный или свой хостинг, так почему бы не расширить его функционал? Поэтому, не будем изобретать велосипед, а просто сядем на него и поедем.
Я не поленился, и нарисовал схему (да простят меня знатоки UML). Как говорилось выше, клиент и сервер общаются по протоколу HTTP, клиент собирает необходимые данные, и в POST запросе направляет их серверу.

image
Сервер это в конечном итоге PHP скрипт, задача которого принять данные от клиента, проверить их на валидатность, и если все нормально, то сохранить эти данные в базе данных, в завершении, сообщить клиенту о результате выполненной операции. Ответы сервера определим следующие:

  • OK— данные прошли проверку, и удачно сохранены в БД. В этом случае клиенту больше нет необходимости предпринимать попытки регистрации программы.
  • COPY_EXIST — программа с таким AppName, AppVersion и MachineID уже зарегистрирована. Это говорит о том, что в предыдущем сеансе, что-то пошло не так. Программа была зарегистрирована, но клиент не отработал ответ сервера. В этом случае, клиенту также больше нет необходимости ломиться на сервер.
  • APP_NOT_EXIST — сервер не настроен для регистрации этой программы. Смысл этого ответа станет ясен позже.


Все остальные ответы сервера будем воспринимать как ошибку.
В нашем случае клиентом выступает программный модуль, находящийся внутри приложения, статистику о котором мы будем собирать. Вот данные, которые отсылает клиент:

  • AppName, AppVer — имя и версия программы.
  • MachineID  — это тот самый уникальный идентификатор.
  • OsVer — версия операционной системы.


Для меня этого было достаточно, но все легко расширяемо.
Помимо переданных клиентом данных сервер еще регистрирует ip адрес, с которого пришли данные, и дату.

Клиент


Весь код клиента я завернул в класс, и обозвал его AppCopy. Работать с ним предельно просто, рассмотрим диаграмму последовательности:
image

Здесь APP — это наша программа, статистику о которой собираем. В первую очередь создается представитель класса AppCopy, и передаются все необходимые параметры.
Затем нужно вызвать метод Registration, и класс начнет делать свое дело. По завершении, он возбудит событие OnRegistrationComplite, куда и передастся результат работы. Далее клиент принимает решение, если регистрация прошла удачно, то больше потребности в нем нет, если неудачно, то очевидно нужно повторить попытку, например, при следующем запуске программы. Тут все зависит от реализации и результата, который мы хотим добиться.

AppCopy
class AppCopy
    {
     // Делегат на событие. Тут Sender - ссылка на объект который сгенерировал событие, 
     // ResultStatus - результат выполения операции
     public delegate void OnRegistrationRef (AppCopy Sender, RegResult ResultStatus);    
     // Событие. Генерируется при завершении регистрации
     public event OnRegistrationRef OnRegistrationComplete;                              
     // Идентификатор
     public string MachineId;
     // Имя программы              
     public string AppName;
     // Версия программы
     public string AppVersion;
     // Версия ОС
     public string OsVersion;    
     // URL скрипта регистрации
     public string RegUrl;
     // Задает количество попыток регистрации
     public int NumbersAttempts;       
     // Задает интервал между попытками (мс)
     public int AttemtsInterval;           
     // Результат выполнения 
     public enum RegResult {             
            // Ошибок нет
            Ok,                 
            // Ошибка соединения, либо переданные параметры неверны
            NetworkError,  
            // Копия с таким id уже зарегистрирована
            AlreadyExist,
            // Не было попыток регистрации.
            NoAttempts 
        };                         
     // Экземпляр класса Thread. Служит для поддержания отдельного потока выполнения   
     private Thread RegistrationThread;     
     // Результат выполения  
     public RegResult ResultStatus            
     {
          get;
          private set;
     }
     // Исходный ответ сервера. Нужно только для отладки.
     public string HttpResponsetData         
     {
          get;
          private set;
     }
        
     // Коструктор
     public AppCopy(string RegUrl, string MachineId, string AppName, string AppVersion, string OsVersion)
      {
          this.MachineId = MachineId;
          this.OsVersion = OsVersion;
          this.AppName = AppName;
          this.AppVersion = AppVersion;
          this.RegUrl = RegUrl;

        //===Значения по умолчанию ===
        NumbersAttempts = 1;    
        AttemtsInterval = 60000;
        ResultStatus = RegResult.NoAttempts;      
        // ThreadMotion - функция потока.
        RegistrationThread = new Thread(ThreadMotion);  
        }      

     // Запуск регистрации
     public void Registration()  
     {
          RegistrationThread.Start();
     }
     // Функция потока. Тут начинается основное действие
     private void ThreadMotion() 
     {
          // Делаем NumbersAttempts количество попыток
          for (int cntAttemps = 0; cntAttemps < NumbersAttempts; cntAttemps++)   
          {
              SendRegistrationData();
              // В случае успеха обрываем цикл
              if (ResultStatus == RegResult.Ok || ResultStatus == RegResult.AlreadyExist) 
                  break;
              // Иначе, приостанавливаем поток на AttemtsInterval мс.                
              Thread.Sleep(AttemtsInterval);              
          }
          // По завершении генерируется событие OnRegistrationComplete. В пареметрах передаем код результата выполнения

          OnRegistrationComplete(this, ResultStatus);     
     }

     // Метод выполняет обмен данными с web сервером. Возвращает код результата выполнения
     private RegResult SendRegistrationData()    
     {
          // Данные ключ=значение
          string postString = "MachineID=" + this.MachineId + "&AppName=" + this.AppName + "&AppVersion=" + this.AppVersion + "&OsVersion=" + this.OsVersion;
          // Преобразуем в массив байт
          byte[] postBytes = Encoding.UTF8.GetBytes(postString);
          // Готовим все необходимые инструменты             
          Stream dataStream = null;
          WebResponse response = null;
          StreamReader reader = null;
          try
            {
                // Создаем объект для отправки запросов
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(this.RegUrl);    
                // Указываем, что будет использоваться метод POST
                request.Method = "POST";       
                // Тип данных
                request.ContentType = "application/x-www-form-urlencoded";
                // Количество байт в запросе
                request.ContentLength = postBytes.Length;                         
                // Получаем поток для отправки запроса
                dataStream = request.GetRequestStream();                                   
                // "Скидываем" данные в поток
                dataStream.Write(postBytes, 0, postBytes.Length);                   
                // Объект, работающий с ответами сервера       
                response = request.GetResponse();                                          
                // Поток куда будут падать данные ответа
                dataStream = response.GetResponseStream();                               
                // Чтение данных с потока
                reader = new StreamReader(dataStream);                                      
                this.HttpResponsetData = reader.ReadToEnd();     
                /* Данные приходят в виде строки в верхнем регистре. Важными являются только "ОК" и "EXIST_ID", в зависимости от этого устанавливаем переменную ResultStatus в нужное состояние. Все остальные варианты ответа воспринимаются как ошибка. "Сырой" ответ сервера сохраняем в HttpResponsetData */
                switch (HttpResponsetData)                  
                {                           
                    // Завершено без ошибок
                    case "OK":
                        ResultStatus = RegResult.Ok;            
                        break;
                    // Копия уже зарегистрирована
                    case "COPY_EXIST":
                        ResultStatus = RegResult.AlreadyExist;   
                        break;
                    // не проходит валидатность переданные данных, APP_NOT_EXISTS, либо любой другой ответ сервера (40*, 50*)
                    default:
                        ResultStatus = RegResult.NetworkError;  
                        break;
                }
                
            }
            catch
            {
                // Любая сетевая ошибка.
                ResultStatus = RegResult.NetworkError;          
            }
            finally
            {
                //=== Освобождаем ресурсы===
                if (dataStream != null)
                    dataStream.Close();
                if (reader != null)
                    reader.Close();
                if (response != null)
                    response.Close();                
            }
            return ResultStatus;
        }        
    }


Немного поясню. Работа с сетью считается «долгоиграющей», при плохой связи (или вообще при отсутствии интернета) приложение может «подвиснуть», и пользователь нажмет на красный крест, а то и вовсе на три кнопки. Поэтому код работающий с web сервером я вынес в отдельный поток. Теперь взаимодействие с сервером будет происходит не заметно, и не влиять на работу основной программы. Код хорошо комментирован, поэтому не буду на нем задерживаться, покажу только пример как с ним работать.

Пример
 static void Main(string[] args)
        {
            // Создаем наш регистратор   
            AppCopy appCopy = new AppCopy("http://test.info/reg_url.php", GetHID(), "Program_name", "Program_ver", GetOsVersion());
            // Подписываемся на событие
            appCopy.OnRegistrationComplete += RegistrationFinish;
            appCopy.AttemtsInterval = 10;
            // Регистрация
            appCopy.Registration();
            Console.Read();
        }

        // Обработка события
 private static void RegistrationFinish(AppCopy Sender, AppCopy.RegResult ResultStatus)
      {
            Console.WriteLine("Registration result: {0} \nInformation: {1}", ResultStatus, Sender.HttpResponsetData);
       }

 private static string GetOsVersion()
      {

           ManagementObjectSearcher mos = new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem");
            string name = "";
            foreach (ManagementObject mobj in mos.Get())
            {
                try
                {
                    name = mobj.GetPropertyValue("Caption").ToString();
                }
                catch { };
            }
            return name;
        }


Функция GetOsVersion () работает аналогично GetHID (), возвращает не версию, а скорее имя ОС в виде «Windows 10 Корпоративная…». Можно пойти более простым путем, и вытащить именно версию ОС через .NET:

Environment.OSVersion


Но, мне показался такой вариант менее информативен.

Сервер


Итак, наша программа собрала всю необходимую информацию и отправила ее серверу. Теперь, перед нами задача принять эти данные, проверить, и в случае успеха сохранить в базе данных в надлежащем виде. Этим и займется сервер. Помимо этого, сервер должен уметь отдавать собранную информацию, и формировать отчеты в удобном виде.
Но сначала, разберемся в базе данных. Наша база состоит из трех таблиц: copies, apps и sum.
image
Таблица copies включает 6 полей, смысл которых понятен по названию. Отмечу только что date и ip клиентом не передается, их получает скрипт сервера, appid — идентификатор программы, является внешним (FOREIGN KEY) ключом и образует связь с таблицей apps. Последняя имеет три поля: уже известный appid, здесь это первичный ключ, appname и appver — соответственно имя и версия программы. Наша систему будет вести подсчет только той программы имя и версия которой занесенный в эту таблицу. И наконец, sum — это сводная таблица тут будет хранится результирующая статистика. Таблица имеет два поля appid и sum. Именно в поле sum хранится общее количество регистраций.
К таблице copies привязан триггер (tg_new_copy), который срабатывает после добавления (AFTER INSERT) новых данных (при регистрация очередной копии). Он увеличивает значение поля sum в таблице sum на единицу. Естественно, изменение sum происходит только то, чей appid соответствует appname и appver регистрируемой программы.
В таблице apps есть триггер (tg_new_app), который, также срабатывает после добавлений данных. Задача этого триггера инициализация таблицы sum.
Логика работы получается следующая. Пускай, у нас есть приложение «Program1», версия »1.0.0.0». Чтобы система знала о этой программы, и начала ее регистрировать первое что нужно сделать это добавить appname и appver в таблицу apps. Вот еще одна схема:
image
При добавлении новой записи ей автоматически присваивается appid (в схеме выше — 1), далее срабатывает триггер tg_new_app, который в свою очередь добавляет новую запись sum = 0 с appid, полученным ранее, в таблицу sum.
Теперь все готово для сбора данных о нашей программе. После добавлении новой строки в таблицу copies триггер tg_new_copy увеличивает поле sum в одноименной таблице на единицу
image
Думаю, хватит схем, займемся программирование. Для начала нужно подготовить нашу базу данных: создать таблицы и триггеры, определить связи. Для этого я сделал отдельный скрипт, который нужно будет запустить только один раз при разворачивании системы. Параметры подключения к БД, имена таблиц и т.п. я вынес в отдельный файл, т.к. они будут нужны в нескольких скриптах.

config.php


Теперь скрипт настройки БД:

setup.php
 ";
if ($db_handle->connect_errno)
    die("Ошибка  ($db_handle->connect_error)");
else
    echo "ОК ";

// ===Создаем необходимые таблицы===
echo "
Создание таблицы $apps_table_name: "; // Таблица с описание программ (app) $sql_query = "CREATE TABLE $apps_table_name (appid INT AUTO_INCREMENT NOT NULL PRIMARY KEY, appname VARCHAR(20) NOT NULL, appver VARCHAR(20) NOT NULL) CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB"; $result = $db_handle->query($sql_query); show_result(); echo "
Создание таблицы $stat_table_name: "; // Основная таблица регистраций $sql_query = "CREATE TABLE $stat_table_name (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, machineid VARCHAR (32) NOT NULL, osver VARCHAR (128), appid INT NOT NULL, date DATETIME, ip VARCHAR(15), FOREIGN KEY fk_stat(appid) REFERENCES $apps_table_name(appid) ON UPDATE CASCADE ON DELETE CASCADE) CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB"; $db_handle->query($sql_query); show_result(); echo "
Создание таблицы $sum_table_name: "; // Сводная таблица $sql_query = "CREATE TABLE $sum_table_name(appid INT NOT NULL , sum INT NOT NULL DEFAULT 0, FOREIGN KEY fk_sum(appid) REFERENCES $apps_table_name(appid) ON UPDATE CASCADE ON DELETE CASCADE) CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB"; $db_handle->query($sql_query); show_result(); // ===Создаем триггеры=== echo "
Создание триггера tg_new_app: "; $sql_query = "CREATE TRIGGER tg_new_app AFTER INSERT ON $apps_table_name FOR EACH ROW BEGIN INSERT INTO $sum_table_name SET appid=NEW.appid; END"; $db_handle->query($sql_query); show_result(); echo "
Создание триггера tg_new_copy: "; $sql_query = "CREATE TRIGGER tg_new_copy AFTER INSERT ON $stat_table_name FOR EACH ROW BEGIN UPDATE $sum_table_name SET sum= $sum_table_name.sum + 1 WHERE appid=NEW.appid; END"; $db_handle->query($sql_query); show_result(); $db_handle->close(); function show_result() { global $db_handle; if($db_handle->errno) die("Ошибка ($db_handle->error)"); else echo "ОК
"; }


Основной код я свернул в класс appstat:

appstat.php
class appstat
{
    private $db_handle;         // Ссылка на объект связанный с СУБД
    private $last_error;        // Информация об ошибках
    private $apps_table_name;    // Имя таблицы apps
    private $stat_table_name;   // Имя таблицы copies
    private $sum_table_name;    // Имя таблицы sum

    // Конструктор
    function __construct($db_name, $db_host, $db_login, $db_pass, $apps_table_name, $stat_table_name, $sum_table_name)
    {
        $this->apps_table_name = $apps_table_name;
        $this->stat_table_name = $stat_table_name;
        $this->sum_table_name = $sum_table_name;
        // Пытаемся подключиться к СУБД
        $this->db_handle = mysqli_connect($db_host, $db_login, $db_pass, $db_name);
        // Сохраняем результат
        $this->last_error = mysqli_connect_error();
    }

    // Метод добавляет новую программу. Если такая программа уже есть, то возвращает ее appid, иначе null
   public function add_app($app_name, $app_ver)
   {
       $app_id = $this->app_exist($app_name, $app_ver);
       if ($app_id != 0)
           return $app_id;
       // Если программы с таким именем и версией нет, то добавляем
       $sql_query = "INSERT INTO $this->apps_table_name(appname, appver) VALUES('$app_name', '$app_ver')";
       mysqli_query($this->db_handle, $sql_query);
       $this->last_error = mysqli_error($this->db_handle);
   }
    // Метод удаляет программу. В качестве параметров имя и версия программы
   public function delete_app($app_name, $app_ver)
   {
       // Проверяем, что такая программа есть, и получаем ее id
       $app_id = $this->app_exist($app_name, $app_ver);
       if ($app_id != 0) {
           $sql_query = "DELETE FROM $this->apps_table_name WHERE appid=$app_id";
           mysqli_query($this->db_handle, $sql_query);
       }
       $this->last_error = mysqli_error($this->db_handle);
   }

   // Проверка на существование программы в базе, возвращает 0 если приложения нет.
   // Иначе, возвращает appid программы в базе
   private function app_exist($app_name, $app_ver)
   {
       $sql_query = "SELECT appid FROM $this->apps_table_name WHERE appname='$app_name' AND appver='$app_ver'";
       $result = mysqli_query($this->db_handle, $sql_query);
       $this->last_error = mysqli_error($this->db_handle);
       if($result->num_rows === 0) {
           return 0;
       }
       else{
           return $result->fetch_assoc()['appid'];
       }
   }

    // Проверка на наличие копии программы в базе. app_id - идентификатор программы в таблице
// Возвращает 0, если такой копии нет, иначе возвращает id копии в таблице
    private function copy_exist ($machine_id, $app_id)
    {
        $sql_query = "SELECT id FROM $this->stat_table_name WHERE appid='$app_id' AND machineid='$machine_id'";
        $result = mysqli_query($this->db_handle, $sql_query);
        if ($result->num_rows != 0){
            return $result->fetch_assoc()['id'];
        }
        return 0;
    }

// Регистрирует новую копию программы. Возвращает ОК, если все прошло удачно, COPY_EXIST - если копия уже зарегистрирована,
// и APP_NOT_EXIXST - если о такой программе ничего не известно.
    public function add_copy($machine_id, $os_ver, $app_name, $app_ver, $ip)
    {
        // Проверяем, что такая программа зарегистрирована в базе
        $app_id = $this->app_exist($app_name, $app_ver);
        if ($app_id != 0){ // Зарегистрирована
            // Проверяем, что такой копии программы нет в базе
            if ($this->copy_exist($machine_id, $app_id) === 0){
                $sql_query = "INSERT INTO $this->stat_table_name(machineid, osver, appid, date, ip) VALUES('$machine_id', '$os_ver', $app_id, NOW(), '$ip')";
                mysqli_query($this->db_handle, $sql_query);
                $this->last_error = $this->db_handle->error;
                return "OK";
            }
            else{ // Копия уже существует
                return "COPY_EXIST";
            }
        }
        else // Приложение не зарегистрировано
            return "APP_NOT_EXIST";
    }

    // Закрывает соединение
    public function db_close()
    {
        mysqli_close($this->db_handle);
    }

    // Возвращает сводную таблицу о зарегистрированных программах в виде arr['appid', 'appname', 'appver', 'sum']
    public function get_sum_apps_list()
    {
        $arr_result = array();
        $sql_query = "SELECT $this->apps_table_name.appid, appname, appver, sum FROM $this->sum_table_name, $this->apps_table_name WHERE $this->sum_table_name.appid=$this->apps_table_name.appid";
        $result = mysqli_query($this->db_handle, $sql_query);
        // Упаковываем в массив
        while ($row = $result->fetch_array(MYSQLI_ASSOC))
        {
            $arr_result[] = $row;
        }
        $this->last_error = mysqli_error($this->db_handle);
        return $arr_result;
    }
    // Возвращает информацию о зарегистрированных копиях программы в виде arr['machineid', 'osver', 'date', 'ip']
// Принимает в качестве входных данных имя и версию программы
    public function get_copys_list($app_name, $app_ver)
    {
        $appid = $this->app_exist($app_name, $app_ver);
        if ($appid != 0)
        {
            $sql_query = "SELECT machineid, osver, date, ip FROM $this->stat_table_name WHERE appid=$appid";
            $result = mysqli_query($this->db_handle, $sql_query);
            $arr_result = array();
            while ($row = $result->fetch_array(MYSQLI_ASSOC))
            {
                $arr_result[] = $row;
            }
            return $arr_result;
        }

    }
    // Возвращает информацию о программах, которые зарегистрировал клиент в виде arr['appname', 'appver', 'date', 'ip']
// machine_id - hardware ID клиента
    public function get_client_apps($machine_id)
    {
        $sql_query = "SELECT appname, appver, date, ip FROM $this->apps_table_name JOIN $this->stat_table_name ON $this->stat_table_name.appid=$this->apps_table_name.appid WHERE machineid='$machine_id'";
        $result = mysqli_query($this->db_handle, $sql_query);
        $arr_result = array();
        while ($row = $result->fetch_array(MYSQLI_ASSOC))
        {
            $arr_result[] =$row;
        }
        return $arr_result;

    }
    // Возвращает информацию об ошибках.
    public function get_error()
    {
        return $this->last_error;
    }

}


Тут код также хорошо комментирован, поэтому останавливаться на нем не буду.

Как с этим работать


Покажу как со всем этим добром работать. Для начала, необходимо зарегистрировать программу, для этого можно воспользоваться формой, показанной ниже:

appform.html



    
    Добавление новой программы










Данные улетают скрипту regapp.php в методе POST, и туда же передается параметр Action, который указывает скрипту, что мы от него хотим. Параметр передается методом GET.

regapp.php
get_error())
    die("Ошибка подключения к СУБД ($app_stat->get_error())");

// Скрипт может делать две опрерации
switch ($_GET['Action'])
{
    // Добавление новой программы в базу
    case 'AddApp':
        // Проверка ожидаемых параметров
        if ((strlen($_POST['AppName']) == 0 || strlen($_POST['AppVersion']) == 0 )) {
            $app_stat->db_close();
            die('Не заданы все параметры');
        }
        else {

            $app_name = $_POST['AppName'];
            $app_ver = $_POST['AppVersion'];

            if ($app_stat->add_app($app_name, $app_ver) == false)
                echo "Программа $app_name ($app_ver) успешно добавлена";
            else
                echo 'Такая программа уже есть в базе';
        }
        break;
        // Регистрация программы
    case 'AddCopy':
        // Проверка ожидаемых параметров
        if (strlen($_POST['MachineID']) == 0 || strlen($_POST['AppName']) == 0 || strlen($_POST['AppVersion']) == 0 || strlen($_POST['OsVersion']) == 0) {
            $app_stat->db_close();
            die('Не заданы все параметры');
        }
        else {
            $app_name = $_POST['AppName'];          // Имя программы
            $machine_id = $_POST['MachineID'];      // HardwarID
            $app_ver = $_POST['AppVersion'];        // Версия программы
            $client_ip = $_SERVER['REMOTE_ADDR'];   // Ip адрес клиента
            $os_ver = $_POST['OsVersion'];          // Версия ОС
            // Регистрируем программу
            echo $app_stat->add_copy($machine_id, $os_ver, $app_name, $app_ver, $client_ip);
        }

        break;

}
$app_stat->db_close();


Как видно, скрипт может делать две операции — это добавлять новую программу в базу, тогда Action=AddApp, и вторая операция — это собственно регистрация копии, в этом случае Action должен быть равен AddCopy.
Особо любопытные могут воспользоваться формой, и зарегистрировать копию вручную:

copyform.html



    
    Регистрация новой копии





    
    
















image
Если вы сейчас попытаетесь это сделать, то получите ожидаемый ответ APP_NOT_EXIST. Поэтому, сначала зарегистрируем программу:
image
Теперь получаем ответ — OK
Дальше больше. В классе appstat есть три метода которые позволяют получить отчет в виде таблиц. Покажу как это сделать:

showstat.php
get_sum_apps_list();
echo "Сводная таблица
"; echo ""; for ($i = 0; $i < count($result); $i++) { $app = $result[$i]; echo ""; } echo "
ИмяВерсияЧисло регистраций
$app[appname] $app[appver]$app[sum]
"; echo "
"; // Детальная статистика по выбранной программе echo "Детальная статистика по программе
"; $result = $app_stat->get_copys_list('Program#1', '1.0.0.0'); echo ""; for ($i = 0; $i < count($result); $i++) { $copy = $result[$i]; echo ""; } echo "
Machine IDВерсия ОСДата регистрацииIP
$copy[machineid]$copy[osver]$copy[date]$copy[ip]
"; echo "
"; // Статистика по клиенту echo "Статистика по клиенту
"; $result = $app_stat->get_client_apps('666'); echo ""; for ($i = 0; $i < count($result); $i++) { $app = $result[$i]; echo ""; } echo "
ИмяВерсияДатаIP
$app[appname]$app[appver]$app[date]$app[ip]
";


Получаем такой результат:
image
Я тут не заморачивался с дизайном, css, и т.п. Пускай этим занимается тот, кто в этом хорошо разбирается. Я ставил задачу только показать, как получить данные, и продемонстрировать работу.
Ну и наконец, давайте протестируем все с живым клиентом. Вернемся в начало статьи и изменим параметры инициализации объекта appCopy на следующие:

AppCopy appCopy = new AppCopy("http://test.info/regapp.php?Action=AddCopy", GetHID(), "Program#1", "1.0.0.0", GetOsVersion());


Все! Можно запускать программу. В ответ получаем результат:
image
А теперь посмотрим, что выдаст showstat.php:
image
Как видно в таблице, приложение удачно зарегистрировалась.
Если еще раз запустить клиента, то программа выдаст сообщение, что такая копия уже есть:
image
Ну вот собственно и все. Если немного доработать, то можно, например, собирать статистику о том, как долго пользователь зависает в вашей программе (особенно актуально для игр), или переписав клиент под Java, внедрить код в Android приложение.

Ссылки


  • Generating Unique Key (Finger Print) for a Computer for Licensing Purposes
  • Что такое GUID
  • Классы HttpWebRequest и HttpWebResponse
  • Исходный код клиента
  • Исходный код сервера

© Habrahabr.ru