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

miércoles, 18 de diciembre de 2019

Demoras en el arranque de los PC "infolab"

Ya contamos como configurar como puestos multiseat los infolabs HP ProDesk 600 G1 SFF que tenemos, permitiendo que un mismo PC fuese usado por 2 usuarios a la vez de forma concurrente.

Había venido observando que el arranque de un PC infolab con multiseat era bastante mas lento (un minuto o más) que el de un infolab normal. En un primer momento lo achaqué a su condición de multiseat: al tener dos escritorios concurrentes quizá el arranque tarda más mientras preparaba el entorno. Pero como no estaba muy convencido he estado investigando.

Lo primero es medir los tiempos de arranque con systemd-analyze. Con:
# systemd-analyze blame
Nos sale una lista decreciente de tiempos y procesos, llamándome la atención esto:
54.4s dev-sda5.device
Comparando con otro infolab sin multiseat:
7.471s dev-sda5.device
Buscando en Internet veo que dev-sdaX.device no es nada en concreto, no se corresponde con ningún servicio y no hay manera de sacar información de ahí. Analizando con "systemd-analyze plot" y "systemd-analyze critical-chain" no saco nada más en claro.

Bueno, pues otra manera mas detallada de analizar el arranque es utilizar systemd-bootchart, que guarda una imagen SVG en /var/run con los tiempos y procesos de arranque más detallada que la de "systemd-analyze plot".


Analilzando veo que el proceso mas destacado es nvidia-smi lanzado desde systemd-udevd, el cual tarda 14s en ejecutarse. Si lo hago en un equipo sin multiseat veo que tarda menos de medio segundo.

Pensando entre las diferencias que hay entre infolabs con y sin multiseat recuerdo el tema de las tarjetas VGA. Un infolab trae 2 tarjetas VGA de serie:
00:02.0 VGA compatible controller [0300]: Intel Corporation HD Graphics 530 [8086:1912] (rev 06)
01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GK208B [GeForce GT 730] [10de:1287] (rev a1)
En el caso de un equipo multiseat ambas VGA están activas (ya que hacen falta para los 2 monitores). En un equipo normal tengo la tarjeta VGA Intel (la de la placa base) desactivada en la BIOS, ya que no me hace falta y la desactivo para evitar problemas.

Como ultima comprobación, mirando el syslog de arranque con dmesg encuentro que, cuando hay retraso, este mensaje aparece en el log:
watchdog: BUG: soft lockup - CPU#2 stuck for 22s! [nvidia-smi:618]
Deduzco que es casi seguro que la combinación del programa nvidia-smi (que viene con el driver propietario nvidia) y la tarjeta VGA intel de la placa base son la causa del problema.

Consideremos estas pruebas y limitaciones:
  • No puedo prescindir de los drivers nvidia, ya que con nouveau no funciona el multiseat.
  • No puedo desactivar la tarjeta intel en la BIOS, ya que la necesito para seat-1 del multiseat.
  • He probado con distintos kernel 4.x y 5.x por si fuera un fallo del driver. No se arregla con ninguno.
  • He actualizado el driver propietario a la ultimísima versión (usando el PPA). No se arregla.
Cuando no se te ocurre nada nuevo es el momento de buscar en Internet. Buscando por el texto del error mostrado en el syslog encuentro:
  • Solución 1: Añadir nomodeset como parámetro de arranque del kernel en el grub. Se arregla el problema, pero no funciona el multiseat. El seat-1 no se levanta.
  • Solución 2: en el fichero /lib/udev/rules.d/71-nvidia.rules comentar la línea:
    ACTION=="add" DEVPATH=="/module/nvidia" SUBSYSTEM=="module" RUN+="/usr/bin/nvidia-smi"
    
Pues está última propuesta funciona. Comentando esa línea impedimos que nvidia-smi (el causante del problema) se ejecute en el arranque del sistema, al iniciar la tarjeta nvidia. Parece temerario pero resulta que al final que el arranque se acelera (dev-sda5.device dura unos segundos en lugar de casi un minuto) y el multiseat funciona. Hablando en plata: la ejecución inicial de nvidia-smi es prescindible.

Como colofón podemos decir que esto no es es solamente aplicable cuando hay multiseat (que es como me ha salpicado a mí): siempre que haya una demora en el arranque producido por un conflicto entre nvidia-smi y una tarjeta no nvidia en la placa madre la solución será comentar esta línea que ejecuta en el inicio nvidia-smi. Tomemos nota para el futuro.


Qué bonitos los robots de Boston Dynamics, no entiendo como a mi compañera Marisa le dan repelús:




martes, 17 de diciembre de 2019

Jkiwi en Ubuntu Bionic 18.04

Ya hicimos funcionar la aplicación de maquillaje y estilismo capilar jkiwi para una arquitectura de 64 bits aquí.

Cuando me han pedido instalarla en Ubuntu 18 me he encontrado con que da error de dependencias porque no encuentra la máquina virtual java. Necesita uno de los siguientes paquetes: sun-java6-jre | sun-java5-jre | icedtea-java7-jre | openjdk-6-jre para funcionar y al no encontrarlos se aborta la instalación.

El motivo es que en Ubuntu 18.04 el paquete con la máquina virtual java es openjdk-8-jre, que no existía cuando se creó el .deb de jkiwi. La solución pasa por editar el paquete .deb y añadir la dependencia de openjdk-8-jre.

El paquete .deb lo modificamos tal como contamos aquí:
# dpkg-deb  -R jkiwi_0.9.5_all.deb jkiwi
En jkiwi/DEBIAN/control hay que modificar la siguiente línea, añadiendo la parte en negrita:
Depends: sun-java6-jre | sun-java5-jre | icedtea-java7-jre | openjdk-6-jre | openjdk-8-jre
Y luego reconstruimos el paquete:
# dpkg-deb -b jkiwi/ jkiwi_0.9.5_all.deb
El recién creado jkiwi_0.9.5_all.deb (puedes descargarlo de aquí) es el que ya podremos instalar (en mi caso lo he metido en mí repositorio local para instalarlo mas cómodamente) al tener su dependencia openjdk-8-jre cumplida. La aplicación funciona perfectamente con este openjdk, aunque sea muy posterior a la propia aplicación.

jueves, 12 de diciembre de 2019

Sustitución del cable de teclado de Infolabs/Siatic HP-KUS1206.

Tenemos un montón de teclados HP KUS1206 con lector SmartCard de muy buena calidad y robustos, pero en mi caso adolecen de un punto débil: el cable USB tiene tendencia a "pelarse" y partirse en el punto donde entra en el teclado, seguramente por el trajineteo al que están sometidos por los múltiples usuarios. Otro problema es que el cable se ha ido deformando y endureciendo, perdiendo flexibilidad, a lo largo de los años.


Como estos teclados por todo lo demás están en perfecto estado y funcionan a las mil maravillas, me daba rabia tener que mandar a reciclar un teclado de 50 euros por un puñetero cable USB roto. Así que me dispuse a desmontar uno a ver como iba conexión del cable.

En algunos teclados/ratones este cable está soldado a una pequeña placa de circuito impreso, pero afortunadamente en estos teclados vienen unidos con un conector:


En un principio me puse muy contento pensando que era un conector de 5 pines Dupont de los usados para montajes electrónicos, ya que esos conectores son fáciles de encontrar y manejar.


Pero no, los pin housing de los conectores Dupont tienen un ancho de 0.1"/2.54mm cada uno, de tal manera que 10 pines ocupan una pulgada (2.54cm). En cambio el pin housing de este teclado tiene un ancho de 2mm, de tal manera que los 5 pines ocupan 1cm (y 10 pines ocupan 2.00cm). Esto es un pin housing:



Los pin housing de 2mm con esta forma no son nada fáciles de encontrar y al final salen caros por su rareza. Parecía que no había solución a no ser que cortásemos y soldásemos a ese pin housing otro cable USB en perfecto estado. Soldar cables USB requiere un grado de destreza y paciencia que no tengo.

Pero vaya, rebuscando entre chatarra encontré varios cables de antiguos ratones averiados o rotos:


Midiendo resulta que su pin housing tiene 1cm de ancho, coincidiendo con los pines macho de nuestro teclado. Hay un pequeño problema: la posición de los hilos es diferente en ambos cables, ya que en el cable del teclado es Negro-Negro-Verde-Blanco-Rojo, mientras que en el cable del ratón es Negro-Negro-Rojo-Blanco-Verde. El coloreado en los cables USB es uno de los pocos cableados que normalmente se respeta.

Con ayuda de un destornillador pequeño es sencillo levantar la pestañita de plástico y extraer el hilo con el "crimp connector" que lo hace encajar en el pinheader.


