El siguiente proyecto trata de dar respuesta al problema de dejar trabajando el ordenador, recogiendo datos o haciendo una simulación, para volver a los varios días y encontrar que se ha colgado.
El tutorial recoge la primera solución que se nos ha ocurrido en inmediatamente la hemos subido. Falta aún mucho trabajo, tanto con los programas como con el tutorial, por lo que es de esperar alguna actualización.
Supongamos que nos encontramos en la situación en que queremos saber si un programa en Python ha dejado de funcionar, ya sea porque se ha colgado el programa o el ordenador. Si tenemos la suerte de estar en una red local podemos implementar una solución en Python en que el otro ordenador de la misma red nos manda un aviso por correo electrónico cuando detecte que la aplicación haya dejado de funcionar o el ordenador se ha colgado.
La idea es muy sencilla (figura 1). Desde la aplicación que queremos vigilar se mandan mensajes periódicamente al watchdog que se encuentra en otro ordenaador. En el momento que dejan de llegar estos mensajes el watchdog envía un correo de alerta.
Figura 1: Esquema.
FASE 1. Envío y recepción de mensajes
Vamos a ir construyendo el programa de una forma incremental partiendo de una forma simple para ir mejorandola poco a poco hasta la versión final. Primero vamos a crear dos programas en el que uno envía un pequeño mensaje al otro. Todo ello dentro de la misma máquina.
Utilizamos comunicación por sockets, exactamente igual que la que aparece como ejemplo en la documentación de sockets en Python.org. En nuestro caso construiremos dos programas, el programa watchdog.py que va a estar la espera de una conexión, por tanto está haciendo una función de servidor, y radiofaro.py que es el cliente que se conecta, envía un mensaje y termina la conexión.
Nota
Como construimos bucles infinitos deberemos de finalizar los programas con ctrl-c.
# watchdog.py# socket serverimport socket
socketHost = ''# interfaces de la maquina en la que corre
socketPort = 50091# puerto de escucha
sockobj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # objeto TCP
sockobj.bind((socketHost, socketPort)) # liga el objeto al puerto y al host
sockobj.listen(5)
print ('watchdog socket creado')
whileTrue:
connection, address = sockobj.accept() # espera a que un cliente se conectewhileTrue:
data = connection.recv(1024)
ifnot data: breakprint(data)
connection.sendall(data)
connection.close()
# radiofaro.py# socket clientimport socket
import time
serverHost = 'localhost'# a quien conectarnos
serverPort = 50091
i = 9
while i > 0:
sockobj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # objeto TCP
sockobj.connect((serverHost, serverPort)) # abre una conexion
sockobj.send(str(i).encode('ascii'))
data = sockobj.recv(1024)
sockobj.close()
print(data)
i -= 1
time.sleep(1)
FASE 2: El temporizador.
La idea es la siguiente:
Si llega un "on" cancelamos el temporizador (si hubiese uno en marcha) y lanzamos uno nuevo que inicia su cuenta atrás.
Si llega un "off" paramos el temporizador.
Si el temporizador llega a cero salta la alarma.
Vamos a usar la clase Timer del módulo threading. Cuando llegue un 'on' al vigilante watchdog crearemos un objeto Timer que lanzamos inmediatamente con objeto_timer.start() con lo que inicia en paralelo una cuenta atrás que si no es detenido antes con objeto_timer.cancel() ejecutará la función que le proporcionamos como argumento, que será la de mandar una alerta.
# watchdog.py
# socket server
import socketimport threading
socketHost = '' # interfaces de la maquina en la que corre
socketPort = 50091 # puerto de escucha
t_alert = 5# segundosdefalert():
print('Alert!!!')
sockobj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # objeto TCP
sockobj.bind((socketHost, socketPort)) # liga el objeto al puerto y al host
sockobj.listen(5)
print ('watchdog socket creado')
while True:
connection, address = sockobj.accept() # espera a que un cliente se conecte
while True:
data = connection.recv(1024)
if not data: break
print(data)if data == b'on':
try: # para no tener que revisar si existe threadobj
threadobj.cancel()
print('canceling')
exceptNameError: # ocurre si threadjob no ha sido inicializadopass
threadobj = threading.Timer(t_alert, alert)
threadobj.start()
if data == b'off':
threadobj.cancel()
connection.sendall(data)
connection.close()
Para probar si funciona, al menos el 'on', cambiamos la siguiente línea que envía el mensaje en radiofaro.py.
sockobj.send(b'on')
Debería aparecer la alerta solo una vez al final de todos los 'on' que le han llegado.
FASE 3: Envíar un correo electrónico de aviso.
Por tocar todos los temas vamos a construir un módulo con el nombre alert_email. Para mandar el mensaje usaremos la biblioteca smtlib de Python. No tiene más complicación que crear el mensaje con sus campos de origen, destino, asunto y cuerpo de texto. Proporcionar el usuario y la clave. Y usar las funciones de la biblioteca para mandar el correo.
Podéis testear el programa corriendo directamente el módulo que debería enviar un correo de alerta a la cuenta que especifiquéis.
# Modulo alert_emailimport smtplib
defsend_alert_email(
from_addr = 'direccion_origen@gmail.com',
to_addr = 'direccion_destino@gmail.com',
subject = 'Alerta',
texto = 'Se ha producido un fallo'):
''' Envia un correo de alerta desde una cuenta de gmail'''# Une los elementos de la lista separandolos con# un carriage return y un line feed
msg = '\r\n'.join([
'From: ' + from_addr,
'To: ' + to_addr,
'Subject: Alert',
'',
texto])
user_name = 'user_name@gmail.com'# direccion de nuestra cuenta
password = '12345678'
server = smtplib.SMTP('smtp.gmail.com:587') # crea un objeto
server.starttls() # seguridad de la conexion
server.login(user_name, password) # login
server.sendmail(from_addr, to_addr, msg)
server.quit()
if __name__ == '__main__':
send_alert_email()
FASE 4: Modelo para usar en la aplicación a vigilar.
Ahora vamos a crear un aplicación gráfica que simulará ser la aplicación que queremos vigilar. Siguiendo es esquema inicial esta lanzará un hilo, en concreto un Timer, que tras el tiempo estipulado envía un mensaje al watchdog a la vez que programa el lanzamiento de un nuevo Timer para mantener un tren de pulsos.
También se ha añadido el tratamiento de algunas exepciones como la de socket.timeout para no quedarnos atascados esperando una respuesta y poder continuar el programa.
Por hacerlo más bonito, y seguramente útil, creamos un widget para iniciar y parar el radio faro que puede utilizarse individualmente para vigilar si el ordenador se cuelga.
# radiofaro_gui.py#import socket
import time
import threading
import queue
from tkinter import *
wdHost='localhost'# ip del host donde el watchdog se encuentra
wdPort=50091
intervalo = 1# segundos
mutex = threading.Lock()
dataqueue = queue.Queue()
defsend_to_watchdog(serverHost, serverPort, msg):
''' Funcion que manda un mensaje al watchdog'''try:
sockobj = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sockobj.settimeout(5) # tiempo de espera max.
sockobj.connect((serverHost, serverPort))
sockobj.send(msg)
data = sockobj.recv(1024)
dataqueue.put(data)
except ConnectionRefusedError: # no conecta al watchdog
dataqueue.put('error conex')
except ConnectionResetError:
dataqueue.put('error conex')
except socket.timeout: # no hay respuesta del watchdog.
dataqueue.put('error conex')
finally:
sockobj.close()
deftemporizador(intervalo=intervalo, msg=b'on'):
''' Programa una nueva llamada a si mismo con un Timer'''
mutex.acquire()
global actual_thread
send_to_watchdog(wdHost, wdPort, msg)
thread = threading.Timer(intervalo, temporizador ) # lanza otro temporizador
thread.start()
actual_thread = thread
mutex.release()
defstop_temporizador():
''' Finaliza el ultimo Timer del temporizador'''
mutex.acquire()
try:
actual_thread.cancel()
exceptNameError: # no se ha lanzado ningun temporizadorpass
send_to_watchdog(wdHost, wdPort, b'off')
mutex.release()
# --------------- funciones ---------------deflaunch_timer():
try: # cuando construyamos metodos guardaremos el estadoifnot actual_thread.isAlive():
threading.Timer(intervalo, temporizador).start()
exceptNameError:
threading.Timer(intervalo, temporizador).start()
deflaunch_stop_timer():
threading.Thread(target = stop_temporizador).start()
# --------------- funciones GUI ---------------defmake_watchdog_widget(parent=None):
global label_watchdog
botonera = Frame(parent)
Button(botonera, text='ON', command=launch_timer).pack(side=LEFT)
Button(botonera, text='OFF', command=launch_stop_timer).pack(side=LEFT)
botonera.pack()
label_watchdog = Label(parent, text='watchdog')
label_watchdog.pack()
defrepeater(t_refresco=1000):
if dataqueue.empty(): label_watchdog.config(text='----')
whilenot dataqueue.empty():
try:
data = dataqueue.get(block=False)
except queue.Empty:
passelse:
label_watchdog.config(text=data)
winroot.after(t_refresco, repeater)
# ------------ programa principal ---------------
winroot = Tk()
make_watchdog_widget(winroot)
winroot.after(1000, repeater)
mainloop()
# ----- finalizar los hilos de ejecucion ------
threading.Thread(target = stop_temporizador).start()
FASE 5: Desplegamos el sistema en la red local.
Hasta ahora hemos hecho todo en un mismo ordenador. Llegó el momento de instalar el watchdog en el otro ordenador del la red local. Supongamos que es un Raspberry Pi al que nos conectamos via ssh.
Antes de pasar los programas a la raspberry vamos a escribir un fichero de configuración para lanzar un servicio con systemd. Brevemente, lo que va a hacer es lanzar el programa watchdog.py cuando el sistema se inicie pero despúes de levantar la red. Además con el parámetro Restart=on-abort si el demonio (watchdog.py) cae este se reinicia.
Supongamos que la dirección de la raspberry es 192.168.0.20, que hay un usuario llamado alberto y una carpeta /home/alberto/.py. Sin olvidar que debemos tener Python 3 instalado.
Copiamos los programas Python watchdog.py, email_alert y py_watchdog.service en una carpeta de la raspberry. Tomad nota de donde se encuentran para actualizar el valor ExecStart en el fichero de configuración en py_watchdog.service.
No olvidéis comprobar que ExecStart en py_watchdog.service apunte al fichero watchdog.py.
Como usuario root o con sudo copiamos el archivo de configuración py_watchdog.service en la carpeta /etc/systemd/system/
$ su
Password:
# cp py_watchdog.service /etc/systemd/system/
Podemos probar a lanzar el servicio para esta sesión. También con la instrucción sudo o como usuario root.
# systemctl start py_watchdog.service
# systemctl status py_watchdog.service
Si todo va bien habilitamos que la unidad se inicie en el arranque. Si hay que modificar el fichero de configuración hay que usar systemctl reload py_watchdog.service para que tengan efectos los cambios.
# systemctl enable py_watchdog.service
Nota
Está planeado un tutorial de systemd. Por ahora cuatro instrucciones importantes:
systemctl start unit - inicia la unidad unit.
systemctl stop unit - desactiva inmediatamente la unidad.
systemctl enable unit - habilita que la unidad se inicie en el arranque.
systemctl disable unit - deshabilita la unidad.
systemctl reload unit - la unidad recarga su fichero de configuración.
Cambiamos la dirección ip en radio_faro.py por la dirección del Raspberry Pi y a funcionar.
wdHost='192.168.0.20'# ip del host donde el watchdog se encuentra
FASE 6: Futuro, hacer más amigable el funcionamiento y utilizar programación orientada a objetos.
En todos los programas solo hemos usado lo que sería programación con procedimientos. El siguiente paso sería cambiar el modelo de programación a uno orientado a objetos que siempre sería más reutilizabe. Ese será el siguiente paso que pospondremos a una revisión de este tutorial.
De momento os dejo los archivos para que no tengáis que teclear.