📜 ⬆️ ⬇️

Program copy counter or usage statistics collection

Some time ago I carried out an order for one office. The essence of the project is not important now (it was a certain appendage to their corporate system, which they put on computers to their clients), one of the requirements was that the application sent a report on its use. And simply speaking, the guys wanted to know how much their program is in demand among customers. And on this wave, I had a question - but really, you wrote the program, gave it | sold it in good hands, or simply put it on the Internet. So, what is next? How many real users have seen it?

If the program is sold, then the number of buyers can be easily calculated by the sold licenses or keys - this is someone like that. But if it is free, then there will be problems. Count the number of downloads from offsite (if any) is meaningless, because If something hits the Internet, it will begin to multiply and crawl along wares resources and torrent trackers.

The solution attached here gives a not-so-detailed report, and does not collect as much data (I simply did not need it), such as Software Statistics Service (but it, if it does not change me, is also paid). But there is a plus: the system can always be finished to its own need.

I do not pretend to originality, probably something like that already exists, I didn’t particularly look for it. I tried to write in detail, it even became like a student's term paper. In general, perhaps someone will come in handy and it will be interesting.
')

Individuality


So, our program has spread all over the world, and now we want to know how global it is. First you need to decide how we will distinguish one copy from another. The first thing that comes to mind is a GUID. For example, generate it when installing or when you first start the application, and save it to a file or to the registry. The advantage of this option is simplicity, but the downside is that the GUID every time you start the installer will be new. Those. If the poor fellow should reinstall the system (or, even easier, our program), we will get an overestimated result. Such a solution is more suitable for counting the number of program installations, rather than the actual number of users, but it is quite acceptable to use it for a rough estimate. A GUID can be obtained, for example, like this:

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

A more accurate result will be given by linking a copy of the program to the computer hardware. But, here we must keep in mind that when upgrading (for example, replacing the HDD) HardwareID will also change. Therefore, it is better to take as a data source what lasts the longest. This may be a motherboard or processor, since they tend to change when the computer is completely replaced. Below is the code that extracts the CPU ID and MotherBoard ID, and calculates the md5 hash from them.

The result can already be used for identification.

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; } 


The method has added another Volume Serial hard disk, because in virtual environments, CpuID and the serial number of the motherboard may not be detected (I, at least, flew zeroes or exceptions).

For information about hardware, WMI is used. Do not forget to connect the corresponding namespace:

 using System.Management; 

And in References add the WMI assembly of the same name contains a huge number of classes that allow you to pull out almost any information about the system, so if you want, you can expand the list.

How it works


Now it's time to set the algorithm of work, and determine what data will take part in the life cycle of our system. In order not to complicate the decision for data exchange we will use the HTTP protocol. After all, WEB is the most common service on the Internet, many now have rented or own hosting, so why not expand its functionality? Therefore, we will not reinvent the wheel, but just sit on it and go.
I was not lazy, and drew a scheme (forgive me UML experts). As mentioned above, the client and server communicate via HTTP, the client collects the necessary data, and in a POST request sends it to the server.

image

The server is ultimately a PHP script, the task of which is to receive data from the client, check them for validity, and if everything is fine, then save this data in the database, finally, inform the client about the result of the operation performed. We will determine the following server responses:


All other server responses will be perceived as an error. In our case, the client is a software module located inside the application, statistics about which we will collect. Here is the data that the client sends:


That was enough for me, but everything is easily expandable.
In addition to the data transmitted by the client, the server also registers the ip address from which the data came and the date.

Customer


I wrapped all the client code in a class and called it AppCopy . Working with him is extremely simple, consider the sequence diagram:

image

Here APP is our program, statistics about which we collect. First, a representative of the AppCopy class is created, and all necessary parameters are passed.
Then you need to call the Registration method, and the class will start doing its thing. Upon completion, it will initiate the OnRegistrationComplite event, to which the result of the work will be transmitted. Next, the client decides if the registration is successful, then there is no more need for it, if unsuccessfully, then obviously you need to try again, for example, the next time you start the program. It all depends on the implementation and the result we want to achieve.

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; } } 


I will explain a little. Working with the network is considered to be “long-playing”, if the connection is poor (or even without the Internet), the application can “hang”, and the user will press the red cross, or even three buttons. Therefore, the code working with the web server, I made a separate thread. Now the interaction with the server will not be noticeable, and will not affect the operation of the main program. The code is well commented, so I will not linger on it, I will show only an example of how to work with it.

Example
  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; } 


The GetOsVersion () function works in the same way as GetHID (), it returns not the version, but rather the name of the OS in the form of “Windows 10 Corporate ...”. You can go a simpler way, and just pull out the OS version via .NET:

 Environment.OSVersion 

But, this option seemed to me less informative.

Server


So, our program has collected all the necessary information and sent it to the server. Now, we have to take this data, check it and, if successful, save it in the database in the proper form. This will be the server. In addition, the server should be able to give the collected information, and generate reports in a convenient form.

But first, let's look into the database. Our database consists of three tables: copies , apps, and sum .

image

The copies table includes 6 fields, the meaning of which is clear from the title. I will just note that date and ip client is not transmitted, the server script receives them, appid is the program identifier, is an external (FOREIGN KEY) key and forms a link to the apps table. The latter has three fields: the already known appid , here it is the primary key, appname and appver , respectively, the name and version of the program. Our system will count only the program name and version of which is listed in this table. And finally, sum - this is the summary table, the resulting statistics will be stored here. The table has two fields appid and sum . It is in the sum field that the total number of registrations is stored.