Intercambiamos los hilos Rojo y Verde del cable del ratón, lo encajamos en el housing y este lo acoplamos con cuidado, forzando suavecito, en los 5 pines macho del circuito impreso del teclado.





Enchufando el cable USB al PC compruebo que el teclado funciona. Cerramos todo y dejamos el teclado listo con el nuevo cable, que aunque no es tan grueso como el original me ha permitido recuperar la funcionalidad.

El cableado antes:


El cabledo ahora:


Como después de tantos años tenía un buen número de cables de ratón con ese conector, he podido reparar varios teclados que tenía desahuciados para piezas y darles una nueva vida.

Para cuando se me acaben los cables de ratón he visto que se pueden localizar cables sueltos "usb to housing 5pin cable for mouse keyboard" o, llegado el caso, comprar pin housings y crimp connectors de 2mm sueltos para hacer el montaje a partir de otros cables USB que hayamos guardado para reciclar.

La sonda solar Parker ha llegado a su destino y empezará a estudiar la corona solar, cayendo en algunos puntos por debajo de la órbita de Mercurio. Una auténtica proeza de precisión y resistencia a 1377º de temperatura.


Si todo sale bien, le quedan 7 años para hacer 24 órbitas en las que recopilará datos sobre viento solar, magnetismo y partículas emitidas por nuestra estrella. ¡Buena suerte!

miércoles, 4 de diciembre de 2019

Ratón por puerto serie

Haciendo limpieza me he encontrado varios ratones serie y de bola. Me ha entrado curiosidad por ver si funcionaban, pero al ser de una época donde el plug'n'play era desconocido no ha bastado con enchufarlos al puerto serie, además he tenido que configurar un par de cosas:

Crear el fichero /etc/X11/xorg.conf.d/01-serial.conf (o /usr/share/X11/xorg.conf.d/01-serial.conf, dependiendo del Linux que usemos) :
# cat /etc/X11/xorg.conf.d/01-serial.conf

Section "InputDevice"
   Identifier "Mouse0"
   Driver "mouse"
   Option "Protocol" "Microsoft"
   Option "Device" "/dev/ttyS0"
   Option "ZAxisMapping" "4 5"
   Option "Emulate3Buttons" "false"
EndSection
Y añadir en rc.local la línea:
inputattach --microsoft /dev/ttyS0
Recordemos que con systemd el fichero rc.local no funciona, hay que activarlo como cuentan aquí.

Los ratones, pasados 25 años o más de su fabricación siguen funcionando. Ya no se hacen ratones como los de antes.

martes, 3 de diciembre de 2019

Bloqueo de funciones en impresora multifunción Epson WF-8590.

No soy muy partidario de las impresoras de inyección, ya que solo las veo útiles en un entorno donde se imprime de forma constante y abundante durante todo el año, pero como nos enviaron unas cuantas a cada centro no nos queda otra que lidiar con ellas. Las Epson WF-8590 no han salido problemáticas, de momento aguantan con las recargas de tinta genérica y se estropean lo normal.

Debido a que permiten ser usadas como fotocopiadora de manera sencilla me encuentro con el problema de tener tentadoras fotocopiadoras de uso público en sitios no controlados. En un centro educativo eso no puede ser, ya que es fácil imaginar que pronto empieza a haber fotocopias de extranjis. Para evitarlo bloqueo las funciones de fotocopia e impresión desde pendrive si sé que va a estar en un sitio sin supervisión.

Para ello nos conectamos a la IP de la impresora, metemos la contraseña de administración y configuramos el bloqueo de control de acceso. Esto hará que no se puedan usar las opciones de fotocopiado e impresión usando pendrives desde panel táctil de la impresora.


Al restringir el acceso me veo obligado a crear un par de usuarios:


Un usuario "administrador" con todos los permisos:


Y un usuario "usuario" con permiso para escanear e imprimir. Este será el que utilicen los usuarios rasos:


Con esto queda bloqueado el acceso por el panel, pero al haber activado el control de acceso necesitamos configurar un usuario y contraseña en Windows (en Linux esto no es necesario), a pesar de haber indicado en la configuración anterior que se "Permite imprimir y digitalizar sin información de autenticación". Parece ser un bug del firmware.

Para ello, en las preferencias de la impresora en Windows configuramos el usuario que definimos anteriormente:


Esto realmente lo que hace es meter en el registro de Windows estas claves:


El inconveniente es que hay que hacerlo para todos los usuarios de Windows uno a uno en cada PC y como no nos gusta repetir una y otra vez la misma configuración hemos definido un script que añade esas claves al registro:
@echo off
rem claves-epson.bat
reg import c:\windows\epson-usuario.reg
El fichero c:\windows\epson-usuario.reg sería:
Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\EPSON\PRINTER\EPSON WF-8590 Series]
"EPRFPW"="usuario"
"EPRFUM"="abcd1234"
Este último fichero lo mas adecuado es que lo creemos exportando desde regedit la clave "HKEY_CURRENT_USER\Software\EPSON\PRINTER\EPSON WF-8590 Series" desde un usuario con los datos configurados, ya que no es un fichero de texto ASCII al uso y tiene algunos bytes raros ocultos, por lo que no se puede crear con un editor de textos.

Para que se ejecute el script en el inicio de sesión de cada usuario de forma automática habría que ponerlo en la carpeta de scripts de inicio de todos los usuarios. En el caso de Windows 8 sería: "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp". En los otros Windows esta carpeta tiene ubicaciones distintas.

Y con esto tenemos la impresora controladita a nuestro gusto.

Es curioso que esto sea noticiable, pero es bueno saber que en SpaceX también explotan cosas:



La noticia es que un prototipo de la Starship ha explotado durante una prueba de presurización máxima. Cuando pruebas al máximo la presurización de un prototipo no es infrecuente que explote. De hecho, esa es la idea.

La Starship es bonita hasta cuando explota.

martes, 19 de noviembre de 2019

Avisos multicanal desde monit.

Aunque ya he usado monit para avisar de determinados eventos, hay veces que necesito una notificación inmediata. En concreto quería monitorizar punto wifi que sospechosamente perdía la conexión más de lo habitual.

Para ello decidí no limitarme a mandar un "alert" cuando se pierde el ping, sino que lanzo un script que hará más ruido:
check host punto-wifi with address 192.168.0.1
       if failed icmp type echo count 3 with timeout 5 seconds then exec "/usr/local/bin/aviso-wifi"
1. Avisos multicanal.

El script aviso-wifi usa distintos tipos de aviso por diferentes cauces:
# cat /usr/local/bin/aviso-wifi 
#!/bin/bash

MENSAJE="Otra vez desconectado $(date)!"

#Log del sistema
echo $MENSAJE >> /var/log/wifilog.log

#Correo
mailsend -to destinatario@gmail.com \
      -from emisor@gmail.com \
      -ssl -smtp smtp.gmail.com -port 465 \
      -sub "Aviso" \
      -M "$MENSAJE" \
      +cc +bc -q -auth -user "emisor@gmail.com" -pass "mipassword"

#Mensaje push al móvil android
PUSHKEY="APcdkK"
curl --data "key=$PUSHKEY&title=Aviso&msg=$MENSAJE" https://api.simplepush.io/send

#Mensaje SMS al móvil android
MOVIL="+34855123456"

curl -X POST https://textbelt.com/text \
       --data-urlencode phone="$MOVIL" \
       --data-urlencode message="$MENSAJE" \
       -d key=textbelt

exit 0
Los distintos métodos usados son:
  • Guarda un registro en /var/log/wifilog.log.
  • Envía un correo mediante gmail usando mailsend, que tendríamos que instalar previamente.
  • Envía un mensaje push a nuestro móvil mediante la aplicación simplepush.io, que saltará de forma inmediata. Es tan sencillo como instalar esta aplicación en el móvil y poner en el script la PUSHKEY que nos aparece al abrirla, que identifica nuestro móvil de forma única como destinatario.
    Como pequeño inconveniente tenemos que los mensajes son gratuitos los 7 primeros días, luego hay que comprar la aplicación (cuesta unos merkels) para poder recibir mensajes. Se pueden establecer alarmas sonoras distintas en función de diversos tipos de mensajes push.
  • Por último, por volver a los 90, mandamos un SMS a nuestro móvil (+34855123456, es un número ficticio, pon el tuyo empezando por +34). Este servicio de textbelt permte mandar un único SMS al día gratis, pero si nos resulta útil podemos comprar paquetes SMS prepago.

Tanto simplepush.io como textbelt son dos de los muchos servicios push y SMS que hay. Yo he puesto estos porque los encontré pronto y me resultaron fáciles de usar mediante sencillas peticiones web con el comando curl.

