Mostrando entradas con la etiqueta errores. Mostrar todas las entradas
Mostrando entradas con la etiqueta errores. Mostrar todas las entradas

jueves, enero 25, 2018

Visual FoxPro 9: Cómo arreglar ciertos tipos de corrupción en librerías VCX (_Memberdata)

En Visual FoxPro las librerías pueden funcionar con ciertos tipos de corrupción sin que nos demos cuenta y sin errores visibles o funcionales, pero cuando se trabaja con Control de Código fuente y se usan las vistas de texto, como la generada por FoxBin2Prg, a veces estos errores se hacen evidentes y siempre es mejor solucionarlos.

En este artículo veremos el caso de _Memberdata y cómo arreglarlo.



_Memberdata

_Memberdata es una propiedad que guarda la capitalización de las propiedades y métodos creados. Por defecto VFP crea todas las propiedades y métodos en minúsculas, pero con la propiedad _Memberdata VFP puede recordar la capitalización que le demos a la propiedad (Ver más información en la ayuda de VFP o en la web de Microsoft sobre MemberData Extensibility)

Por ejemplo, si creamos la propiedad "ValorMaximo" con el PEM Editor (de VFPx), la propiedad _Memberdada tendrá información con esta estructura:

<VFPData>
   <memberdata name="valormaximo" display="ValorMaximo"/>
</VFPData>


Donde:
  • name contiene el nombre original en minúsculas
  • type (opcionalmente) contiene el tipo de elemento ("Property" o "Method" habitualmente)
  • display contiene el nombre capitalizado a mostrar

Propiedad "ValorMaximo" creada con el PEM Editor


A medida que se van agregando propiedades capitalizadas, se irán agregando también elementos <memberdata name="xx" display="Xx">, y llegado un momento dado este campo puede llegar a ser bastante largo (puede tener hasta unos 8 KiB de longitud)

Este es un ejemplo de propiedad _Memberdata corrupta, visto con el Notepad++ desde la vista texto generada por FoxBin2Prg de la librería foxcharts.vcx en GitHub:



Pueden observarse algunos problemas evidentes:

  1. VFPData aparece varias veces, cuando solo debería aparecer 2 veces
  2. Siendo un miembro codificado en XML, debería tener un tag <VFPData> de inicio y un tag </VFPData> final, el cual no se ve
  3. El tag <VFPData> de inicio está anidado en sí mismo varias veces
Bueno, sabemos que esto está mal y queremos solucionarlo, ¿cómo se puede verificar la lista de propiedades que debería ir? Esa parte es fácil, solamente hay que echar un vistazo en la cabecera VC2 de la clase para ver qué propiedades y métodos se han definido:

*<DefinedPropArrayMethod>
    *m: caption_assign
    *m: reset        && Resets the legend GDI+ objects
    *m: rotation_assign
    *m: _drawstring
    *m: _setup
    *m: _updatemeasures
    *m: _value_assign
    *p: backcoloralpha
    *p: forecoloralpha
    *p: format        && Specifies the input and ...
    *p: format2
    *p: isparent
    *p: ogfx
    *p: rotationcenter
    *p: _forceformat
    *p: _height
    *p: _initialized
    *p: _memberdata        && XML Metadata ...
    *p: _obrush
    *p: _ofont
    *p: _orectangle
    *p: _ostringformat
    *p: _transfcaption
    *p: _value
    *p: _vartype
    *p: _width
*</DefinedPropArrayMethod>



Mirando nuevamente la propiedad _Memberdata y su contenido, puede verse un patrón de repetición de varias propiedades, donde básicamente se repite esta parte:

_memberdata = <VFPData>
   <memberdata name="autosize" ... display="AutoSize"/>
   <memberdata name="whatsthishelpid" ... display="WhatsThisHelpID"/>
   <memberdata name="_setup" ...display="_Setup"/>
   <memberdata name="forecoloralpha" ... display="ForeColorAlpha"/>
   <memberdata name="backcoloralpha" ... display="BackColorAlpha"/>
   <memberdata name="width" ... display="Width"/>
   <memberdata name="oledragpicture" ... display="OLEDragPicture"/>

   ...
</VFPData>        && XML Metadata for customizable properties
con lo que recortando cuidadosamente el resto de repeticiones hasta el tag de finalización </VFPData>, quedaría resuelta esta parte.

Nota: Revisando esta librería encontré que tiene más secciones <VFPData> con problemas más adelante, con lo que una vez detectado el problema siempre es conveniente buscar y revisar todas las estructuras <VFPData> para asegurarse de que están correctas.


