如何在 cron 作业调用的脚本中记录 shell 错误?

如何在 cron 作业调用的脚本中记录 shell 错误?

今天早上我发现一条消息,内容是这样的:

编辑:我还添加了消息标题,这样就可以清楚知道它来自哪里。

Return-Path: <root@REDACTED>
Received: from localhost (localhost [127.0.0.1])
  (uid 0)
  by REDACTED with local
  id 00000000005DC0DF.00000000633BA87E.000042C7; Tue, 04 Oct 2022 05:29:02 +0200
From: CronDaemon <root@REDACTED>
To: admlog@REDACTED
Subject: Cron <root@north> test -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
Mime-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
X-Cron-Env: <SHELL=/bin/sh>
X-Cron-Env: <PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin>
X-Cron-Env: <MAILTO=REDACTED>
X-Cron-Env: <HOME=/root>
X-Cron-Env: <LOGNAME=root>
Message-ID: <courier.00000000633BA87E.000042C7@REDACTED>
Date: Tue, 04 Oct 2022 05:29:02 +0200
X-Mime-Autoconverted: from 8bit to 7bit by courier 1.1

/etc/cron.daily/syslogrotate:
parse error: Invalid numeric literal at line 2, column 0
parse error: Invalid numeric literal at line 2, column 0

我修改syslogrotate为调用一个 shell 脚本来查看旋转的文件,并根据需要调用其他脚本。 Cron 将写入 stderr 的任何内容归因于它启动的第一个进程。如果知道实际的脚本文件,甚至行号,那就太好了。难道没有一个工具可以做到这一点吗?

答案1

让 syslog 调用的脚本检查它调用的每个子任务的退出代码,当子任务以非零代码退出时,脚本会向 stderr 写入一条消息,说明脚本的路径和名称。例如,/path/to/the/sub_task.sh returned exit code 2。该消息将包含在来自 cron 的电子邮件中,您将获得所需的脚本名称。

答案2

不,没有这样的工具。

bash有一些工具可以找到错误。

#!/bin/bash -v

您的脚本将在执行之前打印每个命令。这将显示错误到底发生在哪里。

或者您可以使用-x的密钥bash。这将提供有关脚本执行的更多信息。

如果您将错误定位到脚本的一小部分并且不想再打印全部内容,您可以使用set -xv

#!/bin/bash
some-good-commands
set -xv  # debug mode on
some-suspicious-code
set +xv  # debug mode off
some-good-commands

如果手动运行脚本无法让您了解问题的根源,则可以直接在 cron 作业中使用这些内容。

当然,您始终可以添加调试echo

#!/bin/bash
echo "Starting script A"
VAR=$1
echo "Executing `abc $VAR`"
abc $VAR
echo "abc ended with $?"

答案3

Sotto Voce 的想法很好。我们可以测试是否写出了任何内容,而不是测试返回代码。这样一来,人们就可以检测 cron 脚本来编写一些上下文,以防出现错误。

我不知道如何在 bash 中测试输出,所以我编写了一个名为的小 C 实用程序any_output

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    long cur_out = lseek(1, 0, SEEK_END);
    if (cur_out < 0) cur_out = 0;
    long cur_err = lseek(2, 0, SEEK_END);
    if (cur_err < 0) cur_err = 0;
    if (argc > 1)
    {
        char *slash, *end;
        long last_out = strtol(argv[1], &slash, 10);
        if (*slash == '/')
        {
            long last_err = strtol(slash + 1, &end, 10);
            if (*end != 0 && *end != '\n')
                last_err = -1;
            else
            {
                int rtc = cur_err > last_err || cur_out > last_out;
                if (rtc)
                    fprintf(stderr, "%s, %ld > %ld || %ld > %ld\n",
                        argv[1], cur_err, last_err, cur_out, last_out);
                return rtc;
            }
        }
        else
        {
            fputs("argument to any_output is its previous output\n", stderr);
            return 0;
        }
    }
    else
    {
        printf("%ld/%ld\n", cur_out, cur_err);
        return 0;
    }
}

接下来我写了一个测试脚本。它调用了,jq因为这是我得到的错误。

#! /bin/bash

output=$(/home/ale/tmp/any_output)
if [ -f /home/ale/tmp/test.data ]; then
    if [[ "$(jq .j < /home/ale/tmp/test.data)" != "1" ]]; then
        touch /home/ale/tmp/test.data
    fi
fi
/home/ale/tmp/any_output $output || echo ${BASH_SOURCE[*]}

该路径是我当前目录的路径。 test.data 包含在内{"j":1},因此该脚本几乎是无操作的。我设置了 crontab 每两分钟调用一次测试脚本。有一段时间没有发生任何事情,直到我写入hellotest.data。在下次运行时,cron 给我发了这样的邮件:

From: CronDaemon <REDACTED>
To: REDACTED
Subject: Cron <ale@pcale> /home/ale/tmp/test.sh
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
X-Cron-Env: <SHELL=/bin/sh>
X-Cron-Env: <PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin>
X-Cron-Env: <MAILTO=REDACTED>
X-Cron-Env: <HOME=/home/ale>
X-Cron-Env: <LOGNAME=ale>
Date: Thu, 06 Oct 2022 14:18:01 +0200
X-Mime-Autoconverted: from 8bit to 7bit by courier 1.0

parse error: Invalid numeric literal at line 2, column 0
0/0, 57 > 0 || 57 > 0
/home/ale/tmp/test.sh

正文第二行中的乱码用于调试仪器本身。我有一些误报,因为lseek(1, 0, SEEK_END)返回 -1,即使isatty(1)是 false。所以我删除了对上面的调用isatty并设置了代码。

相关内容