Por supuesto, se podría ampliar el script con otros medios de aviso, por ejemplo mensajes telegram o cualquier otro sistema que se pueda hacer mediante scripts.

2. Avisos sonoros.

Un escalón mas es hacer sonar un aviso por los altavoces del PC del aula donde se detecta el evento, para que si es especialmente grave el usuario tome medidas. El script sería:
# cat /usr/local/bin/aviso-audio 
#!/bin/bash

#Subimos el volumen al máximo en el alsamixer
amixer set Master unmute
amixer set Master 100%

#Si hay un usuario logado, conectamos con su pulseaudio para subir el volumen al máximo
user=$(who | grep "(:0)" | head -1 | cut -f1 -d" "); 
if  [ -n "$user" ]
then
  su $user -c "DISPLAY=:0 pactl set-sink-volume 0 150%"
  su $user -c "DISPLAY=:0 pactl set-sink-mute 0 0"
fi

#Si esta instalado placasiatic es una siatic y podemos encender por comando la barra de sonido.
test -f /usr/sbin/placasiatic && placasiatic sonido up

if  [ -n "$user" ]
then
  while true
  do
     ffplay /root/scripts/locucion.mp3 -nodisp -nostats -hide_banner 
     su $user -c "DISPLAY=:0 zenity --info  --width 400 --height 100  --title='Desconexion de sistema' --text='Avise al administrador informatico de forma urgente.'"
  done
else
   ffplay /root/scripts/locucion.mp3 -nodisp -nostats -hide_banner -loop 0
fi


exit 0
Comentamos:
  • Con amixer quitamos el mute (silencio) y subimos al 100% del volumen del canal de audio Master.
  • Si hay un usuario con sesión iniciada, conectamos con su instancia pulseaudio para subir al máximo el volumen allí mediante pactl y hacer unmute.
  • Si estamos en una Siatic encendemos la barra de sonido mediante el relé. Esto garantiza que si el usuario ha apagado la barra la encendemos de nuevo usando la utilidad placasiatic. Por supuesto, si los altavoces del PC son convencionales y están apagados físicamente en su interruptor, no saldrá nada de sonido. Es algo que puede pasar y para lo cual no tenemos solución.
  • Por último, se reproduce una locución en bucle infinito codificado en mp3 mediante ffplay. Si hay usuario con sesión iniciada se muestra además un mensaje de texto en su escritorio mediante zenity.
  • La locución se puede generar fácilmente con texttomp3.
Para ejecutar este script desde el del apartado anterior es suficiente con añadir:
nohup /usr/local/bin/aviso-audio >/dev/null 2>&1 &


Cuando lo extraordinario empieza a ser habitual es señal de que se está haciendo bien: SpaceX ha lanzado una nueva carga de satélites Starlink.


Se lanzan 60 microsatélites Starlink (que mientras se van desplegando en las próximas semanas se podrán ver como un tren por el cielo nocturno una vez más), la etapa inicial del Falcon reutilizado por cuarta vez aterriza en la plataforma barcaza Of Course I Still Love You y la cofia se reutiliza por primera vez y se recupera mediante un barco.

Y todavía hay gente que afirma que todo lo relacionado con Elon Musk es humo.

viernes, 15 de noviembre de 2019

Teamviewer en Manjaro Linux

Como cualquier informático me toca muchas veces dar soporte a familiares, amigos y arrimados usando teamviewer. Cuando tienes Ubuntu o Fedora están en la página de descarga los paquetes preparados para descargar e instalar, pero cuando trabajamos con otra distribución con sistema de paquetes propio no nos queda otra que bajar el fichero .tar.xz con el ejecutable y los ficheros necesarios, descomprimirlo a mano y ejecutarlo.

Exite un paquete AUR de teamviewer para Manjaro, pero la verdad es que la versión original se actualiza tanto que el paquete no va muy fino y muchas veces no funciona. Es mejor descargar el tar.xz y trabajar directamente con él.

Un problema frecuente es que al abrir el programa se nos queda en estado de "error de conexión" y no funciona. Suponiendo que tengamos red y que no haya problema en los servidores de Teamviewer la razón típica es la versión del programa que tenemos instalado. A día de hoy solo Teamviewer 14 me funciona en el Manjaro, las versiones anteriores no conectan. Desconozco si es algo propio de Manjaro o afecta a otras implementaciones.

El segundo problema que me pasa es que cuando lanzamos el ejecutable de teamviewer:
$ cd teamviewer
$ ./teamviewer
Init...
CheckCPU: SSE2 support: yes
Checking setup...
Launching TeamViewer ...
Starting network process (no daemon)
Network process started (3703)
Launching TeamViewer GUI ...
Y ahí se queda sin mostrar nada.

El motivo real es que faltan por instalar dependencias, pero resulta que el ejecutable es tan mudito que no da error ninguno. Esto me recuerda un programa de Indra que vi una vez cuyo código tenia:
Sub XXXX(....)
   Try
      ...
      ...
   Catch 
   End Try
End Sub
en todos los métodos. Nunca daba un error.

Bueno, pues investigando el paquete AUR de teamviewer se pueden ver las dependencias necesarias, que son:
qt5-webkit
qt5-quickcontrols
hicolor-icon-theme
qt5-x11extras
Si instalamos esos paquetes con pacman la cosa funciona sin problema.



El cometa interestelar 2I/Borisov. El segundo objeto extrasolar detectado en la historia, detectado el 30 de agosto de 2019 por el astrónomo aficionado Borisov en Crimea (Rusia, diga lo que diga Ucrania) con un telescopio modesto. Dado el aviso, la comunidad de aficionados se volcó en hacer seguimiento del objeto y determinar su órbita en los días siguientes, resultando que era un cuerpo que venía de fuera del Sistema Solar e iba a irse de nuestro lado. Ciencia ciudadana se llama.

La última noticia es una foto del Hubble y se ha confirmado que es un cometa que trae agua de otro sistema solar.


Teniendo en cuenta que 1I/ʻOumuamua se descubrió 2 años antes está claro que esos visitantes de fuera son mucho mas frecuentes de lo que podríamos pensar. Sería maravilloso ver llegar otro con tiempo para mandar una sonda...

Transferencia rápida de ficheros del móvil android al PC.

Vaya, vaya, que meses mas ocupados entre inicios de curso y repetición de elecciones. A ver si aprendéis a votar bien de una vez y podemos avanzar.

Vamos a contar alguna cosita. Después del verano los móviles vienen petados de fotos y vídeos y cuando toca hacer limpieza y descargar unos cientos o miles de fotos siempre me pasa lo mismo. Si pincho el móvil con el cable USB e intento copiarlas a golpe de ratón resulta que se hace a paso de, tortuga, con mucha posibilidad de que se quede parado o aborte en algún punto.

Esto antes no era así pero en algún momento alguien ha decidido que la conexión USB del móvil no se haga como si fuera un pendrive, sino que use un protocolo MTP que por su (mala) implementación penaliza enormemente la transferencia de muchos archivos. Es el problema de añadir capas a las capas, que al final cada byte recorre tantos escalones que el proceso se eterniza. Una vez vi un programa hecho por Indra que mandaba datos en formato XML mediante SOAP, el cual transforma lo que envía a formato XML. Meter un XML dentro de un XML, que buena idea.

¿Tiene esto solución? Claro, usando la línea de comandos. Para ello necesitamos:

  1. Habilitar la depuración USB en el móvil.
  2. Windows: instalar los drivers y programas para el ADB.
  3. Linux: instalar los paquetes ADB.
  4. Repasar cómo funciona la conexión ADB y sus comandos.

Una vez hecho esto, tenemos que conectar el móvil con el cable USB al PC y ejecutar:
# adb shell
para conectar a una shell dentro del móvil. Una vez allí, mediante los comandos "cd" y "ls" exploramos las diferentes carpetas para dar con la ruta donde están los ficheros multimedia que queremos transferir. Esto puede cambiar de un móvil a otro y por eso no puedo dar un path universal, en mi caso están concretamente en la ruta /storage/sdcard0/DCIM/camera.

Un vez averiguada la ruta, salimos con "exit". Ahora, para transferir los archivos:
# mkdir camera
# adb pull -a /storage/sdcard0/DCIM/camera camera
El comando "pull" de adb realizar una transferencia recursiva de ficheros desde la ruta /storage/sdcard0/DCIM/camera de la memoria del móvil a la ruta ./camera del PC. Esta transferencia de ficheros se realiza a una velocidad endiablada: la velocidad de mover bytes sin tener que hacerlos pasar por capas y protocolos farragosos. En un tiempo razonable tenemos en el PC todos los ficheros.


