¿Cuál es la mejor manera de garantizar que solo se ejecute una instancia de un script Bash?

¿Cuál es la mejor / la mejor forma de garantizar que solo se ejecute una instancia de un determinado guión, suponiendo que sea Bash en Linux?

En este momento estoy haciendo:

ps -C script.name.sh > /dev/null 2>&1 || ./script.name.sh 

pero tiene varios problemas:

  1. pone el cheque fuera de la secuencia de comandos
  2. no me permite ejecutar el mismo script desde cuentas separadas, lo que a veces me gustaría.
  3. -C verifica solo los primeros 14 caracteres del nombre del proceso

Por supuesto, puedo escribir mi propio manejo de archivos pidfile, pero siento que debería haber una manera simple de hacerlo.

Si el script es el mismo para todos los usuarios, puede usar un enfoque de lockfile . Si adquiere el candado, proceda de otra manera, muestre un mensaje y salga.

Como ejemplo:

 [Terminal #1] $ lockfile -r 0 /tmp/the.lock [Terminal #1] $ [Terminal #2] $ lockfile -r 0 /tmp/the.lock [Terminal #2] lockfile: Sorry, giving up on "/tmp/the.lock" [Terminal #1] $ rm -f /tmp/the.lock [Terminal #1] $ [Terminal #2] $ lockfile -r 0 /tmp/the.lock [Terminal #2] $ 

Después de que se haya adquirido /tmp/the.lock su script será el único con acceso a la ejecución. Cuando termines, solo quita el candado. En forma de script, esto podría verse así:

 #!/bin/bash lockfile -r 0 /tmp/the.lock || exit 1 # Do stuff here rm -f /tmp/the.lock 

El locking de asesoramiento se ha usado por años y se puede usar en scripts bash. Prefiero el simple flock (de util-linux[-ng] ) sobre el lockfile (de procmail ). Y siempre recuerde que una trampa en la salida (sigspec == EXIT o 0 , atrapando señales específicas es superflua) en esos scripts.

En 2009, lancé mi repetitivo script bloqueable (originalmente disponible en mi página wiki, actualmente disponible como esencia ). Transformar eso en una sola instancia por usuario es trivial. Utilizándolo también puede escribir fácilmente scripts para otros escenarios que requieran algún locking o sincronización.

Aquí está la repetición mencionada para su conveniencia.

 #!/bin/bash # SPDX-License-Identifier: MIT ## Copyright (C) 2009 Przemyslaw Pawelczyk  ## ## This script is licensed under the terms of the MIT license. ## https://opensource.org/licenses/MIT # # Lockable script boilerplate ### HEADER ### LOCKFILE="/var/lock/`basename $0`" LOCKFD=99 # PRIVATE _lock() { flock -$1 $LOCKFD; } _no_more_locking() { _lock u; _lock xn && rm -f $LOCKFILE; } _prepare_locking() { eval "exec $LOCKFD>\"$LOCKFILE\""; trap _no_more_locking EXIT; } # ON START _prepare_locking # PUBLIC exlock_now() { _lock xn; } # obtain an exclusive lock immediately or fail exlock() { _lock x; } # obtain an exclusive lock shlock() { _lock s; } # obtain a shared lock unlock() { _lock u; } # drop a lock ### BEGIN OF SCRIPT ### # Simplest example is avoiding running multiple instances of script. exlock_now || exit 1 # Remember! Lock file is removed when one of the scripts exits and it is # the only script holding the lock or lock is not acquired at all. 

Creo que el flock es probablemente la variante más fácil (y más memorable). Lo uso en un trabajo cron para autocodificar dvds y cds

 # try to run a command, but fail immediately if it's already running flock -n /var/lock/myjob.lock my_bash_command 

Use -w para los tiempos de espera o las opciones de dejar de esperar hasta que se libere el locking. Finalmente, la página man muestra un buen ejemplo para múltiples comandos:

  ( flock -n 9 || exit 1 # ... commands executed under lock ... ) 9>/var/lock/mylockfile 

Use la opción set -o noclobber e intente sobrescribir un archivo común.

Un pequeño ejemplo

 if ! (set -o noclobber ; echo > /tmp/global.lock) ; then exit 1 # the global.lock already exists fi # ...remainder of script... 

Un ejemplo más largo. Este ejemplo esperará el locking global, pero el tiempo de espera después de demasiado tiempo.

  function lockfile_waithold() { declare -ir time_beg=$(date '+%s') declare -ir maxtime=7140 # 7140 s = 1 hour 59 min. # waiting up to ${maxtime}s for /tmp/global.lock ... while ! \ (set -o noclobber ; \ echo -e "DATE:$(date)\nUSER:$(whoami)\nPID:$$" > /tmp/global.lock \ ) 2>/dev/null do if [ $(( $(date '+%s') - ${time_beg})) -gt ${maxtime} ] ; then echo "waited too long for /tmp/global.lock" 1>&2 return 1 fi sleep 1 done return 0 } function lockfile_release() { rm -f /tmp/global.lock } if ! lockfile_waithold ; then exit 1 fi # ...remainder of script lockfile_release 

Repost desde aquí por @Barry Kelly.

No estoy seguro de que haya una solución robusta de una sola línea, por lo que es posible que termine de hacer la suya propia.

Los archivos de locking son imperfectos, pero menos que el uso de ‘ps | grep | grep -v ‘pipelines.

Una vez dicho esto, puede considerar mantener el control del proceso separado de su secuencia de comandos: tener un script de inicio. O, al menos, factorícela en funciones que se encuentran en un archivo separado, por lo que puede que en el script de llamadas tenga:

 . my_script_control.ksh # Function exits if cannot start due to lockfile or prior running instance. my_start_me_up lockfile_name; trap "rm -f $lockfile_name; exit" 0 2 3 15 

en cada script que necesita la lógica de control. La trampa garantiza que el archivo de locking se elimine cuando la persona que llama sale, por lo que no tiene que codificar esto en cada punto de salida en el script.

Utilizar un script de control separado significa que puede verificar la cordura en casos límite: eliminar archivos de registro obsoletos, verificar que el archivo de locking esté asociado correctamente con una instancia del script en ejecución actualmente, dar una opción para matar el proceso en ejecución, y así sucesivamente. También significa que tienes una mejor oportunidad de usar grep en la salida de ps éxito. Un ps-grep se puede usar para verificar que un archivo de locking tenga un proceso en ejecución asociado. Tal vez podría nombrar sus archivos de locking de alguna manera para incluir información sobre el proceso: usuario, pid, etc., que puede ser utilizado por una invocación de script posterior para decidir si el proceso que creó el archivo de locking todavía existe.

primer ejemplo de prueba

 [[ $(lsof -t $0| wc -l) > 1 ]] && echo "At least one of $0 is running" 

segundo ejemplo de prueba

 currsh=$0 currpid=$$ runpid=$(lsof -t $currsh| paste -s -d " ") if [[ $runpid == $currpid ]] then sleep 11111111111111111 else echo -e "\nPID($runpid)($currpid) ::: At least one of \"$currsh\" is running !!!\n" false exit 1 fi 

explicación

“lsof -t” para listar todos los pides de las secuencias de comandos actuales en ejecución llamado “$ 0”.

El comando “lsof” tendrá dos ventajas.

  1. Ignore pids que está editando un editor como vim, porque vim edita su archivo de mapeo como “.file.swp”.
  2. Ignore los picos bifurcados por las secuencias de comandos actuales de shell en ejecución, que la mayoría de los comandos derivativos “grep” no pueden lograr. Utilice el comando “pstree -pH pidnum” para ver detalles sobre el estado de bifurcación del proceso actual.

Las distribuciones Ubuntu / Debian tienen la herramienta start-stop-daemon , que es para el mismo propósito que usted describe. Consulte también /etc/init.d/skeleton para ver cómo se usa al escribir scripts de inicio / detención.

– Noah

También recomendaría mirar chpst (parte de runit):

 chpst -L /tmp/your-lockfile.loc ./script.name.sh 

Una solución definitiva de una línea:

 [ "$(pgrep -fn $0)" -ne "$(pgrep -fo $0)" ] && echo "At least 2 copies of $0 are running" 

Encontré esto en las dependencias del paquete procmail:

apt install liblockfile-bin

Para ejecutar: dotlockfile -l file.lock

file.lock se creará.

Para desbloquear: dotlockfile -u file.lock

Use esto para listar este paquete de archivos / comando: dpkg-query -L liblockfile-bin

Tuve el mismo problema, y ​​se me ocurrió una plantilla que utiliza archivo de locking, un archivo pid que contiene el número de identificación del proceso y una comprobación kill -0 $(cat $pid_file) para hacer que las secuencias de comandos canceladas no detengan la siguiente ejecución. Esto crea una carpeta foobar- $ USERID en / tmp donde vive el archivo lock y el archivo pid.

Todavía puede llamar al script y hacer otras cosas, siempre y cuando mantenga esas acciones en alertRunningPS .

 #!/bin/bash user_id_num=$(id -u) pid_file="/tmp/foobar-$user_id_num/foobar-$user_id_num.pid" lock_file="/tmp/foobar-$user_id_num/running.lock" ps_id=$$ function alertRunningPS () { local PID=$(cat "$pid_file" 2> /dev/null) echo "Lockfile present. ps id file: $PID" echo "Checking if process is actually running or something left over from crash..." if kill -0 $PID 2> /dev/null; then echo "Already running, exiting" exit 1 else echo "Not running, removing lock and continuing" rm -f "$lock_file" lockfile -r 0 "$lock_file" fi } echo "Hello, checking some stuff before locking stuff" # Lock further operations to one process mkdir -p /tmp/foobar-$user_id_num lockfile -r 0 "$lock_file" || alertRunningPS # Do stuff here echo -n $ps_id > "$pid_file" echo "Running stuff in ONE ps" sleep 30s rm -f "$lock_file" rm -f "$pid_file" exit 0 

desde con su script:

 ps -ef | grep $0 | grep $(whoami) 

Encontré una manera bastante simple de manejar “una copia de script por sistema”. Sin embargo, no me permite ejecutar varias copias del script desde muchas cuentas (en Linux estándar).

Solución:

Al comienzo del guión, di:

 pidof -s -o '%PPID' -x $( basename $0 ) > /dev/null 2>&1 && exit 

Aparentemente pidof funciona muy bien de una manera que:

  • no tiene límite en el nombre del progtwig como ps -C ...
  • no requiere que haga grep -v grep (o algo similar)

Y no depende de los archivos de locking, que para mí es una gran victoria, porque transmitirlos significa que tienes que agregar el manejo de archivos de locking obsoletos, lo cual no es realmente complicado, pero si se puede evitar, ¿por qué no?

En cuanto a verificar con “una copia de script por usuario en ejecución”, escribí esto, pero no estoy muy contento con él:

 ( pidof -s -o '%PPID' -x $( basename $0 ) | tr ' ' '\n' ps xo pid= | tr -cd '[0-9\n]' ) | sort | uniq -d 

y luego verifico su salida, si está vacía, no hay copias del script del mismo usuario.

Aquí está nuestro bit estándar. Puede recuperarse de la secuencia de comandos de alguna manera morir sin limpiar su archivo de locking.

Escribe la ID del proceso en el archivo de locking si se ejecuta normalmente. Si encuentra un archivo de locking cuando comienza a ejecutarse, leerá la identificación del proceso del archivo de locking y comprobará si ese proceso existe. Si el proceso no existe, eliminará el archivo de locking obsoleto y continuará. Y solo si el archivo de locking existe Y el proceso aún se está ejecutando, saldrá. Y escribe un mensaje cuando sale.

 # lock to ensure we don't get two copies of the same job script_name="myscript.sh" lock="/var/run/${script_name}.pid" if [[ -e "${lock}" ]]; then pid=$(cat ${lock}) if [[ -e /proc/${pid} ]]; then echo "${script_name}: Process ${pid} is still running, exiting." exit 1 else # Clean up previous lock file rm -f ${lock} fi fi trap "rm -f ${lock}; exit $?" INT TERM EXIT # write $$ (PID) to the lock file echo "$$" > ${lock}