SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/WorkerCtrl.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use InvalidArgumentException;
  17.  
  18. use Kohana;
  19.  
  20. use Sprout\Exceptions\WorkerJobException;
  21.  
  22.  
  23. /**
  24. * Functions to update and report on worker status
  25. **/
  26. class WorkerCtrl
  27. {
  28.  
  29. /**
  30.   * Starts a new worker
  31.   * Workers are run in their own process (using the PHP CLI)
  32.   *
  33.   * The first argument is the class name
  34.   * Additional arguments can be provided directly in the function call
  35.   *
  36.   * Return value is an array with the following keys
  37.   * job_id The ID of the new job
  38.   * log_url URL to view ongoing status and log
  39.   *
  40.   * @throws InvalidArgumentException The class is not valid
  41.   * @throws QueryException The insert of the job details could not be completed
  42.   * @throws WorkerJobException If the job failed to start
  43.   * @param string $class_name
  44.   * @param mixed ... Additional arguments are passed to the `run` call
  45.   * @return array Job details
  46.   **/
  47. public static function start($class_name)
  48. {
  49. $inst = Sprout::instance($class_name);
  50.  
  51. if (!($inst instanceof WorkerBase)) {
  52. throw new InvalidArgumentException('Provided class is not a subclass of "Worker".');
  53. }
  54.  
  55. // Do some self cleanup
  56. $q = "DELETE FROM ~worker_jobs WHERE DATE_ADD(date_modified, INTERVAL 6 MONTH) < NOW()";
  57. Pdb::query($q, [], 'null');
  58.  
  59. $args = func_get_args();
  60. array_shift($args);
  61.  
  62. $metric_names = $inst->getMetricNames();
  63. $job_code = Security::randStr(8);
  64.  
  65. // Create job record
  66. $update_fields = array();
  67. $update_fields['name'] = $inst->getName();
  68. $update_fields['code'] = $job_code;
  69. $update_fields['class_name'] = $class_name;
  70. $update_fields['args'] = json_encode($args);
  71. $update_fields['log'] = '';
  72. $update_fields['status'] = 'Prepared';
  73. $update_fields['metric1name'] = $metric_names[1];
  74. $update_fields['metric2name'] = $metric_names[2];
  75. $update_fields['metric3name'] = $metric_names[3];
  76. $update_fields['date_added'] = Pdb::now();
  77. $update_fields['date_modified'] = Pdb::now();
  78. $job_id = Pdb::insert('worker_jobs', $update_fields);
  79.  
  80. // If this is called from within a cronjob, let the cron know
  81. if (isset($_ENV['CRON'])) {
  82. Cron::message("Starting worker job # {$job_id}, class '{$class_name}'.");
  83. }
  84.  
  85. // Set up an environment for the worker task
  86. putenv('PHP_S_WORKER=1');
  87. putenv('PHP_S_HTTP_HOST=' . $_SERVER['HTTP_HOST']);
  88. putenv('PHP_S_PROTOCOL=' . Request::protocol());
  89. putenv('PHP_S_WEBDIR=' . Kohana::config('core.site_domain'));
  90.  
  91. // Look for a PHP binary
  92. $php = self::findPhp();
  93. list($php, $version) = $php;
  94. if (! $php) {
  95. Pdb::update('worker_jobs', ['php_bin' => 'NOT FOUND'], ['id' => $job_id]);
  96. throw new WorkerJobException('Unable to find working PHP binary, which is needed for executing background tasks');
  97. }
  98.  
  99. // Confirm it's a CLI binary; CGI binaries are no good
  100. if (strpos($version, 'cli') === false) {
  101. Pdb::update('worker_jobs', ['php_bin' => 'Unuseable (CGI): ' . $php], ['id' => $job_id]);
  102. throw new WorkerJobException('Found a PHP binary, but it\'s a CGI binary; CLI binary required for executing background tasks');
  103. }
  104.  
  105. Pdb::update('worker_jobs', ['php_bin' => $php], ['id' => $job_id]);
  106.  
  107. // Run
  108. $arg = escapeshellarg("worker_job/run/{$job_id}/{$job_code}");
  109. $cmd = "{$php} -d 'safe_mode=0' index.php {$arg} >/dev/null 2>/dev/null & echo $!";
  110. $pid = exec($cmd);
  111.  
  112. if ($pid == 0) {
  113. $ex = new WorkerJobException('Failed to start process');
  114. $ex->cmd = $cmd;
  115. throw $ex;
  116. }
  117.  
  118. // Do several status checks
  119. $num_checks = 20;
  120. $status = null;
  121. for ($i = 0; $i < $num_checks; $i++) {
  122. usleep(1000 * 50);
  123.  
  124. $q = "SELECT status FROM ~worker_jobs WHERE id = ?";
  125. $status = Pdb::query($q, [$job_id], 'val');
  126.  
  127. if ($status != 'Prepared') {
  128. break;
  129. }
  130. }
  131.  
  132. // If it's still not running after all the checks, complain
  133. if ($status == 'Prepared') {
  134. $err = "Process isn't running (failed {$num_checks}x status checks)";
  135. $ex = new WorkerJobException($err);
  136. $ex->cmd = $cmd;
  137. throw $ex;
  138. }
  139.  
  140. return [
  141. 'job_id' => $job_id,
  142. 'log_url' => 'admin/edit/worker_job/' . $job_id
  143. ];
  144. }
  145.  
  146.  
  147. /**
  148.   * Looks in a few places for a PHP binary
  149.   **/
  150. private static function findPhp()
  151. {
  152. $paths = array(
  153. '/usr/bin/php-cli',
  154. '/usr/bin/php',
  155. '/usr/local/bin/php-cli',
  156. '/usr/local/bin/php',
  157. 'php-cli',
  158. 'php',
  159. );
  160.  
  161. // Try various paths, both absolute and relying on $PATH
  162. foreach ($paths as $p) {
  163. $version = @shell_exec($p . ' --version');
  164. if ($version) {
  165. return array($p, $version);
  166. }
  167. }
  168.  
  169. return null;
  170. }
  171.  
  172.  
  173. /**
  174.   * Return the status and metric values for a given worker job.
  175.   *
  176.   * Statuses are:
  177.   * 'Prepared', 'Running', 'Success', 'Failed'.
  178.   *
  179.   * Returns an array of ['status', 'metric1val', 'metric2val', 'metric3val'], or NULL on error.
  180.   **/
  181. public static function getStatus($job_id)
  182. {
  183. $q = "SELECT status, metric1val, metric2val, metric3val FROM ~worker_jobs WHERE id = ?";
  184. return Pdb::query($q, [$job_id], 'row');
  185. }
  186.  
  187. }
  188.  
  189.  
  190.