Ya he hablado de la sonda india Chandrayaan 2, que ha costado menos que las elecciones del 10N en nuestra patria. En su momento esas cabezas parlantes que salen por la TV haciendo con que dan información se limitaron a comentar que el aterrizador Vikram se había estrellado. En realidad se perdió el contacto con él, no se sabe que pasó.

Bueno, pues el aterrizador era una parte de la misión. El resto ha seguido adelante haciendo fotos desde el orbitador:


Esto son fotos con una resolución de 30cm por pixel de la superficie lunar, una resolución nunca vista. Lástima que su órbita no la lleve por encima del lugar de alunizaje del Apolo XI.

Insisto: 140 millones de dolares. El 10N han sido 136 millones de euros.

jueves, 12 de septiembre de 2019

Toquemos el Touchpad de los TC11.

Nuestros Techcomputer TC11 son unos clónicos que van a darnos algunos quebraderos de cabeza hasta hacerlos funcionar completamente con Linux. Uno de estas puñetas es el touchpad, que se queda congelado de forma aleatoria obligando a reiniciar o cerrar sesión para hacerlo de nuevo utilizable.

Hemos probado con distintos kernels y configuraciones de /usr/share/X11/xorg.conf.d/70-synaptics.conf sin éxito. Es el típico problema que un día con una actualización se arreglará de forma inesperada y nadie sabrá que ha pasado, pero mientras tanto hay que dar una solución.

El touchpad se maneja con el módulo "i2c_hid". Si descargamos y cargamos dicho módulo cuando se queda pillado comprobamos que todo vuelve a funcionar. Entonces, en plan chapuza...¿podemos hacer un script que pueda ejecutar el usuario de forma sencilla para realizar tal tarea cuando se quede pillado? Sería una solución rápida y sucia pero al final nos sacaría del apuro. La respuesta a esta cuestión es "SÍ".

El script sería:
# cat /usr/local/bin/reinicio_touchpad 
#!/bin/bash
rmmod i2c_hid
modprobe i2c_hid
exit 0
Este script no puede usarse por un usuario regular, ya que para ejecutar rmmod/modprobe necesitas ser administrador. La solución es utilizar sudoers, añadiendo en /etc/sudoers la línea:
ALL   ALL = (ALL) NOPASSWD: /usr/local/bin/reinicio_touchpad
Para que el usuario no tenga que abrir un terminal y lanzar el script usamos un atajo de teclado de XFCE4, asociando la tecla F12 a dicha acción:
# cat /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-keyboard-shortcuts.xml
<?xml version="1.0" encoding="UTF-8"?>

<channel name="xfce4-keyboard-shortcuts" version="1.0">
  <property name="commands" type="empty">
    <property name="default" type="empty">
      <property name="&lt;Alt&gt;F1" type="string" value="xfce4-popup-applicationsmenu"/>
      <property name="&lt;Alt&gt;F2" type="string" value="xfce4-appfinder --collapsed">
        <property name="startup-notify" type="bool" value="true"/>
      </property>
      <property name="&lt;Alt&gt;F3" type="string" value="xfce4-appfinder">
        <property name="startup-notify" type="bool" value="true"/>
      </property>
      <property name="&lt;Primary&gt;&lt;Alt&gt;Delete" type="string" value="xflock4"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;l" type="string" value="xflock4"/>
      <property name="XF86Display" type="string" value="xfce4-display-settings --minimal"/>
      <property name="&lt;Super&gt;p" type="string" value="xfce4-display-settings --minimal"/>
      <property name="&lt;Primary&gt;Escape" type="string" value="xfdesktop --menu"/>
      <property name="XF86WWW" type="string" value="exo-open --launch WebBrowser"/>
      <property name="XF86Mail" type="string" value="exo-open --launch MailReader"/>
    </property>
    <property name="custom" type="empty">
      <property name="override" type="bool" value="true"/>
      <property name="&lt;Primary&gt;F12" type="string" value="/usr/local/bin/brillo"/>
      <property name="F12" type="string" value="gksu /usr/local/bin/reinicio_touchpad"/>
    </property>
  </property>

  <property name="xfwm4" type="empty">
    <property name="default" type="empty">
      <property name="&lt;Alt&gt;Insert" type="string" value="add_workspace_key"/>
      <property name="Escape" type="string" value="cancel_key"/>
      <property name="Left" type="string" value="left_key"/>
      <property name="Right" type="string" value="right_key"/>
      <property name="Up" type="string" value="up_key"/>
      <property name="Down" type="string" value="down_key"/>
      <property name="&lt;Alt&gt;Tab" type="string" value="cycle_windows_key"/>
      <property name="&lt;Alt&gt;&lt;Shift&gt;Tab" type="string" value="cycle_reverse_windows_key"/>
      <property name="&lt;Alt&gt;Delete" type="string" value="del_workspace_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;Down" type="string" value="down_workspace_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;Left" type="string" value="left_workspace_key"/>
      <property name="&lt;Shift&gt;&lt;Alt&gt;Page_Down" type="string" value="lower_window_key"/>
      <property name="&lt;Alt&gt;F4" type="string" value="close_window_key"/>
      <property name="&lt;Alt&gt;F6" type="string" value="stick_window_key"/>
      <property name="&lt;Alt&gt;F7" type="string" value="move_window_key"/>
      <property name="&lt;Alt&gt;F8" type="string" value="resize_window_key"/>
      <property name="&lt;Alt&gt;F9" type="string" value="hide_window_key"/>
      <property name="&lt;Alt&gt;F10" type="string" value="maximize_window_key"/>
      <property name="&lt;Alt&gt;F11" type="string" value="fullscreen_key"/>
      <property name="&lt;Alt&gt;F12" type="string" value="above_key"/>
      <property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Left" type="string" value="move_window_left_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;End" type="string" value="move_window_next_workspace_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;Home" type="string" value="move_window_prev_workspace_key"/>
      <property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Right" type="string" value="move_window_right_key"/>
      <property name="&lt;Primary&gt;&lt;Shift&gt;&lt;Alt&gt;Up" type="string" value="move_window_up_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_1" type="string" value="move_window_workspace_1_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_2" type="string" value="move_window_workspace_2_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_3" type="string" value="move_window_workspace_3_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_4" type="string" value="move_window_workspace_4_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_5" type="string" value="move_window_workspace_5_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_6" type="string" value="move_window_workspace_6_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_7" type="string" value="move_window_workspace_7_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_8" type="string" value="move_window_workspace_8_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;KP_9" type="string" value="move_window_workspace_9_key"/>
      <property name="&lt;Alt&gt;space" type="string" value="popup_menu_key"/>
      <property name="&lt;Shift&gt;&lt;Alt&gt;Page_Up" type="string" value="raise_window_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;Right" type="string" value="right_workspace_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;d" type="string" value="show_desktop_key"/>
      <property name="&lt;Primary&gt;&lt;Alt&gt;Up" type="string" value="up_workspace_key"/>
      <property name="&lt;Super&gt;Tab" type="string" value="switch_window_key"/>
      <property name="&lt;Primary&gt;F1" type="string" value="workspace_1_key"/>
      <property name="&lt;Primary&gt;F2" type="string" value="workspace_2_key"/>
      <property name="&lt;Primary&gt;F3" type="string" value="workspace_3_key"/>
      <property name="&lt;Primary&gt;F4" type="string" value="workspace_4_key"/>
      <property name="&lt;Primary&gt;F5" type="string" value="workspace_5_key"/>
      <property name="&lt;Primary&gt;F6" type="string" value="workspace_6_key"/>
      <property name="&lt;Primary&gt;F7" type="string" value="workspace_7_key"/>
      <property name="&lt;Primary&gt;F8" type="string" value="workspace_8_key"/>
      <property name="&lt;Primary&gt;F9" type="string" value="workspace_9_key"/>
      <property name="&lt;Primary&gt;F10" type="string" value="workspace_10_key"/>
      <property name="&lt;Primary&gt;F11" type="string" value="workspace_11_key"/>
      <property name="&lt;Primary&gt;F12" type="string" value="workspace_12_key"/>
    </property>
  </property>
</channel>
De esta manera, a nivel de sistema (ya que lo definimos /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/ en lugar del home del usuario) asociamos la ejecución de "gksu /usr/local/bin/reinicio_touchpad" a la tecla F12.

