1
0
mirror of https://github.com/flarum/core.git synced 2025-08-16 13:24:11 +02:00

feat: Queue package manager commands (#3418)

* feat: Queue package manager commands
* adjust tests
* fix: force run whynot command synchronously
* chore: maximize command output box's height
* chore: more user instructions on background queue
* feat: track command peak memory usage
* feat: exit of CLI php version doesn't match web php version
* chore: install deps
* chore: format and typing workflow fix

Signed-off-by: Sami Mazouz <ilyasmazouz@gmail.com>
This commit is contained in:
Sami Mazouz
2022-07-24 14:02:13 +01:00
committed by GitHub
parent 75aaef7d76
commit 795a500adb
71 changed files with 1631 additions and 375 deletions

View File

@@ -11,7 +11,7 @@ namespace Flarum\PackageManager\Api\Controller;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\CheckForUpdates;
use Illuminate\Contracts\Bus\Dispatcher;
use Flarum\PackageManager\Job\Dispatcher;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
@@ -29,14 +29,25 @@ class CheckForUpdatesController implements RequestHandlerInterface
$this->bus = $bus;
}
/**
* @throws \Flarum\User\Exception\PermissionDeniedException
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$actor = RequestUtil::getActor($request);
$lastUpdateCheck = $this->bus->dispatch(
$actor->assertAdmin();
/**
* @TODO somewhere, if we're queuing, check that a similar composer command isn't already running,
* to avoid duplicate jobs.
*/
$response = $this->bus->dispatch(
new CheckForUpdates($actor)
);
return new JsonResponse($lastUpdateCheck);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new JsonResponse($response->data);
}
}

View File

@@ -9,10 +9,11 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\GlobalUpdate;
use Flarum\PackageManager\Job\Dispatcher;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -36,10 +37,12 @@ class GlobalUpdateController implements RequestHandlerInterface
{
$actor = RequestUtil::getActor($request);
$this->bus->dispatch(
$response = $this->bus->dispatch(
new GlobalUpdate($actor)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@@ -1,32 +0,0 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Api\Serializer\TaskSerializer;
use Flarum\PackageManager\Task;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListTaskController extends AbstractListController
{
public $serializer = TaskSerializer::class;
/**
* @throws \Flarum\User\Exception\PermissionDeniedException
*/
protected function data(ServerRequestInterface $request, Document $document)
{
RequestUtil::getActor($request)->assertAdmin();
return Task::query()->orderBy('created_at', 'desc')->get();
}
}

View File

@@ -0,0 +1,73 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Api\Controller\AbstractListController;
use Flarum\Http\RequestUtil;
use Flarum\Http\UrlGenerator;
use Flarum\PackageManager\Api\Serializer\TaskSerializer;
use Flarum\PackageManager\Task\TaskRepository;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
class ListTasksController extends AbstractListController
{
/**
* {@inheritdoc}
*/
public $serializer = TaskSerializer::class;
/**
* @var UrlGenerator
*/
protected $url;
/**
* @var TaskRepository
*/
protected $repository;
public function __construct(UrlGenerator $url, TaskRepository $repository)
{
$this->url = $url;
$this->repository = $repository;
}
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$actor->assertAdmin();
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
$results = $this->repository
->query()
->latest()
->offset($offset)
->limit($limit)
->get();
$total = $this->repository->query()->count();
$document->addMeta('total', $total);
$document->addPaginationLinks(
$this->url->to('api')->route('package-manager.tasks.index'),
$request->getQueryParams(),
$offset,
$limit,
$total
);
return $results;
}
}

View File

@@ -9,11 +9,12 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\MajorUpdate;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -35,10 +36,12 @@ class MajorUpdateController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$dryRun = (bool) (int) Arr::get($request->getParsedBody(), 'data.dryRun', 0);
$this->bus->dispatch(
$response = $this->bus->dispatch(
new MajorUpdate($actor, $dryRun)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@@ -9,10 +9,11 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\MinorUpdate;
use Flarum\PackageManager\Job\Dispatcher;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -36,10 +37,12 @@ class MinorUpdateController implements RequestHandlerInterface
{
$actor = RequestUtil::getActor($request);
$this->bus->dispatch(
$response = $this->bus->dispatch(
new MinorUpdate($actor)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@@ -9,11 +9,12 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\RemoveExtension;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -35,10 +36,12 @@ class RemoveExtensionController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$extensionId = Arr::get($request->getQueryParams(), 'id');
$this->bus->dispatch(
$response = $this->bus->dispatch(
new RemoveExtension($actor, $extensionId)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@@ -9,9 +9,9 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\RequireExtension;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@@ -35,10 +35,12 @@ class RequireExtensionController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$package = Arr::get($request->getParsedBody(), 'data.package');
$data = $this->bus->dispatch(
$response = $this->bus->dispatch(
new RequireExtension($actor, $package)
);
return new JsonResponse($data);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new JsonResponse($response->data);
}
}

View File

@@ -9,11 +9,12 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\UpdateExtension;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@@ -35,10 +36,12 @@ class UpdateExtensionController implements RequestHandlerInterface
$actor = RequestUtil::getActor($request);
$extensionId = Arr::get($request->getQueryParams(), 'id');
$this->bus->dispatch(
$response = $this->bus->dispatch(
new UpdateExtension($actor, $extensionId)
);
return new EmptyResponse(200);
return $response->queueJobs
? new JsonResponse(['processing' => true], 202)
: new EmptyResponse(201);
}
}

View File

@@ -9,9 +9,9 @@
namespace Flarum\PackageManager\Api\Controller;
use Flarum\Bus\Dispatcher;
use Flarum\Http\RequestUtil;
use Flarum\PackageManager\Command\WhyNot;
use Flarum\PackageManager\Job\Dispatcher;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@@ -36,12 +36,10 @@ class WhyNotController implements RequestHandlerInterface
$package = Arr::get($request->getParsedBody(), 'data.package', '');
$version = Arr::get($request->getParsedBody(), 'data.version', '*');
$whyNot = $this->bus->dispatch(
$whyNot = $this->bus->sync()->dispatch(
new WhyNot($actor, $package, $version)
);
return new JsonResponse([
'data' => compact('whyNot')
]);
return new JsonResponse(['data' => ['reason' => $whyNot->data['reason']]]);
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Api\Serializer;
use Flarum\Api\Serializer\AbstractSerializer;
use Flarum\PackageManager\Task\Task;
use InvalidArgumentException;
class TaskSerializer extends AbstractSerializer
{
/**
* {@inheritdoc}
*/
protected $type = 'package-manager-tasks';
/**
* {@inheritdoc}
*
* @param Task $model
* @throws InvalidArgumentException
*/
protected function getDefaultAttributes($model)
{
if (! ($model instanceof Task)) {
throw new InvalidArgumentException(
get_class($this).' can only serialize instances of '.Task::class
);
}
return [
'status' => $model->status,
'operation' => $model->operation,
'command' => $model->command,
'package' => $model->package,
'output' => $model->output,
'createdAt' => $model->created_at,
'startedAt' => $model->started_at,
'finishedAt' => $model->finished_at,
'peakMemoryUsed' => $model->peak_memory_used,
];
}
}

View File

@@ -0,0 +1,15 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Command;
interface BusinessCommandInterface
{
public function getOperationName(): string;
}

View File

@@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class CheckForUpdates
class CheckForUpdates implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var \Flarum\User\User
*/
@@ -22,4 +28,9 @@ class CheckForUpdates
{
$this->actor = $actor;
}
public function getOperationName(): string
{
return Task::UPDATE_CHECK;
}
}

View File

@@ -48,29 +48,24 @@ class CheckForUpdatesHandler
*
* The results from both commands are properly processed and merged to have new key values `latest-minor` and `latest-major`.
*
* @throws \Flarum\User\Exception\PermissionDeniedException|ComposerCommandFailedException
* @todo integration test
* @throws ComposerCommandFailedException
*/
public function handle(CheckForUpdates $command)
{
$actor = $command->actor;
$actor->assertAdmin();
$firstOutput = $this->runComposerCommand(false);
$firstOutput = $this->runComposerCommand(false, $command);
$firstOutput = json_decode($this->cleanJson($firstOutput), true);
$majorUpdates = false;
foreach ($firstOutput['installed'] as $package) {
if ($package['latest-status'] === 'update-possible') {
if (isset($package['latest-status']) && $package['latest-status'] === 'update-possible') {
$majorUpdates = true;
break;
}
}
if ($majorUpdates) {
$secondOutput = $this->runComposerCommand(true);
$secondOutput = $this->runComposerCommand(true, $command);
$secondOutput = json_decode($this->cleanJson($secondOutput), true);
}
@@ -81,7 +76,7 @@ class CheckForUpdatesHandler
foreach ($firstOutput['installed'] as &$mainPackageUpdate) {
$mainPackageUpdate['latest-minor'] = $mainPackageUpdate['latest-major'] = null;
if ($mainPackageUpdate['latest-status'] === 'update-possible') {
if (isset($mainPackageUpdate['latest-status']) && $mainPackageUpdate['latest-status'] === 'update-possible') {
$mainPackageUpdate['latest-major'] = $mainPackageUpdate['latest'];
$minorPackageUpdate = array_filter($secondOutput['installed'], function ($package) use ($mainPackageUpdate) {
@@ -92,7 +87,7 @@ class CheckForUpdatesHandler
$mainPackageUpdate['latest-minor'] = $minorPackageUpdate['latest'];
}
} else {
$mainPackageUpdate['latest-minor'] = $mainPackageUpdate['latest'];
$mainPackageUpdate['latest-minor'] = $mainPackageUpdate['latest'] ?? null;
}
}
@@ -113,7 +108,7 @@ class CheckForUpdatesHandler
/**
* @throws ComposerCommandFailedException
*/
protected function runComposerCommand(bool $minorOnly): string
protected function runComposerCommand(bool $minorOnly, CheckForUpdates $command): string
{
$input = [
'command' => 'outdated',
@@ -125,7 +120,7 @@ class CheckForUpdatesHandler
$input['--minor-only'] = true;
}
$output = $this->composer->run(new ArrayInput($input));
$output = $this->composer->run(new ArrayInput($input), $command->task ?? null);
if ($output->getExitCode() !== 0) {
throw new ComposerCommandFailedException('', $output->getContents());

View File

@@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class GlobalUpdate
class GlobalUpdate implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var \Flarum\User\User
*/
@@ -22,4 +28,9 @@ class GlobalUpdate
{
$this->actor = $actor;
}
public function getOperationName(): string
{
return Task::UPDATE_GLOBAL;
}
}

View File

@@ -48,7 +48,8 @@ class GlobalUpdateHandler
$command->actor->assertAdmin();
$output = $this->composer->run(
new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies')
new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies'),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {
@@ -58,7 +59,5 @@ class GlobalUpdateHandler
$this->events->dispatch(
new FlarumUpdated($command->actor, FlarumUpdated::GLOBAL)
);
return true;
}
}

View File

@@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class MajorUpdate
class MajorUpdate implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var \Flarum\User\User
*/
@@ -28,4 +34,9 @@ class MajorUpdate
$this->actor = $actor;
$this->dryRun = $dryRun;
}
public function getOperationName(): string
{
return Task::UPDATE_MAJOR;
}
}

View File

@@ -76,19 +76,17 @@ class MajorUpdateHandler
$this->updateComposerJson($majorVersion);
$this->runCommand($command->dryRun, $majorVersion);
$this->runCommand($command, $majorVersion);
if ($command->dryRun) {
$this->composerJson->revert();
return true;
return;
}
$this->events->dispatch(
new FlarumUpdated($command->actor, FlarumUpdated::MAJOR)
);
return true;
}
/**
@@ -105,7 +103,7 @@ class MajorUpdateHandler
/**
* @throws MajorUpdateFailedException
*/
protected function runCommand(bool $dryRun, string $majorVersion): void
protected function runCommand(MajorUpdate $command, string $majorVersion): void
{
$input = [
'command' => 'update',
@@ -116,11 +114,11 @@ class MajorUpdateHandler
'--with-all-dependencies' => true,
];
if ($dryRun) {
if ($command->dryRun) {
$input['--dry-run'] = true;
}
$output = $this->composer->run(new ArrayInput($input));
$output = $this->composer->run(new ArrayInput($input), $command->task ?? null);
if ($output->getExitCode() !== 0) {
throw new MajorUpdateFailedException('*', $output->getContents(), $majorVersion);

View File

@@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class MinorUpdate
class MinorUpdate implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var \Flarum\User\User
*/
@@ -22,4 +28,9 @@ class MinorUpdate
{
$this->actor = $actor;
}
public function getOperationName(): string
{
return Task::UPDATE_MINOR;
}
}

View File

@@ -61,7 +61,8 @@ class MinorUpdateHandler
$this->composerJson->require('flarum/core', $coreRequirement);
$output = $this->composer->run(
new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies')
new StringInput('update --prefer-dist --no-dev -a --with-all-dependencies'),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {
@@ -71,7 +72,5 @@ class MinorUpdateHandler
$this->events->dispatch(
new FlarumUpdated($command->actor, FlarumUpdated::MINOR)
);
return true;
}
}

View File

@@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class RemoveExtension
class RemoveExtension implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var User
*/
@@ -28,4 +34,9 @@ class RemoveExtension
$this->actor = $actor;
$this->extensionId = $extensionId;
}
public function getOperationName(): string
{
return Task::EXTENSION_REMOVE;
}
}

View File

@@ -55,8 +55,13 @@ class RemoveExtensionHandler
throw new ExtensionNotInstalledException($command->extensionId);
}
if (isset($command->task)) {
$command->task->package = $extension->name;
}
$output = $this->composer->run(
new StringInput("remove $extension->name")
new StringInput("remove $extension->name"),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {

View File

@@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class RequireExtension
class RequireExtension implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var User
*/
@@ -28,4 +34,9 @@ class RequireExtension
$this->actor = $actor;
$this->package = $package;
}
public function getOperationName(): string
{
return Task::EXTENSION_INSTALL;
}
}

View File

@@ -74,7 +74,8 @@ class RequireExtensionHandler
}
$output = $this->composer->run(
new StringInput("require $packageName")
new StringInput("require $packageName"),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {

View File

@@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class UpdateExtension
class UpdateExtension implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var User
*/
@@ -28,4 +34,9 @@ class UpdateExtension
$this->actor = $actor;
$this->extensionId = $extensionId;
}
public function getOperationName(): string
{
return Task::EXTENSION_UPDATE;
}
}

View File

@@ -77,7 +77,8 @@ class UpdateExtensionHandler
}
$output = $this->composer->run(
new StringInput("require $extension->name:*")
new StringInput("require $extension->name:*"),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {
@@ -87,7 +88,5 @@ class UpdateExtensionHandler
$this->events->dispatch(
new Updated($command->actor, $extension)
);
return true;
}
}

View File

@@ -9,10 +9,16 @@
namespace Flarum\PackageManager\Command;
use Flarum\PackageManager\Task\Task;
use Flarum\User\User;
class WhyNot
class WhyNot implements BusinessCommandInterface
{
/**
* @var Task
*/
public $task = null;
/**
* @var User
*/
@@ -34,4 +40,9 @@ class WhyNot
$this->package = $package;
$this->version = $version;
}
public function getOperationName(): string
{
return Task::WHY_NOT;
}
}

View File

@@ -53,13 +53,14 @@ class WhyNotHandler
]);
$output = $this->composer->run(
new StringInput("why-not $command->package $command->version")
new StringInput("why-not $command->package $command->version"),
$command->task ?? null
);
if ($output->getExitCode() !== 0) {
throw new ComposerRequireFailedException($command->package, $output->getContents());
}
return $output->getContents();
return ['reason' => $output->getContents()];
}
}

View File

@@ -12,6 +12,7 @@ namespace Flarum\PackageManager\Composer;
use Composer\Console\Application;
use Flarum\Foundation\Paths;
use Flarum\PackageManager\OutputLogger;
use Flarum\PackageManager\Task\Task;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\BufferedOutput;
@@ -48,7 +49,7 @@ class ComposerAdapter
$this->output = new BufferedOutput();
}
public function run(InputInterface $input): ComposerOutput
public function run(InputInterface $input, ?Task $task = null): ComposerOutput
{
$this->application->resetComposer();
@@ -58,10 +59,15 @@ class ComposerAdapter
$exitCode = $this->application->run($input, $this->output);
chdir($currDir);
$outputContents = $this->output->fetch();
$command = $input->__toString();
$output = $this->output->fetch();
$this->logger->log($input->__toString(), $outputContents, $exitCode);
if ($task) {
$task->update(compact('command', 'output'));
} else {
$this->logger->log($command, $output, $exitCode);
}
return new ComposerOutput($exitCode, $outputContents);
return new ComposerOutput($exitCode, $output);
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Job;
use Flarum\Bus\Dispatcher;
use Flarum\PackageManager\Command\BusinessCommandInterface;
use Flarum\Queue\AbstractJob;
use Throwable;
class ComposerCommandJob extends AbstractJob
{
/**
* @var BusinessCommandInterface
*/
protected $command;
/**
* @var int[]
*/
protected $phpVersion;
public function __construct(BusinessCommandInterface $command, array $phpVersion)
{
$this->command = $command;
$this->phpVersion = $phpVersion;
}
public function handle(Dispatcher $bus)
{
try {
if ([PHP_MAJOR_VERSION, PHP_MINOR_VERSION] !== [$this->phpVersion[0], $this->phpVersion[1]]) {
$webPhpVersion = implode('.', $this->phpVersion);
$sshPhpVersion = implode('.', [PHP_MAJOR_VERSION, PHP_MINOR_VERSION]);
throw new \Exception("PHP version mismatch. SSH PHP version must match web server PHP version. Found SSH (PHP $sshPhpVersion) and Web Server (PHP $webPhpVersion).");
}
$this->command->task->start();
$bus->dispatch($this->command);
$this->command->task->end(true);
} catch (Throwable $exception) {
$this->abort($exception);
}
}
public function abort(Throwable $exception)
{
if (! $this->command->task->output) {
$this->command->task->output = $exception->getMessage();
}
$this->command->task->end(false);
$this->fail($exception);
}
}

View File

@@ -0,0 +1,84 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Job;
use Flarum\Bus\Dispatcher as Bus;
use Flarum\PackageManager\Command\BusinessCommandInterface;
use Flarum\PackageManager\Task\Task;
use Flarum\Settings\SettingsRepositoryInterface;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Queue\SyncQueue;
class Dispatcher
{
/**
* @var Bus
*/
protected $bus;
/**
* @var Queue
*/
protected $queue;
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* Overrides the user setting for execution mode if set.
* Runs synchronously regardless of user setting if set true.
* Asynchronously if set false.
*
* @var bool|null
*/
protected $runSyncOverride;
public function __construct(Bus $bus, Queue $queue, SettingsRepositoryInterface $settings)
{
$this->bus = $bus;
$this->queue = $queue;
$this->settings = $settings;
}
public function sync(): self
{
$this->runSyncOverride = true;
return $this;
}
public function async(): self
{
$this->runSyncOverride = false;
return $this;
}
public function dispatch(BusinessCommandInterface $command): DispatcherResponse
{
$queueJobs = ($this->runSyncOverride === false) || ($this->runSyncOverride !== true && $this->settings->get('flarum-package-manager.queue_jobs'));
if ($queueJobs && (! $this->queue instanceof SyncQueue)) {
$task = Task::build($command->getOperationName(), $command->package ?? null);
$command->task = $task;
$this->queue->push(
new ComposerCommandJob($command, [PHP_MAJOR_VERSION, PHP_MINOR_VERSION])
);
} else {
$data = $this->bus->dispatch($command);
}
return new DispatcherResponse($queueJobs, $data ?? null);
}
}

View File

@@ -0,0 +1,23 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Job;
class DispatcherResponse
{
public $queueJobs;
public $data;
public function __construct(bool $queueJobs, ?array $data)
{
$this->queueJobs = $queueJobs;
$this->data = $data;
}
}

View File

@@ -0,0 +1,93 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Task;
use Carbon\Carbon;
use Flarum\Database\AbstractModel;
/**
* @property int id
* @property int status
* @property string operation
* @property string command
* @property string package
* @property string output
* @property Carbon created_at
* @property Carbon started_at
* @property Carbon finished_at
* @property int peak_memory_used
*/
class Task extends AbstractModel
{
/**
* Statuses (@todo use an enum with php8.1).
*/
public const PENDING = 'pending';
public const RUNNING = 'running';
public const FAILURE = 'failure';
public const SUCCESS = 'success';
/**
* Operations (@todo use an enum with php8.1).
*/
public const EXTENSION_INSTALL = 'extension_install';
public const EXTENSION_REMOVE = 'extension_remove';
public const EXTENSION_UPDATE = 'extension_update';
public const UPDATE_GLOBAL = 'update_global';
public const UPDATE_MINOR = 'update_minor';
public const UPDATE_MAJOR = 'update_major';
public const UPDATE_CHECK = 'update_check';
public const WHY_NOT = 'why_not';
public const UPDATED_AT = null;
protected $table = 'package_manager_tasks';
protected $fillable = ['command', 'output'];
public $timestamps = true;
protected $casts = [
self::CREATED_AT => 'datetime',
'started_at' => 'datetime',
'finished_at' => 'datetime',
];
public static function build(string $operation, ?string $package): self
{
$task = new static;
$task->operation = $operation;
$task->package = $package;
$task->status = static::PENDING;
$task->created_at = Carbon::now();
$task->save();
return $task;
}
public function start(): bool
{
$this->status = static::RUNNING;
$this->started_at = Carbon::now();
return $this->save();
}
public function end(bool $success): bool
{
$this->status = $success ? static::SUCCESS : static::FAILURE;
$this->finished_at = Carbon::now();
$this->peak_memory_used = round(memory_get_peak_usage() / 1024);
return $this->save();
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/
namespace Flarum\PackageManager\Task;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder;
class TaskRepository
{
/**
* @return Builder
*/
public function query()
{
return Task::query();
}
/**
* @param int $id
* @param User $actor
* @return Task
*/
public function findOrFail($id, User $actor = null): Task
{
return Task::findOrFail($id);
}
}