Poesía Binaria

Notifica, logea y enriquece tu experiencia de trabajo en Bash con este script

En nuestro trabajo diario peleando con sesiones de terminal hay ocasiones en las que, teniendo una sesión de terminal abierta, no sabemos a qué hora se ejecutó un comando determinado. O acabamos de iniciar una tarea que tiene pinta de ser muy larga y nos gustaría que el ordenador nos avisara cuando termine para no estar mirando cada poco tiempo. Además, seguro que a ti también te ha pasado, te acuerdas de que necesitas el aviso cuando la tarea está iniciada y no puedes pararla.

Pequeña introducción

No pretendo crear un sistema muy complejo para este propósito, para eso tenemos auditd, del que hablaré en próximos posts. Este es un pequeño sistema que consume pocos recursos y se dedica a:

Podemos ver, tras la finalización de un comando que ha tardado más de 2 segundos (por ejemplo, comando_largo) el siguiente mensaje, notificación:

Además, como ha tardado más de 10 segundos (los tiempo podremos configurarlos), veremos lo siguiente en el escritorio:

Por supuesto, podemos elegir desactivar/activar las notificaciones, o cómo se va a notificar desde otra ventana mientras la tarea está en ejecución.

El script

Pongo aquí una primera versión del script. Ya que se me ocurren muchas mejoras, y pequeños cambios que podemos hacer para enriquecer la experiencia aún más.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
#!/bin/bash

readonly BASHISTANT_DIR=$HOME/.bashistant
readonly BASHISTANT_LOCK=$BASHISTANT_DIR/lock
readonly BASHISTANT_LOCKFD=99

BASHISTANT_COLOR_ENABLE=1
BASHISTANT_INFO_ENABLE=1
BASHISTANT_NOTIFY_ENABLE=1
BASHISTANT_LOG_ENABLE=1
BASHISTANT_TIMER_COLOR='34'
BASHISTANT_NOTIFY_TIME=10               # If command lasts more than 10 seconds, notify
BASHISTANT_NOTIFY_COMMAND="@default"
BASHISTANT_SHOW_TIME=2
BASHISTANT_NOW_FORMAT="%d/%m/%Y %H:%M:%S"
BASHISTANT_NOW_COLOR='35'
BASHISTANT_INFO_ALIGN="right"
BASHISTANT_INFO_PADDING="  "
BASHISTANT_ELAPSED_COLOR='36'
BASHISTANT_LOAD_COLOR='38'

BASHISTANT_INFO_FORMAT="[ %NOW | %ELAPSED | %CPULOAD ]"

_BASHISTANT_START=

readonly MYPID=$$

MYPIDS=()

function onexit() {
        flock -u BASHISTANT_LOCKFD
        [ ! -r "$BASHISTANT_LOCK" ] || rm -f "$BASHISTANT_LOCK"
        echo "Ocurrió un problema y el programa se cerró inesperadamente" >2
        logger "Bashistant: There was a problem here"
}

function __bashistant_init() {
        [ -d "$BASHISTANT_DIR" ] || mkdir "$BASHISTANT_DIR"
        eval "exec $BASHISTANT_LOCKFD>"$BASHISTANT_LOCKFD"";
        readonly WINDOWPID=$(ps -o ppid,pid| grep $$ | awk '{print $1;exit}')
        if xset q &>/dev/null && hash xdotool; then
                readonly WINDOWID=$(xdotool search --pid $WINDOWPID | tail -1)
        fi
        COMMANDS=()
}

__bashistant_init

function __bashistant_get_timestamp() {
        date +%s
}