Si nos fijamos hay un bonus track: con CTRL-F12 ejecutamos el script "brillo". Este script pone a 100% el brillo de la pantalla del portátil. Esto es debido a que hemos observado a que a veces (algo relacionado con la suspensión o algún enredo similar) baja mucho y no es trivial subirlo de nuevo.
# cat /usr/local/bin/brillo
#!/bin/bash
xrandr --output eDP1 --brightness 1.0
exit 0
Como no vamos a hacer todo esto a mano portátil a portátil, ponemos finalmente las reglas puppet que meten todos estos ficheros y configuraciones anteriores en su sitio:
#No tiene control de brillo con Fn, script que pone el brillo al 100% de nuevo
file {"/usr/local/bin/brillo":
  owner => root , group => root , mode => 755 ,
  source => "puppet:///modules/xubuntu18_portatil_ajustes/brillo",
}
#Script que reinicia el touchpad quitando y poniendo el módulo
file {"/usr/local/bin/reinicio_touchpad":
  owner => root , group => root , mode => 755 ,
  source => "puppet:///modules/xubuntu18_portatil_ajustes/reinicio_touchpad",
}
line { sudoers:
 file => "/etc/sudoers",
 line => "ALL   ALL = (ALL) NOPASSWD: /usr/local/bin/reinicio_touchpad",
 ensure => present
}
# Atajos de teclado: CTRL-F12 ejecuta "brillo", F12 ejecuta "gksu renicio_touchpad"
file {"/etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-keyboard-shortcuts.xml" :
 owner => root , group => root , mode => 644 ,
 source => "puppet:///modules/xubuntu18_portatil_ajustes/xfce4-keyboard-shortcuts.xml"
}
Cambia "xubuntu18_portatil_ajustes" por la tarea puppet que uses en tu caso para configurar los portátiles.

La nave india Chandrayaan 2 finalmente aterrizó en la Luna, aunque durante un tiempo nos hicieron creer que se había pegado un castañazo como hizo la sonda israelí.

El reto ahora es saber por qué se cortó la comunicación en el último momento y determinar el estado operativo de la sonda. Esto convierte a la India en el 4 país en aterrizar sobre nuestro satélite, en su caso con misiones muy baratas y optimizadas: mandar la sonda con un rover ha costado 130 millones de euros, más o menos unos 20 km de autovía en España.


Y otra imagen maravillosa:



Algún día de este siglo, un descendiente de esta máquina aterrizará en Marte con humanos dentro.

viernes, 26 de julio de 2019

Cambiar opción por defecto de Grub para el siguiente arranque

Tengo un grupo de máquinas con arranque dual Linux/Windows. Normalmente arrancan con Linux, siendo esta la opción por defecto en el Grub. Hay veces que estoy conectado a ellas de forma remota y quiero arrancar Windows para hacer algún cambio o actualización en dicho sistema.

Podría modificar la configuración de Grub para arrancar con Windows. El problema es que una vez está cambiada no es sencillo revertir el cambio desde el propio Windows y hay que desplazarse hasta la máquina para elegir y configurar manualmente el arranque de Linux. Yo solo quiero arrancar una única vez el Windows y que luego todo siga igual.

La solución es usar el desconocido comando grub-reboot, que permite especificar cual es la opción de arranque por defecto para el siguiente reinicio. Tras ese reinicio, al apagar la máquina todo volverá a su ser habitual y arrancará el sistema por defecto que hayamos tenido habitualmente.

Lo primero es saber como identificar la opción de arranque a programar:
# grep -e "menuentry\ " -e "submenu" /boot/grub/grub.cfg
menuentry 'Ubuntu'.....
submenu 'Opciones avanzadas para Ubuntu' ...
   menuentry 'Ubuntu, con Linux 4.15.0-54-generic' ...
   menuentry 'Ubuntu, con Linux 4.15.0-54-generic (recovery mode)' ...
   menuentry 'Ubuntu, con Linux 4.15.0-20-generic' ...
   menuentry 'Ubuntu, con Linux 4.15.0-20-generic (recovery mode)' ...
menuentry 'Windows 7 (en /dev/sda1)' ...
menuentry "Clonezilla live 2.5.5.38" ...
menuentry "Arranque por red: PXE" ...
El comando grub-reboot admite un parámetro numérico (empezando por 0, que identifica la posición en la lista), dos o más números separados por > (para indicar posiciones dentro de submenús, por ejemplo 1>2) o un texto con la etiqueta de la opción de arranque. Por ejemplo, para programar el arranque del Windows 7 haríamos:
# grub-reboot  'Windows 7 (en /dev/sda1)' 
# reboot
Y ya está, el sistema se reinicia y arranca en Windows. Podemos conectar a él con rdesktop y hacer nuestras cosas. Cuando ese Windows se apague volverá a arrancar en Linux.

Bueno, ayer 25 de julio SpaceX consiguió elevar sin anclajes hasta una altura de 20 metros la StarHopper, el prototipo de la nave que permitirá poner humanos en la Luna y Marte en el futuro.


Prueba superada. Todo listo para pasar a la siguiente fase.

martes, 23 de julio de 2019

Control del estado de salud de la batería de nuestros portátiles.

El tener muchos portátiles iguales facilita que cuando la batería de uno de ellos se degrada y empieza a funcionar peor podemos comparar entre ellos e identificar las que están degenerando de una forma real.

Como en nuestro caso la garantía de los contratos cubre la batería durante varios años hemos pensado en cómo hacer una revisión de algunos modelos que daban signos de agotamiento, con el objeto de que nos cambien las baterías defectuosas antes de que expire la garantía.

En mi caso son portátiles Inves Helio-1106L, que son remarcados de Clevo W310CZ (y que se vende con otros etiquetados como Stone N120, Zoostorm 7270-9062, ...). Todos son el mismo.

1. Un poco de teoría de baterías.

Las características de una batería se definen por los mAh y el voltaje. Por ejemplo, para nuestros portátiles Inves tenemos baterías de 2800mAh y 14.8V, como se puede apreciar en la pegatina que traen (los Clevo originales traían baterías de 2200mAh, pero seguramente fueron ampliadas por requisitos del contrato, las cuales pedían una duración de 4 horas de uso medio según diversos tests).

Multiplicando ambos valores obtenemos la capacidad de energía acumulada, que sería 2.800*14.8=41.440wh. Eso significa que si el portátil consume de promedio 11w encendido, con 41.4wh tiene para 41.4/11=3.76 horas de funcionamiento. Con el tiempo y los ciclos de carga-descarga esa capacidad va mermando y es ese porcentaje de merma lo que queremos medir.

Para saber en tiempo real los datos de la batería usamos el comando upower. Cojo un portátil Inves cualquiera, lo cargo al 100% y con él enchufado a la corriente hago:
# upower -i /org/freedesktop/UPower/devices/battery_BAT0  
    .....
    energy:              30,0884 Wh
    energy-full:         30,0884 Wh 
    energy-full-design:  41,44 Wh  
    .....
    capacity:            72,6071%
Lo interesante: la capacidad por diseño de batería es 41.44wh. La capacidad real actual con la batería llena es de 30.08wh, por lo que la capacidad real en este momento es un 72% de la original. En nuestro caso la batería se considera averiada a efectos de garantía si está por debajo del 30%, por lo que de momento se libra de ser cambiada. De esta forma chequearemos todos los portátiles que deseemos.

Si nuestra batería tiene una ruta de acceso distinta de /org/freedesktop/UPower/devices/battery_BAT0 la podemos averiguar con:
# upower -e | grep BAT

2. Testeando la batería.

Una vez embarcado en esto, se me ha ocurrido chequear la bateria durante un ciclo de descarga, para hacerme una idea de su estado, duración y comprender el proceso de drenaje. Los datos que tomaré serán:
# upower -i $(upower -e | grep 'BAT') | grep -E "state|to\ full|percentage|energy"
    state:               fully-charged
    energy:              30,0884 Wh
    energy-empty:        0 Wh
    energy-full:         30,0884 Wh
    energy-full-design:  41,44 Wh
    energy-rate:         0 W
    percentage:          100%
Con los que haré un bucle que cada minuto guarde ese resultado en un log hasta que el portátil se apague por falta de energía. Para conseguir simular un uso continuo y exhaustivo desconectaré el apagado de pantalla (con xset), pondré el brillo al máximo (con xrandr), simularé mover el ratón cada minuto para evitar entradas en modo ahorro de energía (con xte) y mientras tanto reproduciré un vídeo de youtube de varias horas de duración conectado a la red wifi.

Este es el sript check-bateria.sh
#!/bin/bash
xset s off -dpms

while true
do
   xrandr --output eDP-1 --brightness 1.0
   xte   'mousermove 4 4'
   date >>  ~/log-bateria.txt
   upower -i $(upower -e | grep 'BAT') | grep -E "state|to\ full|percentage|energy" >> ~/log-bateria.txt
   sleep 60
   xte  'mousermove -4 -4'
done
exit 0
Los pasos a seguir son encender el portátil con la batería cargada al 100%, iniciar sesión, conectar la wifi, iniciar el vídeo de Youtube y ejecutar el script anterior, dejando todo en marcha hasta que el portátil se apague.

