Назад | Перейти на главную страницу

Запуск cron-задания вручную и немедленно

(Я уже читал Как я могу протестировать новый скрипт cron?.)

У меня есть конкретная проблема (задание cron не запускается или не работает должным образом), но проблема является общей: я хотел бы отлаживать скрипты, которые хранятся в кроне. Я знаю, что могу настроить строку * * * * * crontab, но это не полностью удовлетворительное решение. Я хотел бы иметь возможность запускать задание cron из командной строки, как если бы его запускал cron (тот же пользователь, те же переменные среды и т. Д.). Есть ли способ сделать это? Ожидание 60 секунд для проверки изменений сценария нецелесообразно.

Вот что я сделал, и, похоже, в данной ситуации это работает. По крайней мере, он показывает мне ошибку, тогда как запуск из командной строки, поскольку пользователь не показывает ошибку.


Шаг 1: Я временно поместил эту строку в crontab пользователя:

* * * * *   /usr/bin/env > /home/username/tmp/cron-env

затем вынул его, как только файл был записан.

Шаг 2: Сделал себе небольшой сценарий bash run as-cron, содержащий:

#!/bin/bash
/usr/bin/env -i $(cat /home/username/tmp/cron-env) "$@"

Итак, как пользователь, о котором идет речь, я смог

run-as-cron /the/problematic/script --with arguments --and parameters

Это решение, очевидно, можно расширить, чтобы использовать sudo или что-то подобное для большей гибкости.

Надеюсь, это поможет другим.

Я представляю решение, основанное на ответе Пистоса, но без недостатков.

  • Добавьте следующую строку в crontab, например с помощью crontab -e

    * * * * *  /usr/bin/env > /home/username/cron-env
    
  • Создайте сценарий оболочки, который выполняет команду в той же среде, что и задания cron:

    #!/bin/sh
    
    . "$1"
    exec /usr/bin/env -i "$SHELL" -c ". $1; $2"
    

Использование:

run-as-cron <cron-environment> <command>

например

run-as-cron /home/username/cron-env 'echo $PATH'

Обратите внимание, что второй аргумент необходимо указать, если он требует аргумента. Первая строка сценария загружает оболочку POSIX в качестве интерпретатора. Вторая строка является источником файла среды cron. Это необходимо для загрузки правильной оболочки, которая хранится в переменной среды. SHELL. Затем он загружает пустую среду (чтобы предотвратить утечку переменных среды в новую оболочку), запускает ту же оболочку, которая используется для cronjobs, и загружает переменные среды cron. Наконец команда выполняется.

Поскольку crontab не выполняет свою работу, вам придется манипулировать его содержимым:

crontab -l | grep -v '^#' | cut -f 6- -d ' ' | while read CMD; do eval $CMD; done

Что оно делает :

  • перечисляет задания crontab
  • удалить строки комментариев
  • удалить конфигурацию crontab
  • затем запускайте их один за другим

По умолчанию с большинством демонов cron по умолчанию, которые я видел, просто нет способа сказать cron, что он запускается прямо здесь прямо сейчас. Если вы используете anacron, возможно, я думаю, что можно запустить отдельный экземпляр на переднем плане.

Если ваши скрипты работают некорректно, значит, вы не учитываете, что

  • скрипт запущен от имени конкретного пользователя
  • cron имеет ограниченное окружение (наиболее очевидное проявление этого - другой путь).

Из crontab (5):

Некоторые переменные среды устанавливаются автоматически демоном cron (8). SHELL установлен в / bin / sh, а LOGNAME и HOME устанавливаются из строки / etc / passwd владельца crontab. ПУТЬ установлен в "/ usr / bin: / bin". HOME, SHELL и PATH могут быть отменены настройками в crontab; LOGNAME - это пользователь, от имени которого выполняется задание, и его нельзя изменить.

