Skip to content

Commit

Permalink
Use new process factory
Browse files Browse the repository at this point in the history
  • Loading branch information
AydinHassan committed May 7, 2024
1 parent 300070c commit 441b2bd
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 217 deletions.
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
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
131 changes: 44 additions & 87 deletions src/ExerciseRunner/CliRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
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\Cli\RequestFailure;
use PhpSchool\PhpWorkshop\Result\Cli\CliResult;
use PhpSchool\PhpWorkshop\Result\Cli\GenericFailure;
use PhpSchool\PhpWorkshop\Result\Cli\Success;
use PhpSchool\PhpWorkshop\Result\Cli\ResultInterface as CliResultInterface;
use PhpSchool\PhpWorkshop\Result\ResultInterface;
use PhpSchool\PhpWorkshop\Utils\ArrayObject;
use PhpSchool\PhpWorkshop\Utils\Collection;
use RuntimeException;
use Symfony\Component\Process\ExecutableFinder;
use Symfony\Component\Process\Process;
Expand All @@ -39,25 +42,10 @@
*/
class CliRunner implements ExerciseRunnerInterface
{
/**
* @var CliExercise&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 @@ -67,24 +55,13 @@ class CliRunner implements ExerciseRunnerInterface
/**
* Requires the exercise instance and an event dispatcher.
*
* @param CliExercise $exercise The exercise to be invoked.
* @param EventDispatcher $eventDispatcher The event dispatcher.
* @param CliExercise&ExerciseInterface $exercise The exercise to be invoked.
*/
public function __construct(CliExercise $exercise, EventDispatcher $eventDispatcher)
{
$php = (new ExecutableFinder())->find('php');

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

$this->phpLocation = $php;

/** @var CliExercise&ExerciseInterface $exercise */
$this->eventDispatcher = $eventDispatcher;
$this->exercise = $exercise;
public function __construct(
private CliExercise $exercise,
private EventDispatcher $eventDispatcher,
private ProcessFactory $processFactory
) {
}

/**
Expand All @@ -105,59 +82,6 @@ public function getRequiredChecks(): array
return self::$requiredChecks;
}

/**
* @param string $fileName
* @param ArrayObject<int, string> $args
* @param string $type
* @return string
*/
private function executePhpFile(string $fileName, ArrayObject $args, string $type): string
{
$process = $this->getPhpProcess($fileName, $args);

$process->start();
$this->eventDispatcher->dispatch(new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $args));
$process->wait();

if (!$process->isSuccessful()) {
throw CodeExecutionException::fromProcess($process);
}

return $process->getOutput();
}

/**
* @param string $fileName
* @param ArrayObject<int, string> $args
*
* @return Process
*/
private function getPhpProcess(string $fileName, ArrayObject $args): Process
{
return new Process(
$args->prepend($fileName)->prepend($this->phpLocation)->getArrayCopy(),
dirname($fileName),
$this->getDefaultEnv() + ['XDEBUG_MODE' => 'off'],
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);

return $env;
}


/**
* Verifies a solution by invoking PHP from the CLI passing the arguments gathered from the exercise
* as command line arguments to PHP.
Expand Down Expand Up @@ -272,7 +196,12 @@ public function run(Input $input, OutputInterface $output): bool

$args = $event->getArgs();

$process = $this->getPhpProcess($input->getRequiredArgument('program'), $args);
$process = $this->getPhpProcess(
dirname($input->getRequiredArgument('program')),
$input->getRequiredArgument('program'),
$args
);

$process->start();
$this->eventDispatcher->dispatch(
new CliExecuteEvent('cli.run.student.executing', $args, ['output' => $output])
Expand All @@ -296,4 +225,32 @@ public function run(Input $input, OutputInterface $output): bool
$this->eventDispatcher->dispatch(new ExerciseRunnerEvent('cli.run.finish', $this->exercise, $input));
return $success;
}

/**
* @param ArrayObject<int, string> $args
*/
private function executePhpFile(string $fileName, ArrayObject $args, string $type): string
{
$process = $this->getPhpProcess(dirname($fileName), $fileName, $args);

$process->start();
$this->eventDispatcher->dispatch(new CliExecuteEvent(sprintf('cli.verify.%s.executing', $type), $args));
$process->wait();

if (!$process->isSuccessful()) {
throw CodeExecutionException::fromProcess($process);
}

return $process->getOutput();
}

/**
* @param ArrayObject<int, string> $args
*/
private function getPhpProcess(string $workingDirectory, string $fileName, ArrayObject $args): Process
{
return $this->processFactory->create(
new ProcessInput('php', [$fileName, ...$args->getArrayCopy()], $workingDirectory, [])
);
}
}
Loading

0 comments on commit 441b2bd

Please sign in to comment.