Un resumen de los datos capturados sería:
lun jul 22 10:04:51 CEST 2019
state:               fully-charged
energy:              30,0884 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         0 W
percentage:          100%
.................................
.................................
lun jul 22 10:06:51 CEST 2019
state:               discharging
energy:              30,0884 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         0 W
percentage:          100%
lun jul 22 10:07:51 CEST 2019
state:               discharging
energy:              29,6 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         0 W
percentage:          98%
.................................
.................................
lun jul 22 10:09:52 CEST 2019
state:               discharging
energy:              29,0968 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         14,9184 W
percentage:          96%
.................................
.................................
lun jul 22 10:14:52 CEST 2019
state:               discharging
energy:              28,1052 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         14,9057 W
percentage:          93%
.................................
.................................
lun jul 22 11:47:59 CEST 2019
state:               discharging
energy:              2,812 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         16,8519 W
percentage:          9%
lun jul 22 11:48:59 CEST 2019
state:               discharging
energy:              2,0276 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         43,2254 W
percentage:          6%
lun jul 22 11:49:59 CEST 2019
state:               discharging
energy:              1,7316 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         29,9519 W
percentage:          5%
lun jul 22 11:51:00 CEST 2019
state:               discharging
energy:              1,4504 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         17,1257 W
percentage:          4%
lun jul 22 11:52:00 CEST 2019
state:               discharging
energy:              1,1544 Wh
energy-empty:        0 Wh
energy-full:         30,0884 Wh
energy-full-design:  41,44 Wh
energy-rate:         17,6331 W
percentage:          3%
Conclusiones:
  • El consumo instantáneo (energy-rate) está entre 15 y 17.5 watios de promedio. Con 30.0884wh de capacidad aguantaría una hora y 48 minutos de uso continuo intensivo = 1.8 horas expresado en valor decimal.
  • Con los 41.44 Wh iniciales nos daría dos horas y media de uso intensivo, 2.5 horas en valor decimal.
  • Según upower la batería está en el 72,6071% de su capacidad inicial de fábrica. Para cotejar con la realidad hallamos la razón de las duraciones con el cociente 1.8/2.5 = 0.72. Correcto, coincide con lo estimado por upower.
Como la batería aún retiene un 72% de su capacidad de carga inicial no podemos reemplazarla usando la garantía. Bueno, otra vez será.


Ayer día 22 la India lanzó la nave Chandrayaan 2 en dirección a la Luna, con la intención de aterrizar un rover y convertirse en el cuarto país en la historia que ha puesto de forma controlada una sonda de exploración en la superficie de nuestro satélite.


Esto lo hace sin asistencia de ningún otro país, con su programa espacial propio y de forma autónoma.

El coste de la misión es de 126 millones de euros. El coste de una repetición de elecciones en 2019 en el Reino de España sería superior a los 140 millones de euros. Como vemos, cada uno hace lo que puede según su capacidad.

martes, 2 de julio de 2019

Imagen clonezilla conteniendo sistema DRBL para distribuir imágenes de Ubuntu 18 (y otras).

En esta entrada contamos como instalar el sistema DRBL en un PC con Debian 9 para servir imágenes de Ubuntu 18, imágenes duales y variadas. En dicho post enumeré los pasos para partiendo de un Debian Stretch llegar a tener un servidor DRBL, pero por precaución he hecho una imagen Clonezilla (versión clonezilla-live-2.5.5-38) de todo el sistema instalado por si se me va el portátil que uso como servidor para la clonación multicast.

La imagen es una imagen de partición sda1 y se restaura por tanto usando Clonezilla con la opción "restore part". Si deseamos clonarla sobre una partición distinta a sda1 el propio clonezilla en sus versiones modernas nos permite elegir la partición donde cargarla (por si queremos tener un equipo con arranque dual, con otro sistema para su uso diario y un DRBL para cuando queramos clonar) A su vez dentro contiene la imagen /home/partimag/miniportatiles-xubuntu-13062018 que usamos en nuestros centros. Después podremos poner en /home/partimag todas la imágenes adicionales que necesitemos.

La contraseña de root del sistema DRBL es "linex".

El enlace es de descarga de la imagen alojada en Mega es drbl-debian-stretch.tgz.


miércoles, 19 de junio de 2019

Captura remota de pantalla en las sesiones de los alumnos.

Una profesora me pidió este curso escribir un programa para realizar capturas de pantalla de los escritorios de los alumnos de forma periódica. El objetivo era tener en su carpeta personal una colección con las capturas de pantalla realizadas cada X minutos de los PC de todos los alumnos del aula, para controlar a posteriori el trabajo que habían estado haciendo durante la clase.

Capturar la pantalla desde un script no es complicado, aunque si quieres hacerlo de forma remota y traerlo al ordenador del profesor hay que afinar un poco la cosa.

Como paso imprescindible e inicial es obligatorio que entre el ordenador del profesor y los de los alumnos exista una relación de confianza ssh que nos permita conectar desde la cuenta de root del PC del profesor sin necesidad de introducir contraseña. Tras generar y copiar las claves, en /root/.ssh/id_rsa y /root/.ssh/id_rsa.pub del profesor tendremos las claves pública y privada, mientras que en /root/.ssh/... de los alumnos tendremos la clave privada.

Una vez resuelto el problema de la comunicación entre profesor y alumnos, tenemos que pensar en los posibles escenarios que nos encontramos en nuestras aulas:

  • Aula aislada: profesor y alumnos están una red privada con rango de direcciones propio, mediante una VLAN creada en el switch. En mi caso con los alumnos tienen direccionamamiento 192.168.0.200-253, sin importar si se usan portátiles o equipos de sobremesa.
  • Aula integrada: profesor y alumnos no están aislados, si no que usan direcciones públicas de la red del centro. Hay que encontrar una manera de que el PC del profesor sepa a priori cuales son las IP de los PC de alumnos que dependen de él. Esta manera consiste en definir en el fichero /etc/escuela2.0 (el que usamos para configuraciones de nuestras máquinas) una variable llamada IES_IP_ALUMNOS, en la cual indicamos en rango de IP donde están los PC de los alumnos, por ejemplo: 172.23.241.70-85. El valor tomado por defecto es 192.168.0.200-253
  • Thinclients: en este caso también es un aula aislada, con direcciones del rango 192.168.0.200 en adelante, pero con una particularidad consistente en que con los thinclients las sesiones de los alumnos se ejecutan en el PC del profesor, en una sesión privada en la que la captura de pantalla se hace de una forma diferente. Para saber si el aula tiene thinclients usaremos la variable IES_ISLTSP de /etc/escuela2.0

Aparte de eso, la aplicación debe tener un interface gráfico sencillo para el usuario. Por esta razón he optado por hacer la aplicación de captura usando python e interface gráfico Qt con PyQt5. Para ello uso Qt Designer para diseñar el interface gráfico y Geany para escribir el programa.

Una vez implementado, los ficheros necesarios son distribuidos por la siguiente tarea puppet:
# cat xubuntu18_photomaton/manifests/init.pp
import "/etc/puppet/defines/*.pp"
class xubuntu18_photomaton {

  file {"/usr/local/bin/photomaton.py":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu18_photomaton/photomaton.py",
  }


  file {"/usr/local/bin/photomaton_ui.py":
        owner=>root, group=>root, mode=>755,
        source=>"puppet:///modules/xubuntu18_photomaton/photomaton_ui.py",
  }

  file {"/usr/local/bin/photomaton.jpg":        #/usr/share/icons/photomaton.jpg
        owner=>root, group=>root, mode=>644,
        source=>"puppet:///modules/xubuntu18_photomaton/photomaton.jpg",
  }


  file {"/usr/share/applications/Photomaton.desktop" :
          owner => root , group =>root, mode => 644 ,
          source => "puppet:///modules/xubuntu18_photomaton/Photomaton.desktop",
          notify => Exec['update-desktop'],
  }
 
  exec { "update-desktop":
          path => "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
          command => "/usr/bin/update-desktop-database",
          refreshonly => true,
          require => [File["/usr/local/bin/photomaton.jpg"],File["/usr/share/applications/Photomaton.desktop"]],
  }

  line { sudoers:
            file => "/etc/sudoers",
            line => "%teachers ALL = (ALL) NOPASSWD: /usr/local/bin/photomaton.py",
            ensure => present
  }

  package { 'python3-paramiko': ensure => 'installed' }
  package { 'python3-nmap': ensure => 'installed' }
  package { 'python3-configobj':   ensure => 'installed' }  
  package { 'python3-psutil': ensure => 'installed' }
  package { 'python3-pyqt5': ensure => 'installed' }
}
Como vemos la tarea instala varios paquetes necesarios, copia los ficheros y añade una linea a sudoers para permitir a los miembros del grupo teachers ejecutar el programa con permisos de root (necesario para acceder a los pc de los alumnos).