Puede pasar que alguna de las propiedades descriptas en los miembros <memberdata> esté incompleta o cortada, por ejemplo podría contener solamente la parte del "name" pero no la parte del "display".

Nuevamente, conociendo que la estructura mínima de un miembro debe contener el name y el display, simplemente se puede crear manualmente la propiedad display y ponerle un valor que seguramente va a ser bastante obvio: el nombre de la propiedad capitalizada que querramos que se muestre en el IDE de VFP.



Resumen


Una vez resueltos todos los problemas, se regenera el binario desde esta vista texto (click-derecho sobre el archivo VC2, elegir "Enviar a" -> FoxBin2Prg)

Como se ve, realmente no es difícil solucionar varios de estos problemas, pero sí es necesario dedicar un rato para analizar el problema, conocer qué es lo que estamos modificando, cuál es la estructura correcta e implementar la solución.


Hasta la próxima! :D

jueves, diciembre 28, 2017

Cannot find entry point (DLL name) in the DLL with Visual FoxPro

Este error puede ocurrir por dos motivos, hasta donde sé, actualmente.

El motivo oficial está explicado en la MSDN:
Cannot find entry point (DLL name) in the DLL with Visual FoxPro

Básicamente dice cuando se define una una función DLL se debe respetar estrictamente la capitalización de los nombres (mayúsculas/minúsculas) y los tipos de los parámetros (long/string/etc, y si son por referencia "@" o por valor)

Voy a usar el ejemplo con la función GetUserName que usa la MSDN.

Declaración original en C (siempre buscar la original):

   BOOL GetUserName(

      LPTSTR  lpBuffer,// address of name buffer
      LPDWORD  nSize // address of size of name buffer
     );
   Parameters


Declaración en Visual FoxPro:

   DECLARE GetUserName in Win32API ;
      String @mybuffer, Integer @mybuffersize


Si la función se declara de la siguiente forma, generará el error del artículo:

   DECLARE GetuserName in Win32API ;
      String @mybuffer, Integer @mybuffersize



Ahora bien, hay otro motivo que puede causar este error, y que me ha costado unas cuántas horas encontrar. Es un poco enrevesado para reproducir, pero en resumen puede pasar cuando se tiene una DLL (ej: MiLibreria.dll) en el System32 para que se puede acceder públicamente desde cualquier directorio, y una copia de la misma DLL en otro directorio dentro del ámbito del PATH de Visual FoxPro, que se active con posterioridad.

Ejemplo del caso de uso:
- Se declara una función DLL (ej: DECLARE long MiFuncion in MiLibreria.dll long p1, string @p2 )
- Se usa la función (y funciona bien)
- Se activa un PATH donde hay una copia de la misma (ej: SET PATH TO \proj\dlls ADDITIVE)
- Se intenta usar nuevamente la función => Aquí es donde puede fallar!


Algo importante a saber es que, a diferencia de los componentes tradicionales de Visual FoxPro, como forms, clases y programas, cuando se declara una función de DLL la librería se busca automáticamente en el System32 (o SysWow64), por lo cual no sirve hacer la típica prueba de ? FILE('MiLibreria.dll') para saber si VFP encuentra la librería, porque FILE() no busca en el directorio System32 (o SysWow64) a menos que explícitamente se haga un SET PATH con dicho directorio.

Como digo, esto "puede" pasar, aunque no tiene por qué hacerlo, pero lo importante es tener en cuenta este caso para que no les pase lo mismo.


Hasta la próxima! :D

martes, abril 21, 2015

Visual FoxPro - Selección de artículos y notas sobre problemas y troubleshoting para tener a mano

[2015/04/21]

Esta es una selección de artículos y notas que iré actualizando con el tiempo, ya que a medida que van saliendo nuevas versiones de Windows, aparecen nuevos problemas para las aplicaciones que usan file-sharing como FoxPro, Acces y varias otras. Las más nuevas las iré agregando arriba.


(Muy buena explicación técnica sobre cacheo en samba)
Re: Network Slow Query / Christof Wollenhaupt / 2014.12.18

http://leafe.com/archives/msg/497230


Data corruption when multiple users perform read and write operations to a shared file in the SMB2 environment
https://support.microsoft.com/en-us/kb/2028965

 .

sábado, marzo 29, 2014

PlasticSCM: ¡Houston, tenemos problemas!

[Última actualización: 12/11/2017 - Registro de la clase visualfoxpro.application.9]
Por: Fernando D. Bozzo

