Source of file Cron.php

Size: 16,781 Bytes - Last Modified: 2020-10-24T02:46:31+00:00

/home/travis/build/NextDom/nextdom-core/src/Model/Entity/Cron.php

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
<?php

/* This file is part of NextDom Software.
 *
 * NextDom is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * NextDom Software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NextDom Software. If not, see <http://www.gnu.org/licenses/>.
 */

namespace NextDom\Model\Entity;

use NextDom\Enums\DateFormat;
use NextDom\Enums\LogTarget;
use NextDom\Enums\NextDomObj;
use NextDom\Exceptions\CoreException;
use NextDom\Helpers\DBHelper;
use NextDom\Helpers\LogHelper;
use NextDom\Helpers\SystemHelper;
use NextDom\Helpers\Utils;
use NextDom\Managers\CacheManager;
use NextDom\Managers\ConfigManager;
use NextDom\Managers\CronManager;
use NextDom\Model\Entity\Parents\BaseEntity;
use NextDom\Model\Entity\Parents\EnableEntity;

/**
 * Cron
 *
 * @ORM\Table(name="cron", uniqueConstraints={@ORM\UniqueConstraint(name="class_function_option", columns={"class", "function", "option"})}, indexes={@ORM\Index(name="type", columns={"class"}), @ORM\Index(name="logicalId_Type", columns={"class"}), @ORM\Index(name="deamon", columns={"deamon"})})
 * @ORM\Entity
 */
class Cron extends BaseEntity
{
    const TABLE_NAME = NextDomObj::CRON;

    use EnableEntity;

    /**
     * @var string
     *
     * @ORM\Column(name="class", type="string", length=127, nullable=true)
     */
    protected $class = '';

    /**
     * @var string
     *
     * @ORM\Column(name="function", type="string", length=127, nullable=false)
     */
    protected $function;

    /**
     * @var string
     *
     * @ORM\Column(name="schedule", type="string", length=127, nullable=true)
     */
    protected $schedule = '';

    /**
     * @var integer
     *
     * @ORM\Column(name="timeout", type="integer", nullable=true)
     */
    protected $timeout;

    /**
     * @var integer
     *
     * @ORM\Column(name="deamon", type="integer", nullable=true)
     */
    protected $deamon = 0;

    /**
     * @var integer
     *
     * @ORM\Column(name="deamonSleepTime", type="integer", nullable=true)
     */
    protected $deamonSleepTime;

    /**
     * @var string
     *
     * @ORM\Column(name="option", type="string", length=255, nullable=true)
     */
    protected $option;

    /**
     * @var integer
     *
     * @ORM\Column(name="once", type="integer", nullable=true)
     */
    protected $once = 0;

    /**
     * Get enabled state of the cron task
     *
     * @param int $defaultValue Default value if cron task is not initialized
     *
     * @return int 1 for enabled task, 0 for disabled, or defaultValue
     */
    public function getEnable($defaultValue = 0)
    {
        if ($this->enable == '' || !is_numeric($this->enable)) {
            return $defaultValue;
        }
        return $this->enable;
    }

    /**
     * Get timeout of the task
     * If timeout is not configured, return default value from
     *
     * @return int Timeout
     *
     * @throws \Exception
     */
    public function getTimeout()
    {
        $timeout = $this->timeout;
        if ($timeout == 0) {
            $timeout = ConfigManager::byKey('maxExecTimeCrontask');
        }
        return $timeout;
    }

    /**
     *
     * @param $_timeout
     * @return $this
     */
    public function setTimeout($_timeout)
    {
        $this->updateChangeState($this->timeout, $_timeout);
        $this->timeout = $_timeout;
        return $this;
    }

    /**
     * @return int
     */
    public function getDeamon()
    {
        return $this->deamon;
    }

    /**
     *
     * @param $_deamons
     * @return $this
     */
    public function setDeamon($_deamons)
    {
        $this->updateChangeState($this->deamon, $_deamons);
        $this->deamon = $_deamons;
        return $this;
    }

    /**
     * @return int|mixed
     * @throws \Exception
     */
    public function getDeamonSleepTime()
    {
        $deamonSleepTime = $this->deamonSleepTime;
        if ($deamonSleepTime == 0) {
            $deamonSleepTime = ConfigManager::byKey('deamonsSleepTime');
        }
        return $deamonSleepTime;
    }

    /**
     *
     * @param $_deamonSleepTime
     * @return $this
     */
    public function setDeamonSleepTime($_deamonSleepTime)
    {
        $this->updateChangeState($this->deamonSleepTime, $_deamonSleepTime);
        $this->deamonSleepTime = $_deamonSleepTime;
        return $this;
    }

    /**
     * @param int $defaultValue
     * @return int
     */
    public function getOnce($defaultValue = 0)
    {
        if ($this->once == '' || !is_numeric($this->once)) {
            return $defaultValue;
        }
        return $this->once;
    }

    /**
     *
     * @param $_once
     * @return $this
     */
    public function setOnce($_once)
    {
        $this->updateChangeState($this->once, $_once);
        $this->once = $_once;
        return $this;
    }

    /**
     * Check if cron object is valid before save
     * @throws CoreException
     */
    public function preSave()
    {
        if ($this->getFunction() == '') {
            throw new CoreException(__('La fonction ne peut pas être vide'));
        }
        if ($this->getSchedule() == '') {
            throw new CoreException(__('La programmation ne peut pas être vide : ') . print_r($this, true));
        }
        if ($this->getOption() == '' || count($this->getOption()) == 0) {
            $cron = CronManager::byClassAndFunction($this->getClass(), $this->getFunction());
            if (is_object($cron)) {
                $this->setId($cron->getId());
            }
        }
    }

    /**
     * @return string
     */
    public function getFunction()
    {
        return $this->function;
    }

    /**
     *
     * @param $_function
     * @return $this
     */
    public function setFunction($_function)
    {
        $this->updateChangeState($this->function, $_function);
        $this->function = $_function;
        return $this;
    }

    /**
     * @return string
     */
    public function getSchedule()
    {
        return $this->schedule;
    }

    /**
     *
     * @param $_schedule
     * @return $this
     */
    public function setSchedule($_schedule)
    {
        $this->updateChangeState($this->schedule, $_schedule);
        $this->schedule = $_schedule;
        return $this;
    }

    /**
     * @return mixed
     */
    public function getOption()
    {
        return json_decode($this->option, true);
    }

    /**
     *
     * @param $_option
     * @return $this
     */
    public function setOption($_option)
    {
        $_option = json_encode($_option, JSON_UNESCAPED_UNICODE);
        $this->updateChangeState($this->option, $_option);
        $this->option = $_option;
        return $this;
    }

    /**
     * @return string
     */
    public function getClass()
    {
        return $this->class;
    }

    /**
     *
     * @param $newClass
     * @return $this
     */
    public function setClass($newClass)
    {
        $this->updateChangeState($this->class, $newClass);
        $this->class = $newClass;
        return $this;
    }

    /**
     * Stop task after insert in database
     */
    public function postInsert()
    {
        $this->setState('stop');
        $this->setPID();
    }

    /**
     * Set task state
     *
     * @param mixed $state State of the task
     * @throws \Exception
     */
    public function setState($state)
    {
        $this->setCache('state', $state);
    }

    /**
     * Store task data in cache
     *
     * @param mixed $cacheKey
     * @param mixed $cacheValue
     * @throws \Exception
     */
    public function setCache($cacheKey, $cacheValue = null)
    {
        CacheManager::set('cronCacheAttr' . $this->getId(), Utils::setJsonAttr(CacheManager::byKey('cronCacheAttr' . $this->getId())->getValue(), $cacheKey, $cacheValue));
    }

    /**
     * Store PID in cache
     *
     * @param mixed $pid
     * @throws \Exception
     */
    public function setPID($pid = null)
    {
        $this->setCache('pid', $pid);
    }

    /**
     * Save cron object in database
     *
     * @return mixed
     * @throws CoreException
     * @throws \ReflectionException
     */
    public function save()
    {
        return DBHelper::save($this, false, true);
    }

    /**
     * Remove cron object from the database
     *
     * @param bool $haltBefore
     * @return mixed
     * @throws CoreException
     * @throws \ReflectionException
     */
    public function remove($haltBefore = true)
    {
        if ($haltBefore && $this->running()) {
            $this->halt();
        }
        CacheManager::delete('cronCacheAttr' . $this->getId());
        return parent::remove();
    }

    /**
     * Check if this cron is currently running
     *
     * @return boolean
     * @throws \Exception
     */
    public function running(): bool
    {
        if (($this->getState() == 'run' || $this->getState() == 'stoping') && $this->getPID() > 0) {
            if (posix_getsid($this->getPID()) && (!file_exists('/proc/' . $this->getPID() . '/cmdline') || strpos(@file_get_contents('/proc/' . $this->getPID() . '/cmdline'), 'cron_id=' . $this->getId()) !== false)) {
                return true;
            }
        }
        if (count(SystemHelper::ps('cron_id=' . $this->getId() . '$')) > 0) {
            return true;
        }
        return false;
    }

    /**
     * Get current state
     *
     * @return mixed Current state
     * @throws \Exception
     */
    public function getState()
    {
        return $this->getCache('state', 'stop');
    }

    /**
     * Get task data in cache
     *
     * @param string $cacheKey
     * @param string $cacheValue
     * @return mixed
     * @throws \Exception
     */
    public function getCache($cacheKey = '', $cacheValue = '')
    {
        $cache = CacheManager::byKey('cronCacheAttr' . $this->getId())->getValue();
        return Utils::getJsonAttr($cache, $cacheKey, $cacheValue);
    }

    /**
     * Get task PID
     *
     * @param mixed $defaultValue
     *
     * @return mixed Task PID
     * @throws \Exception
     */
    public function getPID($defaultValue = null)
    {
        return $this->getCache('pid', $defaultValue);
    }

    /**
     * Stop immediatly cron (this method must be only call by jeecron master)
     */
    public function halt()
    {
        if (!$this->running()) {
            $this->setState('stop');
            $this->setPID();
        } else {
            LogHelper::addInfo(LogTarget::CRON, __('Arrêt de ') . $this->getClass() . '::' . $this->getFunction() . '(), PID : ' . $this->getPID());
            if ($this->getPID() > 0) {
                SystemHelper::kill($this->getPID());
                $retry = 0;
                while ($this->running() && $retry < (ConfigManager::byKey('deamonsSleepTime') + 5)) {
                    sleep(1);
                    SystemHelper::kill($this->getPID());
                    $retry++;
                }
                $retry = 0;
                while ($this->running() && $retry < (ConfigManager::byKey('deamonsSleepTime') + 5)) {
                    sleep(1);
                    SystemHelper::kill($this->getPID());
                    $retry++;
                }
            }
            if ($this->running()) {
                SystemHelper::kill("cron_id=" . $this->getId() . "$");
                sleep(1);
                if ($this->running()) {
                    SystemHelper::kill("cron_id=" . $this->getId() . "$");
                    sleep(1);
                }
                if ($this->running()) {
                    $this->setState('error');
                    $this->setPID();
                    throw new CoreException($this->getClass() . '::' . $this->getFunction() . __('() : Impossible d\'arrêter la tâche'));
                }
            } else {
                $this->setState('stop');
                $this->setPID();
            }
        }
        return true;
    }

    /**
     * Start cron task
     */
    public function start()
    {
        if (!$this->running()) {
            $this->setState('starting');
        } else {
            $this->setState('run');
        }
    }

