From 92cd659db314d2755494dabbd0ad5637a08fe7fa Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 May 2021 10:44:40 -0700 Subject: [PATCH] Add underlying data changes necessary for new task & schedule features --- .../Api/Client/Servers/ScheduleController.php | 3 +- .../Client/Servers/ScheduleTaskController.php | 2 + app/Jobs/Schedule/RunTaskJob.php | 40 ++++-- app/Models/Schedule.php | 6 + app/Models/Task.php | 12 ++ .../Api/Client/ScheduleTransformer.php | 1 + .../Api/Client/TaskTransformer.php | 1 + ...dd_continue_on_failure_option_to_tasks.php | 32 +++++ ...when_server_online_option_to_schedules.php | 32 +++++ .../schedules/createOrUpdateSchedule.ts | 27 ++-- .../server/schedules/getServerSchedules.ts | 20 +-- .../scripts/components/elements/Field.tsx | 4 +- .../server/schedules/EditScheduleModal.tsx | 128 ++++++++---------- 13 files changed, 201 insertions(+), 107 deletions(-) create mode 100644 database/migrations/2021_05_01_092457_add_continue_on_failure_option_to_tasks.php create mode 100644 database/migrations/2021_05_01_092523_add_only_run_when_server_online_option_to_schedules.php diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php index 5b2609e58..0bbc6bd73 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php @@ -16,7 +16,6 @@ use Pterodactyl\Services\Schedules\ProcessScheduleService; use Pterodactyl\Transformers\Api\Client\ScheduleTransformer; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest; @@ -81,6 +80,7 @@ class ScheduleController extends ClientApiController 'cron_hour' => $request->input('hour'), 'cron_minute' => $request->input('minute'), 'is_active' => (bool) $request->input('is_active'), + 'only_when_online' => (bool) $request->input('only_when_online'), 'next_run_at' => $this->getNextRunAt($request), ]); @@ -128,6 +128,7 @@ class ScheduleController extends ClientApiController 'cron_hour' => $request->input('hour'), 'cron_minute' => $request->input('minute'), 'is_active' => $active, + 'only_when_online' => (bool) $request->input('only_when_online'), 'next_run_at' => $this->getNextRunAt($request), ]; diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php index a652071a2..12bcf968c 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php @@ -59,6 +59,7 @@ class ScheduleTaskController extends ClientApiController 'action' => $request->input('action'), 'payload' => $request->input('payload') ?? '', 'time_offset' => $request->input('time_offset'), + 'continue_on_failure' => (bool) $request->input('continue_on_failure'), ]); return $this->fractal->item($task) @@ -84,6 +85,7 @@ class ScheduleTaskController extends ClientApiController 'action' => $request->input('action'), 'payload' => $request->input('payload') ?? '', 'time_offset' => $request->input('time_offset'), + 'continue_on_failure' => (bool) $request->input('continue_on_failure'), ]); return $this->fractal->item($task->refresh()) diff --git a/app/Jobs/Schedule/RunTaskJob.php b/app/Jobs/Schedule/RunTaskJob.php index f60711f43..69a0d171f 100644 --- a/app/Jobs/Schedule/RunTaskJob.php +++ b/app/Jobs/Schedule/RunTaskJob.php @@ -7,6 +7,7 @@ use Pterodactyl\Jobs\Job; use Carbon\CarbonImmutable; use Pterodactyl\Models\Task; use InvalidArgumentException; +use Illuminate\Http\Response; use Pterodactyl\Models\Schedule; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; @@ -15,6 +16,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs; use Pterodactyl\Services\Backups\InitiateBackupService; use Pterodactyl\Repositories\Wings\DaemonPowerRepository; use Pterodactyl\Repositories\Wings\DaemonCommandRepository; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; class RunTaskJob extends Job implements ShouldQueue { @@ -62,18 +64,32 @@ class RunTaskJob extends Job implements ShouldQueue $server = $this->task->server; // Perform the provided task against the daemon. - switch ($this->task->action) { - case 'power': - $powerRepository->setServer($server)->send($this->task->payload); - break; - case 'command': - $commandRepository->setServer($server)->send($this->task->payload); - break; - case 'backup': - $backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true); - break; - default: - throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.'); + try { + switch ($this->task->action) { + case Task::ACTION_POWER: + $powerRepository->setServer($server)->send($this->task->payload); + break; + case Task::ACTION_COMMAND: + $commandRepository->setServer($server)->send($this->task->payload); + break; + case Task::ACTION_BACKUP: + $backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true); + break; + default: + throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.'); + } + } catch (Exception $exception) { + if ($exception instanceof DaemonConnectionException) { + // If the task "failed" because the server is offline and it was sending a command or + // executing a power action (which shouldn't happen?) then just stop trying to process + // the schedule, but don't actually log the failure. + if ($this->task->action === Task::ACTION_POWER || $this->task->action === Task::ACTION_COMMAND) { + // Do the thing + if ($exception->getStatusCode() === Response::HTTP_CONFLICT) { + } + } + } + throw $exception; } $this->markTaskNotQueued(); diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index 240873cac..82a43c732 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -18,6 +18,7 @@ use Pterodactyl\Contracts\Extensions\HashidsInterface; * @property string $cron_minute * @property bool $is_active * @property bool $is_processing + * @property bool $only_when_online * @property \Carbon\Carbon|null $last_run_at * @property \Carbon\Carbon|null $next_run_at * @property \Carbon\Carbon $created_at @@ -63,6 +64,7 @@ class Schedule extends Model 'cron_minute', 'is_active', 'is_processing', + 'only_when_online', 'last_run_at', 'next_run_at', ]; @@ -75,6 +77,7 @@ class Schedule extends Model 'server_id' => 'integer', 'is_active' => 'boolean', 'is_processing' => 'boolean', + 'only_when_online' => 'boolean', ]; /** @@ -99,6 +102,7 @@ class Schedule extends Model 'cron_minute' => '*', 'is_active' => true, 'is_processing' => false, + 'only_when_online' => false, ]; /** @@ -114,6 +118,7 @@ class Schedule extends Model 'cron_minute' => 'required|string', 'is_active' => 'boolean', 'is_processing' => 'boolean', + 'only_when_online' => 'boolean', 'last_run_at' => 'nullable|date', 'next_run_at' => 'nullable|date', ]; @@ -122,6 +127,7 @@ class Schedule extends Model * Returns the schedule's execution crontab entry as a string. * * @return \Carbon\CarbonImmutable + * * @throws \Exception */ public function getNextRunDate() diff --git a/app/Models/Task.php b/app/Models/Task.php index 84bd3cbf6..82ba72370 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -14,6 +14,7 @@ use Pterodactyl\Contracts\Extensions\HashidsInterface; * @property string $payload * @property int $time_offset * @property bool $is_queued + * @property bool $continue_on_failure * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $updated_at * @property string $hashid @@ -30,6 +31,13 @@ class Task extends Model */ public const RESOURCE_NAME = 'schedule_task'; + /** + * The default actions that can exist for a task in Pterodactyl. + */ + public const ACTION_POWER = 'power'; + public const ACTION_COMMAND = 'command'; + public const ACTION_BACKUP = 'backup'; + /** * The table associated with the model. * @@ -56,6 +64,7 @@ class Task extends Model 'payload', 'time_offset', 'is_queued', + 'continue_on_failure', ]; /** @@ -69,6 +78,7 @@ class Task extends Model 'sequence_id' => 'integer', 'time_offset' => 'integer', 'is_queued' => 'boolean', + 'continue_on_failure' => 'boolean', ]; /** @@ -79,6 +89,7 @@ class Task extends Model protected $attributes = [ 'time_offset' => 0, 'is_queued' => false, + 'continue_on_failure' => false, ]; /** @@ -91,6 +102,7 @@ class Task extends Model 'payload' => 'required_unless:action,backup|string', 'time_offset' => 'required|numeric|between:0,900', 'is_queued' => 'boolean', + 'continue_on_failure' => 'boolean', ]; /** diff --git a/app/Transformers/Api/Client/ScheduleTransformer.php b/app/Transformers/Api/Client/ScheduleTransformer.php index 44ea2c555..25ef9fb58 100644 --- a/app/Transformers/Api/Client/ScheduleTransformer.php +++ b/app/Transformers/Api/Client/ScheduleTransformer.php @@ -45,6 +45,7 @@ class ScheduleTransformer extends BaseClientTransformer ], 'is_active' => $model->is_active, 'is_processing' => $model->is_processing, + 'only_when_online' => $model->only_when_online, 'last_run_at' => $model->last_run_at ? $model->last_run_at->toIso8601String() : null, 'next_run_at' => $model->next_run_at ? $model->next_run_at->toIso8601String() : null, 'created_at' => $model->created_at->toIso8601String(), diff --git a/app/Transformers/Api/Client/TaskTransformer.php b/app/Transformers/Api/Client/TaskTransformer.php index 0b2956262..a2e62cf51 100644 --- a/app/Transformers/Api/Client/TaskTransformer.php +++ b/app/Transformers/Api/Client/TaskTransformer.php @@ -28,6 +28,7 @@ class TaskTransformer extends BaseClientTransformer 'payload' => $model->payload, 'time_offset' => $model->time_offset, 'is_queued' => $model->is_queued, + 'continue_on_failure' => $model->continue_on_failure, 'created_at' => $model->created_at->toIso8601String(), 'updated_at' => $model->updated_at->toIso8601String(), ]; diff --git a/database/migrations/2021_05_01_092457_add_continue_on_failure_option_to_tasks.php b/database/migrations/2021_05_01_092457_add_continue_on_failure_option_to_tasks.php new file mode 100644 index 000000000..4de4b59d4 --- /dev/null +++ b/database/migrations/2021_05_01_092457_add_continue_on_failure_option_to_tasks.php @@ -0,0 +1,32 @@ +unsignedTinyInteger('continue_on_failure')->after('is_queued')->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('tasks', function (Blueprint $table) { + $table->dropColumn('continue_on_failure'); + }); + } +} diff --git a/database/migrations/2021_05_01_092523_add_only_run_when_server_online_option_to_schedules.php b/database/migrations/2021_05_01_092523_add_only_run_when_server_online_option_to_schedules.php new file mode 100644 index 000000000..7fa9758ed --- /dev/null +++ b/database/migrations/2021_05_01_092523_add_only_run_when_server_online_option_to_schedules.php @@ -0,0 +1,32 @@ +unsignedTinyInteger('only_when_online')->after('is_processing')->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('schedules', function (Blueprint $table) { + $table->dropColumn('only_when_online'); + }); + } +} diff --git a/resources/scripts/api/server/schedules/createOrUpdateSchedule.ts b/resources/scripts/api/server/schedules/createOrUpdateSchedule.ts index 481cea777..de1bc075b 100644 --- a/resources/scripts/api/server/schedules/createOrUpdateSchedule.ts +++ b/resources/scripts/api/server/schedules/createOrUpdateSchedule.ts @@ -1,20 +1,19 @@ import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules'; import http from '@/api/http'; -type Data = Pick & { id?: number } +type Data = Pick & { id?: number } -export default (uuid: string, schedule: Data): Promise => { - return new Promise((resolve, reject) => { - http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, { - is_active: schedule.isActive, - name: schedule.name, - minute: schedule.cron.minute, - hour: schedule.cron.hour, - day_of_month: schedule.cron.dayOfMonth, - month: schedule.cron.month, - day_of_week: schedule.cron.dayOfWeek, - }) - .then(({ data }) => resolve(rawDataToServerSchedule(data.attributes))) - .catch(reject); +export default async (uuid: string, schedule: Data): Promise => { + const { data } = await http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, { + is_active: schedule.isActive, + only_when_online: schedule.onlyWhenOnline, + name: schedule.name, + minute: schedule.cron.minute, + hour: schedule.cron.hour, + day_of_month: schedule.cron.dayOfMonth, + month: schedule.cron.month, + day_of_week: schedule.cron.dayOfWeek, }); + + return rawDataToServerSchedule(data.attributes); }; diff --git a/resources/scripts/api/server/schedules/getServerSchedules.ts b/resources/scripts/api/server/schedules/getServerSchedules.ts index f7a07617c..c052e7a3c 100644 --- a/resources/scripts/api/server/schedules/getServerSchedules.ts +++ b/resources/scripts/api/server/schedules/getServerSchedules.ts @@ -12,6 +12,7 @@ export interface Schedule { }; isActive: boolean; isProcessing: boolean; + onlyWhenOnline: boolean; lastRunAt: Date | null; nextRunAt: Date | null; createdAt: Date; @@ -27,6 +28,7 @@ export interface Task { payload: string; timeOffset: number; isQueued: boolean; + continueOnFailure: boolean; createdAt: Date; updatedAt: Date; } @@ -38,6 +40,7 @@ export const rawDataToServerTask = (data: any): Task => ({ payload: data.payload, timeOffset: data.time_offset, isQueued: data.is_queued, + continueOnFailure: data.continue_on_failure, createdAt: new Date(data.created_at), updatedAt: new Date(data.updated_at), }); @@ -54,6 +57,7 @@ export const rawDataToServerSchedule = (data: any): Schedule => ({ }, isActive: data.is_active, isProcessing: data.is_processing, + onlyWhenOnline: data.only_when_online, lastRunAt: data.last_run_at ? new Date(data.last_run_at) : null, nextRunAt: data.next_run_at ? new Date(data.next_run_at) : null, createdAt: new Date(data.created_at), @@ -62,14 +66,12 @@ export const rawDataToServerSchedule = (data: any): Schedule => ({ tasks: (data.relationships?.tasks?.data || []).map((row: any) => rawDataToServerTask(row.attributes)), }); -export default (uuid: string): Promise => { - return new Promise((resolve, reject) => { - http.get(`/api/client/servers/${uuid}/schedules`, { - params: { - include: [ 'tasks' ], - }, - }) - .then(({ data }) => resolve((data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes)))) - .catch(reject); +export default async (uuid: string): Promise => { + const { data } = await http.get(`/api/client/servers/${uuid}/schedules`, { + params: { + include: [ 'tasks' ], + }, }); + + return (data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes)); }; diff --git a/resources/scripts/components/elements/Field.tsx b/resources/scripts/components/elements/Field.tsx index 0c76f285c..e7ea16679 100644 --- a/resources/scripts/components/elements/Field.tsx +++ b/resources/scripts/components/elements/Field.tsx @@ -17,7 +17,7 @@ const Field = forwardRef(({ id, name, light = false, la { ({ field, form: { errors, touched } }: FieldProps) => ( - <> +
{label && } @@ -35,7 +35,7 @@ const Field = forwardRef(({ id, name, light = false, la : description ?

{description}

: null } - +
) }
diff --git a/resources/scripts/components/server/schedules/EditScheduleModal.tsx b/resources/scripts/components/server/schedules/EditScheduleModal.tsx index e0df9e981..3db483244 100644 --- a/resources/scripts/components/server/schedules/EditScheduleModal.tsx +++ b/resources/scripts/components/server/schedules/EditScheduleModal.tsx @@ -1,8 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useContext, useEffect } from 'react'; import { Schedule } from '@/api/server/schedules/getServerSchedules'; -import Modal, { RequiredModalProps } from '@/components/elements/Modal'; import Field from '@/components/elements/Field'; -import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; +import { Form, Formik, FormikHelpers } from 'formik'; import FormikSwitch from '@/components/elements/FormikSwitch'; import createOrUpdateSchedule from '@/api/server/schedules/createOrUpdateSchedule'; import { ServerContext } from '@/state/server'; @@ -11,10 +10,12 @@ import FlashMessageRender from '@/components/FlashMessageRender'; import useFlash from '@/plugins/useFlash'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; +import ModalContext from '@/context/ModalContext'; +import asModal from '@/hoc/asModal'; -type Props = { +interface Props { schedule?: Schedule; -} & RequiredModalProps; +} interface Values { name: string; @@ -24,70 +25,21 @@ interface Values { hour: string; minute: string; enabled: boolean; + onlyWhenOnline: boolean; } -const EditScheduleModal = ({ schedule, ...props }: Omit) => { - const { isSubmitting } = useFormikContext(); - - return ( - -

{schedule ? 'Edit schedule' : 'Create new schedule'}

- -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-

- The schedule system supports the use of Cronjob syntax when defining when tasks should begin - running. Use the fields above to specify when these tasks should begin running. -

-
- -
-
- -
- -
- ); -}; - -export default ({ schedule, visible, ...props }: Props) => { - const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); +const EditScheduleModal = ({ schedule }: Props) => { const { addError, clearFlashes } = useFlash(); - const [ modalVisible, setModalVisible ] = useState(visible); + const { dismiss } = useContext(ModalContext); + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule); useEffect(() => { - setModalVisible(visible); - clearFlashes('schedule:edit'); - }, [ visible ]); + return () => { + clearFlashes('schedule:edit'); + }; + }, []); const submit = (values: Values, { setSubmitting }: FormikHelpers) => { clearFlashes('schedule:edit'); @@ -101,12 +53,13 @@ export default ({ schedule, visible, ...props }: Props) => { month: values.month, dayOfMonth: values.dayOfMonth, }, + onlyWhenOnline: values.onlyWhenOnline, isActive: values.enabled, }) .then(schedule => { setSubmitting(false); appendSchedule(schedule); - setModalVisible(false); + dismiss(); }) .catch(error => { console.error(error); @@ -128,13 +81,50 @@ export default ({ schedule, visible, ...props }: Props) => { dayOfWeek: schedule?.cron.dayOfWeek || '*', enabled: schedule ? schedule.isActive : true, } as Values} - validationSchema={null} > - + {({ isSubmitting }) => ( +
+

{schedule ? 'Edit schedule' : 'Create new schedule'}

+ + +
+ + + + + +
+

+ The schedule system supports the use of Cronjob syntax when defining when tasks should begin + running. Use the fields above to specify when these tasks should begin running. +

+
+ +
+
+ +
+
+ +
+ + )} ); }; + +export default asModal()(EditScheduleModal);