【问题标题】:Script behaves differently when run from cron job and from command line using django manage.py when using a cron supervisor使用 cron 管理员从 cron 作业和使用 django manage.py 的命令行运行时,脚本的行为不同
【发布时间】:2018-09-07 09:12:16
【问题描述】:

我知道 crons 在与命令行不同的环境中运行,但我到处都使用绝对路径,我不明白为什么我的脚本表现不同。我相信它与我的 cron_supervisor 有关,它在子进程中运行 django“manage.py”。

Cron:

0 * * * * /home/p1/.virtualenvs/prod/bin/python /home/p1/p1/manage.py cron_supervisor --command="/home/p1/.virtualenvs/prod/bin/python /home/p1/p1/manage.py envoyer_argent"

这将调用 cron_supervisor,它会调用脚本,但脚本不会像我运行时那样执行:

/home/p1/.virtualenvs/prod/bin/python /home/p1/p1/manage.py envoyer_argent

在通过另一个脚本运行脚本时,是否需要做一些特别的事情才能正确调用该脚本?

这里是主管,主要用于错误处理,并确保在 cron 脚本本身出现问题时得到警告。

import logging
import os
from subprocess import PIPE, Popen

from django.core.management.base import BaseCommand

from command_utils import email_admin_error, isomorphic_logging
from utils.send_slack_message import send_slack_message

CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = CURRENT_DIR + '/../../../'

logging.basicConfig(
  level=logging.INFO,
  filename=PROJECT_DIR + 'cron-supervisor.log',
  format='%(asctime)s %(levelname)s: %(message)s',
  datefmt='%Y-%m-%d %H:%M:%S'
)


class Command(BaseCommand):
  help = "Control a subprocess"

  def add_arguments(self, parser):
    parser.add_argument(
      '--command',
      dest='command',
      help="Command to execute",
    )
    parser.add_argument(
      '--mute_on_success',
      dest='mute_on_success',
      action='store_true',
      help="Don't post any massage on success",
    )

  def handle(self, *args, **options):
    try:
      isomorphic_logging(logging, "Starting cron supervisor with command \"" + options['command'] + "\"")
      if options['command']:
        self.command = options['command']
      else:
        error_message = "Empty required parameter --command"
        # log error
        isomorphic_logging(logging, error_message, "error")
        # send slack message
        send_slack_message("Cron Supervisor Error: " + error_message)
        # send email to admin
        email_admin_error("Cron Supervisor Error", error_message)
        raise ValueError(error_message)

      if options['mute_on_success']:
        self.mute_on_success = True
      else:
        self.mute_on_success = False

      # running process
      process = Popen([self.command], stdout=PIPE, stderr=PIPE, shell=True)
      output, error = process.communicate()

      if output:
        isomorphic_logging(logging, "Output from cron:" + output)

      # check for any subprocess error
      if process.returncode != 0:
        error_message = 'Command \"{command}\" - Error \nReturn code: {code}\n```{error}```'.format(
          code=process.returncode,
          error=error,
          command=self.command,
        )
        self.handle_error(error_message)

      else:
        message = "Command \"{command}\" ended without error".format(command=self.command)
        isomorphic_logging(logging, message)
        # post message on slack if process isn't muted_on_success
        if not self.mute_on_success:
          send_slack_message(message)
    except Exception as e:
      error_message = 'Command \"{command}\" - Error \n```{error}```'.format(
        error=e,
        command=self.command,
      )
      self.handle_error(error_message)

  def handle_error(self, error_message):
    # log the error in local file
    isomorphic_logging(logging, error_message)
    # post message in slack
    send_slack_message(error_message)
    # email admin
    email_admin_error("Cron Supervisor Error", error_message)

通过 cron_supervisor 被 cron 调用时脚本未正确执行的示例:

# -*- coding: utf-8 -*-

import json
import logging
import os

from django.conf import settings
from django.core.management.base import BaseCommand

from utils.lock import handle_lock

logging.basicConfig(
  level=logging.INFO,
  filename=os.path.join(settings.BASE_DIR, 'crons.log'),
  format='%(asctime)s %(levelname)s: %(message)s',
  datefmt='%Y-%m-%d %H:%M:%S'
)
class Command(BaseCommand):
  help = "Envoi de l'argent en attente"

  @handle_lock
  def handle(self, *args, **options):

    logging.info("some logs that won't be log (not called)")

logging.info("Those logs will be correcly logged")

此外,我还有一个我也不太了解的日志记录问题,我指定将日志存储在 cron-supervisor.log 中,但它们没有存储在那里,我不知道为什么。 (但这与我的主要问题无关,只是对调试没有帮助)

【问题讨论】:

  • 根据 .virtualenvs/prod/bin/python 的创建方式,它可能只是系统 Python 的符号链接。您需要做更多的工作才能真正激活virtualenv。此外,cron 和交互式使用之间还有许多其他因素不同;请参阅the Stack Overflow cron tag info page,也许还有这个常见问题解答:stackoverflow.com/questions/22743548/cronjob-not-running
  • 另外,你似乎在重塑subprocess.call()。如果你有一个相当最新的 Python,你可能应该看看subprocess.run()。另请参阅stackoverflow.com/questions/4256107/…,尤其是我的回答。
  • 我已经使用process = subprocess.Popen(bash_command, stdout=subprocess.PIPE, shell=True) 来运行实际的脚本。我查看了您的链接,但找不到与我的问题相关的任何内容。 .virtualenvs 文件夹是使用 virtualenv_wrapper 创建的。

标签: python django cron


【解决方案1】:

您的cron 作业不能只在 virtualenv 中运行 Python 解释器;这是完全不够的。你需要activate env 就像在交互式环境中一样。

0 * * * * . /home/p1/.virtualenvs/prod/bin/activate; python /home/p1/p1/manage.py cron_supervisor --command="python /home/p1/p1/manage.py envoyer_argent"

这已经足够复杂,您可能需要创建一个包含这些命令的单独包装脚本。

如果没有正确诊断当前脚本如何不起作用,完全有可能仅此修复是不够的。 Cron 作业不仅(或特别)需要绝对路径;与交互式 shell 相比的主要区别在于 cron 作业在不同且更空闲的环境中运行,例如shell 的PATH、各种库路径、环境变量等可能不同或完全缺失;当然,没有可用的互动设施。

系统变量有望由您的 virtualenv 处理;如果正确完成,激活它将设置脚本所需的所有变量(PATHPYTHONPATH 等)。只有当您以交互方式登录时,您的 shell 才会设置诸如区域设置之类的东西;但同样,没有细节,我们只是希望这对你来说不是问题。

有些人推荐绝对路径的原因是无论您的工作目录如何,这都可以工作。但是正确编写的脚本应该在任何目录中都可以正常工作;如果重要,cron 作业将在所有者的主目录中启动。如果您想从那里指向相对路径,这将在 cron 作业中正常工作,就像在外部一样。

顺便说一句,如果subprocess 模块中的更高级别包装器之一执行您想要的操作,您可能不应该使用subprocess.Popen()。除非与旧版 Python 版本的兼容性很重要,否则您可能应该使用 subprocess.run() ...尽管将 Python 作为 Python 的子进程运行也通常是无用的。另请参阅我对this related question.的回答

【讨论】:

  • 感谢您的深入解释,很有帮助。我尝试了activate,但得到了permission denied,所以我使用了workon production;,它做同样的事情,但没有解决它。我确实相信问题不在于环境本身,因为 cron 确实调用了我期望的文件,它只是对它没有任何作用。我猜它不知何故不明白它正在处理Command 类实例并且没有实例化它。但确实很难排除故障。你会建议什么来监督正确的 cron 执行并在出现问题时收到警告?
  • 我刚刚添加了一个额外的示例,其中通过 cron_supervisor 调用的脚本的最小设置基本上不会记录其 handle 方法中发生的任何事情
  • 我刚刚浏览了你详细的其他答案。我使用 Python 2.7,这也是我使用 subprocess.Popen() 的原因,因为如果我理解正确,subprocess.run() 在 2.7 中不可用。我还阅读了您关于从 python 运行 python 的建议,如果我理解正确,我宁愿调用我的 cron_supervisor.py ,它不会启动子进程,而是调用其他 python 模块来做需要做的事情。这实际上是有道理的......我们这样做的原因是这些脚本首先是croned,然后在上面创建了主管。我试试看!
  • 每个进程基本上只能调用一次logging.basicConfig,这可能是坚持使用子进程的原因;或者可能切换到更详细的日志记录控制。 subprocess.check_call() 绝对应该在 Python 2.7 中可用;您当前的代码基本上是重复的,除了您错误地将单个字符串作为列表传递给Popen。对于shell=True,第一个参数应该只是一个字符串。
  • 为了调试,我会禁用对文件的日志记录,只是将脚本的标准错误收集到一个文件中。
猜你喜欢
  • 2012-04-10
  • 1970-01-01
  • 2013-07-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-09
  • 2016-12-10
  • 2016-02-25
相关资源
最近更新 更多