get('scheduler.defaults', []);
$this->config = $config;
$this->status_path = Grav::instance()['locator']->findResource('user-data://scheduler', true, true);
if (!file_exists($this->status_path)) {
Folder::create($this->status_path);
}
}
/**
* Load saved jobs from config/scheduler.yaml file
*
* @return $this
*/
public function loadSavedJobs()
{
$this->saved_jobs = [];
$saved_jobs = (array) Grav::instance()['config']->get('scheduler.custom_jobs', []);
foreach ($saved_jobs as $id => $j) {
$args = $j['args'] ?? [];
$id = Grav::instance()['inflector']->hyphenize($id);
$job = $this->addCommand($j['command'], $args, $id);
if (isset($j['at'])) {
$job->at($j['at']);
}
if (isset($j['output'])) {
$mode = isset($j['output_mode']) && $j['output_mode'] === 'append';
$job->output($j['output'], $mode);
}
if (isset($j['email'])) {
$job->email($j['email']);
}
// store in saved_jobs
$this->saved_jobs[] = $job;
}
return $this;
}
/**
* Get the queued jobs as background/foreground
*
* @param bool $all
* @return array
*/
public function getQueuedJobs($all = false)
{
$background = [];
$foreground = [];
foreach ($this->jobs as $job) {
if ($all || $job->getEnabled()) {
if ($job->runInBackground()) {
$background[] = $job;
} else {
$foreground[] = $job;
}
}
}
return [$background, $foreground];
}
/**
* Get all jobs if they are disabled or not as one array
*
* @return Job[]
*/
public function getAllJobs()
{
[$background, $foreground] = $this->loadSavedJobs()->getQueuedJobs(true);
return array_merge($background, $foreground);
}
/**
* Get a specific Job based on id
*
* @param string $jobid
* @return Job|null
*/
public function getJob($jobid)
{
$all = $this->getAllJobs();
foreach ($all as $job) {
if ($jobid == $job->getId()) {
return $job;
}
}
return null;
}
/**
* Queues a PHP function execution.
*
* @param callable $fn The function to execute
* @param array $args Optional arguments to pass to the php script
* @param string|null $id Optional custom identifier
* @return Job
*/
public function addFunction(callable $fn, $args = [], $id = null)
{
$job = new Job($fn, $args, $id);
$this->queueJob($job->configure($this->config));
return $job;
}
/**
* Queue a raw shell command.
*
* @param string $command The command to execute
* @param array $args Optional arguments to pass to the command
* @param string|null $id Optional custom identifier
* @return Job
*/
public function addCommand($command, $args = [], $id = null)
{
$job = new Job($command, $args, $id);
$this->queueJob($job->configure($this->config));
return $job;
}
/**
* Run the scheduler.
*
* @param DateTime|null $runTime Optional, run at specific moment
* @param bool $force force run even if not due
*/
public function run(DateTime $runTime = null, $force = false)
{
$this->loadSavedJobs();
[$background, $foreground] = $this->getQueuedJobs(false);
$alljobs = array_merge($background, $foreground);
if (null === $runTime) {
$runTime = new DateTime('now');
}
// Star processing jobs
foreach ($alljobs as $job) {
if ($job->isDue($runTime) || $force) {
$job->run();
$this->jobs_run[] = $job;
}
}
// Finish handling any background jobs
foreach ($background as $job) {
$job->finalize();
}
// Store states
$this->saveJobStates();
}
/**
* Reset all collected data of last run.
*
* Call before run() if you call run() multiple times.
*
* @return $this
*/
public function resetRun()
{
// Reset collected data of last run
$this->executed_jobs = [];
$this->failed_jobs = [];
$this->output_schedule = [];
return $this;
}
/**
* Get the scheduler verbose output.
*
* @param string $type Allowed: text, html, array
* @return string|array The return depends on the requested $type
*/
public function getVerboseOutput($type = 'text')
{
switch ($type) {
case 'text':
return implode("\n", $this->output_schedule);
case 'html':
return implode('
', $this->output_schedule);
case 'array':
return $this->output_schedule;
default:
throw new InvalidArgumentException('Invalid output type');
}
}
/**
* Remove all queued Jobs.
*
* @return $this
*/
public function clearJobs()
{
$this->jobs = [];
return $this;
}
/**
* Helper to get the full Cron command
*
* @return string
*/
public function getCronCommand()
{
$command = $this->getSchedulerCommand();
return "(crontab -l; echo \"* * * * * {$command} 1>> /dev/null 2>&1\") | crontab -";
}
/**
* @param string|null $php
* @return string
*/
public function getSchedulerCommand($php = null)
{
$phpBinaryFinder = new PhpExecutableFinder();
$php = $php ?? $phpBinaryFinder->find();
$command = 'cd ' . str_replace(' ', '\ ', GRAV_ROOT) . ';' . $php . ' bin/grav scheduler';
return $command;
}
/**
* Helper to determine if cron job is setup
* 0 - Crontab Not found
* 1 - Crontab Found
* 2 - Error
*
* @return int
*/
public function isCrontabSetup()
{
$process = new Process(['crontab', '-l']);
$process->run();
if ($process->isSuccessful()) {
$output = $process->getOutput();
$command = str_replace('/', '\/', $this->getSchedulerCommand('.*'));
$full_command = '/^(?!#).* .* .* .* .* ' . $command . '/m';
return preg_match($full_command, $output) ? 1 : 0;
}
$error = $process->getErrorOutput();
return Utils::startsWith($error, 'crontab: no crontab') ? 0 : 2;
}
/**
* Get the Job states file
*
* @return YamlFile
*/
public function getJobStates()
{
return YamlFile::instance($this->status_path . '/status.yaml');
}
/**
* Save job states to statys file
*
* @return void
*/
private function saveJobStates()
{
$now = time();
$new_states = [];
foreach ($this->jobs_run as $job) {
if ($job->isSuccessful()) {
$new_states[$job->getId()] = ['state' => 'success', 'last-run' => $now];
$this->pushExecutedJob($job);
} else {
$new_states[$job->getId()] = ['state' => 'failure', 'last-run' => $now, 'error' => $job->getOutput()];
$this->pushFailedJob($job);
}
}
$saved_states = $this->getJobStates();
$saved_states->save(array_merge($saved_states->content(), $new_states));
}
/**
* Try to determine who's running the process
*
* @return false|string
*/
public function whoami()
{
$process = new Process(['whoami']);
$process->run();
if ($process->isSuccessful()) {
return trim($process->getOutput());
}
return $process->getErrorOutput();
}
/**
* Queue a job for execution in the correct queue.
*
* @param Job $job
* @return void
*/
private function queueJob(Job $job)
{
$this->jobs[] = $job;
// Store jobs
}
/**
* Add an entry to the scheduler verbose output array.
*
* @param string $string
* @return void
*/
private function addSchedulerVerboseOutput($string)
{
$now = '[' . (new DateTime('now'))->format('c') . '] ';
$this->output_schedule[] = $now . $string;
// Print to stdoutput in light gray
// echo "\033[37m{$string}\033[0m\n";
}
/**
* Push a succesfully executed job.
*
* @param Job $job
* @return Job
*/
private function pushExecutedJob(Job $job)
{
$this->executed_jobs[] = $job;
$command = $job->getCommand();
$args = $job->getArguments();
// If callable, log the string Closure
if (is_callable($command)) {
$command = is_string($command) ? $command : 'Closure';
}
$this->addSchedulerVerboseOutput("Success: {$command} {$args}");
return $job;
}
/**
* Push a failed job.
*
* @param Job $job
* @return Job
*/
private function pushFailedJob(Job $job)
{
$this->failed_jobs[] = $job;
$command = $job->getCommand();
// If callable, log the string Closure
if (is_callable($command)) {
$command = is_string($command) ? $command : 'Closure';
}
$output = trim($job->getOutput());
$this->addSchedulerVerboseOutput("Error: {$command} → {$output}");
return $job;
}
}