Vamos primero el código del programa principal:
# cat xubuntu18_photomaton/files/photomaton.py
#!/usr/bin/python3
from photomaton_ui import *
import sys
import socket
import subprocess
import os, errno
from datetime import datetime
import paramiko
Para acabar un pequeño detalle: en las au
import time
from configobj import ConfigObj
import psutil
import nmap

class MainWindow(QtWidgets.QMainWindow, Ui_Photomaton):
   def __init__(self, *args, **kwargs):
       QtWidgets.QMainWindow.__init__(self, *args, **kwargs)
       self.setupUi(self)
       #Conectamos los botones con los eventos
       self.iniciarCaptura.clicked.connect(self.capturar)
       self.visualizarCapturas.clicked.connect(self.visualizar)
       self.finalizar.clicked.connect(self.finalizarApp)
       #Definimos el entorno, creando el directorio destino y leyendo escuela2.0
       hostname = socket.gethostname()
       #Como se llama usando sudo, el usuario original está en SUDO_USER

       try:
           self.user=os.environ["SUDO_USER"]       
       except Exception as e:
           print("No eres usuario sudo. Abortando")
           #self.user="root"
           sys.exit(0)  

       home = os.path.expanduser("~"+self.user) #Directorio $HOME del usuario
       self.pathcapturas=home+"/capturas/"+hostname
       config = ConfigObj("/etc/escuela2.0")
       self.uso=config.get('USO') #infolab/siatic/...
       self.isltsp=config.get('IES_ISLTSP') #true si tiene thinclients
       self.ip_alumnos=config.get('IES_IP_ALUMNOS') # Rango de ip de los alumnos, para aulas de workstations.    
       self.capturando=False         
       if self.ip_alumnos is None or self.ip_alumnos == "" :
          self.ip_alumnos="192.168.0.200-253" # Rango por defecto en aulas con vlan
       try:
          comando="su "+self.user+" -c 'mkdir -p "+self.pathcapturas+"'"
          os.system(comando)
       except FileExistsError:
          # directory already exists
          pass     
         
   def finalizarApp(self):
       if self.capturando == False:
          sys.exit(0)
       else:
          self.capturando=False
    
   
   def visualizar(self):                            
       comando="su "+self.user+" -c 'nohup thunar "+self.pathcapturas+" &'" 
       os.system(comando)   
   
   def capturar_ip(self, ip):          
       
       s = socket.socket()       #Quizá esto se podria quiar, el ssh_client.connnect ya tiene timeout
       s.settimeout(30)
       address = ip
       port = 22         
       try:
           #Intenta conectar por ssh a ver si la IP acepta peticiones ssh
           s.connect((address, port))  #Si falla se va a la excepcion
           s.close()    
           #Conectamos por ssh y ejecutamos el comando de captura.
           ssh_client = paramiko.SSHClient()
           ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
           ssh_client.connect(ip, 22, 'root', key_filename="/root/.ssh/id_rsa" , timeout=30) 
           fecha=datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
           sleeptime = 0.001
           comando='rm -f /tmp/captura.jpg; pc=$(hostname); user=$(who | grep "(:0)" | head -1 | cut -f1 -d" "); test -n "$user" && (su $user -c "DISPLAY=:0 import -window root /tmp/captura.jpg"; chmod 777 /tmp/captura.jpg; echo -n $pc/$user)'
           #print("Ejecutando "+comando)
           ssh_transp = ssh_client.get_transport()
           chan = ssh_transp.open_session()
           
           chan.setblocking(0)
           outdata=""
           
           chan.exec_command(comando)
           while True:  # Hay que esperar a que acabe el comando
               while chan.recv_ready():
                  outdata += chan.recv(1000).decode('ascii')
               if chan.exit_status_ready():  # If completed
                  break
               time.sleep(sleeptime)
           
           #Como salida se devuelve "pc/usuario"
           signatura=outdata           
           #Aprovechando la conexion ssh se usa sftp para recuperar el fichero /tmp/captura.jpg y guardarlo en la ruta de destino
           sftp = ssh_client.open_sftp()
           destino=self.pathcapturas+"/"+signatura         
           try:
             comando='su '+self.user+' -c "mkdir -p '+destino+'"'
             os.system(comando)              
           except FileExistsError:         
             pass  
           fichero=destino+"/"+fecha+".jpg"           
           sftp.get("/tmp/captura.jpg", "/tmp/captura-local.jpg") #Traemos el fichero a local
           comando='su '+self.user+' -c "cp /tmp/captura-local.jpg '+fichero+'"'
           #print("Ejecutando "+comando)
           os.system(comando)           
           
           # Cerramos todo lo abierto
           ssh_transp.close()
           sftp.close()
           ssh_client.close()           
       except Exception as e:
           print("Error "+str(e))
   
   def capturar_thinclients(self):
   
       #Obtenemos la lista de usuarios conectados en el servidor de aula, filtramos por aquellos que están conectados con la ip 192.X.Y.Z (thinclients)
       for usuario in psutil.users():
           if usuario.host[:4] == "192." :
               try:
                   #Sacamos el home del usuario con getent
                   comando="getent passwd "+usuario.name
                   process = subprocess.Popen(comando.split(), stdout=subprocess.PIPE) 
                   output, error = process.communicate()
                   home_user=output.decode("ascii").split(":")[5]
                   
                   #Conectamos con el thinclient para sacar el hostname
                   ssh_client = paramiko.SSHClient()
                   ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                   ssh_client.connect(usuario.host, 22, 'root', key_filename="/root/.ssh/id_rsa", timeout=30)               
                   fecha=datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
                   sleeptime = 0.001
                   comando='cat /etc/hostname | tr -d "\n"'
                   ssh_transp = ssh_client.get_transport()
                   chan = ssh_transp.open_session()
                   # chan.settimeout(3 * 60 * 60)
                   chan.setblocking(0)
                   outdata=""
                   chan.exec_command(comando)
                   while True:  # Hay que esperar a que acabe el comando
                      while chan.recv_ready():
                         outdata += chan.recv(1000).decode('ascii')
                      if chan.exit_status_ready():  # If completed
                         break
                      time.sleep(sleeptime)
                   host=outdata    
                   # Cerramos todo lo abierto
                   ssh_transp.close()
                   ssh_client.close()                  
                   
                   #Ejecutamos el comando de captura de la sesión del usuario. Se usa os.system porque para comandos complejos es mejor que subprocess.popen
                   comando='su '+usuario.name+' -c  "DISPLAY='+usuario.host+':7  XAUTHORITY='+home_user+'/.Xauthority import -window root /tmp/capture.jpg"'
                   os.system(comando)
                   
                   #Copiamos la captura a la carpeta destino  
                   destino=self.pathcapturas+"/"+host+"/"+usuario.name
                   try:
                      comando='su '+self.user+' -c "mkdir -p '+destino+'"'
                      os.system(comando)
                   except FileExistsError:         
                      pass  
                   fichero=destino+"/"+fecha+".jpg"      
                   comando='su '+self.user+' -c "cp /tmp/capture.jpg '+fichero+'"'
                   os.system(comando)
                   comando='rm -f /tmp/capture.jpg'
                   os.system(comando)
           
               except Exception as e:       
                   pass
                   
   def capturar(self):       
    
       try:
            segundos=int(self.intervaloCaptura.text())*60
       except ValueError:
            segundos=0
        
       self.capturando=True     
       self.iniciarCaptura.setText("Capturando...")
       self.finalizar.setText("Parar captura")
       self.iniciarCaptura.setEnabled(False)
       while self.capturando == True: 
            self.iniciarCaptura.setText("Capturando...")
            self.iniciarCaptura.setEnabled(False)
            self.do_captura()
            if segundos == 0:            
               break
            else: #Espera activa hasta la próxima captura
               for i in range(segundos):
                  time.sleep(1)
                  app.processEvents()
                  if self.capturando==False: break                
       self.iniciarCaptura.setEnabled(True)
       self.iniciarCaptura.setText("Iniciar Captura")
       self.finalizar.setText("Finalizar")
       self.capturando=False
       

   def do_captura(self):
       app.processEvents()
       #print("Inicio captura")
       if self.isltsp == "true":
           self.capturar_thinclients()
       else:          
           nm = nmap.PortScanner()
           nm.scan(self.ip_alumnos, '22')
           for ip in  nm.all_hosts():# Equipos conectados, up.     
               #print("Procesando "+ip)
               if nm[ip]['tcp'][22]['state']=="open": 
                  #print("Puerto ssh abierto") 
                  self.capturar_ip(ip)               
               app.processEvents()   # Para procesar clicks de raton durante el bucle.                     
       app.processEvents()
       #print("Fin captura")
    

