Skip to content

Commit c1ee0ac

Browse files
committed
Add support for executing a scheduled task right now
1 parent f33d0b1 commit c1ee0ac

File tree

13 files changed

+158
-157
lines changed

13 files changed

+158
-157
lines changed

app/Contracts/Repository/ScheduleRepositoryInterface.php

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,6 @@ interface ScheduleRepositoryInterface extends RepositoryInterface
1515
*/
1616
public function findServerSchedules(int $server): Collection;
1717

18-
/**
19-
* Load the tasks relationship onto the Schedule module if they are not
20-
* already present.
21-
*
22-
* @param \Pterodactyl\Models\Schedule $schedule
23-
* @param bool $refresh
24-
* @return \Pterodactyl\Models\Schedule
25-
*/
26-
public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule;
27-
2818
/**
2919
* Return a schedule model with all of the associated tasks as a relationship.
3020
*

app/Http/Controllers/Api/Client/Servers/ScheduleController.php

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,19 @@
1010
use Pterodactyl\Models\Schedule;
1111
use Illuminate\Http\JsonResponse;
1212
use Pterodactyl\Helpers\Utilities;
13+
use Pterodactyl\Jobs\Schedule\RunTaskJob;
1314
use Pterodactyl\Exceptions\DisplayException;
1415
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
16+
use Pterodactyl\Services\Schedules\ProcessScheduleService;
1517
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
1618
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
1719
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
20+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
1821
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest;
1922
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest;
2023
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest;
2124
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest;
25+
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest;
2226

2327
class ScheduleController extends ClientApiController
2428
{
@@ -27,16 +31,23 @@ class ScheduleController extends ClientApiController
2731
*/
2832
private $repository;
2933

34+
/**
35+
* @var \Pterodactyl\Services\Schedules\ProcessScheduleService
36+
*/
37+
private $service;
38+
3039
/**
3140
* ScheduleController constructor.
3241
*
3342
* @param \Pterodactyl\Repositories\Eloquent\ScheduleRepository $repository
43+
* @param \Pterodactyl\Services\Schedules\ProcessScheduleService $service
3444
*/
35-
public function __construct(ScheduleRepository $repository)
45+
public function __construct(ScheduleRepository $repository, ProcessScheduleService $service)
3646
{
3747
parent::__construct();
3848

3949
$this->repository = $repository;
50+
$this->service = $service;
4051
}
4152

4253
/**
@@ -147,6 +158,30 @@ public function update(UpdateScheduleRequest $request, Server $server, Schedule
147158
->toArray();
148159
}
149160

161+
/**
162+
* Executes a given schedule immediately rather than waiting on it's normally scheduled time
163+
* to pass. This does not care about the schedule state.
164+
*
165+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest $request
166+
* @param \Pterodactyl\Models\Server $server
167+
* @param \Pterodactyl\Models\Schedule $schedule
168+
* @return \Illuminate\Http\JsonResponse
169+
*
170+
* @throws \Throwable
171+
*/
172+
public function execute(TriggerScheduleRequest $request, Server $server, Schedule $schedule)
173+
{
174+
if (!$schedule->is_active) {
175+
throw new BadRequestHttpException(
176+
'Cannot trigger schedule exection for a schedule that is not currently active.'
177+
);
178+
}
179+
180+
$this->service->handle($schedule, true);
181+
182+
return new JsonResponse([], JsonResponse::HTTP_ACCEPTED);
183+
}
184+
150185
/**
151186
* Deletes a schedule and it's associated tasks.
152187
*
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
4+
5+
use Pterodactyl\Models\Permission;
6+
use Illuminate\Foundation\Http\FormRequest;
7+
8+
class TriggerScheduleRequest extends FormRequest
9+
{
10+
/**
11+
* @return string
12+
*/
13+
public function permission(): string
14+
{
15+
return Permission::ACTION_SCHEDULE_UPDATE;
16+
}
17+
18+
/**
19+
* @return array
20+
*/
21+
public function rules()
22+
{
23+
return [];
24+
}
25+
}

app/Jobs/Schedule/RunTaskJob.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,13 @@ public function __construct(Task $task)
4242
* @param \Pterodactyl\Repositories\Wings\DaemonCommandRepository $commandRepository
4343
* @param \Pterodactyl\Services\Backups\InitiateBackupService $backupService
4444
* @param \Pterodactyl\Repositories\Wings\DaemonPowerRepository $powerRepository
45-
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $taskRepository
4645
*
4746
* @throws \Throwable
4847
*/
4948
public function handle(
5049
DaemonCommandRepository $commandRepository,
5150
InitiateBackupService $backupService,
52-
DaemonPowerRepository $powerRepository,
53-
TaskRepository $taskRepository
51+
DaemonPowerRepository $powerRepository
5452
) {
5553
// Do not process a task that is not set to active.
5654
if (! $this->task->schedule->is_active) {

app/Models/Schedule.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Pterodactyl\Models;
44

5+
use Cron\CronExpression;
6+
use Carbon\CarbonImmutable;
57
use Illuminate\Container\Container;
68
use Pterodactyl\Contracts\Extensions\HashidsInterface;
79

@@ -114,6 +116,20 @@ class Schedule extends Model
114116
'next_run_at' => 'nullable|date',
115117
];
116118

119+
/**
120+
* Returns the schedule's execution crontab entry as a string.
121+
*
122+
* @return \Carbon\CarbonImmutable
123+
*/
124+
public function getNextRunDate()
125+
{
126+
$formatted = sprintf('%s %s %s * %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_day_of_week);
127+
128+
return CarbonImmutable::createFromTimestamp(
129+
CronExpression::factory($formatted)->getNextRunDate()->getTimestamp()
130+
);
131+
}
132+
117133
/**
118134
* Return a hashid encoded string to represent the ID of the schedule.
119135
*

app/Repositories/Eloquent/ScheduleRepository.php

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,6 @@ public function findServerSchedules(int $server): Collection
3131
return $this->getBuilder()->withCount('tasks')->where('server_id', '=', $server)->get($this->getColumns());
3232
}
3333

34-
/**
35-
* Load the tasks relationship onto the Schedule module if they are not
36-
* already present.
37-
*
38-
* @param \Pterodactyl\Models\Schedule $schedule
39-
* @param bool $refresh
40-
* @return \Pterodactyl\Models\Schedule
41-
*/
42-
public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule
43-
{
44-
if (! $schedule->relationLoaded('tasks') || $refresh) {
45-
$schedule->load('tasks');
46-
}
47-
48-
return $schedule;
49-
}
50-
5134
/**
5235
* Return a schedule model with all of the associated tasks as a relationship.
5336
*

app/Services/Schedules/ProcessScheduleService.php

Lines changed: 19 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
namespace Pterodactyl\Services\Schedules;
44

5-
use Cron\CronExpression;
65
use Pterodactyl\Models\Schedule;
76
use Illuminate\Contracts\Bus\Dispatcher;
87
use Pterodactyl\Jobs\Schedule\RunTaskJob;
9-
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
10-
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
8+
use Illuminate\Database\ConnectionInterface;
119

1210
class ProcessScheduleService
1311
{
@@ -17,62 +15,45 @@ class ProcessScheduleService
1715
private $dispatcher;
1816

1917
/**
20-
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
18+
* @var \Illuminate\Database\ConnectionInterface
2119
*/
22-
private $scheduleRepository;
23-
24-
/**
25-
* @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface
26-
*/
27-
private $taskRepository;
20+
private $connection;
2821

2922
/**
3023
* ProcessScheduleService constructor.
3124
*
25+
* @param \Illuminate\Database\ConnectionInterface $connection
3226
* @param \Illuminate\Contracts\Bus\Dispatcher $dispatcher
33-
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $scheduleRepository
34-
* @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $taskRepository
3527
*/
36-
public function __construct(
37-
Dispatcher $dispatcher,
38-
ScheduleRepositoryInterface $scheduleRepository,
39-
TaskRepositoryInterface $taskRepository
40-
) {
28+
public function __construct(ConnectionInterface $connection, Dispatcher $dispatcher)
29+
{
4130
$this->dispatcher = $dispatcher;
42-
$this->scheduleRepository = $scheduleRepository;
43-
$this->taskRepository = $taskRepository;
31+
$this->connection = $connection;
4432
}
4533

4634
/**
4735
* Process a schedule and push the first task onto the queue worker.
4836
*
4937
* @param \Pterodactyl\Models\Schedule $schedule
38+
* @param bool $now
5039
*
51-
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
52-
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
40+
* @throws \Throwable
5341
*/
54-
public function handle(Schedule $schedule)
42+
public function handle(Schedule $schedule, bool $now = false)
5543
{
56-
$this->scheduleRepository->loadTasks($schedule);
57-
5844
/** @var \Pterodactyl\Models\Task $task */
59-
$task = $schedule->getRelation('tasks')->where('sequence_id', 1)->first();
60-
61-
$formattedCron = sprintf('%s %s %s * %s',
62-
$schedule->cron_minute,
63-
$schedule->cron_hour,
64-
$schedule->cron_day_of_month,
65-
$schedule->cron_day_of_week
66-
);
45+
$task = $schedule->tasks()->where('sequence_id', 1)->firstOrFail();
6746

68-
$this->scheduleRepository->update($schedule->id, [
69-
'is_processing' => true,
70-
'next_run_at' => CronExpression::factory($formattedCron)->getNextRunDate(),
71-
]);
47+
$this->connection->transaction(function () use ($schedule, $task) {
48+
$schedule->forceFill([
49+
'is_processing' => true,
50+
'next_run_at' => $schedule->getNextRunDate(),
51+
])->saveOrFail();
7252

73-
$this->taskRepository->update($task->id, ['is_queued' => true]);
53+
$task->update(['is_queued' => true]);
54+
});
7455

75-
$this->dispatcher->dispatch(
56+
$this->dispatcher->{$now ? 'dispatchNow' : 'dispatch'}(
7657
(new RunTaskJob($task))->delay($task->time_offset)
7758
);
7859
}

resources/scripts/api/server/schedules/getServerSchedules.tsx renamed to resources/scripts/api/server/schedules/getServerSchedules.ts

File renamed without changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import http from '@/api/http';
2+
3+
export default async (server: string, schedule: number): Promise<void> =>
4+
await http.post(`/api/client/servers/${server}/schedules/${schedule}/execute`);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React, { useCallback, useState } from 'react';
2+
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
3+
import tw from 'twin.macro';
4+
import Button from '@/components/elements/Button';
5+
import triggerScheduleExecution from '@/api/server/schedules/triggerScheduleExecution';
6+
import { ServerContext } from '@/state/server';
7+
import useFlash from '@/plugins/useFlash';
8+
import { Schedule } from '@/api/server/schedules/getServerSchedules';
9+
10+
const RunScheduleButton = ({ schedule }: { schedule: Schedule }) => {
11+
const [ loading, setLoading ] = useState(false);
12+
const { clearFlashes, clearAndAddHttpError } = useFlash();
13+
14+
const id = ServerContext.useStoreState(state => state.server.data!.id);
15+
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
16+
17+
const onTriggerExecute = useCallback(() => {
18+
clearFlashes('schedule');
19+
setLoading(true);
20+
triggerScheduleExecution(id, schedule.id)
21+
.then(() => {
22+
setLoading(false);
23+
appendSchedule({ ...schedule, isProcessing: true });
24+
})
25+
.catch(error => {
26+
console.error(error);
27+
clearAndAddHttpError({ error, key: 'schedules' });
28+
})
29+
.then(() => setLoading(false));
30+
}, []);
31+
32+
return (
33+
<>
34+
<SpinnerOverlay visible={loading} size={'large'}/>
35+
<Button
36+
isSecondary
37+
color={'grey'}
38+
css={tw`flex-1 sm:flex-none border-transparent`}
39+
disabled={schedule.isProcessing}
40+
onClick={onTriggerExecute}
41+
>
42+
Run Now
43+
</Button>
44+
</>
45+
);
46+
};
47+
48+
export default RunScheduleButton;

0 commit comments

Comments
 (0)