<?php
/*
* Copyright (C) 2017 Karmabunny Pty Ltd.
*
* This file is a part of SproutCMS.
*
* SproutCMS is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either
* version 2 of the License, or (at your option) any later version.
*
* For more information, visit <http://getsproutcms.com>.
*/
namespace Sprout\Helpers;
use InvalidArgumentException;
use Kohana;
use Sprout\Exceptions\WorkerJobException;
/**
* Functions to update and report on worker status
**/
class WorkerCtrl
{
/**
* Starts a new worker
* Workers are run in their own process (using the PHP CLI)
*
* The first argument is the class name
* Additional arguments can be provided directly in the function call
*
* Return value is an array with the following keys
* job_id The ID of the new job
* log_url URL to view ongoing status and log
*
* @throws InvalidArgumentException The class is not valid
* @throws QueryException The insert of the job details could not be completed
* @throws WorkerJobException If the job failed to start
* @param string $class_name
* @param mixed ... Additional arguments are passed to the `run` call
* @return array Job details
**/
public static function start($class_name)
{
$inst = Sprout::instance($class_name);
if (!($inst instanceof WorkerBase)) {
throw new InvalidArgumentException('Provided class is not a subclass of "Worker".');
}
// Do some self cleanup
$q = "DELETE FROM ~worker_jobs WHERE DATE_ADD(date_modified, INTERVAL 6 MONTH) < NOW()";
Pdb::query($q, [], 'null');
$metric_names = $inst->getMetricNames();
$job_code = Security::randStr(8);
// Create job record
$update_fields = array(); $update_fields['name'] = $inst->getName();
$update_fields['code'] = $job_code;
$update_fields['class_name'] = $class_name;
$update_fields['log'] = '';
$update_fields['status'] = 'Prepared';
$update_fields['metric1name'] = $metric_names[1];
$update_fields['metric2name'] = $metric_names[2];
$update_fields['metric3name'] = $metric_names[3];
$update_fields['date_added'] = Pdb::now();
$update_fields['date_modified'] = Pdb::now();
$job_id = Pdb::insert('worker_jobs', $update_fields);
// If this is called from within a cronjob, let the cron know
if (isset($_ENV['CRON'])) { Cron::message("Starting worker job # {$job_id}, class '{$class_name}'.");
}
// Set up an environment for the worker task
putenv('PHP_S_HTTP_HOST=' . $_SERVER['HTTP_HOST']); putenv('PHP_S_PROTOCOL=' . Request
::protocol()); putenv('PHP_S_WEBDIR=' . Kohana
::config('core.site_domain'));
// Look for a PHP binary
$php = self::findPhp();
list($php, $version) = $php; if (! $php) {
Pdb::update('worker_jobs', ['php_bin' => 'NOT FOUND'], ['id' => $job_id]);
throw new WorkerJobException('Unable to find working PHP binary, which is needed for executing background tasks');
}
// Confirm it's a CLI binary; CGI binaries are no good
if (strpos($version, 'cli') === false) { Pdb::update('worker_jobs', ['php_bin' => 'Unuseable (CGI): ' . $php], ['id' => $job_id]);
throw new WorkerJobException('Found a PHP binary, but it\'s a CGI binary; CLI binary required for executing background tasks');
}
Pdb::update('worker_jobs', ['php_bin' => $php], ['id' => $job_id]);
// Run
$cmd = "{$php} -d 'safe_mode=0' index.php {$arg} >/dev/null 2>/dev/null & echo $!";
if ($pid == 0) {
$ex = new WorkerJobException('Failed to start process');
$ex->cmd = $cmd;
throw $ex;
}
// Do several status checks
$num_checks = 20;
$status = null;
for ($i = 0; $i < $num_checks; $i++) {
$q = "SELECT status FROM ~worker_jobs WHERE id = ?";
$status = Pdb::query($q, [$job_id], 'val');
if ($status != 'Prepared') {
break;
}
}
// If it's still not running after all the checks, complain
if ($status == 'Prepared') {
$err = "Process isn't running (failed {$num_checks}x status checks)";
$ex = new WorkerJobException($err);
$ex->cmd = $cmd;
throw $ex;
}
return [
'job_id' => $job_id,
'log_url' => 'admin/edit/worker_job/' . $job_id
];
}
/**
* Looks in a few places for a PHP binary
**/
private static function findPhp()
{
'/usr/bin/php-cli',
'/usr/bin/php',
'/usr/local/bin/php-cli',
'/usr/local/bin/php',
'php-cli',
'php',
);
// Try various paths, both absolute and relying on $PATH
foreach ($paths as $p) {
if ($version) {
return array($p, $version); }
}
return null;
}
/**
* Return the status and metric values for a given worker job.
*
* Statuses are:
* 'Prepared', 'Running', 'Success', 'Failed'.
*
* Returns an array of ['status', 'metric1val', 'metric2val', 'metric3val'], or NULL on error.
**/
public static function getStatus($job_id)
{
$q = "SELECT status, metric1val, metric2val, metric3val FROM ~worker_jobs WHERE id = ?";
return Pdb::query($q, [$job_id], 'row');
}
}