if __name__ == "__main__":
   app = QtWidgets.QApplication([])
   window = MainWindow()
   window.show()
   app.exec_()
Como se puede apreciar es un código python que utiliza clases de conexión ssh para ejecutar en remoto los comandos bash de captura de sesión y traer los ficheros con la captura al home del usuario en el PC del profesor.

El fichero photomaton.jpg contendría la imagen:


Seguimos con el fichero .desktop, el acceso directo para poner en el menú principal y en el escritorio. Nótese el uso de "gksu" para ejecutar como root el programa photomaton.py.
# cat xubuntu18_photomaton/files/Photomaton.desktop                                                   

[Desktop Entry]
Version=1.0
Encoding=UTF-8
Name=Photomaton
Type=Application
Exec=gksu /usr/local/bin/photomaton.py
TryExec=
Icon=/usr/local/bin/photomaton.jpg
X-GNOME-DocPath=
Terminal=false
Name[es_ES]=Photomaton
GenericName[es_ES]=
Comment[es_ES]=
GenericName=
Comment=
Continuamos con el fichero .ui en formato XML con el interface de usuario creado por Qt Designer:
# cat xubuntu18_photomaton/files/photomaton.ui                                                         
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Photomaton</class>
 <widget class="QDialog" name="Photomaton">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>508</width>
    <height>212</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Photomaton</string>
  </property>
  <widget class="QPushButton" name="iniciarCaptura">
   <property name="geometry">
    <rect>
     <x>290</x>
     <y>70</y>
     <width>191</width>
     <height>28</height>
    </rect>
   </property>
   <property name="text">
    <string>Iniciar Captura</string>
   </property>
  </widget>
  <widget class="QPushButton" name="visualizarCapturas">
   <property name="geometry">
    <rect>
     <x>290</x>
     <y>110</y>
     <width>191</width>
     <height>28</height>
    </rect>
   </property>
   <property name="text">
    <string>Visualizar Capturas</string>
   </property>
  </widget>
  <widget class="QPushButton" name="finalizar">
   <property name="geometry">
    <rect>
     <x>290</x>
     <y>150</y>
     <width>191</width>
     <height>28</height>
    </rect>
   </property>
   <property name="text">
    <string>Finalizar</string>
   </property>
  </widget>
  <widget class="QLabel" name="label">
   <property name="geometry">
    <rect>
     <x>290</x>
     <y>30</y>
     <width>151</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Intervalo capturas (min):</string>
   </property>
  </widget>
  <widget class="QLineEdit" name="intervaloCaptura">
   <property name="geometry">
    <rect>
     <x>440</x>
     <y>20</y>
     <width>41</width>
     <height>28</height>
    </rect>
   </property>
   <property name="inputMask">
    <string>09</string>
   </property>
   <property name="text">
    <string>00</string>
   </property>
   <property name="maxLength">
    <number>2</number>
   </property>
  </widget>
  <widget class="QLabel" name="label_2">
   <property name="geometry">
    <rect>
     <x>10</x>
     <y>20</y>
     <width>261</width>
     <height>181</height>
    </rect>
   </property>
   <property name="text">
    <string/>
   </property>
   <property name="pixmap">
    <pixmap>/usr/local/bin/photomaton.jpg</pixmap>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>
Y seguimos con el fichero photomaton_ui.py, generado a partir del fichero photomaton.ui anterior:
# cat xubuntu18_photomaton/files/photomaton_ui.py 
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'photomaton.ui'
#
# Created by: PyQt5 UI code generator 5.10.1
#
# WARNING! All changes made in this file will be lost!

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Photomaton(object):
    def setupUi(self, Photomaton):
        Photomaton.setObjectName("Photomaton")
        Photomaton.resize(508, 212)
        self.iniciarCaptura = QtWidgets.QPushButton(Photomaton)
        self.iniciarCaptura.setGeometry(QtCore.QRect(290, 70, 191, 28))
        self.iniciarCaptura.setObjectName("iniciarCaptura")
        self.visualizarCapturas = QtWidgets.QPushButton(Photomaton)
        self.visualizarCapturas.setGeometry(QtCore.QRect(290, 110, 191, 28))
        self.visualizarCapturas.setObjectName("visualizarCapturas")
        self.finalizar = QtWidgets.QPushButton(Photomaton)
        self.finalizar.setGeometry(QtCore.QRect(290, 150, 191, 28))
        self.finalizar.setObjectName("finalizar")
        self.label = QtWidgets.QLabel(Photomaton)
        self.label.setGeometry(QtCore.QRect(290, 30, 151, 16))
        self.label.setObjectName("label")
        self.intervaloCaptura = QtWidgets.QLineEdit(Photomaton)
        self.intervaloCaptura.setGeometry(QtCore.QRect(440, 20, 41, 28))
        self.intervaloCaptura.setMaxLength(2)
        self.intervaloCaptura.setObjectName("intervaloCaptura")
        self.label_2 = QtWidgets.QLabel(Photomaton)
        self.label_2.setGeometry(QtCore.QRect(10, 20, 261, 181))
        self.label_2.setText("")
        self.label_2.setPixmap(QtGui.QPixmap("/usr/local/bin/photomaton.jpg"))
        self.label_2.setObjectName("label_2")

        self.retranslateUi(Photomaton)
        QtCore.QMetaObject.connectSlotsByName(Photomaton)

    def retranslateUi(self, Photomaton):
        _translate = QtCore.QCoreApplication.translate
        Photomaton.setWindowTitle(_translate("Photomaton", "Photomaton"))
        self.iniciarCaptura.setText(_translate("Photomaton", "Iniciar Captura"))
        self.visualizarCapturas.setText(_translate("Photomaton", "Visualizar Capturas"))
        self.finalizar.setText(_translate("Photomaton", "Finalizar"))
        self.label.setText(_translate("Photomaton", "Intervalo capturas (min):"))
        self.intervaloCaptura.setInputMask(_translate("Photomaton", "09"))
        self.intervaloCaptura.setText(_translate("Photomaton", "00"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Photomaton = QtWidgets.QDialog()
    ui = Ui_Photomaton()
    ui.setupUi(Photomaton)
    Photomaton.show()
    sys.exit(app.exec_())
Un vez distribuidos los ficheros con la tarea puppet el modo de funcionamiento es lanzarlo en el ordenador del profesor y ejecutarlo para conectar a los equipos de alumnos detectados como encendidos y realizar las capturas. La pantalla es:


En "Intervalo de capturas (min)" indicamos cada cuanto tiempo queremos hacer las capturas de pantalla. Si dejamos 0 solo se realiza una captura en el momento en que damos al botón "Iniciar Captura".


Mientras que está realizando las capturas el botón "Iniciar Captura" se desactiva. El proceso de captura para pulsando el botón "Parar Captura". A veces tarda un rato en responder ya que no se puede interrumpir mientras está ejecutando la conexión con uno de los clientes.

El botón "Visualizar Capturas" nos lleva al directorio donde se realizan las capturas para que veamos las imágenes. Las capturas se estructuran en directorios separados según el patrón: ~/Capturas/$pc_profesor/$pc_alumno/$login_alumno/*.jpg


Los nombres de ficheros jpg nos indican la fecha y hora de captura:

Y ya está. Con este sistema el profesor puede realizar un seguimiento desatendido completo de la actividad de los alumnos. No solo eso: nosotros también desde nuestro PC podremos realizar seguimiento de ordenadores de uso público del centro.


Ahora que con la magnífica serie Chernobyl (quizá desvirtuada con algunos elementos de ficción y drama innecesarios, en detrimento de otros más interesantes como la construcción del sarcófago y los biorobots) se ha reavivado el tema, dejo varios enlaces sobre accidentes nucleares bastante silenciados:

  • Three Mile Island: o como "refrigerar" un reactor con vapor de agua en lugar de con agua líquida durante horas hasta fundir parcialmente el núcleo y no darse cuenta.
  • Vandellós I: o cómo quedarte a dos grados de explotar, mientras que el director de la central se iba a por tabaco en helicóptero a Barcelona y los trabajadores conseguían controlar la temperatura in extremis con el único turbosoplante que no se había parado por inundación.
  • Lucens: o cómo los suizos se lo intentan montar por su cuenta y funden un reactor el día de su puesta en funcionamiento. La única idea buena: como por si las moscas lo hicieron debajo de una montaña, solo se contaminó ésta.

Por supuesto, recomiendo todos los artículos de Yuri sobre Chernobyl:


Y no podía faltar: