control.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. <?php
  2. /**
  3. * The control file of cron of ZenTaoPMS.
  4. *
  5. * @copyright Copyright 2009-2023 禅道软件(青岛)有限公司(ZenTao Software (Qingdao) Co., Ltd. www.cnezsoft.com)
  6. * @license ZPL(http://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
  7. * @author Yidong Wang <yidong@cnezsoft.com>
  8. * @package cron
  9. * @version $Id$
  10. * @link https://www.zentao.net
  11. */
  12. class cron extends control
  13. {
  14. /**
  15. * 定时任务首页。
  16. * Index page.
  17. *
  18. * @access public
  19. * @return void
  20. */
  21. public function index()
  22. {
  23. $this->view->title = $this->lang->cron->common;
  24. $this->view->crons = $this->cron->getCrons();
  25. $this->display();
  26. }
  27. /**
  28. * 开启/关闭定时任务功能。
  29. * Turnon cron.
  30. *
  31. * @access public
  32. * @return void
  33. */
  34. public function turnon()
  35. {
  36. $turnon = empty($this->config->global->cron) ? '1' : '0';
  37. $this->loadModel('setting')->setItem('system.common.global.cron', $turnon);
  38. return $this->sendSuccess(array('load' => inlink('index')));
  39. }
  40. /**
  41. * 开启进程。
  42. * Open cron process.
  43. *
  44. * @access public
  45. * @return void
  46. */
  47. public function openProcess()
  48. {
  49. $this->display();
  50. }
  51. /**
  52. * 创建一个定时任务。
  53. * Create cron.
  54. *
  55. * @access public
  56. * @return void
  57. */
  58. public function create()
  59. {
  60. if($_POST)
  61. {
  62. $this->cron->create();
  63. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  64. return $this->sendSuccess(array('load' => inlink('index'), 'closeModal' => true));
  65. }
  66. $this->view->title = $this->lang->cron->create . $this->lang->cron->common;
  67. $this->display();
  68. }
  69. /**
  70. * 编辑一个定时任务。
  71. * Edit cron.
  72. *
  73. * @param int $cronID
  74. * @access public
  75. * @return void
  76. */
  77. public function edit($cronID)
  78. {
  79. if($_POST)
  80. {
  81. $this->cron->update($cronID);
  82. if(dao::isError()) return $this->send(array('result' => 'fail', 'message' => dao::getError()));
  83. return $this->sendSuccess(array('load' => inlink('index'), 'closeModal' => true));
  84. }
  85. $this->view->title = $this->lang->cron->edit . $this->lang->cron->common;
  86. $this->view->cron = $this->cron->getById($cronID);
  87. $this->display();
  88. }
  89. /**
  90. * 修改一个定时任务状态。
  91. * Toggle run cron.
  92. *
  93. * @param int $cronID
  94. * @param string $status
  95. * @access public
  96. * @return void
  97. */
  98. public function toggle($cronID, $status)
  99. {
  100. $this->cron->changeStatus($cronID, $status);
  101. return $this->send(array('result' => 'success', 'load' => true));
  102. }
  103. /**
  104. * 删除一个定时任务。
  105. * Delete cron.
  106. *
  107. * @param int $cronID
  108. * @access public
  109. * @return void
  110. */
  111. public function delete($cronID)
  112. {
  113. $this->dao->delete()->from(TABLE_CRON)->where('id')->eq($cronID)->exec();
  114. return $this->sendSuccess(array('load' => true));
  115. }
  116. /**
  117. * 使用Ajax请求执行定时任务.
  118. * Ajax execute cron.
  119. *
  120. * @param bool $restart
  121. * @access public
  122. * @return void
  123. */
  124. public function ajaxExec($restart = false)
  125. {
  126. if(empty($this->config->global->cron)) return;
  127. /* Run as daemon. */
  128. ignore_user_abort(true);
  129. set_time_limit(0);
  130. session_write_close();
  131. $execId = mt_rand();
  132. if($restart)
  133. {
  134. $this->cron->restartCron($execId);
  135. return $this->sendSuccess(array('load' => true));
  136. }
  137. while(true)
  138. {
  139. /* Only one scheduler and max 4 consumers. */
  140. $roles = $this->applyExecRoles($execId);
  141. if(empty($roles))
  142. {
  143. ignore_user_abort(false);
  144. return;
  145. }
  146. try
  147. {
  148. if(in_array('scheduler', $roles)) $this->schedule($execId);
  149. if(in_array('consumer', $roles)) $this->consumeTasks($execId);
  150. $this->cron->logCron($execId . " finished job at " . date("Y-m-d H:i:s") . "\n", true);
  151. }
  152. catch(Exception $e)
  153. {
  154. $this->cron->logCron($execId . " error: " . $e->getMessage());
  155. }
  156. sleep(20);
  157. }
  158. }
  159. /**
  160. * RoadRunner的调度进程。
  161. * Schedule cron task by RoadRunner.
  162. *
  163. * @access public
  164. * @return void
  165. */
  166. public function rrSchedule()
  167. {
  168. if('cli' !== PHP_SAPI) return;
  169. set_time_limit(0);
  170. session_write_close();
  171. $execId = mt_rand();
  172. $this->cron->restartCron($execId);
  173. $this->loadModel('common');
  174. while(true)
  175. {
  176. if(empty($this->config->global->cron))
  177. {
  178. sleep(60);
  179. continue;
  180. }
  181. if($this->canSchedule($execId)) $this->schedule($execId);
  182. sleep(20);
  183. }
  184. }
  185. /**
  186. * RoadRunner的消费进程。
  187. * Consume cron task by RoadRunner.
  188. *
  189. * @access public
  190. * @return void
  191. */
  192. public function rrConsume()
  193. {
  194. if('cli' !== PHP_SAPI) return;
  195. set_time_limit(0);
  196. session_write_close();
  197. $this->loadModel('common');
  198. $execId = mt_rand();
  199. while(true)
  200. {
  201. if(empty($this->config->global->cron))
  202. {
  203. sleep(60);
  204. continue;
  205. }
  206. $this->consumeTasks($execId);
  207. sleep(20);
  208. }
  209. }
  210. /**
  211. * 检查该execId是否可以进行调度(最近1分钟没有其他进程在调度).
  212. * Check the execId can schedule(No other execId scheduled 1 minutes ago).
  213. *
  214. * @param int $execId
  215. * @access protected
  216. * @return bool
  217. */
  218. protected function canSchedule($execId)
  219. {
  220. $settings = $this->dao->select('`key`,`value`')->from(TABLE_CONFIG)->where('owner')->eq('system')->andWhere('module')->eq('cron')->andWhere('section')->eq('scheduler')->fetchPairs();
  221. if(!isset($settings['execId']) || $settings['execId'] == $execId) return true;
  222. if(!isset($settings['lastTime']) || $settings['lastTime'] < date('Y-m-d H:i:s', strtotime('-1 minute'))) return true;
  223. return false;
  224. }
  225. /**
  226. * 检查该execId是否可以执行任务(最多允许1个调度进程,4个执行进程).
  227. * Check the execId can exec(1 scheduler, 4 consumers).
  228. *
  229. * @param int $execId
  230. * @access protected
  231. * @return array
  232. */
  233. protected function applyExecRoles($execId)
  234. {
  235. $roles = array();
  236. $settings = $this->dao->select('id,section,`key`,value')->from(TABLE_CONFIG)->where('owner')->eq('system')->andWhere('module')->eq('cron')->fetchAll();
  237. $scheduler = array('execId' => 0, 'lastTime' => '');
  238. $consumers = [];
  239. /* 生成scheduler和consumer的配置信息。 Set scheduler config and consumers. */
  240. $expirDate = date('Y-m-d H:i:s', strtotime('-1 minute'));
  241. foreach($settings as $setting)
  242. {
  243. if($setting->section == 'scheduler')
  244. {
  245. if($setting->key == 'lastTime')
  246. {
  247. if($setting->value < $expirDate) continue;
  248. $scheduler['lastTime'] = $setting->value;
  249. }
  250. if($setting->key == 'execId') $scheduler['execId'] = $setting->value;
  251. }
  252. elseif($setting->section == 'consumer')
  253. {
  254. if($setting->value < $expirDate)
  255. {
  256. $this->dao->delete()->from(TABLE_CONFIG)->where('id')->eq($setting->id)->exec();
  257. continue;
  258. }
  259. $consumers[$setting->key] = $setting->key;
  260. }
  261. }
  262. /* 如果当前进程ID是scheduler,或者没有活动的scheduler,该进程抢占为scheduler. */
  263. if($scheduler['execId'] == $execId || $scheduler['lastTime'] < $expirDate) $roles[] = 'scheduler';
  264. /* 如果当前进程ID是consumer,或者consumer数量不足,该进程抢占为consumer. */
  265. if(in_array($execId, $consumers) || count($consumers) < $this->config->cron->maxConsumer) $roles[] = 'consumer';
  266. $this->cron->logCron("$execId apply roles: [" . implode(',', $roles). "]\n", true);
  267. return $roles;
  268. }
  269. /**
  270. * 调度生成队列任务.
  271. * Schedule, push tasks to queue.
  272. *
  273. * @param int $execId
  274. * @access public
  275. * @return bool
  276. */
  277. public function schedule($execId)
  278. {
  279. if(empty($this->config->global->cron)) return;
  280. $this->dao->clearCache();
  281. $this->loadModel('common');
  282. $this->cron->updateTime('scheduler', $execId);
  283. /* Get and parse crons. */
  284. $tasks = $this->dao->select('cron,MAX(`createdDate`) `datetime`')->from(TABLE_QUEUE)->groupBy('cron')->fetchAll('cron');
  285. $crons = $this->cron->getCrons('nostop');
  286. foreach($crons as $cron)
  287. {
  288. /* 最后一次创建队列任务的时间作为cron的最新执行时间。 The lasttime of task in queue is the last runtime of cron. */
  289. $cron->datetime = isset($tasks[$cron->id]) ? $tasks[$cron->id]->datetime : '1970-01-01';
  290. }
  291. $parsedCrons = $this->cron->parseCron($crons);
  292. $now = date(DT_DATETIME1);
  293. foreach($parsedCrons as $id => $cron)
  294. {
  295. $cronInfo = $crons[$id];
  296. /* Skip empty and stop cron.*/
  297. if(empty($cronInfo) || $cronInfo->status == 'stop') continue;
  298. if(!$cron['command'] || !isset($crons[$id])) continue;
  299. /* Check time. */
  300. if($now < $cron['time']->format(DT_DATETIME1)) continue;
  301. /* Push task into queue. */
  302. $task = new stdclass();
  303. $task->cron = $id;
  304. $task->type = $crons[$id]->type;
  305. $task->command = $cron['command'];
  306. $task->createdDate = $now;
  307. $this->dao->insert(TABLE_QUEUE)->data($task)->exec();
  308. $log = date('G:i:s') . " schedule\ncronId: $id\nexecId: $execId\noutput: push task to queue\n\n";
  309. $this->cron->logCron($log);
  310. }
  311. }
  312. /**
  313. * 执行所有定时任务.
  314. * Execute all tasks.
  315. *
  316. * @param int $execId
  317. * @access public
  318. * @return bool
  319. */
  320. public function consumeTasks($execId)
  321. {
  322. while(true)
  323. {
  324. $this->cron->updateTime('consumer', $execId);
  325. /* Consume. */
  326. $task = $this->dao->select('*')->from(TABLE_QUEUE)->where('status')->eq('wait')->orderBy('createdDate')->fetch();
  327. if(!$task) break;
  328. $this->consumeTask($execId, $task);
  329. }
  330. }
  331. /**
  332. * 执行一个定时任务.
  333. * Execute one task.
  334. *
  335. * @param int $execId
  336. * @param object $task
  337. * @access public
  338. * @return bool
  339. */
  340. public function consumeTask($execId, $task)
  341. {
  342. $this->dao->clearCache();
  343. if(!empty($task->command))
  344. {
  345. $affectedRows = $this->dao->update(TABLE_QUEUE)
  346. ->set('status')->eq('doing')->set('execId')->eq($execId)
  347. ->where('id')->eq($task->id)
  348. ->andWhere('status')->eq('wait')
  349. ->andWhere('execId')->eq('0')
  350. ->exec();
  351. if($affectedRows != 1) return;
  352. /* Other executor may execute the task at the same time,so we mark execId and wait 0.5s to check whether we own it. */
  353. usleep(500000);
  354. $task = $this->dao->select('*')->from(TABLE_QUEUE)->where('id')->eq($task->id)->fetch();
  355. if($task->execId != $execId) return;
  356. /* Execution command. */
  357. $output = '';
  358. $return = '';
  359. unset($_SESSION['company']);
  360. unset($this->app->company);
  361. /* Mark that this request was triggered by the scheduled task, not by the user. */
  362. $_SESSION['fromCron'] = true;
  363. $this->loadModel('common');
  364. $this->common->setCompany();
  365. $this->common->loadConfigFromDB();
  366. try
  367. {
  368. if($task->type == 'zentao')
  369. {
  370. parse_str($task->command, $params);
  371. if(isset($params['moduleName']) and isset($params['methodName']))
  372. {
  373. $this->viewType = 'html';
  374. $moduleName = $params['moduleName'];
  375. $methodName = $params['methodName'];
  376. $this->app->loadLang($moduleName);
  377. $this->app->loadConfig($moduleName);
  378. unset($params['moduleName'], $params['methodName']);
  379. $output = $this->fetch($moduleName, $methodName, $params);
  380. }
  381. }
  382. elseif($task->type == 'system')
  383. {
  384. exec($task->command, $out, $return);
  385. if($out) $output = implode(PHP_EOL, $out);
  386. }
  387. }
  388. catch(EndResponseException $endResponseException)
  389. {
  390. $output = $endResponseException->getContent();
  391. }
  392. catch(Exception $e)
  393. {
  394. $output = $e;
  395. }
  396. }
  397. $this->dao->update(TABLE_QUEUE)->set('status')->eq('done')->where('id')->eq($task->id)->exec();
  398. $this->dao->update(TABLE_CRON)->set('lastTime')->eq(date(DT_DATETIME1))->where('id')->eq($task->cron)->exec();
  399. $log = date('G:i:s') . " execute\ncronId: {$task->cron}\nexecId: $execId\ntaskId: {$task->id}\ncommand: {$task->command}\nreturn : $return\noutput : $output\n\n";
  400. $this->cron->logCron($log);
  401. return true;
  402. }
  403. }