jueves, 3 de diciembre de 2015

C# - Registrar los Megabytes copiados a extraibles USB

Voy a exponer la lógica y los recursos de código empleados por mí en un programa C# para contabilizar y totalizar los megabyte que se copian en un PC hacia dispositivos extraíbles USB.


Los requerimientos del programa:
  •  Debe detectar todas las unidades de almacenamiento USB que se conectan al pc y contabilizar los megabytes copiados.
  • Debe reportar los megabyte copiados en un archivo de valores separados por coma (csv); un archivo por cada día con al menos la siguiente información: Nombre del volumen, Hora de conexión y Megabytes copiados.
  •  Debe ejecutarse como un proceso sin interface visible.
  • Debe garantizar iniciarse cuando se inicie Windows.

El objetivo de este artículo es publicar mi idea para retroalimentarme con otras ideas.

El núcleo de esta aplicación está sustentado sobre la clase DriverDetector.cs de codeproject.com (http://www.codeproject.com/Articles/18062/Detecting-USB-Drive-Removal-in-a-C-Program). 

Esta clase detecta el mensaje WM_DEVICECHANGE que Windows envía a todas las aplicaciones, este mensajes es enviado por Windows cuando ocurre un cambio en el hardware de la pc; adicionalmente se envía con el mensaje el parámetro WParam que contiene el código especifico del cambio ocurrido. Los valores que interesan de WParam son: 

DBT_DEVICEARRIVAL: es enviado cuando se conecta un dispositivo USB.
DBT_DEVICEQUERYREMOVE: se envía cuando se solicita la extracción del dispositivo.
DBT_DEVICEREMOVECOMPLETE: se envía cuando el dispositivo es removido completamente.

Consecuentemente con esto la clase DriverDetector.cs nos da la posibilidad de escribir código en los procedimientos de eventos:
OnDriveArrived: Código que se ejecutara cuando se conecte un dispositivo USB.
OnQueryRemove: Código a ejecutar antes de remover el dispositivo USB.
OnDriveRemove: Código para cuando se extraiga completamente el dispositivo.

Con estos elementos el trabajo consiste en codificar la siguiente lógica:
  


  • En el evento OnDriveArrived tomar el espacio libre del dispositivo y guardarlo en una variable A (mas adelante veremos que debe ser un arreglo de memoria bidimensional).
  • En el evento OnDriveRemove calcular A-A1 (el resultado es el tamaño de la informacion copiada al dispositivo), escribir el resultado en el archivo csv. (El calculo en este punto no es totalmente cierto; si se borra informacion el resultado del calculo no es exactamente la informacion copiada).

  •         En el evento OnQueryRemove volver a leer el espacio libre del dispositivo y guardarlo en otra variable A1 (debe ser en una columna del arreglo bidimensional).
      La clase DriverDetector.cs es perfecta, me quito el sombrero ante el autor, pero tiene una limitacion que por mas que intente no pude solventar.

     El asunto es el siguiente; para que window envie el submensaje (para llamarle de alguna manera) DBT_DEVICEQUERYREMOVE hay que “decirle explícitamente que lo haga” (esto incluye crear un archivo con ciertas características en la unidad entre otras cosas) de lo contrario no se envía. La clase DriverDetector.cs lo hace, pero solo para un dispositivo a la vez, de manera que no podemos controlar el evento OnQueryRemove para varios dispositivos conectados.

De modo que el punto 2 de la lógica no tenemos como resolverlo. La pregunta es:

¿Qué recurso de software tenemos para resolver esto?

Existe una clase en .NET (namespace System.IO) para monitorear cambios en un directorio y/o subdirectorios y provocar eventos para actuar ante estos cambios; la clase es FileSystemWatcher. Con esta clase se pueden inspeccionar cambios en un directorio tales como creación o eliminación de archivos.

Esta pudiera ser la solución, instanciando la clase para cada dispositivo USB detectados podemos tener el tamaño de los archivos creados (copiados) en la unidad USB. Pero esto tiene otro problema y es que cuando intentemos extraer el dispositivo Windows “nos dira” que el mismo está en uso. Lo lógico es que en el evento que ocurre antes del OnDriveRemove detengamos el monitoreo de la unidad USB por parte de la clase FileSystemWatcher, y ese evento es OnQueryRemove. De modo que estamos en las mismas.

SOLUCION

Mi solución fue desechar la escucha del mensaje DBT_DEVICEQUERYREMOVE y su correspondiente evento OnQueryRemove. En su lugar, cada 1 segundo voy a leer el espacio libre en el dispositivo y lo voy a restar a la lectura anterior, si la diferencia es positiva (mayor que cero) significa que el espacio libre va disminuyendo, por tanto hay en marcha un proceso de copia, entonces voy acumulando la diferencia en una variable (columna de un arreglo de memoria). Finalmente al extraer el dispositivo, el valor de esa variable va a ser el tamaño copiado al dispositivo USB.

Ilustrare esta solución con los “trozos” de código fundamentales.

Namespaces referenciados (independientes de los referenciados por la clase DriverDetector)

using System;
using System.Collections.Generic;
using Microsoft.Win32;
using System.Windows.Forms;
using System.Text;
using System.IO;
using System.Text;

Variables con alcance en todo el formulario

// Variables para almacenar el tamaño del disp., Bay libres y ocupados al conectar
long size0,Bay0,Ocup0;
// Variable de tipo DriveInfo (clase .NET q proporciona acceso a información sobre //una unidad
DriveInfo flash;
String NombreVolumen;
char LetraUnidad;
byte fila_matriz;

//Matrix string 26 filas x 6 columnas, 26 posibles unidades a conectar, 6 columnas
//0-Letra unidad,1-EtiquetaVolumen+Hora de conexión,2-Tamaño, 3-Byte libres al
//conectar,4-Byte Ocupados al conectar,5-Columna de apoyo al calculo,6-Byte copiados
public static string[,] usb_conectada = new string[26, 7];
string Hora_conexion;   

Procedimiento Main de program.cs




     El parámetro “install” si es pasado a la aplicación se crea una llave en el registro para garantizar la carga en cada inicio del sistema, lo contrario con el parámetro “uninstall”

      Evento Load del formulario


        Genarq es una clase del programa con métodos de apoyo a la solución del problema.

         La definición de la clase comienza así:



      Como se ve, incluye la declaración de una variable tIntervalo de tipo Timer (clase .NET para ejecutar métodos a intervalos regulares de tiempo, 1 segundo para nuestro caso).
     
       El método HeadFileCSVRW() y  AddLineFileCSV(string namefile,string linea)

       public static string HeadFileCSVRW()
        {
      string namefile = Application.StartupPath + Path.DirectorySeparatorChar.ToString() + "cusb_" + DateTime.Now.Day.ToString() + "_" + DateTime.Now.Month.ToString() + "_" + DateTime.Now.Year.ToString() + ".csv";
         if (!File.Exists(namefile))
          {
         using (FileStream fs = new FileStream(namefile, FileMode.CreateNew, FileAccess.Write,                FileShare.ReadWrite))
                {
                 using (TextWriter fila_csv = new StreamWriter(fs))
                 {
                 // LetraUnidad,Nombre+HoraConexion,Tamaño,MBL0,MBOcup,BLtmp,BCopiados
                fila_csv.WriteLine("Escrito por: Arquimides R. Ribeaux del Rio");                            fila_csv.WriteLine("Letra_Unidad,Etiqueta_Hora,GZ0,MBL_i,MB_o,MBL_Tmp,MBC");
                fila_csv.Close();
                    }
                }
            }
          return namefile;
         }


    public static void AddLineFileCSV(string namefile,string linea)
    {
  using (FileStream fs = new FileStream(namefile, FileMode.Append, FileAccess.Write, FileShare.ReadWrite))
        {
         using (TextWriter fila_csv = new StreamWriter(fs))
           {
            // LetraUnidad,Nombre+HoraConexion,Tamaño,MBL0,MBOcup,BLBorrado,BLCopiado
             fila_csv.WriteLine(linea);                        
             fila_csv.Close();
            }
         }
     }

     El primer método chequea la existencia del archivo csv, sino existe lo crea con un nombre que incluye la fecha del día y escribe las 2 primeras líneas del fichero y el segundo agrega cada linea al csv despues de extraido el dispositivo

      El procedimiento tProcess1Seg, código del evento Tick del Timer

   public static void tProcess1Seg(Object myobj, EventArgs myEA)
        {
            string lunidad;
            tIntervalo.Stop();      
            for (byte i = 0; i <= 25; i++)
            {
                if (frmMain.usb_conectada[i, 0] != null) //La letra de unidad NO es Null
                {
                  try
                  {
                    //     0 ,1, 2, 3, 4, 5 , 6  
                    // LetraUnidad,Nombre+HoraConexion,Tamaño,BL0,Ocupados,BLtmp,BCopiados
                    lunidad = frmMain.usb_conectada[i,0];
                    DriveInfo unidad = new DriveInfo(lunidad);
                    //BL al iniciar                       
                    long lec_anterior = (long)Convert.ToInt64(frmMain.usb_conectada[i,5]);  
                    //Lectura de espacio libre ahora
                    long lec_now = unidad.AvailableFreeSpace; 
                    // El espacio libre va disminuyendo por tanto estan copiando
                    if ((lec_anterior - lec_now) > 0 ) 
                    {
    long lec_tmp = (long)Convert.ToInt64(frmMain.usb_conectada[i,6])+(lec_anterior - lec_now);
    frmMain.usb_conectada[i,6] = lec_tmp.ToString();                           
                    }
                    frmMain.usb_conectada[i,5]=lec_now.ToString();                                             }
                    catch
                    {
                        tIntervalo.Start();
                        Application.DoEvents();
                        return;
                    }                                      
                }
            }                 
            tIntervalo.Enabled = true;
        }


      Procedimiento para el evento OnDriveArrived

  private void OnDriveArrived(object sender, DriveDetectorEventArgs e)
        {
         // LetraUnidad,Nombre+HoraConexion,Tamaño,BL0,Ocup0,BLBorrado,BLCopiado
         // e.Drive is the drive letter for the device which just arrived, e.g. "E:\\"                 try
            {
              flash = new DriveInfo(e.Drive);
              NombreVolumen = flash.VolumeLabel.ToString(); // obtener el nombre del volumen
              if (NombreVolumen.Length == 0)
               { NombreVolumen = "Sin_Nombre"; }
               // Obtener la primera letra de la cadena, la letra de unidad  
               LetraUnidad = e.Drive[0];   
               size0 = flash.TotalSize;
               Bay0 = flash.AvailableFreeSpace; // Espacio libre inicial
               Ocup0 = size0 - Bay0; // Espacio Ocupado inicial            
             // El ascii de letra unidad-65 para obtener la fila de la matriz           
              fila_matriz = Encoding.ASCII.GetBytes(LetraUnidad.ToString())[0];
             Hora_conexion=DateTime.Now.Hour.ToString()+":"+DateTime.Now.Minute.ToString()+":"DateTime.Now.Second.ToString();
              usb_conectada[fila_matriz - 65, 0] = e.Drive;
              usb_conectada[fila_matriz - 65, 1] = NombreVolumen + "_" + Hora_conexion;
              usb_conectada[fila_matriz - 65, 2] = size0.ToString();
              usb_conectada[fila_matriz - 65, 3] = Bay0.ToString();
              usb_conectada[fila_matriz - 65, 4] = Ocup0.ToString();
              usb_conectada[fila_matriz - 65, 5] = Bay0.ToString();
              usb_conectada[fila_matriz - 65, 6] = "0";
            }
            catch
            {
                Genarq.tIntervalo.Start();
                Application.DoEvents();
                Genarq.tIntervalo.Enabled = true;
                return;
            }

      Procedimiento para el evento OnDriveRemoved

  private void OnDriveRemoved(object sender, DriveDetectorEventArgs e)
        {        
            // Manipular unidad
            LetraUnidad = e.Drive[0];            
            fila_matriz = Encoding.ASCII.GetBytes(LetraUnidad.ToString())[0];
        
            //     0      ,           1       ,   2  , 3 ,  4    ,  5      ,    6   
            // LetraUnidad,Nombre+HoraConexion,Tamaño,BL0,Ocup,BLBorrado,BLCopiado

            long ar2, ar3, ar4, ar5, ar6;            
            ar2 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 2]); // Tamaño
            ar3 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 3]);  //BL al conectar
            ar4 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 4]); 
            ar5 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 5]);  //BL Tmp
            ar6 = (long)Convert.ToInt64(usb_conectada[fila_matriz - 65, 6]);  //B copiados

            string letra = usb_conectada[fila_matriz - 65, 0];
            string nombre = usb_conectada[fila_matriz - 65, 1];

            // Limpiar la fila de la matriz
            usb_conectada[fila_matriz - 65, 0] = null;
            usb_conectada[fila_matriz - 65, 1] = "Sin_Nombre";
            usb_conectada[fila_matriz - 65, 2] = string.Empty;
            usb_conectada[fila_matriz - 65, 3] = string.Empty;
            usb_conectada[fila_matriz - 65, 4] = string.Empty;
            usb_conectada[fila_matriz - 65, 5] = string.Empty;
            usb_conectada[fila_matriz - 65, 6] = string.Empty;
                        
            double Mb2, Mb3, Mb4, Mb5, Mb6;
            Mb2 = ((Convert.ToDouble(ar2) / 1024) / 1024) / 1024; //tamaño
            Mb3 = ((Convert.ToDouble(ar3) / 1024) / 1024); //MBL al conectar
            Mb4 = ((Convert.ToDouble(ar4) / 1024) / 1024); //MB Ocupados al conectar
            Mb5 = ((Convert.ToDouble(ar5) / 1024) / 1024); //BL Temporales
            Mb6 = ((Convert.ToDouble(ar6) / 1024) / 1024); //MB copiados

            string tam = Math.Round(Mb2, 2).ToString() + " GB";
            string mbl0 = Math.Round(Mb3, 2).ToString();
            string mbl1 = Math.Round(Mb4, 2).ToString();
            string mbl2 = Math.Round(Mb5, 2).ToString();
            string mbl3 = Math.Round(Mb6, 2).ToString();            

            string linea = letra + "," + nombre + "," + tam + "," + mbl0 + "," + mbl1 + "," + mbl2 + "," + mbl3;

    // Si pc queda encendida hasta dia sig. el proceso esta cargado de modo q no se           //ejecutar el Form_load
   // Debemos asegurarnos q escriba en el dia correcto

            string nombre_ruta_csv = Genarq.HeadFileCSVRW();

            Genarq.AddLineFileCSV(nombre_ruta_csv, linea);
        }

   
    En el siguiente enlace de Google Drive se puede descargar un fichero compactado (cusb.rar) que contiene el ejecutable cusb.exe.

        https://drive.google.com/file/d/0B1y9zYoMQe86S1djekRtV3RCZ28/view?usp=sharing

      Hasta aquí mi solución.  Al momento de terminar de redactar este artículo, me informan unos amigos (padre e hijo programadores) con los cuales compartí la idea inicial, que lograron controlar el DBT_DEVICEQUERYREMOVE e implementaron la solución utilizando directamente la función API de Windows que esta encapsulada en la clase .NET FileSystemWatcher; desarrollaran en Delphi/Pascal; para ellos el mejor lenguaje de programación que existe.