Skip to content

Use new process factory #286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions app/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@
use PhpSchool\PhpWorkshop\Output\OutputInterface;
use PhpSchool\PhpWorkshop\Output\StdOutput;
use PhpSchool\PhpWorkshop\Patch;
use PhpSchool\PhpWorkshop\Process\HostProcessFactory;
use PhpSchool\PhpWorkshop\Process\ProcessFactory;
use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult;
use PhpSchool\PhpWorkshop\Result\Cgi\GenericFailure as CgiGenericFailure;
use PhpSchool\PhpWorkshop\Result\Cgi\RequestFailure as CgiRequestFailure;
Expand Down Expand Up @@ -187,12 +189,16 @@
//Exercise Runners
RunnerManager::class => function (ContainerInterface $c) {
$manager = new RunnerManager();
$manager->addFactory(new CliRunnerFactory($c->get(EventDispatcher::class)));
$manager->addFactory(new CgiRunnerFactory($c->get(EventDispatcher::class)));
$manager->addFactory(new CliRunnerFactory($c->get(EventDispatcher::class), $c->get(ProcessFactory::class)));
$manager->addFactory(new CgiRunnerFactory($c->get(EventDispatcher::class), $c->get(ProcessFactory::class)));
$manager->addFactory(new CustomVerifyingRunnerFactory());
return $manager;
},

ProcessFactory::class => function (ContainerInterface $c) {
return new HostProcessFactory();
},

//commands
MenuCommand::class => function (ContainerInterface $c) {
return new MenuCommand($c->get('menu'));
Expand Down
14 changes: 9 additions & 5 deletions src/Exercise/AbstractExercise.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace PhpSchool\PhpWorkshop\Exercise;

use PhpSchool\PhpWorkshop\Check\FileComparisonCheck;
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\ExerciseDispatcher;
use PhpSchool\PhpWorkshop\Solution\SingleFileSolution;
use PhpSchool\PhpWorkshop\Solution\SolutionInterface;
Expand Down Expand Up @@ -78,12 +80,14 @@ public static function normaliseName(string $name): string
}

/**
* This method is implemented as empty by default, if you want to add additional checks or listen
* to events, you should override this method.
*
* @param ExerciseDispatcher $dispatcher
* @return list<class-string>
*/
public function configure(ExerciseDispatcher $dispatcher): void
public function getRequiredChecks(): array
{
return [];
}

public function defineListeners(EventDispatcher $dispatcher): void
{
}
}
15 changes: 9 additions & 6 deletions src/Exercise/ExerciseInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PhpSchool\PhpWorkshop\Exercise;

use PhpSchool\PhpWorkshop\Event\EventDispatcher;
use PhpSchool\PhpWorkshop\ExerciseDispatcher;

/**
Expand Down Expand Up @@ -34,14 +35,16 @@ public function getType(): ExerciseType;
public function getProblem(): string;

/**
* This is where the exercise specifies the extra checks it may require. It is also
* possible to grab the event dispatcher from the exercise dispatcher and listen to any
* events. This method is automatically invoked just before verifying/running an student's solution
* to an exercise.
* Subscribe to events triggered throughout the verification process
*/
public function defineListeners(EventDispatcher $dispatcher): void;

/**
* This is where the exercise specifies the extra checks it may require.
*
* @param ExerciseDispatcher $dispatcher
* @return array<class-string>
*/
public function configure(ExerciseDispatcher $dispatcher): void;
public function getRequiredChecks(): array;

