4.2.2. Planificación con SystemD

Para la planificación de tareas SystemD provee un tipo de unidades denominadas timer (systemd.timer). La gestión de estas unidades posibilita tal planificación, de modo que centraremos el estudio en ellas. Antes de empezar a verlos es muy útil consultar cuáles son las unidades de tiempo activas en nuestro sistema:

$ systemctl list-timers
NEXT                         LEFT        LAST                        PASSED        UNIT                         ACTIVATES
Mon 2023-03-20 23:18:42 CET  7h left     Sun 2023-03-19 09:49:22 CET 1 day 6h ago  apt-daily.timer              apt-daily.service
Mon 2023-03-20 23:49:22 CET  7h left     Sun 2023-03-19 17:14:01 CET 22h ago       man-db.timer                 man-db.service
Tue 2023-03-21 00:00:00 CET  8h left     -                           -             dpkg-db-backup.timer         dpkg-db-backup.service
Tue 2023-03-21 00:00:00 CET  8h left     Mon 2023-03-20 07:21:04 CET 8h ago        logrotate.timer              logrotate.service
Tue 2023-03-21 06:41:23 CET  14h left    Mon 2023-03-20 07:41:02 CET 8h ago        apt-daily-upgrade.timer      apt-daily-upgrade.service
Tue 2023-03-21 15:49:49 CET  23h left    Mon 2023-03-20 15:49:49 CET 9min ago      systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service
Sun 2023-03-26 03:10:32 CEST 5 days left Sun 2023-03-19 07:24:22 CET 1 day 8h ago  e2scrub_all.timer            e2scrub_all.service
Mon 2023-03-27 00:10:44 CEST 6 days left Mon 2023-03-20 12:38:27 CET 3h 21min ago  fstrim.timer                 fstrim.service

8 timers listed.
Pass --all to see loaded but inactive timers, too

Nota

Añadiendo la opción --user obtendríamos las creadas y gestionadas por el propio usuario.

4.2.2.1. Timers

Las unidades timer son las encargadas de definir la programación de tareas mediante dos estrategias distintas:

  • Un tiempo determinado por el calendario (p.e. los martes a las dos de la mañana) a semejanza de los trabajos ejecutados mediante cron.

  • Un tiempo definido a partir de un momento inicial variable asociado a un evento como el arranque del sistema (p.e. un minuto después del arranque).

Para la programación de tareas necesitamos tres elementos:

  • Un ejecutable encargado de realizar la propia tarea.

  • Un servicio sin sección [Install] que refiere tal ejecutable.

  • Una unidad timer que comparte nombre con el servicio anterior y que define cuándo ejecutarlo.

Antes de generalizar, ilustraremos su uso con el siguiente problema:

mpv es un excelente reproductor multimedia derivado del veterano MPlayer que al pulsar «Q» permite abandonar la reproducción recordando su estado (momento exacto, tamaño de la ventana, etc.), de suerte que, al volver a reproducir el vídeo, lo recupera. Para ello, almacena en el directorio $XDG_STATE_HOME/mpv/watch_later archivos con los estados de las reproducciones que se pidió recordar. El archivo correspondiente a un vídeo se borra cuando la reproducción de este acaba o bien cuando se abandona la reproducción sin pedir que se recuerde (pulsando q). El problema surge cuando un vídeo cuyo estado se pidió recordar, simplemente se olvida y su archivo correspondiente se borra, ya que no volverá a reproducirse y, en consecuencia, el archivo que almacena su estado quedará perennemente almacenado.

Para resolver este problema nos planteamos ejecutar al abrir la sesión de usuario la tarea de borrar los archivos de estado con más de un mes de antiguedad.

Como ejecutable, podemos usar este:

#!/bin/sh

STATEDIR=${XDG_STATE_HOME:-$HOME/.local/state}

[ -d "$STATEDIR/mpv/watch_later" ] || exit 0

find "$STATEDIR/mpv/watch_later" -type f -mtime +30 -delete

Para el cual creamos la unidad de servicio mpvcleaner.service:

[Unit]
Description=Borra los estados de mpv con más de un mes de antigüedad

[Service]
ExecStart=/bin/sh /usr/local/bin/mpvcleaner.sh
Type=oneshot

que es de tipo oneshot, esto es, ejecuta nuestro script y espera a que acabe, lo cual es lógico, puesto que el tiempo de ejecución es mínimo.

Y el timer mpvcleaner.timer:

