仓库初始化

This commit is contained in:
2025-08-13 10:43:56 +08:00
commit e8f9b46680
5180 changed files with 859303 additions and 0 deletions

1
addons/crontab/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.addonrc

View File

@@ -0,0 +1,80 @@
<?php
namespace addons\crontab;
use app\common\library\Menu;
use think\Addons;
use think\Loader;
/**
* 定时任务
*/
class Crontab extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
$menu = [
[
'name' => 'general/crontab',
'title' => '定时任务',
'icon' => 'fa fa-tasks',
'remark' => '按照设定的时间进行任务的执行,目前支持三种任务:请求URL、执行SQL、执行Shell。',
'sublist' => [
['name' => 'general/crontab/index', 'title' => '查看'],
['name' => 'general/crontab/add', 'title' => '添加'],
['name' => 'general/crontab/edit', 'title' => '编辑 '],
['name' => 'general/crontab/del', 'title' => '删除'],
['name' => 'general/crontab/multi', 'title' => '批量更新'],
]
]
];
Menu::create($menu, 'general');
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
Menu::delete('general/crontab');
return true;
}
/**
* 插件启用方法
*/
public function enable()
{
Menu::enable('general/crontab');
}
/**
* 插件禁用方法
*/
public function disable()
{
Menu::disable('general/crontab');
}
/**
* 添加命名空间
*/
public function appInit()
{
//添加命名空间
if (!class_exists('\Cron\CronExpression')) {
Loader::addNamespace('Cron', ADDON_PATH . 'crontab' . DS . 'library' . DS . 'Cron' . DS);
}
if (!class_exists('\Jenner\SimpleFork\Pool')) {
Loader::addNamespace('Jenner\SimpleFork', ADDON_PATH . 'crontab' . DS . 'library' . DS . 'SimpleFork' . DS);
}
}
}

19
addons/crontab/config.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
return [
[
'name' => 'mode',
'title' => '执行模式',
'type' => 'select',
'content' => [
'single' => '单进程,阻塞',
'pcntl' => '子进程无阻塞需支持pcntl不支持时自动切换为单进程',
],
'value' => 'pcntl',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
];

View File

@@ -0,0 +1,125 @@
<?php
namespace addons\crontab\controller;
use addons\crontab\model\Crontab;
use Cron\CronExpression;
use fast\Http;
use think\Controller;
use think\Db;
use think\Exception;
use think\Log;
/**
* 定时任务接口
*
* 以Crontab方式每分钟定时执行,且只可以Cli方式运行
* @internal
*/
class Autotask extends Controller
{
/**
* 初始化方法,最前且始终执行
*/
public function _initialize()
{
// 只可以以cli方式执行
if (!$this->request->isCli()) {
$this->error('Autotask script only work at client!');
}
parent::_initialize();
// 清除错误
error_reporting(0);
// 设置永不超时
set_time_limit(0);
}
/**
* 执行定时任务
*/
public function index()
{
$withPcntl = false;
$pool = null;
$config = get_addon_config('crontab');
$mode = $config['mode'] ?? 'pcntl';
if ($mode == 'pcntl' && function_exists('pcntl_fork')) {
$withPcntl = true;
$pool = new \Jenner\SimpleFork\Pool();
}
$time = time();
$logDir = LOG_PATH . 'crontab' . DS;
if (!is_dir($logDir)) {
mkdir($logDir, 0755);
}
//筛选未过期且未完成的任务
$crontabList = Crontab::where('status', '=', 'normal')->order('weigh DESC,id DESC')->select();
$execTime = time();
foreach ($crontabList as $crontab) {
$update = [];
$execute = false;
if ($time < $crontab['begintime']) {
//任务未开始
continue;
}
if ($crontab['maximums'] && $crontab['executes'] > $crontab['maximums']) {
//任务已超过最大执行次数
$update['status'] = 'completed';
} else {
if ($crontab['endtime'] > 0 && $time > $crontab['endtime']) {
//任务已过期
$update['status'] = 'expired';
} else {
//重复执行
//如果未到执行时间则继续循环
$cron = CronExpression::factory($crontab['schedule']);
if (!$cron->isDue() || ($crontab['executetime'] && date("YmdHi", $execTime) === date("YmdHi", $crontab['executetime']))) {
continue;
}
$execute = true;
}
}
// 如果允许执行
if ($execute) {
$update['executetime'] = $time;
$update['executes'] = $crontab['executes'] + 1;
$update['status'] = ($crontab['maximums'] > 0 && $update['executes'] >= $crontab['maximums']) ? 'completed' : 'normal';
}
// 如果需要更新状态
if (!$update) {
continue;
}
// 更新状态
$crontab->save($update);
Db::connect()->close();
// 将执行放在后面是为了避免超时导致多次执行
if (!$execute) {
continue;
}
$runnable = new \addons\crontab\library\CommandRunnable($crontab);
if ($withPcntl) {
$process = new \Jenner\SimpleFork\Process($runnable);
$name = $crontab['title'];
$pool->execute($process);
} else {
$runnable->run();
}
}
if ($withPcntl && $pool) {
$pool->wait();
}
return "Execute completed!\n";
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace addons\crontab\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
}

10
addons/crontab/info.ini Normal file
View File

@@ -0,0 +1,10 @@
name = crontab
title = 定时任务管理
intro = 便捷的后台定时任务管理
author = FastAdmin
website = https://www.fastadmin.net
version = 1.1.3
state = 1
url = /addons/crontab
license = regular
licenseto = 101612

View File

@@ -0,0 +1,40 @@
CREATE TABLE IF NOT EXISTS `__PREFIX__crontab` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`type` varchar(10) NOT NULL DEFAULT '' COMMENT '事件类型',
`title` varchar(100) NOT NULL DEFAULT '' COMMENT '事件标题',
`content` text NOT NULL COMMENT '事件内容',
`schedule` varchar(100) NOT NULL DEFAULT '' COMMENT 'Crontab格式',
`sleep` tinyint(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '延迟秒数执行',
`maximums` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '最大执行次数 0为不限',
`executes` int(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '已经执行的次数',
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
`begintime` bigint(16) DEFAULT NULL COMMENT '开始时间',
`endtime` bigint(16) DEFAULT NULL COMMENT '结束时间',
`executetime` bigint(16) DEFAULT NULL COMMENT '最后执行时间',
`weigh` int(10) NOT NULL DEFAULT '0' COMMENT '权重',
`status` enum('completed','expired','hidden','normal') NOT NULL DEFAULT 'normal' COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='定时任务表';
BEGIN;
INSERT INTO `__PREFIX__crontab` (`id`, `type`, `title`, `content`, `schedule`, `sleep`, `maximums`, `executes`, `createtime`, `updatetime`, `begintime`, `endtime`, `executetime`, `weigh`, `status`) VALUES
(1, 'url', '请求百度', 'https://www.baidu.com', '* * * * *', 0, 0, 0, 1497070825, 1501253101, 1483200000, 1830268800, 1501253101, 1, 'normal'),
(2, 'sql', '查询一条SQL', 'SELECT 1;', '* * * * *', 0, 0, 0, 1497071095, 1501253101, 1483200000, 1830268800, 1501253101, 2, 'normal');
COMMIT;
CREATE TABLE IF NOT EXISTS `__PREFIX__crontab_log` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`crontab_id` int(10) DEFAULT NULL COMMENT '任务ID',
`executetime` bigint(16) DEFAULT NULL COMMENT '执行时间',
`completetime` bigint(16) DEFAULT NULL COMMENT '结束时间',
`content` text COMMENT '执行结果',
`processid` int(10) NULL DEFAULT 0 COMMENT '进程ID',
`status` enum('success','failure', 'inprogress') DEFAULT 'failure' COMMENT '状态',
PRIMARY KEY (`id`),
KEY `crontab_id` (`crontab_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='定时任务日志表';
-- 1.1.0 --
ALTER TABLE `__PREFIX__crontab_log` ADD `processid` INT(10) NULL DEFAULT 0 COMMENT '进程ID' AFTER `content`, CHANGE `status` `status` ENUM('success','failure','inprogress') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'failure' COMMENT '状态';

View File

@@ -0,0 +1,130 @@
<?php
namespace addons\crontab\library;
use fast\Http;
use think\Config;
use think\Db;
class CommandRunnable implements \Jenner\SimpleFork\Runnable
{
protected $connect = null;
protected $crontab = null;
public function __construct($crontab)
{
$this->crontab = $crontab;
}
public function run()
{
$processId = getmypid();
//这里需要强制重连数据库,使用已有的连接会报2014错误
$this->connect = Db::connect([], true);
$this->connect->execute("SELECT 1");
$message = '';
$result = false;
$this->crontabLog = null;
$log = [
'crontab_id' => $this->crontab['id'],
'executetime' => time(),
'completetime' => null,
'content' => '',
'processid' => $processId,
'status' => 'inprogress',
];
$this->connect->name("crontab_log")->insert($log);
$this->crontabLogId = $this->connect->getLastInsID();
try {
if ($this->crontab['type'] == 'url') {
if (substr($this->crontab['content'], 0, 1) == "/") {
// 本地项目URL
$message = shell_exec('php ' . ROOT_PATH . 'public/index.php ' . $this->crontab['content']);
$result = (bool)$message;
} else {
$arr = explode(" ", $this->crontab['content']);
$url = $arr[0];
$params = $arr[1] ?? '';
$method = $arr[2] ?? 'POST';
try {
// 远程异步调用URL
$ret = Http::sendRequest($url, $params, $method);
$result = $ret['ret'];
$message = $ret['msg'];
} catch (\Exception $e) {
$message = $e->getMessage();
}
}
} elseif ($this->crontab['type'] == 'sql') {
$ret = $this->sql($this->crontab['content']);
$result = $ret['ret'];
$message = $ret['msg'];
} elseif ($this->crontab['type'] == 'shell') {
// 执行Shell
$message = shell_exec($this->crontab['content']);
$result = !is_null($message);
}
} catch (\Exception $e) {
$message = $e->getMessage();
}
//设定任务完成
$this->connect->name("crontab_log")->where('id', $this->crontabLogId)->update(['content' => $message, 'completetime' => time(), 'status' => $result ? 'success' : 'failure']);
}
/**
* 执行SQL语句
*/
protected function sql($sql)
{
// 执行SQL
$sqlquery = str_replace('__PREFIX__', config('database.prefix'), $sql);
$sqls = preg_split("/;[ \t]{0,}\n/i", $sqlquery);
$result = false;
$message = '';
$this->connect->startTrans();
try {
foreach ($sqls as $key => $val) {
if (trim($val) == '' || substr($val, 0, 2) == '--' || substr($val, 0, 2) == '/*') {
continue;
}
$message .= "\nSQL:{$val}\n";
$val = rtrim($val, ';');
if (preg_match("/^(select|explain)(.*)/i ", $val)) {
$count = $this->connect->execute($val);
if ($count > 0) {
$resultlist = Db::query($val);
} else {
$resultlist = [];
}
$message .= "Total:{$count}\n";
$j = 1;
foreach ($resultlist as $m => $n) {
$message .= "\n";
$message .= "Row:{$j}\n";
foreach ($n as $k => $v) {
$message .= "{$k}{$v}\n";
}
$j++;
}
} else {
$count = $this->connect->getPdo()->exec($val);
$message = "Affected rows:{$count}";
}
}
$this->connect->commit();
$result = true;
} catch (\PDOException $e) {
$message = $e->getMessage();
$this->connect->rollback();
$result = false;
}
return ['ret' => $result, 'msg' => $message];
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace Cron;
/**
* Abstract CRON expression field
*/
abstract class AbstractField implements FieldInterface
{
/**
* Full range of values that are allowed for this field type
* @var array
*/
protected $fullRange = [];
/**
* Literal values we need to convert to integers
* @var array
*/
protected $literals = [];
/**
* Start value of the full range
* @var integer
*/
protected $rangeStart;
/**
* End value of the full range
* @var integer
*/
protected $rangeEnd;
public function __construct()
{
$this->fullRange = range($this->rangeStart, $this->rangeEnd);
}
/**
* Check to see if a field is satisfied by a value
*
* @param string $dateValue Date value to check
* @param string $value Value to test
*
* @return bool
*/
public function isSatisfied($dateValue, $value)
{
if ($this->isIncrementsOfRanges($value)) {
return $this->isInIncrementsOfRanges($dateValue, $value);
} elseif ($this->isRange($value)) {
return $this->isInRange($dateValue, $value);
}
return $value == '*' || $dateValue == $value;
}
/**
* Check if a value is a range
*
* @param string $value Value to test
*
* @return bool
*/
public function isRange($value)
{
return strpos($value, '-') !== false;
}
/**
* Check if a value is an increments of ranges
*
* @param string $value Value to test
*
* @return bool
*/
public function isIncrementsOfRanges($value)
{
return strpos($value, '/') !== false;
}
/**
* Test if a value is within a range
*
* @param string $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInRange($dateValue, $value)
{
$parts = array_map('trim', explode('-', $value, 2));
return $dateValue >= $parts[0] && $dateValue <= $parts[1];
}
/**
* Test if a value is within an increments of ranges (offset[-to]/step size)
*
* @param string $dateValue Set date value
* @param string $value Value to test
*
* @return bool
*/
public function isInIncrementsOfRanges($dateValue, $value)
{
$chunks = array_map('trim', explode('/', $value, 2));
$range = $chunks[0];
$step = isset($chunks[1]) ? $chunks[1] : 0;
// No step or 0 steps aren't cool
if (is_null($step) || '0' === $step || 0 === $step) {
return false;
}
// Expand the * to a full range
if ('*' == $range) {
$range = $this->rangeStart . '-' . $this->rangeEnd;
}
// Generate the requested small range
$rangeChunks = explode('-', $range, 2);
$rangeStart = $rangeChunks[0];
$rangeEnd = isset($rangeChunks[1]) ? $rangeChunks[1] : $rangeStart;
if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) {
throw new \OutOfRangeException('Invalid range start requested');
}
if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) {
throw new \OutOfRangeException('Invalid range end requested');
}
if ($step > ($rangeEnd - $rangeStart) + 1) {
throw new \OutOfRangeException('Step cannot be greater than total range');
}
$thisRange = range($rangeStart, $rangeEnd, $step);
return in_array($dateValue, $thisRange);
}
/**
* Returns a range of values for the given cron expression
*
* @param string $expression The expression to evaluate
* @param int $max Maximum offset for range
*
* @return array
*/
public function getRangeForExpression($expression, $max)
{
$values = array();
if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) {
if (!$this->isIncrementsOfRanges($expression)) {
list ($offset, $to) = explode('-', $expression);
$stepSize = 1;
}
else {
$range = array_map('trim', explode('/', $expression, 2));
$stepSize = isset($range[1]) ? $range[1] : 0;
$range = $range[0];
$range = explode('-', $range, 2);
$offset = $range[0];
$to = isset($range[1]) ? $range[1] : $max;
}
$offset = $offset == '*' ? 0 : $offset;
for ($i = $offset; $i <= $to; $i += $stepSize) {
$values[] = $i;
}
sort($values);
}
else {
$values = array($expression);
}
return $values;
}
protected function convertLiterals($value)
{
if (count($this->literals)) {
$key = array_search($value, $this->literals);
if ($key !== false) {
return $key;
}
}
return $value;
}
/**
* Checks to see if a value is valid for the field
*
* @param string $value
* @return bool
*/
public function validate($value)
{
$value = $this->convertLiterals($value);
// All fields allow * as a valid value
if ('*' === $value) {
return true;
}
// You cannot have a range and a list at the same time
if (strpos($value, ',') !== false && strpos($value, '-') !== false) {
return false;
}
if (strpos($value, '/') !== false) {
list($range, $step) = explode('/', $value);
return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT);
}
if (strpos($value, '-') !== false) {
if (substr_count($value, '-') > 1) {
return false;
}
$chunks = explode('-', $value);
$chunks[0] = $this->convertLiterals($chunks[0]);
$chunks[1] = $this->convertLiterals($chunks[1]);
if ('*' == $chunks[0] || '*' == $chunks[1]) {
return false;
}
return $this->validate($chunks[0]) && $this->validate($chunks[1]);
}
// Validate each chunk of a list individually
if (strpos($value, ',') !== false) {
foreach (explode(',', $value) as $listItem) {
if (!$this->validate($listItem)) {
return false;
}
}
return true;
}
// We should have a numeric by now, so coerce this into an integer
if (filter_var($value, FILTER_VALIDATE_INT) !== false) {
$value = (int) $value;
}
return in_array($value, $this->fullRange, true);
}
}

View File

@@ -0,0 +1,402 @@
<?php
namespace Cron;
use DateTime;
use DateTimeImmutable;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use RuntimeException;
/**
* CRON expression parser that can determine whether or not a CRON expression is
* due to run, the next run date and previous run date of a CRON expression.
* The determinations made by this class are accurate if checked run once per
* minute (seconds are dropped from date time comparisons).
*
* Schedule parts must map to:
* minute [0-59], hour [0-23], day of month, month [1-12|JAN-DEC], day of week
* [1-7|MON-SUN], and an optional year.
*
* @link http://en.wikipedia.org/wiki/Cron
*/
class CronExpression
{
const MINUTE = 0;
const HOUR = 1;
const DAY = 2;
const MONTH = 3;
const WEEKDAY = 4;
const YEAR = 5;
/**
* @var array CRON expression parts
*/
private $cronParts;
/**
* @var FieldFactory CRON field factory
*/
private $fieldFactory;
/**
* @var int Max iteration count when searching for next run date
*/
private $maxIterationCount = 1000;
/**
* @var array Order in which to test of cron parts
*/
private static $order = array(self::YEAR, self::MONTH, self::DAY, self::WEEKDAY, self::HOUR, self::MINUTE);
/**
* Factory method to create a new CronExpression.
*
* @param string $expression The CRON expression to create. There are
* several special predefined values which can be used to substitute the
* CRON expression:
*
* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - 0 0 1 1 *
* `@monthly` - Run once a month, midnight, first of month - 0 0 1 * *
* `@weekly` - Run once a week, midnight on Sun - 0 0 * * 0
* `@daily` - Run once a day, midnight - 0 0 * * *
* `@hourly` - Run once an hour, first minute - 0 * * * *
* @param FieldFactory $fieldFactory Field factory to use
*
* @return CronExpression
*/
public static function factory($expression, FieldFactory $fieldFactory = null)
{
$mappings = array(
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@hourly' => '0 * * * *'
);
if (isset($mappings[$expression])) {
$expression = $mappings[$expression];
}
return new static($expression, $fieldFactory ?: new FieldFactory());
}
/**
* Validate a CronExpression.
*
* @param string $expression The CRON expression to validate.
*
* @return bool True if a valid CRON expression was passed. False if not.
* @see \Cron\CronExpression::factory
*/
public static function isValidExpression($expression)
{
try {
self::factory($expression);
} catch (InvalidArgumentException $e) {
return false;
}
return true;
}
/**
* Parse a CRON expression
*
* @param string $expression CRON expression (e.g. '8 * * * *')
* @param FieldFactory $fieldFactory Factory to create cron fields
*/
public function __construct($expression, FieldFactory $fieldFactory)
{
$this->fieldFactory = $fieldFactory;
$this->setExpression($expression);
}
/**
* Set or change the CRON expression
*
* @param string $value CRON expression (e.g. 8 * * * *)
*
* @return CronExpression
* @throws \InvalidArgumentException if not a valid CRON expression
*/
public function setExpression($value)
{
$this->cronParts = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY);
if (count($this->cronParts) < 5) {
throw new InvalidArgumentException(
$value . ' is not a valid CRON expression'
);
}
foreach ($this->cronParts as $position => $part) {
$this->setPart($position, $part);
}
return $this;
}
/**
* Set part of the CRON expression
*
* @param int $position The position of the CRON expression to set
* @param string $value The value to set
*
* @return CronExpression
* @throws \InvalidArgumentException if the value is not valid for the part
*/
public function setPart($position, $value)
{
if (!$this->fieldFactory->getField($position)->validate($value)) {
throw new InvalidArgumentException(
'Invalid CRON field value ' . $value . ' at position ' . $position
);
}
$this->cronParts[$position] = $value;
return $this;
}
/**
* Set max iteration count for searching next run dates
*
* @param int $maxIterationCount Max iteration count when searching for next run date
*
* @return CronExpression
*/
public function setMaxIterationCount($maxIterationCount)
{
$this->maxIterationCount = $maxIterationCount;
return $this;
}
/**
* Get a next run date relative to the current date or a specific date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning a
* matching next run date. 0, the default, will return the current
* date and time if the next run date falls on the current date and
* time. Setting this value to 1 will skip the first match and go to
* the second match. Setting this value to 2 will skip the first 2
* matches and so on.
* @param bool $allowCurrentDate Set to TRUE to return the current date if
* it matches the cron expression.
* @param null|string $timeZone Timezone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
*/
public function getNextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null)
{
return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone);
}
/**
* Get a previous run date relative to the current date or a specific date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param null|string $timeZone Timezone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
* @see \Cron\CronExpression::getNextRunDate
*/
public function getPreviousRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false, $timeZone = null)
{
return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone);
}
/**
* Get multiple run dates starting at the current date or a specific date
*
* @param int $total Set the total number of dates to calculate
* @param string|\DateTime $currentTime Relative calculation date
* @param bool $invert Set to TRUE to retrieve previous dates
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param null|string $timeZone Timezone to use instead of the system default
*
* @return array Returns an array of run dates
*/
public function getMultipleRunDates($total, $currentTime = 'now', $invert = false, $allowCurrentDate = false, $timeZone = null)
{
$matches = array();
for ($i = 0; $i < max(0, $total); $i++) {
try {
$matches[] = $this->getRunDate($currentTime, $i, $invert, $allowCurrentDate, $timeZone);
} catch (RuntimeException $e) {
break;
}
}
return $matches;
}
/**
* Get all or part of the CRON expression
*
* @param string $part Specify the part to retrieve or NULL to get the full
* cron schedule string.
*
* @return string|null Returns the CRON expression, a part of the
* CRON expression, or NULL if the part was specified but not found
*/
public function getExpression($part = null)
{
if (null === $part) {
return implode(' ', $this->cronParts);
} elseif (array_key_exists($part, $this->cronParts)) {
return $this->cronParts[$part];
}
return null;
}
/**
* Helper method to output the full expression.
*
* @return string Full CRON expression
*/
public function __toString()
{
return $this->getExpression();
}
/**
* Determine if the cron is due to run based on the current date or a
* specific date. This method assumes that the current number of
* seconds are irrelevant, and should be called once per minute.
*
* @param string|\DateTime $currentTime Relative calculation date
* @param null|string $timeZone Timezone to use instead of the system default
*
* @return bool Returns TRUE if the cron is due to run or FALSE if not
*/
public function isDue($currentTime = 'now', $timeZone = null)
{
if (is_null($timeZone)) {
$timeZone = date_default_timezone_get();
}
if ('now' === $currentTime) {
$currentDate = date('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} elseif ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
// Ensure time in 'current' timezone is used
$currentDate->setTimezone(new DateTimeZone($timeZone));
$currentDate = $currentDate->format('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
$currentDate->setTimezone(new DateTimeZone($timeZone));
$currentDate = $currentDate->format('Y-m-d H:i');
$currentTime = strtotime($currentDate);
} else {
$currentTime = new DateTime($currentTime);
$currentTime->setTime($currentTime->format('H'), $currentTime->format('i'), 0);
$currentDate = $currentTime->format('Y-m-d H:i');
$currentTime = $currentTime->getTimeStamp();
}
try {
return $this->getNextRunDate($currentDate, 0, true)->getTimestamp() == $currentTime;
} catch (Exception $e) {
return false;
}
}
/**
* Get the next or previous run date of the expression relative to a date
*
* @param string|\DateTime $currentTime Relative calculation date
* @param int $nth Number of matches to skip before returning
* @param bool $invert Set to TRUE to go backwards in time
* @param bool $allowCurrentDate Set to TRUE to return the
* current date if it matches the cron expression
* @param string|null $timeZone Timezone to use instead of the system default
*
* @return \DateTime
* @throws \RuntimeException on too many iterations
*/
protected function getRunDate($currentTime = null, $nth = 0, $invert = false, $allowCurrentDate = false, $timeZone = null)
{
if (is_null($timeZone)) {
$timeZone = date_default_timezone_get();
}
if ($currentTime instanceof DateTime) {
$currentDate = clone $currentTime;
} elseif ($currentTime instanceof DateTimeImmutable) {
$currentDate = DateTime::createFromFormat('U', $currentTime->format('U'));
$currentDate->setTimezone($currentTime->getTimezone());
} else {
$currentDate = new DateTime($currentTime ?: 'now');
$currentDate->setTimezone(new DateTimeZone($timeZone));
}
$currentDate->setTime($currentDate->format('H'), $currentDate->format('i'), 0);
$nextRun = clone $currentDate;
$nth = (int) $nth;
// We don't have to satisfy * or null fields
$parts = array();
$fields = array();
foreach (self::$order as $position) {
$part = $this->getExpression($position);
if (null === $part || '*' === $part) {
continue;
}
$parts[$position] = $part;
$fields[$position] = $this->fieldFactory->getField($position);
}
// Set a hard limit to bail on an impossible date
for ($i = 0; $i < $this->maxIterationCount; $i++) {
foreach ($parts as $position => $part) {
$satisfied = false;
// Get the field object used to validate this part
$field = $fields[$position];
// Check if this is singular or a list
if (strpos($part, ',') === false) {
$satisfied = $field->isSatisfiedBy($nextRun, $part);
} else {
foreach (array_map('trim', explode(',', $part)) as $listPart) {
if ($field->isSatisfiedBy($nextRun, $listPart)) {
$satisfied = true;
break;
}
}
}
// If the field is not satisfied, then start over
if (!$satisfied) {
$field->increment($nextRun, $invert, $part);
continue 2;
}
}
// Skip this match if needed
if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) {
$this->fieldFactory->getField(0)->increment($nextRun, $invert, isset($parts[0]) ? $parts[0] : null);
continue;
}
return $nextRun;
}
// @codeCoverageIgnoreStart
throw new RuntimeException('Impossible CRON expression');
// @codeCoverageIgnoreEnd
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Cron;
use DateTime;
/**
* Day of month field. Allows: * , / - ? L W
*
* 'L' stands for "last" and specifies the last day of the month.
*
* The 'W' character is used to specify the weekday (Monday-Friday) nearest the
* given day. As an example, if you were to specify "15W" as the value for the
* day-of-month field, the meaning is: "the nearest weekday to the 15th of the
* month". So if the 15th is a Saturday, the trigger will fire on Friday the
* 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If
* the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you
* specify "1W" as the value for day-of-month, and the 1st is a Saturday, the
* trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary
* of a month's days. The 'W' character can only be specified when the
* day-of-month is a single day, not a range or list of days.
*
* @author Michael Dowling <mtdowling@gmail.com>
*/
class DayOfMonthField extends AbstractField
{
protected $rangeStart = 1;
protected $rangeEnd = 31;
/**
* Get the nearest day of the week for a given day in a month
*
* @param int $currentYear Current year
* @param int $currentMonth Current month
* @param int $targetDay Target day of the month
*
* @return \DateTime Returns the nearest date
*/
private static function getNearestWeekday($currentYear, $currentMonth, $targetDay)
{
$tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT);
$target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday");
$currentWeekday = (int) $target->format('N');
if ($currentWeekday < 6) {
return $target;
}
$lastDayOfMonth = $target->format('t');
foreach (array(-1, 1, -2, 2) as $i) {
$adjusted = $targetDay + $i;
if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
$target->setDate($currentYear, $currentMonth, $adjusted);
if ($target->format('N') < 6 && $target->format('m') == $currentMonth) {
return $target;
}
}
}
}
public function isSatisfiedBy(DateTime $date, $value)
{
// ? states that the field value is to be skipped
if ($value == '?') {
return true;
}
$fieldValue = $date->format('d');
// Check to see if this is the last day of the month
if ($value == 'L') {
return $fieldValue == $date->format('t');
}
// Check to see if this is the nearest weekday to a particular value
if (strpos($value, 'W')) {
// Parse the target day
$targetDay = substr($value, 0, strpos($value, 'W'));
// Find out if the current day is the nearest day of the week
return $date->format('j') == self::getNearestWeekday(
$date->format('Y'),
$date->format('m'),
$targetDay
)->format('j');
}
return $this->isSatisfied($date->format('d'), $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('previous day');
$date->setTime(23, 59);
} else {
$date->modify('next day');
$date->setTime(0, 0);
}
return $this;
}
/**
* @inheritDoc
*/
public function validate($value)
{
$basicChecks = parent::validate($value);
// Validate that a list don't have W or L
if (strpos($value, ',') !== false && (strpos($value, 'W') !== false || strpos($value, 'L') !== false)) {
return false;
}
if (!$basicChecks) {
if ($value === 'L') {
return true;
}
if (preg_match('/^(.*)W$/', $value, $matches)) {
return $this->validate($matches[1]);
}
return false;
}
return $basicChecks;
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace Cron;
use DateTime;
use InvalidArgumentException;
/**
* Day of week field. Allows: * / , - ? L #
*
* Days of the week can be represented as a number 0-7 (0|7 = Sunday)
* or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT.
*
* 'L' stands for "last". It allows you to specify constructs such as
* "the last Friday" of a given month.
*
* '#' is allowed for the day-of-week field, and must be followed by a
* number between one and five. It allows you to specify constructs such as
* "the second Friday" of a given month.
*/
class DayOfWeekField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 7;
protected $nthRange;
protected $literals = [1 => 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN'];
public function __construct()
{
$this->nthRange = range(1, 5);
parent::__construct();
}
public function isSatisfiedBy(DateTime $date, $value)
{
if ($value == '?') {
return true;
}
// Convert text day of the week values to integers
$value = $this->convertLiterals($value);
$currentYear = $date->format('Y');
$currentMonth = $date->format('m');
$lastDayOfMonth = $date->format('t');
// Find out if this is the last specific weekday of the month
if (strpos($value, 'L')) {
$weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L')));
$tdate = clone $date;
$tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth);
while ($tdate->format('w') != $weekday) {
$tdateClone = new DateTime();
$tdate = $tdateClone
->setTimezone($tdate->getTimezone())
->setDate($currentYear, $currentMonth, --$lastDayOfMonth);
}
return $date->format('j') == $lastDayOfMonth;
}
// Handle # hash tokens
if (strpos($value, '#')) {
list($weekday, $nth) = explode('#', $value);
if (!is_numeric($nth)) {
throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given");
} else {
$nth = (int) $nth;
}
// 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601
if ($weekday === '0') {
$weekday = 7;
}
$weekday = $this->convertLiterals($weekday);
// Validate the hash fields
if ($weekday < 0 || $weekday > 7) {
throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given");
}
if (!in_array($nth, $this->nthRange)) {
throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given");
}
// The current weekday must match the targeted weekday to proceed
if ($date->format('N') != $weekday) {
return false;
}
$tdate = clone $date;
$tdate->setDate($currentYear, $currentMonth, 1);
$dayCount = 0;
$currentDay = 1;
while ($currentDay < $lastDayOfMonth + 1) {
if ($tdate->format('N') == $weekday) {
if (++$dayCount >= $nth) {
break;
}
}
$tdate->setDate($currentYear, $currentMonth, ++$currentDay);
}
return $date->format('j') == $currentDay;
}
// Handle day of the week values
if (strpos($value, '-')) {
$parts = explode('-', $value);
if ($parts[0] == '7') {
$parts[0] = '0';
} elseif ($parts[1] == '0') {
$parts[1] = '7';
}
$value = implode('-', $parts);
}
// Test to see which Sunday to use -- 0 == 7 == Sunday
$format = in_array(7, str_split($value)) ? 'N' : 'w';
$fieldValue = $date->format($format);
return $this->isSatisfied($fieldValue, $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('-1 day');
$date->setTime(23, 59, 0);
} else {
$date->modify('+1 day');
$date->setTime(0, 0, 0);
}
return $this;
}
/**
* @inheritDoc
*/
public function validate($value)
{
$basicChecks = parent::validate($value);
if (!$basicChecks) {
// Handle the # value
if (strpos($value, '#') !== false) {
$chunks = explode('#', $value);
$chunks[0] = $this->convertLiterals($chunks[0]);
if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && in_array($chunks[1], $this->nthRange)) {
return true;
}
}
if (preg_match('/^(.*)L$/', $value, $matches)) {
return $this->validate($matches[1]);
}
return false;
}
return $basicChecks;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Cron;
use InvalidArgumentException;
/**
* CRON field factory implementing a flyweight factory
* @link http://en.wikipedia.org/wiki/Cron
*/
class FieldFactory
{
/**
* @var array Cache of instantiated fields
*/
private $fields = array();
/**
* Get an instance of a field object for a cron expression position
*
* @param int $position CRON expression position value to retrieve
*
* @return FieldInterface
* @throws InvalidArgumentException if a position is not valid
*/
public function getField($position)
{
if (!isset($this->fields[$position])) {
switch ($position) {
case 0:
$this->fields[$position] = new MinutesField();
break;
case 1:
$this->fields[$position] = new HoursField();
break;
case 2:
$this->fields[$position] = new DayOfMonthField();
break;
case 3:
$this->fields[$position] = new MonthField();
break;
case 4:
$this->fields[$position] = new DayOfWeekField();
break;
default:
throw new InvalidArgumentException(
$position . ' is not a valid position'
);
}
}
return $this->fields[$position];
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Cron;
use DateTime;
/**
* CRON field interface
*/
interface FieldInterface
{
/**
* Check if the respective value of a DateTime field satisfies a CRON exp
*
* @param DateTime $date DateTime object to check
* @param string $value CRON expression to test against
*
* @return bool Returns TRUE if satisfied, FALSE otherwise
*/
public function isSatisfiedBy(DateTime $date, $value);
/**
* When a CRON expression is not satisfied, this method is used to increment
* or decrement a DateTime object by the unit of the cron field
*
* @param DateTime $date DateTime object to change
* @param bool $invert (optional) Set to TRUE to decrement
*
* @return FieldInterface
*/
public function increment(DateTime $date, $invert = false);
/**
* Validates a CRON expression for a given field
*
* @param string $value CRON expression value to validate
*
* @return bool Returns TRUE if valid, FALSE otherwise
*/
public function validate($value);
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Cron;
use DateTime;
use DateTimeZone;
/**
* Hours field. Allows: * , / -
*/
class HoursField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 23;
public function isSatisfiedBy(DateTime $date, $value)
{
return $this->isSatisfied($date->format('H'), $value);
}
public function increment(DateTime $date, $invert = false, $parts = null)
{
// Change timezone to UTC temporarily. This will
// allow us to go back or forwards and hour even
// if DST will be changed between the hours.
if (is_null($parts) || $parts == '*') {
$timezone = $date->getTimezone();
$date->setTimezone(new DateTimeZone('UTC'));
if ($invert) {
$date->modify('-1 hour');
} else {
$date->modify('+1 hour');
}
$date->setTimezone($timezone);
$date->setTime($date->format('H'), $invert ? 59 : 0);
return $this;
}
$parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
$hours = array();
foreach ($parts as $part) {
$hours = array_merge($hours, $this->getRangeForExpression($part, 23));
}
$current_hour = $date->format('H');
$position = $invert ? count($hours) - 1 : 0;
if (count($hours) > 1) {
for ($i = 0; $i < count($hours) - 1; $i++) {
if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) ||
($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
$hour = $hours[$position];
if ((!$invert && $date->format('H') >= $hour) || ($invert && $date->format('H') <= $hour)) {
$date->modify(($invert ? '-' : '+') . '1 day');
$date->setTime($invert ? 23 : 0, $invert ? 59 : 0);
}
else {
$date->setTime($hour, $invert ? 59 : 0);
}
return $this;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Cron;
use DateTime;
/**
* Minutes field. Allows: * , / -
*/
class MinutesField extends AbstractField
{
protected $rangeStart = 0;
protected $rangeEnd = 59;
public function isSatisfiedBy(DateTime $date, $value)
{
return $this->isSatisfied($date->format('i'), $value);
}
public function increment(DateTime $date, $invert = false, $parts = null)
{
if (is_null($parts)) {
if ($invert) {
$date->modify('-1 minute');
} else {
$date->modify('+1 minute');
}
return $this;
}
$parts = strpos($parts, ',') !== false ? explode(',', $parts) : array($parts);
$minutes = array();
foreach ($parts as $part) {
$minutes = array_merge($minutes, $this->getRangeForExpression($part, 59));
}
$current_minute = $date->format('i');
$position = $invert ? count($minutes) - 1 : 0;
if (count($minutes) > 1) {
for ($i = 0; $i < count($minutes) - 1; $i++) {
if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) ||
($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) {
$position = $invert ? $i : $i + 1;
break;
}
}
}
if ((!$invert && $current_minute >= $minutes[$position]) || ($invert && $current_minute <= $minutes[$position])) {
$date->modify(($invert ? '-' : '+') . '1 hour');
$date->setTime($date->format('H'), $invert ? 59 : 0);
}
else {
$date->setTime($date->format('H'), $minutes[$position]);
}
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Cron;
use DateTime;
/**
* Month field. Allows: * , / -
*/
class MonthField extends AbstractField
{
protected $rangeStart = 1;
protected $rangeEnd = 12;
protected $literals = [1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL',
8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC'];
public function isSatisfiedBy(DateTime $date, $value)
{
$value = $this->convertLiterals($value);
return $this->isSatisfied($date->format('m'), $value);
}
public function increment(DateTime $date, $invert = false)
{
if ($invert) {
$date->modify('last day of previous month');
$date->setTime(23, 59);
} else {
$date->modify('first day of next month');
$date->setTime(0, 0);
}
return $this;
}
}

View File

@@ -0,0 +1,183 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/11/3
* Time: 14:37
*/
namespace Jenner\SimpleFork;
/**
* processes' pool
*
* @package Jenner\SimpleFork
*/
abstract class AbstractPool
{
/**
* process list
*
* @var Process[]
*/
protected $processes = array();
/**
* get process by pid
*
* @param $pid
* @return null|Process
*/
public function getProcessByPid($pid)
{
foreach ($this->processes as $process) {
if ($process->getPid() == $pid) {
return $process;
}
}
return null;
}
/**
* shutdown sub process and no wait. it is dangerous,
* maybe the sub process is working.
*/
public function shutdownForce()
{
$this->shutdown(SIGKILL);
}
/**
* shutdown all process
*
* @param int $signal
*/
public function shutdown($signal = SIGTERM)
{
foreach ($this->processes as $process) {
if ($process->isRunning()) {
$process->shutdown(true, $signal);
}
}
}
/**
* if all processes are stopped
*
* @return bool
*/
public function isFinished()
{
foreach ($this->processes as $process) {
if (!$process->isStopped()) {
return false;
}
}
return true;
}
/**
* waiting for the sub processes to exit
*
* @param bool|true $block if true the parent process will be blocked until all
* sub processes exit. else it will check if there are processes that had been exited once and return.
* @param int $sleep when $block is true, it will check sub processes every $sleep minute
*/
public function wait($block = true, $sleep = 100)
{
do {
foreach ($this->processes as $process) {
if (!$process->isRunning()) {
continue;
}
}
usleep($sleep);
} while ($block && $this->aliveCount() > 0);
}
/**
* get the count of running processes
*
* @return int
*/
public function aliveCount()
{
$count = 0;
foreach ($this->processes as $process) {
if ($process->isRunning()) {
$count++;
}
}
return $count;
}
/**
* get process by name
*
* @param string $name process name
* @return Process|null
*/
public function getProcessByName($name)
{
foreach ($this->processes as $process) {
if ($process->name() == $name) {
return $process;
}
}
return null;
}
/**
* remove process by name
*
* @param string $name process name
* @throws \RuntimeException
*/
public function removeProcessByName($name)
{
foreach ($this->processes as $key => $process) {
if ($process->name() == $name) {
if ($process->isRunning()) {
throw new \RuntimeException("can not remove a running process");
}
unset($this->processes[$key]);
}
}
}
/**
* remove exited process
*/
public function removeExitedProcess()
{
foreach ($this->processes as $key => $process) {
if ($process->isStopped()) {
unset($this->processes[$key]);
}
}
}
/**
* return process count
*
* @return int
*/
public function count()
{
return count($this->processes);
}
/**
* get all processes
*
* @return Process[]
*/
public function getProcesses()
{
return $this->processes;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 14:59
*/
namespace Jenner\SimpleFork\Cache;
/**
* cache for processes shared variables
*
* @package Jenner\SimpleFork\Cache
*/
interface CacheInterface
{
/**
* get var
*
* @param $key
* @param null $default
* @return bool|mixed
*/
public function get($key, $default = null);
/**
* set var
*
* @param $key
* @param null $value
* @return
*/
public function set($key, $value);
/**
* has var ?
*
* @param $key
* @return bool
*/
public function has($key);
/**
* delete var
*
* @param $key
* @return bool
*/
public function delete($key);
}

View File

@@ -0,0 +1,263 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2016/6/22
* Time: 16:18
*/
namespace Jenner\SimpleFork\Cache;
class FileCache implements CacheInterface
{
/**
* 缓存目录
* @var
*/
private $cache_dir;
/**
* @param string $cache_dir
* @throws \Exception
*/
public function __construct($cache_dir)
{
$this->cache_dir = $cache_dir;
if (!is_dir($cache_dir)) {
$make_dir_result = mkdir($cache_dir, 0755, true);
if ($make_dir_result === false) throw new \Exception('Cannot create the cache directory');
}
}
/**
* get value by key, and check if it is expired
* @param string $key
* @param string $default
* @return mixed
*/
public function get($key, $default = null)
{
$cache_data = $this->getItem($key);
if ($cache_data === false || !is_array($cache_data)) return $default;
return $cache_data['data'];
}
/**
* 添加或覆盖一个key
* @param string $key
* @param mixed $value
* @param int $expire expire time in seconds
* @return mixed
*/
public function set($key, $value, $expire = 0)
{
return $this->setItem($key, $value, time(), $expire);
}
/**
* 设置包含元数据的信息
* @param $key
* @param $value
* @param $time
* @param $expire
* @return bool
*/
private function setItem($key, $value, $time, $expire)
{
$cache_file = $this->createCacheFile($key);
if ($cache_file === false) return false;
$cache_data = array('data' => $value, 'time' => $time, 'expire' => $expire);
$cache_data = serialize($cache_data);
$put_result = file_put_contents($cache_file, $cache_data);
if ($put_result === false) return false;
return true;
}
/**
* 创建缓存文件
* @param $key
* @return bool|string
*/
private function createCacheFile($key)
{
$cache_file = $this->path($key);
if (!file_exists($cache_file)) {
$directory = dirname($cache_file);
if (!is_dir($directory)) {
$make_dir_result = mkdir($directory, 0755, true);
if ($make_dir_result === false) return false;
}
$create_result = touch($cache_file);
if ($create_result === false) return false;
}
return $cache_file;
}
/**
* 判断Key是否存在
* @param $key
* @return mixed
*/
public function has($key)
{
$value = $this->get($key);
if ($value === false) return false;
return true;
}
/**
* 加法递增
* @param $key
* @param int $value
* @return mixed
*/
public function increment($key, $value = 1)
{
$item = $this->getItem($key);
if ($item === false) {
$set_result = $this->set($key, $value);
if ($set_result === false) return false;
return $value;
}
$check_expire = $this->checkExpire($item);
if ($check_expire === false) return false;
$item['data'] += $value;
$result = $this->setItem($key, $item['data'], $item['time'], $item['expire']);
if ($result === false) return false;
return $item['data'];
}
/**
* 减法递增
* @param $key
* @param int $value
* @return mixed
*/
public function decrement($key, $value = 1)
{
$item = $this->getItem($key);
if ($item === false) {
$value = 0 - $value;
$set_result = $this->set($key, $value);
if ($set_result === false) return false;
return $value;
}
$check_expire = $this->checkExpire($item);
if ($check_expire === false) return false;
$item['data'] -= $value;
$result = $this->setItem($key, $item['data'], $item['time'], $item['expire']);
if ($result === false) return false;
return $item['data'];
}
/**
* 删除一个key同事会删除缓存文件
* @param $key
* @return boolean
*/
public function delete($key)
{
$cache_file = $this->path($key);
if (file_exists($cache_file)) {
$unlink_result = unlink($cache_file);
if ($unlink_result === false) return false;
}
return true;
}
/**
* 清楚所有缓存
* @return mixed
*/
public function flush()
{
return $this->delTree($this->cache_dir);
}
/**
* 递归删除目录
* @param $dir
* @return bool
*/
function delTree($dir)
{
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? $this->delTree("$dir/$file") : unlink("$dir/$file");
}
return rmdir($dir);
}
/**
* 根据key获取缓存文件路径
*
* @param string $key
* @return string
*/
protected function path($key)
{
$parts = array_slice(str_split($hash = md5($key), 2), 0, 2);
return $this->cache_dir . '/' . implode('/', $parts) . '/' . $hash;
}
/**
* 获取含有元数据的信息
* @param $key
* @return bool|mixed|string
*/
protected function getItem($key)
{
$cache_file = $this->path($key);
if (!file_exists($cache_file) || !is_readable($cache_file)) {
return false;
}
$data = file_get_contents($cache_file);
if (empty($data)) return false;
$cache_data = unserialize($data);
if ($cache_data === false) {
return false;
}
$check_expire = $this->checkExpire($cache_data);
if ($check_expire === false) {
$this->delete($key);
return false;
}
return $cache_data;
}
/**
* 检查key是否过期
* @param $cache_data
* @return bool
*/
protected function checkExpire($cache_data)
{
$time = time();
$is_expire = intval($cache_data['expire']) !== 0 && (intval($cache_data['time']) + intval($cache_data['expire']) < $time);
if ($is_expire) return false;
return true;
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/20
* Time: 15:14
*/
namespace Jenner\SimpleFork\Cache;
/**
* redis cache
*
* @package Jenner\SimpleFork\Cache
*/
class RedisCache implements CacheInterface
{
/**
* @var \Redis
*/
protected $redis;
protected $prefix;
/**
* @param string $host
* @param int $port
* @param int $database
* @param string $prefix
*/
public function __construct(
$host = '127.0.0.1',
$port = 6379,
$database = 0,
$prefix = 'simple-fork'
)
{
$this->redis = new \Redis();
$connection_result = $this->redis->connect($host, $port);
if (!$connection_result) {
throw new \RuntimeException('can not connect to the redis server');
}
if ($database != 0) {
$select_result = $this->redis->select($database);
if (!$select_result) {
throw new \RuntimeException('can not select the database');
}
}
if (empty($prefix)) {
throw new \InvalidArgumentException('prefix can not be empty');
}
$this->prefix = $prefix;
}
/**
* close redis connection
*/
public function __destruct()
{
$this->close();
}
/**
* close the connection
*/
public function close()
{
$this->redis->close();
}
/**
* get var
*
* @param $key
* @param null $default
* @return bool|string|null
*/
public function get($key, $default = null)
{
$result = $this->redis->hGet($this->prefix, $key);
if ($result !== false) return $result;
return $default;
}
/**
* set var
*
* @param $key
* @param null $value
* @return boolean
*/
public function set($key, $value)
{
return $this->redis->hSet($this->prefix, $key, $value);
}
/**
* has var ?
*
* @param $key
* @return bool
*/
public function has($key)
{
return $this->redis->hExists($this->prefix, $key);
}
/**
* delete var
*
* @param $key
* @return bool
*/
public function delete($key)
{
if ($this->redis->hDel($this->prefix, $key) > 0) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,176 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:00
*/
namespace Jenner\SimpleFork\Cache;
/**
* shared memory cache
*
* @package Jenner\SimpleFork\Cache
*/
class SharedMemory implements CacheInterface
{
/**
* holds shared memory resource
* @var resource
*/
protected $shm;
/**
* shared memory ipc key
* @var string
*/
protected $client_count_key = 'system_client_count';
/**
* memory size
* @var int
*/
protected $size;
/**
* @param int $size memory size
* @param string $file
*/
public function __construct($size = 33554432, $file = __FILE__)
{
$this->size = $size;
if (function_exists("shm_attach") === false) {
$message = "\nYour PHP configuration needs adjustment. " .
"See: http://us2.php.net/manual/en/shmop.setup.php. " .
"To enable the System V shared memory support compile " .
" PHP with the option --enable-sysvshm.";
throw new \RuntimeException($message);
}
$this->attach($file); //create resources (shared memory)
}
/**
* connect shared memory
*
* @param string $file
*/
public function attach($file = __FILE__)
{
if (!file_exists($file)) {
$touch = touch($file);
if (!$touch) {
throw new \RuntimeException("file is not exists and it can not be created. file: {$file}");
}
}
$key = ftok($file, 'a');
$this->shm = shm_attach($key, $this->size); //allocate shared memory
}
/**
* remove shared memory.
* you should know that it maybe does not work.
*
* @return bool
*/
public function remove()
{
//dallocate shared memory
if (!shm_remove($this->shm)) {
return false;
}
$this->dettach();
// shm_remove maybe not working. it likes a php bug.
unset($this->shm);
return true;
}
/**
* @return bool
*/
public function dettach()
{
return shm_detach($this->shm); //allocate shared memory
}
/**
* set var
*
* @param $key
* @param $value
* @return bool
*/
public function set($key, $value)
{
return shm_put_var($this->shm, $this->shm_key($key), $value); //store var
}
/**
* generate shm key
*
* @param $val
* @return mixed
*/
public function shm_key($val)
{ // enable all world langs and chars !
// text to number system.
return preg_replace("/[^0-9]/", "", (preg_replace("/[^0-9]/", "", md5($val)) / 35676248) / 619876);
}
/**
* get var
*
* @param $key
* @param null $default
* @return bool|mixed
*/
public function get($key, $default = null)
{
if ($this->has($key)) {
return shm_get_var($this->shm, $this->shm_key($key));
} else {
return $default;
}
}
/**
* has var ?
*
* @param $key
* @return bool
*/
public function has($key)
{
if (shm_has_var($this->shm, $this->shm_key($key))) { // check is isset
return true;
} else {
return false;
}
}
/**
* delete var
*
* @param $key
* @return bool
*/
public function delete($key)
{
if ($this->has($key)) {
return shm_remove_var($this->shm, $this->shm_key($key));
} else {
return false;
}
}
/**
* init when wakeup
*/
public function __wakeup()
{
$this->attach();
}
}

View File

@@ -0,0 +1,67 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/11/2
* Time: 17:45
*/
namespace Jenner\SimpleFork;
/**
* fixed pool
*
* @package Jenner\SimpleFork
*/
class FixedPool extends AbstractPool
{
/**
* @var int max process count
*/
protected $max;
/**
* @param int $max
*/
public function __construct($max = 4)
{
$this->max = $max;
}
public function execute(Process $process)
{
Utils::checkOverwriteRunMethod(get_class($process));
if ($this->aliveCount() < $this->max && !$process->isStarted()) {
$process->start();
}
array_push($this->processes, $process);
}
/**
* wait for all process done
*
* @param bool $block block the master process
* to keep the sub process count all the time
* @param int $interval check time interval
*/
public function wait($block = false, $interval = 100)
{
do {
if ($this->isFinished()) {
return;
}
parent::wait(false);
if ($this->aliveCount() < $this->max) {
foreach ($this->processes as $process) {
if ($process->isStarted()) continue;
$process->start();
if ($this->aliveCount() >= $this->max) break;
}
}
$block ? usleep($interval) : null;
} while ($block);
}
}

View File

@@ -0,0 +1,126 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/21
* Time: 14:30
*/
namespace Jenner\SimpleFork\Lock;
/**
* file lock
*
* @package Jenner\SimpleFork\Lock
*/
class FileLock implements LockInterface
{
/**
* @var string lock file
*/
protected $file;
/**
* @var resource
*/
protected $fp;
/**
* @var bool
*/
protected $locked = false;
/**
* @param $file
*/
private function __construct($file)
{
if (!file_exists($file) || !is_readable($file)) {
throw new \RuntimeException("{$file} is not exists or not readable");
}
$this->fp = fopen($file, "r+");
if (!is_resource($this->fp)) {
throw new \RuntimeException("open {$file} failed");
}
}
/**
* create a file lock instance
* if the file is not exists, it will be created
*
* @param string $file lock file
* @return FileLock
*/
public static function create($file)
{
return new FileLock($file);
}
/**
* get a lock
*
* @param bool $blocking
* @return mixed
*/
public function acquire($blocking = true)
{
if ($this->locked) {
throw new \RuntimeException('already lock by yourself');
}
if ($blocking) {
$locked = flock($this->fp, LOCK_EX);
} else {
$locked = flock($this->fp, LOCK_EX | LOCK_NB);
}
if ($locked !== true) {
return false;
}
$this->locked = true;
return true;
}
/**
* is locked
*
* @return mixed
*/
public function isLocked()
{
return $this->locked === true ? true : false;
}
/**
*
*/
public function __destory()
{
if ($this->locked) {
$this->release();
}
}
/**
* release lock
*
* @return mixed
*/
public function release()
{
if (!$this->locked) {
throw new \RuntimeException('release a non lock');
}
$unlock = flock($this->fp, LOCK_UN);
fclose($this->fp);
if ($unlock !== true) {
return false;
}
$this->locked = false;
return true;
}
}

View File

@@ -0,0 +1,40 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/21
* Time: 14:24
*/
namespace Jenner\SimpleFork\Lock;
/**
* lock for processes to mutual exclusion
*
* @package Jenner\SimpleFork\Lock
*/
interface LockInterface
{
/**
* get a lock
*
* @param bool $blocking
* @return bool
*/
public function acquire($blocking = true);
/**
* release lock
*
* @return bool
*/
public function release();
/**
* is locked
*
* @return bool
*/
public function isLocked();
}

View File

@@ -0,0 +1,163 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 20:52
*/
namespace Jenner\SimpleFork\Lock;
/**
* sem lock
*
* @package Jenner\SimpleFork\Lock
*/
class Semaphore implements LockInterface
{
/**
* @var
*/
private $lock_id;
/**
* @var bool
*/
private $locked = false;
/**
* init a lock
*
* @param $key
* @param $count
* @throws \RuntimeException
*/
private function __construct($key, $count = 1)
{
if (($this->lock_id = sem_get($this->_stringToSemKey($key), $count)) === false) {
throw new \RuntimeException("Cannot create semaphore for key: {$key}");
}
}
/**
* Semaphore requires a numeric value as the key
*
* @param $identifier
* @return int
*/
protected function _stringToSemKey($identifier)
{
$md5 = md5($identifier);
$key = 0;
for ($i = 0; $i < 32; $i++) {
$key += ord($md5{$i}) * $i;
}
return $key;
}
/**
* create a lock instance
*
* @param $key
* @return Semaphore
*/
public static function create($key)
{
return new Semaphore($key);
}
/**
* release lock
*
* @throws \RuntimeException
*/
public function __destruct()
{
if ($this->isLocked()) {
$this->release();
}
}
/**
* is locked
*
* @return bool
*/
public function isLocked()
{
return $this->locked === true ? true : false;
}
/**
* release lock
*
* @return bool
* @throws \RuntimeException
*/
public function release()
{
if (!$this->locked) {
throw new \RuntimeException("release a non lock");
}
if (!sem_release($this->lock_id)) {
return false;
}
$this->locked = false;
return true;
}
/**
* get a lock
*
* @param bool $blocking
* @return bool
*/
public function acquire($blocking = true)
{
if ($this->locked) {
throw new \RuntimeException('already lock by yourself');
}
if ($blocking === false) {
if (version_compare(PHP_VERSION, '5.6.0') < 0) {
throw new \RuntimeException('php version is at least 5.6.0 for param blocking');
}
if (!sem_acquire($this->lock_id, true)) {
return false;
}
$this->locked = true;
return true;
}
if (!sem_acquire($this->lock_id)) {
return false;
}
$this->locked = true;
return true;
}
/**
* remove the semaphore resource
*
* @return bool
*/
public function remove()
{
if ($this->locked) {
throw new \RuntimeException('can not remove a locked semaphore resource');
}
if (!is_resource($this->lock_id)) {
throw new \RuntimeException('can not remove a empty semaphore resource');
}
if (!sem_release($this->lock_id)) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,106 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/19 20:49
*/
namespace Jenner\SimpleFork;
/**
* parallel pool
*
* @package Jenner\SimpleFork
*/
class ParallelPool extends AbstractPool
{
/**
* @var callable|Runnable sub process callback
*/
protected $runnable;
/**
* @var int max process count
*/
protected $max;
/**
* @param callable|Runnable $callback
* @param int $max
*/
public function __construct($callback, $max = 4)
{
if (!is_callable($callback) && !($callback instanceof Runnable)) {
throw new \InvalidArgumentException('callback must be a callback function or a object of Runnalbe');
}
$this->runnable = $callback;
$this->max = $max;
}
/**
* start the same number processes and kill the old sub process
* just like nginx -s reload
* this method will block until all the old process exit;
*
* @param bool $block
*/
public function reload($block = true)
{
$old_processes = $this->processes;
for ($i = 0; $i < $this->max; $i++) {
$process = new Process($this->runnable);
$process->start();
$this->processes[$process->getPid()] = $process;
}
foreach ($old_processes as $process) {
$process->shutdown();
$process->wait($block);
unset($this->processes[$process->getPid()]);
}
}
/**
* keep sub process count
*
* @param bool $block block the master process
* to keep the sub process count all the time
* @param int $interval check time interval
*/
public function keep($block = false, $interval = 100)
{
do {
$this->start();
// recycle sub process and delete the processes
// which are not running from process list
foreach ($this->processes as $process) {
if (!$process->isRunning()) {
unset($this->processes[$process->getPid()]);
}
}
$block ? usleep($interval) : null;
} while ($block);
}
/**
* start the pool
*/
public function start()
{
$alive_count = $this->aliveCount();
// create sub process and run
if ($alive_count < $this->max) {
$need = $this->max - $alive_count;
for ($i = 0; $i < $need; $i++) {
$process = new Process($this->runnable);
$process->start();
$this->processes[$process->getPid()] = $process;
}
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 17:54
*/
namespace Jenner\SimpleFork;
/**
* pool
*
* @package Jenner\SimpleFork
*/
class Pool extends AbstractPool
{
/**
* add a process
*
* @param Process $process
* @param null|string $name process name
* @return int
*/
public function execute(Process $process, $name = null)
{
if (!is_null($name)) {
$process->name($name);
}
if (!$process->isStarted()) {
$process->start();
}
return array_push($this->processes, $process);
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/19 21:14
*/
namespace Jenner\SimpleFork;
class PoolFactory
{
/**
* create a pool instance
*
* @return Pool
*/
public static function newPool()
{
return new Pool();
}
/**
* create a fixed pool instance
*
* @param int $max
* @return FixedPool
*/
public static function newFixedPool($max = 4)
{
return new FixedPool($max);
}
/**
* create a parallel pool instance
*
* @param $callback
* @param int $max
* @return ParallelPool
*/
public static function newParallelPool($callback, $max = 4)
{
return new ParallelPool($callback, $max);
}
/**
* create a single pool
*
* @return SinglePool
*/
public static function newSinglePool()
{
return new SinglePool();
}
}

View File

@@ -0,0 +1,373 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:25
*/
namespace Jenner\SimpleFork;
class Process
{
/**
* @var Runnable|callable
*/
protected $runnable;
/**
* @var int
*/
protected $pid = 0;
/**
* @var string custom process name
*/
protected $name = null;
/**
* @var bool if the process is started
*/
protected $started = false;
/**
* @var bool
*/
protected $running = false;
/**
* @var int the signal which made the process terminate
*/
protected $term_signal = null;
/**
* @var int the signal which made the process stop
*/
protected $stop_signal = null;
/**
* @var int error code
*/
protected $errno = null;
/**
* @var string error message
*/
protected $errmsg = null;
/**
* @var bool
*/
protected $if_signal = false;
/**
* @var array
*/
protected $callbacks = array();
/**
* @var array signal handlers
*/
protected $signal_handlers = array();
/**
* @param string $execution it can be a Runnable object, callback function or null
* @param null $name process name,you can manager the process by it's name.
*/
public function __construct($execution = null, $name = null)
{
if (!is_null($execution) && $execution instanceof Runnable) {
$this->runnable = $execution;
} elseif (!is_null($execution) && is_callable($execution)) {
$this->runnable = $execution;
} elseif (!is_null($execution)) {
throw new \InvalidArgumentException('param execution is not a object of Runnable or callable');
} else {
Utils::checkOverwriteRunMethod(get_class($this));
}
if (!is_null($name)) {
$this->name = $name;
}
$this->initStatus();
}
/**
* init process status
*/
protected function initStatus()
{
$this->pid = null;
$this->running = null;
$this->term_signal = null;
$this->stop_signal = null;
$this->errno = null;
$this->errmsg = null;
}
/**
* get pid
*
* @return int
*/
public function getPid()
{
return $this->pid;
}
/**
* get or set name
*
* @param string|null $name
* @return mixed
*/
public function name($name = null)
{
if (!is_null($name)) {
$this->name = $name;
} else {
return $this->name;
}
}
/**
* if the process is stopped
*
* @return bool
*/
public function isStopped()
{
if (is_null($this->errno)) {
return false;
}
return true;
}
/**
* if the process is started
*
* @return bool
*/
public function isStarted()
{
return $this->started;
}
/**
* get pcntl errno
*
* @return int
*/
public function errno()
{
return $this->errno;
}
/**
* get pcntl errmsg
*
* @return string
*/
public function errmsg()
{
return $this->errmsg;
}
public function ifSignal()
{
return $this->if_signal;
}
/**
* start the sub process
* and run the callback
*
* @return string pid
*/
public function start()
{
if (!empty($this->pid) && $this->isRunning()) {
throw new \LogicException("the process is already running");
}
$callback = $this->getCallable();
$pid = pcntl_fork();
if ($pid < 0) {
throw new \RuntimeException("fork error");
} elseif ($pid > 0) {
$this->pid = $pid;
$this->running = true;
$this->started = true;
} else {
$this->pid = getmypid();
$this->signal();
foreach ($this->signal_handlers as $signal => $handler) {
pcntl_signal($signal, $handler);
}
call_user_func($callback);
exit(0);
}
}
/**
* if the process is running
*
* @return bool
*/
public function isRunning()
{
$this->updateStatus();
return $this->running;
}
/**
* update the process status
*
* @param bool $block
*/
protected function updateStatus($block = false)
{
if ($this->running !== true) {
return;
}
if ($block) {
$res = pcntl_waitpid($this->pid, $status);
} else {
$res = pcntl_waitpid($this->pid, $status, WNOHANG | WUNTRACED);
}
if ($res === -1) {
throw new \RuntimeException('pcntl_waitpid failed. the process maybe available');
} elseif ($res === 0) {
$this->running = true;
} else {
if (pcntl_wifsignaled($status)) {
$this->term_signal = pcntl_wtermsig($status);
}
if (pcntl_wifstopped($status)) {
$this->stop_signal = pcntl_wstopsig($status);
}
if (pcntl_wifexited($status)) {
$this->errno = pcntl_wexitstatus($status);
$this->errmsg = pcntl_strerror($this->errno);
} else {
$this->errno = pcntl_get_last_error();
$this->errmsg = pcntl_strerror($this->errno);
}
if (pcntl_wifsignaled($status)) {
$this->if_signal = true;
} else {
$this->if_signal = false;
}
$this->running = false;
}
}
/**
* get sub process callback
*
* @return array|callable|null
*/
protected function getCallable()
{
$callback = null;
if (is_object($this->runnable) && $this->runnable instanceof Runnable) {
$callback = array($this->runnable, 'run');
} elseif (is_callable($this->runnable)) {
$callback = $this->runnable;
} else {
$callback = array($this, 'run');
}
return $callback;
}
/**
* register signal SIGTERM handler,
* when the parent process call shutdown and use the default signal,
* this handler will be triggered
*/
protected function signal()
{
pcntl_signal(SIGTERM, function () {
exit(0);
});
}
/**
* kill self
*
* @param bool|true $block
* @param int $signal
*/
public function shutdown($block = true, $signal = SIGTERM)
{
if (empty($this->pid)) {
throw new \LogicException('the process pid is null, so maybe the process is not started');
}
if (!$this->isRunning()) {
throw new \LogicException("the process is not running");
}
if (!posix_kill($this->pid, $signal)) {
throw new \RuntimeException("kill son process failed");
}
$this->updateStatus($block);
}
/**
* waiting for the sub process exit
*
* @param bool|true $block if block the process
* @param int $sleep default 0.1s check sub process status
* every $sleep milliseconds.
*/
public function wait($block = true, $sleep = 100000)
{
while (true) {
if ($this->isRunning() === false) {
return;
}
if (!$block) {
break;
}
usleep($sleep);
}
}
/**
* register sub process signal handler,
* when the sub process start, the handlers will be registered
*
* @param $signal
* @param callable $handler
*/
public function registerSignalHandler($signal, callable $handler)
{
$this->signal_handlers[$signal] = $handler;
}
/**
* after php-5.3.0, we can call pcntl_singal_dispatch to call signal handlers for pending signals
* which can save cpu resources than using declare(tick=n)
*
* @return bool
*/
public function dispatchSignal()
{
return pcntl_signal_dispatch();
}
/**
* you should overwrite this function
* if you do not use the Runnable or callback.
*/
public function run()
{
}
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/24 16:29
*/
namespace Jenner\SimpleFork\Queue;
class Pipe
{
/**
* @var resource
*/
protected $read;
/**
* @var resource
*/
protected $write;
/**
* @var string
*/
protected $filename;
/**
* @var bool
*/
protected $block;
/**
* @param string $filename fifo filename
* @param int $mode
* @param bool $block if blocking
*/
public function __construct($filename = '/tmp/simple-fork.pipe', $mode = 0666, $block = false)
{
if (!file_exists($filename) && !posix_mkfifo($filename, $mode)) {
throw new \RuntimeException('create pipe failed');
}
if (filetype($filename) != 'fifo') {
throw new \RuntimeException('file exists and it is not a fifo file');
}
$this->filename = $filename;
$this->block = $block;
}
public function setBlock($block = true)
{
if (is_resource($this->read)) {
$set = stream_set_blocking($this->read, $block);
if (!$set) {
throw new \RuntimeException('stream_set_blocking failed');
}
}
if (is_resource($this->write)) {
$set = stream_set_blocking($this->write, $block);
if (!$set) {
throw new \RuntimeException('stream_set_blocking failed');
}
}
$this->block = $block;
}
/**
* if the stream is blocking, you would better set the value of size,
* it will not return until the data size is equal to the value of param size
*
* @param int $size
* @return string
*/
public function read($size = 1024)
{
if (!is_resource($this->read)) {
$this->read = fopen($this->filename, 'r+');
if (!is_resource($this->read)) {
throw new \RuntimeException('open file failed');
}
if (!$this->block) {
$set = stream_set_blocking($this->read, false);
if (!$set) {
throw new \RuntimeException('stream_set_blocking failed');
}
}
}
return fread($this->read, $size);
}
/**
* @param $message
* @return int
*/
public function write($message)
{
if (!is_resource($this->write)) {
$this->write = fopen($this->filename, 'w+');
if (!is_resource($this->write)) {
throw new \RuntimeException('open file failed');
}
if (!$this->block) {
$set = stream_set_blocking($this->write, false);
if (!$set) {
throw new \RuntimeException('stream_set_blocking failed');
}
}
}
return fwrite($this->write, $message);
}
/**
*
*/
public function __destruct()
{
$this->close();
}
/**
*
*/
public function close()
{
if (is_resource($this->read)) {
fclose($this->read);
}
if (is_resource($this->write)) {
fclose($this->write);
}
}
public function remove()
{
return unlink($this->filename);
}
}

View File

@@ -0,0 +1,104 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/24 18:38
*/
namespace Jenner\SimpleFork\Queue;
class PipeQueue implements QueueInterface
{
/**
* @var Pipe
*/
protected $pipe;
/**
* @var bool
*/
protected $block;
/**
* @param string $filename fifo filename
* @param int $mode
* @param bool $block if blocking
*/
public function __construct($filename = '/tmp/simple-fork.pipe', $mode = 0666)
{
$this->pipe = new Pipe($filename, $mode);
$this->block = false;
$this->pipe->setBlock($this->block);
}
/**
* put value into the queue of channel
*
* @param $value
* @return bool
*/
public function put($value)
{
$len = strlen($value);
if ($len > 2147483647) {
throw new \RuntimeException('value is too long');
}
$raw = pack('N', $len) . $value;
$write_len = $this->pipe->write($raw);
return $write_len == strlen($raw);
}
/**
* get value from the queue of channel
*
* @param bool $block if block when the queue is empty
* @return bool|string
*/
public function get($block = false)
{
if ($this->block != $block) {
$this->pipe->setBlock($block);
$this->block = $block;
}
$len = $this->pipe->read(4);
if ($len === false) {
throw new \RuntimeException('read pipe failed');
}
if (strlen($len) === 0) {
return null;
}
$len = unpack('N', $len);
if (empty($len) || !array_key_exists(1, $len) || empty($len[1])) {
throw new \RuntimeException('data protocol error');
}
$len = intval($len[1]);
$value = '';
while (true) {
$temp = $this->pipe->read($len);
if (strlen($temp) == $len) {
return $temp;
}
$value .= $temp;
$len -= strlen($temp);
if ($len == 0) {
return $value;
}
}
}
/**
* remove the queue resource
*
* @return bool
*/
public function remove()
{
$this->pipe->close();
$this->pipe->remove();
}
}

View File

@@ -0,0 +1,34 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:11
*/
namespace Jenner\SimpleFork\Queue;
/**
* queue for processes to transfer data
*
* @package Jenner\SimpleFork\Queue
*/
interface QueueInterface
{
/**
* put value into the queue of channel
*
* @param $value
* @return bool
*/
public function put($value);
/**
* get value from the queue of channel
*
* @param bool $block if block when the queue is empty
* @return bool|string
*/
public function get($block = false);
}

View File

@@ -0,0 +1,144 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/20
* Time: 15:03
*/
namespace Jenner\SimpleFork\Queue;
/**
* redis queue
*
* @package Jenner\SimpleFork\Queue
*/
class RedisQueue implements QueueInterface
{
/**
* @var \Redis
*/
protected $redis;
/**
* @var string redis key of queue
*/
protected $channel;
/**
* @param string $host redis server host
* @param int $port redis server port
* @param int $database redis server database num
* @param string $channel redis queue key
* @param string $prefix prefix of redis queue key
*/
public function __construct(
$host = '127.0.0.1',
$port = 6379,
$database = 0,
$channel = 'cache',
$prefix = 'simple-fork-'
)
{
$this->redis = new \Redis();
$connection_result = $this->redis->connect($host, $port);
if (!$connection_result) {
throw new \RuntimeException('can not connect to the redis server');
}
if ($database != 0) {
$select_result = $this->redis->select($database);
if (!$select_result) {
throw new \RuntimeException('can not select the database');
}
}
if (empty($channel)) {
throw new \InvalidArgumentException('channel can not be empty');
}
$this->channel = $channel;
if (empty($prefix)) return;
$set_option_result = $this->redis->setOption(\Redis::OPT_PREFIX, $prefix);
if (!$set_option_result) {
throw new \RuntimeException('can not set the \Redis::OPT_PREFIX Option');
}
}
/**
* put value into the queue
*
* @param $value
* @return bool
*/
public function put($value)
{
if ($this->redis->lPush($this->channel, $value) !== false) {
return true;
}
return false;
}
/**
* get value from the queue
*
* @param bool $block if block when the queue is empty
* @return bool|string
*/
public function get($block = false)
{
if (!$block) {
return $this->redis->rPop($this->channel);
} else {
while (true) {
$record = $this->redis->rPop($this->channel);
if ($record === false) {
usleep(1000);
continue;
}
return $record;
}
}
}
/**
* get the size of the queue
*
* @return int
*/
public function size()
{
return $this->redis->lSize($this->channel);
}
/**
* remove the queue resource
*
* @return mixed
*/
public function remove()
{
return $this->redis->delete($this->channel);
}
/**
* close the connection
*/
public function __destruct()
{
$this->close();
}
/**
* close the connection
*/
public function close()
{
$this->redis->close();
}
}

View File

@@ -0,0 +1,293 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:15
*/
namespace Jenner\SimpleFork\Queue;
/**
* system v message queue
*
* @package Jenner\SimpleFork\Queue
*/
class SystemVMessageQueue implements QueueInterface
{
/**
* @var int channel
*/
protected $msg_type;
/**
* @var
*/
protected $queue;
/**
* @var bool
*/
protected $serialize_needed;
/**
* @var bool
*/
protected $block_send;
/**
* @var int
*/
protected $option_receive;
/**
* @var int
*/
protected $maxsize;
/**
* @var
*/
protected $key_t;
/**
* @var string
*/
protected $ipc_filename;
/**
* @param string $ipc_filename ipc file to make ipc key.
* if it does not exists, it will try to create the file.
* @param int $channel message type
* @param bool $serialize_needed serialize or not
* @param bool $block_send if block when the queue is full
* @param int $option_receive if the value is MSG_IPC_NOWAIT it will not
* going to wait a message coming. if the value is null,
* it will block and wait a message
* @param int $maxsize the max size of queue
*/
public function __construct(
$ipc_filename = __FILE__,
$channel = 1,
$serialize_needed = true,
$block_send = true,
$option_receive = MSG_IPC_NOWAIT,
$maxsize = 100000
)
{
$this->ipc_filename = $ipc_filename;
$this->msg_type = $channel;
$this->serialize_needed = $serialize_needed;
$this->block_send = $block_send;
$this->option_receive = $option_receive;
$this->maxsize = $maxsize;
$this->initQueue($ipc_filename, $channel);
}
/**
* init queue
*
* @param $ipc_filename
* @param $msg_type
* @throws \Exception
*/
protected function initQueue($ipc_filename, $msg_type)
{
$this->key_t = $this->getIpcKey($ipc_filename, $msg_type);
$this->queue = \msg_get_queue($this->key_t);
if (!$this->queue) throw new \RuntimeException('msg_get_queue failed');
}
/**
* @param $ipc_filename
* @param $msg_type
* @throws \Exception
* @return int
*/
public function getIpcKey($ipc_filename, $msg_type)
{
if (!file_exists($ipc_filename)) {
$create_file = touch($ipc_filename);
if ($create_file === false) {
throw new \RuntimeException('ipc_file is not exists and create failed');
}
}
$key_t = \ftok($ipc_filename, $msg_type);
if ($key_t == 0) throw new \RuntimeException('ftok error');
return $key_t;
}
/**
* get message
*
* @param bool $block if block when the queue is empty
* @return bool|string
*/
public function get($block = false)
{
$queue_status = $this->status();
if ($queue_status['msg_qnum'] > 0) {
$option_receive = $block ? 0 : $this->option_receive;
if (\msg_receive(
$this->queue,
$this->msg_type,
$msgtype_erhalten,
$this->maxsize,
$data,
$this->serialize_needed,
$option_receive,
$err
) === true
) {
return $data;
} else {
throw new \RuntimeException($err);
}
} else {
return false;
}
}
public function status()
{
$queue_status = \msg_stat_queue($this->queue);
return $queue_status;
}
/*
* return array's keys
* msg_perm.uid The uid of the owner of the queue.
* msg_perm.gid The gid of the owner of the queue.
* msg_perm.mode The file access mode of the queue.
* msg_stime The time that the last message was sent to the queue.
* msg_rtime The time that the last message was received from the queue.
* msg_ctime The time that the queue was last changed.
* msg_qnum The number of messages waiting to be read from the queue.
* msg_qbytes The maximum number of bytes allowed in one message queue.
* On Linux, this value may be read and modified via /proc/sys/kernel/msgmnb.
* msg_lspid The pid of the process that sent the last message to the queue.
* msg_lrpid The pid of the process that received the last message from the queue.
*
* @return array
*/
/**
* put message
*
* @param $message
* @return bool
* @throws \Exception
*/
public function put($message)
{
if (!\msg_send($this->queue, $this->msg_type, $message, $this->serialize_needed, $this->block_send, $err) === true) {
throw new \RuntimeException($err);
}
return true;
}
/**
* get the size of queue
*
* @return mixed
*/
public function size()
{
$status = $this->status();
return $status['msg_qnum'];
}
/**
* allows you to change the values of the msg_perm.uid,
* msg_perm.gid, msg_perm.mode and msg_qbytes fields of the underlying message queue data structure
*
* @param string $key status key
* @param int $value status value
* @return bool
*/
public function setStatus($key, $value)
{
$this->checkSetPrivilege($key);
if ($key == 'msg_qbytes')
return $this->setMaxQueueSize($value);
$queue_status[$key] = $value;
return \msg_set_queue($this->queue, $queue_status);
}
/**
* check the privilege of update the queue's status
*
* @param $key
* @throws \Exception
*/
private function checkSetPrivilege($key)
{
$privilege_field = array('msg_perm.uid', 'msg_perm.gid', 'msg_perm.mode');
if (!\in_array($key, $privilege_field)) {
$message = 'you can only change msg_perm.uid, msg_perm.gid, ' .
' msg_perm.mode and msg_qbytes. And msg_qbytes needs root privileges';
throw new \RuntimeException($message);
}
}
/**
* update the max size of queue
* need root
*
* @param $size
* @throws \Exception
* @return bool
*/
public function setMaxQueueSize($size)
{
$user = \get_current_user();
if ($user !== 'root')
throw new \Exception('changing msg_qbytes needs root privileges');
return $this->setStatus('msg_qbytes', $size);
}
/**
* remove queue
*
* @return bool
*/
public function remove()
{
return \msg_remove_queue($this->queue);
}
/**
* check if the queue is exists or not
*
* @param $key
* @return bool
*/
public function queueExists($key)
{
return \msg_queue_exists($key);
}
/**
* init when wakeup
*/
public function __wakeup()
{
$this->initQueue($this->ipc_filename, $this->msg_type);
}
/**
*
*/
public function __destruct()
{
unset($this);
}
}

View File

@@ -0,0 +1,20 @@
<?php
/**
* Created by PhpStorm.
* User: Jenner
* Date: 2015/8/12
* Time: 15:28
*/
namespace Jenner\SimpleFork;
interface Runnable
{
/**
* process entry
*
* @return mixed
*/
public function run();
}

View File

@@ -0,0 +1,26 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @blog http://www.huyanping.cn
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/19 21:13
*/
namespace Jenner\SimpleFork;
/**
* Only one process will be started at the same time
*
* @package Jenner\SimpleFork
*/
class SinglePool extends FixedPool
{
/**
* SinglePool constructor.
*/
public function __construct()
{
parent::__construct(1);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* @author Jenner <hypxm@qq.com>
* @license https://opensource.org/licenses/MIT MIT
* @datetime: 2015/11/11 17:50
*/
namespace Jenner\SimpleFork;
class Utils
{
/**
* check if the sub class of Process has overwrite the run method
*
* @param $child_class
*/
public static function checkOverwriteRunMethod($child_class)
{
$parent_class = '\\Jenner\\SimpleFork\\Process';
if ($child_class == $parent_class) {
$message = "you should extend the `{$parent_class}`" .
' and overwrite the run method';
throw new \RuntimeException($message);
}
$child = new \ReflectionClass($child_class);
if ($child->getParentClass() === false) {
$message = "you should extend the `{$parent_class}`" .
' and overwrite the run method';
throw new \RuntimeException($message);
}
$parent_methods = $child->getParentClass()->getMethods(\ReflectionMethod::IS_PUBLIC);
foreach ($parent_methods as $parent_method) {
if ($parent_method->getName() !== 'run') continue;
$declaring_class = $child->getMethod($parent_method->getName())
->getDeclaringClass()
->getName();
if ($declaring_class === $parent_class) {
throw new \RuntimeException('you must overwrite the run method');
}
}
}
}

View File

@@ -0,0 +1,19 @@
Copyright (c) 2011 Michael Dowling <mtdowling@gmail.com> and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Jenner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,54 @@
<?php
namespace addons\crontab\model;
use think\Model;
class Crontab extends Model
{
// 开启自动写入时间戳字段
protected $autoWriteTimestamp = 'integer';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = 'updatetime';
// 定义字段类型
protected $type = [
];
// 追加属性
protected $append = [
'type_text'
];
public static function getTypeList()
{
return [
'url' => __('Request Url'),
'sql' => __('Execute Sql Script'),
'shell' => __('Execute Shell'),
];
}
public function getTypeTextAttr($value, $data)
{
$typelist = self::getTypeList();
$value = $value ? $value : $data['type'];
return $value && isset($typelist[$value]) ? $typelist[$value] : $value;
}
protected function setBegintimeAttr($value)
{
return $value && !is_numeric($value) ? strtotime($value) : $value;
}
protected function setEndtimeAttr($value)
{
return $value && !is_numeric($value) ? strtotime($value) : $value;
}
protected function setExecutetimeAttr($value)
{
return $value && !is_numeric($value) ? strtotime($value) : $value;
}
}