Merge branch 'develop' into feature/react-admin

This commit is contained in:
Matthew Penner 2021-05-18 17:07:25 -06:00
commit a3b59f24af
95 changed files with 2671 additions and 1777 deletions

View file

@ -15,6 +15,7 @@ class UpgradeCommand extends Command
/** @var string */
protected $signature = 'p:upgrade
{--user= : The user that PHP runs under. All files will be owned by this user.}
{--group= : The group that PHP runs under. All files will be owned by this group.}
{--url= : The specific archive to download.}
{--release= : A specific Pterodactyl version to download from GitHub. Leave blank to use latest.}
{--skip-download : If set no archive will be downloaded.}';
@ -46,22 +47,39 @@ class UpgradeCommand extends Command
}
$user = 'www-data';
$group = 'www-data';
if ($this->input->isInteractive()) {
if (!$skipDownload) {
$skipDownload = !$this->confirm('Would you like to download and unpack the archive files for the latest version?', true);
}
if (is_null($this->option('user'))) {
$details = posix_getpwuid(fileowner('public'));
$user = $details['name'] ?? 'www-data';
$userDetails = posix_getpwuid(fileowner('public'));
$user = $userDetails['name'] ?? 'www-data';
if (!$this->confirm("Your webserver user has been detected as [{$user}]: is this correct?", true)) {
$user = $this->anticipate(
'Please enter the name of the user running your webserver process. This varies from system to system, but is generally "www-data", "nginx", or "apache".',
[
'www-data',
'apache',
'nginx',
'apache',
]
);
}
}
if (is_null($this->option('group'))) {
$groupDetails = posix_getgrgid(filegroup('public'));
$group = $groupDetails['name'] ?? 'www-data';
if (!$this->confirm("Your webserver group has been detected as [{$group}]: is this correct?", true)) {
$group = $this->anticipate(
'Please enter the name of the group running your webserver process. Normally this is the same as your user.',
[
'www-data',
'nginx',
'apache',
]
);
}
@ -136,9 +154,9 @@ class UpgradeCommand extends Command
$this->call('migrate', ['--seed' => '', '--force' => '']);
});
$this->withProgress($bar, function () use ($user) {
$this->line("\$upgrader> chown -R {$user}:{$user} *");
$process = Process::fromShellCommandline("chown -R {$user}:{$user} *", $this->getLaravel()->basePath());
$this->withProgress($bar, function () use ($user, $group) {
$this->line("\$upgrader> chown -R {$user}:{$group} *");
$process = Process::fromShellCommandline("chown -R {$user}:{$group} *", $this->getLaravel()->basePath());
$process->setTimeout(10 * 60);
$process->run(function ($type, $buffer) {
$this->{$type === Process::ERR ? 'error' : 'line'}($buffer);

View file

@ -38,6 +38,15 @@ class DaemonConnectionException extends DisplayException
if ($useStatusCode) {
$this->statusCode = is_null($response) ? $this->statusCode : $response->getStatusCode();
// There are rare conditions where wings encounters a panic condition and crashes the
// request being made after content has already been sent over the wire. In these cases
// you can end up with a "successful" response code that is actual an error.
//
// Handle those better here since we shouldn't ever end up in this exception state and
// be returning a 2XX level response.
if ($this->statusCode < 400) {
$this->statusCode = Response::HTTP_BAD_GATEWAY;
}
}
if (is_null($response)) {

View file

@ -0,0 +1,16 @@
<?php
namespace Pterodactyl\Exceptions\Service\Backup;
use Pterodactyl\Exceptions\DisplayException;
class BackupLockedException extends DisplayException
{
/**
* TooManyBackupsException constructor.
*/
public function __construct()
{
parent::__construct('Cannot delete a backup that is marked as locked.');
}
}

View file

@ -108,7 +108,7 @@ class NodeController extends ApplicationApiController
$node = $this->updateService->handle(
$node,
$request->validated(),
$request->input('reset_secret')
$request->input('reset_secret'),
);
return $this->fractal->item($node)

View file

@ -35,7 +35,7 @@ class NodeDeploymentController extends ApplicationApiController
$nodes = $this->viableNodesService->setLocations($data['location_ids'] ?? [])
->setMemory($data['memory'])
->setDisk($data['disk'])
->handle($request->input('page') ?? 0);
->handle($request->query('per_page'), $request->query('page'));
return $this->fractal->collection($nodes)
->transformWith($this->getTransformer(NodeTransformer::class))

View file

@ -72,11 +72,18 @@ class BackupController extends ClientApiController
{
/** @var \Pterodactyl\Models\Backup $backup */
$backup = $server->audit(AuditLog::SERVER__BACKUP_STARTED, function (AuditLog $model, Server $server) use ($request) {
$backup = $this->initiateBackupService
->setIgnoredFiles(
explode(PHP_EOL, $request->input('ignored') ?? '')
)
->handle($server, $request->input('name'));
$action = $this->initiateBackupService
->setIgnoredFiles(explode(PHP_EOL, $request->input('ignored') ?? ''));
// Only set the lock status if the user even has permission to delete backups,
// otherwise ignore this status. This gets a little funky since it isn't clear
// how best to allow a user to create a backup that is locked without also preventing
// them from just filling up a server with backups that can never be deleted?
if ($request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
$action->setIsLocked((bool) $request->input('is_locked'));
}
$backup = $action->handle($server, $request->input('name'));
$model->metadata = ['backup_uuid' => $backup->uuid];
@ -88,6 +95,32 @@ class BackupController extends ClientApiController
->toArray();
}
/**
* Toggles the lock status of a given backup for a server.
*
* @throws \Throwable
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function toggleLock(Request $request, Server $server, Backup $backup): array
{
if (!$request->user()->can(Permission::ACTION_BACKUP_DELETE, $server)) {
throw new AuthorizationException();
}
$action = $backup->is_locked ? AuditLog::SERVER__BACKUP_UNLOCKED : AuditLog::SERVER__BACKUP_LOCKED;
$server->audit($action, function (AuditLog $audit) use ($backup) {
$audit->metadata = ['backup_uuid' => $backup->uuid];
$backup->update(['is_locked' => !$backup->is_locked]);
});
$backup->refresh();
return $this->fractal->item($backup)
->transformWith($this->getTransformer(BackupTransformer::class))
->toArray();
}
/**
* Returns information about a single backup.
*

View file

@ -36,11 +36,10 @@ class ResourceUtilizationController extends ClientApiController
*/
public function __invoke(GetServerRequest $request, Server $server): array
{
$stats = $this->cache
->tags(['resources'])
->remember($server->uuid, Carbon::now()->addSeconds(20), function () use ($server) {
return $this->repository->setServer($server)->getDetails();
});
$key = "resources:{$server->uuid}";
$stats = $this->cache->remember($key, Carbon::now()->addSeconds(20), function () use ($server) {
return $this->repository->setServer($server)->getDetails();
});
return $this->fractal->item($stats)
->transformWith($this->getTransformer(StatsTransformer::class))

View file

@ -15,7 +15,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;
@ -72,6 +71,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),
]);
@ -118,6 +118,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),
];

View file

@ -33,6 +33,7 @@ class ScheduleTaskController extends ClientApiController
/**
* Create a new task for a given schedule and store it in the database.
*
* @throws \Pterodactyl\Exceptions\Http\HttpForbiddenException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\ServiceLimitExceededException
* @throws \Illuminate\Contracts\Container\BindingResolutionException
@ -44,6 +45,10 @@ class ScheduleTaskController extends ClientApiController
throw new ServiceLimitExceededException("Schedules may not have more than {$limit} tasks associated with them. Creating this task would put this schedule over the limit.");
}
if ($server->backup_limit === 0 && $request->action === 'backup') {
throw new HttpForbiddenException("A backup task cannot be created when the server's backup limit is set to 0.");
}
/** @var \Pterodactyl\Models\Task|null $lastTask */
$lastTask = $schedule->tasks()->orderByDesc('sequence_id')->first();
@ -54,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)
@ -64,6 +70,7 @@ class ScheduleTaskController extends ClientApiController
/**
* Updates a given task for a server.
*
* @throws \Pterodactyl\Exceptions\Http\HttpForbiddenException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Illuminate\Contracts\Container\BindingResolutionException
@ -74,10 +81,15 @@ class ScheduleTaskController extends ClientApiController
throw new NotFoundHttpException();
}
if ($server->backup_limit === 0 && $request->action === 'backup') {
throw new HttpForbiddenException("A backup task cannot be created when the server's backup limit is set to 0.");
}
$this->repository->update($task->id, [
'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())

View file

@ -90,7 +90,7 @@ class ServerDetailsController extends Controller
/** @var \Pterodactyl\Models\Server[] $servers */
$servers = Server::query()
->select('servers.*')
->selectRaw('started.metadata->>"$.backup_uuid" as backup_uuid')
->selectRaw('JSON_UNQUOTE(JSON_EXTRACT(started.metadata, "$.backup_uuid")) as backup_uuid')
->leftJoinSub(function (Builder $builder) {
$builder->select('*')->from('audit_logs')
->where('action', AuditLog::SERVER__BACKUP_RESTORE_STARTED)

View file

@ -19,6 +19,7 @@ class StoreBackupRequest extends ClientApiRequest
{
return [
'name' => 'nullable|string|max:191',
'is_locked' => 'nullable|boolean',
'ignored' => 'nullable|string',
];
}

View file

@ -7,7 +7,6 @@ use Pterodactyl\Jobs\Job;
use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task;
use InvalidArgumentException;
use Pterodactyl\Models\Schedule;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
@ -15,6 +14,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
{
@ -27,13 +27,19 @@ class RunTaskJob extends Job implements ShouldQueue
*/
public $task;
/**
* @var bool
*/
public $manualRun;
/**
* RunTaskJob constructor.
*/
public function __construct(Task $task)
public function __construct(Task $task, $manualRun = false)
{
$this->queue = config('pterodactyl.queues.standard');
$this->task = $task;
$this->manualRun = $manualRun;
}
/**
@ -46,8 +52,8 @@ class RunTaskJob extends Job implements ShouldQueue
InitiateBackupService $backupService,
DaemonPowerRepository $powerRepository
) {
// Do not process a task that is not set to active.
if (!$this->task->schedule->is_active) {
// Do not process a task that is not set to active, unless it's been manually triggered.
if (!$this->task->schedule->is_active && !$this->manualRun) {
$this->markTaskNotQueued();
$this->markScheduleComplete();
@ -56,18 +62,26 @@ 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('Invalid task action provided: ' . $this->task->action);
}
} catch (Exception $exception) {
// If this isn't a DaemonConnectionException on a task that allows for failures
// throw the exception back up the chain so that the task is stopped.
if (!($this->task->continue_on_failure && $exception instanceof DaemonConnectionException)) {
throw $exception;
}
}
$this->markTaskNotQueued();
@ -101,7 +115,7 @@ class RunTaskJob extends Job implements ShouldQueue
$nextTask->update(['is_queued' => true]);
$this->dispatch((new self($nextTask))->delay($nextTask->time_offset));
$this->dispatch((new self($nextTask, $this->manualRun))->delay($nextTask->time_offset));
}
/**

View file

@ -7,17 +7,17 @@ use Illuminate\Http\Request;
use Illuminate\Container\Container;
/**
* @property int $id
* @property string $uuid
* @property bool $is_system
* @property int|null $user_id
* @property int|null $server_id
* @property string $action
* @property string|null $subaction
* @property array $device
* @property array $metadata
* @property \Carbon\CarbonImmutable $created_at
* @property \Pterodactyl\Models\User|null $user
* @property int $id
* @property string $uuid
* @property bool $is_system
* @property int|null $user_id
* @property int|null $server_id
* @property string $action
* @property string|null $subaction
* @property array $device
* @property array $metadata
* @property \Carbon\CarbonImmutable $created_at
* @property \Pterodactyl\Models\User|null $user
* @property \Pterodactyl\Models\Server|null $server
*/
class AuditLog extends Model
@ -36,6 +36,8 @@ class AuditLog extends Model
public const SERVER__BACKUP_COMPELTED = 'server:backup.completed';
public const SERVER__BACKUP_DELETED = 'server:backup.deleted';
public const SERVER__BACKUP_DOWNLOADED = 'server:backup.downloaded';
public const SERVER__BACKUP_LOCKED = 'server:backup.locked';
public const SERVER__BACKUP_UNLOCKED = 'server:backup.unlocked';
public const SERVER__BACKUP_RESTORE_STARTED = 'server:backup.restore.started';
public const SERVER__BACKUP_RESTORE_COMPLETED = 'server:backup.restore.completed';
public const SERVER__BACKUP_RESTORE_FAILED = 'server:backup.restore.failed';

View file

@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @property int $server_id
* @property string $uuid
* @property bool $is_successful
* @property bool $is_locked
* @property string $name
* @property string[] $ignored_files
* @property string $disk
@ -46,6 +47,7 @@ class Backup extends Model
protected $casts = [
'id' => 'int',
'is_successful' => 'bool',
'is_locked' => 'bool',
'ignored_files' => 'array',
'bytes' => 'int',
];
@ -62,6 +64,7 @@ class Backup extends Model
*/
protected $attributes = [
'is_successful' => true,
'is_locked' => false,
'checksum' => null,
'bytes' => 0,
'upload_id' => null,
@ -79,6 +82,7 @@ class Backup extends Model
'server_id' => 'bail|required|numeric|exists:servers,id',
'uuid' => 'required|uuid',
'is_successful' => 'boolean',
'is_locked' => 'boolean',
'name' => 'required|string',
'ignored_files' => 'array',
'disk' => 'required|string',

View file

@ -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()

View file

@ -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',
];
/**

View file

@ -9,6 +9,7 @@ use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Extensions\Backups\BackupManager;
use Pterodactyl\Repositories\Eloquent\BackupRepository;
use Pterodactyl\Repositories\Wings\DaemonBackupRepository;
use Pterodactyl\Exceptions\Service\Backup\BackupLockedException;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class DeleteBackupService
@ -55,6 +56,10 @@ class DeleteBackupService
*/
public function handle(Backup $backup)
{
if ($backup->is_locked) {
throw new BackupLockedException();
}
if ($backup->disk === Backup::ADAPTER_AWS_S3) {
$this->deleteFromS3($backup);

View file

@ -21,6 +21,11 @@ class InitiateBackupService
*/
private $ignoredFiles;
/**
* @var bool
*/
private $isLocked = false;
/**
* @var \Pterodactyl\Repositories\Eloquent\BackupRepository
*/
@ -49,7 +54,11 @@ class InitiateBackupService
/**
* InitiateBackupService constructor.
*
* @param \Pterodactyl\Repositories\Eloquent\BackupRepository $repository
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Repositories\Wings\DaemonBackupRepository $daemonBackupRepository
* @param \Pterodactyl\Services\Backups\DeleteBackupService $deleteBackupService
* @param \Pterodactyl\Extensions\Backups\BackupManager $backupManager
*/
public function __construct(
BackupRepository $repository,
@ -65,6 +74,19 @@ class InitiateBackupService
$this->deleteBackupService = $deleteBackupService;
}
/**
* Set if the backup should be locked once it is created which will prevent
* its deletion by users or automated system processes.
*
* @return $this
*/
public function setIsLocked(bool $isLocked): self
{
$this->isLocked = $isLocked;
return $this;
}
/**
* Sets the files to be ignored by this backup.
*
@ -91,7 +113,7 @@ class InitiateBackupService
}
/**
* Initiates the backup process for a server on the daemon.
* Initiates the backup process for a server on Wings.
*
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\Service\Backup\TooManyBackupsException
@ -104,23 +126,30 @@ class InitiateBackupService
if ($period > 0) {
$previous = $this->repository->getBackupsGeneratedDuringTimespan($server->id, $period);
if ($previous->count() >= $limit) {
throw new TooManyRequestsHttpException(CarbonImmutable::now()->diffInSeconds($previous->last()->created_at->addSeconds($period)), sprintf('Only %d backups may be generated within a %d second span of time.', $limit, $period));
$message = sprintf('Only %d backups may be generated within a %d second span of time.', $limit, $period);
throw new TooManyRequestsHttpException(CarbonImmutable::now()->diffInSeconds($previous->last()->created_at->addSeconds($period)), $message);
}
}
// Check if the server has reached or exceeded it's backup limit
if (!$server->backup_limit || $server->backups()->where('is_successful', true)->count() >= $server->backup_limit) {
// Check if the server has reached or exceeded it's backup limit.
$successful = $server->backups()->where('is_successful', true);
if (!$server->backup_limit || $successful->count() >= $server->backup_limit) {
// Do not allow the user to continue if this server is already at its limit and can't override.
if (!$override || $server->backup_limit <= 0) {
throw new TooManyBackupsException($server->backup_limit);
}
// Get the oldest backup the server has.
/** @var \Pterodactyl\Models\Backup $oldestBackup */
$oldestBackup = $server->backups()->where('is_successful', true)->orderBy('created_at')->first();
// Get the oldest backup the server has that is not "locked" (indicating a backup that should
// never be automatically purged). If we find a backup we will delete it and then continue with
// this process. If no backup is found that can be used an exception is thrown.
/** @var \Pterodactyl\Models\Backup $oldest */
$oldest = $successful->where('is_locked', false)->orderBy('created_at')->first();
if (!$oldest) {
throw new TooManyBackupsException($server->backup_limit);
}
// Delete the oldest backup.
$this->deleteBackupService->handle($oldestBackup);
$this->deleteBackupService->handle($oldest);
}
return $this->connection->transaction(function () use ($server, $name) {
@ -131,6 +160,7 @@ class InitiateBackupService
'name' => trim($name) ?: sprintf('Backup at %s', CarbonImmutable::now()->toDateTimeString()),
'ignored_files' => array_values($this->ignoredFiles ?? []),
'disk' => $this->backupManager->getDefaultAdapter(),
'is_locked' => $this->isLocked,
], true, true);
$this->daemonBackupRepository->setServer($server)

View file

@ -83,7 +83,7 @@ class FindViableNodesService
*
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
*/
public function handle(int $page = null)
public function handle(int $perPage = null, int $page = null)
{
Assert::integer($this->disk, 'Disk space must be an int, got %s');
Assert::integer($this->memory, 'Memory usage must be an int, got %s');
@ -103,7 +103,7 @@ class FindViableNodesService
->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk]);
if (!is_null($page)) {
$results = $results->paginate(50, ['*'], 'page', $page);
$results = $results->paginate($perPage ?? 50, ['*'], 'page', $page);
} else {
$results = $results->get()->toBase();
}

View file

@ -8,6 +8,8 @@ use Illuminate\Contracts\Bus\Dispatcher;
use Pterodactyl\Jobs\Schedule\RunTaskJob;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Exceptions\DisplayException;
use Pterodactyl\Repositories\Wings\DaemonServerRepository;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class ProcessScheduleService
{
@ -21,13 +23,19 @@ class ProcessScheduleService
*/
private $connection;
/**
* @var \Pterodactyl\Repositories\Wings\DaemonServerRepository
*/
private $serverRepository;
/**
* ProcessScheduleService constructor.
*/
public function __construct(ConnectionInterface $connection, Dispatcher $dispatcher)
public function __construct(ConnectionInterface $connection, DaemonServerRepository $serverRepository, Dispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
$this->connection = $connection;
$this->serverRepository = $serverRepository;
}
/**
@ -38,7 +46,7 @@ class ProcessScheduleService
public function handle(Schedule $schedule, bool $now = false)
{
/** @var \Pterodactyl\Models\Task $task */
$task = $schedule->tasks()->orderBy('sequence_id', 'asc')->first();
$task = $schedule->tasks()->orderBy('sequence_id')->first();
if (is_null($task)) {
throw new DisplayException('Cannot process schedule for task execution: no tasks are registered.');
@ -53,7 +61,31 @@ class ProcessScheduleService
$task->update(['is_queued' => true]);
});
$job = new RunTaskJob($task);
$job = new RunTaskJob($task, $now);
if ($schedule->only_when_online) {
// Check that the server is currently in a starting or running state before executing
// this schedule if this option has been set.
try {
$details = $this->serverRepository->setServer($schedule->server)->getDetails();
$state = $details['state'] ?? 'offline';
// If the server is stopping or offline just do nothing with this task.
if (in_array($state, ['offline', 'stopping'])) {
$job->failed();
return;
}
} catch (Exception $exception) {
if (!$exception instanceof DaemonConnectionException) {
// If we encountered some exception during this process that wasn't just an
// issue connecting to Wings run the failed sequence for a job. Otherwise we
// can just quietly mark the task as completed without actually running anything.
$job->failed($exception);
}
$job->failed();
return;
}
}
if (!$now) {
$this->dispatcher->dispatch($job->delay($task->time_offset));

View file

@ -1,11 +1,4 @@
<?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Traits\Commands;
@ -13,6 +6,20 @@ use Pterodactyl\Exceptions\PterodactylException;
trait EnvironmentWriterTrait
{
/**
* Escapes an environment value by looking for any characters that could
* reasonablly cause environment parsing issues. Those values are then wrapped
* in quotes before being returned.
*/
public function escapeEnvironmentValue(string $value): string
{
if (!preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) {
return sprintf('"%s"', addslashes($value));
}
return $value;
}
/**
* Update the .env file for the application using the passed in values.
*
@ -28,14 +35,7 @@ trait EnvironmentWriterTrait
$saveContents = file_get_contents($path);
collect($values)->each(function ($value, $key) use (&$saveContents) {
$key = strtoupper($key);
// If the key value is not sorrounded by quotation marks, and contains anything that could reasonably
// cause environment parsing issues, wrap it in quotes before writing it. This also adds slashes to the
// value to ensure quotes within it don't cause us issues.
if (!preg_match('/^\"(.*)\"$/', $value) && preg_match('/([^\w.\-+\/])+/', $value)) {
$value = sprintf('"%s"', addslashes($value));
}
$saveValue = sprintf('%s=%s', $key, $value);
$saveValue = sprintf('%s=%s', $key, $this->escapeEnvironmentValue($value));
if (preg_match_all('/^' . $key . '=(.*)$/m', $saveContents) < 1) {
$saveContents = $saveContents . PHP_EOL . $saveValue;

View file

@ -19,6 +19,7 @@ class BackupTransformer extends BaseClientTransformer
return [
'uuid' => $backup->uuid,
'is_successful' => $backup->is_successful,
'is_locked' => $backup->is_locked,
'name' => $backup->name,
'ignored_files' => $backup->ignored_files,
'checksum' => $backup->checksum,

View file

@ -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(),

View file

@ -21,13 +21,13 @@ class StatsTransformer extends BaseClientTransformer
{
return [
'current_state' => Arr::get($data, 'state', 'stopped'),
'is_suspended' => Arr::get($data, 'suspended', false),
'is_suspended' => Arr::get($data, 'is_suspended', false),
'resources' => [
'memory_bytes' => Arr::get($data, 'memory_bytes', 0),
'cpu_absolute' => Arr::get($data, 'cpu_absolute', 0),
'disk_bytes' => Arr::get($data, 'disk_bytes', 0),
'network_rx_bytes' => Arr::get($data, 'network.rx_bytes', 0),
'network_tx_bytes' => Arr::get($data, 'network.tx_bytes', 0),
'memory_bytes' => Arr::get($data, 'utilization.memory_bytes', 0),
'cpu_absolute' => Arr::get($data, 'utilization.cpu_absolute', 0),
'disk_bytes' => Arr::get($data, 'utilization.disk_bytes', 0),
'network_rx_bytes' => Arr::get($data, 'utilization.network.rx_bytes', 0),
'network_tx_bytes' => Arr::get($data, 'utilization.network.tx_bytes', 0),
],
];
}

View file

@ -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(),
];