[Unit]
Descripcion=Ejecuta al abrir la primera sesión de usuario mpvcleaner.sh

[Timer]
OnStartupSec=5s

[Install]
WantedBy=timers.target

Ahora debemos colocar cada archivo en su lugar:

  • El ejecutable hemos decidido colocarlo en /usr/local/bin/ como se desprende del texto de la unidad de servicio.

  • Por coherencia con la ubicación del ejecutable (un directorio común en vez de ~/bin/), colocaremos las unidades en /etc/systemd/user/.

    # mv mpvcleaner.{timer,service} /etc/systemd/user
    

Y listo, ya puede el usuario habilitarlo:

$ systemctl --user daemon-reload
$ systemctl --user enable mpvcleaner.service
$ systemctl --user enable mpvcleaner.timer
$ systemctl --user start mpvcleaner.timer

Visto el ejemplo, profundicemos más en la propia unidad timer que es realmente lo único nuevo a lo ya visto para systemd. Para ello, copiemos la de nuestro ejemplo y discutamos sobre ella:

[Unit]
Descripcion=Ejecuta al abrir la primera sesión de usuario mpvcleaner.sh

[Timer]
OnStartupSec=5s

[Install]
WantedBy=timers.target

Por lo general se utilizar tres secciones:

[Unit]

en que basta con declarar la descripción.

[Install]

en que normalmente siempre indicaremos lo mismo: las unidades habilitadas se activarán con timers.target.

[Timer]

que es la que realmente tiene más chicha, porque es en la que definimos cuál es el servicio asociado y cuándo y con qué frecuencia debe ejecutarse.

El servicio asociado se define con la variable

Unit= (p.e. Unit = foobar.service)

pero, si no se expresa, se sobrentiende que será el servicio que comparte nombre con el timer, de ahí que en nuestro ejemplo no se haya incluido.

Otra variable importante es

AccuracySec=

que indica la precisión en la periodicidad y que, por defecto, está fijada a 1 minuto, por lo que debe definirse cuando la periodicidad es inferior a este intervalo de tiempo.

El cuándo y la frecuencia se expresan con distintas variables, la expresión de cuyos valores se encuentra recogida en la página de systemd.time:

OnCalendar=

es la única variable que permite definir un tiempo y frecuencia referido a las fechas del calendario a la manera en que puede hacerse con cron. Su formato se divide en tres partes y es:

día_semana año-mes-dia hora:minuto:segundo

en que las comas expresan varias unidades (varios días, varios meses, etc.), los dos puntos seguidos (..) expresan rangos, la barra (/) expresa periodicidad; y el asterisco, cualquier valor. Por ejemplo:

  • Una fecha concreta:

    [Time]
    OnCalendar=Tue 2023-03-21 10:40:32
    

    Obsérvese que podríamos habernos ahorrado la expresión del día de las semana, puesto que el 21 de marzo de 2023 sólo puede ser martes.

  • Dos días concretos:

    [Time]
    OnCalendar=* 2023-03-21,23 10:40:32
    

    Como la primera parte no se fija en absoluto y cada una de las tres tiene una sintaxis diferente, podemos eliminarla, al no existir confusión:

    [Time]
    OnCalendar=2023-03-21,23 10:40:32
    
  • Un rango de días:

    [Time]
    OnCalendar=2023-03-21..30 10:40:32
    
  • Cada veinte segundos (en el segundo 0, 20 y 40):

    [Time]
    OnCalendar=*-*-* *:*:0/20
    

    o bien:

    [Time]
    OnCalendar=*:*:0/20
    
  • Cada veinte minutos:

    [Time]
    OnCalendar=*:0/20:*
    

    aunque podemos ahorrarnos la expresión de los segundos:

    [Time]
    OnCalendar=*:0/20
    
  • Semanalmente, todos los domingos:

    [Time]
    OnCalendar=Sun *-*-* 00:00:00
    
OnBootSec=

el servicio se activa una vez transcurrido el tiempo especificado después del arranque del sistema. Por ejemplo:

[Time]
OnBootSec=5m

lanzaría el servicio 5 minutos después de haber arrancado el sistema. Los espacios de tiempo pueden expresarse en us (microsegundos), ms (milisegundos). s (segundos), m (minutos), h (horas), d (días), M (meses), y (años). Por ejemplo:

50s
1m 30s
12h 12s
1M 1d
OnActiveSec=

el servicio se activa una vez transcurrido el tiempo especificado después de activarse la unidad de tiempo.

OnStartupSec=

el servicio se activa una vez transcurrido el tiempo especificado después de haberse activado el gestor de servicios. Cuando se trata de un servicio de sistema, su gestor se activa poco después del arranque con lo que no hay excesiva diferencia con OnBootSec=. Sin embargo, si se trata de un servicio de usuario, el gestor se activa al ingresar tras el arranque por primera vez el usuario. Por tanto:

# Esta unidad de tiempo la activa el usuario bartolito.

[Time]
OnStartupSec=15s

El servicio se activará quince segundos después de acceder bartolito al sistema y, si nunca llega a ingresar, el servicio no se activará nunca.

OnUnitActiveSec=

El servicio se activa una vez transcurrido el tiempo especificado después de haberse activado previamente el propio servicio. Esto significa que el servicio se ejecutará con una periodicidad definida por dicho tiempo. Pero ¿cómo se activó el servicio por primera vez? Obviamente, porque alguna de las variables anteriores produjo tal activación. Por ejemplo:

[Time]
OnBootSec=5m
OnUnitActiveSec=30s
AccuracySec=1s

En este caso el servicio se activará cinco minutos después de haber arrancado el sistema y, a partir de ese momento, se activará cada treinta segundos.

Nota

Como esa periodicidad es menor al minuto, debemos añadir AccuracySec=.

Por supuesto, la activación pudo producirse con OnCalendar=, por lo que OnUnitActiveSec= en este caso sería un modo alternativo a expresar la periodicidad mediante la sintaxis propia de OnCalendar=.

OnUnitInactiveSec=

Semejante a la anterior, pero el tiempo se cuenta no desde que se activa el servicio, sino desde que se completa.

Además de todas las variables indicadas anteriormente, tiene interés:

Persistent=

que sólo afecta si se utiliza OnCalendar=. Por defecto es falsa (false), pero cuando verdadera (true), el servicio se activa al activarse el timer, si debió activarse mientras el timer se encontraba inactivo. Por ejemplo:

[Timer]
OnCalendar=00:00:00
Persistent=True

El servicio se activa todos los días a medianoche. Pero ¿qué ocurre si una noche a esa hora el sistema está apagado? Sin Persistent= esa activación, simplemente, se perderá. En cambio, al encontrarse definida a verdadero, el servicio se activará tras el arranque del sistema en cuanto se active la unidad de tiempo. Es, por tanto, el equivalente a anacron.

4.2.2.2. Tareas diferidas

Lo visto hasta ahora nos resuelve cómo utilizar SystemD para sustituir al tradicional cron. Ahora bien, ¿hay algún modo de ejecutar puntualmente una tarea en diferido a la manera de at? La respuesta es, con matices, sí, y se halla en la ejecución de servicios efímeros con systemd-run.

Básicamente consiste en usar systemd-run como ya se ha visto, pero añadiendo antes del argumento posicional que comienza a definir la tarea las opciones propias de una unidad timer que son básicamente --on-calendar, --on-boot, --on-active, --on-startup, --on-unit-active y --on-unit-deactive, cuyo significado no es necesario repetir porque sus nombres son calcos de las variables que pueden incluirse en una sección [Timer] de una unidad de tiempo. Obviamente, la unidad efímera se activa al ejecutar la orden systemd-run, por lo que en este caso --on-active indicará el espacio de tiempo que se difiere la ejecución de la orden. Por ejemplo:

$ systemd-run --user --on-active="1m 20s" --timer-property="AccuracySec=1s" touch /tmp/LO.HARE.MAS.TARDE

creará el archivo indicado un minuto y veinte segundos después de haberse ejecutado la orden. Obsérvese, además, que, como la precisión en la periodicidad es de un minuto por defecto, es necesario rebajarla usando AccuracySec=. Tal variable no tiene opción propia, así que puede introducirse a través de --timer-property.

Nota

Por supuesto, como en el caso de las unidades de tiempo persistentes, es posible añadir --on-unit-active para lograr periodicidad:

$ systemd-run --user --on-active=30s --on-unit-active=20s --timer-property=AccuracySec=1s touch /tmp/LO.HARE.MAS.TARDE

Ahora bien, ¿cuál es el problema que impide que systemd-run sea un sustituto completo para at? Básicamente el que se expresa en esta petición registrada en el GitHub del proyecto: que las unidades efímeras no sobreviven a un apagado del sistema. Como consecuencia, si apagamos (o reiniciamos) el equipo, tales unidades se perderán y las tareas diferidas jamás se ejecutarán[1].

Notas al pie