├── 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 |
--------------------------------------------------------------------------------