Source of file Crontab.php
Size: 12,054 Bytes - Last Modified: 2021-12-23T10:11:42+00:00
/var/www/docs.ssmods.com/process/src/code/cronkeep/src/application/models/Crontab.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 | <?php /** * Copyright 2014 Bogdan Ghervan * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace models; use \Symfony\Component\Process\Process; use models\Crontab\Job; use models\Crontab\Exception; /** * Crontab model. * * @author Bogdan Ghervan <bogdan.ghervan@gmail.com> * @copyright 2014 Bogdan Ghervan * @link http://github.com/cronkeep/cronkeep * @license http://opensource.org/licenses/Apache-2.0 Apache License 2.0 * @see man 5 crontab for details */ class Crontab implements \IteratorAggregate, \Countable { /** * Possible errors printed by "crontab". */ const ERROR_EMPTY = "/no crontab for .+/"; const ERROR_SPOOL_UNREACHABLE = "/'\/var\/spool\/cron' is not a directory, bailing out/"; const ERROR_PAM_UNREADABLE = "/You \([\w]+\) are not allowed to access to \(crontab\) because of pam configuration/"; /** * Raw cron table. * * @var string */ protected $_rawTable; /** * Loaded list of cron jobs. * * @var array */ protected $_jobs = array(); /** * Default line separator. * * @var string */ protected $_lineSeparator = PHP_EOL; /** * Class constructor. * * @return void */ public function __construct() { $this->_load(); } /** * Finds job by hash. Returns null if the job couldn't be found. * * @param string $hash * @return Crontab\Job|null */ public function findByHash($hash) { foreach ($this->_jobs as $job) { /* @var $job Crontab\Job */ if ($job->getHash() == $hash) { return $job; } } return null; } /** * Runs the job in background. * @see http://symfony.com/doc/current/components/process.html * * @param Job $job * @return Crontab */ public function run(Job $job) { $command = $job->getCommand(); // remove comment if job is commented out/inactive in cron // if(substr($command, 0, 1)=="#") $command = substr($command,1); // $command = sprintf("sh -c \"%s\" > %s 2>&1 & echo $! >> %s", $job->getCommand(), '/dev/null', '/dev/null'); $command_in_shell = sprintf("sh -c \"%s\"", str_replace("\"", "\\\"", $command)); $command_as_background = sprintf("%s > /dev/null 2>&1 &", $command_in_shell); $process = new Process($command_as_background); $process->run(); return $this; } /** * Adds given cron job to crontab. * Additionally, {@link Crontab::save} should be called to persist the change. * * @param Job $job * @return Crontab */ public function add(Job $job) { // Trim any trailing newlines at the end of the raw cron table $this->_rawTable = rtrim($this->_rawTable, "\r\n"); $this->_rawTable .= $this->_lineSeparator . $job->getRaw(); return $this; } /** * Updates original job definition in the raw crontab with changed data. * * @param Job $job * @return Crontab */ public function update(Job $job) { $originalJob = $job->getOriginalRaw(); $newJob = $job->getRaw(); // Replace the job definition in the raw crontab $this->_rawTable = str_replace($originalJob, $newJob, $this->_rawTable); return $this; } /** * Pauses cron schedule by commenting the job in crontab. * Additionally, {@link Crontab::save} should be called to persist the change. * * @param Job $job * @return Crontab */ public function pause(Job $job) { // Comment the cron job $job->setIsPaused(true); $this->update($job); return $this; } /** * Resumes cron schedule by un-commenting the job in crontab. * Additionally, {@link Crontab::save} should be called to persist the change. * * @param Job $job * @return Crontab */ public function resume(Job $job) { // Uncomment the cron job $job->setIsPaused(false); $this->update($job); return $this; } /** * Deletes given job from crontab. * Additionally, {@link Crontab::save} should be called to persist the change. * * @param Job $job * @return Crontab */ public function delete(Job $job) { $this->_rawTable = str_replace($job->getOriginalRaw(), '', $this->_rawTable); return $this; } /** * Saves the current user's crontab. * * @return Crontab * @throws \RuntimeException */ public function save() { $process = new Process('crontab'); $process->setInput($this->_rawTable); $process->run(); if (!$process->isSuccessful()) { $errorOutput = $process->getErrorOutput(); if ($errorOutput) { throw new \RuntimeException(sprintf( 'There has been an error saving the crontab. Here\'s the output from the shell: %s', trim($errorOutput))); } throw new \RuntimeException('There has been an error saving the crontab.'); } return $this; } /** * Loads system crontab. * * @return Crontab */ protected function _load() { $this->_readCrontab(); $this->_parseCrontab(); return $this; } /** * Reads crontab for current user. * * @return Crontab * @throws \Exception */ protected function _readCrontab() { $process = new Process('crontab -l'); $process->run(); if (!$process->isSuccessful()) { $errorOutput = $process->getErrorOutput(); $this->_handleReadError($errorOutput); } $this->_rawTable = $process->getOutput(); return $this; } /** * Parses a crontab with commentaries. It assumes the comment for a cron job * comes before the job definition. * * @return Crontab */ protected function _parseCrontab() { $pattern = "/ (?(DEFINE) # Subroutine matching the (optional) comment sign preceding a cron definition (?<_commentSign>(?:\#[ \t]*)) # Subroutine matching the time expression (?<_expression> # either match expression (?: (?: [\d\*\/\,\-\%]+ # minute part ) [ \t]+ # space (?: [\d\*\/\,\-\%]+ # hour part ) [ \t]+ # space (?: [\d\*\/\,\-\%]+ # day of month part ) [ \t]+ # space (?: [\d\*\/\,\-\%]+|jan|feb| # month part mar|apr|may|jun|jul|aug| sep|oct|nov|dec ) [ \t]+ # space (?: [\d\*\/\,\-\%]+|mon|tue| # day of week part wed|thu|fri|sat|sun ) ) # or match specials | (?: @hourly|@midnight|@daily| @weekly|@monthly|@yearly| @annually|@reboot ) ) # Subroutine matching the command part (everything except line ending) (?<_command>[^\r\n]*) # Subroutine matching full cron definition (time + command) (?<_cronDefinition> ^(?&_commentSign)? # comment sign (optional) (?&_expression) # time expression part \s+ # space (?&_command) # command part ) # Subroutine matching comment (which is above cron, by convention) (?<_comment>[^\r\n]*) ) # Here's where the actual matching happens. # Subroutine calls are wrapped by named capture groups so we could # easily reference the captured subpatterns later. # A comment isn't allowed to look like a cron definition, or otherwise # commented crons could pass as comments for neighboring crons ^(?(?!(?&_cronDefinition)) # conditional: not a cron definition (?: (?&_commentSign) # comment sign preceding comment (?P<comment>(?&_comment)) # comment \s* # trailing whitespace, if any [\r\n]{1,} # line endings )? # comment is, however, optional ) (?P<cronCommentSign>^(?&_commentSign)?) # comment sign for 'paused' crons (optional) (?P<expression>(?&_expression)) # time expression \s+ # space (?P<command>(?&_command)) # command to be run (everything, but line ending) \s*[\r\n]{1,} # trailing space and line ending /imx"; $this->_jobs = array(); if (preg_match_all($pattern, $this->_rawTable, $lines, PREG_SET_ORDER)) { foreach ($lines as $lineParts) { $job = new Job(); $job->setRaw($lineParts[0]); $job->setComment(empty($lineParts['comment']) ? null : $lineParts['comment']); $job->setIsPaused($lineParts['cronCommentSign'] != ''); $job->setExpression($lineParts['expression']); $job->setCommand($lineParts['command']); $this->_jobs[] = $job; } } return $this; } /** * Handles errors encountered while attempting to read the crontab. * * @param string $errorOutput * @return Crontab * @throws \RuntimeException */ protected function _handleReadError($errorOutput) { $errorOutput = trim($errorOutput); // Unknown error condition if (empty($errorOutput)) { throw new \RuntimeException('There has been an error reading the crontab.'); } // Do nothing if crontab is empty if (preg_match(self::ERROR_EMPTY, $errorOutput)) { return $this; } if (preg_match(self::ERROR_SPOOL_UNREACHABLE, $errorOutput)) { throw new Exception\SpoolUnreachableException($errorOutput); } if (preg_match(self::ERROR_PAM_UNREADABLE, $errorOutput)) { throw new Exception\PamUnreadableException($errorOutput); } // Unrecognized error condition throw new \RuntimeException(sprintf( 'There has been an error reading the crontab. Here\'s the output from the shell: %s', $errorOutput)); } /** * Implementation of Countable::count. * * @return int */ public function count() { return count($this->_jobs); } /** * Implementation of IteratorAggregate::getIterator. * * @return \ArrayIterator */ public function getIterator() { return new \ArrayIterator($this->_jobs); } } |