├── .gitignore ├── etc └── config-example.php ├── composer.json ├── LICENSE ├── src ├── Phpcron │ ├── Adapter.php │ ├── Adapter │ │ └── Pdo.php │ ├── Utils.php │ └── Phpcron.php └── phpcron.sql ├── bin └── phpcron.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea/ 3 | composer.lock 4 | /etc/config.php 5 | -------------------------------------------------------------------------------- /etc/config-example.php: -------------------------------------------------------------------------------- 1 | 'pdo', 12 | 'dsn' => 'mysql:host=localhost;dbname=phpcron', 13 | 'username' => 'root', 14 | 'password' => '', 15 | 'options' => array(), 16 | 'table' => 'crontab', 17 | 'log_table' => 'crontab_log' 18 | ); 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpcron/phpcron", 3 | "description": "Development by PHP. Solve two problem: a. Crontable machine halted ; b. Crontable log collect.", 4 | "require": { 5 | "php": ">=5.4.0", 6 | "pagon/childprocess": "0.0.5", 7 | "mtdowling/cron-expression": "1.0.*" 8 | }, 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "bobchengbin", 13 | "email": "bob@phpor.me" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-0": { 18 | "Phpcron\\": "src/" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 BOB CHENGBIN 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Phpcron/Adapter.php: -------------------------------------------------------------------------------- 1 | options = $options + $this->options; 19 | } 20 | 21 | public function getNeedsExecTasks(){ 22 | $this->tasks = array(); 23 | 24 | $rows = $this->fetch(); 25 | 26 | foreach($rows as $row){ 27 | if($row->online_time==NULL || strtotime($row->online_time)<=time()){ 28 | if($row->offline_time==NULL || strtotime($row->offline_time)>=time()){ 29 | 30 | $cron = \Cron\CronExpression::factory($row->exec_time); 31 | if($cron->isDue()){ 32 | $this->tasks[$row->id] = $row; 33 | } 34 | } 35 | } 36 | } 37 | } 38 | /** 39 | * 获取计划任务列表 40 | * 41 | * @return array 42 | */ 43 | abstract public function fetch(); 44 | 45 | abstract public function checkCurrentMinuteHasRun(); 46 | } 47 | -------------------------------------------------------------------------------- /bin/phpcron.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | on('run', function () use($phpcron) { 11 | Phpcron\Utils::log(array( 12 | 'cron_name'=>$phpcron->role[ROLE].'计划任务服务启动', 13 | 'crontab_id'=>-1, 14 | )); 15 | }); 16 | 17 | $phpcron->on('getNeedsExecTasks', function () use ($phpcron){ 18 | Phpcron\Utils::log(array( 19 | 'cron_name'=>$phpcron->role[ROLE].'获取当前需要执行的计划任务', 20 | 'crontab_id'=>-2, 21 | )); 22 | }); 23 | 24 | $phpcron->on('execute', function ($task) use($phpcron) { 25 | 26 | Phpcron\Utils::log(array( 27 | 'cron_name'=>$phpcron->role[ROLE].$task->cron_name, 28 | 'crontab_id'=> $task->id, 29 | 'status'=>'-1', 30 | 'stdout'=>'开始执行' 31 | )); 32 | }); 33 | 34 | $phpcron->on('executed', function ($task, $output) { 35 | Phpcron\Utils::log(array( 36 | 'cron_name'=>$task->cron_name, 37 | 'crontab_id'=>$task->id, 38 | 'status'=>$output[0], 39 | 'stdout'=>$output[1], 40 | 'stderr'=>$output[2], 41 | )); 42 | }); 43 | 44 | $phpcron->run(); -------------------------------------------------------------------------------- /src/phpcron.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `crontab` ( 2 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 3 | `command` varchar(255) NOT NULL COMMENT '需要执行的命令', 4 | `exec_time` varchar(64) NOT NULL COMMENT '[分] [时] [日] [月] [年] 执行的计划任务周期', 5 | `online_time` datetime DEFAULT NULL COMMENT '该计划任务从何时开始允许被执行', 6 | `offline_time` datetime DEFAULT NULL COMMENT '该计划任务截止何时不再执行', 7 | `cron_name` varchar(255) NOT NULL COMMENT '给该计划任务取个名字吧', 8 | `note` text COMMENT '备注,关于计划任务更多的说明信息', 9 | PRIMARY KEY (`id`) 10 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='计划任务列表'; 11 | 12 | CREATE TABLE `crontab_log` ( 13 | `id` int(11) NOT NULL AUTO_INCREMENT, 14 | `crontab_id` int(11) NOT NULL, 15 | `hostname` varchar(255) DEFAULT NULL COMMENT '执行该计划任务的主机名', 16 | `cron_name` varchar(255) DEFAULT NULL COMMENT '计划任务执行的命令', 17 | `status` varchar(255) DEFAULT NULL COMMENT '状态 执行时的退出状态', 18 | `stdout` text COMMENT '执行后的正常输出', 19 | `stderr` varchar(255) DEFAULT NULL COMMENT '执行完成后的错误输出', 20 | `create_time` datetime NOT NULL COMMENT '插入的时间', 21 | PRIMARY KEY (`id`) 22 | ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='计划任务执行完之后的输出记录表'; 23 | 24 | 25 | INSERT INTO `crontab` (`id`, `command`, `exec_time`, `online_time`, `offline_time`, `cron_name`, `note`) VALUES 26 | (1, 'ls /', '* * * * * *', NULL, NULL, '列出根目录下的所有文件', '测试。。。'); 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | phpcron 2 | ======= 3 | 4 | PHP版计划任务,本程序是Croon的扩展功能延伸。 5 | 6 | ## 功能 7 | - 在两台机器上运行该程序来防止一台机器宕机之后产生的严重后果 8 | - 可以规定该计划任务的上线时间和下线时间 9 | - 计划任务的时间里面可以指定年 10 | - 将执行结果、标准正确输出、标准错误输出统一到数据库,方便检索 11 | 12 | ## 依赖 13 | 14 | - PHP 5.4.0+ 15 | - ext-pcntl 16 | - ext-posix 17 | - [Composer](http://getcomposer.org) 18 | 19 | 库依赖(使用`composer install`自动安装) 20 | - [pagon/childprocess](https://github.com/hfcorriez/php-childprocess) 21 | - [mtdowling/cron-expression](https://github.com/mtdowling/cron-expression) 22 | 23 | ## 安装 24 | ``` 请保证两台服务器上所有的代码一致,包括配置文件,最好采用共享存储 ``` 25 | 代码克隆及依赖的安装 26 | ``` 27 | git clone https://github.com/dcb9/phpcron.git 28 | cd phpcron 29 | composer install 30 | ``` 31 | 配置相应的数据表 32 | ``` 33 | $ cd src // 进入到src目录 34 | 35 | ## 创建一个phpcron库,并创建相应的存储表 36 | mysql> CREATE DATABASE `phpcron`; 37 | mysql> use `phpcron`; 38 | mysql> source phpcron.sql; 39 | ``` 40 | 修改配置文件 41 | ``` 42 | $ cp etc/config-example.php etc/config.php 43 | $ vim etc/config.php 44 | 修改 host dbname username 及 password 45 | ``` 46 | 47 | ## 启动与停止 48 | 主机 角色 49 | server1 主 50 | server2 备主(主要是该执行的时候不执行,它就顶上去) 51 | ``` 52 | 启动 53 | server1 $ nohup bin/phpcron.php & 54 | server2 $ nohup bin/phpcron.php --backend & 55 | 停止 56 | $ ps -ef | grep phpcron 57 | 进程ID 58 | 501 36270 31711 0 12:58上午 ttys000 0:00.12 php bin/phpcron.php 59 | $ kill 进程ID 60 | ``` 61 | 62 | ## 添加和修改计划任务 63 | 计划任务列表信息全部存储在数据库,所以添加或修改计划任务直接用程序进程修改即可。 64 | 65 | ## 日志 66 | 日志见 ```crontab_log``` 表 67 | -------------------------------------------------------------------------------- /src/Phpcron/Adapter/Pdo.php: -------------------------------------------------------------------------------- 1 | options['table']); 20 | $query = $this->pdo->query($sql); 21 | return $query->fetchAll(\PDO::FETCH_OBJ); 22 | 23 | } 24 | 25 | public function __construct(array $options = array()){ 26 | parent::__construct($options); 27 | $this->connect(); 28 | } 29 | 30 | protected function connect() 31 | { 32 | $this->pdo = new \PDO( 33 | $this->options['dsn'], 34 | $this->options['username'], 35 | $this->options['password'], 36 | $this->options['options'] 37 | ); 38 | $this->pdo->query("SET NAMES utf8"); 39 | } 40 | 41 | public function log(array $message = array()){ 42 | 43 | $fields = array( 44 | 'crontab_id'=>'0', 45 | 'hostname'=> Utils::hostname(), 46 | 'cron_name'=>'', 47 | 'status'=>'0', 48 | 'stdout'=>'', 49 | 'stderr'=>'', 50 | 'create_time'=>date('Y-m-d H:i:s'), 51 | ); 52 | $message = $message + $fields; 53 | 54 | $keys = implode('`, `', array_keys($message)); 55 | $values = trim(str_pad("", 3*count($message), ", ?"), ' ,'); 56 | 57 | try { 58 | if(FALSE===$this->pdo->query('SELECT 1')){ 59 | self::connect(); 60 | } 61 | } catch (PDOException $e) { 62 | self::connect(); 63 | } 64 | 65 | $sql = sprintf("/* file:%s line: %d */INSERT INTO `%s` (`%s`) values (%s)" 66 | , __FILE__, __LINE__, $this->options['log_table'], $keys, $values); 67 | 68 | $sth = $this->pdo->prepare($sql); 69 | 70 | $sth->execute(array_values($message)); 71 | } 72 | 73 | 74 | public function __destruct() 75 | { 76 | $this->pdo = null; 77 | } 78 | protected function prepareSql(){ 79 | 80 | } 81 | 82 | public function checkCurrentMinuteHasRun(){ 83 | $sql = sprintf("/* file:%s line: %d */SELECT 1 FROM `%s` WHERE `crontab_id`='%s' AND `create_time`>='%s'" 84 | , __FILE__, __LINE__, $this->options['log_table'], '-2', date('Y-m-d H:i:00')); 85 | $query = $this->pdo->query($sql); 86 | return (boolean)$query->fetch(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Phpcron/Utils.php: -------------------------------------------------------------------------------- 1 | log($message); 25 | $logger=null; 26 | } 27 | 28 | public static function hostname(){ 29 | if(is_null(self::$_hostname)){ 30 | self::$_hostname = exec('hostname'); 31 | if(TRIAL){ 32 | self::$_hostname .= '_'.self::currentToken(); 33 | } 34 | } 35 | return self::$_hostname; 36 | } 37 | 38 | public static function currentToken(){ 39 | if(is_null(self::$_current_token)){ 40 | self::$_current_token = time().rand(111, 999); 41 | } 42 | return self::$_current_token; 43 | } 44 | 45 | public static function setRole($argv){ 46 | 47 | $role = Phpcron::ROLE_MASTER; 48 | if(isset($argv[1])){ 49 | $argv1 = $argv[1]; 50 | if($argv1 =='--backend') 51 | $role = Phpcron::ROLE_BACKEND; 52 | } 53 | 54 | define('ROLE', $role); 55 | } 56 | 57 | /** 58 | * 59 | * Exec the command and return code 60 | * 61 | * @param string $cmd 62 | * @param string $stdout 63 | * @param string $stderr 64 | * @param int $timeout 65 | * @return int|null 66 | */ 67 | public static function exec($cmd, &$stdout, &$stderr, $timeout = 3600) 68 | { 69 | if ($timeout <= 0) $timeout = 3600; 70 | $descriptors = array 71 | ( 72 | 1 => array("pipe", "w"), 73 | 2 => array("pipe", "w") 74 | ); 75 | $stdout = $stderr = $status = null; 76 | $process = proc_open($cmd, $descriptors, $pipes); 77 | $time_end = time() + $timeout; 78 | if (is_resource($process)) { 79 | do { 80 | $time_left = $time_end - time(); 81 | $read = array($pipes[1]); 82 | stream_select($read, $null, $null, $time_left, NULL); 83 | $stdout .= fread($pipes[1], 2048); 84 | } while (!feof($pipes[1]) && $time_left > 0); 85 | fclose($pipes[1]); 86 | if ($time_left <= 0) { 87 | proc_terminate($process); 88 | $stderr = 'process terminated for timeout.'; 89 | return -1; 90 | } 91 | while (!feof($pipes[2])) { 92 | $stderr .= fread($pipes[2], 2048); 93 | } 94 | fclose($pipes[2]); 95 | $status = proc_close($process); 96 | } 97 | return $status; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Phpcron/Phpcron.php: -------------------------------------------------------------------------------- 1 | 'pdo', 19 | ); 20 | public $role = array( 21 | self::ROLE_MASTER=>'主', 22 | self::ROLE_BACKEND=>'备主', 23 | ); 24 | 25 | /** 26 | * @param array $options 27 | * @throws \InvalidArgumentException 28 | */ 29 | public function __construct(array $options = array()) 30 | { 31 | $this->options = $options + $this->options; 32 | 33 | if (empty($this->options['engine'])) { 34 | throw new \UnexpectedValueException('Config "engine" not correct'); 35 | } 36 | 37 | $this->start_time = time(); 38 | $this->process = new ChildProcess(); 39 | } 40 | 41 | /** 42 | * Start to run 43 | */ 44 | public function run() 45 | { 46 | if ($this->_is_run) { 47 | throw new \RuntimeException("Already running!"); 48 | } 49 | 50 | $this->_is_run = true; 51 | $this->emit('run'); 52 | 53 | $engine = ucfirst($this->options['engine']); 54 | 55 | if (!class_exists($try_engine = __NAMESPACE__ . "\\Adapter\\" . $engine) 56 | && !class_exists($try_engine = $engine) 57 | ) { 58 | throw new \RuntimeException('Unknown adapter engine of "' . $try_engine . '"'); 59 | } 60 | 61 | 62 | while (true) { 63 | 64 | $current_time = mktime(date('H'), date('i'), 0); 65 | 66 | $source = new $try_engine($this->options); 67 | if(ROLE===self::ROLE_BACKEND){ 68 | $spread = 25-date('s'); 69 | if($spread>0){ 70 | sleep($spread); 71 | } 72 | } 73 | // 如果当前分钟已经在执行了,则本次休息一下,直接进入继续下一次循环 74 | if($source->checkCurrentMinuteHasRun()){ 75 | $sleep = 60 - date('s'); 76 | sleep($sleep); 77 | continue; 78 | } 79 | 80 | // Load tasks 81 | $source->getNeedsExecTasks(); 82 | $this->emit('getNeedsExecTasks'); 83 | 84 | foreach ($source->tasks as $task) { 85 | $this->dispatch($task); 86 | } 87 | 88 | 89 | $sleep = 60 - (time()-$current_time); 90 | 91 | if($sleep>0){ 92 | sleep($sleep); 93 | } 94 | 95 | $source = null; 96 | unset($sleep, $task, $current_time); 97 | } 98 | } 99 | 100 | /** 101 | * Dispatch command 102 | * 103 | * @param $command 104 | */ 105 | protected function dispatch($task) 106 | { 107 | 108 | $this->emit('execute', $task); 109 | $that = $this; 110 | 111 | $this->process->parallel(function () use ($task, $that) { 112 | $status = Utils::exec($task->command, $stdout, $stderr); 113 | 114 | $that->emit('executed', $task, array($status, $stdout, $stderr)); 115 | } 116 | ); 117 | } 118 | } 119 | --------------------------------------------------------------------------------