/**
* A short description of the exercise.
Expand Down
8 changes: 4 additions & 4 deletions src/ExerciseDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ public function requireCheck(string $requiredCheck): void
*/
public function verify(ExerciseInterface $exercise, Input $input): ResultAggregator
{
$exercise->configure($this);

$runner = $this->runnerManager->getRunner($exercise);

foreach ($runner->getRequiredChecks() as $requiredCheck) {
$exercise->defineListeners($this->eventDispatcher);

foreach ([...$runner->getRequiredChecks(), ...$exercise->getRequiredChecks()] as $requiredCheck) {
$this->requireCheck($requiredCheck);
}

Expand Down Expand Up @@ -181,7 +181,7 @@ public function verify(ExerciseInterface $exercise, Input $input): ResultAggrega
*/
public function run(ExerciseInterface $exercise, Input $input, OutputInterface $output): bool
{
$exercise->configure($this);
$exercise->defineListeners($this->eventDispatcher);

/** @var PhpLintCheck $lint */
$lint = $this->checkRepository->getByClass(PhpLintCheck::class);
Expand Down
91 changes: 31 additions & 60 deletions src/ExerciseRunner/CgiRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
use PhpSchool\PhpWorkshop\Exercise\ExerciseInterface;
use PhpSchool\PhpWorkshop\Input\Input;
use PhpSchool\PhpWorkshop\Output\OutputInterface;
use PhpSchool\PhpWorkshop\Process\ProcessFactory;
use PhpSchool\PhpWorkshop\Process\ProcessInput;
use PhpSchool\PhpWorkshop\Result\Cgi\CgiResult;
use PhpSchool\PhpWorkshop\Result\Cgi\RequestFailure;
use PhpSchool\PhpWorkshop\Result\Cgi\GenericFailure;
Expand All @@ -32,31 +34,18 @@
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;

use function PHPStan\dumpType;

/**
* The `CGI` runner. This runner executes solutions as if they were behind a web-server. They populate the `$_SERVER`,
* `$_GET` & `$_POST` super globals with information based of the request objects returned from the exercise.
*/
class CgiRunner implements ExerciseRunnerInterface
{
/**
* @var CgiExercise&ExerciseInterface
*/
private $exercise;

/**
* @var EventDispatcher
*/
private $eventDispatcher;

/**
* @var string
*/
private $phpLocation;

/**
* @var array<class-string>
*/
private static $requiredChecks = [
private static array $requiredChecks = [
FileExistsCheck::class,
CodeExistsCheck::class,
PhpLintCheck::class,
Expand All @@ -68,26 +57,13 @@ class CgiRunner implements ExerciseRunnerInterface
* be available. It will check for it's existence in the system's $PATH variable or the same
* folder that the CLI php binary lives in.
*
* @param CgiExercise $exercise The exercise to be invoked.
* @param EventDispatcher $eventDispatcher The event dispatcher.
* @param CgiExercise&ExerciseInterface $exercise The exercise to be invoked.
*/
public function __construct(
CgiExercise $exercise,
EventDispatcher $eventDispatcher
private CgiExercise $exercise,
private EventDispatcher $eventDispatcher,
private ProcessFactory $processFactory
) {
$php = (new ExecutableFinder())->find('php-cgi');

if (null === $php) {
throw new RuntimeException(
'Could not load php-cgi binary. Please install php using your package manager.'
);
}

$this->phpLocation = $php;

/** @var CgiExercise&ExerciseInterface $exercise */
$this->eventDispatcher = $eventDispatcher;
$this->exercise = $exercise;
}

/**
Expand Down Expand Up @@ -172,7 +148,7 @@ private function getHeaders(ResponseInterface $response): array
*/
private function executePhpFile(string $fileName, RequestInterface $request, string $type): ResponseInterface
{
$process = $this->getProcess($fileName, $request);
$process = $this->getPhpProcess(dirname($fileName), basename($fileName), $request);

$process->start();
$this->eventDispatcher->dispatch(new CgiExecuteEvent(sprintf('cgi.verify.%s.executing', $type), $request));
Expand All @@ -196,47 +172,38 @@ private function executePhpFile(string $fileName, RequestInterface $request, str
* @param RequestInterface $request
* @return Process
*/
private function getProcess(string $fileName, RequestInterface $request): Process
private function getPhpProcess(string $workingDirectory, string $fileName, RequestInterface $request): Process
{
$env = $this->getDefaultEnv();
$env += [
$env = [
'REQUEST_METHOD' => $request->getMethod(),
'SCRIPT_FILENAME' => $fileName,
'REDIRECT_STATUS' => 302,
'REDIRECT_STATUS' => '302',
'QUERY_STRING' => $request->getUri()->getQuery(),
'REQUEST_URI' => $request->getUri()->getPath(),
'XDEBUG_MODE' => 'off',
];

$cgiBinary = sprintf(
'%s -dalways_populate_raw_post_data=-1 -dhtml_errors=0 -dexpose_php=0',
$this->phpLocation
);

$content = $request->getBody()->__toString();
$cmd = sprintf('echo %s | %s', escapeshellarg($content), $cgiBinary);
$env['CONTENT_LENGTH'] = $request->getBody()->getSize();
$env['CONTENT_LENGTH'] = (string) $request->getBody()->getSize();
$env['CONTENT_TYPE'] = $request->getHeaderLine('Content-Type');

foreach ($request->getHeaders() as $name => $values) {
$env[sprintf('HTTP_%s', strtoupper($name))] = implode(", ", $values);
}

return Process::fromShellCommandline($cmd, null, $env, null, 10);
}

/**
* We need to reset env entirely, because Symfony inherits it. We do that by setting all
* the current env vars to false
*
* @return array<string, false>
*/
private function getDefaultEnv(): array
{
$env = array_map(fn () => false, $_ENV);
$env + array_map(fn () => false, $_SERVER);
$processInput = new ProcessInput(
'php-cgi',
[
'-dalways_populate_raw_post_data=-1',
'-dhtml_errors=0',
'-dexpose_php=0',
],
$workingDirectory,
$env,
$content
);

return $env;
return $this->processFactory->create($processInput);
}

/**
Expand Down Expand Up @@ -297,7 +264,11 @@ public function run(Input $input, OutputInterface $output): bool
$event = $this->eventDispatcher->dispatch(
new CgiExecuteEvent('cgi.run.student-execute.pre', $request)
);
$process = $this->getProcess($input->getRequiredArgument('program'), $event->getRequest());
$process = $this->getPhpProcess(
dirname($input->getRequiredArgument('program')),
$input->getRequiredArgument('program'),
$event->getRequest()
);

$process->start();
$this->eventDispatcher->dispatch(
Expand Down
Loading
Loading