...Y es normal, todo no puede salir siempre bien o tal cual se pone en los tutoriales, en los artículos o en los videos. Además, quienes los hacemos, intentamos de que salga todo perfecto, haciendo pruebas previamente y equivocándonos "antes" de publicar algo, pero el problema es que al no mostrarse los posibles fallos, entonces no se puede aprender de ellos, y este artículo intenta llenar un poco ese hueco, con problemas comunes y cómo resolverlos.



1. Quiero cancelar el merge que estoy haciendo, ¡pero no sé cómo salir!


Este problema tiene 2 partes:

a) Mientras se está haciendo el merge

Cuando se hace merge de muchos archivos y no se quiere estar cancelando uno por uno 200 veces, se puede cancelar todo el proceso desde la ventana principal, para lo que es importante no intentar cerrar la ventana actual de merge (que pueden haber muchas, una tras otra), sino conmutarse a la ventana principal, pulsar el botón Cancelar y recién ahí se puede volver a la ventana de merge que quedó abierta y cancelar.

Luego se debe comprobar si hay Cambios Pendientes (no olvidar refrescar la vista) y deshacer todo, si hubiera algo.


b) Una vez hecho el merge

Si hay Cambios Pendientes (no olvidar refrescar la vista) y deshacer todo, si hubiera algo.



2. ¡No puedo hacer un Merge, sale una pantalla rara!


Ok, estamos haciendo un Merge y nos sale esto:


¿Qué pasó, si lo elegido era un archivo de texto, no un binario?

Pueden haber pasado dos cosas:

1.  Que el archivo efectivamente sea un binario:


Y si es un binario, entonces no hay otra opción que elegir quedarse con el Origen (el archivo desde donde se hace el Merge), con la Base (de donde partieron origen y Destino) o con el Destino (el archivo del workspace). Pero esto solo debe hacerse si no se trabaja con VFP o si no se usan las herramientas de FoxPro para Plastic.

También puede haber pasado que se haya configurado mal la herramienta de Merge de FoxPro en Plastic, ya que esa configuración sirve para que esta pantalla no aparezca. Por las dudas, la configuración, que está en el README.txt, es esta:


CONFIGURACIÓN DE MERGE EN PLASTICSCM:
------------------------------------------------------------------------

  • Clickear en el icono de Preferencias de PlasticSCM
  • Seleccionar "Herramientas Merge" y "agregar" esto:
    • Herramienta Merge externa: "<path-a-las-herramientas>\foxpro_plasticscm_dm.exe" "'PRESERVE_WS' '@sourcefile'"
    • Pattern: .pjx;.pjt;.vcx;.vct;.scx;.sct;.frx;.frt;.lbx;.lbt;.mnx;.mnt;.dbf;.fpt;.cdx;.dbc;.dcx;.dct     (¡usar misúsculas!)
  • Clickear OK
  • Mover la extension agregada al inicio de la lista, para priorizarla


2.  Que no esté reconociendo el archivo como texto:


En ese caso se debe cancelar el merge cerrando la ventana de la imagen anterior, cerrar la solapa e merge, Deshacer los cambios pendientes (ver el tema anterior: "Quiero cancelar el merge que estoy haciendo, ¡pero no sé cómo salir!") y cambiar el tipo de archivo a texto en la vista de Items:


Luego de solucionar esto, podremos volver a reintentar el Merge, y ahí sí que ya debería salir la ventana de merge normal, y no olvidar "saltear" los binarios que queden, como ya expliqué en el artículo "PlasticSCM: ¿Qué es el Merge".

Para más detalle, también está este video corto:





3. ¡No puedo hacer un Diff, sale un error!


Si sale esta pantalla:


Es porque Plastic no reconoció bien un archivo de texto y lo toma como tipo binario, o porque elegimos un archivo para el que no hay conversor disponible, o porque es un archivo binario normal.
Trabajando con las herramientas de FoxPro 9 esto puede pasar en 3 casos:

1.  Plastic no reconció bien un archivo de texto:


Este es el mismo caso que comenté en el punto (2) del problema anterior con el Merge, y tiene la misma solución.

2.  Elegimos un archivo para el que no hay conversor disponible:


Si el archivo es un pjx, vcx, scx, frx, lbx, mnx, dbf o dbc, el problema es que se configuró mal, o no se configuró, la opción de DIFF en las opciones de Plastic. El README.txt indica como hacerlo:

CONFIGURACIÓN DE DIFF EN PLASTICSCM:
------------------------------------------------------------------------

  • Clickear en el icono de Preferencias de PlasticSCM
  • Seleccionar "Herramientas Diff" y "agregar" esto:
    • Herramienta Diff externa: "<path-a-las-herramientas>\foxpro_plasticscm_dm.exe" "'DIFF' '@sourcefile' '@destinationfile' '@sourcesymbolic' '@destinationsymbolic'"
    • Pattern: .pjx;.vcx;.scx;.frx;.lbx;.mnx;.dbf;.dbc     (¡usar misúsculas!)
  • Clickear OK
  • Mover la extension agregada al inicio de la lista, para priorizarla

3.  Elegimos un binario normal:


En este caso hemos querido hacer Diff sobre un EXE o SCT o VCT o cualquier otro binario no soportado.




4. ¡No puedo borrar el workspace!


Cuando hay una sola solapa, será el workspace activo, y no se podrá eliminar:


Para poder borrarlo necesitamos tener un segundo workspace, que podemos crear apuntando a un directorio cualquiera (ya que no lo usaremos) o aprovechar y crear otro repo que necesitemos, lo que creará una segunda solapa que deberemos elegir.

Ahora sí, si en el panel izquierdo seleccionamos "Workspaces", podremos eliminar el workspace anterior.

Nota: Eliminar el workspace solo borra el "alias" del repositorio, no borra el repositorio ni su contenido. Esto permite poder crear un workspace distinto con un nuevo directorio para el mismo repositorio.



5. ¡Me equivoqué al crear el workspace y lo asocié a un directorio equivocado!

En este caso debemos borrar el workspace (no el repositorio!) y volverlo a crear. Si al hacerlo nos da un error como este:


Es porque es el único workspace y está activo. Ver el punto anterior "¡No puedo borrar el workspace!"




6. Error al intentar abrir el cliente Plastic (1)


Si aparece un mensaje de error como este:


Entonces hicieron mal el paso 5 o el 14 de la guía de Instalación de PlasticSCM paso a paso

Si están queriendo usar otro modo de autenticación porque lo van a usar, por ejemplo, en la oficina con autenticación de Directorio Activo u otro, entonces deben verificar que la configuración del cliente y la del servidor son iguales.

Para comprobarlo, deben ir al menú Programas / PlasticSCM y a continuación:

> Para el cliente, abren la opción: Client Tools / Client Configuration Wizard

> Para el servidor, abren la opción: Server Tools / Server Configuration Wizard




7. Error al intentar abrir el cliente Plastic (2)


Si ocurre el siguiente error, es muy probable que el servicio "Plastic Server 5" no esté levantado:


Para solucionarlo, lo primero que se puede hacer es reiniciar la PC si no se hizo al instalar Plastic, y si no también se puede abrir la consola de Servicios, verificar si está levantado, y si no lo está, iniciarlo:


Esta es la configuración que tengo por defecto para este servicio:









8. ¿Cómo se cambia el idioma?

Cuando se instala un programa, a veces no miramos bien las opciones de la pantalla y le damos "aceptar", "aceptar", "aceptar"... hasta que nos damos cuenta de que se nos pasó algo.

Plastic por suerte viene con 2 programas de configuración, uno para el cliente y otro para el servidor, que permiten configurar el idioma, el servidor, el puerto de conexión, el modo de autenticación, etc.

Para reconfigurar el cliente o el servidor, deben ir al menú Programas / PlasticSCM y a continuación:

> Para el cliente, abren la opción: Client Tools / Client Configuration Wizard

> Para el servidor, abren la opción: Server Tools / Server Configuration Wizard

En ambos casos, vayan avanzando en las pantallas hasta encontrar lo que buscan.

Recuerden que si acaban de instalar el programa, es probable que deban reiniciar la PC o probablemente les de errores al intentar ejecutar Plastic.



9. ¡No puedo descargar Plastic, da un error la página!


Aquí hay un enlace a una versión de Plastic actualizada a agosto 2014 que temporalmente va a servir (versión evaluación 30 días y 5 desarrolladores), así que luego hay que bajarse el archivo de licencia gratuita anual para 15 desarrolladores antes de que caduque:
PlasticSCM-5.0.44.596-windows-installer.exe.7z



10. Hice un checkin erróneo en un archivo, ¿cómo lo arreglo?



11. ¡Borré el directorio del workspace sin querer y se perdió todo! ¿Qué hago?


