5 de julio de 2006

Actualizar nuestra aplicación VFP "on the fly"

Autor: Ricardo Fynn Reissig

Planteamos el siguiente escenario. Tenemos nuestra aplicación VFP que es un ejecutable llamado main.exe.

Este reside en los 40 puestos de trabajo de la red de nuestro cliente y accede a una base de datos (no importa cual) en su servidor central.

Ok, esta es un arquitectura tipo, pero que pasa cuando liberamos una nueva versión de nuestro software y hay que distribuirla a los 40 puestos de trabajo ?

Soluciones hay varias, en esta entrega les mostramos como actualizar nuestra aplicación directamente de Internet sin intervención del usuario, algo similar a como trabaja (salvando las distancias) un antivirus.

El primer truco

Lo primero que debemos hacer es partir nuestra aplicación en dos partes, una que lance a main.exe y antes controle la versión y si es necesario la actualice automáticamente. Por que esto ? simplemente por que cuando nuestro ejecutable este cargado en memoria no lo podremos sobrescribir con la nueva versión.

Una idea es renombrar a nuestro main.exe a main.bin, esta continuará siendo nuestra aplicación "camuflada" y usaremos un programa para lanzar a main.bin

También podríamos hacer de nuestra aplicación una DLL, pero no veremos este tema aquí.


Gestionando las versiones



Lo primero que tenemos que hacer es compilar nuestra aplicación con un versionado. Esto es vital para poder controlar que versión de nuestra aplicación estamos usando.

Luego compilamos normalmente nuestro proyecto y renombramos el archivo resultante de main.exe a main.bin

Controlando el número de versión



Si hacemos click derecho en el explorador de Windows al archivo main.bin veremos dentro de sus propiedades, la versión. No se olviden que continua siendo un archivo ejecutable y por eso se puede leer su cabezal y tomar estos datos.

¿Como leer estos datos desde VFP?

Para los que usan la versión 9 usen la función AGETFILEVERSION(miArray,"main.bin")
Los que que tiene VFP8 o menor, pueden usar la función GetFileVersion(miArray,"main.bin") que esta incluida en la librería FOXTOOLS.FLL que viene con el VFP.

La estrategia de actualización

Ahora que manejamos los versionados de nuestra aplicación veamos como implementar esta solución.

Hablamos de actualizar automáticamente desde Internet (vamos a ver mas adelante que pasa si no tenemos Internet) , por lo cual debemos minimizar el tráfico de información. Para eso tendremos dos archivos: la nueva versión de main.bin colgada en nuestro sitio de Internet y un archivo con formato XML llamado MainInfo.xml que tiene la siguiente estructura:

<?xml version="1.0" encoding="Windows-1252" standalone="yes" ?>
<VFPData>
  <MainInfo>
    <ver>3.5.2</ver>
    <date>2006-06-03</date>
    <url>http://www.c-dev.net/Test/update.htm</url>
    <status>00</status>
    </MainInfo>
</VFPData
  • ver: es el número de versión de nuestra aplicación que esta disponible para actualizar
  • date: es la fecha en que fue liberada
  • url: es la página web que tiene toda la historia de cambios de nuestra aplicación
  • status: es un código numérico que indica el estado de la actualización, por ejemplo 0=normal, 1=crítico, etc, sirve para que podamos tomar una descición al actualizar, lo obligo al usuario a descargar la nueva versión o puede trabajar con una versión mas antigua.
Note que status tiene doble cero ("00"), si no cuando convertimos el XML a un cursor VFP este lo tratará como lógico ("0" o "1")

Creando nuestro programa lanzador de main.bin

La idea básicamente es llamar a nuestro programa lanzador que lo llamaremos SuperMain, que este siga los siguientes pasos:
  1. Instancie la clase que tiene toda la lógica de actualización automática
  2. Cargue las propiedades de esta clase.
  3. Controle que versión esta usando el usuario actualmente de main.bin
  4. Descargue el archivo XML con información de la versión mas reciente que haya.
  5. Compare ambas versiones y si la actual es menor, comience la descarga desde Internet automáticamente del nuevo main.bin
  6. Si no hubo problemas de descarga, llamar a nuestra aplicación (main.bin)
Algunas consideraciones antes de ir al código fuente

1) Si no tenemos acceso a Internet, le podemos cambiar el parámetro de la función open del objeto XMLhttp. Esta se carga en las propiedades oVersion.cNewBinURL y oVersion.cXMLinfoURL
Por ejemplo si el archivo a actualizar reside en el servidor central de la empresa podría quedar de la siguiente manera:

oVersion.cNewBinURL = "file://Servidor/Downloads/main.bin"
oVersion.cXMLinfoURL = "file://Servidor/Downloads/MainInfo.xml"
note que cambiamos el protocolo http por el protocolo file

2) En este ejemplo cambiamos la extensión de nuestra aplicación de .EXE a .BIN por dos cosas, una, para diferenciar claramente que no se pude ejecutar directamente y la otra es para prevenir trancas en la descarga en caso de que un firewall impida descargar archivos ejecutables.
También se podría compilar nuestra aplicación principal como un archivo DLL de tipo COM server, pero no era el objetivo de esta entrega.

SuperMain.prg

LOCAL oVersion, lnNewVer, lnCurrentVer, lcCurrentVer, lnStatus

oVersion                = NEWOBJECT("ControlVer","ControlVer.prg")
oVersion.cMyBinFileName = "MAIN.bin"
oVersion.cXMLinfoUpdate = "MainInfo.XML"
oVersion.cNewBinURL     = "http://c-dev.net/Test/MAIN.bin"
oVersion.cXMLinfoURL    = "http://c-dev.net/Test/MainInfo.XML"

IF oVersion.getUpdateInfo()
  oVersion.ReadUpdateInfo()
  lnNewVer     = VAL(STRTRAN(oVersion.getNewBinInfo(),".",""))
  lnCurrentVer = VAL(STRTRAN(oVersion.getMyBinInfo(),".",""))
  lnStatus     = oVersion.getStatus()

  IF lnNewVer > lnCurrentVer
    ? "Existe una nueva versión Y su STATUS es: " + TRANSFORM(lnStatus)
    ? "Ver actual    " + oVersion.getNewBinInfo()
    ? "Nueva versión " + oVersion.getMyBinInfo()
    ? "Descargando nueva versión...."
    IF oVersion.downloadUpdate()
      ? "Actualizo correctamente"
    ELSE
      ? "ERROR. No pudo actualizar"
    ENDIF
  ELSE
    ? "No hay actualizaciones"
  ENDIF
ENDIF

IF FILE("MAIN.bin")
  RENAME MAIN.bin TO MAIN.EXE
  DO MAIN.EXE
  RENAME MAIN.EXE TO MAIN.bin
ENDIF

La lógica de actualización

Esta la escribimos en una clase llamada ControlVer.prg y el código es el siguiente:

DEFINE CLASS ControlVer AS CUSTOM

PROTECTED cMyBinVersion  AS STRING   && STRING con la versión del bin actual (myBin)
PROTECTED cNewBinVersion AS STRING   && STRING con la versión del nuevo bin (newBin)
PROTECTED dNewBinDate    AS DATE     && fecha de actualización del nuevo bin
PROTECTED nStatusUpdate  AS INTEGER  && estado de la actualización (0=NORMAL, 1=crítica)

cMyBinFileName  = ""    && nombre del archivo que buscaremos actualizar, ej: MAIN.bin
cXMLinfoUpdate  = ""    && nombre del archivo XML con información de la nueva versión
cNewBinURL      = ""    && URL del archivo a descargar, 
                        && ej: http://miservidor/downloads/MAIN.bin
cXMLinfoURL     = ""    && URL del archivo XML con INFO sobre la nueva versión a descargar, 
                        && ej: http://miservidor/downloads/udMain.XML
cURLhistoryInfo = ""    && URL con el historial de cambios del archivo MAIN

PROCEDURE INIT
  THIS.cMyBinVersion  = ""
  THIS.cNewBinVersion = ""
  THIS.dNewBinDate    = {}
  THIS.cNewBinURL     = ""
  THIS.nStatusUpdate  = 0
  SET SAFETY OFF
  SET TALK OFF
ENDPROC

*[ descargamos a nuestro disco del archivo XML
*[ con la información del bin para actualizar
*[ de esta manera minimizamos el tráfico y solo 
*[ descargamos el nuevo bin solo si es necesario
FUNCTION getUpdateInfo() AS Boolean
  LOCAL llRet
  LOCAL loXMLHTTP AS "MSXML2.XMLHTTP"
    loXMLHTTP = CREATEOBJECT("MSXML2.XMLHTTP")
  llRet = .T.

  TRY
    loXMLHTTP.OPEN("GET", THIS.cXMLinfoURL, .F.)
    loXMLHTTP.SEND()
    STRTOFILE(loXMLHTTP.responseBody,THIS.cXMLinfoUpdate)
  CATCH TO loErr
    MESSAGEBOX("ERROR en getUpdateInfo: " + loErr.MESSAGE,64,"ERROR")
    llRet = .F.
  ENDTRY
  RETURN llRet
ENDFUNC

*[ una vez que bajamos el XML lo leemos para 
*[ asignar a las propiedad de esta clase
*[ la información sobre el bin a actualizar
PROCEDURE ReadUpdateInfo
  XMLTOCURSOR(THIS.cXMLinfoUpdate,"cur_Info",512)
  SELECT cur_Info
  THIS.cNewBinVersion  = cur_Info.ver
  THIS.dNewBinDate     = cur_Info.DATE
  THIS.cURLhistoryInfo = cur_Info.url
  THIS.nStatusUpdate   = cur_Info.STATUS
  USE IN cur_Info
  ERASE (THIS.cXMLinfoUpdate)
ENDPROC

*[ Obtiene la versión del archivo main.bin actual
FUNCTION getMyBinInfo() AS STRING
  LOCAL laFile[1]
  AGETFILEVERSION(laFile, THIS.cMyBinFileName)
  THIS.cMyBinVersion = laFile[4]
  RETURN laFile[4]
ENDPROC

*[ descarga a nuestro disco el archivo a actualizar
FUNCTION downloadUpdate AS Boolean
  LOCAL llRet
  LOCAL loXMLHTTP AS "MSXML2.XMLHTTP"
  loXMLHTTP = CREATEOBJECT("MSXML2.XMLHTTP")
  llret = .T.

  TRY
    loXMLHTTP.OPEN("GET", THIS.cNewBinURL,.F.)
    loXMLHTTP.SEND()
    STRTOFILE(loXMLHTTP.responseBody, THIS.cMyBinFileName)
  CATCH TO loErr
    MESSAGEBOX("ERROR en downloadUpdate: " + loErr.MESSAGE + CHR(13) + ;
      "# " + STR(loErr.ErrorNo) + CHR(13) + ;
      "LINE: " + STR(loErr.LINENO),64,"ERROR")
    llRet = .F.
  ENDTRY
  RETURN llRet
ENDPROC

FUNCTION getNewBinInfo() AS STRING
  RETURN THIS.cNewBinVersion
ENDFUNC

FUNCTION getStatus() AS STRING
  RETURN THIS.nStatusUpdate
ENDFUNC

ENDDEFINE

1 comentario :