Como controlar el numero de estaciones que pueden ejecutar una aplicacion (VFP)

From codeWiki
Jump to: navigation, search

Por: VictorEspina


Contents

Introducción

Una de las preguntas mas frecuentes que surgen cuando se esta pensando en programar un sistema de protección para las licencias de una de nuestras aplicaciones es ¿como hago para limitar el nro. de estaciones que ejecutan mi aplicación en base a la cantidad de licencias que compró el cliente?


Registrar el número de licencias que el cliente compró es la parte fácil. Un archivo encriptado y problema resuelto. Mucho menos fácil es determinar el nro. de licencias que el cliente está consumiendo en un momento dado de modo que cuando se intente conectar la estación "n + 1" el sistema le indique que ha excedido el número de licencias instaladas.


El problema

Quizás la parte más complicada de determinar el número de usuarios o estaciones que están ejecutando un sistema es el hecho de determinar si la estación aun está activa o no.

Una de las primeras soluciones que intenté en su momento fue que cada estación que se conectaba al sistema creaba un registro en un DBF. De esta forma, para determinar la cantidad de licencias consumidas solo tenia que contar la cantidad de registros en la tabla.

Sin embargo esta solución pronto se enfrentó con el problema de que si una estación no se salía del sistema en forma apropiada (digamos que por la ocurrencia de un error, una falla del sistema operativo y simplemente una falla de energía), el registro no era eliminado de la tabla y se seguia contando como "activo".

El mismo problema se presentó cuando en lugar de un registro en una tabla intenté crear un archivo en una carpeta compartida... de nuevo, si la aplicación en una estación terminaba abruptamente, la licencia quedaba "en uso" y no podia ser reutilizada. Esta solución además permitía al usuario eliminar manualmente un archivo de marca y asi permitir superar el limite de licencias instalado, ya que una vez creado el archivo de marca la estación seguia funcionando correctamente aun cuando el archivo de marca fuera eliminado.


La solución

Una vez llegado a este punto, había dos problemas que resolver:

  • Como evitar que un archivo de marca pudiera ser eliminado manualmente por el usuario
  • Como lograr que si una estación terminaba abruptamente, la licencia que esté estaba consumiendo quede libre para ser utilizada por otra estación.


La solución vino a través de las funciones FCREATE() y FOPEN() de VFP. Cuando se crea un archivo con FCREATE(), VFP bloquea el archivo hasta que el mismo no sea cerrado con FCLOSE(). Si otro proceso intenta abrir el archivo para escritura con FOPEN(), la función retornara un valor negativo indicando que no fue posible abrir el archivo.

Dado que VFP bloquea el archivo, el usuario no puede eliminar el archivo manualmente ya que obtendrá el mensaje de que el archivo esta siendo utilizado en ese momento. Por otra parte, si la aplicación que creó el archivo termina abruptamente sin llegar a cerrar el mismo, VFP libera el bloqueo que tenía sobre el mismo permitiendo que el mismo pueda ser abierto para escritura por otros procesos.

Es justamente este hecho el que brinda una solución elegante al 2do problema: si un archivo de marca puede ser abierto para escritura significa que el proceso que lo creó ya no está activo y por lo tanto la licencia que este estaba consumiendo ahora puede ser usada por otra estación.


La clase VEActiveConnectionsController

La clase VEActiveConnectionsController brinda la funcionalidad básica necesaria para determinar la cantidad de conexiones activas, la cantidad de conexiones permitidas y métodos para conectar y desconectar una estación.

Cuando se conecta una estación se usa una de las licencias disponibles, mientras que cuando se desconecta se libera dicha licencia para que pueda ser usada por otra estación.

La idea es crear una instancia de la clase al inicio del programa principal y almacenarla en una variable global:

PUBLIC goACC
goACC = CREATEOBJECT("VEActiveConnectionsController")

La clase requiere de una carpeta compartida que pueda ser accesada por todas las estaciones y en la que cada usuario tenga permisos de escritura. Adicionalmente se debe definir el número máximo de conexiones disponibles, el cual se puede obtener de la forma que mas le convenga:

goACC.SharedFolder = ".\ACC"
goACC.MaxConnections = 10

