martes, 18 de abril de 2023

Un pequeño y raro bug en PSeInt

La semana pasada corregí un bug de PSeInt de lo más curioso, por la forma en que se manifestaba, y la causa del mismo. Es un ejemplo de las "sutilezas" de programar orientado a eventos y la inversión del control, y de lo importante de la programación defensiva.

 

Todo empieza cuando una alumna me muestra que en su notebook cuando corre un pseudocódigo que genera una división por cero, y luego maximiza la terminal de ejecución, la mitad de las veces el editor de pseudocódigo revienta (se cierra de mala manera).

Recordemos que los módulos (editor y terminal) son ejecutables separados. Entonces, ¿Cómo es que un evento de uno hace reventar al otro? Y además, un evento que no tiene nada de especial, ni callback asociado; un evento que no debería generar ningún tipo de comunicación entre los módulos. Más aún, solo ocurre si se maximiza la ventana haciendo doble click en la barra de título, pero no si se maximiza con el botón de maximizar. ¿WTF?

 

Por suerte logré reproducir el problema de forma más o menos consistente con la versión publicada de PSeInt para Windows. Entonces lo primero que hice fue reemplazar el ejecutable del editor por una versión debug para tratar de agarrar el problema en el depurador. Allí me encuentro con que el programa revienta porque recibe a través del canal de comunicación entre ambos módulos (un socket) la orden de seleccionar un instrucción del algoritmo, pero el número de instrucción es basura (muy fuera de rango). 

https://www.agent-x.com.au/comic/programming-progress/

Entonces, la culpa no es del editor, sino de la terminal que envía ese comando. Pero ¿por qué la terminal enviaría ese comando al maximizar? Ese comando se envía en el momento en que el algoritmo genera un error (para que el editor marque la linea del error) o cuando el usuario hace click en una entrada/salida (para que el editor marque el leer/escribir asociado). Nada tiene que ver, en principio, un cambio de tamaño de la ventana.

Lo siguiente fue entonces depurar la terminal. Para que la terminal envíe estos comandos, debe ejecutarse controlada por el editor, y hay un ida y vuelta de argumentos en la linea de comandos, archivos temporales y alguna info inicial por el socket que es muy difícil de simular artificialmente. Entonces, en lugar de lanzar la terminal en el depurador como normalmente haría, reemplacé el ejecutable de la terminal en la instalación de PSeInt por uno con información de depuración, y el proceso para depurar pasó a ser: iniciar la ejecución del algoritmo (y con ello de l terminal) desde PSeInt (para que arme ese canal de comunicación entre ambos procesos), y luego "adjuntar" el depurador al proceso de esa terminal (para esto sirve lo de "adjuntar" que casi nadie usa).

 

Con este mecanismo, y luego de muchos intentos, logré llegar al caso en que tras maximizar la ventana, esta enviaba el comando conflictivo. Mirando el backtrace veo que estaba en un evento de "soltar el botón del ratón"; y ahí me cayó la ficha. Lo normal sería que no pueda haber un evento de soltar el botón del ratón si no hubo antes uno de apretarlo. Pero en este caso, cuando se maximiza una ventana haciendo doble click, esta cambia de tamaño en el segundo "apretar", no espera al segundo "soltar". Entonces cuando "suelta" el segundo click, el puntero ya no está en la barra de título, sino dentro de la ventana (porque ya se maximizó), y ahí es cuando tenemos el "soltar" sin el "apretar" previo.

https://www.monkeyuser.com/2018/root-cause

Esta clase de cosas "sutilezas" aparecen frecuentemente al programar en base a eventos. Y puede variar de un sistema operativo a otro (por ej, que en otro se maximize al segundo "soltar"). Ya es sabido que hay que prever que el usuario pueda generar los eventos fuera del orden que queremos. Pero aquí se muestra también que hay que estar preparados para combinaciones que a priori parecen imposibles.

Si hubiera programado más defensivamente, el evento de soltar botón podría haber estado verificando si efectivamente habíamos pasado antes por el de apretar en lugar de asumirlo (lo que lo hacía trabajar con variables sin inicializar). O podría haber previsto en el editor que llegue un comando erróneo, y validar los números de linea o de instrucción antes de intentar usarlos.

 

Resumiendo... Conociendo la separación en módulos/ejecutables, y que el evento de maximizar no tiene nada especial programado, pensé que el reporte de error era imposible, que algo había entendido mal. No lo creí hasta que no lo vi, y fue desconcertante. Pero a pesar de ser un error "tonto", es un ejemplo más de cómo se va el tiempo, a veces en "detalles", y la cantidad de cosas que pueden malir sal cuando se empiezan a combinar los eventos de formas difíciles de prever. 

Algo muy bueno para cerrar es que en el camino también encontré por qué en la mayoría de los casos no funcionaba la recuperación de los algoritmos luego de reiniciar el editor, y eso también está corregido para la próxima.

1 comentario:

  1. Me hiciste recordar que en programación de microcontroladores los eventos de interrupción por ejemplo activación de un suiche, convertidor de análogo a digital etc., tienen una prioridad de ejecución, que se puede cambiar.

    Las aplicaciones no son perfectas y mucho menos las que tienen cientos de desarrolladores, me pasa por ejemplo con Microsoft Word que cuando uno da ciertos clics rápidos cuando hay ventanas emergentes u ocultas, o cuando uno salta a otra app y regresa a word esta se cierra, puede ser que esté ocurriendo un problema similar al que encontraste en este informe.

    Y fuera de punto, en un post de hace mucho tiempo mencionaste que estabas escribiendo un intérprete de LISP, por favor colócalo para que sea público, puede ser que a alguien le interese, o que tú lo hayas codificado diferente a otros que ya existen. Relacionado yo había escrito que si una expresión si se descompone en notación polaca inversa(RPN) o directa o como se codifica en LISP se podría evaluar las expresión aun con un mayor paso interno, ya que por ejemplo
    ABS( -3 * ( 4 + 5 )) se convierte en una secuencia de subexpresiones RPN o LISP
    Esta expresión se puede reescribir como:
    NOTACIÓN INVERSA: 4 y 5 SUMAR, luego -3 MULTIPLICAR, luego aplicar VALOR ABSOLUTO
    Ejecutando paso a paso es: 4 y 5 SUMAR devuelve 9, luego -3 MULTIPLICAR devuelve -27, luego VALOR ABSOLUTO devuelve 27
    NOTACIÓN DIRECTA: SUMAR 4 y 5, luego MULTIPLICAR -3, luego aplicar VALOR ABSOLUTO
    Ejecutando paso a paso es: SUMAR 4 y 5 devuelve 9, luego MULTIPLICAR -3 devuelve -27, luego VALOR ABSOLUTO devuelve 27
    mostrar este nivel de detalle en el depurador seria genial para determinar si la expresión ingresada retorna lo deseado en cada subparte, porque solo mostrar el resultado final es difícil detectar donde se falló al escribir la expresión. Esta característica no la he visto en ningún IDE, ni app de PSeudoCode, así que sería una innovación.

    ResponderEliminar