"Después del juego es antes del juego"
Sepp Herberger

lunes, 23 de abril de 2018

Sistema de gestión de turnos y colas (II)

Se acerca la temporada de colas en el IES y desempolvamos nuestro sistema para dar gestionar las colas de espera para hacer algunas reformitas.

En su día creamos 3 scripts para incrementar, decrementar y poner a cero el contador de turnos. Hoy empezamos añadiendo un script nuevo que nos permitirá dar un valor concreto, que entrará como parámetro, al marcador:
# cat pon_turno.sh 
#!/bin/bash

num=$1
test "$num" == "" && num="00"

servidor="172.X.Y.Z"
topic="numero"

if num=$(printf "%02d" $num 2> /dev/null)
then  #Si $num no es un numero, falla el comando anterior
  mosquitto_pub -t $topic -h $servidor -m "A-$num" -r
fi
Esto será útil si hay cualquier fallo que descontrole el marcador y necesitemos ir hasta un número directamente.

Para continuar, se me planteó la siguiente necesidad: controlar los turnos desde una aplicación normal y corriente como alternativa al método de la entrada anterior del blog (la cual usaba un ratón y triggerhappy como sistema para incrementar/decrementar el control del marcador de turnos). Podría escribir una aplicación de escritorio en Python y GTK, ya que con bash sería mas complicado, pero al final me he ido a lo mas sencillo y he optado por usar una aplicación web con Javascript+Mosquitto, usando la misma tecnología con que hice la pantalla que muestra el sistema de turnos del post anterior.

El aspecto de la aplicación será:


Al pulsar "Definir Turno" nos permitirán introducir un turno nuevo directamente:


Todo esto se implementa con una página HTML con mucho CSS y Javascript, ahí va:
# cat /var/www/turnos/GestionTurnos.html 
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
  <meta content="text/html; charset=ISO-8859-1" http-equiv="content-type">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Sistema de Gestión de Turnos</title>

    <style type="text/css">

        html, body {
             margin: 0px;
             height: 100%;
             background-color: #000099;
        }
        @font-face{
             font-family: 'Antonio';
             src: url('Antonio-Regular.ttf') format('truetype');
        }
        #Pagina {
             font-family: 'Antonio', Arial, sans-serif;
             margin: 0px;
             height: 100%;
        }
        #cabecera {
             font-style: normal;
             font-weight: bold;
             background-color: #000099;
             font-size:7vw;
             width: 100%;
             color: white;
             text-align: center;
             background-image: url(img/logo_guadalupe.png);
             background-position: left center;
             background-repeat: no-repeat;
        }
        #cuerpo {
             color: yellow;
             text-align: center;
             background-color: black;
             font-size:22vw;
             font-weight: bold;
             width: 100%;
             height: 60%;
        }
        #pie {
             font-size:7vw;
             background-color: #000099;
             font-weight: bold;
             width: 100%;
             height: 15%;
             color: white;
             font-style: normal;
             text-align: center;
        }

 .botonAvanza {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
 }
 .botonAvanza:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonAvanza:active {
  position:relative;
  top:1px;
 }

 .botonRetrocede {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;

 }
 .botonRetrocede:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonRetrocede:active {
  position:relative;
  top:1px;
 }

       .botonSet {
  -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
  -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
  box-shadow:inset 0px -3px 7px 0px #29bbff;
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
  background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
  background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
  background-color:#2dabf9;
  -moz-border-radius:12px;
  -webkit-border-radius:12px;
  border-radius:12px;
  border:1px solid #0b0e07;
  display:inline-block;
  cursor:pointer;
  color:#ffffff;
  font-family:Arial;
  font-size:28px;
  padding:22px 48px;
  text-decoration:none;
  text-shadow:0px 1px 0px #263666;
                vertical-align:middle;

 }
 .botonSet:hover {
  background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #0688fa), color-stop(1, #2dabf9));
  background:-moz-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-webkit-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-o-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:-ms-linear-gradient(top, #0688fa 5%, #2dabf9 100%);
  background:linear-gradient(to bottom, #0688fa 5%, #2dabf9 100%);
  filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0688fa', endColorstr='#2dabf9',GradientType=0);
  background-color:#0688fa;
 }
 .botonSet:active {
  position:relative;
  top:1px;
 }

        .inputturno {
                -moz-box-shadow:inset 0px -3px 7px 0px #29bbff;
                -webkit-box-shadow:inset 0px -3px 7px 0px #29bbff;
                box-shadow:inset 0px -3px 7px 0px #29bbff;
                background:-webkit-gradient(linear, left top, left bottom, color-stop(0.05, #2dabf9), color-stop(1, #0688fa));
                background:-moz-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-webkit-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-o-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:-ms-linear-gradient(top, #2dabf9 5%, #0688fa 100%);
                background:linear-gradient(to bottom, #2dabf9 5%, #0688fa 100%);
                filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2dabf9', endColorstr='#0688fa',GradientType=0);
                background-color:#2dabf9;
                -moz-border-radius:12px;
                -webkit-border-radius:12px;
                border-radius:12px;
                border:1px solid #0b0e07;
                display:inline-block;
                cursor:pointer;
                color:#ffffff;
                font-family:Arial;
                font-size:28px;
                padding:22px 48px;
                text-decoration:none;
                text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
                width: 30px;
        }

       .labelturno {
                display:inline-block;
                cursor:pointer;
                color:#ffffff;
                font-family:Arial;
                font-size:28px;
                padding:22px 2px;
                text-decoration:none;
                text-shadow:0px 1px 0px #263666;
                vertical-align:middle;
        }

    
    </style>

    <script src="http://tercero/turnos/mqttws31.js" type="text/javascript"></script>
    <script src="http://tercero/turnos/jquery.min.js" type="text/javascript"></script>
    <script src="http://tercero/turnos/config.js" type="text/javascript"></script>

    <script type="text/javascript">

    var valor;
    var mqtt;
    var reconnectTimeout = 2000;
    function MQTTconnect() {
 if (typeof path == "undefined") {
  path = '/mqtt';
 }
 mqtt = new Paho.MQTT.Client(
   host,
   port,
   path,
   "web_" + parseInt(Math.random() * 100, 10)
 );
        var options = {
            timeout: 3,
            useSSL: useTLS,
            cleanSession: cleansession,
            onSuccess: onConnect,
//            mqttVersion: 3, 
            onFailure: function (message) {
                $('#status').val("Connection failed: " + message.errorMessage + "Retrying");
                setTimeout(MQTTconnect, reconnectTimeout);
            }
        };

        mqtt.onConnectionLost = onConnectionLost;
        mqtt.onMessageArrived = onMessageArrived;

        if (username != null) {
            options.userName = username;
            options.password = password;
        }

        console.log("Host="+ host + ", port=" + port + ", path=" + path + " TLS = " + useTLS + " username=" + username + " password=" + password);
        mqtt.connect(options);
    }

    function onConnect() {
        $('#status').val('Connected to ' + host + ':' + port + path);
        // Connection succeeded; subscribe to our topic
        mqtt.subscribe(topic, {qos: 0});
        $('#topic').val(topic);
    }

    function onConnectionLost(response) {
        setTimeout(MQTTconnect, reconnectTimeout);
        $('#status').val("connection lost: " + responseObject.errorMessage + ". Reconnecting");

    };

    function onMessageArrived(message) {

        var topic = message.destinationName;
        var payload = message.payloadString;
        valor=payload;
        $('#cuerpo').html(payload);
    };


    $(document).ready(function() {
        MQTTconnect();
        var caja=$('#id_turno');
        caja.hide();
        var label=$('#id_label');
        label.hide();
    });

    function subirTurno () {

         var numero=valor.substring(2);
         if (numero==99) numero=0;
         else numero++;
          
         topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
         topicMessage.destinationName = "numero";
         topicMessage.retained = true;
         mqtt.send(topicMessage);

    }

    function bajarTurno () {

         var numero=valor.substring(2);
         if (numero==0) numero=0;
         else numero--;

         topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
         topicMessage.destinationName = "numero";
         topicMessage.retained = true;
         mqtt.send(topicMessage);

    }


   function verCaja () {

        var caja=$('#id_turno');
        caja.show();
        var numero=valor.substring(2);
        caja.val(numero); 
        var label=$('#id_label');
        label.show();
        var boton=$('#id_boton');
        boton.hide();

   }


   function ponerTurno () {

        var caja=$('#id_turno');
        var numero=caja.val();

        if ( !! numero && ! isNaN(numero) ) {            // !!numero-> string no vacio.
            caja.hide();
            var label=$('#id_label');
            label.hide();
            var boton=$('#id_boton');
            boton.show();muchomucho
            if ( numero<0 || numero > 99) numero=valor.substring(2);
            topicMessage = new Paho.MQTT.Message("A-"+("00" + numero).slice (-2));  //Para añadir padZero a la izquierda del numero
            topicMessage.destinationName = "numero";
            topicMessage.retained = true;
            mqtt.send(topicMessage);
        }

   }

    </script>
</head>
<body>
  <div id="Pagina">
    <input type='text' id='topic' disabled hidden/>
    <input type='text' id='status' size="80" disabled hidden/>
    <div id="cabecera">IES VIRGEN DE GUADALUPE</div>
    <div id="cuerpo">TURNO</div>
    <div id="pie">
       <a href="#" onclick="subirTurno();" class="botonAvanza">Avanza Turno</a>
       <a href="#" onclick="bajarTurno();" class="botonRetrocede">Retrocede Turno</a>
       <a href="#" onclick="verCaja();" class="botonSet" id="id_boton">Definir Turno</a>
       <label class="labelturno" id="id_label">Introduzca nuevo turno ==></label>
       <input id="id_turno" class="inputturno" onclick="this.select()" onKeyDown="if (event.keyCode==13) ponerTurno();" value="" />
    </div>
  </div>
</body>
</html>
Todos los ficheros necesarios ya los enlacé en entrada anterior del blog. Simplemente habría que añadir GestionTurnos.html en el mismo sitio web donde pusimos SuTurno.html

Al estar basada en Mosquitto la página web GestionTurnos.html se actualizará en tiempo real y si está abierta en varias ubicaciones (por ejemplo todos los PC de administración) cualquier cambio se propagará a todas los demás de forma inmediata.

Una vez la tenemos funcionando se me ocurre dar una vuelta de tuerca más y convertir la aplicación web en una aplicación independiente de escritorio. De esta manera correrá en una ventana autónoma del navegador que podrá minimizarse y maximizarse a voluntad y será mas sencilla de usar. Hay muchas herramientas para realizar esta conversión web->escritorio, pero me ha gustado por su sencillez Nativifier. Lo que haremos será:
# cat /etc/apt/sources.list.d/nodesource.list 
deb https://deb.nodesource.com/node_0.10 trusty main
deb-src https://deb.nodesource.com/node_0.10 trusty main
# apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80   1655A0AB68576280
# apt-get update
# apt-get install nodejs
# npm install nativefier -g
# nativefier -n "Sistema de Gestión de Turnos"  http://172.X.Y.Z/turnos/GestionTurnos.html
Esto crea un directorio con un fichero ejecutable estándar de Linux que al lanzarlo abre la aplicación Web de Gestión de Turnos como si fuera una aplicación de escritorio. Bastará con poner un acceso directo en el escritorio del usuario y quedará todo simple y accesible.

Si queremos que la aplicación sea usada por todos los usuarios de la máquina lo mas sencillo será llevarla a /opt y dar permisos 777 a todo el directorio donde está.

Bueno, pues ya solo nos falta hacer algún día una aplicación para el móvil...

viernes, 20 de abril de 2018

RPG Maker MV en miniportátiles MSI con Ubuntu

Un docente de mi centro está trabajando en un proyecto para realizar aplicaciones educativas en forma de videojuegos de rol usando la herramienta RPG Maker MV, que permite generar un videojuego ejecutable en Linux, Windows, OSX, Android e iPhone.

El programa final es una aplicación portable que se lanza desde un ejecutable ./Game contenido en el directorio principal y que contiene un navegador Chrome/Chromium empotrado, en el cual se ejecuta la aplicación como si fuese de escritorio. Como lenguaje de programación se usa JavaScript y para la parte gráfica usa WebGL mediante PixiJS. El resultado es un videojuego de rol con gráficos en 2D del estilo de los venerables Zelda de los primeros tiempos. El contenido de la carpeta de la versión Linux tiene este aspecto:


Estamos en fase de pruebas del juego en las máquinas de los centros y un compañero nos avisó de que en los miniportátiles MSI L1350D la ventana se quedaba en negro y no mostraba nada. Lanzando el ejecutable Game desde terminal y revisando el montón de mensajes de log se aprecia el error:
GL_INVALID_OPERATION : glConsumeTextureCHROMIUM: invalid mailbox
Buscando un poco por Internet determino que es un error de WebGL en Chromium. No me extraña, ya había tenido antes problemas con páginas que usan WebGL y los navegadores. Normalmente son derivados de bugs o incompatibilidades en el driver de la tarjeta VGA. En el caso de los MSI tenemos una vieja:
00:02.0 VGA compatible controller: Intel Corporation Atom Processor D4xx/D5xx/N4xx/N5xx Integrated Graphics Controller, (8086:a011)
Una prueba rápida: si accedo al portátil con "ssh -X" y ejecuto el juego, si que se ve bien. Eso confirma que es un problema de las X. Los drivers están actualizados, así que por ahí hay poco que rascar. Una solución extrema es desactivar el DRI en un xorg.conf a medida o bien no cargar el módulo "glx". El problema es que esto afectaría al rendimiento de todas las X, no solo del juego, penalizando por tanto el uso normal del escritorio.

La opción mas deseable es arrancar el Chrome/Chromium con WebGL desactivado (usando el parámetro --disable-webgl) y ver si así funciona. El problema es que este Chromium está embebido en el juego...¿cómo manipular su arranque?. Pues investigando un rato veo que en el mismo directorio donde está ./Game hay un fichero llamado package.json con este contenido:
{
    "name": "KADOKAWA/RPGMV",
    "main": "www/index.html",
    "js-flags": "--expose-gc",
    "chromium-args": "--disable-setuid-sandbox",
    "window": {
        "title": "",
        "toolbar": false,
        "width": 816,
        "height": 624,
        "icon": "www/icon/icon.png"
    }
}
Cáspita, parece el fichero de configuración de arranque del Chromium. Si añado lo marcado en negrita:
{
    "name": "KADOKAWA/RPGMV",
    "main": "www/index.html",
    "js-flags": "--expose-gc",
    "chromium-args": "--disable-setuid-sandbox  --disable-webgl",
    "window": {
        "title": "",
        "toolbar": false,
        "width": 816,
        "height": 624,
        "icon": "www/icon/icon.png"
    }
}
Lo arrancamos... et voilá:


Todo funcionando. A otra cosa, mariposa.



lunes, 16 de abril de 2018

Captura de la pantalla de los alumnos (I)

Lamento este tiempo de silencio, he estado muy ocupado haciendo un Máster en Estupefacción.

Siempre es muy útil para el profesor controlar que uso están haciendo los alumnos con sus ordenadores en el aula. Para esto hemos usado hasta ahora Aulalinex o epoptes. El problema es que ambos son sencillos de usar para observar que hace un alumno concreto, pero no lo son para un grupo de ellos o todos a la vez. Además, desde hace un tiempo nuestro Aulalinex no acaba de funcionar al 100% en Ubuntu.

Un profesor me planteó la posibilidad de hacer una "foto" colectiva de todas las pantallas, que dejase las imágenes con las capturas de los escritorios en un directorio concreto para su consulta posterior. El problema es interesante, pero nos encontramos que aunque desde la óptica del profesor todo son aulas con ordenadores de alumnos, desde un punto de vista interno tenemos al menos 3 escenarios:

  • Aulas con thinclients, donde los ordenadores de los alumnos ejecutan su escritorio y aplicaciones dentro del ordenador del profesor. Sus pc son terminales tontos que apenas hacen nada que no sea conectarse remotamente y abrir sus sesiones en el pc del profesor a través de la red interna 192.168.0.X del cada aula.
  • Aulas con ordenadores independientes para alumnos dentro de una VLAN privada, con direcciones 192.168.0.X. Estos ordenadores pueden ser portátiles o sobremesa (aulas "infolab"), realmente son el mismo caso.
  • Aulas con ordenadores independientes para alumnos que no están aislados, sino que están en la red general del centro y tienen IPs 172.X.Y.Z.

Internamente, el manejo de esta casuística es muy diferente. Por ello iremos por partes y solucionaremos primero el caso de las aulas con thinclients, que es seguramente el más sencillo. En este escenario todas las sesiones están dentro del propio pc del profesor de tal manera que no hace falta ninguna conexión remota: todo queda en casa.

Lo primero es averiguar si hay alumnos en el aula conectados desde los thinclient. Eso se consigue lanzando el comando "who" en el pc del profesor:
cbayont04@a23-pro:~$ who
sjimenezt09 pts/5        2018-04-12 08:43 (a23-o03.local)
cbayont04    :0           2018-04-12 08:33 (:0)
sruizb13    pts/1        2018-04-12 08:43 (a23-o06.local)
........
Aquí se ve que hay un profesor en la consola (:0) y dos alumnos conectados remotamente desde los thinclients a23-o03 y a23-o06. El código para verificarlo será:
thinclients=$(who |  grep ".local)$")  #Si hay logeados usuarios con  el prefijo .local) al final estamos en un aula de thinclients y esos son los alumnos
if [ -z "$thinclients" ]
then 
   echo "No detecto thinclients. Abortando.
   exit 1
else #Son thinclients
   ...
Luego preparamos un directorio para grabar las capturas de la pantalla (en negrita):
pc=$(hostname)
fecha=$(date +%Y-%m-%d)
hora=$(date +%H:%M:%S)
destino="$HOME/capturas/$pc/$fecha/$hora"
rm -rf "$destino"
mkdir -p "$destino"
cd "$destino"
A continuación se identifican las IP de los pc encendidos mediante un nmap en el rango 192.168.0.200-253 (el usado en mi caso para los thinclients) e iteramos sobre ellas:
encendidos=$(nmap -oG - --system-dns  -sP 192.168.0.200-253 | grep -v '^#' | tr -d '()' | sed 's/.local//' | awk '{print $2}' | sort)
for ip in $encendidos
do
Seguidamente obtenemos el nombre del thinclient a partir de la IP (usando avahi-resolve-address ya que los thinclients no están dados de alta en nuestro servidor DNS y la resolución de nombres la realizan ellos mismos mediante el demonio avahi) y el nombre del alumno que ha hecho login en dicho thinclient.
pc=$(avahi-resolve-address $ip | cut -f2)  #El delimitador por defecto es  
if [ -n "$pc" ]    #Si resolvemos el nombre, esta operativo
then
  user=$(who | grep -i $pc | cut -d" " -f1)
  if [ -n "$user" ]  #Si tiene user logueado
  then
Por último ya solo queda capturar el escritorio del thinclient y copiarlo a la carpeta $destino. Para ello debemos averiguar la ruta del $HOME del alumno (con "getent passwd ..") y tomar la identidad de dicho usuario (con "su $user") para realizar la captura de su escritorio completo con:
home_user=$(getent passwd "$user" | cut -d: -f6)
su $user -c  "DISPLAY=$ip:7  XAUTHORITY=$home_user/.Xauthority import -window root /tmp/$user-$pc.jpg"
cp /tmp/$user-$pc.jpg $destino/$user-$pc.jpg
El comando anterior:
DISPLAY=$ip:7  XAUTHORITY=$home_user/.Xauthority import -window root /tmp/$user-$pc.jpg
digamos que lo he encontrado a base de prueba-error.

Para poder hacer la captura del escritorio del thinclient conectado remotamente hay que definir las variables DISPLAY y XAUTHORITY correctas para acceder a dicho escritorio con la credencial del alumno y tener los permisos adecuados. El acceso a las sesiones X desde otra sesión X es algo que está siempre bastante protegido para evitar espionaje entre usuarios y es complicado incluso para el usuario root. Finalmente la foto se guarda en /tmp/$user-$pc.jpg y de ahí la llevamos luego a $destino/$user-$pc.jpg.

Una vez recopiladas las capturas de todas las IP abrimos la carpeta $destino con thunar y así el profesor puede ver todas las imágenes cómodamente.

El script completo es:
# cat /usr/bin/captura-escritorio-todos
#!/bin/bash

thinclients=$(who |  grep ".local)$")  #Si hay logeados usuarios con  el prefijo .local) al final estamos en un aula de thinclients y esos son los alumnos

if [ -z "$thinclients" ]
then 
   echo "No detecto thinclients. Abortando."
   exit 1
else #Son thinclients
  pc=$(hostname)
  fecha=$(date +%Y-%m-%d)
  hora=$(date +%H:%M:%S)
  destino="$HOME/capturas/$pc/$fecha/$hora"
  rm -rf "$destino"
  mkdir -p "$destino"
  cd "$destino"

  #En los thinclients el DNS ip<-->nombre se publica desde cada puesto mediante avahi
  encendidos=$(nmap -oG - --system-dns  -sP 192.168.0.200-253 | grep -v '^#' | tr -d '()' | sed 's/.local//' | awk '{print $2}' | sort)
  for ip in $encendidos
  do
     pc=$(avahi-resolve-address $ip | cut -f2)  #El delimitador por defecto es  , por eso no uso -d
     if [ -n "$pc" ]    #Si resolvemos el nombre, esta operativo
     then
         user=$(who | grep -i $pc | cut -d" " -f1)
         if [ -n "$user" ]  #Si tiene user logueado
         then
             sudo /usr/bin/captura-escritorio-thinclient $user $ip $pc
             cp /tmp/$user-$pc.jpg $destino/$user-$pc.jpg
         fi
     fi
  done
  thunar "$destino"
fi

exit 0
El otro script usado es:
# cat /usr/bin/captura-escritorio-thinclient
#!/bin/bash

user=$1; ip=$2; pc=$3
home_user=$(getent passwd "$user" | cut -d: -f6)
su $user -c  "DISPLAY=$ip:7  XAUTHORITY=$home_user/.Xauthority import -window root /tmp/$user-$pc.jpg"
   
exit 0
Para que el el script llamado por el usuario normal pueda hacer el "su $user ..." necesitamos que esa parte se ejecute con permisos de root, que es el único usuario que puede adoptar la identidad de otro sin saber su contraseña. Por eso llevamos esa parte a otro script que ejecutaremos con "sudo". Evidentemente también hay que modificar /etc/sudoers para permitir su ejecución con sudo a todos los usuarios regulares del grupo teachers (profesores del centro).
# cat /etc/sudoers
....
....
%teachers ALL = (ALL) NOPASSWD: /usr/bin/captura-escritorio-thinclient
....
....
Al finalizar la captura de todos los escritorios de alumnos se abre la carpeta $destino con todas las imágenes de las capturas de escritorio para que el profesor pueda revisarlas tranquilamente con el visor de fotografías de nuestro sistema operativo:


Bueno, pues está resuelto el caso mas simple. Quedan dos pendientes para continuar la serie y esta vez no vamos a esperar tanto para continuar.