Una vez configurada la instancia de la clase, todo lo que queda es intentar obtener una licencia para la estación actual. El método Connect() devuelve un valor > 0 si habia al menos una licencia disponible para ejecutar la aplicación o un valor <= 0 si ocurrió algun error. La propiedad LastError puede ser usada para saber cual fué la razón por la que no se pudo obtener una licencia para la estación actual:

IF NOT goACC.Connect()
 MESSAGEBOX(goACC.LastError)
 QUIT
ENDIF

Por último, se debe liberar la licencia antes de la finalización del programa, para permitir asi su reutilización en otra estación que desee accesar al sistema:

goACC.Disconnect()


La clase también puede ser usada dentro de un programa de administración de licencias, para determinar la cantidad de licencias en uso en un momento dado:

PUBLIC goACC
goACC = CREATEOBJECT("VEActiveConnectionsController")
goACC.SharedFolder = ".\ACC"

LOCAL nCurrentConnections
nCurrentConnections = goACC.GetActiveConnectionsCount()


Protección contra fraudes

Dado que este mecanismo depende del hecho de que la estación mantiene un archivo abierto durante la duración de su sesión dentro de la aplicación, un usuario malicioso podría desconectar el cable de red durante el tiempo suficiente como para que el sistema operativo libere la reserva que mantiene sobre el archivo creado, lo cual le permitiria al transgresor iniciar una nueva sesión de la aplicación en otra estación.


Para evitar esta situación, la clase permite configurar un proceso automatico que se encarga de verificar permanentemente el estado de la conexión con el archivo creado y, si se detecta la perdida de dicha conexión, reportarlo a la aplicación mediante un comando. Para lograr esto, se deben configurar un par de parámetros mas al inicio del programa:


  • checkConnectionEvery permite indicar la frecuencia con la que se verificara el estado de la conexión, expresado en minutos. Por ejemplo, si deseamos que se verifique el estado de la conexión cada 5 minutos, haremos:
goACC.checkConnectionEvery = 5 
  • onConnectionLost permite indicar el comando a ejecutar si se pierde la conexión con el archivo de marca. Si se esta usando VFP6, esta propiedad debe contener un comando simple; si se esta usando VFP 7 o superior, la propiedad puede almacenar un bloque de código complejo, ej:
goACC.onConnectionLost = "mainForm.quitApp()"


Una vez configuradas ambas propiedades, al momento de invocar el metodo Connect se dará inicio a la verificación automatica de la conexión; si la misma llegara a perderse, se invocaria el código indicado en onConnectionLost. Finalmente, al invocar a Disconnect la clase desactiva automaticamente la verificación automatica.

El Código

El siguiente código puede ser pegado al final del programa principal o en un archivo de procedimientos enlazado desde el programa principal:

* ACC (Active Connections Controller)
* Controlador de conexiones concurrentes
*
* Esa clase permite determinar el nro. de
* estaciones que estan ejecutando un programa
* cualquiera, y a la vez permite registrar
* la ejecución del programa actual
*
* Autor: Victor Espina
* Fecha: Noviembre 2006
*
* El presente código puede ser usado a 
* conveniencia del usuario. El autor
* no es responsable de cualquier daño 
* ocasionado por el uso de este código.
*
* Modo de uso:
*
* Al inicio del programa principal, colocar:
*
* PUBLIC goACC
* goACC = CREATEOBJECT("VEActiveConnectionsController")
* goACC.SharedFolder = ".\ACC"
* goACC.MaxConnections = 10  
*
* IF NOT goACC.Connect()
*  MESSAGEBOX(goACC.LastError)
*  QUIT
* ENDIF
*
* 
* Al final del programa principal:
*
* goACC.Disconnect()
*
*
* Marzo 2, 2012 - Victor Espina
* -----------------------------
* Se anexaron dos propiedades nuevas y un metodo a la clase, para permitir la validacion
* automatica de la validez de la conexion establecida:
*
* checkConnectionEvery: 
*  Permite indicar cada cuantos minutos la clase debera verificar si el archivo de marca
*  creado sigue siendo valido. Si se indica cero (0) no se realizara la verificacion.
*
* onConnectionLost:
*  Comando a ejecutar si el archivo de marca deja de ser valido. En VFP6 debe ser una 
*  sentencia simple, pero de VFP7 puede ser un codigo complejo.
*
* IsAlive()
*  Metodo que permite verificar si el archivo de marca aun es valido o no. 
*
DEFINE CLASS VEActiveConnectionsController AS Custom
 *
 SharedFolder = ".\"  && Ubicacion de la carpeta compartida a utilizar para crear los archivos de marca
 MaxConnections = 0   && Nro. máximo de conexiones permitidas
 WorkstationID = ""   && ID de la estacion. Si no se indica, se asume el nombre del equipo.
 MarkFileExt = "ACM"  && Extension de los archivos de marca. Si no se indica se asume .ACM
 LastError = "" 	  && Texto del ultimo error ocurrido
 checkConnectionEvery = 0  && Frecuencia (en minutos) para la verificacion del archivo de marca (0 = nunca)
 onConnectionLost = ""     && Codigo a ejecutar si la conexion con el archivo de marca se pierde
 HIDDEN nFH			  && Handle del archivo de marca correspondiente al proceso actual
 HIDDEN oTimer1       && Timer para verificacion de estado de conexion
 
 
 * Class constructor
 * Constructor de la clase
 *
 PROC Init()
  *
  THIS.WorkstationID = ALLT(LEFT(SYS(0),AT("#",SYS(0)) - 1))
  THIS.nFH = 0
  THIS.oTimer1 = CREATE("VEACCTimer")
  THIS.oTimer1.Enabled = .F.
  *
 ENDPROC


 * GetCurrentMarkFile 
 * Devuelve el nombre y ruta del archivo de marca correspondiente
 * a la estacion actual
 *
 PROC GetCurrentMarkFile()
  *
  LOCAL cMarkFile
  cMarkFile=FORCEEXT(THIS.WorkstationID,THIS.MarkFileExt)
  cMarkFile=FORCEPATH(cMarkFile,THIS.SharedFolder)
  cMarkFile=LOWER(cMarkFile)
  
  RETURN cMarkFile
  *
 ENDPROC
 
 
 * GetActiveConnectionsCount
 * Devuelve el nro. de conexiones concurrentes activas. Esto se logra
 * contanto cuantos archivos existentes en la carpeta compartida aun
 * estan bloqueados por otro proceso.
 *
 PROC GetActiveConnectionsCount()
  *
  LOCAL nActiveCount,nCount,i,cFile,nFH
  LOCAL ARRAY aFiles[1]
  nCount=ADIR(aFiles,ADDBS(THIS.SharedFolder)+"*."+THIS.MarkFileExt)
  nActiveCount = 0
  
  FOR i=1 TO nCount
   *
   * Se obtiene el nombre y ubicacion del archivo de marca a validar
   cFile=LOWER(FORCEPATH(aFiles[i,1],THIS.SharedFolder))
   
   * Se intenta abrir el archivo de marca para escritura
   nFH=FOPEN(cFile,1)
    
   * Si no se pudo abrir el archivo significa que hay un proceso activo
   * que aun lo tiene bloqueado, por lo que se cuenta como una conexion
   * activa, de lo contrario se cierra el archivo y se elimina pues 
   * corresponde a una conexion que termino anormalmente (ya que si 
   * hubiera terminado normalmente, el archivo habria sido borrado por
   * la aplicacion directamente).
   IF nFH < 0 
    nActiveCount=nActiveCount + 1
   ELSE
    FCLOSE(nFH)
    ERASE (cFile)
   ENDIF
   *
  ENDFOR
  
  
  RETURN nActiveCount
  *
 ENDPROC
 
 
 * Connect
 * Determina si hay conexiones disponibles y procede a crear
 * un archivo de marca. El metodo devuelte:
 *
 * 1  si se pudo crear la conexion
 * 0  si no hay conexiones disponibles
 * -1 la estacion ya esta conectada
 * -2 si no se pudo crear el archivo de marca
 *
 PROC Connect()
  *
  * Se determina la cantidad de conexiones activas
  LOCAL nActiveCount
  nActiveCount = THIS.GetActiveConnectionsCount()
  
  * Si no hay mas conexiones disponibles, se cancela
  * en este punto. Se utiliza >= y no solo = por razones
  * de programacion defensiva.
  IF nActiveCount >= THIS.MaxConnections
   THIS.LastError = "No hay conexiones disponibles"
   RETURN 0
  ENDIF
  
  * Si ya exite un archivo de marca para la estacion, se
  * cancela pues se asume que el programa ya esta en
  * ejecucion en la estacion
  LOCAL cMarkFile
  cMarkFile = THIS.GetCurrentMarkFile()
  IF FILE(cMarkFile)
   THIS.LastError = "Esta estación ya está conectada"
   RETURN -1
  ENDIF
  
  
  * Se crea el de marca
  THIS.nFH = FCREATE(cMarkFile)
  IF THIS.nFH < 0
   THIS.LastError = "No se pudo crear el archivo " + cMarkFile
   RETURN -2
  ENDIF
  
  * Si se indico un intervalo para verificar la conexion, se configura el timer y se inicia
  IF THIS.checkConnectionEvery > 0
   THIS.oTimer1.Set(THIS)
  ENDIF

  RETURN 1  
  *
 ENDPROC


 
 * Disconnect
 * Libera el archivo de marca correspondiente al proceso actual
 *
 PROC Disconnect()
  *
  * Si no hay un archivo de marca creado, se cancela
  IF THIS.nFH = 0
   RETURN
  ENDIF
  
  * Se cierra y elimina el archivo de marca
  LOCAL cMarkFile
  cMarkFile = THIS.GetCurrentMarkFile()
  FCLOSE(THIS.nFH)
  ERASE (cMarkFile)
  
  * Se libera el timer de verificacion
  THIS.oTimer1.Clear()
  *
 ENDPROC
 
 PROC foo
  FCLOSE(THIS.nFH)
 ENDPROC
 
 * IsAlive
 * Determina si el archivo de marca aun es valido
 *
 PROC IsAlive
  *
  * Si no hay un archivo de marca creado, se cancela
  IF THIS.nFH = 0
   RETURN .F.
  ENDIF
  
  RETURN FFLUSH(THIS.nFH)
  *
 ENDPROC
 *
ENDDEFINE


* VEACCTimer
* Timer de verificacion de conexion para VEActiveConnectionController
*
DEFINE CLASS VEACCTimer AS Timer
 *
 Target = NULL
 
 PROCEDURE Set(poTarget)
  THIS.Target = poTarget
  THIS.Interval = poTarget.checkConnectionEvery * 60 * 1000
  THIS.Enabled = .T.
 ENDPROC
 
 PROCEDURE Timer
  THIS.Enabled = .F.
 
  IF THIS.Target.IsAlive()
   THIS.Enabled = .T.
   RETURN
  ENDIF
  IF !EMPTY(THIS.Target.onConnectionLost)
   LOCAL cCmd
   cCmd = THIS.Target.onConnectionLost
   IF " 06.00" $ VERSION()
    &cCmd
   ELSE
    EXECSCRIPT(cCmd)
   ENDIF
  ENDIF
 ENDPROC
 
 PROCEDURE Clear
  THIS.Enabled = .F.
  THIS.Target = NULL  
 ENDPROC
 *
ENDDEFINE

Prueba

Si quieres probar el funcionamiento de la libreria, crea un PRG con el siguiente codigo y lo ejecutas:

CLOSE ALL
CLEAR ALL
CLEAR

SET PROC TO acc  && change ACC if needed

?"VEActiveconectionsController Test"
?
?"* Creating a public instance of VEActiveConnectionsController class..."
local o
o=CREATE("VEActiveConnectionsController")
??"Done!"

?"* Setting max connections allowed..."
o.MaxConnections = 2
??"Done!"

?"* Check available connections..."
??o.maxConnections - o.getActiveConnectionsCount()

?"* Create a connection..."
IF o.Connect() > 0
 ??"Done!"
ELSE 
 ??"ERROR: " + o.lastError
 RETURN
ENDIF

?"* Check available connections..."
??o.maxConnections - o.getActiveConnectionsCount()

?"* Release connection..."
o.Disconnect()
??"Done!"


RETURN
Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox