crontab 或 cron 表是一个 Linux 系统进程/守护进程,它有助于安排重复性任务,从而简化我们的日常工作。在本教程中,我们将创建一个动态 PHP 类,它允许我们使用安全连接来操作 crontab。
背景:Crontab 概述
能够安排任务在后台运行真是太棒了!备份 SQL 数据库、获取或发送电子邮件、运行清理任务、分析性能,甚至抓取 RSS 源 - cron 作业太棒了!
尽管安排新作业的语法乍一看似乎令人畏惧,但一旦将其分解,就会相对简单地理解。 cron 作业始终有五列,每一列代表一个按时间顺序排列的运算符,后跟完整路径和要执行的命令:
* * * * * home/path/to/command/the_command.sh
登录后复制
每个时间列都与任务计划有特定的相关性。它们如下:
- 分钟表示给定小时的分钟数,分别为 0-59。
- Hours 表示给定日期的小时数,分别为 0-23。
- Days 表示给定月份的天数,分别为 1-31。
- 月份代表给定年份的月份,分别为 1-12。
- 星期几表示星期几,周日到周六,数字分别为 0-6。
Minutes [0-59]
| Hours [0-23]
| | Days [1-31]
| | | Months [1-12]
| | | | Days of the Week [Numeric, 0-6]
| | | | |
* * * * * home/path/to/command/the_command.sh
登录后复制
例如,如果您想在每个月的第一天上午 12 点安排一个任务,它看起来像这样:
0 0 1 * * home/path/to/command/the_command.sh
登录后复制
另一方面,如果您想安排任务在每周六上午 8:30 运行,我们将按如下方式编写:
30 8 * * 6 home/path/to/command/the_command.sh
登录后复制
还有许多运算符可用于进一步自定义时间表:
- 逗号用于为任何 cron 列创建以逗号分隔的值列表。
- 破折号用于指定值范围。
- 星号用于指定全部或每个值。
默认情况下,每当执行计划任务时,crontab 都会发送电子邮件通知。但在许多情况下,这是没有必要的。我们可以通过将此命令的标准输出重定向到黑洞或/dev/null
设备来轻松抑制此功能。本质上,这是一个将丢弃写入其中的所有内容的文件。输出重定向是通过 >
运算符完成的。让我们看一下如何使用示例 cron 作业将标准输出重定向到黑洞,该作业每周六上午 8:30 运行计划任务:
30 8 * * 6 home/path/to/command/the_command.sh >/dev/null
登录后复制
此外,如果我们将标准输出重定向到空设备,我们可能还需要重定向标准错误。我们可以通过简单地将标准错误重定向到标准输出已经重定向的位置(空设备)来做到这一点!
30 8 * * 6 home/path/to/command/the_command.sh >/dev/null 2>&1
登录后复制
使用 PHP 管理 crontab:蓝图
为了使用 PHP 管理 crontab,我们需要能够以我们正在编辑其 crontab 的用户身份在远程服务器上执行命令。虽然您可以使用 PHP 提供的 SSH2 库,但我们将使用 phpseclib 库,它支持不同的协议与远程服务器通信。
如何安装和使用 phpseclib 库
phpseclib 库(PHP 安全通信库)是一个 PHP 库,它提供了一组用于安全通信协议(例如 SSH、SFTP 和 SSL/TLS)的方法和类。它使我们能够将安全通信功能合并到我们的应用程序中,而无需依赖外部命令行工具或扩展。
如果您想知道为什么您更愿意使用 phpseclib 而不是 PHP 提供的核心 SSH2 功能,让我们看看 phpseclib 库的好处:
- SSH2 库提供的功能有限,可能不支持所有 SSH 功能
- 提供抽象,使您可以轻松地在不同协议之间切换
- 抽象底层协议的复杂性
- 易于使用且直观
- 它不需要任何外部依赖项或扩展
- 支持多种安全通信协议,包括 SSH、SFTP、SCP、FTPS、SSL/TLS 等
- 文档齐全、开发积极且有社区支持
如您所见,phpseclib 库可以为安全通信需求提供更全面、更强大的解决方案。
让我们看看如何安装它。您可以使用 Composer 安装 phpseclib 库,如以下代码片段所示。
$composer install phpseclib/phpseclib
登录后复制
安装完成后,您可以通过在 PHP 脚本顶部包含自动加载器脚本来开始在 PHP 代码中使用 phpseclib 库,如以下代码片段所示。
登录后复制
实现 Ssh2_crontab_manager
类
在本节中,我们将实现类的各种方法。
构造函数
类构造函数主要负责建立和验证 SSH2 连接。
让我们看一下__construct
方法。
public function __construct($host = null, $port = null, $username = null, $password = null)
{
$path_length = strrpos(__FILE__, "/");
$this->path = substr(__FILE__, 0, $path_length) . '/';
$this->handle = 'crontab.txt';
$this->cron_file = "{$this->path}{$this->handle}";
try {
if (is_null($host) || is_null($port) || is_null($username) || is_null($password)) {
throw new Exception("Please specify the host, port, username, and password!");
}
$this->connection = new SSH2($host, $port);
if (!$this->connection->login($username, $password)) {
throw new Exception("Could not authenticate '{$username}' using password: '{$password}'.");
}
} catch (Exception $e) {
$this->error_message($e->getMessage());
}
}
登录后复制
首先,它需要四个参数,每个参数都有一个默认值NULL
:
-
$host
:代表我们要连接的远程服务器的IP地址。 -
$port
:用于 SSH2 连接的端口。 -
$username
:代表服务器的用户登录名。 -
$password
:代表服务器的用户密码。
首先,它计算当前文件的路径并将其设置为$this->path
变量。它还设置要在 $this->handle
中使用的 cron 文件的名称,并在 $this->cron_file
中构造 cron 文件的完整路径。 p>
然后,我们检查是否缺少任何必需的参数,如果缺少,则会引发异常并附带相应的错误消息。
接下来,它通过传入 $host
和 $port
值来创建 SSH2
类的实例,以建立 SSH 连接。
最后,它尝试通过调用 SSH2 连接对象的 login()
方法,使用提供的 $username
和 $password
进行身份验证。如果身份验证成功,则建立连接。如果失败,则会抛出异常。
exec
方法
exec
方法负责在远程服务器上执行命令。它类似于手动将命令输入到 shell(例如 PuTTY)中。为了获得更大的灵活性,我们将公开此方法,以便用户可以实际执行他们可能需要运行的任何其他命令。
让我们看一下 exec
方法。
public function exec()
{
$argument_count = func_num_args();
try {
if (!$argument_count) {
throw new Exception("There is nothing to execute, no arguments specified.");
}
$arguments = func_get_args();
$command_string = ($argument_count > 1) ? implode(" && ", $arguments) : $arguments[0];
$stream = $this->connection->exec($command_string);
if (!$stream) {
throw new Exception("Unable to execute the specified commands: {$command_string}");
}
} catch (Exception $e) {
$this->error_message($e->getMessage());
}
return $this;
}
登录后复制
我们的 exec()
方法使用 phpseclib 库的 exec
方法来执行命令。
我们在此方法中要做的第一件事是定义一个变量,表示传递的参数总数。我们将使用 PHP 的 func_num_args() 函数来获取此计数。接下来,在 try 块中,我们将检查是否有任何参数传递给此方法。如果参数计数为 0,我们将抛出一个带有适当消息的新异常。
接下来,使用 func_get_args()
函数,我们将创建一个包含传递给此方法的所有参数的数组。然后,我们将使用三元运算符定义一个新变量 $command_string
,作为我们将执行的实际 Linux 命令的单行字符串表示形式。
如果我们确实要执行多个命令,我们将使用 PHP 的 implode()
函数将参数数组解析为字符串。我们将向 implode()
传递两个参数:粘合或分隔符,它基本上只是一个将插入到数组元素和数组本身之间的字符串。我们将使用 Linux 运算符 &&
分隔每个元素,这允许我们在一行上顺序执行多个命令!
准备好命令并将其解析为字符串后,我们现在可以尝试使用 SSH2 类中的 exec() 方法通过已建立的 SSH 连接执行命令字符串。返回的值存储在 $stream
变量中。
write_to_file
方法
下一个方法,write_to_file()
,将负责将现有的 crontab 写入临时文件,或者在不存在 cron 作业的情况下创建一个空白临时文件。它需要两个参数:
- 我们将创建的临时文件的路径
- 我们在创建它时应该使用的名称
write_to_file
方法应如下所示。
public function write_to_file($path=NULL, $handle=NULL)
{
if (! $this->crontab_file_exists())
{
$this->handle = (is_null($handle)) ? $this->handle : $handle;
$this->path = (is_null($path)) ? $this->path : $path;
$this->cron_file = "{$this->path}{$this->handle}";
$init_cron = "crontab -l > {$this->cron_file} && [ -f {$this->cron_file} ] || > {$this->cron_file}";
$this->exec($init_cron);
}
return $this;
}
登录后复制
首先,我们使用布尔型 crontab_file_exists()
方法检查 cron 文件是否已存在,我们很快就会创建该方法。如果该文件存在,则无需继续。如果不是,我们将使用三元运算符来检查 $path
和 $handle
属性以确定它们是否为 NULL
代码>.如果它们中的任何一个为NULL
,我们将使用构造函数中预定义的回退来定义它们。
然后,我们将这些属性连接在一起形成一个新属性,它表示临时 cron 文件的完整路径和文件名。
接下来,我们使用带有 -l
参数的 Linux 命令 crontab
将用户 crontab 显示为标准输出。然后,使用 Linux 的 >
运算符,我们将标准输出或 STDOUT
重定向到我们的临时 cron 文件,而不是连接 $this->cron_file
进入命令字符串!使用 >
运算符将输出重定向到文件将始终创建该文件(如果该文件不存在)。
这非常有效,但前提是 crontab 中已经安排了作业。如果没有 cron 作业,则永远不会创建此文件!不过,使用 &&
运算符,我们可以向该字符串附加其他命令/表达式。因此,让我们附加一个条件表达式来检查 cron 文件是否存在。如果文件不存在,则该表达式的计算结果为 false。因此,如果需要,我们可以在此条件后使用 ||
或 or
运算符来创建新的空白文件。
如果条件评估为 false,则将执行位于 or
之后的命令。现在,通过再次使用 >
运算符,这次无需在其前面添加特定命令,我们可以创建一个新的空白文件。所以本质上,这串命令会将 crontab 输出到一个文件,然后检查该文件是否存在,这将表明 crontab 中有条目,然后创建一个新的空白文件(如果不存在)。
最后,我们调用了 exec()
方法并将命令字符串作为唯一的参数传递给它。然后,为了使该方法也可链接,我们将返回 $this
。
remove_file
方法
让我们快速浏览一下 remove_file
方法。
public function remove_file()
{
if ($this->crontab_file_exists()) $this->exec("rm {$this->cron_file}");
return $this;
}
登录后复制
在此方法中,我们使用辅助方法 crontab_file_exists()
来检查临时 cron 文件是否存在,然后执行 Linux 命令 rm
如果有则将其删除。像往常一样,我们还将返回 $this
以保持可链接性。
append_cronjob
方法
append_cronjob
方法通过向临时 cron 文件添加新作业/行,然后在该文件上执行 crontab
命令来创建新的 cron 作业,该命令将安装所有这些工作作为新的 crontab。
看起来像这样:
public function append_cronjob($cron_jobs=NULL)
{
if (is_null($cron_jobs)) $this->error_message("Nothing to append! Please specify a cron job or an array of cron jobs.");
$append_cronfile = "echo '";
$append_cronfile .= (is_array($cron_jobs)) ? implode("n", $cron_jobs) : $cron_jobs;
$append_cronfile .= "' >> {$this->cron_file}";
$install_cron = "crontab {$this->cron_file}";
$this->write_to_file()->exec($append_cronfile, $install_cron)->remove_file();
return $this;
}
登录后复制
append_cronjob()
方法采用一个参数 $cron_jobs
,该参数可以是一个字符串,也可以是表示要附加的 cron 作业的字符串数组。
我们将通过确定 $cron_jobs
参数是否为 NULL
来启动此方法。如果是,我们将调用 error_message()
方法来停止任何进一步的执行并向用户显示错误消息。
接下来,我们将一个新变量 $append_cronfile
定义为字符串,文本 echo
后跟一个空格,末尾有一个单引号。我们将立即用我们添加的各种 cron 作业以及结束引号填充此字符串。我们将使用字符串连接运算符 .=
构建此字符串。
接下来,使用三元运算符,我们确定 $cron_jobs 是否是数组。如果是,我们将使用换行符 n
对该数组进行内爆,以便每个 cron 作业都在 cron 文件中写入自己的行。如果 $cron_jobs
参数不是数组,我们只需将该作业连接到 $append_cron
字符串,而不进行任何特殊处理。
本质上,我们可以通过再次将标准输出重定向到文件中来将我们的任务回显到 cron 文件中。因此,通过使用字符串连接运算符,我们将在命令字符串中附加右单引号,以及 Linux 运算符 >>
,后跟 cron 文件的完整路径和文件名。 >>
运算符与始终覆盖文件的 >
运算符不同,它将输出附加到文件末尾。因此,通过使用此运算符,我们不会覆盖任何现有的 cron 作业。
接下来,我们将变量定义为字符串,并使用我们将用来安装即将创建的新 cron 文件的命令。这就像调用 crontab 命令一样简单,后跟 cron 文件的路径和文件名。
最后,在通过 exec()
方法执行这些命令之前,我们首先调用 write_to_file()
方法来创建临时 cron 文件。然后,在链中,我们将执行这些命令并调用remove_file()方法来删除临时文件。最后,我们将返回 $this
,以便 append_cronjob()
方法可链接。
remove_cronjob
方法
既然我们可以创建新的 cron 作业,那么我们也有能力删除它们,这是合乎逻辑的!这就是 remove_cronjob
方法的目的。
让我们来看看。
public function remove_cronjob($cron_jobs=NULL)
{
if (is_null($cron_jobs)) $this->error_message("Nothing to remove! Please specify a cron job or an array of cron jobs.");
$this->write_to_file();
$cron_array = file($this->cron_file, FILE_IGNORE_NEW_LINES);
if (empty($cron_array)) $this->error_message("Nothing to remove! The cronTab is already empty.");
$original_count = count($cron_array);
if (is_array($cron_jobs))
{
foreach ($cron_jobs as $cron_regex) $cron_array = preg_grep($cron_regex, $cron_array, PREG_GREP_INVERT);
}
else
{
$cron_array = preg_grep($cron_jobs, $cron_array, PREG_GREP_INVERT);
}
return ($original_count === count($cron_array)) ? $this->remove_file() : $this->remove_crontab()->append_cronjob($cron_array);
}
登录后复制
它需要一个参数,它是一个(简单的)正则表达式。它将用于在 crontab 中查找匹配的作业并相应地删除它们。
创建 cron 文件后,我们现在将使用 PHP 的 file()
函数将其读入数组。该函数将给定的文件解析为一个数组,每一行作为一个数组元素。我们将 cron 文件作为第一个参数传递给此函数,然后设置一个特殊标志 FILE_IGNORE_NEW_LINES,这将强制 file() 忽略所有新行。因此,我们有一个仅包含 cron 作业本身的数组。如果没有安排 cron 作业,该数组将为空。以后就没有继续下去的理由了。因此,我们检查了 $cron_array
是否为空,如果是则停止执行。
现在,我们确定 $cron_jobs
参数是否是一个数组。如果它是一个数组,我们使用 foreach
循环对其进行迭代。在该循环中,我们执行函数 preg_grep()
。这个漂亮的函数与 preg_match()
不同,将返回与指定的正则表达式匹配的所有数组元素的数组。然而,在这种情况下,我们想要不匹配的数组元素。换句话说,我们需要一个包含要保留的所有 cron 作业的数组,以便我们可以仅使用这些作业来初始化 crontab。因此,我们将在此处设置一个特殊标志 PREG_GREP_INVERT
,这将导致 preg_grep()
返回一个包含所有不匹配元素的数组。 正则表达式。因此,我们将拥有一个包含我们想要保留的所有 cron 作业的数组。
如果 $cron_jobs
参数不是数组,我们将以相同的方式继续,但不进行任何迭代。同样,我们将 $cron_array
重新定义为设置了 PREG_GREP_INVERT
标志的 preg_grep()
函数的结果数组。
通过我们的 $cron_array
设置,现在我们将该数组的当前长度与其原始长度进行比较,原始长度缓存在变量 $original_count
中。如果长度相同,我们将简单地返回 remove_file()
方法来删除临时 cron 文件。如果它们不匹配,我们将删除现有的 crontab,然后安装新的。
remove_crontab
方法
删除整个 crontab 相对容易实现,如以下代码片段所示。
public function remove_crontab()
{
$this->exec("crontab -r")->remove_file();
return $this;
}
登录后复制
其他辅助方法
随着 cron 管理类的编写,我们现在来看看我们在整个类中使用的两个小但有用的方法,crontab_file_exists()
和 error_message ()
.
crontab_file_exists
方法
此方法返回 PHP 的 file_exists()
方法的结果,true
或 false
,具体取决于临时 cron 文件是否存在.
private function crontab_file_exists()
{
return file_exists($this->cron_file);
}
登录后复制
error_message
方法
此方法采用单个参数(一个字符串),表示我们要显示的错误消息。然后,我们将调用 PHP 的 die()
方法来停止执行并显示此消息。字符串本身将连接到
元素中,并应用简单的样式。
private function error_message($error)
{
die("
ERROR: {$error}
");
}
登录后复制
完整的类如下所示: