En Linux, hay muchos parámetros del sistema que se pueden consultar tanto desde la terminal como desde aplicaciones gráficas: Temperaturas, memoria disponible, velocidad de la CPU, conexiones de red... Normalmente, cuando lo necesitamos, lo miramos en alguna aplicación y listo: Un monitor de recursos, un complemento en la barra de tareas o lo que sea. Casi nunca es necesario recurrir a la terminal para estas cosas.
Sin embargo, es muy útil poder hacerlo desde terminal para automatizarlo: Un comando personalizado que va creando un log a petición, un script en segundo plano que almacena cambios relevantes (P.Ej: un corte de red, un pico de temperatura...) con hora y fecha, uno que almacene periódicamente el valor de un sensor para crear una gráfica de temperaturas...
Hoy veremos cómo construir un script para monitorizar temperaturas en segundo plano mediante un archivo de log. Nos servirá para entender cómo se construye un script paso a paso, partiendo de comandos de consola y añadiendo funcionalidades poco a poco.
Se van a utilizar comandos como echo, sensors, grep, awk, sort, head, tail, while, if, getopts, tee, wc... además de algunos operadores y redirecciones, sin profundizar demasiado en ninguno.
Después de varias malas experiencias, he adquirido la costumbre de tener la temperatura de mis ordenadores siempre monitorizada. Siempre tengo algún monitor de temperatura en la barra de tareas (ya sea en LXDE, XFCE, Cinnamon...), y le echo un vistazo cada vez que tengo un proceso pesado funcionando, por si las moscas.
El problema es que no puedo mirar las temperaturas mientras juego a pantalla completa, así que no puedo saber seguro qué temperaturas se han alcanzado mientras tanto.
La solución es crear un script que, cada pocos segundos, extraiga información de los sensores y la añada a un log. Después de jugar no tengo más que mirar este log y ver qué temperaturas se han alcanzado y cuándo.
Script tempertaturas.sh
#!/bin/bash
# -*- ENCODING: UTF-8 -*-
# Variables
logfile="$HOME/temperaturas.txt"
segundos=5
repeticiones=10
lineas=20
salir=0
# Texto de ayuda
ayuda="Este script mide la temperatura más alta del ordenador, la almacena en un archivo de log, espera un número determinado de segundos y repite la operación, tantas veces como se le indique.
Por defecto, espera $segundos segundos entre cada medición, se repite $repeticiones veces, almacena el resultado en $logfile y éste se ha limitado a un máximo de $lineas líneas.
Uso:
LogDeTemperaturas [-t segundos] [-r repeticiones] [-l logfile] [-m líneas]
-t Tiempo de espera entre mediciones
-r Número de mediciones a realizar
-l Ruta al archivo de log
-m Máximo número de líneas del log"
# Modificadores y parámetros
while getopts t:r:l:m:h flag
do
case "${flag}" in
t) segundos=${OPTARG};;
r) repeticiones=${OPTARG};;
l) logfile=${OPTARG};;
m) lineas=${OPTARG};;
h) echo "$ayuda" && exit;;
*) echo -e "Parámetro incorrecto"
exit;;
esac
done
# Bucle principal
while [[ $salir -lt $repeticiones ]]; do
((salir+=1))
# Toma de temperaturas
TempActual=$(sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1)
# Escritura en el log
echo $TempActual | tee -a "$logfile"
# Límite de tamaño del log
if [ $(wc -l < "$logfile") -gt $lineas ]; then
templog=$(tail -n $lineas "$logfile")
echo "$templog" > "$logfile"
fi
#Retardo entre mediciones
sleep $segundos
done
exit
Instrucciones de uso
Este script mide la temperatura más alta del ordenador, la almacena en un archivo de log, espera un número determinado de segundos y repite la operación, tantas veces como se le indique.
Por defecto, espera 5 segundos entre cada medición, se repite 10 veces, almacena el resultado en ~/temperaturas.txt y éste se ha limitado a un máximo de 20 líneas.
Uso:
LogDeTemperaturas [-t segundos] [-r repeticiones] [-l logfile] [-m líneas]
-t Tiempo de espera entre mediciones
-r Número de mediciones a realizar
-l Ruta al archivo de log
-m Máximo número de líneas del log
Para facilitar su uso, estos valores también pueden editarse en el propio script, en la primera sección (Variables). De esta forma, el script funcionará con esos valores incluso si se ejecuta desde un entorno gráfico (doble click sobre el icono y "Ejecutar").
Cómo construirlo
Paso 0 - Precauciones iniciales
Como siempre, empezamos añadiendo el shebang, guardando el archivo y dándole permisos de ejecución.
Además, conviene añadir comentarios en cada fragmento funcional del código; son esas líneas que empiezan por el carácter # y que el intérprete de comandos ignora. Esto puede verse en el script completo del principio del hilo, pero no lo he añadido en los ejemplos posteriores para aligerar un poco el texto.
Paso 1 - Obtener los datos
Empezamos por la obtención de los datos mediante el comando sensors. Este comando devuelve información sobre todos los sensores de temperatura del ordenador. En Debian, Ubuntu y derivados se instala así:
sudo apt install lm-sensors
Para que detecte los sensores disponibles lanzamos esta orden (sólo hay que hacerlo una vez tras la instalación):
sudo sensors-detect
El comando a usar es éste:
sensors
Que nos devuelve algo así (este ejemplo es de mi PC actual):
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +59.0°C (high = +105.0°C, crit = +105.0°C)
Core 0: +59.0°C (high = +105.0°C, crit = +105.0°C)
Core 1: +59.0°C (high = +105.0°C, crit = +105.0°C)
Core 2: +59.0°C (high = +105.0°C, crit = +105.0°C)
Core 3: +59.0°C (high = +105.0°C, crit = +105.0°C)
nvme-pci-0200
Adapter: PCI adapter
Composite: +42.9°C (low = -273.1°C, high = +89.8°C)
(crit = +94.8°C)
Sensor 1: +42.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +34.9°C (low = -273.1°C, high = +65261.8°C)
acpitz-acpi-0
Adapter: ACPI interface
temp1: +0.0°C
iwlwifi_1-virtual-0
Adapter: Virtual device
temp1: +66.0°C
BAT0-acpi-0
Adapter: ACPI interface
in0: 13.03 V
power1: 0.00 W
acpi_fan-acpi-0
Adapter: ACPI interface
fan1: N/A
Está muy bien montado para ver todas las temperaturas actuales del equipo en un momento concreto, pero no es muy práctico cuando queremos tomar la temperatura cada pocos segundos durante mucho rato para observar las variaciones. Vamos a filtrar un poco. La siguiente línea filtra el mayor valor de temperatura de todos los disponibles y lo muestra en pantalla:
sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1
Salida: 66
Es lo que yo llamo un "comando chorizo": Una ristra de comandos encadenados en una sola línea, imposible de memorizar, de escribir del tirón o de comprender de un vistazo. Se usan mucho, entre otras cosas, para tratar cadenas texto, puesto que permiten hacer muchas operaciones distintas de un plumazo, sin pelear con variables ni archivos externos.
Veámoslo desglosado para entenderlo mejor:
NOTA: El carácter | es una "tubería": Hace que la salida de cada comando sea la entrada del siguiente.
grep °C busca las líneas que contengan la cadena de texto "ºC" y elimina todas las demás. Esto ya reduce mucho la salida:
sensors | grep °C
Package id 0: +61.0°C (high = +105.0°C, crit = +105.0°C)
Core 0: +61.0°C (high = +105.0°C, crit = +105.0°C)
Core 1: +61.0°C (high = +105.0°C, crit = +105.0°C)
Core 2: +61.0°C (high = +105.0°C, crit = +105.0°C)
Core 3: +61.0°C (high = +105.0°C, crit = +105.0°C)
Composite: +42.9°C (low = -273.1°C, high = +89.8°C)
(crit = +94.8°C)
Sensor 1: +42.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +34.9°C (low = -273.1°C, high = +65261.8°C)
temp1: +0.0°C
temp1: +66.0°C
grep : hace algo similar, eliminando las líneas que no contengan el carácter ":". Esto es necesario porque, como se ve en la salida anterior, algunas líneas pueden salir partidas. En este caso, la línea que empieza por "Composite" continúa en la línea siguiente; no existía en mi anterior PC, en el que creé el script, y me devolvía valores raros en mi PC actual.
sensors | grep °C | grep :
Package id 0: +61.0°C (high = +105.0°C, crit = +105.0°C)
Core 0: +61.0°C (high = +105.0°C, crit = +105.0°C)
Core 1: +61.0°C (high = +105.0°C, crit = +105.0°C)
Core 2: +61.0°C (high = +105.0°C, crit = +105.0°C)
Core 3: +61.0°C (high = +105.0°C, crit = +105.0°C)
Composite: +42.9°C (low = -273.1°C, high = +89.8°C)
Sensor 1: +42.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +34.9°C (low = -273.1°C, high = +65261.8°C)
temp1: +0.0°C
temp1: +66.0°C
awk -F '+' '{print $2}' divide cada línea en campos separados por el carácter "+" y toma sólo el segundo de ellos. Es decir, trata el texto como si fuera una tabla con cada celda separada de la siguiente mediante ese carácter. Veamos la salida que hemos obtenido de momento:
sensors | grep °C | grep : | awk -F '+' '{print $2}'
62.0°C (high =
61.0°C (high =
61.0°C (high =
61.0°C (high =
61.0°C (high =
42.9°C (low = -273.1°C, high =
42.9°C (low = -273.1°C, high =
34.9°C (low = -273.1°C, high =
0.0°C
67.0°C
awk -F '°C' '{print $1}' hace algo similar: Utiliza la cadena "ºC" como separador y toma sólo el primer campo. En la práctica, conserva sólo lo que hay antes del primer "ºC".
sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}'
66.0
66.0
66.0
66.0
66.0
42.9
42.9
34.9
0.0
67.0
Bien, ya tenemos todas las temperaturas limpitas de texto innecesario. Ahora vamos a ordenarlas de mayor a menor con el comando sort -nr:
sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr
67.0
62.0
61.0
61.0
61.0
61.0
42.9
42.9
34.9
0.0
Por último, usamos head -n1 para tomar sólo la primera línea, que corresponde con el mayor valor de todos (para eso los ordenamos antes), y listo:
sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1
67.0
Paso 2 - Almacenar los datos
Hasta ahora, estábamos ejecutando comandos por consola y obteniendo los resultados en la misma. Vamos a almacenar el resultado de este "comando choricero" en una variable para usarla en el script:
TempActual=$(sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1)
Ahora, basta con volcar el contenido de la variable en un archivo de texto:
echo $TempActual | tee -a $HOME/temperaturas.txt
echo lee el contenido de la variable y lo saca a salida estándar (normalmente, la consola de comandos). tee -a desvía la salida estándar hacia un archivo, añadiendo dicha salida al final del archivo, sin sobrescribir nada. $HOME/ sólo es una forma abreviada de escribir "/home/MiUsuario/", con la ventaja de que tomará el directorio del usuario que ejecute el script, sin necesidad de escribir la ruta exacta.
Paso 3 - Crear el bucle principal
Para que el script ejecute estos comandos una y otra vez, debemos crear un bucle cerrado: que cada vez que el script termine, vuelva a empezar. Hay muchas maneras de hacerlo: Bucles "while", bucles "for", subrutinas que se llaman a sí mismas recursivamente...
-> Los más veteranos quizá recuerden el mítico "GOTO 10" del BASIC de muchos viejos micro ordenadores. ;D
En este caso, vamos a usar un bucle "while", que es fácil de usar:
while [[ condición ]]; do
Lo que sea que queremos hacer
done
Mientras la condición se cumpla, todo lo que haya entre el "do" y el "done" se ejecutará una y otra vez.
La condición puede ser cualquier cosa: que dos valores sean iguales, o que sean diferentes, o uno mayor que el otro, o que exista un archivo... Podemos poner simplemente while [[ 1 ]] y, como "1" siempre es cierto, el bucle se repetirá eternamente... hasta que cerremos el script por las bravas; desde consola, con la combinación de teclas [Ctrl]+[c], o desde el monitor de procesos del entorno gráfico buscando el script entre la lista de procesos y seleccionando "finalizar" o "matar" ("kill").
En este caso, podemos ponerle un sencillo contador:
salir=0
while [[ $salir -lt 10 ]]; do
((salir+=1))
echo $salir
TempActual=$(sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1)
echo $TempActual | tee -a $HOME/temperaturas.txt
done
Primero, iniciamos la variable "salir" a 0.
La condición para ejecutar el bucle es que "salir" valga menos de 10. "-lt" significa "less than". Otros comparadores que se pueden usar son:
-eq Equal Igual a
-lt Less than Menor que
-gt Greater than Mayor que
-le Less or equal Igual o menor que
-ge Greater or equal Igual o mayor que
((salir+=1)) incrementa en 1 el valor almacenado en "salir". El doble paréntesis es para que trate los valores como números, no como texto; de lo contrario, acabaríamos con la variable conteniendo algo como "0+1+1+1+1+1+1+1+1+1+1" o "01111111111", lo que no nos interesa.
NOTA: En este ejemplo, he incluido un "echo $salir" para poder ver en consola qué valor almacena la variable en cada momento y así entender mejor el proceso. Es muy habitual inundar un script de "echo" para ver qué hace el script internamente mientras se prueban nuevas funciones o se buscan errores. Normalmente, estos "echo" se eliminan en la versión final o sólo se muestran si el usuario activa algún tipo de "modo debug".
El problema, ahora mismo, es que el script está almacenando la temperatura tan rápido como la potencia del ordenador le permita. Con los equipos actuales, el script completo se ejecuta en una fracción de segundo, no dando tiempo al procesador a calentarse o enfriarse. Así que... vamos a frenarlo.
La forma habitual es mediante el comando sleep, que pausa la ejecución de script tantos segundos como le indiquemos y luego continúa como si nada.
salir=0
while [[ $salir -lt 10 ]]; do
((salir+=1))
TempActual=$(sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1)
echo $TempActual | tee -a $HOME/temperaturas.txt
sleep 5
done
Ahora el script, cada 5 segundos, medirá las temperaturas, seleccionará la más alta, la escribirá en un archivo y volverá a empezar.
Paso 4 - Sustituir valores fijos por variables
Este script funciona muy bien... durante 50 segundos. ¿Y si queremos que dure más tiempo? ¿O que mida la temperatura cada segundo, o cada 30 segundos? ¿O que use otro archivo de log distinto? Habría que buscar dónde están esos valores dentro del script y cambiarlos manualmente. No es muy difícil de hacer en scripts muy pequeños como éste, pero en programas más complejos puede ser un auténtico dolor de cabeza. Además, tampoco podremos cambiarlos "desde fuera": usar parámetros, o que cambien según distintas condiciones, o lo que sea. Así que vamos a llevarnos esos valores a unas variables que iniciaremos al principio del script:
logfile="$HOME/temperaturas.txt"
segundos=5
repeticiones=10
salir=0
while [[ $salir -lt $repeticiones ]]; do
((salir+=1))
TempActual=$(sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1)
echo $TempActual | tee -a "$logfile"
sleep $segundos
done
Paso 5 - Utilizar modificadores y parámetros
De momento tenemos un script que funciona, y en el que podemos cambiar su funcionamiento sólo con editar las primeras líneas del archivo. Esto es muy útil en entornos donde siempre vamos a usar los mismos valores y lo importante es ejecutar el programa y despreocuparnos.
Podemos hacer nuestro script mucho más reutilizable si incorporamos parámetros, algo así como "comandos secundarios" que se mandan al script durante su primera ejecución. Por ejemplo, poder escribir:
./LogDeTemperaturas.sh -t 10 -r 60 -l "$HOME/Temperaturas al jugar a Minecraft.txt"
Y que se ejecute el script durante 60 bucles, cada 10 segundos, creando un log separado del predefinido.
El formato es sencillo: comando -modificador1 parámetro1 -modificador2 parámetro2 etc...
Lo primero va a ser capturar los parámetros que le pase el usuario. Para este tipo de scripts, funciona muy bien la herramienta getopts. Veamos cómo funciona:
while getopts t:r:l: flag
do
case "${flag}" in
t) segundos=${OPTARG};;
r) repeticiones=${OPTARG};;
l) logfile=${OPTARG};;
*) echo -e "Parámetro incorrecto"
exit;;
esac
done
Lo primero que llama la atención es que se ejecuta dentro de un bucle. En cada iteración de éste se analiza un parámetro y se ejecuta algún comando.
tras el comando getopts vienen los modificadores que queramos usar (en este caso, -t, -r y -l) seguidos del carácter ":" si dicho modificador debe ir seguido de algún valor. En este ejemplo, todos requieren alguno, pero no siempre es así.
Después se utiliza case "${flag}" para comparar cada modificador con la lista siguiente. En cada coincidencia, se ejecuta algún comando, que en el ejemplo es simplemente almacenar en cada variable el parámetro que coincide con cada modificador. El elemendo *) de la lista corresponde con cualquier modificador que no esté en la lista; en el ejemplo, si esto sucediera, se manda el mensaje de error "Parámetro incorrecto" y termina la ejecución del script.
Conviene poner este bloque de código después de establecer los valores por defecto de las variables. Así, los parámetros que mande el usuario sustituirán a los valores por defecto, pero mantendrá estos valores si el usuario no indica nada al respecto.
Por ejemplo, ./LogDeTemperaturas.sh -t 1 cambiará la frecuencia a la que se ejecuta el bucle principal de 5 segundos a 1 segundo, pero mantendrá el número de ejecuciones en 10 y el archivo de log seguirá siendo "$HOME/temperaturas.txt".
Veamos cómo va nuestro bonito script:
logfile="$HOME/temperaturas.txt"
segundos=5
repeticiones=10
salir=0
while getopts t:r:l: flag
do
case "${flag}" in
t) segundos=${OPTARG};;
r) repeticiones=${OPTARG};;
l) logfile=${OPTARG};;
*) echo -e "Parámetro incorrecto"
exit;;
esac
done
while [[ $salir -lt $repeticiones ]]; do
((salir+=1))
TempActual=$(sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1)
echo $TempActual | tee -a "$logfile"
sleep $segundos
done
Paso 6 - Limitar la longitud del log
Un log parece algo inofensivo, hasta que tienes un script en segundo plano en autoarranque funcionando durante meses (o años), el log en cuestión ocupa cada vez más espacio, llega a ser un archivo de varios GB y te deja una partición del disco sin espacio disponible. Entonces empiezan los errores, los fallos y el "¿qué c****es se ha roto ahora?".
Una buena práctica es limitar el número de líneas que puede alcanzar un log. Cuando éste crece demasiado, se van eliminando las primeras líneas (las más antiguas) a medida que se añaden nuevas líneas al final.
Esto se puede resolver con el comando tail, que muestra las últimas filas de un archivo. Lo que haremos es que, cuando se supere un número de líneas determinado, se sustituya todo el contenido del log por únicamente esas últimas filas. Para ello usaremos una variable temporal, $templog, ya que no suele ser buena idea escribir en un archivo al mismo tiempo que lo leemos.
Para saber qué número de filas tiene un archivo podemos usar el comando wc ("word count", contar palabras) con el modificador -l para que cuente líneas en lugar de palabras.
Para comprobar si se ha sobrepasado el número de filas usaremos un salto condicional if then.
Por ejemplo, en nuestro script:
logfile="$HOME/temperaturas.txt"
segundos=5
repeticiones=10
lineas=20
salir=0
while getopts t:r:l:m: flag
do
case "${flag}" in
t) segundos=${OPTARG};;
r) repeticiones=${OPTARG};;
l) logfile=${OPTARG};;
m) lineas=${OPTARG};;
*) echo -e "Parámetro incorrecto"
exit;;
esac
done
while [[ $salir -lt $repeticiones ]]; do
((salir+=1))
TempActual=$(sensors | grep °C | grep : | awk -F '+' '{print $2}' | awk -F '°C' '{print $1}' | sort -nr | head -n1)
echo $TempActual | tee -a "$logfile"
if [ $(wc -l < "$logfile") -gt $lineas ]; then
templog=$(tail -n $lineas "$logfile")
echo "$templog" > "$logfile"
fi
sleep $segundos
done
Nótese que he añadido la variable $lineas al principio del script y un nuevo modificador -m para poder establecer el límite mediante parámetros.
Paso 7 - Ayuda al usuario - El modificador -h
Un buen toque final para cualquier script medianamente complejo: poder ejecutar ./MiScript.sh -h y que aparezca una guía rápida de uso. Para ello, usaremos el mismo comando getopts que ya estábamos usando, pero añadiendo un modificador.
Primero, crearemos una variable que contenga el texto de ayuda. A mí me gusta ponerlo cerca del principio del script, después de las variables iniciales, pero lo importante es que esté definida antes del getopts o éste no sabrá qué hacer al pasarle el parámetro.
ayuda="Este script mide la temperatura más alta del ordenador, la almacena en un archivo de log, espera un número determinado de segundos y repite la operación, tantas veces como se le indique.
Por defecto, espera $segundos segundos entre cada medición, se repite $repeticiones veces, almacena el resultado en $logfile y éste se ha limitado a un máximo de $lineas líneas.
Uso:
LogDeTemperaturas [-t segundos] [-r repeticiones] [-l logfile] [-m líneas]
-t Tiempo de espera entre mediciones
-r Número de mediciones a realizar
-l Ruta al archivo de log
-m Máximo número de líneas del log"
Luego añadiremos el modificador -h a la lista de getopts. En este caso no va seguido del carácter ":", puesto que no requiere de ningún valor.
while getopts t:r:l:m:h flag
do
case "${flag}" in
t) segundos=${OPTARG};;
r) repeticiones=${OPTARG};;
l) logfile=${OPTARG};;
m) lineas=${OPTARG};;
h) echo "$ayuda" && exit;;
*) echo -e "Parámetro incorrecto"
exit;;
esac
done
Así, cuando el usuario utilice ./temperaturas.sh -h, verá esto en la consola:
Y eso es to, eso es to, eso es todo amigos.
He tardado mucho más en escribir esta entrada que en crear el script. También es cierto que buena parte del código lo he sacado de otros scripts que ya tenía creados.
Como siempre, la licencia es "haz lo que quieras". Puede distribuirse, modificarse, utilizarse en otros scripts... sin limitaciones, sin coste y sin siquiera mencionar la autoría. No tiene garantía alguna ni me hago responsable delos daños que pueda ocasionar, etc... Lo hice para mí, funciona, y al que le venga bien que lo use como mejor le convenga.
No hay comentarios:
Publicar un comentario