Lo que has borrado es solamente los archivos del directorio, no lo que tienen guardado en la Base de Datos, por lo que hay 2 soluciones, una rápida y otra algo menos rápida:

  • Opción 1: Si no has borrado el directorio oculto ".plastic" del directorio del workspace, entonces puedes ir al Explorador de Ramas, buscar el último changeset y ubicar la casa ahi (click-derecho -> Apuntar workspace a este changeset). Este último paso descargará de la BDD de Plastic todos los archivos en sus ubicaciones.
  • Opción 2: Si has borrado el directorio oculto ".plastic", entonces debes borrar el workspace de Plastic desde el panel izquierdo (Workspaces), volver a crearlo, asociándolo al mismo directorio (u otro), buscar el último changeset y ubicar la casa ahi (click-derecho -> Apuntar workspace a este changeset). Este último paso descargará de la BDD de Plastic todos los archivos en sus ubicaciones.




12. Quiero ver/comparar el contenido de un archivo tx2 (texto) y dice que es binario!



Cuando esto ocurre, es porque la información del tipo de archivo se perdió en algún momento, o directamente no se indicó en el archivo filetypes.conf del directorio raíz del workspace.


Para solucionarlo, vamos a la vista de Items, seleccionamos los archivos de texto que figuran como binarios, hacemos click-derecho sobre alguno de ellos y elegimos Camiar tipo de revisión -> Texto, y listo, problema resuelto.


13. No me reconoce la licencia plastic.lic

El archivo de licencia plasticd.lic que reciban, se debe copiar en el subdirectorio server dentro del directorio de instalación de Plastic (hay un directorio client y uno server), pero antes de copiarlo deben "bajar" el servicio de Plastic ("Plastic Server 5"), luego copiar al archivo y luego volver a levantar el servicio, para que reconozca la licencia.



14. No se puede crear el objeto "visualfoxpro.application.9"

Esto pasa cuando el IDE de Visual FoxPro (vfp9.exe) no está registrado en el registro de Windows. Aunque se puede usar manuelmante el IDE sin registrarlo, el no tenerlo regitrado impide poder usarlo como clase de automatización.
El arreglo es simple: Se abre una ventana de comandos como admin, y se pone esto, adaptando la ruta donde realmente se encuentre VFP9.exe:

"c:\program files (x86)\microsoft visual foxpro 9\vfp9.exe" /regserver



15. ¡No puedo borrar un changeset! ¿qué hago?


Aunque el mensaje es bastante explícito y "técnicamente" correcto, vamos a comentarlo por partes, ya que no siempre es fácil ni evidente determinar el motivo que impide borrarlo.

Etiquetas: Eso es lo más fácil de ver, porque visualmente se puede comprobar si el changeset que se quiere borrar tiene etiquetas o no (pueden haber varias etiquetas en un mismo changeset). Si las tiene, las mismas se deben borrar antes de poder borrar el changeset.

Ramas: PlasticSCM mantiene y comprueba la integridad de los changesets, por lo que si una rama depende de un changeset, el mismo no se podrá borrar hasta que se eliminen las ramas que dependen de él. Esta solución (borrar ramas) es un tanto brusca y debe usarse como último recurso siempre que realmente sea muy importante borrar un changeset y todas sus ramas dependientes. Normalmente esto solo se hace cuando se realizan pruebas con la herramienta de Control de Código, donde se crean ramas y changesets de prueba para luego eliminarlos. Al igual que las etiquetas, ver las ramas dependientes es visualmente comprobable.

Shelves: Esto ya es más complejo, porque los shelves se crean a partir de un changeset bajo la idea de poder proteger los cambios temporalmente para poder continuar con otra cosa en otra rama y luego volver a aplicarlos en el mismo sitio cuando se vuelva a trabajar en la misma rama. El problema radica en que por un lado los shelves no son visuales y por el otro en que la información del shelve no indica en qué changeset se hizo, por lo que visualmente es imposible verlo.

Una solución (algo salvaje) podría ser borrar todos los shelves guardados, y con eso se solucionaría el problema, pero hay otra solución mucho más moderada, que consiste en preguntar a Plastic qué shelves se basan en el changeset que se quiere borrar.

Por ejemplo, si se quisiera saber qué shelves se basan en el cs:4236, la consulta desde la ventana de comandos sería esta:

cm find shelves where parent = '4236' --format="{parent} # {owner} # {guid} # {date} # {comment}"

Esto listará todos los shelves que se crearon desde ese changeset.
Para evitar tener que recurrir a esto y poder resolverlo visualmente desde la vista de Shelves, es muy recomendable que antes de crear un shelve se ponga como inicio de comentario en la vista de cambios pendientes el changeset del que parte. Por ejemplo: "cs:4236 - Arreglo bug cálculo precio"