function __bashistant_print_info() {
        local INFO="${BASHISTANT_INFO_PADDING}$1"

        local INFO_NOCOLOR="$(echo -e "$INFO" | sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g")"

        if [ "
$BASHISTANT_INFO_ALIGN" = "right" ]; then
                echo -ne "
\033[${COLUMNS}C"
                echo -ne "
\033[${#INFO_NOCOLOR}D"
        fi
        if [ -n "
$BASHISTANT_COLOR_ENABLE" ] && [ $BASHISTANT_COLOR_ENABLE -eq 1 ]; then
                echo -e "
${INFO}"
        else
                echo -e "
${INFO_NOCOLOR}"
        fi
}

function __bashistant_set_color() {
        echo "
\033[${1}m"
}

function __bashistant_show_info() {
        local ELAPSED="
$1"

        if [ $ELAPSED -ge $BASHISTANT_SHOW_TIME ]; then
                local SHOWTIME="
"
                for elem in $BASHISTANT_INFO_FORMAT; do
                        SHOWTIME+="
"
                        case $elem in
                                "
%NOW")
                                        local NOW="
$(date +"${BASHISTANT_NOW_FORMAT}")"
                                        SHOWTIME+="
$(__bashistant_set_color $BASHISTANT_NOW_COLOR)${NOW}\033[0m"
                                        ;;
                                "
%ELAPSED")
                                        local ELTIME
                                        if [ $ELAPSED -eq 0 ]; then
                                                ELTIME="
0s"
                                        else
                                                ELTIME="
$((ELAPSED/86400))d $(date -ud@"$ELAPSED" "+%Hh %Mm %Ss")"
                                                ELTIME="
$(echo " $ELTIME" | sed -e 's/[[:space:]]00\?[dhms]//g' -e 's/^[[:space:]]*//')"
                                        fi
                                        SHOWTIME+="
$(__bashistant_set_color $BASHISTANT_ELAPSED_COLOR)$ELTIME\033[0m"
                                        ;;
                                "
%CPULOAD")
                                        local LOAD="
$(cat /proc/loadavg | awk '{ print $1 }')"
                                        SHOWTIME+="
$(__bashistant_set_color $BASHISTANT_LOAD_COLOR)$LOAD\033[0m"
                                        ;;
                                *)
                                        SHOWTIME+=$elem
                        esac
                done

                __bashistant_print_info "
$SHOWTIME"
        fi

}

function __bashistant_log_info() {
        local ELAPSED=$1
        local COMMAND="
$2"

        logger -t "
Bashistant" -i --id=$$ "($(id -un)) Time: ${ELAPSED}sec Command: $COMMAND"
}

function __bashistant_desktop_notification() {
        local COMAND="
$2"
        local ELAPSED="
$1"
        local MSG="
$3"
        if [ -z "
$MSG" ]; then
                MSG="
Comando finalizado: "$COMMAND" en $ELAPSED segundos"
        fi
        notify-send "
$MSG"
}

function __bashistant_zenity_notification() {
        local COMAND="
$2"
        local ELAPSED="
$1"
        local MSG="
$3"

        if [ -z "
$MSG" ]; then
                MSG="
Comando finalizado: "$COMMAND" en $ELAPSED segundos"
        fi
        echo zenity --info --width=300 --title="
Tarea finalizada" --text="$MSG"
}


function __bashistant_bringtofront_notification() {
        if [ -n "
$WINDOWID" ]; then
                xdotool windowactivate $WINDOWID
        fi
}


function __bashistant_notify_info() {
        local ELAPSED=$1
        local COMMAND="
$2"

        if [ $ELAPSED -ge $BASHISTANT_NOTIFY_TIME ]; then
                flock -x $BASHISTANT_LOCKFD
                NOTIFY="
$(cat $BASHISTANT_DIR/notify 2>/dev/null)"
                flock -u $BASHISTANT_LOCKFD
                rm -f "
$BASHISTANT_LOCK"
                NOTIFY="
${NOTIFY//%ELAPSED%/$ELAPSED}"
                NOTIFY="
${NOTIFY//%COMMAND%/$COMMAND}"
                NOTIFY="
${NOTIFY//%USER%/$(id -un)}"
                NOTIFY="
${NOTIFY//%HOSTNAME%/$(hostname)}"

                while read notifycommand; do
                        if [ -n "
$notifycommand" ]; then
                                declare -a "
ncommand=($notifycommand)"
                                case ${ncommand[0]} in
                                        "
@notify")
                                                __bashistant_desktop_notification "
$ELAPSED" "$COMMAND" "${ncommand[@]:1}"
                                                ;;
                                        "
@zenity")
                                                __bashistant_zenity_notification "
$ELAPSED" "$COMMAND" "${ncommamd[@]:1}"
                                                ;;
                                        "
@bringtofront")
                                                __bashistant_bringtofront_notification
                                                ;;
                                        *)
                                                "
${ncommand[@]}"
                                esac
                                unset ncommand
                        fi
                done <<< $NOTIFY

        fi
}

function notify() {
        local ARGUMENT="
$1"

        if [ -z "
$ARGUMENT" ]; then
                cat $BASHISTANT_DIR/notify 2>/dev/null
        else
                echo "
$ARGUMENT" > $BASHISTANT_DIR/notify
                echo "
Notificación definida con éxito"
        fi
}

function postcmd() {
    if [ "
${#COMMANDS[@]}" -gt 1 ]; then
                HISTORY=$(history 1)
                COMMAND="
${HISTORY:7}"
                if [ -z "
$_BASHISTANT_START" ]; then
                        # No start info
                        return
                fi
                local END=$(__bashistant_get_timestamp)
                local ELAPSED=$(($END - $_BASHISTANT_START))
                if [ -n "
$BASHISTANT_INFO_ENABLE" ] && [ $BASHISTANT_INFO_ENABLE -eq 1 ]; then
                        __bashistant_show_info "
$ELAPSED"
                fi

                if [ -n "
$BASHISTANT_LOG_ENABLE" ] && [ $BASHISTANT_INFO_ENABLE -eq 1 ]; then
                        __bashistant_log_info "
$ELAPSED" "$COMMAND"
                fi

            if [ -n "
$BASHISTANT_NOTIFY_ENABLE" ] && [ $BASHISTANT_NOTIFY_ENABLE -eq 1 ]; then
                        __bashistant_notify_info "
$ELAPSED" "$COMMAND"
                fi
fi;
    COMMANDS=();
    trap 'precmd' debug

}

function precmd() {
        if [ ${#COMMANDS[@]} -eq 0 ]; then
                _BASHISTANT_START=$(__bashistant_get_timestamp)
                #echo "
INICIA EJECUCIÓN: "$BASH_COMMAND
        fi
        COMMANDS+=("
$BASH_COMMAND");
}

readonly PROMPT_COMMAND="
postcmd"
trap 'precmd' debug

En principio el archivo lo llamé bashistant.sh (que viene de Bash Assistant, todo mezclado). Y quiero modificarlo un poco para integrarlo en los gscripts.

Activación

Para poder utilizar este script automáticamente en nuestras sesiones de terminal, podemos editar nuestro archivo ~/.bashrc y añadir la siguiente línea (cambiando la ruta del archivo por la adecuada en tu sistema):

1
source $HOME/gscripts/bashistant.sh

También podemos utilizar los archivo $HOME/.profile o /etc/profile. El último para instalar a nivel de sistema para todos los usuarios.
En principio se creará el directorio .bashistant en tu $HOME para almacenar información sobre las notificaciones, aunque en el futuro se utilizará para más cosas. La inicialización no es muy pesada. Aparte de crear el directorio mencionado anteriormente, obtenemos el ID del proceso emulador de terminal (konsole, xfce4-terminal, gnome-terminal…), y si estamos en un entorno gráfico, obtiene el ID de la ventana que lo gobierna, para resaltar la ventana cuando no nos encontramos visualizándola.

Log de comandos

Esta es la parte menos currada por el momento, se limita a llamar a logger con el comando una vez finalizado. Podemos ver un fragmento de /var/log/syslog aquí:

Dec 18 14:18:04 gaspy-ReDDraG0N Bashistant[25301]: (gaspy) Time: 0sec Command: cat pkey.pem
Dec 18 14:18:50 gaspy-ReDDraG0N Bashistant[25301]: message repeated 2 times: [ (malvado) Time: 4sec Command: rm -rf archivos_confidenciales]
Dec 18 14:43:48 gaspy-ReDDraG0N Bashistant[25301]: (gaspy) Time: 578sec Command: find -name ‘confidencial’
Dec 18 16:24:34 gaspy-ReDDraG0N Bashistant[10252]: (gaspy) Time: 0sec Command: pgrep -f apache

Y esto podría delatar al usuario malvado, que normalmente no debe tener permiso para editar logs ni nada parecido.

Este log podemos desactivarlo haciendo:

BASHISTANT_LOG_ENABLE=0

Si queremos pillar a alguien que ha ejecutado comandos malignos (que no es el cometido de este script), podríamos desactivar esta característica en el código.

Información tras la ejecución

En realidad esto lo encontré en el GitHub de Chuan Ji mientras buscaba información y me gustó la visualización que hacía tras cada comando. No le copié el código, como vemos, su proyecto tiene más precisión midiendo el tiempo. A mí, para Bash, no me interesaba tener demasiada precisión en ello. Pero además, quise completarlo y hacerlo algo más personalizable.

Para modificar el comportamiento de esta característica tenemos una serie de variables que podremos modificar desde nuestro terminal:

Después de unos días de uso se ha convertido en una buena herramienta sobre todo para determinar de un vistazo cuánto tiempo llevas trabajando en un servidor o necesitas saber cuánto ha tardado una tarea que has olvidado cronometrar. Sí, en ocasiones, lanzamos tareas como mysqldump, restauraciones de base de datos, instalaciones, orquestaciones de un sistema, etc. En definitiva, tareas que pueden llegar a tardar incluso varias horas y que, muchas veces te interesa saber cuánto tiempo han necesitado esas tareas, por ejemplo para hacer documentación. Pero cuando te acuerdas de que deberías cronometrarlo (bastaría con ejecutar un comando poniendo time delante), es cuando el proceso ha terminado, puede llevar ya una hora y no vas a cancelar el proceso a estas alturas… espero no ser el único al que le pasa esto 🙂

Configurar notificaciones

El archivo $HOME/.bashistant/notify lo podremos modificar con un editor de textos para introducir el comando que queremos que se ejecute para notificar la finalización de la orden de Bash. Igual que en el punto anterior, muchas veces puedo lanzar un comando que necesitará mucho tiempo para finalizar y me pongo a hacer otra cosa. Es en esos casos en los que olvido esa primera tarea que dejé haciéndose y puede pasar mucho tiempo hasta que me acuerdo de ella. Una solución rápida, sería ejecutar el comando así:

time comando_largo ; zenity --info --text=»El comando ha terminado»

Pero, como siempre, se me olvida hacer esto cuando voy a ejecutar la orden. Otra opción sería:

comando largo
^Z
fg ; zenity --info --text=»El comando ha terminado»

Es decir, una vez hemos lanzazdo el comando, pulsamos Control+Z para pausarlo y luego utilizamos fg para reanudarlo, haciendo que una vez reanudado se ejecute zenity para notificar su finalización. Aunque muchas veces no se pueden pausar los comandos sin cargarnos algo.

Así que este script, cuando termina la ejecución de la orden, mirará el archivo ~/.bashistant/notify para ver qué comando tiene que ejecutar. Lo que significa que, aunque la tarea esté en ejecución puedo modificar el contenido de ese archivo, que cuando el comando termine se leerá y se ejecutará lo que ahí diga. Para agilizar un poco más la definición de notificaciones podemos utilizar en el mismo comando de notificación las siguientes palabras clave:

Por lo que el archivo ~/.bashistant/notify quedaría así:

1
zenity --info --text="El comando %COMMAND% ejecutado por %USER%@%HOSTNAME% ha finalizado en %ELAPSED% segundos."

Además, disponemos de algunos comandos rápidos como:

Que permiten que ~/.bashistant/notify contenga:

1
@notify

Para realizar la función.

También podemos utilizar el comando notify para definir el comando de notificación. Por lo que, podemos ejecutar un comando muy largo en un terminal, y luego desde otro sitio (con el mismo usuario), hacer:

notify @bringtofront

Así, cuando termine el primer comando (el que nos lleva mucho tiempo), se traerá a primer plano la ventana de terminal desde la que se disparó.

Por otro lado, como sería muy pesado estar todo el rato viendo notificaciones de comandos terminados, ya que un simple cd o ls dispararía la notificación tenemos las variables:

Más posibilidades

En el comando de notificaciones podríamos, por ejemplo, reproducir un sonido, o una voz, o generar una línea con cURL que dispare un webhook de slack, gitlab o cualquier otra aplicación. Podemos programar el envío de un e-mail. O incluso podemos ejecutar varios comandos, uno por línea.

Si miráis el código podéis ver que hay ciertas cosas que no se usan, por ahora, como la captura de los comandos justo antes de que se ejecuten, o la variable MYPIDS reservada para una futura sorpresa en la siguiente versión. Eso sí, estoy abierto a sugerencias y, por supuesto, os invito a contribuir con este pequeño script.

Foto principal: unsplash-logoMathew Schwartz

También podría interesarte....