├── 1631693468455_.pic.jpg ├── 1641693468493_.pic.jpg ├── src ├── Mutex │ ├── ServerMutex.php │ ├── TaskMutex.php │ ├── RedisTaskMutex.php │ └── RedisServerMutex.php ├── config │ └── plugin │ │ └── yzh52521 │ │ └── task │ │ ├── app.php │ │ └── process.php ├── Client.php ├── AsyncTask.php ├── util │ └── MacAddress.php ├── Install.php └── Server.php ├── composer.json └── README.md /1631693468455_.pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanzhihai/webman-task/HEAD/1631693468455_.pic.jpg -------------------------------------------------------------------------------- /1641693468493_.pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yuanzhihai/webman-task/HEAD/1641693468493_.pic.jpg -------------------------------------------------------------------------------- /src/Mutex/ServerMutex.php: -------------------------------------------------------------------------------- 1 | true, 4 | 'task' => [ 5 | 'listen' => '0.0.0.0:2345', 6 | 'async_listen' => '0.0.0.0:2346', 7 | 'crontab_table' => 'th_system_crontab', //任务计划表 8 | 'crontab_table_log' => 'th_system_crontab_log',//任务计划流水表 9 | 'debug' => true, //控制台输出日志 10 | 'write_log' => true,// 任务计划日志 11 | 'runInBackground' => false //命令行任务是否后台运行 12 | ], 13 | ]; 14 | -------------------------------------------------------------------------------- /src/config/plugin/yzh52521/task/process.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'handler' => Server::class, 9 | 'listen' => 'text://' . config('plugin.yzh52521.task.app.task.listen'), // 这里用了text协议,也可以用frame或其它协议 10 | 'count' => 1, // 支持多进程 同时只有一个进程在运行 11 | ], 12 | //定时任务异步处理worker 13 | 'cron_async_worker' => [ 14 | 'listen' => 'tcp://' . config('plugin.yzh52521.task.app.task.async_listen'), 15 | 'handler' => AsyncTask::class, 16 | 'count' => 10 17 | ] 18 | ]; -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yzh52521/webman-task", 3 | "description": " dynamic crontab task plugin for webman. ", 4 | "keywords": ["webman", "crontab", "task"], 5 | "type": "library", 6 | "require": { 7 | "php": ">=7.2.5", 8 | "ext-json": "*" 9 | }, 10 | "require-dev":{ 11 | "workerman/crontab":"^1.0", 12 | "webman/think-orm":"^1.0", 13 | "guzzlehttp/guzzle": "^7.0" 14 | }, 15 | "license": "MIT", 16 | "autoload": { 17 | "psr-4": { 18 | "yzh52521\\Task\\": "src/" 19 | } 20 | }, 21 | "authors": [ 22 | { 23 | "name": "yzh52521", 24 | "email": "396751927@qq.com" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/Mutex/TaskMutex.php: -------------------------------------------------------------------------------- 1 | client = stream_socket_client('tcp://' . config('plugin.yzh52521.task.app.task.listen')); 15 | } 16 | 17 | public static function instance() 18 | { 19 | if (!static::$instance) { 20 | static::$instance = new static(); 21 | } 22 | return static::$instance; 23 | } 24 | 25 | /** 26 | * @param array $param 27 | * @return mixed 28 | */ 29 | public function request(array $param) 30 | { 31 | fwrite($this->client, json_encode($param) . "\n"); // text协议末尾有个换行符"\n" 32 | $result = fgets($this->client, 10240000); 33 | return json_decode($result); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /src/AsyncTask.php: -------------------------------------------------------------------------------- 1 | getMessage(); 27 | } 28 | } else { 29 | $code = 1; 30 | $res = "方法或类不存在或者错误"; 31 | } 32 | $connection->send(json_encode(['code' => $code, 'msg' => $res])); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Mutex/RedisTaskMutex.php: -------------------------------------------------------------------------------- 1 | redisFactory = $redisFactory; 20 | } 21 | 22 | private function getMutexExpires() 23 | { 24 | return $this->mutexExpires; 25 | } 26 | 27 | /** 28 | * Attempt to obtain a task mutex for the given crontab. 29 | * @param $crontab 30 | * @return bool 31 | */ 32 | public function create($crontab): bool 33 | { 34 | return (bool)$this->redisFactory::set( 35 | $this->getMutexName($crontab), 36 | $crontab['title'], 37 | 'EX', $this->getMutexExpires(), 'NX' 38 | ); 39 | } 40 | 41 | /** 42 | * Determine if a task mutex exists for the given crontab. 43 | * @param $crontab 44 | * @return bool 45 | */ 46 | public function exists($crontab): bool 47 | { 48 | return (bool)$this->redisFactory::exists( 49 | $this->getMutexName($crontab) 50 | ); 51 | } 52 | 53 | /** 54 | * Clear the task mutex for the given crontab. 55 | * @param $crontab 56 | */ 57 | public function remove($crontab) 58 | { 59 | $this->redisFactory::del( 60 | $this->getMutexName($crontab) 61 | ); 62 | } 63 | 64 | protected function getMutexName($crontab): string 65 | { 66 | return 'framework' . DIRECTORY_SEPARATOR . 'crontab-' . sha1($crontab['title'] . $crontab['rule']); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/util/MacAddress.php: -------------------------------------------------------------------------------- 1 | forLinux(); 17 | break; 18 | case "unix": 19 | case "aix": 20 | case "solaris" : 21 | break; 22 | default : 23 | $this->forWindows(); 24 | break; 25 | } 26 | $temp_array = []; 27 | foreach ($this->return_array as $value) { 28 | if (preg_match("/[0-9a-f][0-9a-f][:-]" . "[0-9a-f][0-9a-f][:-]" . "[0-9a-f][0-9a-f][:-]" . "[0-9a-f][0-9a-f][:-]" . "[0-9a-f][0-9a-f][:-]" . "[0-9a-f][0-9a-f]/i", $value, $temp_array)) { 29 | $this->mac = $temp_array [0]; 30 | break; 31 | } 32 | } 33 | unset ($temp_array); 34 | return $this->mac; 35 | } 36 | 37 | private function forWindows() 38 | { 39 | @exec("ipconfig /all", $this->return_array); 40 | if ($this->return_array) 41 | return $this->return_array; 42 | else { 43 | $ipconfig = $_SERVER["WINDIR"] . "\system32\ipconfig.exe"; 44 | if (is_file($ipconfig)) 45 | @exec($ipconfig . " /all", $this->return_array); 46 | else 47 | @exec($_SERVER["WINDIR"] . "\system\ipconfig.exe /all", $this->return_array); 48 | return $this->return_array; 49 | } 50 | } 51 | 52 | 53 | private function forLinux() 54 | { 55 | @exec("ifconfig -a", $this->return_array); 56 | return $this->return_array; 57 | } 58 | } -------------------------------------------------------------------------------- /src/Install.php: -------------------------------------------------------------------------------- 1 | 'config/plugin/yzh52521/task', 14 | ); 15 | 16 | /** 17 | * Install 18 | * @return void 19 | */ 20 | public static function install() 21 | { 22 | static::installByRelation(); 23 | } 24 | 25 | /** 26 | * Uninstall 27 | * @return void 28 | */ 29 | public static function uninstall() 30 | { 31 | self::uninstallByRelation(); 32 | } 33 | 34 | /** 35 | * installByRelation 36 | * @return void 37 | */ 38 | public static function installByRelation() 39 | { 40 | foreach (static::$pathRelation as $source => $dest) { 41 | if ($pos = strrpos($dest, '/')) { 42 | $parent_dir = base_path().'/'.substr($dest, 0, $pos); 43 | if (!is_dir($parent_dir)) { 44 | mkdir($parent_dir, 0777, true); 45 | } 46 | } 47 | //symlink(__DIR__ . "/$source", base_path()."/$dest"); 48 | copy_dir(__DIR__ . "/$source", base_path()."/$dest"); 49 | echo "Create $dest 50 | "; 51 | } 52 | } 53 | 54 | /** 55 | * uninstallByRelation 56 | * @return void 57 | */ 58 | public static function uninstallByRelation() 59 | { 60 | foreach (static::$pathRelation as $source => $dest) { 61 | $path = base_path()."/$dest"; 62 | if (!is_dir($path) && !is_file($path)) { 63 | continue; 64 | } 65 | echo "Remove $dest 66 | "; 67 | if (is_file($path) || is_link($path)) { 68 | unlink($path); 69 | continue; 70 | } 71 | remove_dir($path); 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/Mutex/RedisServerMutex.php: -------------------------------------------------------------------------------- 1 | redisFactory = $redisFactory; 27 | 28 | $this->macAddress = $this->getMacAddress(); 29 | } 30 | 31 | private function getMutexExpires() 32 | { 33 | return $this->mutexExpires; 34 | } 35 | 36 | 37 | /** 38 | * Attempt to obtain a server mutex for the given crontab. 39 | */ 40 | public function attempt($crontab): bool 41 | { 42 | $result = (bool)$this->redisFactory::set( 43 | $this->getMutexName($crontab), 44 | $this->macAddress, 'EX', $this->getMutexExpires(), 'NX' 45 | ); 46 | 47 | if ($result === true) { 48 | return true; 49 | } 50 | return $this->redisFactory::get($this->getMutexName($crontab)) === $this->macAddress; 51 | } 52 | 53 | /** 54 | * Get the task mutex for the given crontab. 55 | */ 56 | public function get($crontab): string 57 | { 58 | return (string)$this->redisFactory::get( 59 | $this->getMutexName($crontab) 60 | ); 61 | } 62 | 63 | protected function getMutexName($crontab): string 64 | { 65 | return 'webman' . DIRECTORY_SEPARATOR . 'crontab-' . sha1($crontab['title'] . $crontab['rule']) . '-sv'; 66 | } 67 | 68 | 69 | protected function getMacAddress(): ?string 70 | { 71 | $macAddresses = (new MacAddress())->Local_Mac_Address(); 72 | foreach (Arr::wrap($macAddresses) as $name => $address) { 73 | if ($address && $address !== '00:00:00:00:00:00') { 74 | return $name . ':' . str_replace(':', '', $address); 75 | } 76 | } 77 | 78 | return null; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 动态秒级定时任务 2 | 3 | ## 概述 4 | 5 | 基于 **webman** + 的动态秒级定时任务管理,兼容 Windows 和 Linux 系统。 6 | 7 | 使用`tp-orm` 8 | 9 | ```shell 10 | composer require yzh52521/webman-task 11 | ``` 12 | 使用`laravel orm` 13 | ```shell 14 | composer require yzh52521/webman-task dev-lv 15 | ``` 16 | 17 | ## 简单使用 18 | 19 | ``` 20 | $param = [ 21 | 'method' => 'crontabIndex',//计划任务列表 22 | 'args' => ['limit' => 10, 'page' => 1]//参数 23 | ]; 24 | $result= yzh52521\Task\Client::instance()->request($param); 25 | return json($result); 26 | 27 | ``` 28 | 29 | ## 计划任务列表 30 | 31 | ### 方法名 32 | 33 | **method:** crontabIndex 34 | 35 | ### 请求参数 36 | 37 | **args** 38 | 39 | | 参数名称 | 是否必须 | 示例 | 备注 | 40 | | -------- | -------- | ------------------------ | ------------ | 41 | | page | 是 | 1 | 页码 | 42 | | limit | 是 | 15 | 每页条数 | 43 | 44 | ### 返回数据 45 | 46 | ```json 47 | { 48 | "code": 200, 49 | "msg": "ok", 50 | "data": { 51 | "total": 4, 52 | "per_page": 15, 53 | "current_page": 1, 54 | "last_page": 1, 55 | "data": [ 56 | { 57 | "id": 6, 58 | "title": "class任务 每月1号清理所有日志", 59 | "type": 2, 60 | "rule": "0 0 1 * *", 61 | "target": "app\\common\\crontab\\ClearLogCrontab", 62 | "parameter": "", 63 | "running_times": 71, 64 | "last_running_time": 1651121710, 65 | "remark": "", 66 | "sort": 0, 67 | "status": 1, 68 | "create_time": 1651114277, 69 | "update_time": 1651114277, 70 | "singleton": 1 71 | }, 72 | { 73 | "id": 5, 74 | "title": "eavl任务 输出 hello world", 75 | "type": 4, 76 | "rule": "* * * * *", 77 | "target": "echo 'hello world';", 78 | "parameter": "", 79 | "running_times": 25, 80 | "last_running_time": 1651121701, 81 | "remark": "", 82 | "sort": 0, 83 | "status": 1, 84 | "create_time": 1651113561, 85 | "update_time": 1651113561, 86 | "singleton": 0 87 | }, 88 | { 89 | "id": 3, 90 | "title": "url任务 打开 workerman 网站", 91 | "type": 3, 92 | "rule": "*/20 * * * * *", 93 | "target": "https://www.workerman.net/", 94 | "parameter": "", 95 | "running_times": 39, 96 | "last_running_time": 1651121700, 97 | "remark": "请求workerman网站", 98 | "sort": 0, 99 | "status": 1, 100 | "create_time": 1651112925, 101 | "update_time": 1651112925, 102 | "singleton": 0 103 | }, 104 | { 105 | "id": 1, 106 | "title": "command任务 输出 webman 版本", 107 | "type": 1, 108 | "rule": "*/20 * * * * *", 109 | "target": "version", 110 | "parameter": null, 111 | "running_times": 112, 112 | "last_running_time": 1651121700, 113 | "remark": "20秒", 114 | "sort": 0, 115 | "status": 1, 116 | "create_time": 1651047480, 117 | "update_time": 1651047480, 118 | "singleton": 1 119 | } 120 | ] 121 | } 122 | } 123 | ``` 124 | 125 | ## 计划任务日志列表 126 | 127 | **method:** crontabLog 128 | 129 | ### 请求参数 130 | 131 | **args** 132 | 133 | | 参数名称 | 是否必须 | 示例 | 备注 | 134 | |------------| -------- | ----------- | ------------ | 135 | | page | 是 | 1 | 页码 | 136 | | limit | 是 | 15 | 每页条数 | 137 | | crontab_id | 否 | 1 | 计划任务ID | 138 | 139 | ### 返回数据 140 | 141 | ```json 142 | 143 | { 144 | "code": 200, 145 | "msg": "ok", 146 | "data": { 147 | "total": 97, 148 | "per_page": 15, 149 | "current_page": 1, 150 | "last_page": 7, 151 | "data": [ 152 | { 153 | "id": 257, 154 | "crontab_id": 1, 155 | "target": "version", 156 | "parameter": "", 157 | "exception": "Webman-framework v1.3.11", 158 | "return_code": 0, 159 | "running_time": "0.834571", 160 | "create_time": 1651123800, 161 | "update_time": 1651123800 162 | }, 163 | { 164 | "id": 251, 165 | "crontab_id": 1, 166 | "target": "version", 167 | "parameter": "", 168 | "exception": "Webman-framework v1.3.11", 169 | "return_code": 0, 170 | "running_time": "0.540384", 171 | "create_time": 1651121700, 172 | "update_time": 1651121700 173 | }, 174 | { 175 | "id": 246, 176 | "crontab_id": 1, 177 | "target": "version", 178 | "parameter": "{}", 179 | "exception": "Webman-framework v1.3.11", 180 | "return_code": 0, 181 | "running_time": "0.316019", 182 | "create_time": 1651121640, 183 | "update_time": 1651121640 184 | }, 185 | { 186 | "id": 244, 187 | "crontab_id": 1, 188 | "target": "version", 189 | "parameter": "{}", 190 | "exception": "Webman-framework v1.3.11", 191 | "return_code": 0, 192 | "running_time": "0.493848", 193 | "create_time": 1651121580, 194 | "update_time": 1651121580 195 | } 196 | ] 197 | } 198 | } 199 | 200 | ``` 201 | 202 | ## 添加任务 203 | 204 | **method:** crontabCreate 205 | 206 | ### 请求参数 207 | 208 | **args** 209 | 210 | | 参数名称 | 参数类型 | 是否必须 | 示例 | 备注 | 211 | |--------| -------- |-----|-----------------------------|---------------------------------------------------| 212 | | title | text | 是 | 输出 webman 版本 | 任务标题 | 213 | | type | text | 是 | 1 | 任务类型 (1 command, 2 class, 3 url, 4 eval ,5 shell) | 214 | | rule | text | 是 | */3 * * * * * | 任务执行表达式 | 215 | | target | text | 是 | version | 调用任务字符串 | 216 | | parameter | text | 否 | {} | 调用任务参数(url和eval无效) | 217 | | remark | text | 是 | 每3秒执行 | 备注 | 218 | | sort | text | 是 | 0 | 排序 | 219 | | status | text | 是 | 1 | 状态[0禁用; 1启用] | 220 | | singleton | text | 否 | 1 | 是否单次执行 [0 是 1 不是] | 221 | 222 | 223 | ### 返回数据 224 | 225 | ```json 226 | { 227 | "code": 200, 228 | "msg": "ok", 229 | "data": { 230 | 231 | } 232 | } 233 | ``` 234 | 235 | ## 重启任务 236 | 237 | **method:** crontabReload 238 | 239 | ### 请求参数 240 | 241 | **args** 242 | 243 | | 参数名称 | 参数类型 | 是否必须 | 示例 | 备注 | 244 | | -------- | -------- | -------- | ---- | ---- | 245 | | id | text | 是 | 1,2 | 计划任务ID 多个逗号隔开 | 246 | 247 | ### 返回数据 248 | 249 | ```json 250 | { 251 | "code": 200, 252 | "msg": "ok", 253 | "data": { 254 | 255 | } 256 | } 257 | ``` 258 | ## 修改任务 259 | 260 | **method:** crontabUpdate 261 | 262 | ### 请求参数 263 | 264 | **args** 265 | 266 | | 参数名称 | 参数类型 | 是否必须 | 示例 | 备注 | 267 | |--------| -------- |------|----------------------------|--------------------------------------------------| 268 | | id | text | 是 | 1 | | 269 | | title | text | 否 | 输出 webman 版本 | 任务标题 | 270 | | type | text | 否 | 1 | 任务类型 (1 command, 2 class, 3 url, 4 eval,5 shell) | 271 | | rule | text | 否 | */3 * * * * * | 任务执行表达式 | 272 | | target | text | 否 | version | 调用任务字符串 | 273 | | parameter | text | 否 | {} | 调用任务参数(url和eval无效) | 274 | | remark | text | 否 | 每3秒执行 | 备注 | 275 | | sort | text | 否 | 0 | 排序 | 276 | | status | text | 否 | 1 | 状态[0禁用; 1启用] | 277 | | singleton | text | 否 | 1 | 是否单次执行 [0 是 1 不是] | 278 | 279 | ### 返回数据 280 | 281 | ```json 282 | 283 | { 284 | "code": 200, 285 | "msg": "ok", 286 | "data": { 287 | 288 | } 289 | } 290 | ``` 291 | 292 | ## 删除任务 293 | 294 | **method:** crontabDelete 295 | 296 | ### 请求参数 297 | 298 | **args** 299 | 300 | | 参数名称 | 参数类型 | 是否必须 | 示例 | 备注 | 301 | | -------- | -------- | -------- | ---- | ---- | 302 | | id | text | 是 | 1,2 | 计划任务ID 多个逗号隔开 | 303 | 304 | ### 返回数据 305 | 306 | ``` 307 | { 308 | "code": 200, 309 | "msg": "ok", 310 | "data": { 311 | 312 | } 313 | } 314 | ``` 315 | 316 | ## 支持我 317 | 您的认可是我继续前行的动力,如果您觉得webman-task对您有帮助,请支持我,谢谢您! 318 | * 方式一: 点击右上角`⭐Star`按钮 319 | * 方式二: 扫描下方二维码,打赏我 320 |
321 | 扫码打赏我扫码打赏我
322 | 323 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | delTaskMutex(); 96 | } 97 | 98 | 99 | public function onWorkerStart(Worker $worker) 100 | { 101 | $config = config('plugin.yzh52521.task.app.task'); 102 | $this->debug = $config['debug'] ?? true; 103 | $this->writeLog = $config['write_log'] ?? true; 104 | $this->crontabTable = $config['crontab_table']; 105 | $this->crontabLogTable = $config['crontab_table_log']; 106 | $this->runInBackground = $config['runInBackground'] ?? false; 107 | $this->worker = $worker; 108 | 109 | $this->checkCrontabTables(); 110 | $this->crontabInit(); 111 | } 112 | 113 | /** 114 | * 当客户端与Workman建立连接时(TCP三次握手完成后)触发的回调函数 115 | * 每个连接只会触发一次onConnect回调 116 | * 此时客户端还没有发来任何数据 117 | * 由于udp是无连接的,所以当使用udp时不会触发onConnect回调,也不会触发onClose回调 118 | * @param TcpConnection $connection 119 | */ 120 | public function onConnect(TcpConnection $connection) 121 | { 122 | 123 | } 124 | 125 | 126 | public function onMessage(TcpConnection $connection, $data) 127 | { 128 | $data = json_decode($data, true); 129 | $method = $data['method']; 130 | $args = $data['args']; 131 | $connection->send(call_user_func([$this, $method], $args)); 132 | } 133 | 134 | 135 | /** 136 | * 定时器列表 137 | * @param array $data 138 | * @return false|string 139 | */ 140 | private function crontabIndex(array $data) 141 | { 142 | $limit = $data['limit'] ?? 15; 143 | $page = $data['page'] ?? 1; 144 | $where = $data['where'] ?? []; 145 | $data = Db::table($this->crontabTable) 146 | ->where($where) 147 | ->order('id', 'desc') 148 | ->paginate(['list_rows' => $limit, 'page' => $page]); 149 | 150 | return json_encode(['code' => 200, 'msg' => 'ok', 'data' => $data]); 151 | } 152 | 153 | /** 154 | * 初始化定时任务 155 | * @return void 156 | */ 157 | private function crontabInit(): void 158 | { 159 | $ids = Db::table($this->crontabTable) 160 | ->where('status', self::NORMAL_STATUS) 161 | ->order('sort', 'desc') 162 | ->column('id'); 163 | if (!empty($ids)) { 164 | foreach ($ids as $id) { 165 | $this->crontabRun($id); 166 | } 167 | } 168 | } 169 | 170 | /** 171 | * 创建定时器 172 | * @param $id 173 | */ 174 | private function crontabRun($id) 175 | { 176 | $data = Db::table($this->crontabTable) 177 | ->where('id', $id) 178 | ->where('status', self::NORMAL_STATUS) 179 | ->find(); 180 | 181 | if (!empty($data)) { 182 | switch ($data['type']) { 183 | case self::COMMAND_CRONTAB: 184 | if ($this->decorateRunnable($data)) { 185 | $this->crontabPool[$data['id']] = [ 186 | 'id' => $data['id'], 187 | 'target' => $data['target'], 188 | 'rule' => $data['rule'], 189 | 'parameter' => $data['parameter'], 190 | 'singleton' => $data['singleton'], 191 | 'create_time' => date('Y-m-d H:i:s'), 192 | 'crontab' => new Crontab($data['rule'], function () use ($data) { 193 | $time = time(); 194 | $parameter = $data['parameter'] ?: ''; 195 | $startTime = microtime(true); 196 | $code = 0; 197 | $result = true; 198 | try { 199 | $parameters = !empty($data['parameter']) ? json_decode($data['parameter'], true) : []; 200 | $compiled = $data['target']; 201 | foreach ($parameters as $key => $value) { 202 | $compiled .= ' ' . escapeshellarg($key); 203 | if ($value !== null) { 204 | $compiled .= ' ' . escapeshellarg($value); 205 | } 206 | } 207 | if ($this->runInBackground) { 208 | // Parentheses are need execute the chain of commands in a subshell 209 | // that can then run in background 210 | $compiled = $compiled . ' > /dev/null 2>&1 &'; 211 | } 212 | 213 | $command = PHP_BINARY . ' ' . self::WEBMAN_BINARY . ' ' . trim($compiled); 214 | exec($command, $output, $code); 215 | $exception = join(PHP_EOL, $output); 216 | 217 | } catch (\Throwable $e) { 218 | $result = false; 219 | $code = 1; 220 | $exception = $e->getMessage(); 221 | } finally { 222 | $taskMutex = $this->getTaskMutex(); 223 | $taskMutex->remove($data); 224 | } 225 | 226 | $this->debug && $this->writeln('执行定时器任务#' . $data['id'] . ' ' . $data['rule'] . ' ' . $data['target'], $result); 227 | 228 | $this->isSingleton($data); 229 | 230 | $endTime = microtime(true); 231 | $this->updateRunning($data['id'], $time); 232 | $this->writeLog && $this->crontabRunLog([ 233 | 'crontab_id' => $data['id'], 234 | 'target' => $data['target'], 235 | 'parameter' => $parameter, 236 | 'exception' => $exception, 237 | 'return_code' => $code, 238 | 'running_time' => round($endTime - $startTime, 6), 239 | 'create_time' => $time, 240 | 'update_time' => $time, 241 | ]); 242 | 243 | }) 244 | ]; 245 | } 246 | break; 247 | case self::CLASS_CRONTAB: 248 | if ($this->decorateRunnable($data)) { 249 | $this->crontabPool[$data['id']] = [ 250 | 'id' => $data['id'], 251 | 'target' => $data['target'], 252 | 'rule' => $data['rule'], 253 | 'parameter' => $data['parameter'], 254 | 'singleton' => $data['singleton'], 255 | 'create_time' => date('Y-m-d H:i:s'), 256 | 'crontab' => new Crontab($data['rule'], function () use ($data) { 257 | $time = time(); 258 | $class = trim($data['target']); 259 | $startTime = microtime(true); 260 | if ($class && strpos($class, '@') !== false) { 261 | $class = explode('@', $class); 262 | $method = end($class); 263 | array_pop($class); 264 | $class = implode('@', $class); 265 | } else { 266 | $method = 'execute'; 267 | } 268 | try { 269 | $code = 0; 270 | $result = true; 271 | $parameters = !empty($data['parameter']) ? json_decode($data['parameter'], true) : []; 272 | $this->delivery($class, $method, $parameters); 273 | } catch (\Throwable $throwable) { 274 | $result = false; 275 | $code = 1; 276 | } finally { 277 | $taskMutex = $this->getTaskMutex(); 278 | $taskMutex->remove($data); 279 | } 280 | 281 | $this->debug && $this->writeln('执行定时器任务#' . $data['id'] . ' ' . $data['rule'] . ' ' . $data['target'], $result); 282 | 283 | $this->isSingleton($data); 284 | 285 | $endTime = microtime(true); 286 | $this->updateRunning($data['id'], $time); 287 | $this->writeLog && $this->crontabRunLog([ 288 | 'crontab_id' => $data['id'], 289 | 'target' => $data['target'], 290 | 'parameter' => $data['parameter'] ?? '', 291 | 'exception' => $exception ?? '', 292 | 'return_code' => $code, 293 | 'running_time' => round($endTime - $startTime, 6), 294 | 'create_time' => $time, 295 | 'update_time' => $time, 296 | ]); 297 | 298 | }) 299 | ]; 300 | } 301 | break; 302 | case self::URL_CRONTAB: 303 | if ($this->decorateRunnable($data)) { 304 | $this->crontabPool[$data['id']] = [ 305 | 'id' => $data['id'], 306 | 'target' => $data['target'], 307 | 'rule' => $data['rule'], 308 | 'parameter' => $data['parameter'], 309 | 'singleton' => $data['singleton'], 310 | 'create_time' => date('Y-m-d H:i:s'), 311 | 'crontab' => new Crontab($data['rule'], function () use ($data) { 312 | $time = time(); 313 | $url = trim($data['target']); 314 | $startTime = microtime(true); 315 | $client = new \GuzzleHttp\Client(); 316 | try { 317 | $response = $client->get($url); 318 | $result = $response->getStatusCode() === 200; 319 | $code = 0; 320 | } catch (\Throwable $throwable) { 321 | $result = false; 322 | $code = 1; 323 | $exception = $throwable->getMessage(); 324 | } finally { 325 | $taskMutex = $this->getTaskMutex(); 326 | $taskMutex->remove($data); 327 | } 328 | 329 | $this->debug && $this->writeln('执行定时器任务#' . $data['id'] . ' ' . $data['rule'] . ' ' . $data['target'], $result); 330 | 331 | $this->isSingleton($data); 332 | 333 | $endTime = microtime(true); 334 | $this->updateRunning($data['id'], $time); 335 | $this->writeLog && $this->crontabRunLog([ 336 | 'crontab_id' => $data['id'], 337 | 'target' => $data['target'], 338 | 'parameter' => $data['parameter'], 339 | 'exception' => $exception ?? '', 340 | 'return_code' => $code, 341 | 'running_time' => round($endTime - $startTime, 6), 342 | 'create_time' => $time, 343 | 'update_time' => $time, 344 | ]); 345 | 346 | }) 347 | ]; 348 | } 349 | break; 350 | case self::SHELL_CRONTAB: 351 | if ($this->decorateRunnable($data)) { 352 | $this->crontabPool[$data['id']] = [ 353 | 'id' => $data['id'], 354 | 'target' => $data['target'], 355 | 'rule' => $data['rule'], 356 | 'parameter' => $data['parameter'], 357 | 'singleton' => $data['singleton'], 358 | 'create_time' => date('Y-m-d H:i:s'), 359 | 'crontab' => new Crontab($data['rule'], function () use ($data) { 360 | $time = time(); 361 | $parameter = $data['parameter'] ?: ''; 362 | $startTime = microtime(true); 363 | $code = 0; 364 | $result = true; 365 | try { 366 | $exception = shell_exec($data['target']); 367 | } catch (\Throwable $e) { 368 | $result = false; 369 | $code = 1; 370 | $exception = $e->getMessage(); 371 | } finally { 372 | $taskMutex = $this->getTaskMutex(); 373 | $taskMutex->remove($data); 374 | } 375 | 376 | $this->debug && $this->writeln('执行定时器任务#' . $data['id'] . ' ' . $data['rule'] . ' ' . $data['target'], $result); 377 | 378 | $this->isSingleton($data); 379 | 380 | $endTime = microtime(true); 381 | $this->updateRunning($data['id'], $time); 382 | $this->writeLog && $this->crontabRunLog([ 383 | 'crontab_id' => $data['id'], 384 | 'target' => $data['target'], 385 | 'parameter' => $parameter, 386 | 'exception' => $exception, 387 | 'return_code' => $code, 388 | 'running_time' => round($endTime - $startTime, 6), 389 | 'create_time' => $time, 390 | 'update_time' => $time, 391 | ]); 392 | 393 | }) 394 | ]; 395 | } 396 | break; 397 | case self::EVAL_CRONTAB: 398 | if ($this->decorateRunnable($data)) { 399 | $this->crontabPool[$data['id']] = [ 400 | 'id' => $data['id'], 401 | 'target' => $data['target'], 402 | 'rule' => $data['rule'], 403 | 'parameter' => $data['parameter'], 404 | 'singleton' => $data['singleton'], 405 | 'create_time' => date('Y-m-d H:i:s'), 406 | 'crontab' => new Crontab($data['rule'], function () use ($data) { 407 | $time = time(); 408 | $startTime = microtime(true); 409 | $result = true; 410 | $code = 0; 411 | try { 412 | eval($data['target']); 413 | } catch (\Throwable $throwable) { 414 | $result = false; 415 | $code = 1; 416 | $exception = $throwable->getMessage(); 417 | } finally { 418 | $taskMutex = $this->getTaskMutex(); 419 | $taskMutex->remove($data); 420 | } 421 | 422 | $this->debug && $this->writeln('执行定时器任务#' . $data['id'] . ' ' . $data['rule'] . ' ' . $data['target'], $result); 423 | 424 | $this->isSingleton($data); 425 | 426 | $endTime = microtime(true); 427 | $this->updateRunning($data['id'], $time); 428 | $this->writeLog && $this->crontabRunLog([ 429 | 'crontab_id' => $data['id'], 430 | 'target' => $data['target'], 431 | 'parameter' => $data['parameter'], 432 | 'exception' => $exception ?? '', 433 | 'return_code' => $code, 434 | 'running_time' => round($endTime - $startTime, 6), 435 | 'create_time' => $time, 436 | 'update_time' => $time, 437 | ]); 438 | 439 | }) 440 | ]; 441 | } 442 | break; 443 | } 444 | } 445 | } 446 | 447 | /** 448 | * 投递到异步进程 449 | * 450 | * @param string $class 451 | * @param string $method 452 | * @param array $parameter 453 | * @return void 454 | * @throws \Exception 455 | */ 456 | private function delivery(string $class, string $method, array $parameter): void 457 | { 458 | $taskConnection = new AsyncTcpConnection(config('plugin.yzh52521.task.app.task.async_listen')); 459 | $taskConnection->send(json_encode(['class' => $class, 'method' => $method, 'parameter' => $parameter])); 460 | $taskConnection->onMessage = function (AsyncTcpConnection $asyncTcpConnection, $taskResult) { 461 | if ($this->writeLog) { 462 | echo '异步返回值' . $taskResult . PHP_EOL; 463 | } 464 | $asyncTcpConnection->close(); 465 | }; 466 | $taskConnection->connect(); 467 | } 468 | 469 | /** 470 | * 更新运行次数/时间 471 | * @param $id 472 | * @param $time 473 | * @return void 474 | */ 475 | private function updateRunning($id, $time) 476 | { 477 | Db::query("UPDATE {$this->crontabTable} SET running_times = running_times + 1, last_running_time = {$time} WHERE id = {$id}"); 478 | } 479 | 480 | /** 481 | * 是否单次 482 | * @param $crontab 483 | * @return void 484 | */ 485 | private function isSingleton($crontab) 486 | { 487 | if ($crontab['singleton'] == 0 && isset($this->crontabPool[$crontab['id']])) { 488 | $this->debug && $this->writeln("定时器销毁", true); 489 | $this->crontabPool[$crontab['id']]['crontab']->destroy(); 490 | } 491 | } 492 | 493 | 494 | /** 495 | * 解决任务的并发执行问题,任务永远只会同时运行 1 个 496 | * @param $crontab 497 | * @return bool 498 | */ 499 | private function runInSingleton($crontab): bool 500 | { 501 | $taskMutex = $this->getTaskMutex(); 502 | if ($taskMutex->exists($crontab) || !$taskMutex->create($crontab)) { 503 | $this->debug && $this->writeln(sprintf('Crontab task [%s] skipped execution at %s.', $crontab['title'], date('Y-m-d H:i:s')), true); 504 | return false; 505 | } 506 | return true; 507 | } 508 | 509 | 510 | /** 511 | * 只能一个实例执行 512 | * @param $crontab 513 | * @return bool 514 | */ 515 | private function runOnOneServer($crontab): bool 516 | { 517 | $taskMutex = $this->getServerMutex(); 518 | if (!$taskMutex->attempt($crontab)) { 519 | $this->debug && $this->writeln(sprintf('Crontab task [%s] skipped execution at %s.', $crontab['title'], date('Y-m-d H:i:s')), true); 520 | return false; 521 | } 522 | return true; 523 | } 524 | 525 | protected function decorateRunnable($crontab): bool 526 | { 527 | if ($this->runInSingleton($crontab) && $this->runOnOneServer($crontab)) { 528 | return true; 529 | } 530 | return false; 531 | } 532 | 533 | private function getTaskMutex(): TaskMutex 534 | { 535 | if (!$this->taskMutex) { 536 | $this->taskMutex = Container::has(TaskMutex::class) 537 | ? Container::get(TaskMutex::class) 538 | : Container::get(RedisTaskMutex::class); 539 | } 540 | return $this->taskMutex; 541 | } 542 | 543 | private function getServerMutex(): ServerMutex 544 | { 545 | if (!$this->serverMutex) { 546 | $this->serverMutex = Container::has(ServerMutex::class) 547 | ? Container::get(ServerMutex::class) 548 | : Container::get(RedisServerMutex::class); 549 | } 550 | return $this->serverMutex; 551 | } 552 | 553 | /** 554 | * 记录执行日志 555 | * @param array $param 556 | * @return void 557 | */ 558 | private function crontabRunLog(array $param): void 559 | { 560 | Db::table($this->crontabLogTable)->insert($param); 561 | } 562 | 563 | /** 564 | * 创建定时任务 565 | * @param array $param 566 | * @return string 567 | */ 568 | private function crontabCreate(array $param): string 569 | { 570 | $param['create_time'] = $param['update_time'] = time(); 571 | $id = Db::table($this->crontabTable)->insertGetId($param); 572 | $id && $this->crontabRun($id); 573 | 574 | return json_encode(['code' => 200, 'msg' => 'ok', 'data' => ['crontab_id' => $id]]); 575 | } 576 | 577 | /** 578 | * 修改定时器 579 | * @param array $param 580 | * @return string 581 | */ 582 | private function crontabUpdate(array $param): string 583 | { 584 | $row = Db::table($this->crontabTable) 585 | ->where('id', $param['id']) 586 | ->update($param); 587 | 588 | if (isset($this->crontabPool[$param['id']])) { 589 | $this->crontabPool[$param['id']]['crontab']->destroy(); 590 | unset($this->crontabPool[$param['id']]); 591 | } 592 | if ($param['status'] == self::NORMAL_STATUS) { 593 | $this->crontabRun($param['id']); 594 | } 595 | 596 | return json_encode(['code' => 200, 'msg' => 'ok', 'data' => ['code' => (bool)$row]]); 597 | 598 | } 599 | 600 | 601 | /** 602 | * 清除定时任务 603 | * @param array $param 604 | * @return string 605 | */ 606 | private function crontabDelete(array $param): string 607 | { 608 | if ($id = $param['id']) { 609 | $ids = explode(',', (string)$id); 610 | 611 | foreach ($ids as $item) { 612 | if (isset($this->crontabPool[$item])) { 613 | $this->crontabPool[$item]['crontab']->destroy(); 614 | unset($this->crontabPool[$item]); 615 | } 616 | } 617 | 618 | $rows = Db::table($this->crontabTable) 619 | ->where('id in (' . $id . ')') 620 | ->delete(); 621 | 622 | return json_encode(['code' => 200, 'msg' => 'ok', 'data' => ['code' => (bool)$rows]]); 623 | } 624 | 625 | return json_encode(['code' => 200, 'msg' => 'ok', 'data' => ['code' => true]]); 626 | } 627 | 628 | /** 629 | * 重启定时任务 630 | * @param array $param 631 | * @return string 632 | */ 633 | private function crontabReload(array $param): string 634 | { 635 | $ids = explode(',', (string)$param['id']); 636 | 637 | foreach ($ids as $id) { 638 | if (isset($this->crontabPool[$id])) { 639 | $this->crontabPool[$id]['crontab']->destroy(); 640 | unset($this->crontabPool[$id]); 641 | } 642 | Db::table($this->crontabTable) 643 | ->where('id', $id) 644 | ->update(['status' => self::NORMAL_STATUS]); 645 | $this->crontabRun($id); 646 | } 647 | 648 | return json_encode(['code' => 200, 'msg' => 'ok', 'data' => ['code' => true]]); 649 | } 650 | 651 | 652 | /** 653 | * 执行日志列表 654 | * @param array $param 655 | * @return string 656 | */ 657 | private function crontabLog(array $param): string 658 | { 659 | $where = $param['where'] ?? []; 660 | $limit = $param['limit'] ?? 15; 661 | $page = $param['page'] ?? 1; 662 | $param['crontab_id'] && $where[] = ['crontab_id', '=', $param['crontab_id']]; 663 | 664 | $data = Db::table($this->crontabLogTable) 665 | ->where($where) 666 | ->order('id', 'desc') 667 | ->paginate(['list_rows' => $limit, 'page' => $page]); 668 | 669 | return json_encode(['code' => 200, 'msg' => 'ok', 'data' => $data]); 670 | } 671 | 672 | /** 673 | * 输出日志 674 | * @param $msg 675 | * @param bool $isSuccess 676 | */ 677 | private function writeln($msg, bool $isSuccess) 678 | { 679 | echo 'worker:' . $this->worker->id . ' [' . date('Y-m-d H:i:s') . '] ' . $msg . ($isSuccess ? " [Ok] " : " [Fail] ") . PHP_EOL; 680 | } 681 | 682 | /** 683 | * 检测表是否存在 684 | */ 685 | private function checkCrontabTables() 686 | { 687 | $allTables = $this->getDbTables(); 688 | !in_array($this->crontabTable, $allTables) && $this->createCrontabTable(); 689 | !in_array($this->crontabLogTable, $allTables) && $this->createCrontabLogTable(); 690 | } 691 | 692 | /** 693 | * 删除执行失败的任务key 694 | * @return void 695 | */ 696 | private function delTaskMutex() 697 | { 698 | $keys = Redis::keys('framework' . DIRECTORY_SEPARATOR . 'crontab-*'); 699 | Redis::del($keys); 700 | } 701 | 702 | /** 703 | * 获取数据库表名 704 | * @return array 705 | */ 706 | private function getDbTables(): array 707 | { 708 | return Db::getTables(); 709 | } 710 | 711 | /** 712 | * 创建定时器任务表 713 | */ 714 | private function createCrontabTable() 715 | { 716 | $sql = <<crontabTable}` ( 718 | `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, 719 | `title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务标题', 720 | `type` tinyint(1) NOT NULL DEFAULT 1 COMMENT '任务类型 (1 command, 2 class, 3 url, 4 eval)', 721 | `rule` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '任务执行表达式', 722 | `target` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '调用任务字符串', 723 | `parameter` varchar(500) COMMENT '任务调用参数', 724 | `running_times` int(11) NOT NULL DEFAULT '0' COMMENT '已运行次数', 725 | `last_running_time` int(11) NOT NULL DEFAULT '0' COMMENT '上次运行时间', 726 | `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '备注', 727 | `sort` int(11) NOT NULL DEFAULT 0 COMMENT '排序,越大越前', 728 | `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '任务状态状态[0:禁用;1启用]', 729 | `create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', 730 | `update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', 731 | `singleton` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否单次执行 (0 是 1 不是)', 732 | PRIMARY KEY (`id`) USING BTREE, 733 | INDEX `title`(`title`) USING BTREE, 734 | INDEX `create_time`(`create_time`) USING BTREE, 735 | INDEX `status`(`status`) USING BTREE, 736 | INDEX `type`(`type`) USING BTREE 737 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '定时器任务表' ROW_FORMAT = DYNAMIC 738 | SQL; 739 | 740 | return Db::query($sql); 741 | } 742 | 743 | /** 744 | * 定时器任务流水表 745 | */ 746 | private function createCrontabLogTable() 747 | { 748 | $sql = <<crontabLogTable}` ( 750 | `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, 751 | `crontab_id` bigint UNSIGNED NOT NULL COMMENT '任务id', 752 | `target` varchar(255) NOT NULL COMMENT '任务调用目标字符串', 753 | `parameter` varchar(500) COMMENT '任务调用参数', 754 | `exception` text COMMENT '任务执行或者异常信息输出', 755 | `return_code` tinyint(1) NOT NULL DEFAULT 0 COMMENT '执行返回状态[0成功; 1失败]', 756 | `running_time` varchar(10) NOT NULL COMMENT '执行所用时间', 757 | `create_time` int(11) NOT NULL DEFAULT 0 COMMENT '创建时间', 758 | `update_time` int(11) NOT NULL DEFAULT 0 COMMENT '更新时间', 759 | PRIMARY KEY (`id`) USING BTREE, 760 | INDEX `create_time`(`create_time`) USING BTREE, 761 | INDEX `crontab_id`(`crontab_id`) USING BTREE 762 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '定时器任务执行日志表' ROW_FORMAT = DYNAMIC 763 | SQL; 764 | 765 | return Db::query($sql); 766 | } 767 | 768 | } 769 | --------------------------------------------------------------------------------