Observaciones:


De algunos de los errores comentados, también puede deducirse que a veces vamos tan a las apuradas que no prestamos atención al instalar o configurar algo, lo que luego nos trae un sinfín de problemas que en muchos casos se podían haber evitado. Es mejor dedicar el tiempo necesario a configurar bien y sobre todo a entender cómo funciona lo que estamos configurando, y no quedarse solo con los paso-a-paso, para no perder tiempo luego y dominar mejor nuestras herramientas.


Este contenido lo iré actualizando con otros problemas comunes que me vayan surgiendo o que me sugieran.


Hasta la próxima!

martes, enero 14, 2014

Desmitificando el control de errores con Try/Catch

Por: Fernando D. Bozzo

He visto en muchas ocasiones que hay como cierto miedo a tratar los errores, como si fuera algo complejo, difícil de controlar o directamente como si los errores "no fueran a ocurrir", que es peor, porque no se controlan y se confía ciegamente en la suerte.

En esta entrada voy a presentar una forma de trabajar con el Try/Catch que cumple dos objetivos distintos con una misma rutina:
  1. Poder relanzar el error para que lo trate un nivel superior
  2. Poder tratar el error localmente sin relanzarlo
Y lo mejor de todo es que este comportamiento se puede elegir "desde fuera" del procedimiento, antes de llamarlo, de modo que un mismo procedimiento se puede usar de las dos formas, dependiendo de cómo se utilice.

¿Pero qué sentido tiene poder hacer esto? ¿Para qué puede servir llamarlo de una forma u otra?


Imaginemos dos situaciones distintas:

Situación 1: Tenemos una rutina en la que si ocurre un error, queremos mostrarlo para que el usuario esté notificado y nada más

Situación 2: Tenemos un método de negocio (procedimiento sin interfaz ni mensajes que se muestren al usuario que realiza alguna operación), donde de ocurrir un error, y al no poder mostrarlo, la única salida es reenviarlo hacia el nivel anterior para que sea tratado en el origen, por ejemplo, el botón de comando que originó todo, o un programa externo.

Normalmente esto requiere dos tratamientos distintos, pero hay una solución muy simple que permite utilizar una u otra de acuerdo a lo que nos convenga, que es la siguiente:

PROCEDURE Proc_X( toEx as Exception, tlRelanzarError, otros_params )
   TRY
      toEx = NULL
      * Aquí va el proceso
 

   CATCH TO toEx
      IF tlRelanzarError
         THROW
      ENDIF

   FINALLY
      * Este IF/MESSAGE/ENDIF sólo debe ir si este es un método de Interfaz!

      IF NOT ISNULL(toEx) AND NOT tlRelanzarError
         MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
      ENDIF

      * Recolección de basura
   ENDTRY
ENDPROC




En azul y amarillo están resaltados los dos parámetros que permiten esto. Por un lado, toEx guarda la información del error y por el otro tlRelanzarError indica si se desea, o no, relanzar el error al nivel superior. No hay más! Y así de simple como se ve, es sumamente versátil.

Veamos algunos ejemplos en la práctica.


Ejemplo 1: Tratamiento de errores local

En este caso se quiere controlar el error y mostrar un mensaje al usuario. Los métodos de este tipo suelen ser de la interfaz visual, y como se ve, es parecido al ejemplo original, pero más recortado y sin la parte de relanzar:


PROCEDURE Form1.cmdEjecutar.CLICK
   TRY
      LOCAL loEx as Esception
      loEx = NULL
      * Aquí va el proceso
   CATCH TO loEx

   FINALLY
      IF NOT ISNULL(loEx)
         MESSAGEBOX( "Error " + TRANSFORM(loEx.ErrorNo) + ", " + loEx.Message )
      ENDIF

      * Recolección de basura
   ENDTRY
ENDPROC




Dentro de este mismo tipo de tratamiento tenemos otro caso, una actualización que solo actualiza un campo TIMESTAMP que se llama con un ON KEY LABEL F2 y que, por llamado con una tecla ON KEY, no permite otro tratamiento de errores que no sea local:

ON KEY LABEL F2 DO Actualiza WITH "", .F., 0

PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
   TRY
      LOCAL lnCodError
      lnCodError = 0 
      toEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO toEx
      IF tlRelanzarError
         THROW
      ENDIF
      lnCodError = toEx.ErrorNo

   FINALLY
      IF NOT ISNULL(toEx) AND NOT tlRelanzarError
         MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
      ENDIF

      USE IN (SELECT("USUARIO"))
   ENDTRY
   RETURN lnCodError
ENDPROC



Como se ve, desde el DO Actualiza se está indicando .F. para tlRelanzarError, por lo que en el FINALLY, si ocurrió un error, se mostrará el mensaje con sus datos. Además en el tercer parámetro se pasa 0 porque no se podrá leer su valor.




Ejemplo 2: Tratamiento de errores en nivel superior

En este caso vamos a usar los ejemplos anteriores, pero encadenados de tal forma que el segundo (la actualización) cambia su forma de trabajar para devolver el error y dejar que que sea el nivel superior quien muestre los datos del mismo:


PROCEDURE Form1.cmdEjecutar.CLICK
   TRY
      LOCAL loEx as Esception, lnCount
      loEx = NULL
      DO Actualiza WITH "", .T., lnCount
      IF lnCount = 0
         MESSAGEBOX( "No se actualizaron registros!" )
      ENDIF

   CATCH TO loEx

   FINALLY
      IF NOT ISNULL(loEx)
         MESSAGEBOX( "Error " + TRANSFORM(loEx.ErrorNo) + ", " + loEx.Message )
      ENDIF

      * Recolección de basura
   ENDTRY
ENDPROC


PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
   TRY
      LOCAL lnCodError
      lnCodError = 0 
      toEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO toEx
      IF tlRelanzarError
         THROW
      ENDIF
      lnCodError = toEx.ErrorNo

   FINALLY
      IF NOT ISNULL(toEx) AND NOT tlRelanzarError
         MESSAGEBOX( "Error " + TRANSFORM(toEx.ErrorNo) + ", " + toEx.Message )
      ENDIF

      USE IN (SELECT("USUARIO"))
   ENDTRY
   RETURN lnCodError
ENDPROC



En este caso, si ocurriera un error en la rutina "Actualiza" se relanza el error (THROW) y se haría la recolección de basura (cerrar la tabla, limpiar vars, etc) tanto haya error o no.
El Throw salta hasta el CATCH del nivel anterior, que es el cmdEjecutar.Click() y recién ahí es donde se mostraría.

Así es como la misma rutina "Actualiza" se puede reutilizar controlando el error de dos formas distintas.


Control de errores compacto


Habiendo visto los casos de uso anteriores surgen varias ideas y optimizaciones para casos más concretos, por ejemplo, si sabemos de antemano que un método jamás mostrará un error porque es de negocio o para un proceso batch, entonces la estructura podría cambiar a esto:

PROCEDURE Actualiza( toEx as Exception, tlRelanzarError, tnCount )
   TRY
      LOCAL lnCodError
      lnCodError = 0 
      toEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO toEx
      IF tlRelanzarError
         THROW
      ENDIF
      lnCodError = toEx.ErrorNo

   FINALLY
      USE IN (SELECT("USUARIO")) 

   ENDTRY

   RETURN lnCodError
ENDPROC




Lo que sigue dejando 2 opciones:
  1. Llamar al método Actualiza relanzando el error, que lo redirigirá al Catch del nivel superior
  2. Llamarlo sin relanzar el error para evaluarlo localmente, como el siguiente caso

DO Actualiza WITH loEx, .F., lnCount
IF NOT ISNULL(loEx)
   *-- Ocurrió un error, hacer algo
ELSE
   *-- Ejecución OK
ENDIF




Achicando aún más


Finalmente hay una forma aún más compacta, asumiendo que solo se quiere relanzar el error y nunca se tratará como el caso anterior, sino directamente en el Catch del nivel superior que corresponda:

PROCEDURE Actualiza( tnCount )
   TRY
      LOCAL loEx as Exception
      lnCodError = 0 
      loEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO loEx
      THROW
   FINALLY
      USE IN (SELECT("USUARIO"))   ENDTRY
   RETURN
ENDPROC







Ayuda para Depurar en el control de errores


Esto es algo que no suele tenerse en cuenta, y que debería, ya que permite encontrar los errores mucho antes.
Cuando ocurre un error y se relanza la excepción hacia los niveles superiores, se pierde la información del entorno donde ocurrió (tablas abiertas, variables creadas, etc), ya que cada nivel fue cerrando y limpiando sus cosas, entonces se hace necesario una forma rápida que permita poder depurar "in-situ" cuando se está ejecutando en modo Desarrollo. Esta es una forma de hacerlo:

PROCEDURE Actualiza( tnCount )
   TRY
      LOCAL loEx as Exception
      lnCodError = 0 
      loEx = NULL 
      UPDATE USUARIO SET TIMESTAMP = DATETIME() WHERE USUA_PC = SYS(0)
      tnCount = _TALLY

   CATCH TO loEx
      IF _VFP.StartMode = 0 THEN
         SET STEP ON
      ENDIF
      THROW
 

   FINALLY
      USE IN (SELECT("USUARIO")) 

   ENDTRY

   RETURN
ENDPROC





Es así de simple y muy efectivo. Incluso se podría encapsular ese bloque en una función externa reutilizable, que también se encargue de generar un Log.


Una cosa importante sobre THROW:

No es lo mismo esto:

THROW


que esto:

THROW loEx



El primero relanzará el error tal cual, y se podrán seguir leyendo sus datos en loEx en cada Catch por el que pase, mientras que el segundo está relanzando específicamente un objeto Exception que al Catch de nivel superior le llegará en la propiedad UserValue, lo que lo hace más difícil de tratar si no se sabe esto previamente.


Información de error complementaria muy útil

Hay una propiedad del objeto Exception que suele pasarse por alto y que resulta ser muy útil para agregar información específica, que es UserValue. Obviamente que para aprovecharla se deben relanzar los errores con Throw solamente, o de otro modo se pisará su valor con una Excepción.

Lo que le da un valor especial es que al ser una propiedad "para el usuario" podremos poner allí lo que querramos, tanto un valor como un objeto como un XML, lo que sea, y esto permite agregar más información específica del sitio donde ocurrió el error.

Por ejemplo, en el procedimiento AnalizarBloque_CDATA_inline() de FoxBin2Prg encontramos esto:

CATCH TO loEx
    IF loEx.ErrorNo = 1470    && Incorrect property name.
        loEx.UserValue    = 'PropName=[' + TRANSFORM(tcPropName) + '], Value=[' ;

           + TRANSFORM(lcValue) + ']'
    ENDIF



En este caso concreto me resulta muy importante adjuntar al objeto del error el nombre y el valor de la propiedad que puede causarlo, ya que si no, no saldría reflejado en ningún otro sitio o a lo sumo saldría solo el nombre de la propiedad.

Luego, en el nivel superior, junto la información de esta forma para mostrarla o loguearla a un archivo:

lcError = 'Error ' + TRANSFORM(toEx.ERRORNO) + ', ' + toEx.MESSAGE + CR_LF ;
    + toEx.Procedure + ', ' + TRANSFORM(toEx.LineNo) + CR_LF ;

    + toEx.LineContents + CR_LF + CR_LF ; 
    + toEx.StackLevel + CR_LF + CR_LF ; 
    + toEx.Detailt + CR_LF + CR_LF ;
    + EVL(toEx.UserValue,'')



En el método escribirArchivoBin() de la clase c_conversor_prg_a_dbf, guardo datos sobre el índice, el campo y la sentencia de creación de la tabla, ya que se intentará crear en ese método, pero si falla necesito saber qué ocurrió:

loEx.UserValue = 'lcIndex="' + TRANSFORM(lcIndex) + '"' + CR_LF ;
    + 'lcFieldDef="' + TRANSFORM(lcFieldDef) + '"' + CR_LF ;
    + 'lcCreateTable="' + TRANSFORM(lcCreateTable) + '"'




Para cada caso se debe contemplar adjuntar información específica para que ubicar el error, en caso de ocurrir, se haga lo más rápido posible.


Conclusión

Vimos algunas técnicas para controlar los errores, que es una de las cosas más importantes en cualquier aplicación, ya que de ello depende que aparezca un error descontrolado y que el usuario decida si "Aceptar, Cancelar o Ignorar" (lo peor que puede pasar, sobre todo si "Ignora"), o que el sistema tenga el flujo de errores definido desde el método más profundo para reenviarlo hasta llegar la interfaz o al programa externo.

FoxBin2Prg, por ejemplo, usa el control compacto de errores, ya que no me interesa mostrar mensajes en cualquier momento, e incluso se puede configurar para no mostrar ningún mensaje y opcionalmente generar un log.

La recolección de basura también es muy importante, ya que no hacerlo de forma correcta puede llevar a un sistema inestable o que a la larga genere el temido error C0000005

Hasta la próxima!


Relacionados:
Técnicas de programación en VFP: Procesos desatendidos y control de progreso visual