(Я уже читал Как я могу протестировать новый скрипт 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
Что оно делает :
По умолчанию с большинством демонов cron по умолчанию, которые я видел, просто нет способа сказать cron, что он запускается прямо здесь прямо сейчас. Если вы используете anacron, возможно, я думаю, что можно запустить отдельный экземпляр на переднем плане.
Если ваши скрипты работают некорректно, значит, вы не учитываете, что
Из crontab (5):
Некоторые переменные среды устанавливаются автоматически демоном cron (8). SHELL установлен в / bin / sh, а LOGNAME и HOME устанавливаются из строки / etc / passwd владельца crontab. ПУТЬ установлен в "/ usr / bin: / bin". HOME, SHELL и PATH могут быть отменены настройками в crontab; LOGNAME - это пользователь, от имени которого выполняется задание, и его нельзя изменить.
В общем, самая большая проблема - PATH, поэтому вам необходимо:
Если вам нужно запустить сценарий от имени другого пользователя без оболочки (например, 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-)"
Пояснение:
Запускать задачу, как 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"