A trigger (tg_new_copy) is attached to the copies table, which is triggered after adding (AFTER INSERT) new data (when registering the next copy). It increases the value of the sum field in the sum table by one. Naturally, the change in sum is only that whose appid corresponds to appname and appver of the program being registered.

There is a trigger in the apps table (tg_new_app), which also works after adding data. The task of this trigger is to initialize the sum table.

The logic of work is obtained as follows. Let, we have the application "Program1", version "1.0.0.0". To let the system know about this program, and start registering it, the first thing to do is add appname and appver to the apps table. Here is another scheme:

image

When a new record is added, it is automatically assigned appid (in the diagram above - 1), then the trigger tg_new_app is triggered , which in turn adds a new record sum = 0 with the appid obtained earlier to the table sum .

Now everything is ready to collect data about our program. After adding a new row to the copies table, the tg_new_copy trigger increases the sum field in the table of the same name by one

image

I think enough circuits, let's do programming. First you need to prepare our database: create tables and triggers, define relationships. To do this, I made a separate script, which will need to be run only once when the system is deployed. Database connection parameters, table names, etc. I rendered it in a separate file, because They will be needed in several scripts.

config.php
 <?php //     //    $db_name = 'db_regapps'; //  $db_login = 'db_regapps'; //  $db_pass = '12345678'; //  $db_host = 'localhost'; //  $apps_table_name = 'tb_apps'; $stat_table_name = 'tb_copies'; $sum_table_name = 'tb_sum'; 


Now the database setup script:

setup.php
 <?php require 'config.php'; //    $db_handle = new mysqli($db_host, $db_login, $db_pass, $db_name); echo "   : <b> "; if ($db_handle->connect_errno) die(" </b> ($db_handle->connect_error)"); else echo " </b>"; // ===  === echo "<br>  $apps_table_name: <b>"; //     (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 "<br>  $stat_table_name: <b>"; //    $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 "<br>  $sum_table_name: <b>"; //   $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 "<br>  tg_new_app: <b>"; $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 "<br>  tg_new_copy: <b>"; $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(" </b> ($db_handle->error)"); else echo " </b>"; } 


I turned the main code into the appstat class:

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; } } 


Here the code is also well commented, so I will not dwell on it.

How to work with it


I will show you how to work with all this. First, you need to register the program, for this you can use the form shown below:

appform.html
 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <titl>  </titl> </head> <body> <form action="http://test.info/regapp.php?Action=AddApp" method="post"> <label>AppName</label> <br> <input type="text" name="AppName" ><br> <br> <label>AppVersion</label><br> <input type="text" name="AppVersion" ><br> <br> <button type="submit" >Send</button> </form> </body> </html> 

The data flies to the regapp.php script in the POST method, and the Action parameter is passed to it, which tells the script what we want from it. The parameter is passed by the GET method.

regapp.php
 <?php require 'config.php'; require 'appstat.php'; //    $app_stat = new appstat($db_name, $db_host, $db_login, $db_pass, $apps_table_name, $stat_table_name, $sum_table_name ); if ($app_stat->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
 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>  </title> </head> <body> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title></title> </head> <body> <form action="http://test.info/regapp.php?Action=AddCopy" method="post"> <label>AppName</label> <br> <input type="text" name="AppName" ><br> <br> <label>AppVersion</label><br> <input type="text" name="AppVersion" ><br> <br> <label>MachineID</label><br> <input type="text" name="MachineID" ><br> <br> <label>OsVersion</label><br> <input type="text" name="OsVersion" ><br> <br> <button type="submit" >Send</button> </form> </body> </html> </body> </html> 

image

, APP_NOT_EXIST . , :

image

– OK

Further more. appstat . :

showstat.php
 <?php require "config.php"; require "appstat.php"; $app_stat = new appstat($db_name, $db_host, $db_login, $db_pass, $apps_table_name, $stat_table_name, $sum_table_name); //   $result = $app_stat->get_sum_apps_list(); echo " <br>"; echo "<table border='1'><tr><th></th><th></th><th> </th></tr>"; for ($i = 0; $i < count($result); $i++) { $app = $result[$i]; echo "<tr><td>$app[appname] </td><td>$app[appver]</td><td>$app[sum]</td></tr>"; } echo "</table>"; echo "<br>"; //      echo "   <br>"; $result = $app_stat->get_copys_list('Program#1', '1.0.0.0'); echo "<table border='1'><tr><th>Machine ID</th><th> </th><th> </th><th>IP</th></tr>"; for ($i = 0; $i < count($result); $i++) { $copy = $result[$i]; echo "<tr><td>$copy[machineid]</td><td>$copy[osver]</td><td>$copy[date]</td><td>$copy[ip]</td></tr>"; } echo "</table>"; echo "<br>"; //    echo "  <br>"; $result = $app_stat->get_client_apps('666'); echo "<table border='1'><tr><th></th><th></th><th></th><th>IP</th></tr>"; for ($i = 0; $i < count($result); $i++) { $app = $result[$i]; echo "<tr><td>$app[appname]</td><td>$app[appver]</td><td>$app[date]</td><td>$app[ip]</td></tr>"; } echo "</table>"; 

:

image

, css, .. , . , , .

, . appCopy :

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

Everything! . :

image

, showstat.php:

image

, . , , :

image

. , , , , ( ), Java, Android .

Links


Source: https://habr.com/ru/post/339546/


All Articles