From c5f2dfd6f6ecdc91002a13ad66e0aea05169c601 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 16 Sep 2017 23:10:00 -0500 Subject: [PATCH] Begin adding schedule processing jobs. --- .../Schedule/ProcessRunnableCommand.php | 110 ++++++++++ app/Console/Kernel.php | 19 +- .../ScheduleRepositoryInterface.php | 8 + .../Repository/TaskRepositoryInterface.php | 18 ++ app/Jobs/Schedule/RunTaskJob.php | 191 ++++++++++++++++++ app/Models/Schedule.php | 4 +- app/Models/Task.php | 12 +- .../Eloquent/ScheduleRepository.php | 11 + app/Repositories/Eloquent/TaskRepository.php | 37 ++-- .../Schedules/ProcessScheduleService.php | 80 ++++++++ .../Schedules/ScheduleCreationService.php | 20 ++ .../Schedules/Tasks/RunTaskService.php | 75 +++++++ composer.json | 3 +- composer.lock | 54 ++++- resources/lang/en/command/messages.php | 3 + .../Schedules/ScheduleCreationServiceTest.php | 30 ++- 16 files changed, 626 insertions(+), 49 deletions(-) create mode 100644 app/Console/Commands/Schedule/ProcessRunnableCommand.php create mode 100644 app/Jobs/Schedule/RunTaskJob.php create mode 100644 app/Services/Schedules/ProcessScheduleService.php create mode 100644 app/Services/Schedules/Tasks/RunTaskService.php diff --git a/app/Console/Commands/Schedule/ProcessRunnableCommand.php b/app/Console/Commands/Schedule/ProcessRunnableCommand.php new file mode 100644 index 000000000..12c90fa16 --- /dev/null +++ b/app/Console/Commands/Schedule/ProcessRunnableCommand.php @@ -0,0 +1,110 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Console\Commands\Schedule; + +use Carbon\Carbon; +use Illuminate\Console\Command; +use Illuminate\Support\Collection; +use Pterodactyl\Services\Schedules\ProcessScheduleService; +use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; + +class ProcessRunnableCommand extends Command +{ + /** + * @var \Carbon\Carbon + */ + protected $carbon; + + /** + * @var string + */ + protected $description = 'Process schedules in the database and determine which are ready to run.'; + + /** + * @var \Pterodactyl\Services\Schedules\ProcessScheduleService + */ + protected $processScheduleService; + + /** + * @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface + */ + protected $repository; + + /** + * @var string + */ + protected $signature = 'p:schedule:process'; + + /** + * ProcessRunnableCommand constructor. + * + * @param \Carbon\Carbon $carbon + * @param \Pterodactyl\Services\Schedules\ProcessScheduleService $processScheduleService + * @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository + */ + public function __construct( + Carbon $carbon, + ProcessScheduleService $processScheduleService, + ScheduleRepositoryInterface $repository + ) { + parent::__construct(); + + $this->carbon = $carbon; + $this->processScheduleService = $processScheduleService; + $this->repository = $repository; + } + + /** + * Handle command execution. + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle() + { + $schedules = $this->repository->getSchedulesToProcess($this->carbon->now()->toAtomString()); + + $bar = $this->output->createProgressBar(count($schedules)); + foreach ($schedules as $schedule) { + if (! $schedule->tasks instanceof Collection || count($schedule->tasks) < 1) { + $bar->advance(); + + return; + } + + $this->processScheduleService->handle($schedule); + if ($this->input->isInteractive()) { + $this->line(trans('command/messages.schedule.output_line', [ + 'schedule' => $schedule->name, + 'hash' => $schedule->hashid, + ])); + } + + $bar->advance(); + } + + $this->line(''); + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index f937d4a3c..db3eddde9 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Pterodactyl\Console\Commands\Location\MakeLocationCommand; use Pterodactyl\Console\Commands\User\DisableTwoFactorCommand; use Pterodactyl\Console\Commands\Location\DeleteLocationCommand; +use Pterodactyl\Console\Commands\Schedule\ProcessRunnableCommand; class Kernel extends ConsoleKernel { @@ -25,17 +26,7 @@ class Kernel extends ConsoleKernel InfoCommand::class, MakeLocationCommand::class, MakeUserCommand::class, -// \Pterodactyl\Console\Commands\MakeUser::class, -// \Pterodactyl\Console\Commands\ShowVersion::class, -// \Pterodactyl\Console\Commands\UpdateEnvironment::class, -// \Pterodactyl\Console\Commands\RunTasks::class, -// \Pterodactyl\Console\Commands\ClearTasks::class, -// \Pterodactyl\Console\Commands\ClearServices::class, -// \Pterodactyl\Console\Commands\UpdateEmailSettings::class, -// \Pterodactyl\Console\Commands\CleanServiceBackup::class, -// \Pterodactyl\Console\Commands\AddNode::class, -// \Pterodactyl\Console\Commands\MakeLocationCommand::class, -// \Pterodactyl\Console\Commands\RebuildServer::class, + ProcessRunnableCommand::class, ]; /** @@ -45,8 +36,8 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule) { - $schedule->command('pterodactyl:tasks')->everyMinute()->withoutOverlapping(); - $schedule->command('pterodactyl:tasks:clearlog')->twiceDaily(3, 15); - $schedule->command('pterodactyl:cleanservices')->twiceDaily(1, 13); + // $schedule->command('pterodactyl:tasks')->everyMinute()->withoutOverlapping(); +// $schedule->command('pterodactyl:tasks:clearlog')->twiceDaily(3, 15); +// $schedule->command('pterodactyl:cleanservices')->twiceDaily(1, 13); } } diff --git a/app/Contracts/Repository/ScheduleRepositoryInterface.php b/app/Contracts/Repository/ScheduleRepositoryInterface.php index 36760b2db..c26230484 100644 --- a/app/Contracts/Repository/ScheduleRepositoryInterface.php +++ b/app/Contracts/Repository/ScheduleRepositoryInterface.php @@ -41,4 +41,12 @@ interface ScheduleRepositoryInterface extends RepositoryInterface * @return \Illuminate\Support\Collection */ public function getScheduleWithTasks($schedule); + + /** + * Return all of the schedules that should be processed. + * + * @param string $timestamp + * @return \Illuminate\Support\Collection + */ + public function getSchedulesToProcess($timestamp); } diff --git a/app/Contracts/Repository/TaskRepositoryInterface.php b/app/Contracts/Repository/TaskRepositoryInterface.php index f105747b8..e3a4fe0d7 100644 --- a/app/Contracts/Repository/TaskRepositoryInterface.php +++ b/app/Contracts/Repository/TaskRepositoryInterface.php @@ -26,4 +26,22 @@ namespace Pterodactyl\Contracts\Repository; interface TaskRepositoryInterface extends RepositoryInterface { + /** + * Get a task and the server relationship for that task. + * + * @param int $id + * @return \Pterodactyl\Models\Task + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getTaskWithServer($id); + + /** + * Returns the next task in a schedule. + * + * @param int $schedule the ID of the schedule to select the next task from + * @param int $index the index of the current task + * @return null|\Pterodactyl\Models\Task + */ + public function getNextTask($schedule, $index); } diff --git a/app/Jobs/Schedule/RunTaskJob.php b/app/Jobs/Schedule/RunTaskJob.php new file mode 100644 index 000000000..f933af0dc --- /dev/null +++ b/app/Jobs/Schedule/RunTaskJob.php @@ -0,0 +1,191 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Jobs\Schedule; + +use Exception; +use Carbon\Carbon; +use Pterodactyl\Jobs\Job; +use Webmozart\Assert\Assert; +use InvalidArgumentException; +use Illuminate\Queue\SerializesModels; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\DispatchesJobs; +use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; +use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface; + +class RunTaskJob extends Job implements ShouldQueue +{ + use DispatchesJobs, InteractsWithQueue, SerializesModels; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface + */ + protected $commandRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface + */ + protected $powerRepository; + + /** + * @var int + */ + protected $schedule; + + /** + * @var int + */ + public $task; + + /** + * @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface + */ + protected $taskRepository; + + /** + * RunTaskJob constructor. + * + * @param int $task + * @param int $schedule + */ + public function __construct($task, $schedule) + { + Assert::integerish($task, 'First argument passed to constructor must be numeric, received %s.'); + + $this->queue = app()->make('config')->get('pterodactyl.queues.standard'); + $this->task = $task; + $this->schedule = $schedule; + } + + /** + * Run the job and send actions to the daemon running the server. + * + * @param \Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface $commandRepository + * @param \Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface $powerRepository + * @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $taskRepository + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle( + CommandRepositoryInterface $commandRepository, + PowerRepositoryInterface $powerRepository, + TaskRepositoryInterface $taskRepository + ) { + $this->commandRepository = $commandRepository; + $this->powerRepository = $powerRepository; + $this->taskRepository = $taskRepository; + + $task = $this->taskRepository->getTaskWithServer($this->task); + $server = $task->server; + + // Perform the provided task aganist the daemon. + switch ($task->action) { + case 'power': + $this->powerRepository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($server->daemonSecret) + ->sendSignal($task->payload); + break; + case 'command': + $this->commandRepository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($server->daemonSecret) + ->send($task->payload); + break; + default: + throw new InvalidArgumentException('Cannot run a task that points to a non-existant action.'); + } + + $this->markTaskNotQueued(); + $this->queueNextTask($task->sequence_id); + } + + /** + * Handle a failure while sending the action to the daemon or otherwise processing the job. + * + * @param null|\Exception $exception + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function failed(Exception $exception = null) + { + $this->markTaskNotQueued(); + $this->markScheduleComplete(); + } + + /** + * Get the next task in the schedule and queue it for running after the defined period of wait time. + * + * @param int $sequence + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + private function queueNextTask($sequence) + { + $nextTask = $this->taskRepository->getNextTask($this->schedule, $sequence); + if (is_null($nextTask)) { + $this->markScheduleComplete(); + + return; + } + + $this->taskRepository->update($nextTask->id, ['is_queued' => true]); + $this->dispatch((new self($nextTask->id, $this->schedule))->delay($nextTask->time_offset)); + } + + /** + * Marks the parent schedule as being complete. + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + private function markScheduleComplete() + { + $repository = app()->make(ScheduleRepositoryInterface::class); + $repository->withoutFresh()->update($this->schedule, [ + 'is_processing' => false, + 'last_run_at' => app()->make(Carbon::class)->now()->toDateTimeString(), + ]); + } + + /** + * Mark a specific task as no longer being queued. + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + private function markTaskNotQueued() + { + $repository = app()->make(TaskRepositoryInterface::class); + $repository->update($this->task, ['is_queued' => false]); + } +} diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index aefea1984..6899d51a0 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -117,8 +117,8 @@ class Schedule extends Model implements CleansAttributes, ValidableContract 'cron_minute' => 'string', 'is_active' => 'boolean', 'is_processing' => 'boolean', - 'last_run_at' => 'nullable|timestamp', - 'next_run_at' => 'nullable|timestamp', + 'last_run_at' => 'nullable|date', + 'next_run_at' => 'nullable|date', ]; /** diff --git a/app/Models/Task.php b/app/Models/Task.php index c2b226da9..7995f4f54 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -27,12 +27,13 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Znck\Eloquent\Traits\BelongsToThrough; use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; class Task extends Model implements CleansAttributes, ValidableContract { - use Eloquence, Validable; + use BelongsToThrough, Eloquence, Validable; /** * The table associated with the model. @@ -130,14 +131,11 @@ class Task extends Model implements CleansAttributes, ValidableContract /** * Return the server a task is assigned to, acts as a belongsToThrough. * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + * @return \Znck\Eloquent\Relations\BelongsToThrough + * @throws \Exception */ public function server() { - if ($schedule = $this->schedule) { - return $schedule->server(); - } else { - throw new \InvalidArgumentException('Instance of Task must have an associated Schedule in the database.'); - } + return $this->belongsToThrough(Server::class, Schedule::class); } } diff --git a/app/Repositories/Eloquent/ScheduleRepository.php b/app/Repositories/Eloquent/ScheduleRepository.php index f30c80d1c..7c05847f6 100644 --- a/app/Repositories/Eloquent/ScheduleRepository.php +++ b/app/Repositories/Eloquent/ScheduleRepository.php @@ -52,4 +52,15 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor { return $this->getBuilder()->with('tasks')->find($schedule, $this->getColumns()); } + + /** + * {@inheritdoc} + */ + public function getSchedulesToProcess($timestamp) + { + return $this->getBuilder()->with('tasks') + ->where('is_active', true) + ->where('next_run_at', '<=', $timestamp) + ->get($this->getColumns()); + } } diff --git a/app/Repositories/Eloquent/TaskRepository.php b/app/Repositories/Eloquent/TaskRepository.php index 3cc5b10a9..c27b31123 100644 --- a/app/Repositories/Eloquent/TaskRepository.php +++ b/app/Repositories/Eloquent/TaskRepository.php @@ -42,33 +42,28 @@ class TaskRepository extends EloquentRepository implements TaskRepositoryInterfa /** * {@inheritdoc} */ - public function getParentTasksWithChainCount($server) + public function getTaskWithServer($id) { - Assert::numeric($server, 'First argument passed to GetParentTasksWithChainCount must be numeric, received %s.'); - - return $this->getBuilder()->withCount('chained')->where([ - ['server_id', '=', $server], - ['parent_task_id', '=', null], - ])->get($this->getColumns()); - } - - /** - * {@inheritdoc} - */ - public function getTaskForServer($task, $server) - { - Assert::numeric($task, 'First argument passed to getTaskForServer must be numeric, received %s.'); - Assert::numeric($server, 'Second argument passed to getTaskForServer must be numeric, received %s.'); - - $instance = $this->getBuilder()->with('chained')->where([ - ['server_id', '=', $server], - ['parent_task_id', '=', null], - ])->find($task, $this->getColumns()); + Assert::integerish($id, 'First argument passed to getTaskWithServer must be numeric, received %s.'); + $instance = $this->getBuilder()->with('server')->find($id, $this->getColumns()); if (! $instance) { throw new RecordNotFoundException; } return $instance; } + + /** + * {@inheritdoc} + */ + public function getNextTask($schedule, $index) + { + Assert::integerish($schedule, 'First argument passed to getNextTask must be integer, received %s.'); + Assert::integerish($index, 'Second argument passed to getNextTask must be integer, received %s.'); + + return $this->getBuilder()->where('schedule_id', '=', $schedule) + ->where('sequence_id', '=', $index + 1) + ->first($this->getColumns()); + } } diff --git a/app/Services/Schedules/ProcessScheduleService.php b/app/Services/Schedules/ProcessScheduleService.php new file mode 100644 index 000000000..6e2f61d28 --- /dev/null +++ b/app/Services/Schedules/ProcessScheduleService.php @@ -0,0 +1,80 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Schedules; + +use Cron\CronExpression; +use Webmozart\Assert\Assert; +use Pterodactyl\Models\Schedule; +use Pterodactyl\Services\Schedules\Tasks\RunTaskService; +use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; + +class ProcessScheduleService +{ + protected $repository; + + protected $runnerService; + + public function __construct( + RunTaskService $runnerService, + ScheduleRepositoryInterface $repository + ) { + $this->repository = $repository; + $this->runnerService = $runnerService; + } + + /** + * Process a schedule and push the first task onto the queue worker. + * + * @param int|\Pterodactyl\Models\Schedule $schedule + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($schedule) + { + Assert::true(($schedule instanceof Schedule || is_digit($schedule)), + 'First argument passed to handle must be instance of \Pterodactyl\Models\Schedule or an integer, received %s.' + ); + + if (($schedule instanceof Schedule && ! $schedule->relationLoaded('tasks')) || ! $schedule instanceof Schedule) { + $schedule = $this->repository->getScheduleWithTasks(is_digit($schedule) ? $schedule : $schedule->id); + } + + $formattedCron = sprintf('%s %s %s * %s *', + $schedule->cron_minute, + $schedule->cron_hour, + $schedule->cron_day_of_month, + $schedule->cron_day_of_week + ); + + $this->repository->update($schedule->id, [ + 'is_processing' => true, + 'next_run_at' => CronExpression::factory($formattedCron)->getNextRunDate(), + ]); + + $task = $schedule->tasks->where('sequence_id', 1)->first(); + $this->runnerService->handle($task); + } +} diff --git a/app/Services/Schedules/ScheduleCreationService.php b/app/Services/Schedules/ScheduleCreationService.php index 837469d95..a6ca7a079 100644 --- a/app/Services/Schedules/ScheduleCreationService.php +++ b/app/Services/Schedules/ScheduleCreationService.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Services\Schedules; +use Cron\CronExpression; use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; @@ -83,6 +84,7 @@ class ScheduleCreationService $server = ($server instanceof Server) ? $server->id : $server; $data['server_id'] = $server; + $data['next_run_at'] = $this->getCronTimestamp($data); $this->connection->beginTransaction(); $schedule = $this->repository->create($data); @@ -103,4 +105,22 @@ class ScheduleCreationService return $schedule; } + + /** + * Return a DateTime object after parsing the cron data provided. + * + * @param array $data + * @return \DateTime + */ + private function getCronTimestamp(array $data) + { + $formattedCron = sprintf('%s %s %s * %s *', + array_get($data, 'cron_minute', '*'), + array_get($data, 'cron_hour', '*'), + array_get($data, 'cron_day_of_month', '*'), + array_get($data, 'cron_day_of_week', '*') + ); + + return CronExpression::factory($formattedCron)->getNextRunDate(); + } } diff --git a/app/Services/Schedules/Tasks/RunTaskService.php b/app/Services/Schedules/Tasks/RunTaskService.php new file mode 100644 index 000000000..74d7557d3 --- /dev/null +++ b/app/Services/Schedules/Tasks/RunTaskService.php @@ -0,0 +1,75 @@ +. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Pterodactyl\Services\Schedules\Tasks; + +use Pterodactyl\Models\Task; +use Illuminate\Contracts\Bus\Dispatcher; +use Pterodactyl\Jobs\Schedule\RunTaskJob; +use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; + +class RunTaskService +{ + /** + * @var \Illuminate\Contracts\Bus\Dispatcher + */ + protected $dispatcher; + + /** + * @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface + */ + protected $repository; + + /** + * RunTaskService constructor. + * + * @param \Illuminate\Contracts\Bus\Dispatcher $dispatcher + * @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $repository + */ + public function __construct( + Dispatcher $dispatcher, + TaskRepositoryInterface $repository + ) { + $this->dispatcher = $dispatcher; + $this->repository = $repository; + } + + /** + * Push a single task onto the queue. + * + * @param int|\Pterodactyl\Models\Task $task + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($task) + { + if (! $task instanceof Task) { + $task = $this->repository->find($task); + } + + $this->repository->update($task->id, ['is_queued' => true]); + $this->dispatcher->dispatch((new RunTaskJob($task->id, $task->schedule_id))->delay($task->time_offset)); + } +} diff --git a/composer.json b/composer.json index a81baeec8..db04ade72 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,8 @@ "spatie/laravel-fractal": "^4.0", "watson/validating": "^3.0", "webmozart/assert": "^1.2", - "webpatser/laravel-uuid": "^2.0" + "webpatser/laravel-uuid": "^2.0", + "znck/belongs-to-through": "^2.3" }, "require-dev": { "barryvdh/laravel-debugbar": "^2.4", diff --git a/composer.lock b/composer.lock index 5b4809a02..4c112e5f4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "15a4dc6de122bc1e47d1d9ca3b1224d6", + "content-hash": "46a0a06ec8f3af50ed6ec05c2bb3b9a3", "packages": [ { "name": "aws/aws-sdk-php", @@ -3891,6 +3891,58 @@ "UUID RFC4122" ], "time": "2017-08-30T20:45:29+00:00" + }, + { + "name": "znck/belongs-to-through", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/znck/belongs-to-through.git", + "reference": "8ac53e9134072902a8d3f3e18c327d4a8fd70d3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/znck/belongs-to-through/zipball/8ac53e9134072902a8d3f3e18c327d4a8fd70d3d", + "reference": "8ac53e9134072902a8d3f3e18c327d4a8fd70d3d", + "shasum": "" + }, + "require": { + "illuminate/database": "~5.0" + }, + "require-dev": { + "fabpot/php-cs-fixer": "^1.11", + "orchestra/testbench": "~3.0", + "phpunit/php-code-coverage": "^3.3", + "phpunit/phpunit": "~5.0", + "satooshi/php-coveralls": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Znck\\Eloquent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rahul Kadyan", + "email": "hi@znck.me" + } + ], + "description": "Adds belongsToThrough relation to laravel models", + "homepage": "https://github.com/znck/belongs-to-through", + "keywords": [ + "belongsToThrough", + "eloquent", + "laravel", + "model", + "models", + "znck" + ], + "time": "2017-07-23T13:11:16+00:00" } ], "packages-dev": [ diff --git a/resources/lang/en/command/messages.php b/resources/lang/en/command/messages.php index 9335b2e5b..c6540d6d9 100644 --- a/resources/lang/en/command/messages.php +++ b/resources/lang/en/command/messages.php @@ -51,4 +51,7 @@ return [ ], '2fa_disabled' => '2-Factor authentication has been disabled for :email.', ], + 'schedule' => [ + 'output_line' => 'Dispatching job for first task in `:schedule` (:hash).', + ], ]; diff --git a/tests/Unit/Services/Schedules/ScheduleCreationServiceTest.php b/tests/Unit/Services/Schedules/ScheduleCreationServiceTest.php index 98f0b105c..7ef399680 100644 --- a/tests/Unit/Services/Schedules/ScheduleCreationServiceTest.php +++ b/tests/Unit/Services/Schedules/ScheduleCreationServiceTest.php @@ -41,6 +41,11 @@ class ScheduleCreationServiceTest extends TestCase */ protected $connection; + /** + * @var \Cron\CronExpression + */ + protected $cron; + /** * @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface */ @@ -64,6 +69,7 @@ class ScheduleCreationServiceTest extends TestCase parent::setUp(); $this->connection = m::mock(ConnectionInterface::class); + $this->cron = m::mock('overload:\Cron\CronExpression'); $this->repository = m::mock(ScheduleRepositoryInterface::class); $this->taskCreationService = m::mock(TaskCreationService::class); @@ -78,8 +84,14 @@ class ScheduleCreationServiceTest extends TestCase $schedule = factory(Schedule::class)->make(); $server = factory(Server::class)->make(); + $this->cron->shouldReceive('factory')->with('* * * * * *')->once()->andReturnSelf() + ->shouldReceive('getNextRunDate')->withNoArgs()->once()->andReturn('nextDate'); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('create')->with(['server_id' => $server->id, 'test_data' => 'test_value'])->once()->andReturn($schedule); + $this->repository->shouldReceive('create')->with([ + 'server_id' => $server->id, + 'next_run_at' => 'nextDate', + 'test_data' => 'test_value', + ])->once()->andReturn($schedule); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $response = $this->service->handle($server, ['test_data' => 'test_value']); @@ -96,8 +108,14 @@ class ScheduleCreationServiceTest extends TestCase $schedule = factory(Schedule::class)->make(); $server = factory(Server::class)->make(); + $this->cron->shouldReceive('factory')->with('* * * * * *')->once()->andReturnSelf() + ->shouldReceive('getNextRunDate')->withNoArgs()->once()->andReturn('nextDate'); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('create')->with(['server_id' => $server->id, 'test_data' => 'test_value'])->once()->andReturn($schedule); + $this->repository->shouldReceive('create')->with([ + 'next_run_at' => 'nextDate', + 'server_id' => $server->id, + 'test_data' => 'test_value', + ])->once()->andReturn($schedule); $this->taskCreationService->shouldReceive('handle')->with($schedule, [ 'time_interval' => 'm', 'time_value' => 10, @@ -123,8 +141,14 @@ class ScheduleCreationServiceTest extends TestCase { $schedule = factory(Schedule::class)->make(); + $this->cron->shouldReceive('factory')->with('* * * * * *')->once()->andReturnSelf() + ->shouldReceive('getNextRunDate')->withNoArgs()->once()->andReturn('nextDate'); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('create')->with(['server_id' => 1234, 'test_data' => 'test_value'])->once()->andReturn($schedule); + $this->repository->shouldReceive('create')->with([ + 'next_run_at' => 'nextDate', + 'server_id' => 1234, + 'test_data' => 'test_value', + ])->once()->andReturn($schedule); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $response = $this->service->handle(1234, ['test_data' => 'test_value']);