Scheduler.php 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <?php
  2. /**
  3. * @package Grav\Common\Scheduler
  4. * @author Originally based on peppeocchi/php-cron-scheduler modified for Grav integration
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Scheduler;
  9. use Grav\Common\Filesystem\Folder;
  10. use Grav\Common\Grav;
  11. use Grav\Common\Utils;
  12. use Symfony\Component\Process\PhpExecutableFinder;
  13. use Symfony\Component\Process\Process;
  14. use RocketTheme\Toolbox\File\YamlFile;
  15. class Scheduler
  16. {
  17. /**
  18. * The queued jobs.
  19. *
  20. * @var array
  21. */
  22. private $jobs = [];
  23. private $saved_jobs = [];
  24. private $executed_jobs = [];
  25. private $failed_jobs = [];
  26. private $jobs_run = [];
  27. private $output_schedule = [];
  28. private $config;
  29. private $status_path;
  30. /**
  31. * Create new instance.
  32. */
  33. public function __construct()
  34. {
  35. $config = Grav::instance()['config']->get('scheduler.defaults', []);
  36. $this->config = $config;
  37. $this->status_path = Grav::instance()['locator']->findResource('user-data://scheduler', true, true);
  38. if (!file_exists($this->status_path)) {
  39. Folder::create($this->status_path);
  40. }
  41. }
  42. /**
  43. * Load saved jobs from config/scheduler.yaml file
  44. */
  45. public function loadSavedJobs()
  46. {
  47. $this->saved_jobs = [];
  48. $saved_jobs = (array) Grav::instance()['config']->get('scheduler.custom_jobs', []);
  49. foreach ($saved_jobs as $id => $j) {
  50. $args = $j['args'] ?? [];
  51. $id = Grav::instance()['inflector']->hyphenize($id);
  52. $job = $this->addCommand($j['command'], $args, $id);
  53. if (isset($j['at'])) {
  54. $job->at($j['at']);
  55. }
  56. if (isset($j['output'])) {
  57. $mode = isset($j['output_mode']) && $j['output_mode'] === 'append' ? true : false;
  58. $job->output($j['output'], $mode);
  59. }
  60. if (isset($j['email'])) {
  61. $job->email($j['email']);
  62. }
  63. // store in saved_jobs
  64. $this->saved_jobs[] = $job;
  65. }
  66. return $this;
  67. }
  68. /**
  69. * Get the queued jobs as background/foreground
  70. *
  71. * @param bool $all
  72. * @return array
  73. */
  74. public function getQueuedJobs($all = false)
  75. {
  76. $background = [];
  77. $foreground = [];
  78. foreach ($this->jobs as $job) {
  79. if ($all || $job->getEnabled()) {
  80. if ($job->runInBackground()) {
  81. $background[] = $job;
  82. } else {
  83. $foreground[] = $job;
  84. }
  85. }
  86. }
  87. return [$background, $foreground];
  88. }
  89. /**
  90. * Get all jobs if they are disabled or not as one array
  91. *
  92. * @return array
  93. */
  94. public function getAllJobs()
  95. {
  96. list($background, $foreground) = $this->loadSavedJobs()->getQueuedJobs(true);
  97. return array_merge($background, $foreground);
  98. }
  99. /**
  100. * Queues a PHP function execution.
  101. *
  102. * @param callable $fn The function to execute
  103. * @param array $args Optional arguments to pass to the php script
  104. * @param string $id Optional custom identifier
  105. * @return Job
  106. */
  107. public function addFunction(callable $fn, $args = [], $id = null)
  108. {
  109. $job = new Job($fn, $args, $id);
  110. $this->queueJob($job->configure($this->config));
  111. return $job;
  112. }
  113. /**
  114. * Queue a raw shell command.
  115. *
  116. * @param string $command The command to execute
  117. * @param array $args Optional arguments to pass to the command
  118. * @param string $id Optional custom identifier
  119. * @return Job
  120. */
  121. public function addCommand($command, $args = [], $id = null)
  122. {
  123. $job = new Job($command, $args, $id);
  124. $this->queueJob($job->configure($this->config));
  125. return $job;
  126. }
  127. /**
  128. * Run the scheduler.
  129. *
  130. * @param \DateTime|null $runTime Optional, run at specific moment
  131. */
  132. public function run(\DateTime $runTime = null)
  133. {
  134. $this->loadSavedJobs();
  135. list($background, $foreground) = $this->getQueuedJobs(false);
  136. $alljobs = array_merge($background, $foreground);
  137. if (null === $runTime) {
  138. $runTime = new \DateTime('now');
  139. }
  140. // Star processing jobs
  141. foreach ($alljobs as $job) {
  142. if ($job->isDue($runTime)) {
  143. $job->run();
  144. $this->jobs_run[] = $job;
  145. }
  146. }
  147. // Finish handling any background jobs
  148. foreach($background as $job) {
  149. $job->finalize();
  150. }
  151. // Store states
  152. $this->saveJobStates();
  153. }
  154. /**
  155. * Reset all collected data of last run.
  156. *
  157. * Call before run() if you call run() multiple times.
  158. */
  159. public function resetRun()
  160. {
  161. // Reset collected data of last run
  162. $this->executed_jobs = [];
  163. $this->failed_jobs = [];
  164. $this->output_schedule = [];
  165. return $this;
  166. }
  167. /**
  168. * Get the scheduler verbose output.
  169. *
  170. * @param string $type Allowed: text, html, array
  171. * @return mixed The return depends on the requested $type
  172. */
  173. public function getVerboseOutput($type = 'text')
  174. {
  175. switch ($type) {
  176. case 'text':
  177. return implode("\n", $this->output_schedule);
  178. case 'html':
  179. return implode('<br>', $this->output_schedule);
  180. case 'array':
  181. return $this->output_schedule;
  182. default:
  183. throw new \InvalidArgumentException('Invalid output type');
  184. }
  185. }
  186. /**
  187. * Remove all queued Jobs.
  188. */
  189. public function clearJobs()
  190. {
  191. $this->jobs = [];
  192. return $this;
  193. }
  194. /**
  195. * Helper to get the full Cron command
  196. *
  197. * @return string
  198. */
  199. public function getCronCommand()
  200. {
  201. $phpBinaryFinder = new PhpExecutableFinder();
  202. $php = $phpBinaryFinder->find();
  203. $command = 'cd ' . str_replace(' ', '\ ', GRAV_ROOT) . ';' . $php . ' bin/grav scheduler';
  204. return "(crontab -l; echo \"* * * * * {$command} 1>> /dev/null 2>&1\") | crontab -";
  205. }
  206. /**
  207. * Helper to determine if cron job is setup
  208. *
  209. * @return int
  210. */
  211. public function isCrontabSetup()
  212. {
  213. $process = new Process(['crontab', '-l']);
  214. $process->run();
  215. if ($process->isSuccessful()) {
  216. $output = $process->getOutput();
  217. return preg_match('$bin\/grav schedule$', $output) ? 1 : 0;
  218. }
  219. $error = $process->getErrorOutput();
  220. return Utils::startsWith($error, 'crontab: no crontab') ? 0 : 2;
  221. }
  222. /**
  223. * Get the Job states file
  224. *
  225. * @return \RocketTheme\Toolbox\File\FileInterface|YamlFile
  226. */
  227. public function getJobStates()
  228. {
  229. return YamlFile::instance($this->status_path . '/status.yaml');
  230. }
  231. /**
  232. * Save job states to statys file
  233. */
  234. private function saveJobStates()
  235. {
  236. $now = time();
  237. $new_states = [];
  238. foreach ($this->jobs_run as $job) {
  239. if ($job->isSuccessful()) {
  240. $new_states[$job->getId()] = ['state' => 'success', 'last-run' => $now];
  241. $this->pushExecutedJob($job);
  242. } else {
  243. $new_states[$job->getId()] = ['state' => 'failure', 'last-run' => $now, 'error' => $job->getOutput()];
  244. $this->pushFailedJob($job);
  245. }
  246. }
  247. $saved_states = $this->getJobStates();
  248. $saved_states->save(array_merge($saved_states->content(), $new_states));
  249. }
  250. /**
  251. * Queue a job for execution in the correct queue.
  252. *
  253. * @param Job $job
  254. * @return void
  255. */
  256. private function queueJob(Job $job)
  257. {
  258. $this->jobs[] = $job;
  259. // Store jobs
  260. }
  261. /**
  262. * Add an entry to the scheduler verbose output array.
  263. *
  264. * @param string $string
  265. * @return void
  266. */
  267. private function addSchedulerVerboseOutput($string)
  268. {
  269. $now = '[' . (new \DateTime('now'))->format('c') . '] ';
  270. $this->output_schedule[] = $now . $string;
  271. // Print to stdoutput in light gray
  272. // echo "\033[37m{$string}\033[0m\n";
  273. }
  274. /**
  275. * Push a succesfully executed job.
  276. *
  277. * @param Job $job
  278. * @return Job
  279. */
  280. private function pushExecutedJob(Job $job)
  281. {
  282. $this->executed_jobs[] = $job;
  283. $command = $job->getCommand();
  284. $args = $job->getArguments();
  285. // If callable, log the string Closure
  286. if (is_callable($command)) {
  287. $command = \is_string($command) ? $command : 'Closure';
  288. }
  289. $this->addSchedulerVerboseOutput("<green>Success</green>: <white>{$command} {$args}</white>");
  290. return $job;
  291. }
  292. /**
  293. * Push a failed job.
  294. *
  295. * @param Job $job
  296. * @return Job
  297. */
  298. private function pushFailedJob(Job $job)
  299. {
  300. $this->failed_jobs[] = $job;
  301. $command = $job->getCommand();
  302. // If callable, log the string Closure
  303. if (is_callable($command)) {
  304. $command = \is_string($command) ? $command : 'Closure';
  305. }
  306. $output = trim($job->getOutput());
  307. $this->addSchedulerVerboseOutput("<red>Error</red>: <white>{$command}</white> → <normal>{$output}</normal>");
  308. return $job;
  309. }
  310. }