В общем, самая большая проблема - PATH, поэтому вам необходимо:

  • Явно установите PATH в скрипте во время тестирования на / usr / bin: / bin. Вы можете сделать это в bash с помощью экспорт PATH = "/ usr / bin: / bin"
  • Явно установите правильный PATH, который вы хотите, в верхней части crontab. например ПУТЬ = "/ usr / bin: / bin: / usr / local / bin: / usr / sbin: / sbin"

Если вам нужно запустить сценарий от имени другого пользователя без оболочки (например, www-data), используйте sudo:

sudo -u www-data /path/to/crontab-script.sh

Конечно, первое, что нужно проверить перед всем этим, - это то, что ваш скрипт действительно выполняет то, что должен делать из командной строки. Если вы не можете запустить его из командной строки, очевидно, что он не будет работать с cron.

Сценарий Марко мне почему-то не подошел. У меня не было времени на отладку, поэтому я написал сценарий Python, который делает то же самое. Он длиннее, но: во-первых, у меня работает, во-вторых, мне легче понять. Измените «/ tmp / cron-env» на то место, где вы сохранили свою среду. Вот:

#!/usr/bin/env python
from __future__ import division, print_function

import sys
import os

def main():
    if len(sys.argv) != 2 or sys.argv[1] in ('-h', '--help'):
        print("Usage: {} CMD\n"
              "Run a command as cron would. Note that CMD must be quoted to be only one argument."
              .format(sys.argv[0]))
        sys.exit(1)
    _me, cmd = sys.argv
    env = dict(line.strip().split('=', 1) for line in open('/tmp/cron-env'))
    sh = env['SHELL']
    os.execvpe(sh, [sh, '-c', cmd], env)

if __name__ == '__main__':
    main()

Я нащупал ответ Марко. Код показан ниже, но я буду поддерживать этот сценарий Вот.

Учитывая этот crontab:

# m h  dom mon dow   command

X=Y
1 2 3 4 5 6 echo "Hello, world"
1 2 3 4 5 6 echo "Goodby, cruel world"
1 2 3 4 5 6 echo "Please spare me the drama"

Пример сеанса использования:

$ cronTest
This is the crontab for  without comment lines or blank lines:
     1  X=Y
     2  echo "Hello, world"
     3  echo "Goodby, cruel world"
     4  echo "Please spare me the drama"
Which line would you like to run as  now?
55
55 is not valid, please enter an integer from 1 to 4
2

Evaluating 1: X=Y

Evaluating 2: echo "Hello, world"
Hello, world

Это cronTest2, который необходимо правильно вызвать для настройки переменных среды так же, как это делает cron:

#!/bin/bash

# Prompt user for a user crontab entry to execute

function deleteTempFile {
  rm -f $TEMP_FILE
}

function debug {
  if [ "$DEBUG" ]; then >&2 printf "$1\n"; fi
}

function isValidLineNumber {
  # $1 - number of lines
  # $2 - requested line number
  if [[ -n "${2//[0-9]+/}" ]] && (( $2 <= $1 )); then echo true; else echo false; fi
}

function isVariableAssignment {
  [[ "$( echo "$1" | grep "=" )" ]]
}