    /**
     * Launch cron (this method must be only call by jeeCron master)
     *
     * @param bool $noErrorReport
     * @throws CoreException
     */
    public function run($noErrorReport = false)
    {
        $cmd = NEXTDOM_ROOT . '/src/Api/start_cron.php';
        $cmd .= ' "cron_id=' . $this->getId() . '"';
        if (!$this->running()) {
            SystemHelper::php($cmd . ' >> ' . LogHelper::getPathToLog('cron_execution') . ' 2>&1 &');
        } else {
            if (!$noErrorReport) {
                $this->halt();
                if (!$this->running()) {
                    exec($cmd . ' >> ' . LogHelper::getPathToLog('cron_execution') . ' 2>&1 &');
                } else {
                    throw new CoreException(__('Impossible d\'exécuter la tâche car elle est déjà en cours d\'exécution (') . ' : ' . $cmd);
                }
            }
        }
    }

    /**
     * Refresh DB state of this cron
     *
     * @return boolean
     * @throws \Exception
     */
    public function refresh(): bool
    {
        if (($this->getState() == 'run' || $this->getState() == 'stoping') && !$this->running()) {
            $this->setState('stop');
            $this->setPID();
        }
        return true;
    }

    /**
     * Stop task
     */
    public function stop()
    {
        if ($this->running()) {
            $this->setState('stoping');
        }
    }

    /**
     * Check if it's time to launch cron
     *
     * @return boolean
     * @throws \Exception
     */
    public function isDue(): bool
    {
        //check if already sent on that minute
        $last = strtotime($this->getLastRun());
        $now = time();
        $now = ($now - $now % 60);
        $last = ($last - $last % 60);
        if ($now == $last) {
            return false;
        }
        try {
            $c = new \Cron\CronExpression($this->getSchedule(), new \Cron\FieldFactory);
            try {
                if ($c->isDue()) {
                    return true;
                }
            } catch (\Exception $e) {

            }
            try {
                $prev = $c->getPreviousRunDate()->getTimestamp();
            } catch (\Exception $e) {
                return false;
            }
            $diff = abs((strtotime('now') - $prev) / 60);
            if (strtotime($this->getLastRun()) < $prev && ($diff <= ConfigManager::byKey('maxCatchAllow') || ConfigManager::byKey('maxCatchAllow') == -1)) {
                return true;
            }
        } catch (\Exception $e) {
            LogHelper::addDebug(LogTarget::CRON, 'Error on isDue : ' . $e->getMessage() . ', cron : ' . $this->getSchedule());
        }
        return false;
    }

    /**
     * Get last task run
     *
     * @return mixed Last task run
     * @throws \Exception
     */
    public function getLastRun()
    {
        return $this->getCache('lastRun');
    }

    /**
     * Get date of the next task run
     *
     * @return bool|string
     */
    public function getNextRunDate()
    {
        try {
            $cronExpression = new \Cron\CronExpression($this->getSchedule(), new \Cron\FieldFactory);
            return $cronExpression->getNextRunDate()->format(DateFormat::FULL);
        } catch (\Exception $e) {

        }
        return false;
    }

    /**
     * Get human name of cron
     * @return string
     */
    public function getName()
    {
        if ($this->getClass() != '') {
            return $this->getClass() . '::' . $this->getFunction() . '()';
        }
        return $this->getFunction() . '()';
    }

    /**
     * Get cron data in array
     *
     * @return array Cron data
     * @throws \Exception
     */
    public function toArray()
    {
        $return = Utils::o2a($this, true);
        $return['state'] = $this->getState();
        $return['lastRun'] = $this->getLastRun();
        $return['pid'] = $this->getPID();
        $return['runtime'] = $this->getCache('runtime');
        return $return;
    }

    /**
     * Set last task run
     *
     * @param mixed $lastRun Last task run
     * @throws \Exception
     */
    public function setLastRun($lastRun)
    {
        $this->setCache('lastRun', $lastRun);
    }
}