function makeTempCrontab {
  local -r ASTERISK=\\*
  local -r NUMBER='[[:digit:]]{1,2}'
  local -r NUMBERS="$NUMBER(,$NUMBER)+"
  local -r CRON="^(($ASTERISK|$NUMBER|$NUMBERS)[[:space:]]+)"
  local -r CRON5_REGEX="$CRON{5}"
  local -r CRON6_REGEX="$CRON{6}"

  rm -f "$TEMP_FILE"

  local -r ALL_LINES="$( crontab -l )"

  # Ignore empty lines and lines starting with # (comment lines)
  local -r LINES="$( 
    echo "$ALL_LINES" | \
    grep -v '^[[:space:]]*#' | \
    grep -v '^[[:space:]]*$'
  )"

  if [[ -z "$LINES" ]]; then
    echo "Your crontab is empty, nothing to do"
    exit 1
  fi

  IFS=$'\n' 
  for LINE in $LINES; do
    LINE="$( echo "$LINE" | sed 's/\s\+$//e' )" # remove trailing space
    if [ "$( echo "$LINE" | grep "^$" )" ]; then  
      debug ""  # ignore empty line
    elif [ "$( echo "$LINE" | egrep "$CRON6_REGEX" )" ]; then
      debug "6 field date/time specifier: $LINE"
      # strip out when to run debug, leaving just the command to execute
      echo "$LINE" | cut -f 7- -d ' ' >> "$TEMP_FILE"
    elif [ "$( echo "$LINE" | egrep "$CRON5_REGEX" )" ]; then
      debug "5 field date/time specifier: $LINE"
      # strip out when to run debug, leaving just the command to execute
      echo "$LINE" | cut -f 6- -d ' ' >> "$TEMP_FILE"
    elif [ "$( echo "$LINE" | grep '^@' )" ]; then
      debug "@declaration: $LINE"
      # strip out @declaration, leaving just the command to execute
      echo "$LINE" | cut -f 2- -d ' ' >> "$TEMP_FILE"
    elif [ "$( echo "$LINE" | grep '=' )" ]; then
      debug "Variable assignment: $LINE"
      echo "$LINE"  >> "$TEMP_FILE"
    else
      debug "Ignored: $LINE"
    fi
  done
  unset IFS
}

function runUpToLine {
  # Scans up to given line number in $TEMP_FILE
  # Evaluates variable assignment
  # Executes specified line
  # Ignores remainder of file
  # Function definitions are not supported
  #
  # $1 - line number to run

  readarray CONTENTS < "$TEMP_FILE"
  for (( i=0; i<=$1; i++ )); do
    # >&2 echo "\$i=$i, \$1=$1, isVariableAssignment: $( isVariableAssignment $CONTENTS[$i] ), CONTENTS[$i]=${CONTENTS[$i]}"
    if isVariableAssignment ${CONTENTS[$i]} || (( $i == $1 )); then
      printf "\nEvaluating $(( i+1 )): ${CONTENTS[$i]}"
      eval "${CONTENTS[$i]}"
    fi
  done
}

function selectLine {
  >&2 echo "This is the crontab for $USER without comment lines or blank lines:"
  cat -n "$TEMP_FILE" >&2
  >&2 echo "Which line would you like to run as $USER now?"

  local -r NUM_LINES=$( cat "$TEMP_FILE" | wc -l )
  read LINE_NUMBER
  # >&2 echo "NUM_LINES=$NUM_LINES, LINE_NUMBER=$LINE_NUMBER;  valid: $( isValidLineNumber $NUM_LINES $LINE_NUMBER )"
  while [[ $( isValidLineNumber $NUM_LINES $LINE_NUMBER ) == false ]]; do
    >&2 echo "$LINE_NUMBER is not valid, please enter an integer from 1 to $NUM_LINES"
    read LINE_NUMBER
    # >&2 echo "NUM_LINES=$NUM_LINES, LINE_NUMBER=$LINE_NUMBER;  valid: $( isValidLineNumber $NUM_LINES $LINE_NUMBER )"
  done
  (( LINE_NUMBER-- ))
  echo ${LINE_NUMBER}
}

function doIt {
  export USER=$1
  local -r TEMP_FILE="$( mktemp crontabTest.XXX )"
  trap deleteTempFile EXIT

  makeTempCrontab
  local -r LINE_NUMBER="$( selectLine )"
  runUpToLine $LINE_NUMBER
}

doIt "$1" 

cronTest бежит cronTest2 с правильным набором переменных среды:

#!/bin/bash

# Execute a user crontab entry with the proper environment

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"

env -i bash --noprofile --norc -c "$DIR/cronTest2 $USER"

Ну, это тот же пользователь, который вы указали в записи crontab (или в чей crontab вы поместили его, поочередно), так что это не проблема. crontab(5) должен предоставить вам список установленных переменных среды, их всего несколько.

В большинстве crontab, например, vixie-cron вы можете разместить переменные в самом crontab, как это, а затем использовать / usr / bin / env, чтобы проверить, работает ли это. Таким образом, вы можете заставить свой скрипт работать в crontab, как только вы обнаружите, что не так со скриптом run-as-cron.

SHELL=/bin/bash
LANG=en
FASEL=BLA

* * * * *   /usr/bin/env > /home/username/cron-env

Решение Марко не сработало для меня, но скрипт Python Ноама работал. Вот небольшая модификация сценария Марко, которая заставила меня работать:

#!/bin/sh
. "$1"
exec /usr/bin/env -i "$SHELL" -c "set -a;. $1; $2"

Добавленный set -a экспортировать переменные, определенные в скрипте $ 1, и сделать их доступными для команды $ 2

p.s. Питон Ноама работал, потому что он «экспортировал» среду в дочерний процесс.

Если это сценарий оболочки, это должно помочь вам в большинстве случаев:

sudo su  # (assuming it's run as root, if not switch to the user you want it to run as)
cd  # Switch to home folder
sh <full-path/my-shell-script>

Это определенно высветит некоторые проблемы, если не все.

Я никогда не находил способ запускать задания cron вручную, но этот запись предлагает установить ту же среду, что и cronjob, и запустить скрипт вручную.

вы можете запрограммировать работу на следующую минуту :)

Вдохновленный ответом @ DjangoJanny, вот что я использую для запустить задание на 5 строчке моего crontab:

 eval "$(crontab -l | sed -n '5p' | tr -s ' ' | cut -d' ' -f 6-)"

Пояснение:

  • запускает команду, заданную:
  • отображает crontab
  • получает 5-ю строчку
  • заменяет несколько пробелов одним пробелом
  • берет все от 6-го столбца до конца

Запускать задачу, как cron, сложно. Для этого требуется измененная среда, неинтерактивная оболочка, не подключенный входной терминал и, возможно, также определенная оболочка (например, bin/sh вместо того /bin/bash).

Я написал сценарий, который решает все эти проблемы. Запустите его со своей командой / скриптом для запуска в качестве первого аргумента, и все готово. Он также размещен (и, возможно, обновляется в Github).

#!/bin/bash
# Run as if it was called from cron, that is to say:
#  * with a modified environment
#  * with a specific shell, which may or may not be bash
#  * without an attached input terminal
#  * in a non-interactive shell

function usage(){
    echo "$0 - Run a script or a command as it would be in a cron job, then display its output"
    echo "Usage:"
    echo "   $0 [command | script]"
}

if [ "$1" == "-h" -o "$1" == "--help" ]; then
    usage
    exit 0
fi

if [ $(whoami) != "root" ]; then
    echo "Only root is supported at the moment"
    exit 1
fi

# This file should contain the cron environment.
cron_env="/root/cron-env"
if [ ! -f "$cron_env" ]; then
    echo "Unable to find $cron_env"
    echo "To generate it, run \"/usr/bin/env > /root/cron-env\" as a cron job"
    exit 0
fi

# It will be a nightmare to expand "$@" inside a shell -c argument.
# Let's rather generate a string where we manually expand-and-quote the arguments
env_string="/usr/bin/env -i "
for envi in $(cat "$cron_env"); do
   env_string="${env_string} $envi "
done

cmd_string=""
for arg in "$@"; do
    cmd_string="${cmd_string} \"${arg}\" "
done

# Which shell should we use?
the_shell=$(grep -E "^SHELL=" /root/cron-env | sed 's/SHELL=//')
echo "Running with $the_shell the following command: $cmd_string"


# Let's route the output in a file
# and do not provide any input (so that the command is executed without an attached terminal)
so=$(mktemp "/tmp/fakecron.out.XXXX")
se=$(mktemp "/tmp/fakecron.err.XXXX")
"$the_shell" -c "$env_string $cmd_string" >"$so" 2>"$se" < /dev/null

echo -e "Done. Here is \033[1mstdout\033[0m:"
cat "$so"
echo -e "Done. Here is \033[1mstderr\033[0m:"
cat "$se"
rm "$so" "$se"